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                root_codegen: RootCodegen::default(),
117                codegen_defaults: CodegenDefaults::default(),
118            },
119            cache: HashMap::new(),
120        }
121    }
122
123    /// Build the schema for type `T`, with caching and recursion handling.
124    ///
125    /// This is the primary method for building nested types. It:
126    /// 1. Returns cached ID if already built (idempotent)
127    /// 2. Reserves a node slot before building (handles recursion)
128    /// 3. Calls `T::build_schema_node()` to get the full node spec
129    /// 4. For named types: registers in $types and returns a Reference node
130    pub fn build<T: BuildSchema + 'static>(&mut self) -> SchemaNodeId {
131        let type_id = TypeId::of::<T>();
132
133        // Return cached if already built
134        if let Some(&id) = self.cache.get(&type_id) {
135            return id;
136        }
137
138        // Check if this type has a name (for registration)
139        let type_name = T::type_name();
140
141        // For named types, we need two nodes: content + reference
142        // For unnamed types, just the content node
143        if let Some(name) = type_name {
144            // Reserve a slot for the content node
145            let content_id = self.reserve_node();
146
147            // Build the full node spec
148            let spec = T::build_schema_node(self);
149            self.set_node_spec(content_id, spec);
150
151            // Register the type
152            if let Ok(ident) = name.parse::<eure_document::identifier::Identifier>() {
153                self.doc.types.insert(ident, content_id);
154            }
155
156            // Create a Reference node that points to this type
157            let ref_id = self.create_node(SchemaNodeContent::Reference(crate::TypeReference {
158                namespace: None,
159                name: name.parse().expect("valid type name"),
160            }));
161
162            // Cache the reference ID so subsequent calls return the reference
163            self.cache.insert(type_id, ref_id);
164            ref_id
165        } else {
166            // Unnamed type: just build and cache the content node
167            let id = self.reserve_node();
168            self.cache.insert(type_id, id);
169
170            let spec = T::build_schema_node(self);
171            self.set_node_spec(id, spec);
172
173            id
174        }
175    }
176
177    /// Create a schema node with the given content.
178    ///
179    /// Use this for creating anonymous/inline nodes that don't need caching.
180    /// For types that implement `BuildSchema`, prefer `build::<T>()`.
181    pub fn create_node(&mut self, content: SchemaNodeContent) -> SchemaNodeId {
182        let id = SchemaNodeId(self.doc.nodes.len());
183        self.doc.nodes.push(SchemaNode {
184            content,
185            metadata: SchemaMetadata::default(),
186            ext_types: Default::default(),
187            type_codegen: TypeCodegen::None,
188        });
189        id
190    }
191
192    /// Create a schema node with content and metadata.
193    pub fn create_node_with_metadata(
194        &mut self,
195        content: SchemaNodeContent,
196        metadata: SchemaMetadata,
197    ) -> SchemaNodeId {
198        let id = SchemaNodeId(self.doc.nodes.len());
199        self.doc.nodes.push(SchemaNode {
200            content,
201            metadata,
202            ext_types: Default::default(),
203            type_codegen: TypeCodegen::None,
204        });
205        id
206    }
207
208    /// Reserve a node slot, returning its ID.
209    ///
210    /// The node is initialized with `Any` content and must be finalized
211    /// with `set_node()` before the schema is complete.
212    fn reserve_node(&mut self) -> SchemaNodeId {
213        let id = SchemaNodeId(self.doc.nodes.len());
214        self.doc.nodes.push(SchemaNode {
215            content: SchemaNodeContent::Any, // Placeholder
216            metadata: SchemaMetadata::default(),
217            ext_types: Default::default(),
218            type_codegen: TypeCodegen::None,
219        });
220        id
221    }
222
223    /// Apply a full [`SchemaNodeSpec`] to a reserved node.
224    fn set_node_spec(&mut self, id: SchemaNodeId, spec: SchemaNodeSpec) {
225        let node = &mut self.doc.nodes[id.0];
226        node.content = spec.content;
227        node.metadata = spec.metadata;
228        node.ext_types = spec.ext_types;
229        node.type_codegen = spec.type_codegen;
230    }
231
232    /// Get mutable access to a node for adding ext_types or modifying metadata.
233    pub fn node_mut(&mut self, id: SchemaNodeId) -> &mut SchemaNode {
234        &mut self.doc.nodes[id.0]
235    }
236
237    /// Register a named type in the `$types` namespace.
238    pub fn register_type(&mut self, name: &str, id: SchemaNodeId) {
239        if let Ok(ident) = name.parse() {
240            self.doc.types.insert(ident, id);
241        }
242    }
243
244    /// Consume the builder and produce the final schema document.
245    pub fn finish(mut self, root: SchemaNodeId) -> SchemaDocument {
246        self.doc.root = root;
247        self.doc
248    }
249}
250
251impl Default for SchemaBuilder {
252    fn default() -> Self {
253        Self::new()
254    }
255}
256
257impl SchemaDocument {
258    /// Generate a schema document for type `T`.
259    ///
260    /// This is the main entry point for schema generation from Rust types.
261    ///
262    /// # Example
263    ///
264    /// ```ignore
265    /// use eure_schema::SchemaDocument;
266    ///
267    /// let schema = SchemaDocument::of::<MyType>();
268    /// ```
269    pub fn of<T: BuildSchema + 'static>() -> SchemaDocument {
270        let mut builder = SchemaBuilder::new();
271        let root = builder.build::<T>();
272        builder.finish(root)
273    }
274}
275
276// ============================================================================
277// Primitive Type Implementations
278// ============================================================================
279
280impl BuildSchema for String {
281    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
282        SchemaNodeContent::Text(crate::TextSchema::default())
283    }
284}
285
286impl BuildSchema for &str {
287    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
288        SchemaNodeContent::Text(crate::TextSchema::default())
289    }
290}
291
292impl BuildSchema for bool {
293    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
294        SchemaNodeContent::Boolean
295    }
296}
297
298macro_rules! impl_build_schema_int {
299    ($($ty:ty),*) => {
300        $(
301            impl BuildSchema for $ty {
302                fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
303                    SchemaNodeContent::Integer(crate::IntegerSchema::default())
304                }
305            }
306        )*
307    };
308}
309
310impl_build_schema_int!(
311    u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
312);
313
314// Floats
315impl BuildSchema for f32 {
316    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
317        SchemaNodeContent::Float(crate::FloatSchema {
318            precision: crate::FloatPrecision::F32,
319            ..Default::default()
320        })
321    }
322}
323
324impl BuildSchema for f64 {
325    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
326        SchemaNodeContent::Float(crate::FloatSchema {
327            precision: crate::FloatPrecision::F64,
328            ..Default::default()
329        })
330    }
331}
332
333// Unit type
334impl BuildSchema for () {
335    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
336        SchemaNodeContent::Null
337    }
338}
339
340impl BuildSchema for Text {
341    fn build_schema(_ctx: &mut SchemaBuilder) -> SchemaNodeContent {
342        SchemaNodeContent::Text(TextSchema {
343            language: None,
344            min_length: None,
345            max_length: None,
346            pattern: None,
347            unknown_fields: IndexMap::new(),
348        })
349    }
350}
351
352// ============================================================================
353// Compound Type Implementations
354// ============================================================================
355
356/// Option<T> is represented as a union: some(T) | none(null)
357impl<T: BuildSchema + 'static> BuildSchema for Option<T> {
358    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
359        let some_schema = ctx.build::<T>();
360        let none_schema = ctx.create_node(SchemaNodeContent::Null);
361
362        SchemaNodeContent::Union(crate::UnionSchema {
363            variants: IndexMap::from([
364                ("some".to_string(), some_schema),
365                ("none".to_string(), none_schema),
366            ]),
367            unambiguous: Default::default(),
368            interop: crate::interop::UnionInterop::default(),
369            deny_untagged: Default::default(),
370        })
371    }
372}
373
374/// Result<T, E> is represented as a union: ok(T) | err(E)
375impl<T: BuildSchema + 'static, E: BuildSchema + 'static> BuildSchema for Result<T, E> {
376    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
377        let ok_schema = ctx.build::<T>();
378        let err_schema = ctx.build::<E>();
379
380        SchemaNodeContent::Union(crate::UnionSchema {
381            variants: IndexMap::from([
382                ("ok".to_string(), ok_schema),
383                ("err".to_string(), err_schema),
384            ]),
385            unambiguous: Default::default(),
386            interop: crate::interop::UnionInterop::default(),
387            deny_untagged: Default::default(),
388        })
389    }
390}
391
392/// Vec<T> is represented as an array with item type T
393impl<T: BuildSchema + 'static> BuildSchema for Vec<T> {
394    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
395        let item = ctx.build::<T>();
396        SchemaNodeContent::Array(crate::ArraySchema {
397            item,
398            min_length: None,
399            max_length: None,
400            unique: false,
401            contains: None,
402            binding_style: None,
403        })
404    }
405}
406
407/// HashMap<K, V> is represented as a map
408impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
409    for std::collections::HashMap<K, V>
410{
411    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
412        let key = ctx.build::<K>();
413        let value = ctx.build::<V>();
414        SchemaNodeContent::Map(crate::MapSchema {
415            key,
416            value,
417            min_size: None,
418            max_size: None,
419        })
420    }
421}
422
423/// BTreeMap<K, V> is represented as a map
424impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema
425    for std::collections::BTreeMap<K, V>
426{
427    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
428        let key = ctx.build::<K>();
429        let value = ctx.build::<V>();
430        SchemaNodeContent::Map(crate::MapSchema {
431            key,
432            value,
433            min_size: None,
434            max_size: None,
435        })
436    }
437}
438
439/// IndexMap<K, V> is represented as a map (preserves insertion order)
440impl<K: BuildSchema + 'static, V: BuildSchema + 'static> BuildSchema for IndexMap<K, V> {
441    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
442        let key = ctx.build::<K>();
443        let value = ctx.build::<V>();
444        SchemaNodeContent::Map(crate::MapSchema {
445            key,
446            value,
447            min_size: None,
448            max_size: None,
449        })
450    }
451}
452
453/// Box<T> delegates to T
454impl<T: BuildSchema + 'static> BuildSchema for Box<T> {
455    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
456        T::build_schema(ctx)
457    }
458}
459
460/// Rc<T> delegates to T
461impl<T: BuildSchema + 'static> BuildSchema for std::rc::Rc<T> {
462    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
463        T::build_schema(ctx)
464    }
465}
466
467/// Arc<T> delegates to T
468impl<T: BuildSchema + 'static> BuildSchema for std::sync::Arc<T> {
469    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
470        T::build_schema(ctx)
471    }
472}
473
474// Tuples
475impl<A: BuildSchema + 'static> BuildSchema for (A,) {
476    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
477        let elements = vec![ctx.build::<A>()];
478        SchemaNodeContent::Tuple(crate::TupleSchema {
479            elements,
480            binding_style: None,
481        })
482    }
483}
484
485impl<A: BuildSchema + 'static, B: BuildSchema + 'static> BuildSchema for (A, B) {
486    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
487        let elements = vec![ctx.build::<A>(), ctx.build::<B>()];
488        SchemaNodeContent::Tuple(crate::TupleSchema {
489            elements,
490            binding_style: None,
491        })
492    }
493}
494
495impl<A: BuildSchema + 'static, B: BuildSchema + 'static, C: BuildSchema + 'static> BuildSchema
496    for (A, B, C)
497{
498    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
499        let elements = vec![ctx.build::<A>(), ctx.build::<B>(), ctx.build::<C>()];
500        SchemaNodeContent::Tuple(crate::TupleSchema {
501            elements,
502            binding_style: None,
503        })
504    }
505}
506
507impl<
508    A: BuildSchema + 'static,
509    B: BuildSchema + 'static,
510    C: BuildSchema + 'static,
511    D: BuildSchema + 'static,
512> BuildSchema for (A, B, C, D)
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        ];
521        SchemaNodeContent::Tuple(crate::TupleSchema {
522            elements,
523            binding_style: None,
524        })
525    }
526}
527
528impl<
529    A: BuildSchema + 'static,
530    B: BuildSchema + 'static,
531    C: BuildSchema + 'static,
532    D: BuildSchema + 'static,
533    E: BuildSchema + 'static,
534> BuildSchema for (A, B, C, D, E)
535{
536    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
537        let elements = vec![
538            ctx.build::<A>(),
539            ctx.build::<B>(),
540            ctx.build::<C>(),
541            ctx.build::<D>(),
542            ctx.build::<E>(),
543        ];
544        SchemaNodeContent::Tuple(crate::TupleSchema {
545            elements,
546            binding_style: None,
547        })
548    }
549}
550
551impl<
552    A: BuildSchema + 'static,
553    B: BuildSchema + 'static,
554    C: BuildSchema + 'static,
555    D: BuildSchema + 'static,
556    E: BuildSchema + 'static,
557    F: BuildSchema + 'static,
558> BuildSchema for (A, B, C, D, E, F)
559{
560    fn build_schema(ctx: &mut SchemaBuilder) -> SchemaNodeContent {
561        let elements = vec![
562            ctx.build::<A>(),
563            ctx.build::<B>(),
564            ctx.build::<C>(),
565            ctx.build::<D>(),
566            ctx.build::<E>(),
567            ctx.build::<F>(),
568        ];
569        SchemaNodeContent::Tuple(crate::TupleSchema {
570            elements,
571            binding_style: None,
572        })
573    }
574}