Skip to main content

qubit_metadata/schema/
metadata_schema.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! [`MetadataSchema`] — schema validation for metadata and filters.
9
10// Implements filter validation and its private compatibility checks.
11mod filter_validation;
12
13#[cfg(feature = "json")]
14use std::cell::RefCell;
15use std::collections::BTreeMap;
16#[cfg(feature = "json")]
17use std::io::Write;
18#[cfg(feature = "json")]
19use std::rc::Rc;
20
21#[cfg(feature = "json")]
22use qubit_budget::json::JsonDecodeSession;
23#[cfg(feature = "json")]
24use qubit_budget::json::JsonEncodeLimits;
25#[cfg(feature = "json")]
26use qubit_budget::json::JsonEncodeSession;
27use qubit_datatype::DataType;
28#[cfg(feature = "json")]
29use qubit_json::decode::JsonDecoder;
30#[cfg(feature = "json")]
31use qubit_json::encode::JsonEncoder;
32use qubit_value::Value;
33use serde::Deserialize;
34use serde::Deserializer;
35use serde::Serialize;
36use serde::Serializer;
37use serde::de;
38use serde::ser::Error as SerError;
39
40use crate::Metadata;
41use crate::MetadataError;
42use crate::MetadataResult;
43use crate::MetadataValidationError;
44use crate::MetadataValidationResult;
45use crate::constants::STRICT_STRING_MAP_MAX_ENTRIES;
46use crate::constants::STRICT_STRING_MAP_MAX_KEY_BYTES;
47#[cfg(feature = "json")]
48use crate::metadata_limits::MetadataLimits;
49use crate::schema::MetadataField;
50use crate::schema::MetadataSchemaBuilder;
51use crate::schema::UnknownFilterFieldPolicy;
52use crate::schema::UnknownMetadataFieldPolicy;
53use crate::wire::METADATA_SCHEMA_WIRE_VERSION_V1;
54use crate::wire::MetadataSchemaWireV1;
55#[cfg(feature = "json")]
56use crate::wire::MetadataSchemaWireV1Seed;
57use crate::wire::StrictStringMap;
58#[cfg(feature = "json")]
59use crate::wire::StrictStringMapSeed;
60
61/// Schema for metadata fields.
62///
63/// A schema declares valid keys, their concrete [`DataType`], and whether they
64/// are required. It can validate actual [`Metadata`] values and validate that a
65/// [`crate::MetadataFilter`] references known fields with compatible operators.
66///
67/// # Examples
68///
69/// ```
70/// use qubit_datatype::DataType;
71/// use qubit_metadata::Metadata;
72/// use qubit_metadata::MetadataSchema;
73///
74/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
75/// let metadata = Metadata::new().with("tenant", "acme");
76/// let schema = MetadataSchema::builder()
77///     .required("tenant", DataType::String)
78///     .build()?;
79/// schema.validate(&metadata)?;
80/// # Ok(())
81/// # }
82/// ```
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct MetadataSchema {
85    /// Field definitions keyed by metadata key.
86    fields: BTreeMap<String, MetadataField>,
87    /// How validation handles unknown metadata keys.
88    unknown_metadata_field_policy: UnknownMetadataFieldPolicy,
89    /// How validation handles unknown filter keys.
90    unknown_filter_field_policy: UnknownFilterFieldPolicy,
91}
92
93impl MetadataSchema {
94    /// Creates a schema builder.
95    ///
96    /// # Returns
97    ///
98    /// An empty schema builder using the default unknown-field policy.
99    #[inline]
100    #[must_use]
101    pub fn builder() -> MetadataSchemaBuilder {
102        MetadataSchemaBuilder::default()
103    }
104
105    /// Decodes a strict metadata-schema JSON envelope using the metadata
106    /// profile.
107    ///
108    /// # Parameters
109    ///
110    /// * `input` - Complete untrusted JSON input.
111    ///
112    /// # Returns
113    ///
114    /// The decoded metadata schema.
115    ///
116    /// # Errors
117    ///
118    /// Returns a structured budget error or `InvalidJson` for malformed strict
119    /// schema input.
120    #[cfg(feature = "json")]
121    #[inline]
122    pub fn decode_json_slice(input: &[u8]) -> Result<Self, crate::MetadataWireDecodeError> {
123        Self::decode_json_slice_with_limits(input, MetadataLimits::default())
124    }
125
126    /// Decodes a strict metadata-schema JSON envelope after applying
127    /// `limits`.
128    ///
129    /// # Parameters
130    ///
131    /// * `input` - Complete untrusted JSON input.
132    /// * `limits` - Shared JSON limits for this decoding session.
133    ///
134    /// # Returns
135    ///
136    /// The decoded metadata schema.
137    ///
138    /// # Errors
139    ///
140    /// Returns a structured budget error, `Domain` for field/key limits,
141    /// `UnsupportedVersion` for version mismatches, or redacted JSON errors for
142    /// syntax and other strict schema-envelope failures.
143    #[cfg(feature = "json")]
144    pub fn decode_json_slice_with_limits(
145        input: &[u8],
146        limits: MetadataLimits,
147    ) -> Result<Self, crate::MetadataWireDecodeError> {
148        limits
149            .validate()
150            .map_err(crate::MetadataWireDecodeError::InvalidLimits)?;
151        let mut decoder = JsonDecoder::new(JsonDecodeSession::from_limits(limits.json_decode()));
152        let error_slot = Rc::new(RefCell::new(None));
153        let wire = decoder
154            .decode_seed_utf8(
155                MetadataSchemaWireV1Seed::new(
156                    StrictStringMapSeed::new(limits.max_schema_fields(), limits.max_key_bytes())
157                        .with_error_slot(Rc::clone(&error_slot)),
158                ),
159                input,
160            )
161            .map_err(|error| {
162                error_slot.borrow_mut().take().map_or_else(
163                    || Into::<crate::MetadataWireDecodeError>::into(error),
164                    crate::MetadataWireDecodeError::Domain,
165                )
166            })?;
167        if wire.version != METADATA_SCHEMA_WIRE_VERSION_V1 {
168            return Err(crate::MetadataWireDecodeError::UnsupportedVersion {
169                expected: METADATA_SCHEMA_WIRE_VERSION_V1,
170                actual: wire.version,
171            });
172        }
173        Ok(Self::new(
174            wire.fields.into_inner(),
175            wire.unknown_metadata_field_policy,
176            wire.unknown_filter_field_policy,
177        ))
178    }
179
180    /// Encodes this schema with the default JSON budget profile.
181    #[cfg(feature = "json")]
182    pub fn to_json_vec(&self) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
183        self.to_json_vec_with_limits(crate::metadata_limits::default_json_encode_limits())
184    }
185
186    /// Encodes this schema with caller-provided JSON budgets.
187    ///
188    /// # Parameters
189    ///
190    /// * `limits` - Output and JSON-value budgets for this operation.
191    ///
192    /// # Errors
193    ///
194    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
195    /// budget or serialization fails.
196    #[cfg(feature = "json")]
197    pub fn to_json_vec_with_limits(&self, limits: JsonEncodeLimits) -> Result<Vec<u8>, crate::MetadataWireEncodeError> {
198        let session = JsonEncodeSession::from_limits(limits);
199        JsonEncoder::new(session).to_vec(self).map_err(Into::into)
200    }
201
202    /// Encodes this schema to a writer with the default JSON budget profile.
203    #[cfg(feature = "json")]
204    pub fn to_json_writer<W>(&self, writer: W) -> Result<(), crate::MetadataWireEncodeError>
205    where
206        W: Write,
207    {
208        self.to_json_writer_with_limits(writer, crate::metadata_limits::default_json_encode_limits())
209    }
210
211    /// Encodes this schema to a writer with caller-provided JSON budgets.
212    ///
213    /// # Parameters
214    ///
215    /// * `writer` - Destination receiving the compact JSON document.
216    /// * `limits` - Output and JSON-value budgets for this operation.
217    ///
218    /// # Errors
219    ///
220    /// Returns [`crate::MetadataWireEncodeError`] when encoding exceeds a
221    /// budget, serialization fails, or `writer` rejects the output.
222    #[cfg(feature = "json")]
223    pub fn to_json_writer_with_limits<W>(
224        &self,
225        writer: W,
226        limits: JsonEncodeLimits,
227    ) -> Result<(), crate::MetadataWireEncodeError>
228    where
229        W: Write,
230    {
231        let session = JsonEncodeSession::from_limits(limits);
232        JsonEncoder::new(session)
233            .write_buffered(writer, self)
234            .map_err(Into::into)
235    }
236
237    /// Creates a schema from field definitions and unknown-field policies.
238    ///
239    /// # Parameters
240    ///
241    /// * `fields` - Field definitions keyed by metadata key.
242    /// * `unknown_metadata_field_policy` - Policy for undeclared metadata keys.
243    /// * `unknown_filter_field_policy` - Policy for undeclared filter keys.
244    ///
245    /// # Returns
246    ///
247    /// A new immutable schema.
248    #[inline]
249    pub(crate) fn new(
250        fields: BTreeMap<String, MetadataField>,
251        unknown_metadata_field_policy: UnknownMetadataFieldPolicy,
252        unknown_filter_field_policy: UnknownFilterFieldPolicy,
253    ) -> Self {
254        Self {
255            fields,
256            unknown_metadata_field_policy,
257            unknown_filter_field_policy,
258        }
259    }
260
261    /// Returns the field definition for `key`.
262    ///
263    /// # Parameters
264    ///
265    /// * `key` - Metadata key to look up.
266    ///
267    /// # Returns
268    ///
269    /// `Some` field definition for a declared key; otherwise, `None`.
270    #[inline]
271    #[must_use]
272    pub fn field(&self, key: &str) -> Option<&MetadataField> {
273        self.fields.get(key)
274    }
275
276    /// Returns the declared data type for `key`.
277    ///
278    /// # Parameters
279    ///
280    /// * `key` - Metadata key to look up.
281    ///
282    /// # Returns
283    ///
284    /// `Some` declared data type for a known key; otherwise, `None`.
285    #[inline]
286    #[must_use]
287    pub fn field_type(&self, key: &str) -> Option<DataType> {
288        self.field(key).map(MetadataField::data_type)
289    }
290
291    /// Returns the policy for undeclared metadata fields.
292    ///
293    /// # Returns
294    ///
295    /// The policy applied to undeclared metadata keys.
296    #[inline]
297    #[must_use]
298    pub fn unknown_metadata_field_policy(&self) -> UnknownMetadataFieldPolicy {
299        self.unknown_metadata_field_policy
300    }
301
302    /// Returns the policy for undeclared filter fields.
303    ///
304    /// # Returns
305    ///
306    /// The policy applied to filter keys not declared in this schema.
307    #[inline]
308    #[must_use]
309    pub fn unknown_filter_field_policy(&self) -> UnknownFilterFieldPolicy {
310        self.unknown_filter_field_policy
311    }
312
313    /// Returns an iterator over schema fields in key-sorted order.
314    ///
315    /// # Returns
316    ///
317    /// An iterator yielding key and field-definition pairs.
318    #[inline]
319    #[must_use = "the schema field iterator must be consumed to inspect fields"]
320    pub fn fields(&self) -> impl Iterator<Item = (&str, &MetadataField)> {
321        self.fields.iter().map(|(key, field)| (key.as_str(), field))
322    }
323
324    /// Validates a metadata object against this schema.
325    ///
326    /// # Parameters
327    ///
328    /// * `meta` - Metadata object to validate.
329    ///
330    /// # Returns
331    ///
332    /// `Ok(())` when every entry satisfies the schema.
333    ///
334    /// # Errors
335    ///
336    /// Returns an aggregate error containing every required-field,
337    /// declared-type, and unknown-field issue discovered during this
338    /// validation pass.
339    pub fn validate(&self, meta: &Metadata) -> MetadataValidationResult<()> {
340        let mut issues = Vec::new();
341        for (key, field) in &self.fields {
342            if field.is_required() && meta.get_raw(key).is_none_or(Value::is_unset) {
343                issues.push(MetadataError::MissingRequiredField {
344                    key: key.clone(),
345                    expected: field.data_type(),
346                });
347            }
348        }
349
350        for (key, value) in meta.iter() {
351            if self
352                .field(key)
353                .is_some_and(|field| field.is_required() && value.is_unset())
354            {
355                continue;
356            }
357            if let Err(error) = self.validate_entry(key, value) {
358                issues.push(error);
359            }
360        }
361        if let Some(error) = MetadataValidationError::from_issues(issues) {
362            Err(error)
363        } else {
364            Ok(())
365        }
366    }
367
368    /// Validates one metadata entry against this schema.
369    ///
370    /// # Parameters
371    ///
372    /// * `key` - Metadata key to validate.
373    /// * `value` - Stored value to validate.
374    ///
375    /// # Returns
376    ///
377    /// `Ok(())` when the entry is accepted.
378    ///
379    /// # Errors
380    ///
381    /// Returns [`MetadataError::UnknownField`] for a rejected undeclared key,
382    /// or [`MetadataError::TypeMismatch`] for a declared type mismatch.
383    pub(crate) fn validate_entry(&self, key: &str, value: &Value) -> MetadataResult<()> {
384        match self.field(key) {
385            Some(field) if field.is_required() && value.is_unset() => Err(MetadataError::MissingRequiredField {
386                key: key.to_string(),
387                expected: field.data_type(),
388            }),
389            Some(field) if field.data_type() != value.data_type() => {
390                Err(MetadataError::type_mismatch(key, field.data_type(), value.data_type()))
391            }
392            Some(_) => Ok(()),
393            None if matches!(self.unknown_metadata_field_policy, UnknownMetadataFieldPolicy::Reject) => {
394                Err(MetadataError::UnknownField { key: key.to_string() })
395            }
396            None => Ok(()),
397        }
398    }
399
400    /// Validates that this schema fits the strict V1 wire contract.
401    ///
402    /// This preflight checks the same field-count and key-byte limits enforced
403    /// by [`Serialize::serialize`], allowing callers to reject invalid schemas
404    /// before crossing a persistence or network boundary.
405    ///
406    /// # Returns
407    ///
408    /// `Ok(())` when every field can satisfy the schema map limits.
409    ///
410    /// # Errors
411    ///
412    /// Returns [`MetadataError::WireLimitExceeded`] when the field count or a
413    /// field key exceeds the strict V1 limit.
414    #[inline]
415    pub fn validate_wire_contract(&self) -> MetadataResult<()> {
416        if self.fields.len() > STRICT_STRING_MAP_MAX_ENTRIES {
417            return Err(MetadataError::WireLimitExceeded {
418                kind: crate::MetadataWireLimitKind::Entries,
419                value: self.fields.len(),
420                maximum: STRICT_STRING_MAP_MAX_ENTRIES,
421            });
422        }
423        if let Some(key) = self
424            .fields
425            .keys()
426            .find(|key| key.len() > STRICT_STRING_MAP_MAX_KEY_BYTES)
427        {
428            return Err(MetadataError::WireLimitExceeded {
429                kind: crate::MetadataWireLimitKind::KeyBytes,
430                value: key.len(),
431                maximum: STRICT_STRING_MAP_MAX_KEY_BYTES,
432            });
433        }
434        Ok(())
435    }
436}
437
438impl Serialize for MetadataSchema {
439    /// Serializes this schema as its strict v1 envelope.
440    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
441    where
442        S: Serializer,
443    {
444        self.validate_wire_contract().map_err(<S::Error as SerError>::custom)?;
445        MetadataSchemaWireV1 {
446            version: METADATA_SCHEMA_WIRE_VERSION_V1,
447            fields: &self.fields,
448            unknown_metadata_field_policy: self.unknown_metadata_field_policy,
449            unknown_filter_field_policy: self.unknown_filter_field_policy,
450        }
451        .serialize(serializer)
452    }
453}
454
455impl<'de> Deserialize<'de> for MetadataSchema {
456    /// Deserializes only the strict v1 schema envelope.
457    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458    where
459        D: Deserializer<'de>,
460    {
461        let wire: MetadataSchemaWireV1<StrictStringMap<MetadataField>> =
462            MetadataSchemaWireV1::deserialize(deserializer)?;
463        if wire.version != METADATA_SCHEMA_WIRE_VERSION_V1 {
464            return Err(de::Error::custom("unsupported MetadataSchema wire format version"));
465        }
466        Ok(Self::new(
467            wire.fields.into_inner(),
468            wire.unknown_metadata_field_policy,
469            wire.unknown_filter_field_policy,
470        ))
471    }
472}
473
474impl Default for MetadataSchema {
475    #[inline]
476    fn default() -> Self {
477        Self {
478            fields: BTreeMap::new(),
479            unknown_metadata_field_policy: UnknownMetadataFieldPolicy::Reject,
480            unknown_filter_field_policy: UnknownFilterFieldPolicy::Reject,
481        }
482    }
483}