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) => 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 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 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}