use eure_document::document::NodeId;
use eure_document::identifier::Identifier;
use eure_document::parse::{FromEure, ParseContext, ParseError, ParseErrorKind};
use indexmap::{IndexMap, IndexSet};
use num_bigint::BigInt;
use crate::interop::UnionInterop;
use crate::{BindingStyle, Description, FieldCodegen, TextSchema, TypeReference};
impl FromEure<'_> for TypeReference {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let path: &str = ctx.parse()?;
let path = path.strip_prefix("$types.").ok_or_else(|| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidPattern {
kind: "type reference".to_string(),
reason: format!(
"expected '$types.<name>' or '$types.<namespace>.<name>', got '{}'",
path
),
},
})?;
let parts: Vec<&str> = path.split('.').collect();
match parts.as_slice() {
[name] => {
let name: Identifier = name.parse().map_err(|e| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidIdentifier(e),
})?;
Ok(TypeReference {
namespace: None,
name,
})
}
[namespace, name] => {
let name: Identifier = name.parse().map_err(|e| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidIdentifier(e),
})?;
Ok(TypeReference {
namespace: Some((*namespace).to_string()),
name,
})
}
_ => Err(ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidPattern {
kind: "type reference".to_string(),
reason: format!(
"expected '$types.<name>' or '$types.<namespace>.<name>', got '$types.{}'",
path
),
},
}),
}
}
}
impl FromEure<'_> for crate::SchemaRef {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let schema_ctx = ctx.ext("schema")?;
let path: String = schema_ctx.parse()?;
Ok(crate::SchemaRef {
path,
node_id: schema_ctx.node_id(),
})
}
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, rename_all = "kebab-case")]
pub struct ParsedIntegerSchema {
#[eure(default)]
pub range: Option<String>,
#[eure(default)]
pub multiple_of: Option<BigInt>,
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, rename_all = "kebab-case")]
pub struct ParsedFloatSchema {
#[eure(default)]
pub range: Option<String>,
#[eure(default)]
pub multiple_of: Option<f64>,
#[eure(default)]
pub precision: Option<String>,
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, rename_all = "kebab-case")]
pub struct ParsedArraySchema {
pub item: NodeId,
#[eure(default)]
pub min_length: Option<u32>,
#[eure(default)]
pub max_length: Option<u32>,
#[eure(default)]
pub unique: bool,
#[eure(default)]
pub contains: Option<NodeId>,
#[eure(ext, default)]
pub binding_style: Option<BindingStyle>,
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, rename_all = "kebab-case")]
pub struct ParsedMapSchema {
pub key: NodeId,
pub value: NodeId,
#[eure(default)]
pub min_size: Option<u32>,
#[eure(default)]
pub max_size: Option<u32>,
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, parse_ext, rename_all = "kebab-case")]
pub struct ParsedRecordFieldSchema {
#[eure(flatten_ext)]
pub schema: NodeId,
#[eure(default)]
pub optional: bool,
#[eure(default)]
pub binding_style: Option<BindingStyle>,
#[eure(default)]
pub codegen: Option<FieldCodegen>,
}
#[derive(Debug, Clone, Default)]
pub enum ParsedUnknownFieldsPolicy {
#[default]
Deny,
Allow,
Schema(NodeId),
}
impl FromEure<'_> for ParsedUnknownFieldsPolicy {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let node = ctx.node();
let node_id = ctx.node_id();
if let NodeValue::Primitive(PrimitiveValue::Text(text)) = &node.content {
if text.language == Language::Plaintext {
return match text.as_str() {
"deny" => Ok(ParsedUnknownFieldsPolicy::Deny),
"allow" => Ok(ParsedUnknownFieldsPolicy::Allow),
_ => Err(ParseError {
node_id,
kind: ParseErrorKind::UnknownVariant(text.as_str().to_string()),
}),
};
}
}
Ok(ParsedUnknownFieldsPolicy::Schema(node_id))
}
}
#[derive(Debug, Clone, Default)]
pub struct ParsedRecordSchema {
pub properties: IndexMap<String, ParsedRecordFieldSchema>,
pub flatten: Vec<NodeId>,
pub unknown_fields: ParsedUnknownFieldsPolicy,
}
impl FromEure<'_> for ParsedRecordSchema {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let unknown_fields = ctx
.parse_ext_optional::<ParsedUnknownFieldsPolicy>("unknown-fields")?
.unwrap_or_default();
let flatten = ctx
.parse_ext_optional::<Vec<NodeId>>("flatten")?
.unwrap_or_default();
let rec = ctx.parse_record()?;
let mut properties = IndexMap::new();
for result in rec.unknown_fields() {
let (field_name, field_ctx) = result.map_err(|(key, ctx)| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidKeyType(key.clone()),
})?;
let field_schema = ParsedRecordFieldSchema::parse(&field_ctx)?;
properties.insert(field_name.to_string(), field_schema);
}
Ok(ParsedRecordSchema {
properties,
flatten,
unknown_fields,
})
}
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, rename_all = "kebab-case")]
pub struct ParsedTupleSchema {
pub elements: Vec<NodeId>,
#[eure(ext, default)]
pub binding_style: Option<BindingStyle>,
}
#[derive(Debug, Clone)]
pub struct ParsedUnionSchema {
pub variants: IndexMap<String, NodeId>,
pub unambiguous: IndexSet<String>,
pub interop: UnionInterop,
pub deny_untagged: IndexSet<String>,
}
impl FromEure<'_> for ParsedUnionSchema {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let rec = ctx.parse_record()?;
let mut variants = IndexMap::new();
let mut unambiguous = IndexSet::new();
let mut deny_untagged = IndexSet::new();
if let Some(variants_ctx) = rec.field_optional("variants") {
let variants_rec = variants_ctx.parse_record()?;
for result in variants_rec.unknown_fields() {
let (name, var_ctx) = result.map_err(|(key, ctx)| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidKeyType(key.clone()),
})?;
variants.insert(name.to_string(), var_ctx.node_id());
if var_ctx
.parse_ext_optional::<bool>("deny-untagged")?
.unwrap_or(false)
{
deny_untagged.insert(name.to_string());
}
if var_ctx
.parse_ext_optional::<bool>("unambiguous")?
.unwrap_or(false)
{
unambiguous.insert(name.to_string());
}
}
}
rec.allow_unknown_fields()?;
if ctx.ext_optional("variant-repr").is_some() {
return Err(ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidPattern {
kind: "legacy extension".to_string(),
reason: "`$variant-repr` is removed; use `$interop.variant-repr`".to_string(),
},
});
}
let interop = ctx
.parse_ext_optional::<UnionInterop>("interop")?
.unwrap_or_default();
Ok(ParsedUnionSchema {
variants,
unambiguous,
interop,
deny_untagged,
})
}
}
#[derive(Debug, Clone, eure_macros::FromEure)]
#[eure(crate = eure_document, parse_ext)]
pub struct ParsedExtTypeSchema {
#[eure(flatten_ext)]
pub schema: NodeId,
#[eure(default)]
pub optional: bool,
#[eure(default)]
pub binding_style: Option<BindingStyle>,
}
#[derive(Debug, Clone, Default)]
pub struct ParsedSchemaMetadata {
pub description: Option<Description>,
pub deprecated: bool,
pub default: Option<NodeId>,
pub examples: Option<Vec<NodeId>>,
}
impl ParsedSchemaMetadata {
pub fn parse_from_extensions(ctx: &ParseContext<'_>) -> Result<Self, ParseError> {
let description = ctx.parse_ext_optional::<Description>("description")?;
let deprecated = ctx
.parse_ext_optional::<bool>("deprecated")?
.unwrap_or(false);
let default = ctx.ext_optional("default").map(|ctx| ctx.node_id());
let examples = ctx.parse_ext_optional::<Vec<NodeId>>("examples")?;
Ok(ParsedSchemaMetadata {
description,
deprecated,
default,
examples,
})
}
}
#[derive(Debug, Clone)]
pub enum ParsedSchemaNodeContent {
Any,
Text(TextSchema),
Integer(ParsedIntegerSchema),
Float(ParsedFloatSchema),
Boolean,
Null,
Literal(NodeId),
Array(ParsedArraySchema),
Map(ParsedMapSchema),
Record(ParsedRecordSchema),
Tuple(ParsedTupleSchema),
Union(ParsedUnionSchema),
Reference(TypeReference),
}
#[derive(Debug, Clone)]
pub struct ParsedSchemaNode {
pub content: ParsedSchemaNodeContent,
pub metadata: ParsedSchemaMetadata,
pub ext_types: IndexMap<Identifier, ParsedExtTypeSchema>,
pub codegen: Option<NodeId>,
}
use eure_document::document::node::NodeValue;
use eure_document::text::Language;
use eure_document::value::{PrimitiveValue, ValueKind};
fn get_variant_string(ctx: &ParseContext<'_>) -> Result<Option<String>, ParseError> {
let variant_ctx = ctx.ext_optional("variant");
match variant_ctx {
Some(var_ctx) => {
let node = var_ctx.node();
match &node.content {
NodeValue::Primitive(PrimitiveValue::Text(t)) => Ok(Some(t.as_str().to_string())),
_ => Err(ParseError {
node_id: var_ctx.node_id(),
kind: ParseErrorKind::TypeMismatch {
expected: ValueKind::Text,
actual: node.content.value_kind(),
},
}),
}
}
None => Ok(None),
}
}
fn parse_type_reference_string(
node_id: NodeId,
s: &str,
) -> Result<ParsedSchemaNodeContent, ParseError> {
if s.is_empty() {
return Err(ParseError {
node_id,
kind: ParseErrorKind::InvalidPattern {
kind: "type reference".to_string(),
reason: "expected non-empty type reference, got empty string".to_string(),
},
});
}
let segments: Vec<&str> = s.split('.').collect();
match segments.as_slice() {
["text"] => Ok(ParsedSchemaNodeContent::Text(TextSchema::default())),
["integer"] => Ok(ParsedSchemaNodeContent::Integer(ParsedIntegerSchema {
range: None,
multiple_of: None,
})),
["float"] => Ok(ParsedSchemaNodeContent::Float(ParsedFloatSchema {
range: None,
multiple_of: None,
precision: None,
})),
["boolean"] => Ok(ParsedSchemaNodeContent::Boolean),
["null"] => Ok(ParsedSchemaNodeContent::Null),
["any"] => Ok(ParsedSchemaNodeContent::Any),
["text", lang] => Ok(ParsedSchemaNodeContent::Text(TextSchema {
language: Some((*lang).to_string()),
..Default::default()
})),
["$types", type_name] => {
let name: Identifier = type_name.parse().map_err(|e| ParseError {
node_id,
kind: ParseErrorKind::InvalidIdentifier(e),
})?;
Ok(ParsedSchemaNodeContent::Reference(TypeReference {
namespace: None,
name,
}))
}
["$types", namespace, type_name] => {
let name: Identifier = type_name.parse().map_err(|e| ParseError {
node_id,
kind: ParseErrorKind::InvalidIdentifier(e),
})?;
Ok(ParsedSchemaNodeContent::Reference(TypeReference {
namespace: Some((*namespace).to_string()),
name,
}))
}
_ => Err(ParseError {
node_id,
kind: ParseErrorKind::InvalidPattern {
kind: "type reference".to_string(),
reason: format!(
"expected 'text', 'integer', '$types.name', etc., got '{}'",
s
),
},
}),
}
}
fn parse_primitive_as_schema(
ctx: &ParseContext<'_>,
prim: &PrimitiveValue,
) -> Result<ParsedSchemaNodeContent, ParseError> {
let node_id = ctx.node_id();
match prim {
PrimitiveValue::Text(t) => {
match &t.language {
Language::Implicit => parse_type_reference_string(node_id, t.as_str()),
Language::Other(lang) if lang == "eure-path" => {
parse_type_reference_string(node_id, t.as_str())
}
_ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
}
}
_ => Ok(ParsedSchemaNodeContent::Literal(node_id)),
}
}
fn parse_map_as_schema(
ctx: &ParseContext<'_>,
variant: Option<String>,
) -> Result<ParsedSchemaNodeContent, ParseError> {
let node_id = ctx.node_id();
match variant.as_deref() {
Some("text") => Ok(ParsedSchemaNodeContent::Text(ctx.parse()?)),
Some("integer") => Ok(ParsedSchemaNodeContent::Integer(ctx.parse()?)),
Some("float") => Ok(ParsedSchemaNodeContent::Float(ctx.parse()?)),
Some("boolean") => Ok(ParsedSchemaNodeContent::Boolean),
Some("null") => Ok(ParsedSchemaNodeContent::Null),
Some("any") => Ok(ParsedSchemaNodeContent::Any),
Some("array") => Ok(ParsedSchemaNodeContent::Array(ctx.parse()?)),
Some("map") => Ok(ParsedSchemaNodeContent::Map(ctx.parse()?)),
Some("tuple") => Ok(ParsedSchemaNodeContent::Tuple(ctx.parse()?)),
Some("union") => Ok(ParsedSchemaNodeContent::Union(ctx.parse()?)),
Some("literal") => Ok(ParsedSchemaNodeContent::Literal(node_id)),
Some("record") | None => Ok(ParsedSchemaNodeContent::Record(ctx.parse()?)),
Some(other) => Err(ParseError {
node_id,
kind: ParseErrorKind::UnknownVariant(other.to_string()),
}),
}
}
impl FromEure<'_> for ParsedSchemaNodeContent {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let node_id = ctx.node_id();
let node = ctx.node();
let variant = get_variant_string(ctx)?;
match &node.content {
NodeValue::Hole(_) => Err(ParseError {
node_id,
kind: ParseErrorKind::UnexpectedHole,
}),
NodeValue::Primitive(prim) => {
if variant.as_deref() == Some("literal") {
return Ok(ParsedSchemaNodeContent::Literal(node_id));
}
parse_primitive_as_schema(ctx, prim)
}
NodeValue::Array(arr) => {
if arr.len() == 1 {
Ok(ParsedSchemaNodeContent::Array(ParsedArraySchema {
item: arr.get(0).unwrap(),
min_length: None,
max_length: None,
unique: false,
contains: None,
binding_style: None,
}))
} else {
Err(ParseError {
node_id,
kind: ParseErrorKind::InvalidPattern {
kind: "array schema shorthand".to_string(),
reason: format!(
"expected single-element array [type], got {}-element array",
arr.len()
),
},
})
}
}
NodeValue::Tuple(tup) => {
Ok(ParsedSchemaNodeContent::Tuple(ParsedTupleSchema {
elements: tup.to_vec(),
binding_style: None,
}))
}
NodeValue::Map(_) => parse_map_as_schema(ctx, variant),
NodeValue::PartialMap(_) => Err(ParseError {
node_id,
kind: ParseErrorKind::TypeMismatch {
expected: ValueKind::Map,
actual: ValueKind::PartialMap,
},
}),
}
}
}
impl FromEure<'_> for ParsedSchemaNode {
type Error = ParseError;
fn parse(ctx: &ParseContext<'_>) -> Result<Self, Self::Error> {
let flatten_ctx = ctx.flatten();
let ext_types = parse_ext_types(&flatten_ctx)?;
let metadata = ParsedSchemaMetadata::parse_from_extensions(&flatten_ctx)?;
let codegen = flatten_ctx.ext_optional("codegen").map(|ctx| ctx.node_id());
let content = flatten_ctx.parse::<ParsedSchemaNodeContent>()?;
Ok(ParsedSchemaNode {
content,
metadata,
ext_types,
codegen,
})
}
}
fn parse_ext_types(
ctx: &ParseContext<'_>,
) -> Result<IndexMap<Identifier, ParsedExtTypeSchema>, ParseError> {
let ext_type_ctx = ctx.ext_optional("ext-type");
let mut result = IndexMap::new();
if let Some(ext_type_ctx) = ext_type_ctx {
let rec = ext_type_ctx.parse_record()?;
let ext_fields: Vec<_> = rec
.unknown_fields()
.map(|r| {
r.map_err(|(key, ctx)| ParseError {
node_id: ctx.node_id(),
kind: ParseErrorKind::InvalidKeyType(key.clone()),
})
})
.collect::<Result<Vec<_>, _>>()?;
for (name, type_ctx) in ext_fields {
let ident: Identifier = name.parse().map_err(|e| ParseError {
node_id: ext_type_ctx.node_id(),
kind: ParseErrorKind::InvalidIdentifier(e),
})?;
let schema = type_ctx.parse::<ParsedExtTypeSchema>()?;
result.insert(ident, schema);
}
rec.allow_unknown_fields()?;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interop::VariantRepr;
use eure_document::document::EureDocument;
use eure_document::document::node::NodeValue;
use eure_document::text::Text;
use eure_document::value::PrimitiveValue;
fn create_text_node(doc: &mut EureDocument, text: &str) -> NodeId {
let root_id = doc.get_root_id();
doc.node_mut(root_id).content =
NodeValue::Primitive(PrimitiveValue::Text(Text::plaintext(text.to_string())));
root_id
}
#[test]
fn test_binding_style_parse() {
let mut doc = EureDocument::new();
let node_id = create_text_node(&mut doc, "section");
let result: BindingStyle = doc.parse(node_id).unwrap();
assert_eq!(result, BindingStyle::Section);
}
#[test]
fn test_binding_style_parse_unknown() {
let mut doc = EureDocument::new();
let node_id = create_text_node(&mut doc, "unknown");
let result: Result<BindingStyle, _> = doc.parse(node_id);
let err = result.unwrap_err();
assert_eq!(
err.kind,
ParseErrorKind::UnknownVariant("unknown".to_string())
);
}
#[test]
fn test_description_parse_default() {
let mut doc = EureDocument::new();
let node_id = create_text_node(&mut doc, "Hello world");
let result: Description = doc.parse(node_id).unwrap();
assert!(matches!(result, Description::String(s) if s == "Hello world"));
}
#[test]
fn test_variant_repr_parse_string() {
let mut doc = EureDocument::new();
let node_id = create_text_node(&mut doc, "untagged");
let result: VariantRepr = doc.parse(node_id).unwrap();
assert_eq!(result, VariantRepr::Untagged);
}
}