1use std::collections::{HashMap, HashSet};
2
3use eure::document::EureDocument;
4use eure::document::NodeId;
5use eure::document::identifier::Identifier;
6use eure::document::node::NodeValue;
7use eure::document::parse::VariantPath;
8use eure::value::ObjectKey;
9use eure::value::PrimitiveValue;
10use eure_schema::UnknownFieldsPolicy;
11use eure_schema::interop::VariantRepr;
12use eure_schema::{SchemaDocument, SchemaNodeContent, SchemaNodeId};
13use num_bigint::BigInt;
14use serde::Serialize;
15use serde::ser::Error as _;
16use serde::ser::{SerializeMap, SerializeSeq, SerializeTuple};
17
18use crate::error::SerError;
19
20pub fn to_serializer<S: serde::Serializer>(
21 ser: S,
22 doc: &EureDocument,
23 schema: &SchemaDocument,
24) -> Result<S::Ok, S::Error> {
25 NodeWithSchema {
26 doc,
27 node_id: doc.get_root_id(),
28 schema,
29 schema_node_id: schema.root,
30 variant_path: None,
31 }
32 .serialize(ser)
33}
34
35pub fn to_serializer_root<S: serde::Serializer>(
36 ser: S,
37 doc: &EureDocument,
38 schema: &SchemaDocument,
39) -> Result<S::Ok, S::Error> {
40 to_serializer(ser, doc, schema)
41}
42
43#[derive(Clone)]
44struct NodeWithSchema<'a> {
45 doc: &'a EureDocument,
46 node_id: NodeId,
47 schema: &'a SchemaDocument,
48 schema_node_id: SchemaNodeId,
49 variant_path: Option<VariantPath>,
50}
51
52impl Serialize for NodeWithSchema<'_> {
53 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
54 serialize_node(
55 ser,
56 self.doc,
57 self.node_id,
58 self.schema,
59 self.schema_node_id,
60 self.variant_path.clone(),
61 )
62 }
63}
64
65struct UntypedNode<'a> {
66 doc: &'a EureDocument,
67 node_id: NodeId,
68}
69
70impl Serialize for UntypedNode<'_> {
71 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
72 serialize_untyped_node(ser, self.doc, self.node_id)
73 }
74}
75
76struct ObjectKeyValue<'a> {
77 key: &'a ObjectKey,
78}
79
80impl Serialize for ObjectKeyValue<'_> {
81 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
82 serialize_object_key(ser, self.key)
83 }
84}
85
86struct ObjectKeyWithSchema<'a> {
87 key: &'a ObjectKey,
88 schema: &'a SchemaDocument,
89 schema_node_id: SchemaNodeId,
90}
91
92impl Serialize for ObjectKeyWithSchema<'_> {
93 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
94 serialize_object_key_with_schema(ser, self.key, self.schema, self.schema_node_id)
95 }
96}
97
98struct InternalTaggedVariant<'a> {
99 tag: &'a str,
100 variant_name: &'a str,
101 variant_schema_id: SchemaNodeId,
102 variant_path: Option<VariantPath>,
103}
104
105fn serialize_node<S: serde::Serializer>(
106 ser: S,
107 doc: &EureDocument,
108 node_id: NodeId,
109 schema: &SchemaDocument,
110 schema_node_id: SchemaNodeId,
111 variant_path: Option<VariantPath>,
112) -> Result<S::Ok, S::Error> {
113 let schema_node_id = resolve_schema_node_id(schema, schema_node_id)
114 .map_err(|err| S::Error::custom(err.to_string()))?;
115 let schema_content = &schema.node(schema_node_id).content;
116
117 if let Some(path) = &variant_path
118 && !path.is_empty()
119 && !matches!(schema_content, SchemaNodeContent::Union(_))
120 {
121 return Err(S::Error::custom(
122 SerError::Custom(format!("invalid variant name: {}", path)).to_string(),
123 ));
124 }
125
126 match schema_content {
127 SchemaNodeContent::Any | SchemaNodeContent::Literal(_) => {
128 serialize_untyped_node(ser, doc, node_id)
129 }
130 SchemaNodeContent::Text(_) => match &doc.node(node_id).content {
131 NodeValue::Primitive(PrimitiveValue::Text(text)) => ser.serialize_str(text.as_str()),
132 other => Err(S::Error::custom(
133 SerError::Custom(format!(
134 "type mismatch: expected text, got {}",
135 node_value_type(other)
136 ))
137 .to_string(),
138 )),
139 },
140 SchemaNodeContent::Integer(_) => match &doc.node(node_id).content {
141 NodeValue::Primitive(PrimitiveValue::Integer(value)) => serialize_bigint(ser, value),
142 other => Err(S::Error::custom(
143 SerError::Custom(format!(
144 "type mismatch: expected integer, got {}",
145 node_value_type(other)
146 ))
147 .to_string(),
148 )),
149 },
150 SchemaNodeContent::Float(_) => match &doc.node(node_id).content {
151 NodeValue::Primitive(PrimitiveValue::F32(value)) => {
152 if value.is_finite() {
153 ser.serialize_f32(*value)
154 } else {
155 Err(S::Error::custom(SerError::NonFiniteFloat.to_string()))
156 }
157 }
158 NodeValue::Primitive(PrimitiveValue::F64(value)) => {
159 if value.is_finite() {
160 ser.serialize_f64(*value)
161 } else {
162 Err(S::Error::custom(SerError::NonFiniteFloat.to_string()))
163 }
164 }
165 other => Err(S::Error::custom(
166 SerError::Custom(format!(
167 "type mismatch: expected float, got {}",
168 node_value_type(other)
169 ))
170 .to_string(),
171 )),
172 },
173 SchemaNodeContent::Boolean => match &doc.node(node_id).content {
174 NodeValue::Primitive(PrimitiveValue::Bool(value)) => ser.serialize_bool(*value),
175 other => Err(S::Error::custom(
176 SerError::Custom(format!(
177 "type mismatch: expected boolean, got {}",
178 node_value_type(other)
179 ))
180 .to_string(),
181 )),
182 },
183 SchemaNodeContent::Null => match &doc.node(node_id).content {
184 NodeValue::Primitive(PrimitiveValue::Null) => ser.serialize_unit(),
185 other => Err(S::Error::custom(
186 SerError::Custom(format!(
187 "type mismatch: expected null, got {}",
188 node_value_type(other)
189 ))
190 .to_string(),
191 )),
192 },
193 SchemaNodeContent::Array(array_schema) => match &doc.node(node_id).content {
194 NodeValue::Array(values) => {
195 let mut seq = ser.serialize_seq(Some(values.len()))?;
196 for &child_id in values.iter() {
197 seq.serialize_element(&NodeWithSchema {
198 doc,
199 node_id: child_id,
200 schema,
201 schema_node_id: array_schema.item,
202 variant_path: None,
203 })?;
204 }
205 seq.end()
206 }
207 other => Err(S::Error::custom(
208 SerError::Custom(format!(
209 "type mismatch: expected array, got {}",
210 node_value_type(other)
211 ))
212 .to_string(),
213 )),
214 },
215 SchemaNodeContent::Tuple(tuple_schema) => match &doc.node(node_id).content {
216 NodeValue::Tuple(values) => {
217 if values.len() != tuple_schema.elements.len() {
218 return Err(S::Error::custom(
219 SerError::Custom(format!(
220 "tuple arity mismatch: schema has {} elements, document has {}",
221 tuple_schema.elements.len(),
222 values.len()
223 ))
224 .to_string(),
225 ));
226 }
227 let mut tuple = ser.serialize_tuple(values.len())?;
228 for (index, &child_id) in values.iter().enumerate() {
229 let schema_node_id = tuple_schema.elements[index];
230 tuple.serialize_element(&NodeWithSchema {
231 doc,
232 node_id: child_id,
233 schema,
234 schema_node_id,
235 variant_path: None,
236 })?;
237 }
238 tuple.end()
239 }
240 other => Err(S::Error::custom(
241 SerError::Custom(format!(
242 "type mismatch: expected tuple, got {}",
243 node_value_type(other)
244 ))
245 .to_string(),
246 )),
247 },
248 SchemaNodeContent::Map(map_schema) => match &doc.node(node_id).content {
249 NodeValue::Map(map) => {
250 let mut map_ser = ser.serialize_map(Some(map.len()))?;
251 for (key, &child_id) in map.iter() {
252 map_ser.serialize_entry(
253 &ObjectKeyWithSchema {
254 key,
255 schema,
256 schema_node_id: map_schema.key,
257 },
258 &NodeWithSchema {
259 doc,
260 node_id: child_id,
261 schema,
262 schema_node_id: map_schema.value,
263 variant_path: None,
264 },
265 )?;
266 }
267 map_ser.end()
268 }
269 other => Err(S::Error::custom(
270 SerError::Custom(format!(
271 "type mismatch: expected map, got {}",
272 node_value_type(other)
273 ))
274 .to_string(),
275 )),
276 },
277 SchemaNodeContent::Record(_) => match &doc.node(node_id).content {
278 NodeValue::Map(map) => {
279 let mut map_ser = ser.serialize_map(Some(map.len()))?;
280 serialize_record_entries(&mut map_ser, doc, node_id, schema, schema_node_id)?;
281 map_ser.end()
282 }
283 other => Err(S::Error::custom(
284 SerError::Custom(format!(
285 "type mismatch: expected record, got {}",
286 node_value_type(other)
287 ))
288 .to_string(),
289 )),
290 },
291 SchemaNodeContent::Union(union_schema) => {
292 let variant_path = match variant_path {
293 Some(path) => path,
294 None => extract_variant_path(doc, node_id)
295 .map_err(|err| S::Error::custom(err.to_string()))?,
296 };
297 let Some(variant_name) = variant_path.first() else {
298 return Err(S::Error::custom(
299 SerError::Custom("no variant matched".to_string()).to_string(),
300 ));
301 };
302 let Some(&variant_schema_id) = union_schema.variants.get(variant_name.as_ref()) else {
303 return Err(S::Error::custom(
304 SerError::Custom(format!("invalid variant name: {}", variant_name)).to_string(),
305 ));
306 };
307 let rest = variant_path.rest();
308 let variant_name = variant_name.as_ref();
309 let repr = union_schema
310 .interop
311 .variant_repr
312 .as_ref()
313 .unwrap_or(&VariantRepr::External);
314
315 match repr {
316 VariantRepr::External => {
317 let mut map = ser.serialize_map(Some(1))?;
318 map.serialize_entry(
319 variant_name,
320 &NodeWithSchema {
321 doc,
322 node_id,
323 schema,
324 schema_node_id: variant_schema_id,
325 variant_path: rest,
326 },
327 )?;
328 map.end()
329 }
330 VariantRepr::Internal { tag } => {
331 let mut map = ser.serialize_map(None)?;
332 serialize_internal_tagged_entries(
333 &mut map,
334 doc,
335 node_id,
336 schema,
337 InternalTaggedVariant {
338 tag,
339 variant_name,
340 variant_schema_id,
341 variant_path: rest,
342 },
343 )?;
344 map.end()
345 }
346 VariantRepr::Adjacent { tag, content } => {
347 if tag == content {
348 return Err(S::Error::custom(
349 SerError::Custom(format!(
350 "adjacent variant tag and content fields conflict: {tag}"
351 ))
352 .to_string(),
353 ));
354 }
355 let resolved_id = resolve_schema_node_id(schema, variant_schema_id)
356 .map_err(|e| S::Error::custom(e.to_string()))?;
357 let is_unit =
358 matches!(schema.node(resolved_id).content, SchemaNodeContent::Null);
359 if is_unit {
360 let mut map = ser.serialize_map(Some(1))?;
361 map.serialize_entry(tag, variant_name)?;
362 map.end()
363 } else {
364 let mut map = ser.serialize_map(Some(2))?;
365 map.serialize_entry(tag, variant_name)?;
366 map.serialize_entry(
367 content,
368 &NodeWithSchema {
369 doc,
370 node_id,
371 schema,
372 schema_node_id: variant_schema_id,
373 variant_path: rest,
374 },
375 )?;
376 map.end()
377 }
378 }
379 VariantRepr::Untagged => {
380 if union_schema.deny_untagged.contains(variant_name) {
381 return Err(S::Error::custom(
382 SerError::Custom(format!(
383 "variant {variant_name:?} requires explicit $variant tag and cannot be serialized as untagged"
384 ))
385 .to_string(),
386 ));
387 }
388 serialize_node(ser, doc, node_id, schema, variant_schema_id, rest)
389 }
390 }
391 }
392 SchemaNodeContent::Reference(_) => unreachable!("references are resolved above"),
393 }
394}
395
396fn serialize_internal_tagged_entries<M: SerializeMap>(
397 map_ser: &mut M,
398 doc: &EureDocument,
399 node_id: NodeId,
400 schema: &SchemaDocument,
401 variant: InternalTaggedVariant<'_>,
402) -> Result<(), M::Error> {
403 let InternalTaggedVariant {
404 tag,
405 variant_name,
406 variant_schema_id,
407 variant_path,
408 } = variant;
409 let selected_variant_schema_id = resolve_schema_node_id(schema, variant_schema_id)
410 .map_err(|err| M::Error::custom(err.to_string()))?;
411 let is_unit_variant = matches!(
412 schema.node(selected_variant_schema_id).content,
413 SchemaNodeContent::Null
414 );
415
416 if is_unit_variant {
417 let NodeValue::Primitive(PrimitiveValue::Null) = &doc.node(node_id).content else {
418 return Err(M::Error::custom(
419 SerError::Custom(format!(
420 "type mismatch: expected null, got {}",
421 node_value_type(&doc.node(node_id).content)
422 ))
423 .to_string(),
424 ));
425 };
426 } else if let NodeValue::Map(content_map) = &doc.node(node_id).content
427 && content_map.contains_key(&ObjectKey::String(tag.to_string()))
428 {
429 return Err(M::Error::custom(
430 SerError::Custom(format!(
431 "variant tag field conflicts with content field: {tag}"
432 ))
433 .to_string(),
434 ));
435 }
436
437 map_ser.serialize_entry(tag, variant_name)?;
438
439 if is_unit_variant {
440 Ok(())
441 } else {
442 serialize_map_like_entries(
443 map_ser,
444 doc,
445 node_id,
446 schema,
447 variant_schema_id,
448 variant_path,
449 )
450 }
451}
452
453fn collect_flatten_fields(
456 schema: &SchemaDocument,
457 node_id: SchemaNodeId,
458 out: &mut HashMap<String, (SchemaNodeId, bool)>,
459) {
460 let Ok(resolved) = resolve_schema_node_id(schema, node_id) else {
461 return;
462 };
463
464 match &schema.node(resolved).content {
465 SchemaNodeContent::Record(rec) => {
466 for (name, field) in &rec.properties {
467 out.entry(name.clone())
468 .or_insert((field.schema, field.optional));
469 }
470 for &inner in &rec.flatten {
471 collect_flatten_fields(schema, inner, out);
472 }
473 }
474 SchemaNodeContent::Union(_)
475 | SchemaNodeContent::Any
476 | SchemaNodeContent::Text(_)
477 | SchemaNodeContent::Integer(_)
478 | SchemaNodeContent::Float(_)
479 | SchemaNodeContent::Boolean
480 | SchemaNodeContent::Null
481 | SchemaNodeContent::Literal(_)
482 | SchemaNodeContent::Array(_)
483 | SchemaNodeContent::Map(_)
484 | SchemaNodeContent::Tuple(_)
485 | SchemaNodeContent::Reference(_) => {}
486 }
487}
488
489fn serialize_record_entries<M: SerializeMap>(
490 map_ser: &mut M,
491 doc: &EureDocument,
492 node_id: NodeId,
493 schema: &SchemaDocument,
494 schema_node_id: SchemaNodeId,
495) -> Result<(), M::Error> {
496 let schema_node_id = resolve_schema_node_id(schema, schema_node_id)
497 .map_err(|err| M::Error::custom(err.to_string()))?;
498 let SchemaNodeContent::Record(record_schema) = &schema.node(schema_node_id).content else {
499 return Err(M::Error::custom("record schema expected"));
500 };
501 let NodeValue::Map(map) = &doc.node(node_id).content else {
502 return Err(M::Error::custom("record node must be a map"));
503 };
504
505 let mut written = HashSet::new();
506 let mut flatten_fields = HashMap::new();
507 for &fid in &record_schema.flatten {
508 collect_flatten_fields(schema, fid, &mut flatten_fields);
509 }
510
511 for (field_name, field_schema) in &record_schema.properties {
512 let key = ObjectKey::String(field_name.clone());
513 if let Some(&child_id) = map.get(&key) {
514 map_ser.serialize_entry(
515 field_name,
516 &NodeWithSchema {
517 doc,
518 node_id: child_id,
519 schema,
520 schema_node_id: field_schema.schema,
521 variant_path: None,
522 },
523 )?;
524 written.insert(key);
525 }
526 }
527
528 for (field_name, &(fschema_id, _optional)) in &flatten_fields {
529 let key = ObjectKey::String(field_name.clone());
530 if written.contains(&key) {
531 continue;
532 }
533 if let Some(&child_id) = map.get(&key) {
534 map_ser.serialize_entry(
535 field_name,
536 &NodeWithSchema {
537 doc,
538 node_id: child_id,
539 schema,
540 schema_node_id: fschema_id,
541 variant_path: None,
542 },
543 )?;
544 written.insert(key);
545 }
546 }
547
548 for (field_name, field_schema) in &record_schema.properties {
549 if !field_schema.optional && !map.contains_key(&ObjectKey::String(field_name.clone())) {
550 return Err(M::Error::custom(
551 SerError::MissingField(field_name.clone()).to_string(),
552 ));
553 }
554 }
555 for (field_name, &(_schema_id, optional)) in &flatten_fields {
556 if !optional && !map.contains_key(&ObjectKey::String(field_name.clone())) {
557 return Err(M::Error::custom(
558 SerError::MissingField(field_name.clone()).to_string(),
559 ));
560 }
561 }
562
563 for (key, &child_id) in map.iter() {
564 if written.contains(key) {
565 continue;
566 }
567
568 match &record_schema.unknown_fields {
569 UnknownFieldsPolicy::Schema(unknown_schema_id) => map_ser.serialize_entry(
570 &ObjectKeyValue { key },
571 &NodeWithSchema {
572 doc,
573 node_id: child_id,
574 schema,
575 schema_node_id: *unknown_schema_id,
576 variant_path: None,
577 },
578 )?,
579 UnknownFieldsPolicy::Allow => map_ser.serialize_entry(
580 &ObjectKeyValue { key },
581 &UntypedNode {
582 doc,
583 node_id: child_id,
584 },
585 )?,
586 UnknownFieldsPolicy::Deny => {
587 return Err(M::Error::custom(
588 SerError::Custom(format!(
589 "document contains field {:?} not allowed by Deny-policy record schema",
590 key
591 ))
592 .to_string(),
593 ));
594 }
595 }
596 }
597 Ok(())
598}
599
600fn serialize_map_like_entries<M: SerializeMap>(
601 map_ser: &mut M,
602 doc: &EureDocument,
603 node_id: NodeId,
604 schema: &SchemaDocument,
605 schema_node_id: SchemaNodeId,
606 variant_path: Option<VariantPath>,
607) -> Result<(), M::Error> {
608 let schema_node_id = resolve_schema_node_id(schema, schema_node_id)
609 .map_err(|err| M::Error::custom(err.to_string()))?;
610 let schema_content = &schema.node(schema_node_id).content;
611
612 if let Some(path) = &variant_path
613 && !path.is_empty()
614 && !matches!(schema_content, SchemaNodeContent::Union(_))
615 {
616 return Err(M::Error::custom(
617 SerError::Custom(
618 "variant path has remaining components but schema is not a union".to_string(),
619 )
620 .to_string(),
621 ));
622 }
623
624 match schema_content {
625 SchemaNodeContent::Record(_) => {
626 serialize_record_entries(map_ser, doc, node_id, schema, schema_node_id)
627 }
628 SchemaNodeContent::Union(union_schema) => serialize_union_map_like_entries(
629 map_ser,
630 doc,
631 node_id,
632 schema,
633 union_schema,
634 variant_path,
635 ),
636 SchemaNodeContent::Map(map_schema) => {
637 let NodeValue::Map(map) = &doc.node(node_id).content else {
638 return Err(M::Error::custom("map node must be a map"));
639 };
640 for (key, &child_id) in map.iter() {
641 map_ser.serialize_entry(
642 &ObjectKeyWithSchema {
643 key,
644 schema,
645 schema_node_id: map_schema.key,
646 },
647 &NodeWithSchema {
648 doc,
649 node_id: child_id,
650 schema,
651 schema_node_id: map_schema.value,
652 variant_path: None,
653 },
654 )?;
655 }
656 Ok(())
657 }
658 SchemaNodeContent::Any => {
659 let NodeValue::Map(map) = &doc.node(node_id).content else {
661 return Err(M::Error::custom("map-like node must be a map"));
662 };
663 for (key, &child_id) in map.iter() {
664 map_ser.serialize_entry(
665 &ObjectKeyValue { key },
666 &UntypedNode {
667 doc,
668 node_id: child_id,
669 },
670 )?;
671 }
672 Ok(())
673 }
674 other => Err(M::Error::custom(
675 SerError::Custom(format!(
676 "schema type {} cannot be serialized as a map-like internal-tagged variant",
677 schema_content_type_name(other)
678 ))
679 .to_string(),
680 )),
681 }
682}
683
684fn schema_content_type_name(content: &SchemaNodeContent) -> &'static str {
685 match content {
686 SchemaNodeContent::Any => "any",
687 SchemaNodeContent::Text(_) => "text",
688 SchemaNodeContent::Integer(_) => "integer",
689 SchemaNodeContent::Float(_) => "float",
690 SchemaNodeContent::Boolean => "boolean",
691 SchemaNodeContent::Null => "null",
692 SchemaNodeContent::Literal(_) => "literal",
693 SchemaNodeContent::Array(_) => "array",
694 SchemaNodeContent::Tuple(_) => "tuple",
695 SchemaNodeContent::Map(_) => "map",
696 SchemaNodeContent::Record(_) => "record",
697 SchemaNodeContent::Union(_) => "union",
698 SchemaNodeContent::Reference(_) => "reference",
699 }
700}
701
702fn serialize_union_map_like_entries<M: SerializeMap>(
703 map_ser: &mut M,
704 doc: &EureDocument,
705 node_id: NodeId,
706 schema: &SchemaDocument,
707 union_schema: &eure_schema::UnionSchema,
708 variant_path: Option<VariantPath>,
709) -> Result<(), M::Error> {
710 let variant_path = match variant_path {
711 Some(path) => path,
712 None => {
713 extract_variant_path(doc, node_id).map_err(|err| M::Error::custom(err.to_string()))?
714 }
715 };
716 let Some(variant_name) = variant_path.first() else {
717 return Err(M::Error::custom(
718 SerError::Custom("no variant matched".to_string()).to_string(),
719 ));
720 };
721 let Some(&variant_schema_id) = union_schema.variants.get(variant_name.as_ref()) else {
722 return Err(M::Error::custom(
723 SerError::Custom(format!("invalid variant name: {variant_name}")).to_string(),
724 ));
725 };
726
727 let rest = variant_path.rest();
728 let variant_name = variant_name.as_ref();
729 let repr = union_schema
730 .interop
731 .variant_repr
732 .as_ref()
733 .unwrap_or(&VariantRepr::External);
734
735 match repr {
736 VariantRepr::External => map_ser.serialize_entry(
737 variant_name,
738 &NodeWithSchema {
739 doc,
740 node_id,
741 schema,
742 schema_node_id: variant_schema_id,
743 variant_path: rest,
744 },
745 ),
746 VariantRepr::Internal { tag } => serialize_internal_tagged_entries(
747 map_ser,
748 doc,
749 node_id,
750 schema,
751 InternalTaggedVariant {
752 tag,
753 variant_name,
754 variant_schema_id,
755 variant_path: rest,
756 },
757 ),
758 VariantRepr::Adjacent { tag, content } => {
759 if tag == content {
760 return Err(M::Error::custom(
761 SerError::Custom(format!(
762 "adjacent variant tag and content fields conflict: {tag}"
763 ))
764 .to_string(),
765 ));
766 }
767 let resolved_id = resolve_schema_node_id(schema, variant_schema_id)
768 .map_err(|e| M::Error::custom(e.to_string()))?;
769 let is_unit = matches!(schema.node(resolved_id).content, SchemaNodeContent::Null);
770 map_ser.serialize_entry(tag, variant_name)?;
771 if !is_unit {
772 map_ser.serialize_entry(
773 content,
774 &NodeWithSchema {
775 doc,
776 node_id,
777 schema,
778 schema_node_id: variant_schema_id,
779 variant_path: rest,
780 },
781 )?;
782 }
783 Ok(())
784 }
785 VariantRepr::Untagged => {
786 if union_schema.deny_untagged.contains(variant_name) {
787 return Err(M::Error::custom(
788 SerError::Custom(format!(
789 "variant {variant_name:?} requires explicit $variant tag and cannot be serialized as untagged"
790 ))
791 .to_string(),
792 ));
793 }
794 serialize_map_like_entries(map_ser, doc, node_id, schema, variant_schema_id, rest)
795 }
796 }
797}
798
799fn serialize_untyped_node<S: serde::Serializer>(
800 ser: S,
801 doc: &EureDocument,
802 node_id: NodeId,
803) -> Result<S::Ok, S::Error> {
804 match &doc.node(node_id).content {
805 NodeValue::Hole(_) => Err(S::Error::custom(SerError::UnexpectedHole.to_string())),
806 NodeValue::PartialMap(_) => Err(S::Error::custom(
807 SerError::PartialMapUnsupported.to_string(),
808 )),
809 NodeValue::Primitive(primitive) => serialize_primitive(ser, primitive),
810 NodeValue::Array(values) => {
811 let mut seq = ser.serialize_seq(Some(values.len()))?;
812 for &child_id in values.iter() {
813 seq.serialize_element(&UntypedNode {
814 doc,
815 node_id: child_id,
816 })?;
817 }
818 seq.end()
819 }
820 NodeValue::Tuple(values) => {
821 let mut tuple = ser.serialize_tuple(values.len())?;
822 for &child_id in values.iter() {
823 tuple.serialize_element(&UntypedNode {
824 doc,
825 node_id: child_id,
826 })?;
827 }
828 tuple.end()
829 }
830 NodeValue::Map(values) => {
831 let mut map = ser.serialize_map(Some(values.len()))?;
832 for (key, &child_id) in values.iter() {
833 map.serialize_entry(
834 &ObjectKeyValue { key },
835 &UntypedNode {
836 doc,
837 node_id: child_id,
838 },
839 )?;
840 }
841 map.end()
842 }
843 }
844}
845
846fn serialize_primitive<S: serde::Serializer>(
847 ser: S,
848 primitive: &PrimitiveValue,
849) -> Result<S::Ok, S::Error> {
850 match primitive {
851 PrimitiveValue::Null => ser.serialize_unit(),
852 PrimitiveValue::Bool(value) => ser.serialize_bool(*value),
853 PrimitiveValue::Integer(value) => serialize_bigint(ser, value),
854 PrimitiveValue::F32(value) => {
855 if value.is_finite() {
856 ser.serialize_f32(*value)
857 } else {
858 Err(S::Error::custom(SerError::NonFiniteFloat.to_string()))
859 }
860 }
861 PrimitiveValue::F64(value) => {
862 if value.is_finite() {
863 ser.serialize_f64(*value)
864 } else {
865 Err(S::Error::custom(SerError::NonFiniteFloat.to_string()))
866 }
867 }
868 PrimitiveValue::Text(text) => ser.serialize_str(text.as_str()),
869 }
870}
871
872fn serialize_object_key<S: serde::Serializer>(ser: S, key: &ObjectKey) -> Result<S::Ok, S::Error> {
873 match key {
874 ObjectKey::String(value) => ser.serialize_str(value),
875 ObjectKey::Number(value) => serialize_bigint(ser, value),
876 ObjectKey::Tuple(values) => {
877 let mut tuple = ser.serialize_tuple(values.len())?;
878 for key in &values.0 {
879 tuple.serialize_element(&ObjectKeyValue { key })?;
880 }
881 tuple.end()
882 }
883 }
884}
885
886fn serialize_object_key_with_schema<S: serde::Serializer>(
887 ser: S,
888 key: &ObjectKey,
889 schema: &SchemaDocument,
890 schema_node_id: SchemaNodeId,
891) -> Result<S::Ok, S::Error> {
892 let schema_node_id = resolve_schema_node_id(schema, schema_node_id)
893 .map_err(|err| S::Error::custom(err.to_string()))?;
894
895 match &schema.node(schema_node_id).content {
896 SchemaNodeContent::Any => serialize_object_key(ser, key),
897 SchemaNodeContent::Text(_) => serialize_text_object_key(ser, key),
898 SchemaNodeContent::Integer(_) => serialize_integer_object_key(ser, key),
899 SchemaNodeContent::Boolean => serialize_bool_object_key(ser, key),
900 SchemaNodeContent::Literal(expected) => serialize_literal_object_key(ser, key, expected),
901 SchemaNodeContent::Tuple(tuple_schema) => {
902 serialize_tuple_object_key(ser, key, schema, &tuple_schema.elements)
903 }
904 SchemaNodeContent::Union(union_schema) => {
905 serialize_union_object_key(ser, key, schema, union_schema)
906 }
907 SchemaNodeContent::Reference(_) => unreachable!("references are resolved above"),
908 SchemaNodeContent::Float(_)
909 | SchemaNodeContent::Null
910 | SchemaNodeContent::Array(_)
911 | SchemaNodeContent::Map(_)
912 | SchemaNodeContent::Record(_) => {
913 Err(S::Error::custom(unsupported_complex_map_keys().to_string()))
914 }
915 }
916}
917
918fn serialize_text_object_key<S: serde::Serializer>(
919 ser: S,
920 key: &ObjectKey,
921) -> Result<S::Ok, S::Error> {
922 match key {
923 ObjectKey::String(value) => ser.serialize_str(value),
924 ObjectKey::Number(value) => ser.serialize_str(&value.to_string()),
925 ObjectKey::Tuple(_) => Err(S::Error::custom(
926 object_key_type_mismatch("text", key).to_string(),
927 )),
928 }
929}
930
931fn serialize_integer_object_key<S: serde::Serializer>(
932 ser: S,
933 key: &ObjectKey,
934) -> Result<S::Ok, S::Error> {
935 match key {
936 ObjectKey::Number(value) => serialize_bigint(ser, value),
937 ObjectKey::String(value) => {
938 let parsed = parse_bigint(value).map_err(|err| S::Error::custom(err.to_string()))?;
939 serialize_bigint(ser, &parsed)
940 }
941 ObjectKey::Tuple(_) => Err(S::Error::custom(
942 object_key_type_mismatch("integer", key).to_string(),
943 )),
944 }
945}
946
947fn serialize_bool_object_key<S: serde::Serializer>(
948 ser: S,
949 key: &ObjectKey,
950) -> Result<S::Ok, S::Error> {
951 match key {
952 ObjectKey::String(value) => match value.as_str() {
953 "true" => ser.serialize_bool(true),
954 "false" => ser.serialize_bool(false),
955 _ => Err(S::Error::custom(
956 object_key_type_mismatch("boolean", key).to_string(),
957 )),
958 },
959 ObjectKey::Number(_) | ObjectKey::Tuple(_) => Err(S::Error::custom(
960 object_key_type_mismatch("boolean", key).to_string(),
961 )),
962 }
963}
964
965fn serialize_literal_object_key<S: serde::Serializer>(
966 ser: S,
967 key: &ObjectKey,
968 expected: &EureDocument,
969) -> Result<S::Ok, S::Error> {
970 match expected_primitive(expected).map_err(|err| S::Error::custom(err.to_string()))? {
971 PrimitiveValue::Text(expected_value) => match key {
972 ObjectKey::String(value) if value == expected_value.as_str() => {
973 ser.serialize_str(value)
974 }
975 ObjectKey::String(_) => Err(S::Error::custom(literal_mismatch().to_string())),
976 ObjectKey::Number(_) | ObjectKey::Tuple(_) => Err(S::Error::custom(
977 object_key_type_mismatch("text", key).to_string(),
978 )),
979 },
980 PrimitiveValue::Integer(expected_value) => match key {
981 ObjectKey::Number(value) if value == expected_value => serialize_bigint(ser, value),
982 ObjectKey::Number(_) => Err(S::Error::custom(literal_mismatch().to_string())),
983 ObjectKey::String(value) => {
984 let parsed =
985 parse_bigint(value).map_err(|err| S::Error::custom(err.to_string()))?;
986 if &parsed == expected_value {
987 serialize_bigint(ser, &parsed)
988 } else {
989 Err(S::Error::custom(literal_mismatch().to_string()))
990 }
991 }
992 ObjectKey::Tuple(_) => Err(S::Error::custom(
993 object_key_type_mismatch("integer", key).to_string(),
994 )),
995 },
996 PrimitiveValue::Bool(expected_value) => match key {
997 ObjectKey::String(value)
998 if (*expected_value && value == "true")
999 || (!*expected_value && value == "false") =>
1000 {
1001 ser.serialize_bool(*expected_value)
1002 }
1003 ObjectKey::String(_) => Err(S::Error::custom(literal_mismatch().to_string())),
1004 ObjectKey::Number(_) | ObjectKey::Tuple(_) => Err(S::Error::custom(
1005 object_key_type_mismatch("boolean", key).to_string(),
1006 )),
1007 },
1008 PrimitiveValue::Null | PrimitiveValue::F32(_) | PrimitiveValue::F64(_) => Err(
1009 S::Error::custom(unsupported_complex_literal_key().to_string()),
1010 ),
1011 }
1012}
1013
1014fn serialize_tuple_object_key<S: serde::Serializer>(
1015 ser: S,
1016 key: &ObjectKey,
1017 schema: &SchemaDocument,
1018 elements: &[SchemaNodeId],
1019) -> Result<S::Ok, S::Error> {
1020 let ObjectKey::Tuple(values) = key else {
1021 return Err(S::Error::custom(
1022 object_key_type_mismatch("tuple", key).to_string(),
1023 ));
1024 };
1025 if values.len() != elements.len() {
1026 return Err(S::Error::custom(
1027 SerError::Custom(format!(
1028 "tuple length mismatch: expected {}, got {}",
1029 elements.len(),
1030 values.len()
1031 ))
1032 .to_string(),
1033 ));
1034 }
1035
1036 let mut tuple = ser.serialize_tuple(values.len())?;
1037 for (key, &schema_node_id) in values.0.iter().zip(elements) {
1038 tuple.serialize_element(&ObjectKeyWithSchema {
1039 key,
1040 schema,
1041 schema_node_id,
1042 })?;
1043 }
1044 tuple.end()
1045}
1046
1047fn serialize_union_object_key<S: serde::Serializer>(
1048 ser: S,
1049 key: &ObjectKey,
1050 schema: &SchemaDocument,
1051 union_schema: &eure_schema::UnionSchema,
1052) -> Result<S::Ok, S::Error> {
1053 if let ObjectKey::String(value) = key
1054 && union_schema.variants.contains_key(value)
1055 {
1056 return ser.serialize_str(value);
1057 }
1058
1059 for &variant_schema_id in union_schema.variants.values() {
1060 if object_key_matches_schema(schema, variant_schema_id, key)
1061 .map_err(|err| S::Error::custom(err.to_string()))?
1062 {
1063 return serialize_object_key_with_schema(ser, key, schema, variant_schema_id);
1064 }
1065 }
1066
1067 Err(S::Error::custom(
1068 object_key_type_mismatch("union-compatible", key).to_string(),
1069 ))
1070}
1071
1072fn object_key_matches_schema(
1073 schema: &SchemaDocument,
1074 schema_node_id: SchemaNodeId,
1075 key: &ObjectKey,
1076) -> Result<bool, SerError> {
1077 let schema_node_id = resolve_schema_node_id(schema, schema_node_id)?;
1078
1079 match &schema.node(schema_node_id).content {
1080 SchemaNodeContent::Any => Ok(true),
1081 SchemaNodeContent::Text(_) => Ok(!matches!(key, ObjectKey::Tuple(_))),
1082 SchemaNodeContent::Integer(_) => Ok(match key {
1083 ObjectKey::Number(_) => true,
1084 ObjectKey::String(value) => value.parse::<BigInt>().is_ok(),
1085 ObjectKey::Tuple(_) => false,
1086 }),
1087 SchemaNodeContent::Boolean => Ok(matches!(
1088 key,
1089 ObjectKey::String(value) if value == "true" || value == "false"
1090 )),
1091 SchemaNodeContent::Literal(expected) => literal_object_key_matches(expected, key),
1092 SchemaNodeContent::Tuple(tuple_schema) => match key {
1093 ObjectKey::Tuple(values) if values.len() == tuple_schema.elements.len() => {
1094 for (key, &element_schema_id) in values.0.iter().zip(&tuple_schema.elements) {
1095 if !object_key_matches_schema(schema, element_schema_id, key)? {
1096 return Ok(false);
1097 }
1098 }
1099 Ok(true)
1100 }
1101 _ => Ok(false),
1102 },
1103 SchemaNodeContent::Union(union_schema) => {
1104 if let ObjectKey::String(value) = key
1105 && union_schema.variants.contains_key(value)
1106 {
1107 return Ok(true);
1108 }
1109
1110 for &variant_schema_id in union_schema.variants.values() {
1111 if object_key_matches_schema(schema, variant_schema_id, key)? {
1112 return Ok(true);
1113 }
1114 }
1115 Ok(false)
1116 }
1117 SchemaNodeContent::Reference(_) => unreachable!("references are resolved above"),
1118 SchemaNodeContent::Float(_)
1119 | SchemaNodeContent::Null
1120 | SchemaNodeContent::Array(_)
1121 | SchemaNodeContent::Map(_)
1122 | SchemaNodeContent::Record(_) => Ok(false),
1123 }
1124}
1125
1126fn literal_object_key_matches(expected: &EureDocument, key: &ObjectKey) -> Result<bool, SerError> {
1127 match expected_primitive(expected)? {
1128 PrimitiveValue::Text(expected_value) => Ok(matches!(
1129 key,
1130 ObjectKey::String(value) if value == expected_value.as_str()
1131 )),
1132 PrimitiveValue::Integer(expected_value) => Ok(match key {
1133 ObjectKey::Number(value) => value == expected_value,
1134 ObjectKey::String(value) => value
1135 .parse::<BigInt>()
1136 .is_ok_and(|parsed| &parsed == expected_value),
1137 ObjectKey::Tuple(_) => false,
1138 }),
1139 PrimitiveValue::Bool(expected_value) => Ok(matches!(
1140 key,
1141 ObjectKey::String(value)
1142 if (*expected_value && value == "true") || (!*expected_value && value == "false")
1143 )),
1144 PrimitiveValue::Null | PrimitiveValue::F32(_) | PrimitiveValue::F64(_) => Ok(false),
1145 }
1146}
1147
1148fn expected_primitive(expected: &EureDocument) -> Result<&PrimitiveValue, SerError> {
1149 match &expected.node(expected.get_root_id()).content {
1150 NodeValue::Primitive(primitive) => Ok(primitive),
1151 _ => Err(unsupported_complex_literal_key()),
1152 }
1153}
1154
1155fn parse_bigint(value: &str) -> Result<BigInt, SerError> {
1156 value
1157 .parse()
1158 .map_err(|_| SerError::Custom(format!("invalid integer map key: {value}")))
1159}
1160
1161fn object_key_type_mismatch(expected: &str, key: &ObjectKey) -> SerError {
1162 SerError::Custom(format!(
1163 "type mismatch: expected {expected} map key, got {}",
1164 object_key_type(key)
1165 ))
1166}
1167
1168fn object_key_type(key: &ObjectKey) -> &'static str {
1169 match key {
1170 ObjectKey::String(_) => "text",
1171 ObjectKey::Number(_) => "integer",
1172 ObjectKey::Tuple(_) => "tuple",
1173 }
1174}
1175
1176fn literal_mismatch() -> SerError {
1177 SerError::Custom("literal mismatch".to_string())
1178}
1179
1180fn unsupported_complex_map_keys() -> SerError {
1181 SerError::Custom("complex map keys are unsupported in serde-eure v1".to_string())
1182}
1183
1184fn unsupported_complex_literal_key() -> SerError {
1185 SerError::Custom("complex literal map keys are unsupported in serde-eure v1".to_string())
1186}
1187
1188fn serialize_bigint<S: serde::Serializer>(ser: S, value: &BigInt) -> Result<S::Ok, S::Error> {
1189 if let Ok(value) = i64::try_from(value) {
1190 ser.serialize_i64(value)
1191 } else if let Ok(value) = u64::try_from(value) {
1192 ser.serialize_u64(value)
1193 } else if let Ok(value) = i128::try_from(value) {
1194 ser.serialize_i128(value)
1195 } else if let Ok(value) = u128::try_from(value) {
1196 ser.serialize_u128(value)
1197 } else {
1198 Err(S::Error::custom(SerError::BigIntOutOfRange.to_string()))
1199 }
1200}
1201
1202fn extract_variant_path(doc: &EureDocument, node_id: NodeId) -> Result<VariantPath, SerError> {
1203 let node = doc.node(node_id);
1204 let Some(variant_id) = node.extensions.get(&Identifier::VARIANT).copied() else {
1205 return Err(SerError::Custom("no variant matched".to_string()));
1206 };
1207 let variant_node = doc.node(variant_id);
1208 let value = match &variant_node.content {
1209 NodeValue::Primitive(PrimitiveValue::Text(text)) => text.as_str(),
1210 other => {
1211 return Err(SerError::Custom(format!(
1212 "type mismatch: expected text, got {}",
1213 node_value_type(other)
1214 )));
1215 }
1216 };
1217 VariantPath::parse(value)
1218 .map_err(|_| SerError::Custom(format!("invalid variant name: {value}")))
1219}
1220
1221fn resolve_schema_node_id(
1222 schema: &SchemaDocument,
1223 mut schema_node_id: SchemaNodeId,
1224) -> Result<SchemaNodeId, SerError> {
1225 for _ in 0..schema.nodes.len() {
1226 match &schema.node(schema_node_id).content {
1227 SchemaNodeContent::Reference(type_ref) => {
1228 if let Some(namespace) = &type_ref.namespace {
1229 return Err(SerError::Custom(format!(
1230 "cross-schema references are unsupported: {namespace}.{}",
1231 type_ref.name
1232 )));
1233 }
1234 schema_node_id = schema.types.get(&type_ref.name).copied().ok_or_else(|| {
1235 SerError::Custom(format!("undefined type reference: {}", type_ref.name))
1236 })?;
1237 }
1238 _ => return Ok(schema_node_id),
1239 }
1240 }
1241
1242 Err(SerError::Custom(
1243 "schema reference cycle detected".to_string(),
1244 ))
1245}
1246
1247fn node_value_type(value: &NodeValue) -> &'static str {
1248 match value {
1249 NodeValue::Hole(_) => "hole",
1250 NodeValue::Primitive(PrimitiveValue::Null) => "null",
1251 NodeValue::Primitive(PrimitiveValue::Bool(_)) => "boolean",
1252 NodeValue::Primitive(PrimitiveValue::Integer(_)) => "integer",
1253 NodeValue::Primitive(PrimitiveValue::F32(_))
1254 | NodeValue::Primitive(PrimitiveValue::F64(_)) => "float",
1255 NodeValue::Primitive(PrimitiveValue::Text(_)) => "text",
1256 NodeValue::Array(_) => "array",
1257 NodeValue::Map(_) => "map",
1258 NodeValue::Tuple(_) => "tuple",
1259 NodeValue::PartialMap(_) => "partial-map",
1260 }
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use eure::eure;
1266 use eure_schema::TextSchema;
1267 use eure_schema::interop::UnionInterop;
1268 use eure_schema::{
1269 IntegerSchema, RecordFieldSchema, RecordSchema, SchemaDocument, SchemaNodeContent,
1270 };
1271 use serde_json::json;
1272
1273 use super::to_serializer;
1274
1275 fn make_record_schema() -> SchemaDocument {
1276 let mut schema = SchemaDocument::new();
1277 let text_id = schema.create_node(SchemaNodeContent::Text(TextSchema::default()));
1278 let int_id = schema.create_node(SchemaNodeContent::Integer(IntegerSchema::default()));
1279 let record_id = schema.create_node(SchemaNodeContent::Record(RecordSchema {
1280 properties: [
1281 (
1282 "name".to_string(),
1283 RecordFieldSchema {
1284 schema: text_id,
1285 optional: false,
1286 binding_style: None,
1287 field_codegen: Default::default(),
1288 },
1289 ),
1290 (
1291 "age".to_string(),
1292 RecordFieldSchema {
1293 schema: int_id,
1294 optional: false,
1295 binding_style: None,
1296 field_codegen: Default::default(),
1297 },
1298 ),
1299 ]
1300 .into_iter()
1301 .collect(),
1302 flatten: Vec::new(),
1303 unknown_fields: eure_schema::UnknownFieldsPolicy::Deny,
1304 }));
1305 schema.root = record_id;
1306 let _ = UnionInterop::default();
1307 schema
1308 }
1309
1310 #[test]
1311 fn serializes_simple_record_to_json() {
1312 let schema = make_record_schema();
1313 let doc = eure!({
1314 name = "Alice",
1315 age = 30,
1316 });
1317
1318 let actual = to_serializer(serde_json::value::Serializer, &doc, &schema).unwrap();
1319 assert_eq!(actual, json!({ "name": "Alice", "age": 30 }));
1320 }
1321}