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::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#[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
58pub 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
67pub fn schema_to_source_document(
73 _schema: &SchemaDocument,
74 plan: LayoutPlan,
75) -> Result<SourceDocument, SchemaWriteError> {
76 Ok(plan.emit())
77}
78
79pub 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}