Skip to main content

eure_schema/
interop.rs

1//! Interop-only schema metadata.
2//!
3//! These types describe non-native wire representations used when bridging Eure
4//! with external ecosystems (JSON/Serde/codegen).
5
6use eure_document::parse::{FromEure, ParseContext, ParseError, ParseErrorKind};
7
8/// How to represent union variants on an external wire format.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
10pub enum VariantRepr {
11    // Default intentionally stays `External` for interop consumers that call
12    // `VariantRepr::default()`. Eure union semantics do not use this default;
13    // they flow through `UnionInterop { variant_repr: None }`.
14    /// External tagging: {"variant-name": {...}}
15    #[default]
16    External,
17    /// Internal tagging: {"type": "variant-name", ...fields...}
18    Internal { tag: String },
19    /// Adjacent tagging: {"type": "variant-name", "content": {...}}
20    Adjacent { tag: String, content: String },
21    /// Untagged: infer variant from content shape/value.
22    Untagged,
23}
24
25/// Interop metadata attached to union schemas.
26#[derive(Debug, Clone, PartialEq, Default, eure_macros::FromEure)]
27#[eure(crate = eure_document, rename_all = "kebab-case")]
28pub struct UnionInterop {
29    /// Optional representation hint for external wire formats.
30    #[eure(default)]
31    pub variant_repr: Option<VariantRepr>,
32}
33
34impl FromEure<'_> for VariantRepr {
35    type Error = ParseError;
36
37    fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
38        if let Ok(value) = ctx.parse::<&str>() {
39            return match value {
40                "external" => Ok(VariantRepr::External),
41                "untagged" => Ok(VariantRepr::Untagged),
42                "internal" => Ok(VariantRepr::Internal {
43                    tag: "type".to_string(),
44                }),
45                "adjacent" => Ok(VariantRepr::Adjacent {
46                    tag: "type".to_string(),
47                    content: "content".to_string(),
48                }),
49                _ => Err(ParseError {
50                    node_id: ctx.node_id(),
51                    kind: ParseErrorKind::UnknownVariant(value.to_string()),
52                }),
53            };
54        }
55
56        let rec = ctx.parse_record()?;
57        let tag = rec.parse_field_optional::<String>("tag")?;
58        let content = rec.parse_field_optional::<String>("content")?;
59        rec.allow_unknown_fields()?;
60
61        match (tag, content) {
62            (Some(tag), Some(content)) => Ok(VariantRepr::Adjacent { tag, content }),
63            (Some(tag), None) => Ok(VariantRepr::Internal { tag }),
64            (None, None) => Ok(VariantRepr::External),
65            (None, Some(_)) => Err(ParseError {
66                node_id: ctx.node_id(),
67                kind: ParseErrorKind::MissingField(
68                    "tag (required when content is present)".to_string(),
69                ),
70            }),
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn variant_repr_default_is_external() {
81        assert_eq!(VariantRepr::default(), VariantRepr::External);
82    }
83
84    #[test]
85    fn union_interop_default_has_no_variant_repr() {
86        assert_eq!(UnionInterop::default().variant_repr, None);
87    }
88}