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