1use 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#[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
56pub 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
65pub 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}