Skip to main content

qubit_metadata/schema/
metadata_schema.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 *    SPDX-License-Identifier: Apache-2.0
6 *
7 *    Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! [`MetadataSchema`] — schema validation for metadata and filters.
11
12use std::collections::BTreeMap;
13
14use qubit_datatype::DataType;
15use qubit_value::Value;
16use serde::{Deserialize, Serialize};
17
18use crate::schema::{MetadataField, MetadataSchemaBuilder, UnknownFieldPolicy};
19use crate::{Metadata, MetadataError, MetadataResult};
20
21/// Schema for metadata fields.
22///
23/// A schema declares valid keys, their concrete [`DataType`], and whether they
24/// are required. It can validate actual [`Metadata`] values and validate that a
25/// [`crate::MetadataFilter`] references known fields with compatible operators.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct MetadataSchema {
28    /// Field definitions keyed by metadata key.
29    fields: BTreeMap<String, MetadataField>,
30    /// How validation handles unknown metadata keys.
31    unknown_field_policy: UnknownFieldPolicy,
32}
33
34impl MetadataSchema {
35    /// Creates a schema builder.
36    #[inline]
37    #[must_use]
38    pub fn builder() -> MetadataSchemaBuilder {
39        MetadataSchemaBuilder::default()
40    }
41
42    /// Creates a schema from field definitions and unknown-field policy.
43    #[inline]
44    pub(crate) fn new(
45        fields: BTreeMap<String, MetadataField>,
46        unknown_field_policy: UnknownFieldPolicy,
47    ) -> Self {
48        Self {
49            fields,
50            unknown_field_policy,
51        }
52    }
53
54    /// Returns the field definition for `key`.
55    #[inline]
56    #[must_use]
57    pub fn field(&self, key: &str) -> Option<&MetadataField> {
58        self.fields.get(key)
59    }
60
61    /// Returns the declared data type for `key`.
62    #[inline]
63    #[must_use]
64    pub fn field_type(&self, key: &str) -> Option<DataType> {
65        self.field(key).map(MetadataField::data_type)
66    }
67
68    /// Returns the unknown-field policy.
69    #[inline]
70    #[must_use]
71    pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
72        self.unknown_field_policy
73    }
74
75    /// Returns an iterator over schema fields in key-sorted order.
76    #[inline]
77    pub fn fields(&self) -> impl Iterator<Item = (&str, &MetadataField)> {
78        self.fields.iter().map(|(key, field)| (key.as_str(), field))
79    }
80
81    /// Validates a metadata object against this schema.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error when a required field is missing, a declared field has a
86    /// different concrete type, or an unknown field is present while the schema
87    /// rejects unknown fields.
88    pub fn validate(&self, meta: &Metadata) -> MetadataResult<()> {
89        for (key, field) in &self.fields {
90            if field.is_required() && !meta.contains_key(key) {
91                return Err(MetadataError::MissingRequiredField {
92                    key: key.clone(),
93                    expected: field.data_type(),
94                });
95            }
96        }
97
98        for (key, value) in meta.iter() {
99            self.validate_entry(key, value)?;
100        }
101        Ok(())
102    }
103
104    /// Validates one metadata entry against this schema.
105    pub(crate) fn validate_entry(&self, key: &str, value: &Value) -> MetadataResult<()> {
106        match self.field(key) {
107            Some(field) if field.data_type() != value.data_type() => Err(
108                MetadataError::type_mismatch(key, field.data_type(), value.data_type()),
109            ),
110            Some(_) => Ok(()),
111            None if matches!(self.unknown_field_policy, UnknownFieldPolicy::Reject) => {
112                Err(MetadataError::UnknownField {
113                    key: key.to_string(),
114                })
115            }
116            None => Ok(()),
117        }
118    }
119}
120
121impl Default for MetadataSchema {
122    #[inline]
123    fn default() -> Self {
124        Self {
125            fields: BTreeMap::new(),
126            unknown_field_policy: UnknownFieldPolicy::Reject,
127        }
128    }
129}