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