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
266// Signed integers
267impl BuildSchema for i8 {
268    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
269        SchemaNodeContent::Integer(crate::IntegerSchema::default())
270    }
271}
272
273impl BuildSchema for i16 {
274    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
275        SchemaNodeContent::Integer(crate::IntegerSchema::default())
276    }
277}
278
279impl BuildSchema for i32 {
280    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
281        SchemaNodeContent::Integer(crate::IntegerSchema::default())
282    }
283}
284
285impl BuildSchema for i64 {
286    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
287        SchemaNodeContent::Integer(crate::IntegerSchema::default())
288    }
289}
290
291impl BuildSchema for i128 {
292    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
293        SchemaNodeContent::Integer(crate::IntegerSchema::default())
294    }
295}
296
297impl BuildSchema for isize {
298    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
299        SchemaNodeContent::Integer(crate::IntegerSchema::default())
300    }
301}
302
303// Unsigned integers
304impl BuildSchema for u8 {
305    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
306        SchemaNodeContent::Integer(crate::IntegerSchema::default())
307    }
308}
309
310impl BuildSchema for u16 {
311    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
312        SchemaNodeContent::Integer(crate::IntegerSchema::default())
313    }
314}
315
316impl BuildSchema for u32 {
317    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
318        SchemaNodeContent::Integer(crate::IntegerSchema::default())
319    }
320}
321
322impl BuildSchema for u64 {
323    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
324        SchemaNodeContent::Integer(crate::IntegerSchema::default())
325    }
326}
327
328impl BuildSchema for u128 {
329    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
330        SchemaNodeContent::Integer(crate::IntegerSchema::default())
331    }
332}
333
334impl BuildSchema for usize {
335    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
336        SchemaNodeContent::Integer(crate::IntegerSchema::default())
337    }
338}
339
340// Floats
341impl BuildSchema for f32 {
342    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
343        SchemaNodeContent::Float(crate::FloatSchema {
344            precision: crate::FloatPrecision::F32,
345            ..Default::default()
346        })
347    }
348}
349
350impl BuildSchema for f64 {
351    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
352        SchemaNodeContent::Float(crate::FloatSchema {
353            precision: crate::FloatPrecision::F64,
354            ..Default::default()
355        })
356    }
357}
358
359// Unit type
360impl BuildSchema for () {
361    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
362        SchemaNodeContent::Null
363    }
364}
365
366impl BuildSchema for Text {
367    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
368        SchemaNodeContent::Text(TextSchema {
369            language: None,
370            min_length: None,
371            max_length: None,
372            pattern: None,
373            unknown_fields: IndexMap::new(),
374        })
375    }
376}
377
378// ============================================================================
379// Compound Type Implementations
380// ============================================================================
381
382/// Option<T> is represented as a union: some(T) | none(null)
383impl<T: BuildSchema + 'static> BuildSchema for Option<T> {
384    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
385        let some_schema = ctx.build::<T>();
386        let none_schema = ctx.create_node(SchemaNodeContent::Null);
387
388        SchemaNodeContent::Union(crate::UnionSchema {
389            variants: IndexMap::from([
390                ("some".to_string(), some_schema),
391                ("none".to_string(), none_schema),
392            ]),
393            unambiguous: Default::default(),
394            repr: eure_document::data_model::VariantRepr::default(),
395            deny_untagged: Default::default(),
396        })
397    }
398}
399
400/// Result<T, E> is represented as a union: ok(T) | err(E)
401impl<T: BuildSchema + 'static, E: BuildSchema + 'static> BuildSchema for Result<T, E> {
402    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
403        let ok_schema = ctx.build::<T>();
404        let err_schema = ctx.build::<E>();
405
406        SchemaNodeContent::Union(crate::UnionSchema {
407            variants: IndexMap::from([
408                ("ok".to_string(), ok_schema),
409                ("err".to_string(), err_schema),
410            ]),
411            unambiguous: Default::default(),
412            repr: eure_document::data_model::VariantRepr::default(),
413            deny_untagged: Default::default(),
414        })
415    }
416}
417
418/// Vec<T> is represented as an array with item type T
419impl<T: BuildSchema + 'static> BuildSchema for Vec<T> {
420    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
421        let item = ctx.build::<T>();
422        SchemaNodeContent::Array(crate::ArraySchema {
423            item,
424            min_length: None,
425            max_length: None,
426            unique: false,
427            contains: None,
428            binding_style: None,
429        })
430    }
431}
432
433/// HashMap<K, V> is represented as a map
434impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
435    for std::collections::HashMap<K, V>
436{
437    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
438        let key = ctx.build::<K>();
439        let value = ctx.build::<V>();
440        SchemaNodeContent::Map(crate::MapSchema {
441            key,
442            value,
443            min_size: None,
444            max_size: None,
445        })
446    }
447}
448
449/// BTreeMap<K, V> is represented as a map
450impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
451    for std::collections::BTreeMap<K, V>
452{
453    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
454        let key = ctx.build::<K>();
455        let value = ctx.build::<V>();
456        SchemaNodeContent::Map(crate::MapSchema {
457            key,
458            value,
459            min_size: None,
460            max_size: None,
461        })
462    }
463}
464
465/// Box<T> delegates to T
466impl<T: BuildSchema + 'static> BuildSchema for Box<T> {
467    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
468        T::build_schema(ctx)
469    }
470}
471
472/// Rc<T> delegates to T
473impl<T: BuildSchema + 'static> BuildSchema for std::rc::Rc<T> {
474    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
475        T::build_schema(ctx)
476    }
477}
478
479/// Arc<T> delegates to T
480impl<T: BuildSchema + 'static> BuildSchema for std::sync::Arc<T> {
481    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
482        T::build_schema(ctx)
483    }
484}
485
486// Tuples
487impl<A: BuildSchema + 'static> BuildSchema for (A,) {
488    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
489        let elements = vec![ctx.build::<A>()];
490        SchemaNodeContent::Tuple(crate::TupleSchema {
491            elements,
492            binding_style: None,
493        })
494    }
495}
496
497impl<A: BuildSchema + 'static, B: BuildSchema + 'static> BuildSchema for (A, B) {
498    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
499        let elements = vec![ctx.build::<A>(), ctx.build::<B>()];
500        SchemaNodeContent::Tuple(crate::TupleSchema {
501            elements,
502            binding_style: None,
503        })
504    }
505}
506
507impl<A: BuildSchema + 'static, B: BuildSchema + 'static, C: BuildSchema + 'static> BuildSchema
508    for (A, B, C)
509{
510    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
511        let elements = vec![ctx.build::<A>(), ctx.build::<B>(), ctx.build::<C>()];
512        SchemaNodeContent::Tuple(crate::TupleSchema {
513            elements,
514            binding_style: None,
515        })
516    }
517}
518
519impl<
520    A: BuildSchema + 'static,
521    B: BuildSchema + 'static,
522    C: BuildSchema + 'static,
523    D: BuildSchema + 'static,
524> BuildSchema for (A, B, C, D)
525{
526    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
527        let elements = vec![
528            ctx.build::<A>(),
529            ctx.build::<B>(),
530            ctx.build::<C>(),
531            ctx.build::<D>(),
532        ];
533        SchemaNodeContent::Tuple(crate::TupleSchema {
534            elements,
535            binding_style: None,
536        })
537    }
538}
539
540impl<
541    A: BuildSchema + 'static,
542    B: BuildSchema + 'static,
543    C: BuildSchema + 'static,
544    D: BuildSchema + 'static,
545    E: BuildSchema + 'static,
546> BuildSchema for (A, B, C, D, E)
547{
548    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
549        let elements = vec![
550            ctx.build::<A>(),
551            ctx.build::<B>(),
552            ctx.build::<C>(),
553            ctx.build::<D>(),
554            ctx.build::<E>(),
555        ];
556        SchemaNodeContent::Tuple(crate::TupleSchema {
557            elements,
558            binding_style: None,
559        })
560    }
561}
562
563impl<
564    A: BuildSchema + 'static,
565    B: BuildSchema + 'static,
566    C: BuildSchema + 'static,
567    D: BuildSchema + 'static,
568    E: BuildSchema + 'static,
569    F: BuildSchema + 'static,
570> BuildSchema for (A, B, C, D, E, F)
571{
572    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
573        let elements = vec![
574            ctx.build::<A>(),
575            ctx.build::<B>(),
576            ctx.build::<C>(),
577            ctx.build::<D>(),
578            ctx.build::<E>(),
579            ctx.build::<F>(),
580        ];
581        SchemaNodeContent::Tuple(crate::TupleSchema {
582            elements,
583            binding_style: None,
584        })
585    }
586}