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