Skip to main content

eure_schema/
build.rs

1//! Schema building from Rust types
2//!
3//! This module provides the [`BuildSchema`] trait and [`SchemaBuilder`] for
4//! generating schema definitions from Rust types, either manually or via derive.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use eure_schema::{BuildSchema, SchemaDocument};
10//!
11//! #[derive(BuildSchema)]
12//! #[eure(type_name = "user")]
13//! struct User {
14//!     name: String,
15//!     age: Option<u32>,
16//! }
17//!
18//! let schema = SchemaDocument::of::<User>();
19//! ```
20
21use std::any::TypeId;
22use std::collections::HashMap;
23
24use eure_document::Text;
25use indexmap::IndexMap;
26
27use crate::{
28    SchemaDocument, SchemaMetadata, SchemaNode, SchemaNodeContent, SchemaNodeId, TextSchema,
29};
30
31/// Trait for types that can build their schema representation.
32///
33/// This trait is typically derived using `#[derive(BuildSchema)]`, but can also
34/// be implemented manually for custom schema generation.
35///
36/// # Type Registration
37///
38/// Types can optionally provide a `type_name()` to register themselves in the
39/// schema's `$types` namespace. This is useful for:
40/// - Creating reusable type definitions
41/// - Enabling type references across the schema
42/// - Providing meaningful names in generated schemas
43///
44/// Primitive types typically return `None` for `type_name()`.
45pub trait BuildSchema {
46    /// The type name for registration in `$types` namespace.
47    ///
48    /// Return `Some("my-type")` to register this type as `$types.my-type`.
49    /// Return `None` (default) for inline/anonymous types.
50    fn type_name() -> Option<&'static str> {
51        None
52    }
53
54    /// Build the schema content for this type.
55    ///
56    /// Use `ctx.build::<T>()` for nested types - this handles caching
57    /// and recursion automatically.
58    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent;
59
60    /// Optional metadata for this type's schema node.
61    ///
62    /// Override to provide description, deprecation status, defaults, or examples.
63    fn schema_metadata() -> SchemaMetadata {
64        SchemaMetadata::default()
65    }
66}
67
68/// Builder for constructing schema documents from Rust types.
69///
70/// The builder maintains:
71/// - An arena of schema nodes
72/// - A cache by `TypeId` to prevent duplicate definitions and handle recursion
73/// - Type registrations for the `$types` namespace
74pub struct SchemaBuilder {
75    /// The schema document being built
76    doc: SchemaDocument,
77    /// Cache of built types by TypeId (prevents duplicates, handles recursion)
78    cache: HashMap<TypeId, SchemaNodeId>,
79}
80
81impl SchemaBuilder {
82    /// Create a new schema builder.
83    pub fn new() -> Self {
84        Self {
85            doc: SchemaDocument {
86                nodes: Vec::new(),
87                root: SchemaNodeId(0), // Will be set in finish()
88                types: Default::default(),
89            },
90            cache: HashMap::new(),
91        }
92    }
93
94    /// Build the schema for type `T`, with caching and recursion handling.
95    ///
96    /// This is the primary method for building nested types. It:
97    /// 1. Returns cached ID if already built (idempotent)
98    /// 2. Reserves a node slot before building (handles recursion)
99    /// 3. Calls `T::build_schema()` to get the content
100    /// 4. For named types: registers in $types and returns a Reference node
101    pub fn build<T: BuildSchema + 'static>(&mut self) -> SchemaNodeId {
102        let type_id = TypeId::of::<T>();
103
104        // Return cached if already built
105        if let Some(&id) = self.cache.get(&type_id) {
106            return id;
107        }
108
109        // Check if this type has a name (for registration)
110        let type_name = T::type_name();
111
112        // For named types, we need two nodes: content + reference
113        // For unnamed types, just the content node
114        if let Some(name) = type_name {
115            // Reserve a slot for the content node
116            let content_id = self.reserve_node();
117
118            // Build the schema content
119            let content = T::build_schema(self);
120            let metadata = T::schema_metadata();
121            self.set_node(content_id, content, metadata);
122
123            // Register the type
124            if let Ok(ident) = name.parse::<eure_document::identifier::Identifier>() {
125                self.doc.types.insert(ident, content_id);
126            }
127
128            // Create a Reference node that points to this type
129            let ref_id = self.create_node(SchemaNodeContent::Reference(crate::TypeReference {
130                namespace: None,
131                name: name.parse().expect("valid type name"),
132            }));
133
134            // Cache the reference ID so subsequent calls return the reference
135            self.cache.insert(type_id, ref_id);
136            ref_id
137        } else {
138            // Unnamed type: just build and cache the content node
139            let id = self.reserve_node();
140            self.cache.insert(type_id, id);
141
142            let content = T::build_schema(self);
143            let metadata = T::schema_metadata();
144            self.set_node(id, content, metadata);
145
146            id
147        }
148    }
149
150    /// Create a schema node with the given content.
151    ///
152    /// Use this for creating anonymous/inline nodes that don't need caching.
153    /// For types that implement `BuildSchema`, prefer `build::<T>()`.
154    pub fn create_node(&mut self, content: SchemaNodeContent) -> SchemaNodeId {
155        let id = SchemaNodeId(self.doc.nodes.len());
156        self.doc.nodes.push(SchemaNode {
157            content,
158            metadata: SchemaMetadata::default(),
159            ext_types: Default::default(),
160        });
161        id
162    }
163
164    /// Create a schema node with content and metadata.
165    pub fn create_node_with_metadata(
166        &mut self,
167        content: SchemaNodeContent,
168        metadata: SchemaMetadata,
169    ) -> SchemaNodeId {
170        let id = SchemaNodeId(self.doc.nodes.len());
171        self.doc.nodes.push(SchemaNode {
172            content,
173            metadata,
174            ext_types: Default::default(),
175        });
176        id
177    }
178
179    /// Reserve a node slot, returning its ID.
180    ///
181    /// The node is initialized with `Any` content and must be finalized
182    /// with `set_node()` before the schema is complete.
183    fn reserve_node(&mut self) -> SchemaNodeId {
184        let id = SchemaNodeId(self.doc.nodes.len());
185        self.doc.nodes.push(SchemaNode {
186            content: SchemaNodeContent::Any, // Placeholder
187            metadata: SchemaMetadata::default(),
188            ext_types: Default::default(),
189        });
190        id
191    }
192
193    /// Set the content and metadata of a reserved node.
194    fn set_node(&mut self, id: SchemaNodeId, content: SchemaNodeContent, metadata: SchemaMetadata) {
195        let node = &mut self.doc.nodes[id.0];
196        node.content = content;
197        node.metadata = metadata;
198    }
199
200    /// Get mutable access to a node for adding ext_types or modifying metadata.
201    pub fn node_mut(&mut self, id: SchemaNodeId) -> &mut SchemaNode {
202        &mut self.doc.nodes[id.0]
203    }
204
205    /// Register a named type in the `$types` namespace.
206    pub fn register_type(&mut self, name: &str, id: SchemaNodeId) {
207        if let Ok(ident) = name.parse() {
208            self.doc.types.insert(ident, id);
209        }
210    }
211
212    /// Consume the builder and produce the final schema document.
213    pub fn finish(mut self, root: SchemaNodeId) -> SchemaDocument {
214        self.doc.root = root;
215        self.doc
216    }
217}
218
219impl Default for SchemaBuilder {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225impl SchemaDocument {
226    /// Generate a schema document for type `T`.
227    ///
228    /// This is the main entry point for schema generation from Rust types.
229    ///
230    /// # Example
231    ///
232    /// ```ignore
233    /// use eure_schema::SchemaDocument;
234    ///
235    /// let schema = SchemaDocument::of::<MyType>();
236    /// ```
237    pub fn of<T: BuildSchema + 'static>() -> SchemaDocument {
238        let mut builder = SchemaBuilder::new();
239        let root = builder.build::<T>();
240        builder.finish(root)
241    }
242}
243
244// ============================================================================
245// Primitive Type Implementations
246// ============================================================================
247
248impl BuildSchema for String {
249    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
250        SchemaNodeContent::Text(crate::TextSchema::default())
251    }
252}
253
254impl BuildSchema for &str {
255    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
256        SchemaNodeContent::Text(crate::TextSchema::default())
257    }
258}
259
260impl BuildSchema for bool {
261    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
262        SchemaNodeContent::Boolean
263    }
264}
265
266macro_rules! impl_build_schema_int {
267    ($($ty:ty),*) => {
268        $(
269            impl BuildSchema for $ty {
270                fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
271                    SchemaNodeContent::Integer(crate::IntegerSchema::default())
272                }
273            }
274        )*
275    };
276}
277
278impl_build_schema_int!(
279    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
280);
281
282// Floats
283impl BuildSchema for f32 {
284    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
285        SchemaNodeContent::Float(crate::FloatSchema {
286            precision: crate::FloatPrecision::F32,
287            ..Default::default()
288        })
289    }
290}
291
292impl BuildSchema for f64 {
293    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
294        SchemaNodeContent::Float(crate::FloatSchema {
295            precision: crate::FloatPrecision::F64,
296            ..Default::default()
297        })
298    }
299}
300
301// Unit type
302impl BuildSchema for () {
303    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
304        SchemaNodeContent::Null
305    }
306}
307
308impl BuildSchema for Text {
309    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
310        SchemaNodeContent::Text(TextSchema {
311            language: None,
312            min_length: None,
313            max_length: None,
314            pattern: None,
315            unknown_fields: IndexMap::new(),
316        })
317    }
318}
319
320// ============================================================================
321// Compound Type Implementations
322// ============================================================================
323
324/// Option<T> is represented as a union: some(T) | none(null)
325impl<T: BuildSchema + 'static> BuildSchema for Option<T> {
326    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
327        let some_schema = ctx.build::<T>();
328        let none_schema = ctx.create_node(SchemaNodeContent::Null);
329
330        SchemaNodeContent::Union(crate::UnionSchema {
331            variants: IndexMap::from([
332                ("some".to_string(), some_schema),
333                ("none".to_string(), none_schema),
334            ]),
335            unambiguous: Default::default(),
336            repr: eure_document::data_model::VariantRepr::default(),
337            deny_untagged: Default::default(),
338        })
339    }
340}
341
342/// Result<T, E> is represented as a union: ok(T) | err(E)
343impl<T: BuildSchema + 'static, E: BuildSchema + 'static> BuildSchema for Result<T, E> {
344    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
345        let ok_schema = ctx.build::<T>();
346        let err_schema = ctx.build::<E>();
347
348        SchemaNodeContent::Union(crate::UnionSchema {
349            variants: IndexMap::from([
350                ("ok".to_string(), ok_schema),
351                ("err".to_string(), err_schema),
352            ]),
353            unambiguous: Default::default(),
354            repr: eure_document::data_model::VariantRepr::default(),
355            deny_untagged: Default::default(),
356        })
357    }
358}
359
360/// Vec<T> is represented as an array with item type T
361impl<T: BuildSchema + 'static> BuildSchema for Vec<T> {
362    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
363        let item = ctx.build::<T>();
364        SchemaNodeContent::Array(crate::ArraySchema {
365            item,
366            min_length: None,
367            max_length: None,
368            unique: false,
369            contains: None,
370            binding_style: None,
371        })
372    }
373}
374
375/// HashMap<K, V> is represented as a map
376impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
377    for std::collections::HashMap<K, V>
378{
379    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
380        let key = ctx.build::<K>();
381        let value = ctx.build::<V>();
382        SchemaNodeContent::Map(crate::MapSchema {
383            key,
384            value,
385            min_size: None,
386            max_size: None,
387        })
388    }
389}
390
391/// BTreeMap<K, V> is represented as a map
392impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
393    for std::collections::BTreeMap<K, V>
394{
395    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
396        let key = ctx.build::<K>();
397        let value = ctx.build::<V>();
398        SchemaNodeContent::Map(crate::MapSchema {
399            key,
400            value,
401            min_size: None,
402            max_size: None,
403        })
404    }
405}
406
407/// Box<T> delegates to T
408impl<T: BuildSchema + 'static> BuildSchema for Box<T> {
409    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
410        T::build_schema(ctx)
411    }
412}
413
414/// Rc<T> delegates to T
415impl<T: BuildSchema + 'static> BuildSchema for std::rc::Rc<T> {
416    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
417        T::build_schema(ctx)
418    }
419}
420
421/// Arc<T> delegates to T
422impl<T: BuildSchema + 'static> BuildSchema for std::sync::Arc<T> {
423    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
424        T::build_schema(ctx)
425    }
426}
427
428// Tuples
429impl<A: BuildSchema + 'static> BuildSchema for (A,) {
430    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
431        let elements = vec![ctx.build::<A>()];
432        SchemaNodeContent::Tuple(crate::TupleSchema {
433            elements,
434            binding_style: None,
435        })
436    }
437}
438
439impl<A: BuildSchema + 'static, B: BuildSchema + 'static> BuildSchema for (A, B) {
440    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
441        let elements = vec![ctx.build::<A>(), ctx.build::<B>()];
442        SchemaNodeContent::Tuple(crate::TupleSchema {
443            elements,
444            binding_style: None,
445        })
446    }
447}
448
449impl<A: BuildSchema + 'static, B: BuildSchema + 'static, C: BuildSchema + 'static> BuildSchema
450    for (A, B, C)
451{
452    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
453        let elements = vec![ctx.build::<A>(), ctx.build::<B>(), ctx.build::<C>()];
454        SchemaNodeContent::Tuple(crate::TupleSchema {
455            elements,
456            binding_style: None,
457        })
458    }
459}
460
461impl<
462    A: BuildSchema + 'static,
463    B: BuildSchema + 'static,
464    C: BuildSchema + 'static,
465    D: BuildSchema + 'static,
466> BuildSchema for (A, B, C, D)
467{
468    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
469        let elements = vec![
470            ctx.build::<A>(),
471            ctx.build::<B>(),
472            ctx.build::<C>(),
473            ctx.build::<D>(),
474        ];
475        SchemaNodeContent::Tuple(crate::TupleSchema {
476            elements,
477            binding_style: None,
478        })
479    }
480}
481
482impl<
483    A: BuildSchema + 'static,
484    B: BuildSchema + 'static,
485    C: BuildSchema + 'static,
486    D: BuildSchema + 'static,
487    E: BuildSchema + 'static,
488> BuildSchema for (A, B, C, D, E)
489{
490    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
491        let elements = vec![
492            ctx.build::<A>(),
493            ctx.build::<B>(),
494            ctx.build::<C>(),
495            ctx.build::<D>(),
496            ctx.build::<E>(),
497        ];
498        SchemaNodeContent::Tuple(crate::TupleSchema {
499            elements,
500            binding_style: None,
501        })
502    }
503}
504
505impl<
506    A: BuildSchema + 'static,
507    B: BuildSchema + 'static,
508    C: BuildSchema + 'static,
509    D: BuildSchema + 'static,
510    E: BuildSchema + 'static,
511    F: BuildSchema + 'static,
512> BuildSchema for (A, B, C, D, E, F)
513{
514    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
515        let elements = vec![
516            ctx.build::<A>(),
517            ctx.build::<B>(),
518            ctx.build::<C>(),
519            ctx.build::<D>(),
520            ctx.build::<E>(),
521            ctx.build::<F>(),
522        ];
523        SchemaNodeContent::Tuple(crate::TupleSchema {
524            elements,
525            binding_style: None,
526        })
527    }
528}