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 base_schema = convert_record_properties(ctx, eure, metadata.clone())?;
525
526 if eure.flatten.is_empty() {
528 return Ok(base_schema);
529 }
530
531 let mut all_of_schemas = vec![base_schema];
533
534 for &flatten_id in &eure.flatten {
535 let flatten_schema = convert_node(ctx, flatten_id)?;
536 all_of_schemas.push(flatten_schema);
537 }
538
539 Ok(JsonSchema::AllOf(AllOfSchema {
540 schemas: all_of_schemas,
541 metadata,
542 }))
543}
544
545fn convert_record_properties(
554 ctx: &mut ConversionContext,
555 eure: &RecordSchema,
556 metadata: SchemaMetadata,
557) -> Result<JsonSchema, ConversionError> {
558 let mut properties = IndexMap::new();
559 let mut required = Vec::new();
560
561 for (field_name, field) in &eure.properties {
562 let is_optional = field.optional;
563 let field_schema = convert_node(ctx, field.schema)?;
564
565 properties.insert(field_name.clone(), field_schema);
566
567 if !is_optional {
569 required.push(field_name.clone());
570 }
571 }
572
573 let additional_properties = match &eure.unknown_fields {
574 UnknownFieldsPolicy::Deny => Some(AdditionalProperties::Bool(false)),
575 UnknownFieldsPolicy::Allow => Some(AdditionalProperties::Bool(true)),
576 UnknownFieldsPolicy::Schema(node_id) => {
577 let schema = convert_node(ctx, *node_id)?;
578 Some(AdditionalProperties::Schema(Box::new(schema)))
579 }
580 };
581
582 let properties = if properties.is_empty() {
583 None
584 } else {
585 Some(properties)
586 };
587
588 let required = if required.is_empty() {
589 None
590 } else {
591 Some(required)
592 };
593
594 Ok(JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
595 properties,
596 required,
597 additional_properties,
598 metadata,
599 })))
600}
601
602fn convert_tuple_schema(
607 ctx: &mut ConversionContext,
608 eure: &TupleSchema,
609 metadata: SchemaMetadata,
610) -> Result<JsonSchema, ConversionError> {
611 let prefix_items: Vec<JsonSchema> = eure
613 .elements
614 .iter()
615 .map(|node_id| convert_node(ctx, *node_id))
616 .collect::<Result<Vec<_>, _>>()?;
617
618 Ok(JsonSchema::Typed(TypedSchema::Array(ArraySchema {
621 items: Some(Box::new(JsonSchema::Boolean(false))),
622 prefix_items: if prefix_items.is_empty() {
623 None
624 } else {
625 Some(prefix_items)
626 },
627 min_items: Some(eure.elements.len() as u32),
628 max_items: Some(eure.elements.len() as u32),
629 unique_items: None,
630 contains: None,
631 metadata,
632 })))
633}
634
635fn convert_union_schema(
643 ctx: &mut ConversionContext,
644 eure: &UnionSchema,
645 metadata: SchemaMetadata,
646) -> Result<JsonSchema, ConversionError> {
647 match &eure.repr {
648 VariantRepr::External => convert_external_variant(ctx, eure, metadata),
649 VariantRepr::Internal { tag } => convert_internal_variant(ctx, eure, tag, metadata),
650 VariantRepr::Adjacent { tag, content } => {
651 convert_adjacent_variant(ctx, eure, tag, content, metadata)
652 }
653 VariantRepr::Untagged => convert_untagged_variant(ctx, eure, metadata),
654 }
655}
656
657fn convert_external_variant(
659 ctx: &mut ConversionContext,
660 eure: &UnionSchema,
661 metadata: SchemaMetadata,
662) -> Result<JsonSchema, ConversionError> {
663 let mut schemas = Vec::new();
664
665 for (variant_name, node_id) in &eure.variants {
666 let variant_schema = convert_node(ctx, *node_id)?;
667
668 let mut properties = IndexMap::new();
670 properties.insert(variant_name.clone(), variant_schema);
671
672 let obj = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
673 properties: Some(properties),
674 required: Some(vec![variant_name.clone()]),
675 additional_properties: Some(AdditionalProperties::Bool(false)),
676 metadata: SchemaMetadata::default(),
677 }));
678
679 schemas.push(obj);
680 }
681
682 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
683}
684
685fn convert_internal_variant(
687 ctx: &mut ConversionContext,
688 eure: &UnionSchema,
689 tag: &str,
690 metadata: SchemaMetadata,
691) -> Result<JsonSchema, ConversionError> {
692 let mut schemas = Vec::new();
693
694 for (variant_name, node_id) in &eure.variants {
695 let variant_schema = convert_node(ctx, *node_id)?;
696
697 let tag_schema = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
699 properties: Some({
700 let mut props = IndexMap::new();
701 props.insert(
702 tag.to_string(),
703 JsonSchema::Const(ConstSchema {
704 value: serde_json::Value::String(variant_name.clone()),
705 metadata: SchemaMetadata::default(),
706 }),
707 );
708 props
709 }),
710 required: Some(vec![tag.to_string()]),
711 additional_properties: None,
712 metadata: SchemaMetadata::default(),
713 }));
714
715 let combined = JsonSchema::AllOf(AllOfSchema {
716 schemas: vec![tag_schema, variant_schema],
717 metadata: SchemaMetadata::default(),
718 });
719
720 schemas.push(combined);
721 }
722
723 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
724}
725
726fn convert_adjacent_variant(
728 ctx: &mut ConversionContext,
729 eure: &UnionSchema,
730 tag: &str,
731 content: &str,
732 metadata: SchemaMetadata,
733) -> Result<JsonSchema, ConversionError> {
734 let mut schemas = Vec::new();
735
736 for (variant_name, node_id) in &eure.variants {
737 let variant_schema = convert_node(ctx, *node_id)?;
738
739 let mut properties = IndexMap::new();
741 properties.insert(
742 tag.to_string(),
743 JsonSchema::Const(ConstSchema {
744 value: serde_json::Value::String(variant_name.clone()),
745 metadata: SchemaMetadata::default(),
746 }),
747 );
748 properties.insert(content.to_string(), variant_schema);
749
750 let obj = JsonSchema::Typed(TypedSchema::Object(ObjectSchema {
751 properties: Some(properties),
752 required: Some(vec![tag.to_string(), content.to_string()]),
753 additional_properties: Some(AdditionalProperties::Bool(false)),
754 metadata: SchemaMetadata::default(),
755 }));
756
757 schemas.push(obj);
758 }
759
760 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
761}
762
763fn convert_untagged_variant(
765 ctx: &mut ConversionContext,
766 eure: &UnionSchema,
767 metadata: SchemaMetadata,
768) -> Result<JsonSchema, ConversionError> {
769 let mut schemas = Vec::new();
770
771 for node_id in eure.variants.values() {
772 let variant_schema = convert_node(ctx, *node_id)?;
773 schemas.push(variant_schema);
774 }
775
776 Ok(JsonSchema::OneOf(OneOfSchema { schemas, metadata }))
777}
778
779#[cfg(test)]
780mod tests {
781 use super::*;
782 use eure_document::data_model::VariantRepr;
783 use eure_schema::{
784 Bound, IntegerSchema as EureIntegerSchema, RecordFieldSchema, RecordSchema, SchemaDocument,
785 SchemaNodeContent, UnknownFieldsPolicy,
786 };
787
788 #[test]
789 fn test_convert_simple_text() {
790 let mut doc = SchemaDocument::new();
791 doc.root = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
792
793 let result = eure_to_json_schema(&doc).unwrap();
794 assert!(matches!(result, JsonSchema::Typed(TypedSchema::String(_))));
795 }
796
797 #[test]
798 fn test_convert_text_with_language() {
799 let mut doc = SchemaDocument::new();
801 doc.root = doc.create_node(SchemaNodeContent::Text(TextSchema {
802 language: Some("rust".to_string()),
803 ..Default::default()
804 }));
805
806 let result = eure_to_json_schema(&doc).unwrap();
807 assert!(matches!(result, JsonSchema::Typed(TypedSchema::String(_))));
808 }
809
810 #[test]
811 fn test_convert_integer_with_bounds() {
812 let mut doc = SchemaDocument::new();
813 doc.root = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema {
814 min: Bound::Inclusive(0.into()),
815 max: Bound::Exclusive(100.into()),
816 multiple_of: None,
817 }));
818
819 let result = eure_to_json_schema(&doc).unwrap();
820 match result {
821 JsonSchema::Typed(TypedSchema::Integer(schema)) => {
822 assert_eq!(schema.minimum, Some(0));
823 assert_eq!(schema.exclusive_maximum, Some(100));
824 }
825 _ => panic!("Expected Integer schema"),
826 }
827 }
828
829 #[test]
830 fn test_convert_record_to_object() {
831 let mut doc = SchemaDocument::new();
832
833 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
834 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
835
836 let mut properties = IndexMap::new();
837 properties.insert(
838 "name".to_string(),
839 RecordFieldSchema {
840 schema: text_id,
841 optional: false,
842 binding_style: None,
843 },
844 );
845 properties.insert(
846 "age".to_string(),
847 RecordFieldSchema {
848 schema: int_id,
849 optional: false,
850 binding_style: None,
851 },
852 );
853
854 doc.root = doc.create_node(SchemaNodeContent::Record(RecordSchema {
855 properties,
856 flatten: vec![],
857 unknown_fields: UnknownFieldsPolicy::Deny,
858 }));
859
860 let result = eure_to_json_schema(&doc).unwrap();
861 match result {
862 JsonSchema::Typed(TypedSchema::Object(schema)) => {
863 assert!(schema.properties.is_some());
864 let props = schema.properties.unwrap();
865 assert_eq!(props.len(), 2);
866 assert!(props.contains_key("name"));
867 assert!(props.contains_key("age"));
868 }
869 _ => panic!("Expected Object schema"),
870 }
871 }
872
873 #[test]
874 fn test_convert_untagged_union_to_oneof() {
875 let mut doc = SchemaDocument::new();
876
877 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
878 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
879
880 let mut variants = IndexMap::new();
881 variants.insert("TextVariant".to_string(), text_id);
882 variants.insert("IntVariant".to_string(), int_id);
883
884 doc.root = doc.create_node(SchemaNodeContent::Union(UnionSchema {
885 variants,
886 unambiguous: Default::default(),
887 repr: VariantRepr::Untagged,
888 deny_untagged: Default::default(),
889 }));
890
891 let result = eure_to_json_schema(&doc).unwrap();
892 match result {
893 JsonSchema::OneOf(schema) => {
894 assert_eq!(schema.schemas.len(), 2);
895 }
896 _ => panic!("Expected OneOf schema for untagged union"),
897 }
898 }
899
900 #[test]
901 fn test_convert_external_union_to_oneof() {
902 let mut doc = SchemaDocument::new();
903
904 let text_id = doc.create_node(SchemaNodeContent::Text(TextSchema::default()));
905 let int_id = doc.create_node(SchemaNodeContent::Integer(EureIntegerSchema::default()));
906
907 let mut variants = IndexMap::new();
908 variants.insert("A".to_string(), text_id);
909 variants.insert("B".to_string(), int_id);
910
911 doc.root = doc.create_node(SchemaNodeContent::Union(UnionSchema {
912 variants,
913 unambiguous: Default::default(),
914 repr: VariantRepr::External,
915 deny_untagged: Default::default(),
916 }));
917
918 let result = eure_to_json_schema(&doc).unwrap();
919 match result {
920 JsonSchema::OneOf(schema) => {
921 assert_eq!(schema.schemas.len(), 2);
922 }
924 _ => panic!("Expected OneOf schema for external union"),
925 }
926 }
927}