diff --git a/Cargo.toml b/Cargo.toml index 488e5dd..750f565 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/Artem-Romanenia/o2o" [dependencies] -o2o-impl = { version = "0.5.2", path = "o2o-impl", optional = true } +o2o-impl = { version = "0.5.2", path = "o2o-impl", default-features = false, optional = true } o2o-macros = { version = "0.5.2", path = "o2o-macros", default-features = false, optional = true } [features] diff --git a/README.md b/README.md index 8f160ce..79e3c37 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ And here's the code that `o2o` generates (from here on, generated code is produc ## Some milestones +* **v0.5.3** Better parsing of [inline expression](#inline-expressions). Now both 'braced' and 'braceless' flavors are supported in all contexts. * **v0.5.0** Refactoring and improved support for 'flattened' child fields: `#[child()]`, `#[child_parents()]` and `#[parent()]` instructions * **v0.4.9** Support for `#![no_std]` * **v0.4.4** Fallible conversions @@ -269,7 +270,7 @@ For most projects, just add this to `Cargo.toml`: ``` toml [dependencies] -o2o = "0.5.2" +o2o = "0.5.3" ``` ### `syn >=2.*` @@ -278,7 +279,7 @@ Currently o2o uses `syn >=1.0.3, <2` by default. If you want `syn >=2.*` to be u ``` toml [dependencies] -o2o = { version = "0.5.2", default-features = false, features = "syn2" } +o2o = { version = "0.5.3", default-features = false, features = "syn2" } ``` ### no_std @@ -287,18 +288,18 @@ In `#![no_std]` project, add this to `Cargo.toml`: ``` toml [dependencies] -o2o-macros = "0.5.2" +o2o-macros = "0.5.3" # Following line can be ommited if you don't need o2o to produce o2o::traits::(Try)IntoExisting implementations -o2o = { version = "0.5.2", default-features = false } +o2o = { version = "0.5.3", default-features = false } ``` Or, if you want `no_std` *and* `syn2`: ``` toml [dependencies] -o2o-macros = { version = "0.5.2", default-features = false, features = "syn2" } +o2o-macros = { version = "0.5.3", default-features = false, features = "syn2" } # Following line can be ommited if you don't need o2o to produce o2o::traits::(Try)IntoExisting implementations -o2o = { version = "0.5.2", default-features = false } +o2o = { version = "0.5.3", default-features = false } ``` ## The (not so big) Problem @@ -327,8 +328,11 @@ If o2o is wrong in any of its assumptions, you will have to tell it that. ## Inline expressions -o2o has a concept of Inline Expressions, which can be passed as a parameter to some of the o2o instructions. You can think of inline expression as a closure, which always has two *implicit* params: `|@, ~| {` **...expression body...** `}` or `|@, ~|` **{ ...expression body... }** +o2o has a concept of Inline Expressions, which can be passed as a parameter to some of the o2o instructions. You can think of inline expression as a closure, which always has two *implicit* params. There are two flavors: +* `|@, ~|` **{ ...expression body... }** (aka *'braced'*) +* `|@, ~|` **...expression body...** *(aka 'braceless')* +In these expressions: * `@` represents the object that is being converted from. * `~` represents the path to a specific field of the object that is being converted from. @@ -339,7 +343,7 @@ struct Entity { some_int: i32 } #[map_owned(Entity)] // tells o2o to implement 'From for EntityDto' and 'Into for EntityDto' struct EntityDto { #[from(~ * 2)] // Let's say for whatever reason we want to multiply 'some_int' by 2 when converting from Entity - #[into(~ / 2)] // And divide back by 2 when converting into it + #[into({~ / 2})] // And divide back by 2 when converting into it some_int: i32 } ``` @@ -372,7 +376,7 @@ struct Entity { some_int: i32 } #[map_owned(Entity)] struct EntityDto { #[from(@.some_int * 2)] - #[into(@.some_int / 2)] + #[into({@.some_int / 2})] some_int: i32 } ``` @@ -715,7 +719,7 @@ use o2o::o2o; #[derive(o2o)] #[map_owned(PersonDto)] -#[ghosts(zodiac_sign: {None})] +#[ghosts(zodiac_sign: None)] struct Person { id: i32, full_name: String, @@ -817,11 +821,11 @@ struct Person { #[derive(o2o)] #[from_owned(Person| vars(first_name: {@.first_name}, last_name: {@.last_name}))] -#[owned_into(Person| vars(first: {"John"}, last: {"Doe"}))] -#[ghosts(first_name: {first.into()}, last_name: {last.into()})] +#[owned_into(Person| vars(first: "John", last: "Doe"))] +#[ghosts(first_name: first.into(), last_name: last.into())] struct PersonDto { age: i8, - #[ghost({format!("{} {}", first_name, last_name)})] + #[ghost(format!("{} {}", first_name, last_name))] full_name: String } ``` @@ -889,7 +893,7 @@ Quick returns work well with helper variables: use o2o::o2o; #[derive(o2o)] -#[owned_into(i32| vars(hrs: {@.hours as i32}, mns: {@.minutes as i32}, scs: {@.seconds as i32}), +#[owned_into(i32| vars(hrs: @.hours as i32, mns: @.minutes as i32, scs: @.seconds as i32), return hrs * 3600 + mns * 60 + scs)] struct Time { hours: i8, @@ -1075,8 +1079,8 @@ impl Employee { #[derive(o2o)] #[map(Employee)] #[ghosts( - first_name: {@.get_first_name()}, - last_name: {@.get_last_name()} + first_name: @.get_first_name(), + last_name: @.get_last_name() )] struct EmployeeDto { #[map(id)] @@ -1854,7 +1858,7 @@ struct Machine { #[derive(o2o)] #[map_ref(Car)] #[child_parents(vehicle: Vehicle, vehicle.machine: Machine)] -#[ghosts(vehicle.machine@id: {321})] +#[ghosts(vehicle.machine@id: 321)] struct CarDto { number_of_doors: i8, @@ -1880,7 +1884,7 @@ struct CarDto { height: f32, #[o2o(stop_repeat)] - #[o2o(repeat)] #[ghost({123})] + #[o2o(repeat)] #[ghost(123)] useless_param: i32, useless_param_2: i32, useless_param_3: i32, @@ -2276,7 +2280,7 @@ enum Enum { enum EnumDto { Var1, Var2(i32, String), - #[ghost({Err(format!("unknown: {}", _str_field))?})] + #[ghost(Err(format!("unknown: {}", _str_field))?)] Var3 { _field: i32, _str_field: String } } ``` @@ -2312,7 +2316,7 @@ Reverse case: #[derive(o2o::o2o)] #[try_from_owned(EnumDto, String)] #[owned_into(EnumDto)] -#[ghosts(Var3 { _str_field, .. }: {Err(format!("Unknown: {}", _str_field))?})] +#[ghosts(Var3 { _str_field, .. }: Err(format!("Unknown: {}", _str_field))?)] enum Enum { Var1, Var2(i32, String), @@ -2413,11 +2417,11 @@ Missing fields and default values: #[map_owned(EnumDto)] enum Enum { Var1, - #[ghosts(f: {123.0})] + #[ghosts(f: 123.0)] Var2 { field: i32, }, - #[ghosts(1: {321.0})] + #[ghosts(1: 321.0)] Var3( i32, ) diff --git a/o2o-impl/Cargo.toml b/o2o-impl/Cargo.toml index f244870..325a465 100644 --- a/o2o-impl/Cargo.toml +++ b/o2o-impl/Cargo.toml @@ -13,6 +13,9 @@ syn = { package = "syn", version = "1.0.3", optional = true } syn2 = { package = "syn", version = "2.0.0", optional = true } quote = "1.0.0" +[features] +default = ["syn"] + [dev-dependencies] criterion = "0.4" test-case = "3" diff --git a/o2o-impl/src/attr.rs b/o2o-impl/src/attr.rs index b87c483..6cf8f7e 100644 --- a/o2o-impl/src/attr.rs +++ b/o2o-impl/src/attr.rs @@ -6,7 +6,7 @@ use std::ops::{Index, Not}; #[cfg(feature = "syn2")] use syn2 as syn; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::{Span, TokenStream, TokenTree}; use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseBuffer, ParseStream}; use syn::punctuated::Punctuated; @@ -42,6 +42,35 @@ impl OptionalParenthesizedTokenStream { } } +struct CommaDelimitedTokenStream { + token_stream: TokenStream +} + +impl Parse for CommaDelimitedTokenStream { + fn parse(input: ParseStream) -> Result { + let mut tokens: Vec = vec![]; + + loop { + if input.peek(Token![,]) || input.is_empty() { break; } + tokens.push(input.parse()?); + } + + Ok(CommaDelimitedTokenStream { token_stream: TokenStream::from_iter(tokens) }) + } +} + +#[derive(Clone)] +pub struct TokenStreamWithSpan { + pub token_stream: TokenStream, + pub span: Span +} + +impl TokenStreamWithSpan { + fn new (token_stream: TokenStream, span: Span) -> TokenStreamWithSpan { + TokenStreamWithSpan { token_stream, span } + } +} + pub(crate) enum DataTypeInstruction { Map(TraitAttr), Ghosts(GhostsAttr), @@ -431,7 +460,7 @@ impl TypeHint { } } -type TraitRepeatFor = [bool; 4]; +type TraitRepeatFor = [bool; 5]; struct TraitRepeatForWrap(TraitRepeatFor); enum TraitAttrType { @@ -439,6 +468,7 @@ enum TraitAttrType { Update, QuickReturn, DefaultCase, + MatchExpr } impl Index<&TraitAttrType> for TraitRepeatFor { @@ -450,20 +480,21 @@ impl Index<&TraitAttrType> for TraitRepeatFor { TraitAttrType::Update => &self[1], TraitAttrType::QuickReturn => &self[2], TraitAttrType::DefaultCase => &self[3], + TraitAttrType::MatchExpr => &self[4] } } } -const TRAIT_REPEAT_TYPES: [&str; 4] = ["vars", "update", "quick_return", "default_case"]; +const TRAIT_REPEAT_TYPES: [&str; 5] = ["vars", "update", "quick_return", "default_case", "match_expr"]; impl Parse for TraitRepeatForWrap { fn parse(input: ParseStream) -> Result { let types: Punctuated = Punctuated::parse_terminated(input)?; if types.is_empty() { - return Ok(TraitRepeatForWrap([true, true, true, true])); + return Ok(TraitRepeatForWrap([true, true, true, true, true])); } - let mut repeat: TraitRepeatFor = [false, false, false, false]; + let mut repeat: TraitRepeatFor = [false, false, false, false, false]; for ty in types { let str = ty.to_token_stream().to_string(); @@ -491,9 +522,10 @@ pub(crate) struct TraitAttrCore { pub err_ty: Option, pub type_hint: TypeHint, pub init_data: Option>, - pub update: Option, - pub quick_return: Option, - pub default_case: Option, + pub update: Option, + pub quick_return: Option, + pub default_case: Option, + pub match_expr: Option, pub repeat: Option, pub skip_repeat: bool, pub stop_repeat: bool, @@ -516,23 +548,29 @@ impl TraitAttrCore { self.init_data = other.init_data } if attr_to_repeat[&TraitAttrType::Update] { - if self.update.is_some() { - Err(syn::Error::new(self.update.span(), "Update statement will be overriden. Did you forget to use 'skip_repeat'?"))? + if let Some(update) = &self.update { + Err(syn::Error::new(update.span, "Update instruction will be overriden. Did you forget to use 'skip_repeat'?"))? } self.update = other.update } if attr_to_repeat[&TraitAttrType::QuickReturn] { - if self.quick_return.is_some() { - Err(syn::Error::new(self.quick_return.span(), "Quick Return statement will be overriden. Did you forget to use 'skip_repeat'?"))? + if let Some(quick_return) = &self.quick_return { + Err(syn::Error::new(quick_return.span, "Quick Return instruction will be overriden. Did you forget to use 'skip_repeat'?"))? } self.quick_return = other.quick_return } if attr_to_repeat[&TraitAttrType::DefaultCase] { - if self.default_case.is_some() { - Err(syn::Error::new(self.default_case.span(), "Default Case statement will be overriden. Did you forget to use 'skip_repeat'?"))? + if let Some(default_case) = &self.default_case { + Err(syn::Error::new(default_case.span, "Default Case instruction will be overriden. Did you forget to use 'skip_repeat'?"))? } self.default_case = other.default_case } + if attr_to_repeat[&TraitAttrType::MatchExpr] { + if let Some(match_expr) = &self.match_expr { + Err(syn::Error::new(match_expr.span, "Match instruction will be overriden. Did you forget to use 'skip_repeat'?"))? + } + self.match_expr = other.match_expr + } } Ok(()) } @@ -552,7 +590,7 @@ impl Parse for TraitAttrCore { Some(input.parse::()?.into()) } else { None }; - let mut attr = TraitAttrCore { ty, err_ty, type_hint, init_data: None, update: None, quick_return: None, default_case: None, repeat: None, skip_repeat: false, stop_repeat: false, attribute: None, impl_attribute: None, inner_attribute: None }; + let mut attr = TraitAttrCore { ty, err_ty, type_hint, init_data: None, update: None, quick_return: None, default_case: None, match_expr: None, repeat: None, skip_repeat: false, stop_repeat: false, attribute: None, impl_attribute: None, inner_attribute: None }; if !input.peek(Token![|]) { return Ok(attr); @@ -566,65 +604,62 @@ impl Parse for TraitAttrCore { } } -fn parse_trait_instruction_param_inner_1(input: &syn::parse::ParseBuffer, condition: bool, setter: impl FnOnce(), span: impl Fn(T) -> Span, name: &str) -> Result { +fn parse_trait_instruction_param_inner(input: &syn::parse::ParseBuffer, parser: impl Fn(&ParseBuffer, &T) -> Result, condition: bool, setter: impl FnOnce(U), span: impl Fn(T) -> Span, name: &str) -> Result { let a = input.parse::()?; - if input.peek(Token![,]) { - input.parse::()?; - } + let b = parser(input, &a)?; if condition { Err(syn::Error::new(span(a), format!("Instruction parameter '{}' was already set.", name)))? } else { - setter(); - Ok(true) + setter(b); + if input.peek(Token![,]) { + input.parse::()?; + Ok(true) + } else { Ok(false) } } } -fn parse_trait_instruction_param_inner_2(input: &syn::parse::ParseBuffer, parser: impl Fn(ParseBuffer) -> Result, condition: bool, setter: impl FnOnce(U), span: impl Fn(T) -> Span, name: &str) -> Result { +fn parse_parenthesized_trait_instruction_param_inner(input: &syn::parse::ParseBuffer, parser: impl Fn(ParseBuffer) -> Result, condition: bool, setter: impl FnOnce(U), span: impl Fn(T) -> Span, name: &str) -> Result { let a = input.parse::()?; let content; parenthesized!(content in input); let content = parser(content)?; - if input.peek(Token![,]) { - input.parse::()?; - } if condition { Err(syn::Error::new(span(a), format!("Instruction parameter '{}' was already set.", name)))? } else { setter(content); - Ok(true) + if input.peek(Token![,]) { + input.parse::()?; + Ok(true) + } else { Ok(false) } } } fn parse_trait_instruction_param(input: &syn::parse::ParseBuffer, attr: &mut TraitAttrCore) -> Result { if input.peek(kw::stop_repeat) { - return parse_trait_instruction_param_inner_1::(input, attr.stop_repeat, || attr.stop_repeat = true, |a| a.span, "stop_repeat"); + return parse_trait_instruction_param_inner::(input, |_, _| Ok(()), attr.stop_repeat, |_| attr.stop_repeat = true, |a| a.span, "stop_repeat") } else if input.peek(kw::skip_repeat) { - return parse_trait_instruction_param_inner_1::(input, attr.skip_repeat, || attr.skip_repeat = true, |a| a.span, "skip_repeat"); + return parse_trait_instruction_param_inner::(input, |_, _| Ok(()), attr.skip_repeat, |_| attr.skip_repeat = true, |a| a.span, "skip_repeat") } else if input.peek(kw::repeat) { - return parse_trait_instruction_param_inner_2::(input, |c| c.parse(), attr.repeat.is_some(), |x| attr.repeat = Some(x.0), |a| a.span, "repeat"); + return parse_parenthesized_trait_instruction_param_inner::(input, |c| c.parse(), attr.repeat.is_some(), |x| attr.repeat = Some(x.0), |a| a.span, "repeat") } else if input.peek(kw::vars) { - return parse_trait_instruction_param_inner_2::>(input, |c| Punctuated::parse_separated_nonempty(&c), attr.init_data.is_some(), |x| attr.init_data = Some(x), |a| a.span, "vars"); + return parse_parenthesized_trait_instruction_param_inner::>(input, |c| Punctuated::parse_separated_nonempty(&c), attr.init_data.is_some(), |x| attr.init_data = Some(x), |a| a.span, "vars") } else if input.peek(Token![..]) { - input.parse::()?; - attr.update = try_parse_action(input, true)?; - return Ok(false); + return parse_trait_instruction_param_inner::>(input, |x, t| try_parse_action(x).map(|x| x.map(|x| TokenStreamWithSpan::new(x, t.span()))), attr.update.is_some(), |x| attr.update = x, |a| a.span(), "update") } else if input.peek(Token![return]) { - input.parse::()?; - attr.quick_return = try_parse_action(input, true)?; - return Ok(false); + return parse_trait_instruction_param_inner::>(input, |x, t| try_parse_action(x).map(|x| x.map(|x| TokenStreamWithSpan::new(x, t.span))), attr.quick_return.is_some(), |x| attr.quick_return = x, |a| a.span(), "quick_return") } else if input.peek(Token![_]) { - input.parse::()?; - attr.default_case = try_parse_action(input, true)?; - return Ok(false); + return parse_trait_instruction_param_inner::>(input, |x, t| try_parse_action(x).map(|x| x.map(|x| TokenStreamWithSpan::new(x, t.span))), attr.default_case.is_some(), |x| attr.default_case = x, |a| a.span(), "default_case") + } else if input.peek(Token![match]) { + return parse_trait_instruction_param_inner::>(input, |x, t| try_parse_action(x).map(|x| x.map(|x| TokenStreamWithSpan::new(x, t.span))), attr.match_expr.is_some(), |x| attr.match_expr = x, |a| a.span(), "match_expr") } else if input.peek(kw::attribute) { - return parse_trait_instruction_param_inner_2::(input, |c| c.parse(), attr.attribute.is_some(), |x| attr.attribute = Some(quote!(#[ #x ])), |a| a.span, "attribute"); + return parse_parenthesized_trait_instruction_param_inner::(input, |c| c.parse(), attr.attribute.is_some(), |x| attr.attribute = Some(quote!(#[ #x ])), |a| a.span, "attribute") } else if input.peek(kw::impl_attribute) { - return parse_trait_instruction_param_inner_2::(input, |c| c.parse(), attr.impl_attribute.is_some(), |x| attr.impl_attribute = Some(quote!(#[ #x ])), |a| a.span, "impl_attribute"); + return parse_parenthesized_trait_instruction_param_inner::(input, |c| c.parse(), attr.impl_attribute.is_some(), |x| attr.impl_attribute = Some(quote!(#[ #x ])), |a| a.span, "impl_attribute") } else if input.peek(kw::inner_attribute) { - return parse_trait_instruction_param_inner_2::(input, |c| c.parse(), attr.inner_attribute.is_some(), |x| attr.inner_attribute = Some(quote!(#![ #x ])), |a| a.span, "inner_attribute"); + return parse_parenthesized_trait_instruction_param_inner::(input, |c| c.parse(), attr.inner_attribute.is_some(), |x| attr.inner_attribute = Some(quote!(#![ #x ])), |a| a.span, "inner_attribute") } - Ok(false) + Ok(true) } #[derive(Clone)] @@ -639,7 +674,7 @@ impl Parse for InitData { Ok(InitData { ident: input.parse()?, _colon: input.parse()?, - action: try_parse_braced_action(input)?, + action: try_parse_action(input).map(|x| x.unwrap())?, }) } } @@ -745,7 +780,7 @@ impl Parse for GhostData { input.parse::()?; - Ok(GhostData { child_path, ghost_ident, action: try_parse_braced_action(input)? }) + Ok(GhostData { child_path, ghost_ident, action: try_parse_action(input).map(|x| x.unwrap())? }) } } @@ -822,7 +857,7 @@ impl Parse for MemberAttrCore { Ok(MemberAttrCore { container_ty: try_parse_container_ident(input, false), member: try_parse_optional_ident(input), - action: try_parse_action(input, true)?, + action: try_parse_action(input)?, }) } } @@ -886,7 +921,7 @@ impl Parse for ParentChildFieldAsParsed { "owned_into" | "ref_into" | "into" | "from_owned" | "from_ref" | "from" | "map_owned" | "map_ref" | "map" | "owned_into_existing" | "ref_into_existing" | "into_existing" => { attrs.push(ParentChildFieldAttr { that_member: try_parse_optional_ident(&content_inner), - action: try_parse_action(&content_inner, true)?, + action: try_parse_action(&content_inner)?, applicable_to: [ appl_owned_into(instr_str), appl_ref_into(instr_str), @@ -965,7 +1000,7 @@ impl Parse for FieldGhostAttrCore { fn parse(input: ParseStream) -> Result { Ok(FieldGhostAttrCore { container_ty: try_parse_container_ident(input, true), - action: try_parse_action(input, true)?, + action: try_parse_action(input)?, }) } } @@ -1434,13 +1469,12 @@ fn try_parse_child_parents(input: ParseStream) -> Result Result> { +fn try_parse_action(input: ParseStream) -> Result> { if input.is_empty() { Ok(None) - } else if input.peek(Token![@]) || input.peek(Token![~]) { - return Ok(Some(input.parse()?)); - } else if allow_braceless && !input.peek(Brace) { - Ok(Some(input.parse()?)) + } else if !input.peek(Brace) { + let f: CommaDelimitedTokenStream = input.parse()?; + Ok(Some(f.token_stream)) } else { let content; braced!(content in input); @@ -1448,13 +1482,6 @@ fn try_parse_action(input: ParseStream, allow_braceless: bool) -> Result Result { - let content; - braced!(content in input); - - content.parse::() -} - fn add_as_type_attrs(input: &syn::Field, attr: AsAttr, attrs: &mut Vec) { let this_ty = input.ty.to_token_stream(); let that_ty = attr.tokens; diff --git a/o2o-impl/src/expand.rs b/o2o-impl/src/expand.rs index 6af66eb..b0f7ee0 100644 --- a/o2o-impl/src/expand.rs +++ b/o2o-impl/src/expand.rs @@ -188,10 +188,10 @@ fn main_code_block(ctx: &ImplContext) -> TokenStream { if let Some(quick_return) = &ctx.struct_attr.quick_return { //TODO: Consider removing quick returns for into_existing because they are confusing if ctx.kind.is_into_existing() { - let action = quote_action(quick_return, None, ctx); + let action = quote_action(&quick_return.token_stream, None, ctx); return quote!(*other = #action;); } - return quote_action(quick_return, None, ctx); + return quote_action(&quick_return.token_stream, None, ctx); } match ctx.input { @@ -204,10 +204,10 @@ fn main_code_block_ok(ctx: &ImplContext) -> TokenStream { if let Some(quick_return) = &ctx.struct_attr.quick_return { //TODO: Consider removing quick returns for into_existing because they are confusing if ctx.kind.is_into_existing() { - let action = quote_action(quick_return, None, ctx); + let action = quote_action(&quick_return.token_stream, None, ctx); return quote!(*other = #action;); } - return quote_action(quick_return, None, ctx); + return quote_action(&quick_return.token_stream, None, ctx); } let inner = match ctx.input { @@ -247,10 +247,12 @@ fn enum_main_code_block(input: &Enum, ctx: &ImplContext) -> TokenStream { match ctx.kind { Kind::FromOwned | Kind::FromRef => { - quote!(match value #enum_init_block) + let match_expr = if let Some(ts) = &ctx.struct_attr.match_expr { replace_tilde_or_at_in_expr(&ts.token_stream, Some("e!(value)), None) } else { quote!(value) }; + quote!(match #match_expr #enum_init_block) }, Kind::OwnedInto | Kind::RefInto => { - quote!(match self #enum_init_block) + let match_expr = if let Some(ts) = &ctx.struct_attr.match_expr { replace_tilde_or_at_in_expr(&ts.token_stream, Some("e!(self)), None) } else { quote!(self) }; + quote!(match #match_expr #enum_init_block) }, Kind::OwnedIntoExisting | Kind::RefIntoExisting => enum_init_block, } @@ -377,7 +379,7 @@ fn struct_init_block_inner( } if let Some(update) = &ctx.struct_attr.update { - let a = quote_action(update, None, ctx); + let a = quote_action(&update.token_stream, None, ctx); fragments.push(quote!(..#a)) } @@ -443,7 +445,7 @@ fn enum_init_block_inner(members: &mut Peekable>, ctx: &ImplCo } if let Some(default_case) = &ctx.struct_attr.default_case { - let g = quote_action(default_case, None, ctx); + let g = quote_action(&default_case.token_stream, None, ctx); fragments.push(quote!(_ #g)) } diff --git a/o2o-impl/src/tests.rs b/o2o-impl/src/tests.rs index 7ba9737..efb6bd2 100644 --- a/o2o-impl/src/tests.rs +++ b/o2o-impl/src/tests.rs @@ -1694,7 +1694,7 @@ fn infallible_map_instruction_error_type(instr: TokenStream, postfix: Option Default::default())] + enum Enum {} +}, "Update instructions are only applicable to structs."; "1")] +#[test_case(quote! { + #[from_owned(StructDto| match @.test())] + struct Struct(); +}, "Match instructions are only applicable to enums."; "2")] +#[test_case(quote! { + #[from_owned(StructDto| _ => todo!())] + struct Struct(); +}, "Default case instructions are only applicable to enums."; "3")] +fn misplaced_trait_attr_instr(code_fragment: TokenStream, err: &str) { + let input: DeriveInput = syn::parse2(code_fragment).unwrap(); + let output = derive(&input); + let message = get_error(output, true); + + assert_eq!(message, err); +} + +// endregion: misplaced_trait_attr_instr + // region: permeating_repeat #[test_case(quote!{ diff --git a/o2o-impl/src/validate.rs b/o2o-impl/src/validate.rs index 2d4ce7a..7c6296c 100644 --- a/o2o-impl/src/validate.rs +++ b/o2o-impl/src/validate.rs @@ -96,11 +96,20 @@ pub(crate) fn validate(input: &DataType) -> Result<()> { match input { DataType::Struct(s) => { validate_fields(s, attrs, &data_type_attrs_by_kind, &type_paths, &mut errors); + + for attr in &attrs.attrs { + check_misplaced_instrs_struct(&attr.core, &mut errors); + } + }, DataType::Enum(e) => { for v in &e.variants { validate_variant_fields(v, attrs, &type_paths, &mut errors); } + + for attr in &attrs.attrs { + check_misplaced_instrs_enum(&attr.core, &mut errors); + } }, } @@ -409,3 +418,18 @@ fn check_child_errors(child_attr: &ChildAttr, struct_attrs: &DataTypeAttrs, tp: } } } + +fn check_misplaced_instrs_struct(attr: &TraitAttrCore, errors: &mut HashMap) { + if let Some(default_case) = &attr.default_case { + errors.insert(format!("Default case instructions are only applicable to enums."), default_case.span); + } + if let Some(match_expr) = &attr.match_expr { + errors.insert(format!("Match instructions are only applicable to enums."), match_expr.span); + } +} + +fn check_misplaced_instrs_enum(attr: &TraitAttrCore, errors: &mut HashMap) { + if let Some(update) = &attr.update { + errors.insert(format!("Update instructions are only applicable to structs."), update.span); + } +} \ No newline at end of file diff --git a/o2o-macros/Cargo.toml b/o2o-macros/Cargo.toml index b5c3626..712aa94 100644 --- a/o2o-macros/Cargo.toml +++ b/o2o-macros/Cargo.toml @@ -11,11 +11,11 @@ repository = "https://github.com/Artem-Romanenia/o2o" proc-macro = true [dependencies] -o2o-impl = { version = "0.5.2", path = "../o2o-impl" } +o2o-impl = { version = "0.5.2", path = "../o2o-impl", default-features = false } syn = { package = "syn", version = "1.0.3", optional = true } syn2 = { package = "syn", version = "2.0.0", optional = true } [features] default = ["syn1"] syn1 = ["dep:syn", "o2o-impl/syn"] -syn2 = ["dep:syn2", "o2o-impl/syn2"] +syn2 = ["dep:syn2", "o2o-impl/syn2"] \ No newline at end of file diff --git a/o2o-tests/tests/19_deep_ghost_attr_tests.rs b/o2o-tests/tests/19_deep_ghost_attr_tests.rs index f7c8174..2fe9d08 100644 --- a/o2o-tests/tests/19_deep_ghost_attr_tests.rs +++ b/o2o-tests/tests/19_deep_ghost_attr_tests.rs @@ -22,7 +22,7 @@ struct Machine { #[map(Car)] #[into_existing(Car)] #[child_parents(vehicle: Vehicle, vehicle.machine: Machine)] -#[ghosts(vehicle.machine@id: { 321 })] +#[ghosts(vehicle.machine@id: 321)] struct CarDto { number_of_doors: i8, @@ -68,12 +68,12 @@ pub struct Team { into_existing(Team), child_parents(base: Base, division: Division, division.base: Base, division.league: League, division.league.base: Base), ghosts_owned( - division_id: { @.division.id }, - division.base@id: { @.division.id }, - division.base@name: { @.division.name }, - division@league_id: { @.league.id }, - division.league.base@id: { @.league.id }, - division.league.base@name: { @.league.name } + division_id: @.division.id, + division.base@id: @.division.id, + division.base@name: @.division.name, + division@league_id:@.league.id, + division.league.base@id: @.league.id, + division.league.base@name: @.league.name ), ghosts_ref( division_id: { @.division.id }, diff --git a/o2o-tests/tests/19_deep_ghost_attr_tests_fallible.rs b/o2o-tests/tests/19_deep_ghost_attr_tests_fallible.rs index 9d1c35b..1c278cb 100644 --- a/o2o-tests/tests/19_deep_ghost_attr_tests_fallible.rs +++ b/o2o-tests/tests/19_deep_ghost_attr_tests_fallible.rs @@ -22,7 +22,7 @@ struct Machine { #[try_map(Car, String)] #[try_into_existing(Car, String)] #[child_parents(vehicle: Vehicle, vehicle.machine: Machine)] -#[ghosts(vehicle.machine@id: { 321 })] +#[ghosts(vehicle.machine@id: 321)] struct CarDto { number_of_doors: i8, @@ -76,12 +76,12 @@ pub struct Team { division.league.base@name: { @.league.name } ), ghosts_ref( - division_id: { @.division.id }, - division.base@id: { @.division.id }, - division.base@name: { @.division.name.clone() }, - division@league_id: { @.league.id }, - division.league.base@id: { @.league.id }, - division.league.base@name: { @.league.name.clone() } + division_id: @.division.id, + division.base@id: @.division.id, + division.base@name: @.division.name.clone(), + division@league_id: @.league.id, + division.league.base@id: @.league.id, + division.league.base@name: @.league.name.clone() ) )] pub struct TeamDto { diff --git a/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests.rs b/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests.rs index 83ee017..245ec2d 100644 --- a/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests.rs +++ b/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests.rs @@ -59,7 +59,7 @@ fn extra_float_2(e: &UnnamedEntityDto) -> f32 { #[map(Entity)] #[into_existing(Entity)] #[child_parents(child: Child)] -#[ghosts(extra_float: { extra_float(&@) })] +#[ghosts(extra_float: extra_float(&@))] struct EntityDto { some_int: i32, @@ -86,7 +86,7 @@ struct EntityDto { #[child(child)] #[from(child_float, { float_to_string(~) })] - #[into(child_float, { string_to_float(~.clone()) })] + #[into(child_float, string_to_float(~.clone()))] child_float_string: String, } diff --git a/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests_fallible.rs b/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests_fallible.rs index 2e61bdd..0eb3a48 100644 --- a/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests_fallible.rs +++ b/o2o-tests/tests/26_at_and_tilde_inside_inline_expr_tests_fallible.rs @@ -94,7 +94,7 @@ struct EntityDto { #[try_map(Entity as {}, String)] #[try_into_existing(Entity as {}, String)] #[child_parents(child: Child as {})] -#[ghosts(extra_float: { extra_float_2(&@) })] +#[ghosts(extra_float: extra_float_2(&@))] struct UnnamedEntityDto( #[map(Entity| some_int)] i32, #[o2o( @@ -114,7 +114,7 @@ struct UnnamedEntityDto( String, #[ghost({ extra_string(&@) })] String, #[child(child)] - #[from(child_float, { float_to_string(~) })] + #[from(child_float, float_to_string(~))] #[into(child_float, { string_to_float(~.clone()) })] String, ); diff --git a/o2o-tests/tests/41_enum_ghost_tests_1.rs b/o2o-tests/tests/41_enum_ghost_tests_1.rs index dcf865f..dffe1f5 100644 --- a/o2o-tests/tests/41_enum_ghost_tests_1.rs +++ b/o2o-tests/tests/41_enum_ghost_tests_1.rs @@ -10,9 +10,9 @@ enum EnumDto { Var1, #[map(Var22)] Var2, - #[ghost({panic!("impl var3")})] + #[ghost(panic!("impl var3"))] Var3, - #[ghost({panic!("impl var4")})] + #[ghost(panic!("impl var4"))] Var4 { _str: String, _i: i32, @@ -23,7 +23,7 @@ enum EnumDto { #[derive(Clone, PartialEq, o2o::o2o)] #[map(EnumDto2)] -#[ghosts(Var3: {panic!("impl var3")}, Var4 { .. }: {panic!("impl var4")}, Var5(..): {panic!("impl var5")})] +#[ghosts(Var3: panic!("impl var3"), Var4 { .. }: panic!("impl var4"), Var5(..): { panic!("impl var5") })] enum Enum2 { Var1, #[map(Var2)] diff --git a/o2o-tests/tests/41_enum_ghost_tests_1_fallible.rs b/o2o-tests/tests/41_enum_ghost_tests_1_fallible.rs index 509b716..a409a0e 100644 --- a/o2o-tests/tests/41_enum_ghost_tests_1_fallible.rs +++ b/o2o-tests/tests/41_enum_ghost_tests_1_fallible.rs @@ -10,9 +10,9 @@ enum EnumDto { Var1, #[map(Var22)] Var2, - #[ghost({Err("impl var3")?})] + #[ghost(Err("impl var3")?)] Var3, - #[ghost({Err("impl var4")?})] + #[ghost(Err("impl var4")?)] Var4 { _str: String, _i: i32, @@ -23,7 +23,7 @@ enum EnumDto { #[derive(Clone, PartialEq, o2o::o2o)] #[try_map(EnumDto2, String)] -#[ghosts(Var3: {Err("impl var3")?}, Var4 { .. }: {Err("impl var4")?}, Var5(..): {Err("impl var5")?})] +#[ghosts(Var3: Err("impl var3")?, Var4 { .. }: {Err("impl var4")?}, Var5(..): Err("impl var5")?)] enum Enum2 { Var1, #[map(Var2)] diff --git a/o2o-tests/tests/41_enum_ghost_tests_2.rs b/o2o-tests/tests/41_enum_ghost_tests_2.rs index 5879f74..c92147f 100644 --- a/o2o-tests/tests/41_enum_ghost_tests_2.rs +++ b/o2o-tests/tests/41_enum_ghost_tests_2.rs @@ -21,7 +21,7 @@ enum EnumDto { #[derive(Clone, PartialEq, o2o::o2o)] #[map(EnumDto2)] -#[ghosts(Var3 { _str, .. }: {Enum2::Error(_str.clone())})] +#[ghosts(Var3 { _str, .. }: Enum2::Error(_str.clone()))] enum Enum2 { Var1, #[map(Var2)] diff --git a/o2o-tests/tests/41_enum_ghost_tests_2_fallible.rs b/o2o-tests/tests/41_enum_ghost_tests_2_fallible.rs index dd68f6d..fba95da 100644 --- a/o2o-tests/tests/41_enum_ghost_tests_2_fallible.rs +++ b/o2o-tests/tests/41_enum_ghost_tests_2_fallible.rs @@ -21,7 +21,7 @@ enum EnumDto { #[derive(Clone, PartialEq, o2o::o2o)] #[try_map(EnumDto2, String)] -#[ghosts(Var3 { _str, .. }: {Enum2::Error(_str.clone())})] +#[ghosts(Var3 { _str, .. }: Enum2::Error(_str.clone()))] enum Enum2 { Var1, #[map(Var2)] diff --git a/o2o-tests/tests/44_enum_ghost_default_tests.rs b/o2o-tests/tests/44_enum_ghost_default_tests.rs index bfb280c..0730636 100644 --- a/o2o-tests/tests/44_enum_ghost_default_tests.rs +++ b/o2o-tests/tests/44_enum_ghost_default_tests.rs @@ -18,7 +18,7 @@ enum EnumDto { #[derive(Clone, PartialEq, o2o::o2o)] #[try_map(EnumDto2, String| _ => Err("unknown")?)] -#[ghosts(Var3: {Err("todo")?})] +#[ghosts(Var3: Err("todo")?)] enum Enum2 { Var1, #[map(Var2)] diff --git a/o2o-tests/tests/49_enum_match_expr_tests.rs b/o2o-tests/tests/49_enum_match_expr_tests.rs new file mode 100644 index 0000000..b9d7f97 --- /dev/null +++ b/o2o-tests/tests/49_enum_match_expr_tests.rs @@ -0,0 +1,52 @@ +use test_case::test_case; + +#[derive(Debug, PartialEq, o2o::o2o)] +#[from_owned(String| match @.as_str(), _ => todo!())] +#[from_owned(i32| repeat(), match @.to_string().as_str(), _ => todo!())] +#[from_owned(f32)] +pub enum Enum { + #[literal("1")] + Opt1, + #[literal("2")] + Opt2, +} + +#[test_case(Enum::Opt1, "1".into())] +#[test_case(Enum::Opt2, "2".into())] +fn legit_options(l: Enum, r: String) { + let e: Enum = r.into(); + assert_eq!(l, e); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +fn default_case() { + let str: String = "3".into(); + let _: Enum = str.into(); +} + +#[test_case(Enum::Opt1, 1)] +#[test_case(Enum::Opt2, 2)] +fn legit_options_i32(l: Enum, r: i32) { + let e: Enum = r.into(); + assert_eq!(l, e); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +fn default_case_i32() { + let _: Enum = 3.into(); +} + +#[test_case(Enum::Opt1, 1.0)] +#[test_case(Enum::Opt2, 2.0)] +fn legit_options_f32(l: Enum, r: f32) { + let e: Enum = r.into(); + assert_eq!(l, e); +} + +#[test] +#[should_panic(expected = "not yet implemented")] +fn default_case_f32() { + let _: Enum = 3.into(); +} \ No newline at end of file diff --git a/o2o-tests/tests/49_enum_match_expr_tests_fallible.rs b/o2o-tests/tests/49_enum_match_expr_tests_fallible.rs new file mode 100644 index 0000000..7963cbc --- /dev/null +++ b/o2o-tests/tests/49_enum_match_expr_tests_fallible.rs @@ -0,0 +1,36 @@ +use test_case::test_case; + +#[derive(Debug, PartialEq, o2o::o2o)] +#[try_from_owned(String, String| match @.as_str(), _ => Err("error")?)] +#[try_from_owned(i32, String| repeat(), match @.to_string().as_str(), _ => Err("error")?)] +#[try_from_owned(f32, String)] +pub enum Enum { + #[literal("1")] + Opt1, + #[literal("2")] + Opt2, +} + +#[test_case(Ok(Enum::Opt1), "1".into())] +#[test_case(Ok(Enum::Opt2), "2".into())] +#[test_case(Err("error".into()), "3".into())] +fn legit_options(l: Result, r: String) { + let e: Result = r.try_into(); + assert_eq!(l, e); +} + +#[test_case(Ok(Enum::Opt1), 1)] +#[test_case(Ok(Enum::Opt2), 2)] +#[test_case(Err("error".into()), 3)] +fn legit_options_i32(l: Result, r: i32) { + let e: Result = r.try_into(); + assert_eq!(l, e); +} + +#[test_case(Ok(Enum::Opt1), 1.0)] +#[test_case(Ok(Enum::Opt2), 2.0)] +#[test_case(Err("error".into()), 3.0)] +fn legit_options_f32(l: Result, r: f32) { + let e: Result = r.try_into(); + assert_eq!(l, e); +} \ No newline at end of file diff --git a/tests.sh b/tests.sh index 355f9d6..79d1c5a 100755 --- a/tests.sh +++ b/tests.sh @@ -3,5 +3,5 @@ cargo test -q -p o2o --no-default-features --features syn1 cargo test -q -p o2o --no-default-features --features syn2 cargo test -q -p o2o-tests --no-default-features --features syn1 cargo test -q -p o2o-tests --no-default-features --features syn2 -cargo test -q -p o2o-impl --features syn -cargo test -q -p o2o-impl --features syn2 \ No newline at end of file +cargo test -q -p o2o-impl --no-default-features --features syn +cargo test -q -p o2o-impl --no-default-features --features syn2 \ No newline at end of file