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