Skip to main content

qubit_metadata/
metadata_schema.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! [`MetadataSchema`] — schema validation for metadata and filters.
10
11use std::collections::BTreeMap;
12
13use qubit_common::DataType;
14use qubit_value::Value;
15use serde::{Deserialize, Serialize};
16
17use crate::{
18    Condition, Metadata, MetadataError, MetadataField, MetadataFilter, MetadataResult,
19    MetadataSchemaBuilder, UnknownFieldPolicy,
20};
21
22/// Schema for metadata fields.
23///
24/// A schema declares valid keys, their concrete [`DataType`], and whether they
25/// are required. It can validate actual [`Metadata`] values and validate that a
26/// [`MetadataFilter`] references known fields with compatible operators.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct MetadataSchema {
29    /// Field definitions keyed by metadata key.
30    fields: BTreeMap<String, MetadataField>,
31    /// How validation handles unknown metadata keys.
32    unknown_field_policy: UnknownFieldPolicy,
33}
34
35impl MetadataSchema {
36    /// Creates a schema builder.
37    #[inline]
38    #[must_use]
39    pub fn builder() -> MetadataSchemaBuilder {
40        MetadataSchemaBuilder::default()
41    }
42
43    /// Creates a schema from field definitions and unknown-field policy.
44    #[inline]
45    pub(crate) fn new(
46        fields: BTreeMap<String, MetadataField>,
47        unknown_field_policy: UnknownFieldPolicy,
48    ) -> Self {
49        Self {
50            fields,
51            unknown_field_policy,
52        }
53    }
54
55    /// Returns the field definition for `key`.
56    #[inline]
57    #[must_use]
58    pub fn field(&self, key: &str) -> Option<&MetadataField> {
59        self.fields.get(key)
60    }
61
62    /// Returns the declared data type for `key`.
63    #[inline]
64    #[must_use]
65    pub fn field_type(&self, key: &str) -> Option<DataType> {
66        self.field(key).map(MetadataField::data_type)
67    }
68
69    /// Returns the unknown-field policy.
70    #[inline]
71    #[must_use]
72    pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
73        self.unknown_field_policy
74    }
75
76    /// Returns an iterator over schema fields in key-sorted order.
77    #[inline]
78    pub fn fields(&self) -> impl Iterator<Item = (&str, &MetadataField)> {
79        self.fields.iter().map(|(key, field)| (key.as_str(), field))
80    }
81
82    /// Validates a metadata object against this schema.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error when a required field is missing, a declared field has a
87    /// different concrete type, or an unknown field is present while the schema
88    /// rejects unknown fields.
89    pub fn validate(&self, meta: &Metadata) -> MetadataResult<()> {
90        for (key, field) in &self.fields {
91            if field.is_required() && !meta.contains_key(key) {
92                return Err(MetadataError::MissingRequiredField {
93                    key: key.clone(),
94                    expected: field.data_type(),
95                });
96            }
97        }
98
99        for (key, value) in meta.iter() {
100            match self.field(key) {
101                Some(field) if field.data_type() != value.data_type() => {
102                    return Err(MetadataError::type_mismatch(
103                        key,
104                        field.data_type(),
105                        value.data_type(),
106                    ));
107                }
108                Some(_) => {}
109                None if matches!(self.unknown_field_policy, UnknownFieldPolicy::Reject) => {
110                    return Err(MetadataError::UnknownField {
111                        key: key.to_string(),
112                    });
113                }
114                None => {}
115            }
116        }
117        Ok(())
118    }
119
120    /// Validates a metadata filter against this schema.
121    ///
122    /// # Errors
123    ///
124    /// Returns an error when the filter references an unknown field, uses a range
125    /// operator on a non-comparable field, or compares a field with an incompatible
126    /// value type.
127    pub fn validate_filter(&self, filter: &MetadataFilter) -> MetadataResult<()> {
128        filter.visit_conditions(|condition| self.validate_condition(condition))
129    }
130
131    /// Validates one filter condition against this schema.
132    fn validate_condition(&self, condition: &Condition) -> MetadataResult<()> {
133        match condition {
134            Condition::Equal { key, value } | Condition::NotEqual { key, value } => {
135                self.validate_value_condition(key, "eq", value)
136            }
137            Condition::Less { key, value } => self.validate_range_condition(key, "lt", value),
138            Condition::LessEqual { key, value } => self.validate_range_condition(key, "le", value),
139            Condition::Greater { key, value } => self.validate_range_condition(key, "gt", value),
140            Condition::GreaterEqual { key, value } => {
141                self.validate_range_condition(key, "ge", value)
142            }
143            Condition::In { key, values } | Condition::NotIn { key, values } => {
144                for value in values {
145                    self.validate_value_condition(key, "in_set", value)?;
146                }
147                Ok(())
148            }
149            Condition::Exists { key } | Condition::NotExists { key } => {
150                self.require_field(key)?;
151                Ok(())
152            }
153        }
154    }
155
156    /// Validates a non-range value condition.
157    fn validate_value_condition(
158        &self,
159        key: &str,
160        operator: &'static str,
161        value: &Value,
162    ) -> MetadataResult<()> {
163        let field = self.require_field(key)?;
164        if value_matches_field_type(value, field.data_type()) {
165            return Ok(());
166        }
167        Err(MetadataError::InvalidFilterOperator {
168            key: key.to_string(),
169            operator,
170            data_type: field.data_type(),
171            message: format!(
172                "filter value type {} is not compatible with field type {}",
173                value.data_type(),
174                field.data_type()
175            ),
176        })
177    }
178
179    /// Validates a range value condition.
180    fn validate_range_condition(
181        &self,
182        key: &str,
183        operator: &'static str,
184        value: &Value,
185    ) -> MetadataResult<()> {
186        let field = self.require_field(key)?;
187        if !is_range_comparable_type(field.data_type()) {
188            return Err(MetadataError::InvalidFilterOperator {
189                key: key.to_string(),
190                operator,
191                data_type: field.data_type(),
192                message: "range operators require a numeric or string field".to_string(),
193            });
194        }
195        if value_matches_field_type(value, field.data_type()) {
196            return Ok(());
197        }
198        Err(MetadataError::InvalidFilterOperator {
199            key: key.to_string(),
200            operator,
201            data_type: field.data_type(),
202            message: format!(
203                "filter value type {} is not compatible with field type {}",
204                value.data_type(),
205                field.data_type()
206            ),
207        })
208    }
209
210    /// Returns the field for `key` or a schema error if it is unknown.
211    fn require_field(&self, key: &str) -> MetadataResult<&MetadataField> {
212        self.field(key)
213            .ok_or_else(|| MetadataError::UnknownFilterField {
214                key: key.to_string(),
215            })
216    }
217}
218
219impl Default for MetadataSchema {
220    #[inline]
221    fn default() -> Self {
222        Self {
223            fields: BTreeMap::new(),
224            unknown_field_policy: UnknownFieldPolicy::Reject,
225        }
226    }
227}
228
229/// Returns `true` when `data_type` is numeric.
230#[inline]
231fn is_numeric_data_type(data_type: DataType) -> bool {
232    matches!(
233        data_type,
234        DataType::Int8
235            | DataType::Int16
236            | DataType::Int32
237            | DataType::Int64
238            | DataType::Int128
239            | DataType::UInt8
240            | DataType::UInt16
241            | DataType::UInt32
242            | DataType::UInt64
243            | DataType::UInt128
244            | DataType::Float32
245            | DataType::Float64
246            | DataType::BigInteger
247            | DataType::BigDecimal
248            | DataType::IntSize
249            | DataType::UIntSize
250    )
251}
252
253/// Returns `true` when `data_type` supports range comparisons.
254#[inline]
255fn is_range_comparable_type(data_type: DataType) -> bool {
256    is_numeric_data_type(data_type) || matches!(data_type, DataType::String)
257}
258
259/// Returns `true` when a filter value is compatible with a schema field type.
260#[inline]
261fn value_matches_field_type(value: &Value, field_type: DataType) -> bool {
262    let value_type = value.data_type();
263    value_type == field_type || is_numeric_data_type(value_type) && is_numeric_data_type(field_type)
264}