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