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(fields: BTreeMap<String, MetadataField>, unknown_field_policy: UnknownFieldPolicy) -> Self {
58        Self {
59            fields,
60            unknown_field_policy,
61        }
62    }
63
64    /// Returns the field definition for `key`.
65    #[inline]
66    #[must_use]
67    pub fn field(&self, key: &str) -> Option<&MetadataField> {
68        self.fields.get(key)
69    }
70
71    /// Returns the declared data type for `key`.
72    #[inline]
73    #[must_use]
74    pub fn field_type(&self, key: &str) -> Option<DataType> {
75        self.field(key).map(MetadataField::data_type)
76    }
77
78    /// Returns the unknown-field policy.
79    #[inline]
80    #[must_use]
81    pub fn unknown_field_policy(&self) -> UnknownFieldPolicy {
82        self.unknown_field_policy
83    }
84
85    /// Returns an iterator over schema fields in key-sorted order.
86    #[inline]
87    pub fn fields(&self) -> impl Iterator<Item = (&str, &MetadataField)> {
88        self.fields.iter().map(|(key, field)| (key.as_str(), field))
89    }
90
91    /// Validates a metadata object against this schema.
92    ///
93    /// # Errors
94    ///
95    /// Returns an aggregate error containing every required-field, declared-type,
96    /// and unknown-field issue discovered during this validation pass.
97    pub fn validate(&self, meta: &Metadata) -> MetadataValidationResult<()> {
98        let mut issues = Vec::new();
99        for (key, field) in &self.fields {
100            if field.is_required() && !meta.contains_key(key) {
101                issues.push(MetadataError::MissingRequiredField {
102                    key: key.clone(),
103                    expected: field.data_type(),
104                });
105            }
106        }
107
108        for (key, value) in meta.iter() {
109            if let Err(error) = self.validate_entry(key, value) {
110                issues.push(error);
111            }
112        }
113        if let Some(error) = MetadataValidationError::from_issues(issues) {
114            Err(error)
115        } else {
116            Ok(())
117        }
118    }
119
120    /// Validates one metadata entry against this schema.
121    pub(crate) fn validate_entry(&self, key: &str, value: &Value) -> MetadataResult<()> {
122        match self.field(key) {
123            Some(field) if field.data_type() != value.data_type() => {
124                Err(MetadataError::type_mismatch(key, field.data_type(), value.data_type()))
125            }
126            Some(_) => Ok(()),
127            None if matches!(self.unknown_field_policy, UnknownFieldPolicy::Reject) => {
128                Err(MetadataError::UnknownField { key: key.to_string() })
129            }
130            None => Ok(()),
131        }
132    }
133}
134
135impl Default for MetadataSchema {
136    #[inline]
137    fn default() -> Self {
138        Self {
139            fields: BTreeMap::new(),
140            unknown_field_policy: UnknownFieldPolicy::Reject,
141        }
142    }
143}