1use crate::json_schema::*;
8use eure_document::data_model::VariantRepr;
9use eure_document::document::EureDocument;
10use eure_json::Config as JsonConfig;
11use eure_schema::{
12 ArraySchema as EureArraySchema, Bound, Description, FloatSchema,
13 IntegerSchema as EureIntegerSchema, MapSchema, RecordSchema, SchemaDocument,
14 SchemaMetadata as EureMetadata, SchemaNode, SchemaNodeContent, SchemaNodeId, TextSchema,
15 TupleSchema, UnionSchema, UnknownFieldsPolicy,
16};
17use indexmap::IndexMap;
18use num_traits::ToPrimitive;
19
20fn document_to_json(doc: &EureDocument) -> Result<serde_json::Value, ConversionError> {
22 Ok(eure_json::document_to_value(doc, &JsonConfig::default())?)
23}
24
25#[derive(Debug, Clone, PartialEq, thiserror::Error)]
27pub enum ConversionError {
28 #[error("Eure Hole type cannot be represented in JSON Schema")]
30 HoleNotSupported,
31
32 #[error("Eure Hole in literal value cannot be represented in JSON Schema")]
34 HoleInLiteral,
35
36 #[error("Eure Map with non-string keys cannot be represented in JSON Schema")]
38 NonStringMapKeysNotSupported,
39
40 #[error("BigInt value {0} is out of range for JSON Schema i64")]
42 BigIntOutOfRange(String),
43
44 #[error("Invalid float value: {0}")]
46 InvalidFloatValue(String),
47
48 #[error("Invalid schema node reference: {0}")]
50 InvalidNodeReference(usize),
51
52 #[error("Circular reference detected: {0}")]
54 CircularReference(String),
55
56 #[error(transparent)]
58 JsonConversion(#[from] eure_json::EureToJsonError),
59
60 #[error("Invalid default value: expected {expected}, got {actual}")]
62 InvalidDefaultValue {
63 expected: &'static str,
64 actual: String,
65 },
66}
67
68struct ConversionContext<'a> {
70 document: &'a SchemaDocument,
72 visiting: Vec<SchemaNodeId>,
74}
75
76impl<'a> ConversionContext<'a> {
77 fn new(document: &'a SchemaDocument) -> Self {
78 Self {
79 document,
80 visiting: Vec::new(),
81 }
82 }
83
84 fn get_node(&self, id: SchemaNodeId) -> Result<&SchemaNode, ConversionError> {
86 self.document
87 .nodes
88 .get(id.0)
89 .ok_or(ConversionError::InvalidNodeReference(id.0))
90 }
91
92 fn push_visiting(&mut self, id: SchemaNodeId) -> Result<(), ConversionError> {
94 if self.visiting.contains(&id) {
95 return Err(ConversionError::CircularReference(format!(
96 "Node {} creates a cycle",
97 id.0
98 )));
99 }
100 self.visiting.push(id);
101 Ok(())
102 }
103
104 fn pop_visiting(&mut self) {
106 self.visiting.pop();
107 }
108}
109
110pub fn eure_to_json_schema(doc: &SchemaDocument) -> Result<JsonSchema, ConversionError> {
115 let mut ctx = ConversionContext::new(doc);
116
117 let root_schema = convert_node(&mut ctx, doc.root)?;
119
120 if !doc.types.is_empty() {
122 let mut defs = IndexMap::new();
123
124 for (name, node_id) in &doc.types {
125 let converted = convert_node(&mut ctx, *node_id)?;
126 defs.insert(name.to_string(), converted);
127 }
128
129 Ok(wrap_with_definitions(root_schema, defs))
131 } else {
132 Ok(root_schema)
133 }
134}
135
136fn wrap_with_definitions(root: JsonSchema, defs: IndexMap<String, JsonSchema>) -> JsonSchema {
138 if let JsonSchema::Generic(mut generic) = root {
140 generic.defs = Some(defs);
141 JsonSchema::Generic(generic)
142 } else {
143 JsonSchema::AllOf(AllOfSchema {
145 schemas: vec![
146 root,
147 JsonSchema::Generic(GenericSchema {
148 defs: Some(defs),
149 ..Default::default()
150 }),
151 ],
152 metadata: SchemaMetadata::default(),
153 })
154 }
155}
156
157fn convert_node(
159 ctx: &mut ConversionContext,
160 id: SchemaNodeId,
161) -> Result<JsonSchema, ConversionError> {
162 ctx.push_visiting(id)?;
163
164 let node = ctx.get_node(id)?;
166 let content = node.content.clone();
167 let metadata = node.metadata.clone();
168
169 let result = convert_schema_content(ctx, &content, &metadata)?;
170
171 ctx.pop_visiting();
172 Ok(result)
173}
174
175fn convert_schema_content(
177 ctx: &mut ConversionContext,
178 content: &SchemaNodeContent,
179 eure_meta: &EureMetadata,
180) -> Result<JsonSchema, ConversionError> {
181 let json_metadata = convert_metadata(eure_meta)?;
182
183 match content {
184 SchemaNodeContent::Any => Ok(JsonSchema::Generic(GenericSchema {
185 metadata: json_metadata,
186 ..Default::default()
187 })),
188
189 SchemaNodeContent::Text(t) => convert_text_schema(t, eure_meta, json_metadata),
190
191 SchemaNodeContent::Integer(i) => convert_integer_schema(i, eure_meta, json_metadata),
192
193 SchemaNodeContent::Float(f) => convert_float_schema(f, eure_meta, json_metadata),
194
195 SchemaNodeContent::Boolean => convert_boolean_schema(eure_meta, json_metadata),
196
197 SchemaNodeContent::Null => Ok(JsonSchema::Typed(TypedSchema::Null(NullSchema {
198 metadata: json_metadata,
199 }))),
200
201 SchemaNodeContent::Array(a) => convert_array_schema(ctx, a, json_metadata),
202
203 SchemaNodeContent::Map(m) => convert_map_schema(ctx, m, json_metadata),
204
205 SchemaNodeContent::Record(r) => convert_record_schema(ctx, r, json_metadata),
206
207 SchemaNodeContent::Tuple(t) => convert_tuple_schema(ctx, t, json_metadata),
208
209 SchemaNodeContent::Union(u) => convert_union_schema(ctx, u, json_metadata),
210
211 SchemaNodeContent::Reference(ref_type) => {
212 Ok(JsonSchema::Reference(ReferenceSchema {
214 reference: format!("#/$defs/{}", ref_type.name),
215 metadata: json_metadata,
216 }))
217 }
218
219 SchemaNodeContent::Literal(val) => Ok(JsonSchema::Const(ConstSchema {
220 value: document_to_json(val)?,
221 metadata: json_metadata,
222 })),
223 }
224}
225
226fn convert_metadata(eure_meta: &EureMetadata) -> Result<SchemaMetadata, ConversionError> {
228 let examples = eure_meta
229 .examples
230 .as_ref()
231 .map(|examples| {
232 examples
233 .iter()
234 .map(document_to_json)
235 .collect::<Result<Vec<_>, _>>()
236 })
237 .transpose()?;
238
239 Ok(SchemaMetadata {
240 title: None, description: eure_meta.description.as_ref().map(|d| match d {
242 Description::String(s) => s.clone(),
243 Description::Markdown(s) => s.clone(),
244 }),
245 deprecated: if eure_meta.deprecated {
246 Some(true)
247 } else {
248 None
249 },
250 examples,
251 })
252}
253
254const JSON_SCHEMA_FORMATS: &[&str] = &[
256 "date-time",
257 "date",
258 "time",
259 "duration",
260 "email",
261 "idn-email",
262 "hostname",
263 "idn-hostname",
264 "ipv4",
265 "ipv6",
266 "uri",
267 "uri-reference",
268 "iri",
269 "iri-reference",
270 "uuid",
271 "uri-template",
272 "json-pointer",
273 "relative-json-pointer",
274 "regex",
275];
276
277fn convert_text_schema(
282 eure: &TextSchema,
283 eure_meta: &EureMetadata,
284 metadata: SchemaMetadata,
285) -> Result<JsonSchema, ConversionError> {
286 let format = eure.language.as_ref().and_then(|lang| {
288 if JSON_SCHEMA_FORMATS.contains(&lang.as_str()) {
289 Some(lang.clone())
290 } else {
291 None
292 }
293 });
294
295 let default = eure_meta
297 .default
298 .as_ref()
299 .map(|doc| {
300 let json_val = document_to_json(doc)?;
301 match json_val {
302 serde_json::Value::String(s) => Ok(s),
303 other => Err(ConversionError::InvalidDefaultValue {
304 expected: "string",
305 actual: format!("{:?}", other),
306 }),
307 }
308 })
309 .transpose()?;
310
311 Ok(JsonSchema::Typed(TypedSchema::String(StringSchema {
312 min_length: eure.min_length,
313 max_length: eure.max_length,
314 pattern: eure.pattern.as_ref().map(|r| r.as_str().to_string()),
315 format,
316 default,
317 metadata,
318 })))
319}
320
321fn convert_integer_schema(
323 eure: &EureIntegerSchema,
324 eure_meta: &EureMetadata,
325 metadata: SchemaMetadata,
326) -> Result<JsonSchema, ConversionError> {
327 let (minimum, exclusive_minimum) = match &eure.min {
329 Bound::Unbounded => (None, None),
330 Bound::Inclusive(val) => (Some(bigint_to_i64(val)?), None),
331 Bound::Exclusive(val) => (None, Some(bigint_to_i64(val)?)),
332 };
333
334 let (maximum, exclusive_maximum) = match &eure.max {
335 Bound::Unbounded => (None, None),
336 Bound::Inclusive(val) => (Some(bigint_to_i64(val)?), None),
337 Bound::Exclusive(val) => (None, Some(bigint_to_i64(val)?)),
338 };
339
340 let multiple_of = eure.multiple_of.as_ref().map(bigint_to_i64).transpose()?;
341
342 let default = eure_meta
344 .default
345 .as_ref()
346 .map(|doc| {
347 let json_val = document_to_json(doc)?;
348 match json_val {
349 serde_json::Value::Number(n) if n.is_i64() => Ok(n.as_i64().unwrap()),
350 other => Err(ConversionError::InvalidDefaultValue {
351 expected: "integer",
352 actual: format!("{:?}", other),
353 }),
354 }
355 })
356 .transpose()?;
357
358 Ok(JsonSchema::Typed(TypedSchema::Integer(IntegerSchema {
359 minimum,
360 maximum,
361 exclusive_minimum,
362 exclusive_maximum,
363 multiple_of,
364 default,
365 metadata,
366 })))
367}
368
369fn bigint_to_i64(val: &num_bigint::BigInt) -> Result<i64, ConversionError> {
371 val.to_i64()
372 .ok_or_else(|| ConversionError::BigIntOutOfRange(val.to_string()))
373}
374
375fn convert_float_schema(
377 eure: &FloatSchema,
378 eure_meta: &EureMetadata,
379 metadata: SchemaMetadata,
380) -> Result<JsonSchema, ConversionError> {
381 let validate_float = |f: f64| -> Result<f64, ConversionError> {
383 if f.is_nan() || f.is_infinite() {
384 Err(ConversionError::InvalidFloatValue(f.to_string()))
385 } else {
386 Ok(f)
387 }
388 };
389
390 let (minimum, exclusive_minimum) = match &eure.min {
392 Bound::Unbounded => (None, None),
393 Bound::Inclusive(val) => (Some(validate_float(*val)?), None),
394 Bound::Exclusive(val) => (None, Some(validate_float(*val)?)),
395 };
396
397 let (maximum, exclusive_maximum) = match &eure.max {
398 Bound::Unbounded => (None, None),
399 Bound::Inclusive(val) => (Some(validate_float(*val)?), None),
400 Bound::Exclusive(val) => (None, Some(validate_float(*val)?)),
401 };
402
403 let multiple_of = eure.multiple_of.map(validate_float).transpose()?;
404
405 let default = eure_meta
407 .default
408 .as_ref()
409 .map(|doc| {
410 let json_val = document_to_json(doc)?;
411 match json_val {
412 serde_json::Value::Number(n) => n
413 .as_f64()
414 .ok_or_else(|| ConversionError::InvalidDefaultValue {
415 expected: "number",
416 actual: format!("{:?}", n),
417 })
418 .and_then(validate_float),
419 other => Err(ConversionError::InvalidDefaultValue {
420 expected: "number",
421 actual: format!("{:?}", other),
422 }),
423 }
424 })
425 .transpose()?;
426
427 Ok(JsonSchema::Typed(TypedSchema::Number(NumberSchema {
428 minimum,
429 maximum,
430 exclusive_minimum,
431 exclusive_maximum,
432 multiple_of,
433 default,
434 metadata,
435 })))
436}
437
438fn convert_boolean_schema(
440 eure_meta: &EureMetadata,
441 metadata: SchemaMetadata,
442) -> Result<JsonSchema, ConversionError> {
443 let default = eure_meta
445 .default
446 .as_ref()
447 .map(|doc| {
448 let json_val = document_to_json(doc)?;
449 match json_val {
450 serde_json::Value::Bool(b) => Ok(b),
451 other => Err(ConversionError::InvalidDefaultValue {
452 expected: "boolean",
453 actual: format!("{:?}", other),
454 }),
455 }
456 })
457 .transpose()?;
458
459 Ok(JsonSchema::Typed(TypedSchema::Boolean(BooleanSchema {
460 default,
461 metadata,
462 })))
463}
464
465fn convert_array_schema(
467 ctx: &mut ConversionContext,
468 eure: &EureArraySchema,
469 metadata: SchemaMetadata,
470) -> Result<JsonSchema, ConversionError> {
471 let items = Some(Box::new(convert_node(ctx, eure.item)?));
472
473 let contains = if let Some(contains_id) = &eure.contains {
474 Some(Box::new(convert_node(ctx, *contains_id)?))
476 } else {
477 None
478 };
479
480 Ok(JsonSchema::Typed(TypedSchema::Array(ArraySchema {
481 items,
482 prefix_items: None, min_items: eure.min_length,
484 max_items: eure.max_length,
485 unique_items: if eure.unique { Some(true) } else { None },
486 contains,
487 metadata,
488 })))
489}
490
491fn convert_map_schema(
496 ctx: &mut ConversionContext,
497 eure: &MapSchema,
498 metadata: SchemaMetadata,
499) -> Result<JsonSchema, ConversionError> {
500 let key_node = ctx.get_node(eure.key)?;
502 if !matches!(key_node.content, SchemaNodeContent::Text(_)) {
503 return Err(ConversionError::NonStringMapKeysNotSupported);
504 }
505
506 let value_schema = convert_node(ctx, eure.value)?;
508
509 Ok(JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
511 properties: None,
512 required: None,
513 additional_properties: Some(AdditionalProperties::Schema(Box::new(value_schema))),
514 metadata,
515 })))
516}
517
518fn convert_record_schema(
520 ctx: &mut ConversionContext,
521 eure: &RecordSchema,
522 metadata: SchemaMetadata,
523) -> Result<JsonSchema, ConversionError> {
524 let mut properties = IndexMap::new();
525 let mut required = Vec::new();
526
527 for (field_name, field) in &eure.properties {
528 let is_optional = field.optional;
529 let field_schema = convert_node(ctx, field.schema)?;
530
531 properties.insert(field_name.clone(), field_schema);
532
533 if !is_optional {
535 required.push(field_name.clone());
536 }
537 }
538
539 let additional_properties = match &eure.unknown_fields {
540 UnknownFieldsPolicy::Deny => Some(AdditionalProperties::Bool(false)),
541 UnknownFieldsPolicy::Allow => Some(AdditionalProperties::Bool(true)),
542 UnknownFieldsPolicy::Schema(node_id) => {
543 let schema = convert_node(ctx, *node_id)?;
544 Some(AdditionalProperties::Schema(Box::new(schema)))
545 }
546 };
547
548 let properties = if properties.is_empty() {
549 None
550 } else {
551 Some(properties)
552 };
553
554 let required = if required.is_empty() {
555 None
556 } else {
557 Some(required)
558 };
559
560 Ok(JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
561 properties,
562 required,
563 additional_properties,
564 metadata,
565 })))
566}
567
568fn convert_tuple_schema(
573 ctx: &mut ConversionContext,
574 eure: &TupleSchema,
575 metadata: SchemaMetadata,
576) -> Result<JsonSchema, ConversionError> {
577 let prefix_items: Vec<JsonSchema> = eure
579 .elements
580 .iter()
581 .map(|node_id| convert_node(ctx, *node_id))
582 .collect::<Result<Vec<_>, _>>()?;
583
584 Ok(JsonSchema::Typed(TypedSchema::Array(ArraySchema {
587 items: Some(Box::new(JsonSchema::Boolean(false))),
588 prefix_items: if prefix_items.is_empty() {
589 None
590 } else {
591 Some(prefix_items)
592 },
593 min_items: Some(eure.elements.len() as u32),
594 max_items: Some(eure.elements.len() as u32),
595 unique_items: None,
596 contains: None,
597 metadata,
598 })))
599}
600
601fn convert_union_schema(
609 ctx: &mut ConversionContext,
610 eure: &UnionSchema,
611 metadata: SchemaMetadata,
612) -> Result<JsonSchema, ConversionError> {
613 match &eure.repr {
614 VariantRepr::External => convert_external_variant(ctx, eure, metadata),
615 VariantRepr::Internal { tag } => convert_internal_variant(ctx, eure, tag, metadata),
616 VariantRepr::Adjacent { tag, content } => {
617 convert_adjacent_variant(ctx, eure, tag, content, metadata)
618 }
619 VariantRepr::Untagged => convert_untagged_variant(ctx, eure, metadata),
620 }
621}
622
623fn convert_external_variant(
625 ctx: &mut ConversionContext,
626 eure: &UnionSchema,
627 metadata: SchemaMetadata,
628) -> Result<JsonSchema, ConversionError> {
629 let mut schemas = Vec::new();
630
631 for (variant_name, node_id) in &eure.variants {
632 let variant_schema = convert_node(ctx, *node_id)?;
633
634 let mut properties = IndexMap::new();
636 properties.insert(variant_name.clone(), variant_schema);
637
638 let obj = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
639 properties: Some(properties),
640 required: Some(vec![variant_name.clone()]),
641 additional_properties: Some(AdditionalProperties::Bool(false)),
642 metadata: SchemaMetadata::default(),
643 }));
644
645 schemas.push(obj);
646 }
647
648 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
649}
650
651fn convert_internal_variant(
653 ctx: &mut ConversionContext,
654 eure: &UnionSchema,
655 tag: &str,
656 metadata: SchemaMetadata,
657) -> Result<JsonSchema, ConversionError> {
658 let mut schemas = Vec::new();
659
660 for (variant_name, node_id) in &eure.variants {
661 let variant_schema = convert_node(ctx, *node_id)?;
662
663 let tag_schema = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
665 properties: Some({
666 let mut props = IndexMap::new();
667 props.insert(
668 tag.to_string(),
669 JsonSchema::Const(ConstSchema {
670 value: serde_json::Value::String(variant_name.clone()),
671 metadata: SchemaMetadata::default(),
672 }),
673 );
674 props
675 }),
676 required: Some(vec![tag.to_string()]),
677 additional_properties: None,
678 metadata: SchemaMetadata::default(),
679 }));
680
681 let combined = JsonSchema::AllOf(AllOfSchema {
682 schemas: vec![tag_schema, variant_schema],
683 metadata: SchemaMetadata::default(),
684 });
685
686 schemas.push(combined);
687 }
688
689 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
690}
691
692fn convert_adjacent_variant(
694 ctx: &mut ConversionContext,
695 eure: &UnionSchema,
696 tag: &str,
697 content: &str,
698 metadata: SchemaMetadata,
699) -> Result<JsonSchema, ConversionError> {
700 let mut schemas = Vec::new();
701
702 for (variant_name, node_id) in &eure.variants {
703 let variant_schema = convert_node(ctx, *node_id)?;
704
705 let mut properties = IndexMap::new();
707 properties.insert(
708 tag.to_string(),
709 JsonSchema::Const(ConstSchema {
710 value: serde_json::Value::String(variant_name.clone()),
711 metadata: SchemaMetadata::default(),
712 }),
713 );
714 properties.insert(content.to_string(), variant_schema);
715
716 let obj = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
717 properties: Some(properties),
718 required: Some(vec![tag.to_string(), content.to_string()]),
719 additional_properties: Some(AdditionalProperties::Bool(false)),
720 metadata: SchemaMetadata::default(),
721 }));
722
723 schemas.push(obj);
724 }
725
726 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
727}
728
729fn convert_untagged_variant(
731 ctx: &mut ConversionContext,
732 eure: &UnionSchema,
733 metadata: SchemaMetadata,
734) -> Result<JsonSchema, ConversionError> {
735 let mut schemas = Vec::new();
736
737 for node_id in eure.variants.values() {
738 let variant_schema = convert_node(ctx, *node_id)?;
739 schemas.push(variant_schema);
740 }
741
742 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use eure_document::data_model::VariantRepr;
749 use eure_schema::{
750 Bound, IntegerSchema as EureIntegerSchema, RecordFieldSchema, RecordSchema, SchemaDocument,
751 SchemaNodeContent, UnknownFieldsPolicy,
752 };
753
754 #[test]
755 fn test_convert_simple_text() {
756 let mut doc = SchemaDocument::new();
757 doc.root = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
758
759 let result = eure_to_json_schema(&doc).unwrap();
760 assert!(matches!(result, JsonSchema::Typed(TypedSchema::String(_))));
761 }
762
763 #[test]
764 fn test_convert_text_with_language() {
765 let mut doc = SchemaDocument::new();
767 doc.root = doc.create_node(SchemaNodeContent::Text(TextSchema {
768 language: Some("rust".to_string()),
769 ..Default::default()
770 }));
771
772 let result = eure_to_json_schema(&doc).unwrap();
773 assert!(matches!(result, JsonSchema::Typed(TypedSchema::String(_))));
774 }
775
776 #[test]
777 fn test_convert_integer_with_bounds() {
778 let mut doc = SchemaDocument::new();
779 doc.root = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema {
780 min: Bound::Inclusive(0.into()),
781 max: Bound::Exclusive(100.into()),
782 multiple_of: None,
783 }));
784
785 let result = eure_to_json_schema(&doc).unwrap();
786 match result {
787 JsonSchema::Typed(TypedSchema::Integer(schema)) => {
788 assert_eq!(schema.minimum, Some(0));
789 assert_eq!(schema.exclusive_maximum, Some(100));
790 }
791 _ => panic!("Expected Integer schema"),
792 }
793 }
794
795 #[test]
796 fn test_convert_record_to_object() {
797 let mut doc = SchemaDocument::new();
798
799 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
800 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
801
802 let mut properties = IndexMap::new();
803 properties.insert(
804 "name".to_string(),
805 RecordFieldSchema {
806 schema: text_id,
807 optional: false,
808 binding_style: None,
809 },
810 );
811 properties.insert(
812 "age".to_string(),
813 RecordFieldSchema {
814 schema: int_id,
815 optional: false,
816 binding_style: None,
817 },
818 );
819
820 doc.root = doc.create_node(SchemaNodeContent::Record(RecordSchema {
821 properties,
822 unknown_fields: UnknownFieldsPolicy::Deny,
823 }));
824
825 let result = eure_to_json_schema(&doc).unwrap();
826 match result {
827 JsonSchema::Typed(TypedSchema::Object(schema)) => {
828 assert!(schema.properties.is_some());
829 let props = schema.properties.unwrap();
830 assert_eq!(props.len(), 2);
831 assert!(props.contains_key("name"));
832 assert!(props.contains_key("age"));
833 }
834 _ => panic!("Expected Object schema"),
835 }
836 }
837
838 #[test]
839 fn test_convert_untagged_union_to_oneof() {
840 let mut doc = SchemaDocument::new();
841
842 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
843 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
844
845 let mut variants = IndexMap::new();
846 variants.insert("TextVariant".to_string(), text_id);
847 variants.insert("IntVariant".to_string(), int_id);
848
849 doc.root = doc.create_node(SchemaNodeContent::Union(UnionSchema {
850 variants,
851 unambiguous: Default::default(),
852 repr: VariantRepr::Untagged,
853 deny_untagged: Default::default(),
854 }));
855
856 let result = eure_to_json_schema(&doc).unwrap();
857 match result {
858 JsonSchema::OneOf(schema) => {
859 assert_eq!(schema.schemas.len(), 2);
860 }
861 _ => panic!("Expected OneOf schema for untagged union"),
862 }
863 }
864
865 #[test]
866 fn test_convert_external_union_to_oneof() {
867 let mut doc = SchemaDocument::new();
868
869 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
870 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
871
872 let mut variants = IndexMap::new();
873 variants.insert("A".to_string(), text_id);
874 variants.insert("B".to_string(), int_id);
875
876 doc.root = doc.create_node(SchemaNodeContent::Union(UnionSchema {
877 variants,
878 unambiguous: Default::default(),
879 repr: VariantRepr::External,
880 deny_untagged: Default::default(),
881 }));
882
883 let result = eure_to_json_schema(&doc).unwrap();
884 match result {
885 JsonSchema::OneOf(schema) => {
886 assert_eq!(schema.schemas.len(), 2);
887 }
889 _ => panic!("Expected OneOf schema for external union"),
890 }
891 }
892}