This is an automated email from the ASF dual-hosted git repository. Kriskras99 pushed a commit to branch feat/full_enum_support in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit 06bfaf48da37ac4e5cb6f1218ff5e86283926bcc Author: Kriskras99 <[email protected]> AuthorDate: Sun Mar 22 21:43:55 2026 +0100 wip: derive --- Cargo.lock | 1 + avro_derive/Cargo.toml | 1 + avro_derive/src/attributes/avro.rs | 121 +++++++++- avro_derive/src/attributes/mod.rs | 450 ++++++++++++++++++++++++++++-------- avro_derive/src/attributes/serde.rs | 34 +-- avro_derive/src/lib.rs | 185 +++++++++------ 6 files changed, 604 insertions(+), 188 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 239fd96..8203033 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "darling", "macrotest", "pretty_assertions", + "prettyplease", "proc-macro2", "proptest", "quote", diff --git a/avro_derive/Cargo.toml b/avro_derive/Cargo.toml index 15e6971..f6798f3 100644 --- a/avro_derive/Cargo.toml +++ b/avro_derive/Cargo.toml @@ -43,6 +43,7 @@ uuid = { workspace = true } apache-avro = { default-features = false, path = "../avro", features = ["derive"] } macrotest = { version = "1.2.1", default-features = false } pretty_assertions = { workspace = true } +prettyplease = "0.2.37" proptest = { default-features = false, version = "1.11.0", features = ["std"] } rustversion = "1.0.22" serde = { workspace = true } diff --git a/avro_derive/src/attributes/avro.rs b/avro_derive/src/attributes/avro.rs index e0a1433..5a0b328 100644 --- a/avro_derive/src/attributes/avro.rs +++ b/avro_derive/src/attributes/avro.rs @@ -21,12 +21,39 @@ //! Although a user will mostly use the Serde attributes, there are some Avro specific attributes //! a user can use. These add extra metadata to the generated schema. -use crate::attributes::FieldDefault; use crate::case::RenameRule; use darling::FromMeta; use proc_macro2::Span; +use serde_json::Value; use syn::Expr; +/// What `Schema` representation to generate for a type. +#[derive(Debug, FromMeta, PartialEq, Clone, Copy)] +pub enum Repr { + /// Generate a `Schema::Enum` for a `enum`. + /// + /// Only works for unit variants. + Enum, + /// Generate a `Schema::Union` for a `enum`. + /// + /// Requires `#[serde(untagged)]` + /// + /// There can only be one unit variant and every newtype/struct/tuple variant must be unique. + BareUnion, + /// Generate a `Schema::Union` with a `Schema::Record` for a `enum`. + /// + /// This works for every enum as the records will have unique names for every variant. + UnionOfRecords, + /// Generate a `Schema::Record` with a tag and content field for a `enum`. + /// + /// Requires `#[serde(tag = "..", content = "..")]`. + RecordTagContent, + /// Generate a `Schema::Record` with a tag field and flattened variant fields for a `enum`. + /// + /// Requires `#[serde(tag = "..")]` + RecordInternallyTagged, +} + /// All the Avro attributes a container can have. #[derive(darling::FromAttributes)] #[darling(attributes(avro))] @@ -57,6 +84,9 @@ pub struct ContainerAttributes { /// Set the default value if this schema is used as a field #[darling(default)] pub default: Option<String>, + /// Force the generator to use a certain schema representation + #[darling(default)] + pub repr: Option<Repr>, } impl ContainerAttributes { @@ -78,6 +108,28 @@ impl ContainerAttributes { } } +/// How to get the schema for a variant. +#[derive(Debug, PartialEq, Default)] +pub enum With { + /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`. + #[default] + Trait, + /// Use `module::get_schema_in_ctxt` where the module is defined by Serde's `with` attribute. + Serde, + /// Call the function in this expression. + Expr(Expr), +} + +impl FromMeta for With { + fn from_word() -> darling::Result<Self> { + Ok(Self::Serde) + } + + fn from_expr(expr: &Expr) -> darling::Result<Self> { + Ok(Self::Expr(expr.clone())) + } +} + /// All the Avro attributes a variant can have. #[derive(darling::FromAttributes)] #[darling(attributes(avro))] @@ -89,6 +141,19 @@ pub struct VariantAttributes { /// [`serde::VariantAttributes::rename`]: crate::attributes::serde::VariantAttributes::rename #[darling(default)] pub rename: Option<String>, + /// How to get the schema for a variant. + /// + /// By default uses `<T as AvroSchemaComponent>::get_schema_in_ctxt`. + /// + /// When it's provided without an argument (`#[avro(with)]`), it will use the function `get_schema_in_ctxt` defined + /// in the same module as the `#[serde(with = "some_module")]` attribute. + /// + /// When it's provided with an argument (`#[avro(with = some_fn)]`), it will use that function. + #[darling(default)] + pub with: With, + /// Adds a `doc` field to the schema. + #[darling(default)] + pub doc: Option<String>, } impl VariantAttributes { @@ -103,21 +168,59 @@ impl VariantAttributes { } } -/// How to get the schema for a field. -#[derive(Debug, FromMeta, PartialEq, Default)] -#[darling(from_expr = |expr| Ok(With::Expr(expr.clone())))] -pub enum With { - /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`. +/// How to get the default value for a value. +#[derive(Debug, PartialEq, Default)] +pub enum FieldDefault { + /// Use `<T as AvroSchemaComponent>::field_default`. #[default] - #[darling(skip)] Trait, - /// Use `module::get_schema_in_ctxt` where the module is defined by Serde's `with` attribute. - #[darling(word, skip)] + /// Don't set a default. + Disabled, + /// Use `module::field_default` where the module is defined by Serde's `with` attribute. Serde, + /// Use this JSON value. + Value(Value), /// Call the function in this expression. Expr(Expr), } +impl FromMeta for FieldDefault { + fn from_word() -> darling::Result<Self> { + Ok(Self::Serde) + } + + fn from_expr(expr: &Expr) -> darling::Result<Self> { + match expr { + Expr::Lit(lit) => Self::from_value(&lit.lit), + Expr::Group(group) => { + // syn may generate this invisible group delimiter when the input to the darling + // proc macro (specifically, the attributes) are generated by a + // macro_rules! (e.g. propagating a macro_rules!'s expr) + // Since we want to basically ignore these invisible group delimiters, + // we just propagate the call to the inner expression. + Self::from_expr(&group.expr) + } + expr => Ok(Self::Expr(expr.clone())), + } + } + + fn from_string(value: &str) -> darling::Result<Self> { + serde_json::from_str(value) + .map(Self::Value) + .map_err(darling::Error::custom) + } + + fn from_bool(value: bool) -> darling::Result<Self> { + if value { + Err(darling::Error::custom( + "Expected `false` or a JSON string, got `true`", + )) + } else { + Ok(Self::Disabled) + } + } +} + /// All the Avro attributes a field can have. #[derive(darling::FromAttributes)] #[darling(attributes(avro))] diff --git a/avro_derive/src/attributes/mod.rs b/avro_derive/src/attributes/mod.rs index ebb2997..8d97b3f 100644 --- a/avro_derive/src/attributes/mod.rs +++ b/avro_derive/src/attributes/mod.rs @@ -19,19 +19,126 @@ use crate::case::RenameRule; use darling::{FromAttributes, FromMeta}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::{AttrStyle, Attribute, Expr, Ident, Path, spanned::Spanned}; +use serde_json::Value; +use std::cmp::PartialEq; +use syn::{AttrStyle, Attribute, Expr, Field, Ident, Path, Type, spanned::Spanned}; mod avro; mod serde; -#[derive(Default)] +/// What `Schema` representation to generate for a type. +#[derive(Debug, PartialEq)] +pub enum Repr { + /// Generate a `Schema::Enum` for a `enum`. + /// + /// Only works for unit variants. + Enum, + /// Generate a `Schema::Union` for a `enum`. + /// + /// Requires `#[serde(untagged)]` + /// + /// There can only be one unit variant and every newtype/struct/tuple variant must be unique. + BareUnion, + /// Generate a `Schema::Union` with a `Schema::Record` for a `enum`. + /// + /// This works for every enum as the records will have unique names for every variant. + UnionOfRecords, + /// Generate a `Schema::Record` with a tag and content field for a `enum`. + /// + /// Requires `#[serde(tag = "..", content = "..")]`. + RecordTagContent { tag: String, content: String }, + /// Generate a `Schema::Record` with a tag field and flattened variant fields for a `enum`. + /// + /// Requires `#[serde(tag = "..")]` + RecordInternallyTagged { tag: String }, +} + +impl Repr { + fn from_avro_and_serde( + avro: Option<avro::Repr>, + tag: Option<String>, + content: Option<String>, + untagged: bool, + span: Span, + ) -> Result<Option<Self>, syn::Error> { + match avro { + Some(avro::Repr::Enum) => { + if tag.is_some() || content.is_some() || untagged { + Err(syn::Error::new( + span, + r#"AvroSchema: `#[avro(repr = "enum")]` is incompatible with `#[serde(tag = "..")]`, `#[serde(content = "..")]`, and `#[serde(untagged)]`"#, + )) + } else { + Ok(Some(Self::Enum)) + } + } + Some(avro::Repr::BareUnion) => { + if tag.is_some() || content.is_some() { + Err(syn::Error::new( + span, + r#"AvroSchema: `#[avro(repr = "bare_union")]` is incompatible with `#[serde(tag = "..")]` and `#[serde(content = "..")]`"#, + )) + } else { + Ok(Some(Self::BareUnion)) + } + } + Some(avro::Repr::UnionOfRecords) => { + if tag.is_some() || content.is_some() || untagged { + Err(syn::Error::new( + span, + r#"AvroSchema: `#[avro(repr = "union_of_records")]` is incompatible with `#[serde(tag = "..")]`, `#[serde(content = "..")]`, and `#[serde(untagged)]`"#, + )) + } else { + Ok(Some(Self::UnionOfRecords)) + } + } + Some(avro::Repr::RecordTagContent) => { + if let Some(tag) = tag + && let Some(content) = content + && !untagged + { + Ok(Some(Self::RecordTagContent { tag, content })) + } else { + Err(syn::Error::new( + span, + r#"AvroSchema: `#[avro(repr = "record_tag_content")]` requires `#[serde(tag = "..", content = "..")]` and is incompatible with `#[serde(untagged)]`"#, + )) + } + } + Some(avro::Repr::RecordInternallyTagged) => { + if let Some(tag) = tag + && content.is_none() + && !untagged + { + Ok(Some(Self::RecordInternallyTagged { tag })) + } else { + Err(syn::Error::new( + span, + r#"AvroSchema: `#[avro(repr = "record_internally_tagged")]` requires `#[serde(tag = "..")]` and is incompatible with `#[serde(content = "..")]` and `#[serde(untagged)]`"#, + )) + } + } + None if tag.is_some() && content.is_some() => Ok(Some(Self::RecordTagContent { + tag: tag.unwrap(), + content: content.unwrap(), + })), + None if tag.is_some() => Ok(Some(Self::RecordInternallyTagged { tag: tag.unwrap() })), + None if untagged => Ok(Some(Self::BareUnion)), + None => Ok(None), + } + } +} + pub struct NamedTypeOptions { pub name: String, pub doc: Option<String>, pub aliases: Vec<String>, pub rename_all: RenameRule, + pub rename_all_fields: RenameRule, pub transparent: bool, pub default: TokenStream, + pub repr: Option<Repr>, + pub with: Option<Path>, } impl NamedTypeOptions { @@ -52,27 +159,28 @@ impl NamedTypeOptions { let mut errors = Vec::new(); // Check for any Serde attributes that are hard errors - if serde.tag.is_some() - || serde.content.is_some() - || serde.untagged - || serde.variant_identifier - || serde.field_identifier - { + if serde.variant_identifier || serde.field_identifier { errors.push(syn::Error::new( span, - "AvroSchema derive does not support changing the tagging Serde generates (`tag`, `content`, `untagged`, `variant_identifier`, `field_identifier`)", + r#"AvroSchema: `#[serde(variant_identifier)]` and `#[serde(field_identifier)]` are not supported"#, )); } - if serde.remote.is_some() { + if serde.rename_all.deserialize != serde.rename_all.serialize { errors.push(syn::Error::new( span, - "AvroSchema derive does not support the Serde `remote` attribute", + r#"AvroSchema: rename rules for serializing and deserializing must match (`rename_all(serialize = "..", deserialize = "..")`)"# )); } - if serde.rename_all.deserialize != serde.rename_all.serialize { + if serde.rename_all_fields.deserialize != serde.rename_all_fields.serialize { errors.push(syn::Error::new( span, - r#"AvroSchema derive does not support different rename rules for serializing and deserializing (`rename_all(serialize = "..", deserialize = "..")`)"# + r#"AvroSchema: rename rules for serializing and deserializing must match (`rename_all_fields(serialize = "..", deserialize = "..")`)"# + )); + } + if serde.from != serde.into && serde.try_from != serde.into { + errors.push(syn::Error::new( + span, + r#"AvroSchema: `#[serde({try_,}from = "..")]` must match `#[serde(into = "..")]`"#, )); } @@ -80,13 +188,13 @@ impl NamedTypeOptions { if avro.name.is_some() && avro.name != serde.rename { errors.push(syn::Error::new( span, - r#"#[avro(name = "..")] must match #[serde(rename = "..")], it's also deprecated. Please use only `#[serde(rename = "..")]`"#, + r#"AvroSchema: #[avro(name = "..")] must match #[serde(rename = "..")] and it's deprecated. Please use only `#[serde(rename = "..")]`"#, )); } if avro.rename_all != RenameRule::None && serde.rename_all.serialize != avro.rename_all { errors.push(syn::Error::new( span, - r#"#[avro(rename_all = "..")] must match #[serde(rename_all = "..")], it's also deprecated. Please use only `#[serde(rename_all = "..")]`"#, + r#"AvroSchema: #[avro(rename_all = "..")] must match #[serde(rename_all = "..")] and it's deprecated. Please use only `#[serde(rename_all = "..")]`"#, )); } if serde.transparent @@ -96,14 +204,86 @@ impl NamedTypeOptions { || avro.doc.is_some() || !avro.alias.is_empty() || avro.rename_all != RenameRule::None + || avro.repr.is_some() + || serde.rename_all.serialize != RenameRule::None + || serde.rename_all.deserialize != RenameRule::None + || serde.rename_all_fields.serialize != RenameRule::None + || serde.rename_all_fields.deserialize != RenameRule::None + || serde.untagged + || serde.tag.is_some() + || serde.content.is_some() + || serde.into.is_some() + || serde.from.is_some() + || serde.try_from.is_some()) + { + errors.push(syn::Error::new( + span, + "AvroSchema: `#[serde(transparent)]` is incompatible with all other attributes", + )); + } + if serde.into.is_some() + && (serde.rename.is_some() + || avro.name.is_some() + || avro.namespace.is_some() + || avro.doc.is_some() + || !avro.alias.is_empty() + || avro.rename_all != RenameRule::None + || avro.repr.is_some() || serde.rename_all.serialize != RenameRule::None - || serde.rename_all.deserialize != RenameRule::None) + || serde.rename_all.deserialize != RenameRule::None + || serde.rename_all_fields.serialize != RenameRule::None + || serde.rename_all_fields.deserialize != RenameRule::None + || serde.untagged + || serde.tag.is_some() + || serde.content.is_some()) { errors.push(syn::Error::new( span, - "AvroSchema: #[serde(transparent)] is incompatible with all other attributes", + r#"AvroSchema: `#[serde({,try_}from = "..", into = "..")]` are incompatible with all other attributes"#, )); } + let repr = match Repr::from_avro_and_serde( + avro.repr, + serde.tag, + serde.content, + serde.untagged, + span, + ) { + Ok(repr) => repr, + Err(err) => { + errors.push(err); + None + } + }; + + let default = match avro.default { + None => quote! { None }, + Some(default_value) => { + if let Err(err) = serde_json::from_str::<serde_json::Value>(&default_value[..]) { + errors.push(syn::Error::new( + span, + format!("Invalid Avro `default` JSON: \n{err}"), + )); + quote! { ::std::option::Option::None } + } else { + quote! { + ::std::option::Option::Some(::serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str())) + } + } + } + }; + + let with = match serde.into.as_deref().map(Path::from_string) { + Some(Ok(path)) => Some(path), + Some(Err(err)) => { + errors.push(syn::Error::new( + span, + format!(r#"AvroSchema: Expected a path for `#[serde(into = "..")]`: {err:?}"#), + )); + None + } + None => None, + }; if !errors.is_empty() { return Err(errors); @@ -118,35 +298,70 @@ impl NamedTypeOptions { let doc = avro.doc.or_else(|| extract_rustdoc(attributes)); - let default = match avro.default { - None => quote! { None }, - Some(default_value) => { - let _: serde_json::Value = - serde_json::from_str(&default_value[..]).map_err(|e| { - vec![syn::Error::new( - ident.span(), - format!("Invalid Avro `default` JSON: \n{e}"), - )] - })?; - quote! { - Some(::serde_json::from_str(#default_value).expect(format!("Invalid JSON: {:?}", #default_value).as_str())) - } - } - }; - Ok(Self { name: full_schema_name, doc, aliases: avro.alias, rename_all: serde.rename_all.serialize, + rename_all_fields: serde.rename_all_fields.serialize, transparent: serde.transparent, default, + repr, + with, }) } } +/// How to get the schema for this field or variant. +#[derive(Debug, PartialEq, Default, Clone)] +pub enum With { + /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`. + #[default] + Trait, + /// Use `module::get_schema_in_ctxt` where the module is defined by Serde's `with` attribute. + Serde(Path), + /// Call the function in this expression. + Expr(Expr), +} + +impl With { + fn from_avro_and_serde( + avro: &avro::With, + serde: &Option<String>, + span: Span, + ) -> Result<Self, syn::Error> { + match &avro { + avro::With::Trait => Ok(Self::Trait), + avro::With::Serde => { + if let Some(serde) = serde { + let path = Path::from_string(serde).map_err(|err| { + syn::Error::new( + span, + format!( + r#"AvroSchema: Expected a path for `#[serde(with = "..")]`: {err:?}"# + ), + ) + })?; + Ok(Self::Serde(path)) + } else { + Err(syn::Error::new( + span, + r#"`#[avro(with)]` requires `#[serde(with = "some_module")]` or provide a function to call `#[avro(with = some_fn)]`"#, + )) + } + } + avro::With::Expr(expr) => Ok(Self::Expr(expr.clone())), + } + } +} + pub struct VariantOptions { + pub doc: Option<String>, pub rename: Option<String>, + pub rename_all: RenameRule, + pub aliases: Vec<String>, + pub skip: bool, + pub with: With, } impl VariantOptions { @@ -165,7 +380,19 @@ impl VariantOptions { if serde.other || serde.untagged { errors.push(syn::Error::new( span, - "AvroSchema derive does not support changing the tagging Serde generates (`other`, `untagged`)", + r#"AvroSchema: `#[serde(other)]` and `#[serde(untagged)]` are not supported"#, + )); + } + if serde.rename_all.deserialize != serde.rename_all.serialize { + errors.push(syn::Error::new( + span, + r#"AvroSchema: rename rules for serializing and deserializing must match (`rename_all(serialize = "..", deserialize = "..")`)"# + )); + } + if serde.skip_serializing != serde.skip_deserializing { + errors.push(syn::Error::new( + span, + "`#[serde(skip_serializing)]` must match `#[serde(skip_deserializing)]`", )); } @@ -176,88 +403,120 @@ impl VariantOptions { r#"`#[avro(rename = "..")]` must match `#[serde(rename = "..")]`, it's also deprecated. Please use only `#[serde(rename = "..")]`"# )); } + let with = match With::from_avro_and_serde(&avro.with, &serde.with, span) { + Ok(with) => with, + Err(error) => { + errors.push(error); + // This won't actually be used, but it does simplify the code + With::Trait + } + }; if !errors.is_empty() { return Err(errors); } + let doc = avro.doc.or_else(|| extract_rustdoc(attributes)); + Ok(Self { rename: serde.rename, + rename_all: serde.rename_all.serialize, + aliases: serde.alias, + doc, + skip: serde.skip || serde.skip_serializing, + with, }) } } -/// How to get the schema for this field or variant. -#[derive(Debug, PartialEq, Default, Clone)] -pub enum With { - /// Use `<T as AvroSchemaComponent>::get_schema_in_ctxt`. - #[default] +/// How to get the default value for a value. +#[derive(PartialEq)] +pub enum FieldDefault { + /// Use `<T as AvroSchemaComponent>::field_default`. Trait, - /// Use `module::get_schema_in_ctxt` where the module is defined by Serde's `with` attribute. + /// Don't set a default. + Disabled, + /// Use `module::field_default` where the module is defined by Serde's `with` attribute. Serde(Path), + /// Use this JSON value. + Value(Value), /// Call the function in this expression. Expr(Expr), } -impl With { +impl FieldDefault { fn from_avro_and_serde( - avro: &avro::With, - serde: Option<&String>, + avro: avro::FieldDefault, + with: &With, span: Span, ) -> Result<Self, syn::Error> { - match &avro { - avro::With::Trait => Ok(Self::Trait), - avro::With::Serde => { - if let Some(serde) = serde { - let path = Path::from_string(serde).map_err(|err| { - syn::Error::new( - span, - format!( - r#"AvroSchema: Expected a path for `#[serde(with = "..")]`: {err:?}"# - ), - ) - })?; - Ok(Self::Serde(path)) + match avro { + // Only get the default from the type if the schema is also retrieved from the type + avro::FieldDefault::Trait if with == &With::Trait => Ok(Self::Trait), + avro::FieldDefault::Trait | avro::FieldDefault::Disabled => Ok(Self::Disabled), + avro::FieldDefault::Serde => { + if let With::Serde(path) = with { + Ok(Self::Serde(path.clone())) } else { Err(syn::Error::new( span, - r#"`#[avro(with)]` requires `#[serde(with = "some_module")]` or provide a function to call `#[avro(with = some_fn)]`"#, + r##"`#[avro(default)]` requires `#[serde(with = "some_module")]` or provide a function to call `#[avro(default = some_fn)]` or a JSON value `#[avro(default = r#""string""#)]`"##, )) } } - avro::With::Expr(expr) => Ok(Self::Expr(expr.clone())), + avro::FieldDefault::Value(value) => Ok(Self::Value(value)), + avro::FieldDefault::Expr(expr) => Ok(Self::Expr(expr)), } } -} -/// How to get the default value for a value. -#[derive(Debug, PartialEq, Default)] -pub enum FieldDefault { - /// Use `<T as AvroSchemaComponent>::field_default`. - #[default] - Trait, - /// Don't set a default. - Disabled, - /// Use this JSON value. - Value(String), -} - -impl FromMeta for FieldDefault { - fn from_string(value: &str) -> darling::Result<Self> { - Ok(Self::Value(value.to_string())) - } - fn from_bool(value: bool) -> darling::Result<Self> { - if value { - Err(darling::Error::custom( - "Expected `false` or a JSON string, got `true`", - )) - } else { - Ok(Self::Disabled) + pub fn to_expr(&self, field: &Field) -> Result<TokenStream, syn::Error> { + match self { + FieldDefault::Trait => { + let ty = &field.ty; + match &field.ty { + Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => { + Ok(quote! {<#ty as :: apache_avro::AvroSchemaComponent>::field_default()}) + } + Type::Ptr(_) => Err(syn::Error::new( + ty.span(), + "AvroSchema: Raw pointers are not supported", + )), + _ => Err(syn::Error::new( + ty.span(), + format!("AvroSchema: Unexpected type encountered: {ty:?}"), + )), + } + } + FieldDefault::Disabled => Ok(quote! { ::std::option::Option::None }), + FieldDefault::Serde(path) => Ok(quote! { #path::field_default() }), + FieldDefault::Value(value) => { + let value = serde_json::to_string(&value) + .expect("serde_json::Value will always successfully serialize"); + Ok(quote! { + ::std::option::Option::Some(::serde_json::from_str(#value).expect("Unreachable! This is checked at compile time!")) + }) + } + FieldDefault::Expr(Expr::Closure(closure)) => { + if closure.inputs.is_empty() { + Ok(quote! { (#closure)() }) + } else { + Err(syn::Error::new( + field.span(), + "AvroSchema: Expected closure with 0 parameters for `#[avro(default = ..)]`", + )) + } + } + FieldDefault::Expr(Expr::Path(path)) => { + Ok(quote! { #path(named_schemas, enclosing_namespace) }) + } + FieldDefault::Expr(_expr) => Err(syn::Error::new( + field.span(), + "AvroSchema: Invalid expression, expected function or closure for `#[avro(default = ..)]`", + )), } } } -#[derive(Default)] pub struct FieldOptions { pub doc: Option<String>, pub default: FieldDefault, @@ -318,8 +577,7 @@ impl FieldOptions { r#"`#[avro(alias = "..")]` must match `#[serde(alias = "..")]`, it's also deprecated. Please use only `#[serde(alias = "..")]`"# )); } - - let with = match With::from_avro_and_serde(&avro.with, serde.with.as_ref(), span) { + let with = match With::from_avro_and_serde(&avro.with, &serde.with, span) { Ok(with) => with, Err(error) => { errors.push(error); @@ -327,19 +585,21 @@ impl FieldOptions { With::Trait } }; - // TODO: Implement a better way to do this (maybe if user specifies `#[avro(with)]` also use that for the default) - // Disable getting the field default, if the schema is not retrieved from the field type - if with != With::Trait && avro.default == FieldDefault::Trait { - avro.default = FieldDefault::Disabled; - } - + let default = match FieldDefault::from_avro_and_serde(avro.default, &with, span) { + Ok(with) => with, + Err(error) => { + errors.push(error); + // This won't actually be used, but it does simplify the code + FieldDefault::Disabled + } + }; if ((serde.skip_serializing && !serde.skip_deserializing) || serde.skip_serializing_if.is_some()) - && avro.default == FieldDefault::Disabled + && default == FieldDefault::Disabled { errors.push(syn::Error::new( span, - "`#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = false)]`" + "`#[serde(skip_serializing)]` and `#[serde(skip_serializing_if)]` are incompatible with `#[avro(default = false)]` and `#[avro(with)]` without `#[avro(default)]`" )); } @@ -351,7 +611,7 @@ impl FieldOptions { Ok(Self { doc, - default: avro.default, + default, alias: serde.alias, rename: serde.rename, skip: serde.skip || (serde.skip_serializing && serde.skip_deserializing), @@ -396,7 +656,7 @@ fn darling_to_syn(e: darling::Error) -> Vec<syn::Error> { fn warn(span: Span, message: &str, help: &str) { proc_macro::Diagnostic::spanned(span.unwrap(), proc_macro::Level::Warning, message) .help(help) - .emit(); + .emit() } #[cfg(not(nightly))] diff --git a/avro_derive/src/attributes/serde.rs b/avro_derive/src/attributes/serde.rs index b1ca6fa..c9ed8a5 100644 --- a/avro_derive/src/attributes/serde.rs +++ b/avro_derive/src/attributes/serde.rs @@ -92,8 +92,8 @@ pub struct ContainerAttributes { #[darling(default)] pub rename_all: RenameAll, /// Rename all the fields of the struct variants in this enum. - #[darling(default, rename = "rename_all_fields")] - pub _rename_all_fields: RenameAll, + #[darling(default)] + pub rename_all_fields: RenameAll, /// Error when encountering unknown fields when deserialising. #[darling(default, rename = "deny_unknown_fields")] pub _deny_unknown_fields: bool, @@ -117,20 +117,20 @@ pub struct ContainerAttributes { pub _default: Option<SerdeDefault>, /// This type is the serde implementation for a "remote" type. /// - /// This makes the (de)serialisation use/return a different type. - pub remote: Option<String>, + /// Inverse of `#[serde(from = "..", into = "..")]. + pub _remote: Option<String>, /// Directly use the inner type for (de)serialisation. #[darling(default)] pub transparent: bool, /// Deserialize using the given type and then convert to this type with `From`. - #[darling(default, rename = "from")] - pub _from: Option<String>, + #[darling(default)] + pub from: Option<String>, /// Deserialize using the given type and then convert to this type with `TryFrom`. - #[darling(default, rename = "try_from")] - pub _try_from: Option<String>, + #[darling(default)] + pub try_from: Option<String>, /// Convert this type to the given type using `Into` and then serialize using the given type. - #[darling(default, rename = "into")] - pub _into: Option<String>, + #[darling(default)] + pub into: Option<String>, /// Use the Serde API at this path. #[darling(default, rename = "crate")] pub _crate: Option<String>, @@ -154,18 +154,18 @@ pub struct VariantAttributes { pub rename: Option<String>, /// Aliases for this variant, only used during deserialisation. #[darling(multiple, rename = "alias")] - pub _alias: Vec<String>, - #[darling(default, rename = "rename_all")] - pub _rename_all: RenameAll, + pub alias: Vec<String>, + #[darling(default)] + pub rename_all: RenameAll, /// Do not serialize or deserialize this variant. #[darling(default, rename = "skip")] - pub _skip: bool, + pub skip: bool, /// Do not serialize this variant. #[darling(default, rename = "skip_serializing")] - pub _skip_serializing: bool, + pub skip_serializing: bool, /// Do not deserialize this variant. #[darling(default, rename = "skip_deserializing")] - pub _skip_deserializing: bool, + pub skip_deserializing: bool, /// Use this function for serializing. #[darling(rename = "serialize_with")] pub _serialize_with: Option<String>, @@ -174,7 +174,7 @@ pub struct VariantAttributes { pub _deserialize_with: Option<String>, /// Use this module for (de)serializing. #[darling(rename = "with")] - pub _with: Option<String>, + pub with: Option<String>, /// Put trait bounds on the implementations. #[darling(rename = "bound")] pub _bound: Option<String>, diff --git a/avro_derive/src/lib.rs b/avro_derive/src/lib.rs index d63ed8f..31869f1 100644 --- a/avro_derive/src/lib.rs +++ b/avro_derive/src/lib.rs @@ -127,7 +127,7 @@ fn create_trait_definition( } fn field_default() -> ::std::option::Option<::serde_json::Value> { - ::std::option::Option::#field_default_impl + #field_default_impl } } } @@ -153,7 +153,8 @@ fn get_struct_schema_def( data_struct: DataStruct, ident_span: Span, ) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { - let mut record_field_exprs = vec![]; + let mut errors = Vec::new(); + let mut record_field_exprs = Vec::new(); match data_struct.fields { Fields::Named(a) => { for field in a.named { @@ -165,7 +166,13 @@ fn get_struct_schema_def( if let Some(raw_name) = name.strip_prefix("r#") { name = raw_name.to_string(); } - let field_attrs = FieldOptions::new(&field.attrs, field.span())?; + let field_attrs = match FieldOptions::new(&field.attrs, field.span()) { + Ok(field_attrs) => field_attrs, + Err(errs) => { + errors.extend(errs); + continue; + } + }; let doc = preserve_optional(field_attrs.doc); match (field_attrs.rename, container_attrs.rename_all) { (Some(rename), _) => { @@ -182,7 +189,13 @@ fn get_struct_schema_def( // Inline the fields of the child record at runtime, as we don't have access to // the schema here. let get_record_fields = - get_field_get_record_fields_expr(&field, field_attrs.with)?; + match get_field_get_record_fields_expr(&field, field_attrs.with) { + Ok(get_record_fields) => get_record_fields, + Err(err) => { + errors.push(err); + continue; + } + }; record_field_exprs.push(quote! { if let Some(flattened_fields) = #get_record_fields { schema_fields.extend(flattened_fields); @@ -194,24 +207,21 @@ fn get_struct_schema_def( // Don't add this field as it's been replaced by the child record fields continue; } - let default_value = match field_attrs.default { - FieldDefault::Disabled => quote! { ::std::option::Option::None }, - FieldDefault::Trait => type_to_field_default_expr(&field.ty)?, - FieldDefault::Value(default_value) => { - let _: serde_json::Value = serde_json::from_str(&default_value[..]) - .map_err(|e| { - vec![syn::Error::new( - field.ident.span(), - format!("Invalid avro default json: \n{e}"), - )] - })?; - quote! { - ::std::option::Option::Some(::serde_json::from_str(#default_value).expect("Unreachable! This parsed at compile time!")) - } + let default_value = match field_attrs.default.to_expr(&field) { + Ok(default_value) => default_value, + Err(err) => { + errors.push(err); + continue; } }; let aliases = field_aliases(&field_attrs.alias); - let schema_expr = get_field_schema_expr(&field, field_attrs.with)?; + let schema_expr = match get_field_schema_expr(&field, field_attrs.with) { + Ok(schema_expr) => schema_expr, + Err(errs) => { + errors.extend(errs); + continue; + } + }; record_field_exprs.push(quote! { schema_fields.push(::apache_avro::schema::RecordField { name: #name.to_string(), @@ -225,19 +235,23 @@ fn get_struct_schema_def( } } Fields::Unnamed(_) => { - return Err(vec![syn::Error::new( + errors.push(syn::Error::new( ident_span, "AvroSchema derive does not work for tuple structs", - )]); + )); } Fields::Unit => { - return Err(vec![syn::Error::new( + errors.push(syn::Error::new( ident_span, "AvroSchema derive does not work for unit structs", - )]); + )); } } + if !errors.is_empty() { + return Err(errors); + } + let record_doc = preserve_optional(container_attrs.doc.as_ref()); let record_aliases = aliases(&container_attrs.aliases); let full_schema_name = &container_attrs.name; @@ -284,30 +298,54 @@ fn get_transparent_struct_schema_def( ) -> Result<(TokenStream, TokenStream), Vec<syn::Error>> { match fields { Fields::Named(fields_named) => { + let mut errors = Vec::new(); let mut found = None; for field in fields_named.named { - let attrs = FieldOptions::new(&field.attrs, field.span())?; + let attrs = match FieldOptions::new(&field.attrs, field.span()) { + Ok(attrs) => attrs, + Err(errs) => { + errors.extend(errs); + continue; + } + }; if attrs.skip { continue; } + let span = field.span(); if found.replace((field, attrs)).is_some() { - return Err(vec![syn::Error::new( - input_span, + errors.push(syn::Error::new( + span, "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field", - )]); + )); } } if let Some((field, attrs)) = found { - Ok(( - get_field_schema_expr(&field, attrs.with.clone())?, - get_field_get_record_fields_expr(&field, attrs.with)?, - )) + let schema_expr = match get_field_schema_expr(&field, attrs.with.clone()) { + Ok(schema_expr) => schema_expr, + Err(err) => { + errors.push(err); + TokenStream::default() + } + }; + let fields_expr = match get_field_get_record_fields_expr(&field, attrs.with) { + Ok(fields_expr) => fields_expr, + Err(err) => { + errors.push(err); + TokenStream::default() + } + }; + if !errors.is_empty() { + Err(errors) + } else { + Ok((schema_expr, fields_expr)) + } } else { - Err(vec![syn::Error::new( + errors.push(syn::Error::new( input_span, "AvroSchema: #[serde(transparent)] is only allowed on structs with one unskipped field", - )]) + )); + Err(errors) } } Fields::Unnamed(_) => Err(vec![syn::Error::new( @@ -321,7 +359,7 @@ fn get_transparent_struct_schema_def( } } -fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<syn::Error>> { +fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, syn::Error> { match with { With::Trait => Ok(type_to_schema_expr(&field.ty)?), With::Serde(path) => { @@ -331,24 +369,21 @@ fn get_field_schema_expr(field: &Field, with: With) -> Result<TokenStream, Vec<s if closure.inputs.is_empty() { Ok(quote! { (#closure)() }) } else { - Err(vec![syn::Error::new( + Err(syn::Error::new( field.span(), "Expected closure with 0 parameters", - )]) + )) } } With::Expr(Expr::Path(path)) => Ok(quote! { #path(named_schemas, enclosing_namespace) }), - With::Expr(_expr) => Err(vec![syn::Error::new( + With::Expr(_expr) => Err(syn::Error::new( field.span(), "Invalid expression, expected function or closure", - )]), + )), } } -fn get_field_get_record_fields_expr( - field: &Field, - with: With, -) -> Result<TokenStream, Vec<syn::Error>> { +fn get_field_get_record_fields_expr(field: &Field, with: With) -> Result<TokenStream, syn::Error> { match with { With::Trait => Ok(type_to_get_record_fields_expr(&field.ty)?), With::Serde(path) => { @@ -364,86 +399,86 @@ fn get_field_get_record_fields_expr( ) }) } else { - Err(vec![syn::Error::new( + Err(syn::Error::new( field.span(), "Expected closure with 0 parameters", - )]) + )) } } With::Expr(Expr::Path(path)) => Ok(quote! { ::apache_avro::serde::get_record_fields_in_ctxt(named_schemas, enclosing_namespace, #path) }), - With::Expr(_expr) => Err(vec![syn::Error::new( + With::Expr(_expr) => Err(syn::Error::new( field.span(), "Invalid expression, expected function or closure", - )]), + )), } } /// Takes in the Tokens of a type and returns the tokens of an expression with return type `Schema` -fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { +fn type_to_schema_expr(ty: &Type) -> Result<TokenStream, syn::Error> { match ty { Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}, ), - Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + Type::Ptr(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support raw pointers", - )]), - Type::Tuple(_) => Err(vec![syn::Error::new_spanned( + )), + Type::Tuple(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support tuples", - )]), - _ => Err(vec![syn::Error::new_spanned( + )), + _ => Err(syn::Error::new_spanned( ty, format!( "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" ), - )]), + )), } } -fn type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { +fn type_to_get_record_fields_expr(ty: &Type) -> Result<TokenStream, syn::Error> { match ty { Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => Ok( quote! {<#ty as :: apache_avro::AvroSchemaComponent>::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)}, ), - Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + Type::Ptr(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support raw pointers", - )]), - Type::Tuple(_) => Err(vec![syn::Error::new_spanned( + )), + Type::Tuple(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support tuples", - )]), - _ => Err(vec![syn::Error::new_spanned( + )), + _ => Err(syn::Error::new_spanned( ty, format!( "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" ), - )]), + )), } } -fn type_to_field_default_expr(ty: &Type) -> Result<TokenStream, Vec<syn::Error>> { +fn type_to_field_default_expr(ty: &Type) -> Result<TokenStream, syn::Error> { match ty { Type::Array(_) | Type::Slice(_) | Type::Path(_) | Type::Reference(_) => { Ok(quote! {<#ty as :: apache_avro::AvroSchemaComponent>::field_default()}) } - Type::Ptr(_) => Err(vec![syn::Error::new_spanned( + Type::Ptr(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support raw pointers", - )]), - Type::Tuple(_) => Err(vec![syn::Error::new_spanned( + )), + Type::Tuple(_) => Err(syn::Error::new_spanned( ty, "AvroSchema: derive does not support tuples", - )]), - _ => Err(vec![syn::Error::new_spanned( + )), + _ => Err(syn::Error::new_spanned( ty, format!( "AvroSchema: Unexpected type encountered! Please open an issue if this kind of type should be supported: {ty:?}" ), - )]), + )), } } @@ -496,4 +531,20 @@ mod tests { assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{Vec<T>}).unwrap()).unwrap().to_string(), quote!{<Vec<T> as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); assert_eq!(type_to_schema_expr(&syn::parse2::<Type>(quote!{AnyType}).unwrap()).unwrap().to_string(), quote!{<AnyType as :: apache_avro::AvroSchemaComponent>::get_schema_in_ctxt(named_schemas, enclosing_namespace)}.to_string()); } + + // #[test] + // fn cassss() { + // let stream = quote! { + // #[avro(default = r#"{"_field": true}"#)] + // struct Spam { + // _field: bool, + // } + // }; + // let result = syn::parse2::<DeriveInput>(stream).unwrap(); + // let output = derive_avro_schema(result).unwrap(); + // println!("{}", output.to_string()); + // let pretty = prettyplease::unparse(&syn::parse2(output).unwrap()); + // println!("{}", pretty); + // panic!(); + // } }
