Skip to main content

eure_schema/
write.rs

1//! Write Eure documents/sources from `SchemaDocument` using generic write API composition.
2
3use crate::identifiers::{CONTENT, EXT_TYPE, OPTIONAL, TAG, VARIANT, VARIANT_REPR};
4use crate::interop::VariantRepr;
5use crate::{
6    ArraySchema, BindingStyle, Bound, CodegenDefaults, Description, ExtTypeSchema, FieldCodegen,
7    FloatPrecision, FloatSchema, IntegerSchema, MapSchema, RecordCodegen, RecordFieldSchema,
8    RecordSchema, RootCodegen, SchemaDocument, SchemaMetadata, SchemaNodeContent, SchemaNodeId,
9    TupleSchema, TypeCodegen, TypeReference, UnionCodegen, UnionSchema, UnknownFieldsPolicy,
10};
11use eure_document::document::constructor::DocumentConstructor;
12use eure_document::document::node::NodeValue;
13use eure_document::document::{EureDocument, NodeId};
14use eure_document::identifier::Identifier;
15use eure_document::layout::{DocLayout, project_with_layout};
16use eure_document::path::PathSegment;
17use eure_document::source::SourceDocument;
18use eure_document::text::Text;
19use eure_document::value::{ObjectKey, PrimitiveValue};
20use eure_document::write::{IntoEure, WriteError};
21use num_bigint::BigInt;
22use thiserror::Error;
23
24const IDENT_TYPES: Identifier = Identifier::new_unchecked("types");
25const IDENT_BINDING_STYLE: Identifier = Identifier::new_unchecked("binding-style");
26const IDENT_UNKNOWN_FIELDS: Identifier = Identifier::new_unchecked("unknown-fields");
27const IDENT_FLATTEN: Identifier = Identifier::new_unchecked("flatten");
28const IDENT_DESCRIPTION: Identifier = Identifier::new_unchecked("description");
29const IDENT_DEPRECATED: Identifier = Identifier::new_unchecked("deprecated");
30const IDENT_DEFAULT: Identifier = Identifier::new_unchecked("default");
31const IDENT_EXAMPLES: Identifier = Identifier::new_unchecked("examples");
32const IDENT_DENY_UNTAGGED: Identifier = Identifier::new_unchecked("deny-untagged");
33const IDENT_UNAMBIGUOUS: Identifier = Identifier::new_unchecked("unambiguous");
34const IDENT_INTEROP: Identifier = Identifier::new_unchecked("interop");
35const IDENT_CODEGEN: Identifier = Identifier::new_unchecked("codegen");
36const IDENT_CODEGEN_DEFAULTS: Identifier = Identifier::new_unchecked("codegen-defaults");
37
38const KEY_VARIANTS: &str = "variants";
39
40/// Errors that can occur during schema writing.
41#[derive(Debug, Error, Clone)]
42pub enum SchemaWriteError {
43    #[error("write error: {0}")]
44    Write(#[from] WriteError),
45    #[error("literal root cannot be a hole")]
46    LiteralRootIsHole,
47    #[error(
48        "conflicting root $codegen type names: root={root_type_name}, type_codegen={type_codegen_type_name}"
49    )]
50    ConflictingRootCodegenTypeName {
51        root_type_name: String,
52        type_codegen_type_name: String,
53    },
54}
55
56/// Emit an [`EureDocument`] from a [`SchemaDocument`].
57pub fn schema_to_document(schema: &SchemaDocument) -> Result<EureDocument, SchemaWriteError> {
58    validate_schema_for_write(schema)?;
59
60    let mut c = DocumentConstructor::new();
61    c.write(schema.clone())?;
62    Ok(c.finish())
63}
64
65/// Project a schema document to source using a caller-provided generic layout plan.
66pub fn schema_to_source_document(
67    schema: &SchemaDocument,
68    layout: &DocLayout,
69) -> Result<SourceDocument, SchemaWriteError> {
70    let doc = schema_to_document(schema)?;
71    Ok(project_with_layout(&doc, layout))
72}
73
74impl IntoEure for SchemaDocument {
75    type Error = WriteError;
76
77    fn write(value: Self, c: &mut DocumentConstructor) -> Result<(), Self::Error> {
78        write_schema_document(&value, c)
79    }
80}
81
82fn validate_schema_for_write(schema: &SchemaDocument) -> Result<(), SchemaWriteError> {
83    for node in &schema.nodes {
84        if let SchemaNodeContent::Literal(literal_doc) = &node.content
85            && matches!(literal_doc.root().content, NodeValue::Hole(_))
86        {
87            return Err(SchemaWriteError::LiteralRootIsHole);
88        }
89    }
90
91    if let Some(root_type_name) = schema.root_codegen.type_name.as_deref()
92        && let Some(type_codegen_type_name) = root_type_codegen_type_name(schema)
93        && root_type_name != type_codegen_type_name
94    {
95        return Err(SchemaWriteError::ConflictingRootCodegenTypeName {
96            root_type_name: root_type_name.to_string(),
97            type_codegen_type_name: type_codegen_type_name.to_string(),
98        });
99    }
100
101    Ok(())
102}
103
104fn write_schema_document(
105    schema: &SchemaDocument,
106    c: &mut DocumentConstructor,
107) -> Result<(), WriteError> {
108    write_schema_node_internal(schema, schema.root, false, c)?;
109    write_types_extension(schema, c)?;
110    write_root_codegen_extension(schema, c)?;
111    write_codegen_defaults_extension(&schema.codegen_defaults, c)?;
112    Ok(())
113}
114
115fn write_schema_node(
116    schema: &SchemaDocument,
117    schema_id: SchemaNodeId,
118    c: &mut DocumentConstructor,
119) -> Result<(), WriteError> {
120    write_schema_node_internal(schema, schema_id, true, c)
121}
122
123fn write_schema_node_internal(
124    schema: &SchemaDocument,
125    schema_id: SchemaNodeId,
126    write_type_codegen: bool,
127    c: &mut DocumentConstructor,
128) -> Result<(), WriteError> {
129    let node = schema.node(schema_id);
130    write_schema_content(schema, &node.content, c)?;
131    write_ext_types(schema, &node.ext_types, c)?;
132    write_metadata(&node.metadata, c)?;
133    if write_type_codegen {
134        write_type_codegen_extension(&node.type_codegen, c)?;
135    }
136    Ok(())
137}
138
139fn write_schema_content(
140    schema_doc: &SchemaDocument,
141    content: &SchemaNodeContent,
142    c: &mut DocumentConstructor,
143) -> Result<(), WriteError> {
144    match content {
145        SchemaNodeContent::Any => c.write(Text::inline_implicit("any")),
146        SchemaNodeContent::Boolean => c.write(Text::inline_implicit("boolean")),
147        SchemaNodeContent::Null => c.write(Text::inline_implicit("null")),
148        SchemaNodeContent::Integer(schema) => schema.write(c),
149        SchemaNodeContent::Float(schema) => schema.write(c),
150        SchemaNodeContent::Text(schema) => schema.write(c),
151        SchemaNodeContent::Array(schema) => write_array_schema(schema_doc, schema, c),
152        SchemaNodeContent::Map(schema) => write_map_schema(schema_doc, schema, c),
153        SchemaNodeContent::Record(schema) => write_record_schema(schema_doc, schema, c),
154        SchemaNodeContent::Tuple(schema) => write_tuple_schema(schema_doc, schema, c),
155        SchemaNodeContent::Union(schema) => write_union_schema(schema_doc, schema, c),
156        SchemaNodeContent::Reference(reference) => reference.write(c),
157        SchemaNodeContent::Literal(doc) => write_literal(doc, c),
158    }
159}
160
161impl IntegerSchema {
162    pub fn is_shorthand_compatible(&self) -> bool {
163        matches!(self.min, Bound::Unbounded)
164            && matches!(self.max, Bound::Unbounded)
165            && self.multiple_of.is_none()
166    }
167
168    pub fn shorthand(&self) -> Option<Text> {
169        self.is_shorthand_compatible()
170            .then(|| Text::inline_implicit("integer"))
171    }
172
173    pub fn write(&self, c: &mut DocumentConstructor) -> Result<(), WriteError> {
174        if let Some(shorthand) = self.shorthand() {
175            return c.write(shorthand);
176        }
177
178        c.record(|rec| {
179            rec.constructor().set_variant("integer")?;
180            rec.field_optional(
181                "range",
182                format_bound_range(&self.min, &self.max, format_bigint),
183            )?;
184            rec.field_optional("multiple-of", self.multiple_of.clone())?;
185            Ok(())
186        })
187    }
188}
189
190impl FloatSchema {
191    pub fn is_shorthand_compatible(&self) -> bool {
192        matches!(self.min, Bound::Unbounded)
193            && matches!(self.max, Bound::Unbounded)
194            && self.multiple_of.is_none()
195            && matches!(self.precision, FloatPrecision::F64)
196    }
197
198    pub fn shorthand(&self) -> Option<Text> {
199        self.is_shorthand_compatible()
200            .then(|| Text::inline_implicit("float"))
201    }
202
203    pub fn write(&self, c: &mut DocumentConstructor) -> Result<(), WriteError> {
204        if let Some(shorthand) = self.shorthand() {
205            return c.write(shorthand);
206        }
207
208        c.record(|rec| {
209            rec.constructor().set_variant("float")?;
210            rec.field_optional(
211                "range",
212                format_bound_range(&self.min, &self.max, format_f64),
213            )?;
214            rec.field_optional("multiple-of", self.multiple_of)?;
215            if matches!(self.precision, FloatPrecision::F32) {
216                rec.field("precision", "f32")?;
217            }
218            Ok(())
219        })
220    }
221}
222
223fn write_array_schema(
224    schema_doc: &SchemaDocument,
225    schema: &ArraySchema,
226    c: &mut DocumentConstructor,
227) -> Result<(), WriteError> {
228    let use_shorthand = schema.min_length.is_none()
229        && schema.max_length.is_none()
230        && !schema.unique
231        && schema.contains.is_none()
232        && schema.binding_style.is_none()
233        && can_emit_as_single_inline_text(schema_doc, schema.item);
234
235    if use_shorthand {
236        c.bind_empty_array()?;
237        let scope = c.begin_scope();
238        c.navigate(PathSegment::ArrayIndex(None))?;
239        write_schema_node(schema_doc, schema.item, c)?;
240        c.end_scope(scope)?;
241        return Ok(());
242    }
243
244    c.record(|rec| {
245        rec.constructor().set_variant("array")?;
246        rec.field_with("item", |c| write_schema_node(schema_doc, schema.item, c))?;
247        rec.field_optional("min-length", schema.min_length)?;
248        rec.field_optional("max-length", schema.max_length)?;
249        if schema.unique {
250            rec.field("unique", true)?;
251        }
252        if let Some(contains) = schema.contains {
253            rec.field_with("contains", |c| write_schema_node(schema_doc, contains, c))?;
254        }
255        if let Some(style) = schema.binding_style {
256            write_binding_style_extension(style, rec.constructor())?;
257        }
258        Ok(())
259    })
260}
261
262fn write_tuple_schema(
263    schema_doc: &SchemaDocument,
264    schema: &TupleSchema,
265    c: &mut DocumentConstructor,
266) -> Result<(), WriteError> {
267    if schema.binding_style.is_none() {
268        c.bind_empty_tuple()?;
269        for (index, schema_id) in schema.elements.iter().enumerate() {
270            let scope = c.begin_scope();
271            c.navigate(PathSegment::TupleIndex(index as u8))?;
272            write_schema_node(schema_doc, *schema_id, c)?;
273            c.end_scope(scope)?;
274        }
275        return Ok(());
276    }
277
278    c.record(|rec| {
279        rec.constructor().set_variant("tuple")?;
280        rec.field_with("elements", |c| {
281            c.bind_empty_array()?;
282            for schema_id in &schema.elements {
283                let scope = c.begin_scope();
284                c.navigate(PathSegment::ArrayIndex(None))?;
285                write_schema_node(schema_doc, *schema_id, c)?;
286                c.end_scope(scope)?;
287            }
288            Ok(())
289        })?;
290        if let Some(style) = schema.binding_style {
291            write_binding_style_extension(style, rec.constructor())?;
292        }
293        Ok(())
294    })
295}
296
297fn write_map_schema(
298    schema_doc: &SchemaDocument,
299    schema: &MapSchema,
300    c: &mut DocumentConstructor,
301) -> Result<(), WriteError> {
302    c.record(|rec| {
303        rec.constructor().set_variant("map")?;
304        rec.field_with("key", |c| write_schema_node(schema_doc, schema.key, c))?;
305        rec.field_with("value", |c| write_schema_node(schema_doc, schema.value, c))?;
306        rec.field_optional("min-size", schema.min_size)?;
307        rec.field_optional("max-size", schema.max_size)?;
308        Ok(())
309    })
310}
311
312fn write_record_schema(
313    schema_doc: &SchemaDocument,
314    schema: &RecordSchema,
315    c: &mut DocumentConstructor,
316) -> Result<(), WriteError> {
317    c.record(|rec| {
318        write_unknown_fields_policy(schema_doc, &schema.unknown_fields, rec.constructor())?;
319        write_flatten(schema_doc, &schema.flatten, rec.constructor())?;
320
321        for (name, field_schema) in &schema.properties {
322            rec.field_with(name, |c| {
323                write_schema_node(schema_doc, field_schema.schema, c)?;
324                write_record_field_extensions(field_schema, c)?;
325                Ok(())
326            })?;
327        }
328
329        Ok(())
330    })
331}
332
333fn write_record_field_extensions(
334    schema: &RecordFieldSchema,
335    c: &mut DocumentConstructor,
336) -> Result<(), WriteError> {
337    if schema.optional {
338        c.set_extension(OPTIONAL.as_ref(), true)?;
339    }
340    if let Some(style) = schema.binding_style {
341        write_binding_style_extension(style, c)?;
342    }
343    write_field_codegen_extension(&schema.field_codegen, c)?;
344    Ok(())
345}
346
347fn write_root_codegen_extension(
348    schema: &SchemaDocument,
349    c: &mut DocumentConstructor,
350) -> Result<(), WriteError> {
351    match &schema.node(schema.root).type_codegen {
352        TypeCodegen::None => {
353            if schema.root_codegen == RootCodegen::default() {
354                return Ok(());
355            }
356            write_extension(c, IDENT_CODEGEN, |c| c.write(schema.root_codegen.clone()))
357        }
358        TypeCodegen::Record(record_codegen) => {
359            let merged = RecordCodegen {
360                type_name: merge_root_type_name(
361                    schema.root_codegen.type_name.as_deref(),
362                    record_codegen.type_name.as_deref(),
363                )?,
364                derive: record_codegen.derive.clone(),
365            };
366            if merged == RecordCodegen::default() {
367                return Ok(());
368            }
369            write_extension(c, IDENT_CODEGEN, |c| c.write(merged))
370        }
371        TypeCodegen::Union(union_codegen) => {
372            let merged = UnionCodegen {
373                type_name: merge_root_type_name(
374                    schema.root_codegen.type_name.as_deref(),
375                    union_codegen.type_name.as_deref(),
376                )?,
377                derive: union_codegen.derive.clone(),
378                variant_types: union_codegen.variant_types,
379                variant_types_suffix: union_codegen.variant_types_suffix.clone(),
380            };
381            if merged == UnionCodegen::default() {
382                return Ok(());
383            }
384            write_extension(c, IDENT_CODEGEN, |c| c.write(merged))
385        }
386    }
387}
388
389fn write_codegen_defaults_extension(
390    defaults: &CodegenDefaults,
391    c: &mut DocumentConstructor,
392) -> Result<(), WriteError> {
393    if defaults == &CodegenDefaults::default() {
394        return Ok(());
395    }
396    write_extension(c, IDENT_CODEGEN_DEFAULTS, |c| c.write(defaults.clone()))
397}
398
399fn write_type_codegen_extension(
400    codegen: &TypeCodegen,
401    c: &mut DocumentConstructor,
402) -> Result<(), WriteError> {
403    match codegen {
404        TypeCodegen::None => Ok(()),
405        TypeCodegen::Record(record) => {
406            write_extension(c, IDENT_CODEGEN, |c| c.write(record.clone()))
407        }
408        TypeCodegen::Union(union) => write_extension(c, IDENT_CODEGEN, |c| c.write(union.clone())),
409    }
410}
411
412fn write_field_codegen_extension(
413    codegen: &FieldCodegen,
414    c: &mut DocumentConstructor,
415) -> Result<(), WriteError> {
416    if codegen == &FieldCodegen::default() {
417        return Ok(());
418    }
419    write_extension(c, IDENT_CODEGEN, |c| c.write(codegen.clone()))
420}
421
422fn write_union_schema(
423    schema_doc: &SchemaDocument,
424    schema: &UnionSchema,
425    c: &mut DocumentConstructor,
426) -> Result<(), WriteError> {
427    c.record(|rec| {
428        rec.constructor().set_variant("union")?;
429
430        write_interop_extension(&schema.interop.variant_repr, rec.constructor())?;
431
432        rec.field_with(KEY_VARIANTS, |c| {
433            c.record(|variants_rec| {
434                for (name, schema_id) in &schema.variants {
435                    variants_rec.field_with(name, |c| {
436                        write_schema_node(schema_doc, *schema_id, c)?;
437                        if schema.deny_untagged.contains(name) {
438                            c.set_extension(IDENT_DENY_UNTAGGED.as_ref(), true)?;
439                        }
440                        if schema.unambiguous.contains(name) {
441                            c.set_extension(IDENT_UNAMBIGUOUS.as_ref(), true)?;
442                        }
443                        Ok(())
444                    })?;
445                }
446                Ok(())
447            })
448        })?;
449
450        Ok(())
451    })
452}
453
454impl TypeReference {
455    pub fn write(&self, c: &mut DocumentConstructor) -> Result<(), WriteError> {
456        let mut path = String::from("$types.");
457        if let Some(namespace) = &self.namespace {
458            path.push_str(namespace);
459            path.push('.');
460        }
461        path.push_str(self.name.as_ref());
462
463        c.write(Text::inline_implicit(path))
464    }
465}
466
467fn write_literal(
468    literal_doc: &EureDocument,
469    c: &mut DocumentConstructor,
470) -> Result<(), WriteError> {
471    let root_id = literal_doc.get_root_id();
472    let root = literal_doc.node(root_id);
473    if matches!(root.content, NodeValue::Hole(_)) {
474        return Err(WriteError::InvalidIdentifier(
475            "literal root cannot be a hole".to_string(),
476        ));
477    }
478
479    copy_subtree(literal_doc, root_id, c, true)?;
480
481    if literal_needs_variant(root) {
482        c.set_variant("literal")?;
483    }
484
485    Ok(())
486}
487
488fn write_types_extension(
489    schema: &SchemaDocument,
490    c: &mut DocumentConstructor,
491) -> Result<(), WriteError> {
492    if schema.types.is_empty() {
493        return Ok(());
494    }
495
496    write_extension(c, IDENT_TYPES, |c| {
497        c.record(|rec| {
498            for (name, schema_id) in &schema.types {
499                rec.field_with(name.as_ref(), |c| write_schema_node(schema, *schema_id, c))?;
500            }
501            Ok(())
502        })
503    })
504}
505
506fn write_ext_types(
507    schema_doc: &SchemaDocument,
508    ext_types: &indexmap::IndexMap<Identifier, ExtTypeSchema>,
509    c: &mut DocumentConstructor,
510) -> Result<(), WriteError> {
511    if ext_types.is_empty() {
512        return Ok(());
513    }
514
515    write_extension(c, EXT_TYPE, |c| {
516        c.record(|rec| {
517            for (name, ext_schema) in ext_types {
518                rec.field_with(name.as_ref(), |c| {
519                    write_schema_node(schema_doc, ext_schema.schema, c)?;
520                    if ext_schema.optional {
521                        c.set_extension(OPTIONAL.as_ref(), true)?;
522                    }
523                    if let Some(style) = ext_schema.binding_style {
524                        write_binding_style_extension(style, c)?;
525                    }
526                    Ok(())
527                })?;
528            }
529            Ok(())
530        })
531    })
532}
533
534fn write_metadata(
535    metadata: &SchemaMetadata,
536    c: &mut DocumentConstructor,
537) -> Result<(), WriteError> {
538    if let Some(description) = &metadata.description {
539        match description {
540            Description::String(v) => c.set_extension(IDENT_DESCRIPTION.as_ref(), v.clone())?,
541            Description::Markdown(v) => {
542                let text = if v.contains('\n') {
543                    Text::block(v, "markdown")
544                } else {
545                    Text::inline(v, "markdown")
546                };
547                c.set_extension(IDENT_DESCRIPTION.as_ref(), text)?;
548            }
549        }
550    }
551
552    if metadata.deprecated {
553        c.set_extension(IDENT_DEPRECATED.as_ref(), true)?;
554    }
555
556    if let Some(default_doc) = &metadata.default {
557        write_extension(c, IDENT_DEFAULT, |c| {
558            copy_subtree(default_doc, default_doc.get_root_id(), c, false)
559        })?;
560    }
561
562    if let Some(examples) = &metadata.examples {
563        write_extension(c, IDENT_EXAMPLES, |c| {
564            c.bind_empty_array()?;
565            for example in examples {
566                let scope = c.begin_scope();
567                c.navigate(PathSegment::ArrayIndex(None))?;
568                copy_subtree(example, example.get_root_id(), c, false)?;
569                c.end_scope(scope)?;
570            }
571            Ok(())
572        })?;
573    }
574
575    Ok(())
576}
577
578fn write_unknown_fields_policy(
579    schema_doc: &SchemaDocument,
580    policy: &UnknownFieldsPolicy,
581    c: &mut DocumentConstructor,
582) -> Result<(), WriteError> {
583    match policy {
584        UnknownFieldsPolicy::Deny => Ok(()),
585        UnknownFieldsPolicy::Allow => c.set_extension(IDENT_UNKNOWN_FIELDS.as_ref(), "allow"),
586        UnknownFieldsPolicy::Schema(schema_id) => write_extension(c, IDENT_UNKNOWN_FIELDS, |c| {
587            write_schema_node(schema_doc, *schema_id, c)
588        }),
589    }
590}
591
592fn write_flatten(
593    schema_doc: &SchemaDocument,
594    flatten: &[SchemaNodeId],
595    c: &mut DocumentConstructor,
596) -> Result<(), WriteError> {
597    if flatten.is_empty() {
598        return Ok(());
599    }
600
601    write_extension(c, IDENT_FLATTEN, |c| {
602        c.bind_empty_array()?;
603        for schema_id in flatten {
604            let scope = c.begin_scope();
605            c.navigate(PathSegment::ArrayIndex(None))?;
606            write_schema_node(schema_doc, *schema_id, c)?;
607            c.end_scope(scope)?;
608        }
609        Ok(())
610    })
611}
612
613fn write_interop_extension(
614    repr: &Option<VariantRepr>,
615    c: &mut DocumentConstructor,
616) -> Result<(), WriteError> {
617    let Some(repr) = repr else {
618        return Ok(());
619    };
620
621    let scope = c.begin_scope();
622    c.navigate(PathSegment::Extension(IDENT_INTEROP))?;
623    c.navigate(PathSegment::Value(ObjectKey::String(
624        VARIANT_REPR.as_ref().to_string(),
625    )))?;
626    write_variant_repr_value(repr, c)?;
627    c.end_scope(scope)?;
628    Ok(())
629}
630
631fn write_variant_repr_value(
632    repr: &VariantRepr,
633    c: &mut DocumentConstructor,
634) -> Result<(), WriteError> {
635    match repr {
636        VariantRepr::External => c.write("external"),
637        VariantRepr::Untagged => c.write("untagged"),
638        VariantRepr::Internal { tag } => c.record(|rec| {
639            rec.field(TAG.as_ref(), tag.clone())?;
640            Ok(())
641        }),
642        VariantRepr::Adjacent { tag, content } => c.record(|rec| {
643            rec.field(TAG.as_ref(), tag.clone())?;
644            rec.field(CONTENT.as_ref(), content.clone())?;
645            Ok(())
646        }),
647    }
648}
649
650fn write_binding_style_extension(
651    style: BindingStyle,
652    c: &mut DocumentConstructor,
653) -> Result<(), WriteError> {
654    c.set_extension(
655        IDENT_BINDING_STYLE.as_ref(),
656        Text::plaintext(binding_style_as_str(style)),
657    )
658}
659
660fn binding_style_as_str(style: BindingStyle) -> &'static str {
661    match style {
662        BindingStyle::Auto => "auto",
663        BindingStyle::Passthrough => "passthrough",
664        BindingStyle::Section => "section",
665        BindingStyle::Nested => "nested",
666        BindingStyle::Binding => "binding",
667        BindingStyle::SectionBinding => "section-binding",
668        BindingStyle::SectionRootBinding => "section-root-binding",
669    }
670}
671
672fn root_type_codegen_type_name(schema: &SchemaDocument) -> Option<&str> {
673    match &schema.node(schema.root).type_codegen {
674        TypeCodegen::None => None,
675        TypeCodegen::Record(codegen) => codegen.type_name.as_deref(),
676        TypeCodegen::Union(codegen) => codegen.type_name.as_deref(),
677    }
678}
679
680fn merge_root_type_name(
681    root_type_name: Option<&str>,
682    type_codegen_type_name: Option<&str>,
683) -> Result<Option<String>, WriteError> {
684    match (root_type_name, type_codegen_type_name) {
685        (Some(root), Some(ty)) if root != ty => Err(WriteError::InvalidIdentifier(format!(
686            "conflicting root $codegen type names: root={root}, type_codegen={ty}"
687        ))),
688        (Some(root), _) => Ok(Some(root.to_string())),
689        (None, Some(ty)) => Ok(Some(ty.to_string())),
690        (None, None) => Ok(None),
691    }
692}
693
694fn write_extension<F>(
695    c: &mut DocumentConstructor,
696    ident: Identifier,
697    writer: F,
698) -> Result<(), WriteError>
699where
700    F: FnOnce(&mut DocumentConstructor) -> Result<(), WriteError>,
701{
702    let scope = c.begin_scope();
703    c.navigate(PathSegment::Extension(ident))?;
704    writer(c)?;
705    c.end_scope(scope)?;
706    Ok(())
707}
708
709fn copy_subtree(
710    src_doc: &EureDocument,
711    src_node_id: NodeId,
712    c: &mut DocumentConstructor,
713    skip_variant_extension: bool,
714) -> Result<(), WriteError> {
715    let src_node = src_doc.node(src_node_id);
716
717    match &src_node.content {
718        NodeValue::Hole(label) => {
719            c.bind_hole(label.clone())?;
720        }
721        NodeValue::Primitive(prim) => {
722            c.bind_primitive(prim.clone())?;
723        }
724        NodeValue::Array(array) => {
725            c.bind_empty_array()?;
726            for &child_id in array.iter() {
727                let scope = c.begin_scope();
728                c.navigate(PathSegment::ArrayIndex(None))?;
729                copy_subtree(src_doc, child_id, c, skip_variant_extension)?;
730                c.end_scope(scope)?;
731            }
732        }
733        NodeValue::Tuple(tuple) => {
734            c.bind_empty_tuple()?;
735            for (index, &child_id) in tuple.iter().enumerate() {
736                let scope = c.begin_scope();
737                c.navigate(PathSegment::TupleIndex(index as u8))?;
738                copy_subtree(src_doc, child_id, c, skip_variant_extension)?;
739                c.end_scope(scope)?;
740            }
741        }
742        NodeValue::Map(map) => {
743            c.bind_empty_map()?;
744            for (key, &child_id) in map.iter() {
745                let scope = c.begin_scope();
746                c.navigate(PathSegment::Value(key.clone()))?;
747                copy_subtree(src_doc, child_id, c, skip_variant_extension)?;
748                c.end_scope(scope)?;
749            }
750        }
751        NodeValue::PartialMap(map) => {
752            c.bind_empty_partial_map()?;
753            for (key, &child_id) in map.iter() {
754                let scope = c.begin_scope();
755                c.navigate_partial_map_entry(key.clone())?;
756                copy_subtree(src_doc, child_id, c, skip_variant_extension)?;
757                c.end_scope(scope)?;
758            }
759        }
760    }
761
762    for (ident, &ext_node_id) in src_node.extensions.iter() {
763        if skip_variant_extension && ident == &VARIANT {
764            continue;
765        }
766        let scope = c.begin_scope();
767        c.navigate(PathSegment::Extension(ident.clone()))?;
768        copy_subtree(src_doc, ext_node_id, c, skip_variant_extension)?;
769        c.end_scope(scope)?;
770    }
771
772    Ok(())
773}
774
775fn literal_needs_variant(node: &eure_document::document::node::Node) -> bool {
776    match &node.content {
777        NodeValue::Primitive(PrimitiveValue::Text(t)) => {
778            t.language.is_implicit() || t.language.is_other("eure-path")
779        }
780        NodeValue::Primitive(_) => false,
781        NodeValue::Array(_)
782        | NodeValue::Tuple(_)
783        | NodeValue::Map(_)
784        | NodeValue::PartialMap(_) => true,
785        NodeValue::Hole(_) => true,
786    }
787}
788
789fn can_emit_as_single_inline_text(schema: &SchemaDocument, schema_id: SchemaNodeId) -> bool {
790    let schema_node = schema.node(schema_id);
791    if !schema_node.ext_types.is_empty() || schema_node.metadata != SchemaMetadata::default() {
792        return false;
793    }
794
795    match &schema_node.content {
796        SchemaNodeContent::Any
797        | SchemaNodeContent::Boolean
798        | SchemaNodeContent::Null
799        | SchemaNodeContent::Reference(_) => true,
800        SchemaNodeContent::Integer(s) => {
801            matches!(s.min, Bound::Unbounded)
802                && matches!(s.max, Bound::Unbounded)
803                && s.multiple_of.is_none()
804        }
805        SchemaNodeContent::Float(s) => {
806            matches!(s.min, Bound::Unbounded)
807                && matches!(s.max, Bound::Unbounded)
808                && s.multiple_of.is_none()
809                && matches!(s.precision, FloatPrecision::F64)
810        }
811        SchemaNodeContent::Text(s) => {
812            s.min_length.is_none()
813                && s.max_length.is_none()
814                && s.pattern.is_none()
815                && s.unknown_fields.is_empty()
816        }
817        _ => false,
818    }
819}
820
821fn format_bound_range<T>(
822    min: &Bound<T>,
823    max: &Bound<T>,
824    format_value: fn(&T) -> String,
825) -> Option<String> {
826    if matches!(min, Bound::Unbounded) && matches!(max, Bound::Unbounded) {
827        return None;
828    }
829
830    let left = match min {
831        Bound::Inclusive(_) => '[',
832        Bound::Exclusive(_) | Bound::Unbounded => '(',
833    };
834    let right = match max {
835        Bound::Inclusive(_) => ']',
836        Bound::Exclusive(_) | Bound::Unbounded => ')',
837    };
838
839    let min_str = match min {
840        Bound::Unbounded => String::new(),
841        Bound::Inclusive(v) | Bound::Exclusive(v) => format_value(v),
842    };
843    let max_str = match max {
844        Bound::Unbounded => String::new(),
845        Bound::Inclusive(v) | Bound::Exclusive(v) => format_value(v),
846    };
847
848    Some(format!("{left}{min_str}, {max_str}{right}"))
849}
850
851fn format_bigint(value: &BigInt) -> String {
852    value.to_string()
853}
854
855fn format_f64(value: &f64) -> String {
856    let s = value.to_string();
857    if !s.contains('.') && !s.contains('e') && !s.contains('E') {
858        format!("{s}.0")
859    } else {
860        s
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use super::*;
867    use crate::convert::document_to_schema;
868    use crate::interop::UnionInterop;
869    use crate::{
870        CodegenDefaults, FieldCodegen, RecordCodegen, RootCodegen, TextSchema, TypeCodegen,
871        UnknownFieldsPolicy,
872    };
873    use eure_document::document::node::NodeMap;
874    use eure_document::value::ObjectKey;
875
876    fn make_union_schema(repr: Option<VariantRepr>) -> SchemaDocument {
877        let mut schema = SchemaDocument::new();
878        let variant_node = schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
879        let mut variants = indexmap::IndexMap::new();
880        variants.insert("v".to_string(), variant_node);
881
882        schema.root = schema.create_node(SchemaNodeContent::Union(UnionSchema {
883            variants,
884            unambiguous: Default::default(),
885            interop: UnionInterop { variant_repr: repr },
886            deny_untagged: Default::default(),
887        }));
888        schema
889    }
890
891    #[test]
892    fn schema_to_document_delegates_to_into_eure_path() {
893        let schema = make_union_schema(Some(VariantRepr::Untagged));
894
895        let mut c = DocumentConstructor::new();
896        c.write(schema.clone()).expect("into-eure write");
897        let expected = c.finish();
898
899        let actual = schema_to_document(&schema).expect("schema_to_document");
900        assert_eq!(actual, expected);
901    }
902
903    #[test]
904    fn emits_union_repr_when_untagged_was_explicit() {
905        let schema = make_union_schema(Some(VariantRepr::Untagged));
906        let doc = schema_to_document(&schema).expect("schema emit");
907
908        let interop_id = doc
909            .root()
910            .extensions
911            .get(&IDENT_INTEROP)
912            .expect("interop extension should be emitted");
913        let interop_ctx = doc.parse_context(*interop_id);
914        let interop_rec = interop_ctx.parse_record().expect("interop record");
915        let repr_ctx = interop_rec
916            .field(VARIANT_REPR.as_ref())
917            .expect("variant-repr field");
918        let repr = repr_ctx.parse::<&str>().expect("repr parse");
919        assert_eq!(repr, "untagged");
920    }
921
922    #[test]
923    fn omits_union_repr_when_untagged_is_implicit() {
924        let schema = make_union_schema(None);
925        let doc = schema_to_document(&schema).expect("schema emit");
926
927        assert!(!doc.root().extensions.contains_key(&IDENT_INTEROP));
928    }
929
930    #[test]
931    fn array_shorthand_requires_single_inline_type_token() {
932        let mut inline_schema = SchemaDocument::new();
933        let int_id =
934            inline_schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
935        inline_schema.root = inline_schema.create_node(SchemaNodeContent::Array(ArraySchema {
936            item: int_id,
937            min_length: None,
938            max_length: None,
939            unique: false,
940            contains: None,
941            binding_style: None,
942        }));
943        let inline_doc = schema_to_document(&inline_schema).expect("inline array");
944        assert!(matches!(inline_doc.root().content, NodeValue::Array(_)));
945        assert!(!inline_doc.root().extensions.contains_key(&VARIANT));
946
947        let mut complex_schema = SchemaDocument::new();
948        let x_schema =
949            complex_schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
950        let item_id = complex_schema.create_node(SchemaNodeContent::Record(RecordSchema {
951            properties: indexmap::IndexMap::from([(
952                "x".to_string(),
953                RecordFieldSchema {
954                    schema: x_schema,
955                    optional: false,
956                    binding_style: None,
957                    field_codegen: Default::default(),
958                },
959            )]),
960            flatten: Vec::new(),
961            unknown_fields: UnknownFieldsPolicy::Deny,
962        }));
963        complex_schema.root = complex_schema.create_node(SchemaNodeContent::Array(ArraySchema {
964            item: item_id,
965            min_length: None,
966            max_length: None,
967            unique: false,
968            contains: None,
969            binding_style: None,
970        }));
971
972        let complex_doc = schema_to_document(&complex_schema).expect("complex array");
973        assert!(matches!(complex_doc.root().content, NodeValue::Map(_)));
974        let variant_id = complex_doc
975            .root()
976            .extensions
977            .get(&VARIANT)
978            .expect("non-inline array should emit explicit array variant");
979        let variant = complex_doc
980            .parse::<&str>(*variant_id)
981            .expect("variant parse");
982        assert_eq!(variant, "array");
983    }
984
985    #[test]
986    fn literal_preserves_extensions_except_variant() {
987        let mut literal = EureDocument::new();
988        let root_id = literal.get_root_id();
989        literal.node_mut(root_id).content = NodeValue::Map(NodeMap::default());
990
991        let child_id = literal
992            .add_map_child(ObjectKey::String("x".to_string()), root_id)
993            .expect("insert child")
994            .node_id;
995        literal.node_mut(child_id).content =
996            NodeValue::Primitive(PrimitiveValue::Integer(1.into()));
997
998        let root_variant_id = literal
999            .add_extension(VARIANT, root_id)
1000            .expect("root variant ext")
1001            .node_id;
1002        literal.node_mut(root_variant_id).content =
1003            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("old-root")));
1004
1005        let foo_ext_id = literal
1006            .add_extension("foo".parse().unwrap(), root_id)
1007            .expect("root foo ext")
1008            .node_id;
1009        literal.node_mut(foo_ext_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
1010
1011        let child_variant_id = literal
1012            .add_extension(VARIANT, child_id)
1013            .expect("child variant ext")
1014            .node_id;
1015        literal.node_mut(child_variant_id).content =
1016            NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext("old-child")));
1017
1018        let child_baz_id = literal
1019            .add_extension("baz".parse().unwrap(), child_id)
1020            .expect("child baz ext")
1021            .node_id;
1022        literal.node_mut(child_baz_id).content = NodeValue::Primitive(PrimitiveValue::Bool(true));
1023
1024        let mut schema = SchemaDocument::new();
1025        schema.root = schema.create_node(SchemaNodeContent::Literal(literal));
1026
1027        let doc = schema_to_document(&schema).expect("schema emit");
1028
1029        let root = doc.root();
1030        let variant_id = root
1031            .extensions
1032            .get(&VARIANT)
1033            .expect("literal map should emit $variant = literal");
1034        let root_variant = doc.parse::<&str>(*variant_id).expect("variant parse");
1035        assert_eq!(root_variant, "literal");
1036
1037        assert!(root.extensions.contains_key(&"foo".parse().unwrap()));
1038
1039        let root_map = match &root.content {
1040            NodeValue::Map(map) => map,
1041            other => panic!("expected map root, got {other:?}"),
1042        };
1043        let child = doc.node(*root_map.get(&ObjectKey::String("x".to_string())).unwrap());
1044        assert!(child.extensions.contains_key(&"baz".parse().unwrap()));
1045        assert!(!child.extensions.contains_key(&VARIANT));
1046    }
1047
1048    #[test]
1049    fn text_schema_uses_shorthand_when_compatible() {
1050        let mut schema = SchemaDocument::new();
1051        schema.root = schema.create_node(SchemaNodeContent::Text(TextSchema {
1052            language: Some("uuid".to_string()),
1053            min_length: None,
1054            max_length: None,
1055            pattern: None,
1056            unknown_fields: Default::default(),
1057        }));
1058
1059        let doc = schema_to_document(&schema).expect("schema emit");
1060        match &doc.root().content {
1061            NodeValue::Primitive(PrimitiveValue::Text(t)) => {
1062                assert!(t.language.is_implicit());
1063                assert_eq!(t.as_str(), "text.uuid");
1064            }
1065            other => panic!("expected shorthand text token, got {other:?}"),
1066        }
1067
1068        let mut schema_constrained = SchemaDocument::new();
1069        schema_constrained.root =
1070            schema_constrained.create_node(SchemaNodeContent::Text(TextSchema {
1071                language: None,
1072                min_length: Some(1),
1073                max_length: None,
1074                pattern: None,
1075                unknown_fields: Default::default(),
1076            }));
1077        let constrained_doc = schema_to_document(&schema_constrained).expect("schema emit");
1078        assert!(matches!(constrained_doc.root().content, NodeValue::Map(_)));
1079        let variant_id = constrained_doc
1080            .root()
1081            .extensions
1082            .get(&VARIANT)
1083            .expect("constrained text should emit explicit text variant");
1084        let variant = constrained_doc
1085            .parse::<&str>(*variant_id)
1086            .expect("variant parse");
1087        assert_eq!(variant, "text");
1088    }
1089
1090    #[test]
1091    fn roundtrips_root_type_and_field_codegen_metadata() {
1092        let mut schema = SchemaDocument::new();
1093        schema.root_codegen = RootCodegen {
1094            type_name: Some("User".to_string()),
1095        };
1096        schema.codegen_defaults = CodegenDefaults {
1097            derive: Some(vec!["Debug".to_string(), "Clone".to_string()]),
1098            ext_types_field_prefix: Some("ext_".to_string()),
1099            ext_types_type_prefix: Some("Ext".to_string()),
1100            document_node_id_field: Some("node_id".to_string()),
1101        };
1102
1103        let text_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1104        schema.root = schema.create_node(SchemaNodeContent::Record(RecordSchema {
1105            properties: indexmap::IndexMap::from([(
1106                "user-name".to_string(),
1107                RecordFieldSchema {
1108                    schema: text_id,
1109                    optional: false,
1110                    binding_style: None,
1111                    field_codegen: FieldCodegen {
1112                        name: Some("user_name".to_string()),
1113                    },
1114                },
1115            )]),
1116            flatten: Vec::new(),
1117            unknown_fields: UnknownFieldsPolicy::Deny,
1118        }));
1119        schema.node_mut(schema.root).type_codegen = TypeCodegen::Record(RecordCodegen {
1120            type_name: Some("User".to_string()),
1121            derive: Some(vec!["Debug".to_string()]),
1122        });
1123
1124        let doc = schema_to_document(&schema).expect("write schema");
1125        let (roundtrip, _) = document_to_schema(&doc).expect("parse schema");
1126
1127        assert_eq!(roundtrip.root_codegen.type_name.as_deref(), Some("User"));
1128        assert_eq!(
1129            roundtrip.codegen_defaults.document_node_id_field.as_deref(),
1130            Some("node_id")
1131        );
1132        let TypeCodegen::Record(record_codegen) = &roundtrip.node(roundtrip.root).type_codegen
1133        else {
1134            panic!("expected record codegen")
1135        };
1136        assert_eq!(record_codegen.type_name.as_deref(), Some("User"));
1137        let record = match &roundtrip.node(roundtrip.root).content {
1138            SchemaNodeContent::Record(record) => record,
1139            _ => panic!("expected record root"),
1140        };
1141        assert_eq!(
1142            record.properties["user-name"].field_codegen.name.as_deref(),
1143            Some("user_name")
1144        );
1145    }
1146
1147    #[test]
1148    fn rejects_conflicting_root_codegen_type_names() {
1149        let mut schema = SchemaDocument::new();
1150        schema.root_codegen = RootCodegen {
1151            type_name: Some("Root".to_string()),
1152        };
1153        schema.root = schema.create_node(SchemaNodeContent::Record(RecordSchema::default()));
1154        schema.node_mut(schema.root).type_codegen = TypeCodegen::Record(RecordCodegen {
1155            type_name: Some("User".to_string()),
1156            derive: None,
1157        });
1158
1159        let error = schema_to_document(&schema).expect_err("conflict must be rejected");
1160        assert!(matches!(
1161            error,
1162            SchemaWriteError::ConflictingRootCodegenTypeName { .. }
1163        ));
1164    }
1165}