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}