Skip to main content

qubit_metadata/schema/
metadata_schema_builder.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//! [`MetadataSchemaBuilder`] — fluent schema construction API.
9
10use std::collections::BTreeMap;
11use std::fmt;
12
13use qubit_datatype::DataType;
14use qubit_redact::Redact;
15use qubit_redact::RedactionWriter;
16use qubit_redact::Redactor;
17
18use crate::MetadataError;
19use crate::MetadataResult;
20use crate::schema::MetadataField;
21use crate::schema::MetadataSchema;
22use crate::schema::UnknownFilterFieldPolicy;
23use crate::schema::UnknownMetadataFieldPolicy;
24use crate::schema::internal::SchemaFields;
25
26/// Builder for [`MetadataSchema`].
27///
28/// Its [`fmt::Debug`] output uses strict redaction with one cumulative
29/// diagnostic budget for the field map and builder state. If that budget is
30/// exceeded, Debug displays `<redaction incomplete>`. A stored build error is
31/// represented only by whether an error exists; [`Self::build`] returns its
32/// original details. Debug output is bounded, but field names are not treated
33/// as a confidentiality boundary.
34///
35/// # Examples
36///
37/// ```
38/// use qubit_datatype::DataType;
39/// use qubit_metadata::MetadataSchema;
40///
41/// # fn main() -> qubit_metadata::MetadataResult<()> {
42/// let schema = MetadataSchema::builder()
43///     .required("tenant", DataType::String)
44///     .build()?;
45/// assert!(schema.field("tenant").is_some());
46/// # Ok(())
47/// # }
48/// ```
49#[derive(Clone, PartialEq, Eq, Default)]
50pub struct MetadataSchemaBuilder {
51    /// Field definitions being built.
52    fields: BTreeMap<String, MetadataField>,
53    /// Policy for metadata keys not declared in the schema.
54    unknown_metadata_field_policy: UnknownMetadataFieldPolicy,
55    /// Policy for filter keys not declared in the schema.
56    unknown_filter_field_policy: UnknownFilterFieldPolicy,
57    /// First duplicate declaration detected while building the schema.
58    error: Option<MetadataError>,
59}
60
61impl Redact for MetadataSchemaBuilder {
62    /// Writes builder fields through one cumulative diagnostic budget.
63    ///
64    /// # Parameters
65    ///
66    /// * `writer` - Redaction writer receiving the builder representation.
67    fn write_redacted(&self, writer: &mut RedactionWriter<'_>) {
68        writer.record("MetadataSchemaBuilder", |fields| {
69            fields.nested("fields", &SchemaFields(&self.fields));
70            fields.unmarked("unknown_metadata_field_policy", || self.unknown_metadata_field_policy);
71            fields.unmarked("unknown_filter_field_policy", || self.unknown_filter_field_policy);
72            fields.unmarked("has_error", || self.error.is_some());
73        });
74    }
75}
76
77impl fmt::Debug for MetadataSchemaBuilder {
78    /// Writes the strict-policy diagnostic representation.
79    ///
80    /// # Parameters
81    ///
82    /// * `formatter` - Formatter receiving the redacted representation.
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        let output = Redactor::strict().redact_text(self);
85        let text = output.text_or_marker("<redaction incomplete>");
86        formatter.write_str(text.as_ref())
87    }
88}
89
90impl MetadataSchemaBuilder {
91    /// Adds a required field definition.
92    ///
93    /// # Parameters
94    ///
95    /// * `key` - Metadata key to declare.
96    /// * `data_type` - Concrete data type accepted by the field.
97    ///
98    /// # Returns
99    ///
100    /// The updated builder.
101    #[inline]
102    #[must_use]
103    pub fn required(self, key: &str, data_type: DataType) -> Self {
104        self.declare_field(key, MetadataField::new(data_type, true))
105    }
106
107    /// Adds an optional field definition.
108    ///
109    /// # Parameters
110    ///
111    /// * `key` - Metadata key to declare.
112    /// * `data_type` - Concrete data type accepted by the field.
113    ///
114    /// # Returns
115    ///
116    /// The updated builder.
117    #[inline]
118    #[must_use]
119    pub fn optional(self, key: &str, data_type: DataType) -> Self {
120        self.declare_field(key, MetadataField::new(data_type, false))
121    }
122
123    /// Explicitly replaces a field definition.
124    ///
125    /// # Parameters
126    ///
127    /// * `key` - Metadata key to replace.
128    /// * `field` - Replacement definition.
129    ///
130    /// # Returns
131    ///
132    /// The updated builder. This is the only declaration method that may
133    /// overwrite an existing field.
134    #[inline]
135    #[must_use]
136    pub fn replace_field(mut self, key: &str, field: MetadataField) -> Self {
137        let _ = self.fields.insert(key.to_string(), field);
138        self
139    }
140
141    /// Sets the policy for metadata keys not declared by the schema.
142    ///
143    /// # Parameters
144    ///
145    /// * `policy` - Unknown metadata-field policy to store.
146    ///
147    /// # Returns
148    ///
149    /// The updated builder.
150    #[inline]
151    #[must_use]
152    pub fn unknown_metadata_field_policy(mut self, policy: UnknownMetadataFieldPolicy) -> Self {
153        self.unknown_metadata_field_policy = policy;
154        self
155    }
156
157    /// Sets the policy for filter keys not declared by the schema.
158    ///
159    /// # Parameters
160    ///
161    /// * `policy` - Unknown filter-field policy to store.
162    ///
163    /// # Returns
164    ///
165    /// The updated builder.
166    #[inline]
167    #[must_use]
168    pub fn unknown_filter_field_policy(mut self, policy: UnknownFilterFieldPolicy) -> Self {
169        self.unknown_filter_field_policy = policy;
170        self
171    }
172
173    /// Builds the schema.
174    ///
175    /// # Returns
176    ///
177    /// The immutable schema described by this builder.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`MetadataError::DuplicateSchemaField`] when `required` or
182    /// `optional` declare the same key more than once.
183    #[inline]
184    pub fn build(self) -> MetadataResult<MetadataSchema> {
185        if let Some(error) = self.error {
186            return Err(error);
187        }
188        Ok(MetadataSchema::new(
189            self.fields,
190            self.unknown_metadata_field_policy,
191            self.unknown_filter_field_policy,
192        ))
193    }
194
195    /// Declares a field unless the key has already been declared.
196    ///
197    /// # Parameters
198    ///
199    /// * `key` - Name assigned to the field.
200    /// * `field` - Definition stored for the key.
201    ///
202    /// # Returns
203    ///
204    /// The updated builder, retaining the first duplicate-key error if present.
205    fn declare_field(mut self, key: &str, field: MetadataField) -> Self {
206        if self.fields.contains_key(key) {
207            if self.error.is_none() {
208                self.error = Some(MetadataError::DuplicateSchemaField { key: key.to_string() });
209            }
210            return self;
211        }
212        let _ = self.fields.insert(key.to_string(), field);
213        self
214    }
215}