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