Skip to main content

serde_eure/
ser.rs

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
453/// Recursively collect `(field_name → SchemaNodeId)` from flattened record schemas.
454/// Only `Record` schemas contribute fields; other node types are silently ignored.
455fn 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            // Schema-free: emit whatever the document has untyped.
660            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}