diff --git a/Cargo.toml b/Cargo.toml index cc033b4..dbabbb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "o2o" -version = "0.3.1" +version = "0.3.2" edition = "2021" authors = ["Artem Romanenia "] categories = ["rust-patterns"] @@ -10,8 +10,8 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/Artem-Romanenia/o2o" [dependencies] -o2o-impl = { version = "0.3.0", path = "o2o-impl" } -o2o-macros = { version = "0.3.0", path = "o2o-macros" } +o2o-impl = { version = "0.3.1", path = "o2o-impl" } +o2o-macros = { version = "0.3.1", path = "o2o-macros" } [workspace] members = ["o2o-impl", "o2o-macros", "o2o-tests"] diff --git a/README.md b/README.md index b846475..70607f0 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,98 @@ ================================ [github.com](https://github.com/Artem-Romanenia/o2o/) [crates.io](https://crates.io/crates/o2o) +[docs.rs](https://docs.rs/o2o) + +## Quick pitch + +``` rust ignore +impl From for PersonDto { + fn from(value: Person) -> PersonDto { + PersonDto { + id: value.id, + name: value.name, + age: value.age, + } + } +} +``` + +Writing code like above is not the most exciting or emotionally rewarding part of working with Rust. If you're Ok with letting procedural macro write it for you, welcome to the rest of this page. + +## Basic Example + +``` rust +use o2o::o2o; + +struct Person { + id: u32, + name: String, + age: u8 +} + +#[derive(o2o)] +#[from_owned(Person)] // This tells o2o to generate 'From for PersonDto' implementation +#[owned_into(Person)] // This generates 'Into for PersonDto' +struct PersonDto { + id: u32, + name: String, + age: u8 +} + +// Applying #[derive(o2o)] on PersonDto allows you to do this: + +let person = Person { id: 123, name: "John".into(), age: 42 }; +let dto = PersonDto::from(person); + +assert_eq!(dto.id, 123); assert_eq!(dto.name, "John"); assert_eq!(dto.age, 42); + +// and this: + +let dto = PersonDto { id: 321, name: "Jack".into(), age: 23 }; +let person: Person = dto.into(); + +assert_eq!(person.id, 321); assert_eq!(person.name, "Jack"); assert_eq!(person.age, 23); +``` + +And here's the code that `o2o` generates (from here on, generated code is produced by [rust-analyzer: Expand macro recursively](https://rust-analyzer.github.io/manual.html#expand-macro-recursively) command): +
+ View generated code + + ``` rust ignore + impl std::convert::From for PersonDto { + fn from(value: Person) -> PersonDto { + PersonDto { + id: value.id, + name: value.name, + age: value.age, + } + } + } + impl std::convert::Into for PersonDto { + fn into(self) -> Person { + Person { + id: self.id, + name: self.name, + age: self.age, + } + } + } + ``` +
## Content -- [Quick pitch](#quick-pitch) -- [Brief description](#brief-description) +- [Traits and `o2o` *trait instructions*](#traits-and-o2o-trait-instructions) +- [The (not so big) Problem](#the-not-so-big-problem) - [Examples](#examples) - - [Simplest Case](#simplest-case) - [Different field name](#different-field-name) - [Different field type](#different-field-type) - [Nested structs](#nested-structs) - [Nested collection](#nested-collection) - [Assymetric fields (skipping and providing default values)](#assymetric-fields-skipping-and-providing-default-values) - [Expressions](#expressions) - - [Expressions for struct level instructions](#expressions-for-struct-level-instructions) - - [Expressions for member level instructions](#expressions-for-member-level-instructions) + - [Expressions for struct instructions](#expressions-for-struct-instructions) + - [Expressions for member instructions](#expressions-for-member-instructions) - [More examples](#more-examples) - [Slightly complex example](#slightly-complex-example) - [Flatened children](#flatened-children) @@ -37,47 +114,43 @@ - [Contributions](#contributions) - [License](#license) -## Quick pitch +## Traits and `o2o` *trait instructions* -``` rust ignore -impl From for PersonDto { - fn from(value: Person) -> PersonDto { - PersonDto { - id: value.id, - name: value.name, - age: value.age, - } - } -} -``` +To let o2o know what traits you want implemented, you have to use type-level `o2o` *trait instructions* (i.e. proc macro attributes): -Writing code like above is not the most exciting or rewarding part of working with Rust. If you're Ok with letting procedural macro write it for you, welcome to the rest of this page. +``` rust +struct Entity { } -## Brief description +#[derive(o2o::o2o)] +#[from_owned(Entity)] +struct EntityDto { } +``` -**o2o** procedural macro is able to generate implementation of 6 kinds of traits (currently for structs only): +o2o procedural macro is able to generate implementation of 6 kinds of traits: ``` rust ignore -// #[from_owned()] +// When applied to a struct B: + +// #[from_owned(A)] impl std::convert::From for B { ... } -// #[from_ref()] +// #[from_ref(A)] impl std::convert::From<&A> for B { ... } -// #[owned_into()] +// #[owned_into(A)] impl std::convert::Into for B { ... } -// #[ref_into()] +// #[ref_into(A)] impl std::convert::Into for &B { ... } -// #[owned_into_existing()] +// #[owned_into_existing(A)] impl o2o::traits::IntoExisting for B { ... } -// #[ref_into_existing()] +// #[ref_into_existing(A)] impl o2o::traits::IntoExisting for &B { ... } ``` -It also has shortcuts to configure multiple trait implementations with fewer lines of code: +o2o also has shortcuts to configure multiple trait implementations with fewer lines of code: | | #[map()] | #[from()] | #[into()] | #[map_owned()] | #[map_ref()] | #[into_existing()] | | ---------------------------- | -------- | ---------- | --------- | ---------------| ------------ | -------------------| @@ -88,65 +161,57 @@ It also has shortcuts to configure multiple trait implementations with fewer lin | **#[owned_into_existing()]** | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | | **#[ref_into_existing()]** | ❌ | ❌ | ❌ | ❌ | ❌ | ✔️ | -With that, let's look at some examples. +E.g. following two bits of code are equivalent: -## Examples +``` rust +struct Entity { } -### Simplest Case +#[derive(o2o::o2o)] +#[map(Entity)] +struct EntityDto { } +``` ``` rust -use o2o::o2o; +struct Entity { } -struct Person { - id: u32, - name: String, - age: u8 -} - -#[derive(o2o)] -#[map_owned(Person)] -struct PersonDto { - id: u32, - name: String, - age: u8 -} +#[derive(o2o::o2o)] +#[from_owned(Entity)] +#[from_ref(Entity)] +#[owned_into(Entity)] +#[ref_into(Entity)] +struct EntityDto { } ``` -From here on, generated code is produced by [rust-analyzer: Expand macro recursively](https://rust-analyzer.github.io/manual.html#expand-macro-recursively) command -
- View generated code - ``` rust ignore - impl std::convert::From for PersonDto { - fn from(value: Person) -> PersonDto { - PersonDto { - id: value.id, - name: value.name, - age: value.age, - } - } - } - impl std::convert::Into for PersonDto { - fn into(self) -> Person { - Person { - id: self.id, - name: self.name, - age: self.age, - } - } - } - ``` -
+And before looking at some examples, let's talk about... + +## The (not so big) Problem + +This section may be useful for people which are not very familiar with Rust's procedural macros. -With the above code you should be able to do this: +Being procedural macro, o2o has knowledge only about the side of the mapping where `#[derive(o2o)]` is applied. ``` rust ignore -let person = Person { id: 123, name: "John".into(), age: 42 }; -let dto: PersonDto = person.into(); -// and this: -let dto = PersonDto { id: 123, name: "John".into(), age: 42 }; -let person: Person = dto.into(); +#[derive(o2o::o2o)] +#[map(Entity)] +struct EntityDto { } ``` +In code above, o2o knows everything about `EntityDto`, but it doesn't know anything about `Entity`. It doens't know if it is a struct, doesn't know what fields it has, doesn't know if it is struct or a tuple, *it doesn't even know if it exists*. + +So unlike mappers from languages like C#, Java, Go etc. that can use reflection to find out what they need to know, `o2o` can only assume things. + +For the piece of code above, o2o will assume that: + +* `Entity` exists *(duh!)* +* `Entity` is the same data type that `EntityDto` is (in this case a struct) +* `Entity` has exactly the same fields that `EntityDto` has + +If o2o is wrong in any of its assumptions, you will have to tell it that. + +So now let's see how you do it... + +## Examples + ### Different field name ``` rust @@ -322,8 +387,8 @@ struct Child { struct EntityDto { some_int: i32, // Here field name as well as type are different, so we pass in field name and tilde inline expression. - // Also, it doesn't hurt to use member level instruction #[map()], - // which is broader than struct level instruction #[map_owned] + // Also, it doesn't hurt to use member trait instruction #[map()], + // which is broader than trait instruction #[map_owned] #[map(children, ~.iter().map(|p|p.into()).collect())] children_vec: Vec } @@ -430,7 +495,7 @@ use o2o::o2o; #[derive(o2o)] #[map_owned(PersonDto)] -#[ghost(zodiac_sign: {None})] // #[ghost()] struct level instruction accepts only braced closures. +#[ghost(zodiac_sign: {None})] // #[ghost()] struct instruction accepts only braced closures. struct Person { id: i32, full_name: String, @@ -473,7 +538,7 @@ enum ZodiacSign {} ## Expressions -### Expressions for struct level instructions +### Expressions for struct instructions ``` rust ignore #[ghost(field: { None })] @@ -493,7 +558,7 @@ enum ZodiacSign {} // field: (|x: &Type| x.get_value())(&self), ``` -### Expressions for member level instructions +### Expressions for member instructions ``` rust ignore #[map({ None })] @@ -550,7 +615,7 @@ struct EmployeeDto { full_name: String, #[from(|x| Box::new(x.subordinate_of.as_ref().into()))] - #[into(subordinate_of, |x| Box::new(x.reports_to.as_ref().into()))] + #[into(subordinate_of, { Box::new(@.reports_to.as_ref().into()) })] reports_to: Box, #[map(~.iter().map(|p| Box::new(p.as_ref().into())).collect())] diff --git a/o2o-impl/Cargo.toml b/o2o-impl/Cargo.toml index fdd0a82..61d128e 100644 --- a/o2o-impl/Cargo.toml +++ b/o2o-impl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "o2o-impl" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["Artem Romanenia "] description = "Implementation of 'o2o' crate" diff --git a/o2o-impl/src/ast.rs b/o2o-impl/src/ast.rs index cfbf9e9..e703177 100644 --- a/o2o-impl/src/ast.rs +++ b/o2o-impl/src/ast.rs @@ -1,16 +1,14 @@ use crate::attr::{self}; -use crate::attr::{StructAttrs, FieldAttrs}; -use proc_macro2::Span; -use syn::{ - DataStruct, DeriveInput, Fields, Generics, Ident, Index, Member, Result -}; +use crate::attr::{FieldAttrs, StructAttrs}; +use syn::spanned::Spanned; +use syn::{DataStruct, DeriveInput, Fields, Generics, Ident, Index, Member, Result}; pub(crate) struct Struct<'a> { pub attrs: StructAttrs, pub ident: &'a Ident, pub generics: &'a Generics, pub fields: Vec, - pub named: bool, + pub named_fields: bool, } pub(crate) struct Field { @@ -22,32 +20,29 @@ pub(crate) struct Field { impl<'a> Struct<'a> { pub fn from_syn(node: &'a DeriveInput, data: &'a DataStruct) -> Result { let attrs = attr::get_struct_attrs(&node.attrs)?; - let fields = Field::multiple_from_syn(&data.fields, Span::call_site())?; + let fields = Field::multiple_from_syn(&data.fields)?; Ok(Struct { attrs, ident: &node.ident, generics: &node.generics, fields, - named: matches!(&data.fields, Fields::Named(_)) + named_fields: matches!(&data.fields, Fields::Named(_)), }) } } impl<'a> Field { - fn multiple_from_syn( - fields: &'a Fields, - span: Span, - ) -> Result> { + fn multiple_from_syn(fields: &'a Fields) -> Result> { let mut attrs_to_repeat = None; fields .iter() .enumerate() .map(|(i, field)| { - let mut field = Field::from_syn(i, field, span)?; + let mut field = Field::from_syn(i, field)?; if field.attrs.stop_repeat { - attrs_to_repeat = None; + attrs_to_repeat = None; } if field.attrs.repeat.is_some() { @@ -65,18 +60,14 @@ impl<'a> Field { .collect() } - fn from_syn( - i: usize, - node: &'a syn::Field, - span: Span, - ) -> Result { + fn from_syn(i: usize, node: &'a syn::Field) -> Result { Ok(Field { attrs: attr::get_field_attrs(node)?, idx: i, member: node.ident.clone().map(Member::Named).unwrap_or_else(|| { Member::Unnamed(Index { index: i as u32, - span, + span: node.ty.span(), }) }), }) diff --git a/o2o-impl/src/attr.rs b/o2o-impl/src/attr.rs index 21635da..f139494 100644 --- a/o2o-impl/src/attr.rs +++ b/o2o-impl/src/attr.rs @@ -1,3 +1,4 @@ +use std::fmt::Display; use std::hash::Hash; use std::ops::Index; @@ -61,7 +62,7 @@ pub(crate) struct TypePath { pub span: Span, pub path: TokenStream, pub path_str: String, - pub nameless: bool, + pub nameless_tuple: bool, } impl From for TypePath { @@ -70,7 +71,7 @@ impl From for TypePath { span: value.span(), path: value.to_token_stream(), path_str: value.to_token_stream().to_string(), - nameless: false + nameless_tuple: false } } } @@ -81,7 +82,7 @@ impl From for TypePath { span: value.span(), path_str: value.to_string(), path: value, - nameless: true + nameless_tuple: true } } } @@ -120,6 +121,19 @@ impl Kind { } } +impl Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Kind::OwnedInto => f.write_str("owned_into"), + Kind::RefInto => f.write_str("ref_into"), + Kind::FromOwned => f.write_str("from_owned"), + Kind::FromRef => f.write_str("from_ref"), + Kind::OwnedIntoExisting => f.write_str("owned_into_existing"), + Kind::RefIntoExisting => f.write_str("ref_into_existing"), + } + } +} + type ApplicableTo = [bool; 6]; impl Index<&Kind> for ApplicableTo { @@ -233,19 +247,29 @@ pub(crate) struct FieldAttrs { } impl<'a> FieldAttrs { - pub(crate) fn iter_for_kind(&'a self, kind: &'a Kind) -> impl Iterator { - self.attrs.iter().filter(move |x| x.applicable_to[kind]).map(|x| &x.attr) + pub(crate) fn iter_for_kind(&'a self, kind: &'a Kind) -> impl Iterator { + self.attrs.iter().filter(move |x| x.applicable_to[kind]) + } + + pub(crate) fn iter_for_kind_core(&'a self, kind: &'a Kind) -> impl Iterator { + self.iter_for_kind(kind).map(|x| &x.attr) } pub(crate) fn applicable_attr(&'a self, kind: &'a Kind, container_ty: &TypePath) -> Option { self.ghost(container_ty, kind) .map(ApplicableAttr::Ghost) - .or_else(|| self.field_attr(kind, container_ty) - .or_else(|| if kind == &Kind::OwnedIntoExisting { self.field_attr(&Kind::OwnedInto, container_ty) } else { None }) - .or_else(|| if kind == &Kind::RefIntoExisting { self.field_attr(&Kind::RefInto, container_ty) } else { None }) + .or_else(|| self.field_attr_core(kind, container_ty) + .or_else(|| if kind == &Kind::OwnedIntoExisting { self.field_attr_core(&Kind::OwnedInto, container_ty) } else { None }) + .or_else(|| if kind == &Kind::RefIntoExisting { self.field_attr_core(&Kind::RefInto, container_ty) } else { None }) .map(ApplicableAttr::Field)) } + pub(crate) fn applicable_field_attr(&'a self, kind: &'a Kind, container_ty: &TypePath) -> Option<&'a FieldAttr> { + self.field_attr(kind, container_ty) + .or_else(|| if kind == &Kind::OwnedIntoExisting { self.field_attr(&Kind::OwnedInto, container_ty) } else { None }) + .or_else(|| if kind == &Kind::RefIntoExisting { self.field_attr(&Kind::RefInto, container_ty) } else { None }) + } + pub(crate) fn child(&'a self, container_ty: &TypePath) -> Option<&FieldChildAttr>{ self.child_attrs.iter() .find(|x| x.container_ty.is_some() && x.container_ty.as_ref().unwrap() == container_ty) @@ -263,10 +287,16 @@ impl<'a> FieldAttrs { .any(|x| x.container_ty.is_none() || x.container_ty.as_ref().unwrap() == container_ty) } - pub(crate) fn field_attr(&'a self, kind: &'a Kind, container_ty: &TypePath) -> Option<&FieldAttrCore>{ + pub(crate) fn field_attr(&'a self, kind: &'a Kind, container_ty: &TypePath) -> Option<&FieldAttr>{ self.iter_for_kind(kind) + .find(|x| x.attr.container_ty.is_some() && x.attr.container_ty.as_ref().unwrap() == container_ty) + .or_else(|| self.iter_for_kind(kind).find(|x| x.attr.container_ty.is_none())) + } + + pub(crate) fn field_attr_core(&'a self, kind: &'a Kind, container_ty: &TypePath) -> Option<&FieldAttrCore>{ + self.iter_for_kind_core(kind) .find(|x| x.container_ty.is_some() && x.container_ty.as_ref().unwrap() == container_ty) - .or_else(|| self.iter_for_kind(kind).find(|x| x.container_ty.is_none())) + .or_else(|| self.iter_for_kind_core(kind).find(|x| x.container_ty.is_none())) } pub(crate) fn merge(&'a mut self, other: Self) { @@ -314,7 +344,7 @@ impl Parse for StructAttrCore { let content_stream = content.parse::()?; quote!((#content_stream)).into() } else { input.parse::()?.into() }; - let struct_kind_hint = if ty.nameless { StructKindHint::Tuple } else { try_parse_struct_kind_hint(input)? }; + let struct_kind_hint = if ty.nameless_tuple { StructKindHint::Tuple } else { try_parse_struct_kind_hint(input)? }; let init_data: Option> = if input.peek(Token![|]) { input.parse::()?; Some(Punctuated::parse_separated_nonempty(input)?) @@ -493,6 +523,7 @@ impl Parse for WrappedAttr { #[derive(Clone)] pub(crate) struct FieldAttr { pub attr: FieldAttrCore, + pub original_instr: String, applicable_to: ApplicableTo, } @@ -738,7 +769,7 @@ fn parse_struct_instruction(instr: &Ident, input: TokenStream, bark: bool) -> Re "children" => Ok(StructInstruction::Children(syn::parse2(input)?)), "where_clause" => Ok(StructInstruction::Where(syn::parse2(input)?)), "wrapped" => Ok(StructInstruction::Wrapped(syn::parse2(input)?)), - _ if bark => Err(Error::new(instr.span(), format_args!("Struct level instruction '{}' is not supported.", instr))), + _ if bark => Err(Error::new(instr.span(), format_args!("Struct instruction '{}' is not supported.", instr))), _ => Ok(StructInstruction::Unrecognized), } } @@ -750,6 +781,7 @@ fn parse_member_instruction(instr: &Ident, input: TokenStream, bark: bool) -> Re "map_owned" | "map_ref" | "map" | "owned_into_existing" | "ref_into_existing" | "into_existing" => Ok(MemberInstruction::Map(FieldAttr { attr: syn::parse2(input)?, + original_instr: instr_str.clone(), applicable_to: [ appl_owned_into(instr_str), appl_ref_into(instr_str), @@ -779,7 +811,7 @@ fn parse_member_instruction(instr: &Ident, input: TokenStream, bark: bool) -> Re }, "stop_repeat" => Ok(MemberInstruction::StopRepeat), "wrapper" => Ok(MemberInstruction::Wrapper(syn::parse2(input)?)), - _ if bark => Err(Error::new(instr.span(), format_args!("Member level instruction '{}' is not supported.", instr))), + _ if bark => Err(Error::new(instr.span(), format_args!("Member instruction '{}' is not supported.", instr))), _ => Ok(MemberInstruction::Unrecognized) } } @@ -970,6 +1002,7 @@ fn add_as_type_attrs(input: &syn::Field, attr: AsAttr, attrs: &mut Vec, attr: WrapperA action: Some(Action::InlineAtExpr(TokenStream::new())), wrapper: true, }, + original_instr: "wrapper".into(), applicable_to: [false ^ inverse, attr.0.is_none() & inverse, true ^ inverse, attr.0.is_none() & !inverse, false, false] }); attrs.push(FieldAttr { @@ -1000,6 +1035,7 @@ pub(crate) fn add_wrapper_attrs (container_ty: &Option, attr: WrapperA action: None, wrapper: true, }, + original_instr: "wrapper".into(), applicable_to: [true ^ inverse, attr.0.is_none() & !inverse, false ^ inverse, attr.0.is_none() & inverse, false, false] }); @@ -1011,6 +1047,7 @@ pub(crate) fn add_wrapper_attrs (container_ty: &Option, attr: WrapperA action: Some(Action::InlineAtExpr(action.clone())), wrapper: true, }, + original_instr: "wrapper".into(), applicable_to: [false, false ^ inverse, false, true ^ inverse, false, false] }); attrs.push(FieldAttr { @@ -1020,6 +1057,7 @@ pub(crate) fn add_wrapper_attrs (container_ty: &Option, attr: WrapperA action: Some(Action::InlineTildeExpr(action)), wrapper: true, }, + original_instr: "wrapper".into(), applicable_to: [false, true ^ inverse, false, false ^ inverse, false, false] }); } diff --git a/o2o-impl/src/expand.rs b/o2o-impl/src/expand.rs index 6eb73d9..47adf71 100644 --- a/o2o-impl/src/expand.rs +++ b/o2o-impl/src/expand.rs @@ -98,7 +98,7 @@ fn struct_impl(input: Struct) -> TokenStream { has_post_init: false, }; - let wrapped_field = input.fields.iter().find(|f| f.attrs.field_attr(&Kind::OwnedInto, &struct_attr.ty).filter(|&g| g.wrapper).is_some()); + let wrapped_field = input.fields.iter().find(|f| f.attrs.field_attr_core(&Kind::OwnedInto, &struct_attr.ty).filter(|&g| g.wrapper).is_some()); if let Some(wrapped_field) = wrapped_field { let f = &wrapped_field.member; @@ -111,7 +111,7 @@ fn struct_impl(input: Struct) -> TokenStream { let post_init = struct_post_init(&ctx); ctx.has_post_init = post_init.is_some(); let init = struct_init_block(&ctx); - let dst = if struct_attr.ty.nameless || post_init.is_some() { TokenStream::new() } else { struct_attr.ty.path.clone() }; + let dst = if struct_attr.ty.nameless_tuple || post_init.is_some() { TokenStream::new() } else { struct_attr.ty.path.clone() }; quote_into_trait(&ctx, pre_init, quote!(#dst #init), post_init) } }); @@ -128,7 +128,7 @@ fn struct_impl(input: Struct) -> TokenStream { }; let wrapped_field = input.fields.iter().find_map(|f| - f.attrs.field_attr(&Kind::RefInto, &struct_attr.ty) + f.attrs.field_attr_core(&Kind::RefInto, &struct_attr.ty) .filter(|&g| g.wrapper) .map(|x| (f, &x.action)) ); @@ -148,7 +148,7 @@ fn struct_impl(input: Struct) -> TokenStream { let post_init = struct_post_init(&ctx); ctx.has_post_init = post_init.is_some(); let init = struct_init_block(&ctx); - let dst = if struct_attr.ty.nameless || post_init.is_some() { TokenStream::new() } else { struct_attr.ty.path.clone() }; + let dst = if struct_attr.ty.nameless_tuple || post_init.is_some() { TokenStream::new() } else { struct_attr.ty.path.clone() }; quote_into_trait(&ctx, pre_init, quote!(#dst #init), post_init) } }); @@ -353,7 +353,7 @@ fn struct_init_block_inner( return quote!(#(#fragments)*) } - match (&ctx.kind, struct_kind_hint, ctx.input.named) { + match (&ctx.kind, struct_kind_hint, ctx.input.named_fields) { (Kind::FromOwned | Kind::FromRef, _, true) => quote!({#(#fragments)*}), (Kind::FromOwned | Kind::FromRef, _, false) => quote!((#(#fragments)*)), (_, StructKindHint::Struct, _) => quote!({#(#fragments)*}), @@ -454,7 +454,7 @@ fn render_child( let child_data = children.find(|child_data| child_data.check_match(path)).unwrap(); let ty = &child_data.ty; let init = struct_init_block_inner(fields, ctx, Some((field_ctx.0, Some(child_data), field_ctx.1 ))); - match (ctx.input.named, hint) { + match (ctx.input.named_fields, hint) { (true, StructKindHint::Struct | StructKindHint::Unspecified) => quote!(#child_name: #ty #init,), (true, StructKindHint::Tuple) => quote!(#ty #init,), (false, StructKindHint::Tuple | StructKindHint::Unspecified) => quote!(#ty #init,), @@ -543,7 +543,7 @@ fn render_line( }, (syn::Member::Named(ident), Some(attr), Kind::OwnedInto | Kind::RefInto, StructKindHint::Struct | StructKindHint::Unspecified) =>{ let field_name = attr.get_field_name_or(&f.member); - let right_side = attr.get_action_or(Some(f.member.to_token_stream()), ctx, || quote!(self.#ident)); + let right_side = attr.get_action_or(Some(ident.to_token_stream()), ctx, || quote!(self.#ident)); if ctx.has_post_init { quote!(obj.#field_name = #right_side;) } else { quote!(#field_name: #right_side,) } }, (syn::Member::Named(ident), Some(attr), Kind::OwnedIntoExisting | Kind::RefIntoExisting, StructKindHint::Struct | StructKindHint::Unspecified) =>{ @@ -565,22 +565,22 @@ fn render_line( quote!(#ident: #right_side,) }, (syn::Member::Unnamed(index), Some(attr), Kind::OwnedInto | Kind::RefInto, StructKindHint::Tuple | StructKindHint::Unspecified) =>{ - let right_side = attr.get_action_or(Some(get_field_path(&f.member)), ctx, || quote!(self.#index)); + let right_side = attr.get_action_or(Some(index.to_token_stream()), ctx, || quote!(self.#index)); quote!(#right_side,) }, (syn::Member::Unnamed(index), Some(attr), Kind::OwnedIntoExisting | Kind::RefIntoExisting, StructKindHint::Tuple | StructKindHint::Unspecified) =>{ let field_path = get_field_path(attr.get_field_name_or(&f.member)); - let right_side = attr.get_action_or(Some(get_field_path(&f.member)), ctx, || quote!(self.#index)); + let right_side = attr.get_action_or(Some(index.to_token_stream()), ctx, || quote!(self.#index)); quote!(other.#field_path = #right_side;) }, (syn::Member::Unnamed(index), Some(attr), Kind::OwnedInto | Kind::RefInto, StructKindHint::Struct) =>{ let field_name = attr.get_ident(); - let right_side = attr.get_action_or(Some(get_field_path(&f.member)), ctx, || quote!(self.#index)); + let right_side = attr.get_action_or(Some(index.to_token_stream()), ctx, || quote!(self.#index)); if ctx.has_post_init { quote!(obj.#field_name = #right_side;) } else { quote!(#field_name: #right_side,) } }, (syn::Member::Unnamed(index), Some(attr), Kind::OwnedIntoExisting | Kind::RefIntoExisting, StructKindHint::Struct) =>{ let field_path = get_field_path(attr.get_ident()); - let right_side = attr.get_action_or(Some(get_field_path(&f.member)), ctx, || quote!(self.#index)); + let right_side = attr.get_action_or(Some(index.to_token_stream()), ctx, || quote!(self.#index)); quote!(other.#field_path = #right_side;) }, (syn::Member::Unnamed(_), Some(attr), Kind::FromOwned | Kind::FromRef, _) =>{ @@ -626,12 +626,46 @@ fn type_closure_param(input: &TokenStream, ctx: &ImplContext) -> TokenStream { tokens.push(x.parse::().unwrap().to_token_stream()); tokens.push(x.parse().unwrap()); - cl = TokenStream::from_iter(tokens.into_iter()); + cl = TokenStream::from_iter(tokens); Ok(()) }, input.clone()).unwrap(); cl } +fn replace_tilde_or_at_in_expr(input: &TokenStream, ident: &TokenStream, path: &TokenStream) -> TokenStream { + let mut tokens = Vec::new(); + + input.clone().into_iter().for_each(|x| { + let f = match x { + proc_macro2::TokenTree::Group(group) => { + let inner = replace_tilde_or_at_in_expr(&group.stream(), ident, path); + match group.delimiter() { + proc_macro2::Delimiter::Parenthesis => quote!(( #inner )), + proc_macro2::Delimiter::Brace => quote!({ #inner }), + proc_macro2::Delimiter::Bracket => quote!([ #inner ]), + proc_macro2::Delimiter::None => quote!(#inner) + } + }, + proc_macro2::TokenTree::Punct(ppp) => { + let ch = ppp.as_char(); + + if ch == '~' { + quote!(#path) + } else if ch == '@' { + quote!(#ident) + } else { + quote!(#ppp) + } + }, + _ => quote!(#x) + }; + + tokens.push(f) + }); + + TokenStream::from_iter(tokens) +} + fn quote_action(action: &Action, field_path: Option, ctx: &ImplContext) -> TokenStream { match action { Action::InlineAtExpr(args) => { @@ -648,7 +682,17 @@ fn quote_action(action: &Action, field_path: Option, ctx: &ImplCont }; quote!(#path #args) }, - Action::InlineExpr(args) => args.clone(), + Action::InlineExpr(args) => { + let ident = match ctx.kind { + Kind::FromOwned | Kind::FromRef => quote!(value), + _ => quote!(self), + }; + let path = match ctx.kind { + Kind::FromOwned | Kind::FromRef => quote!(value.#field_path), + _ => quote!(self.#field_path), + }; + replace_tilde_or_at_in_expr(args, &ident, &path) + }, Action::Closure(args) => { let ident = match ctx.kind { Kind::FromOwned | Kind::FromRef => quote!(value), diff --git a/o2o-impl/src/tests.rs b/o2o-impl/src/tests.rs index 0e0ac70..5b90111 100644 --- a/o2o-impl/src/tests.rs +++ b/o2o-impl/src/tests.rs @@ -1,16 +1,16 @@ #![cfg(test)] -//use std::io::Write; - use proc_macro2::TokenStream; use quote::quote; use syn::{DeriveInput, Error}; use crate::expand::derive; +// region: Debuger + +// use std::io::Write; // #[test] // fn debuger() { // let code_fragment = quote!{ - // }; // let input: DeriveInput = syn::parse2(code_fragment).unwrap(); @@ -30,6 +30,8 @@ use crate::expand::derive; // } // } +// endregion: Debuger + #[test] fn missing_map_instructions() { let code_fragments = vec![ @@ -51,9 +53,9 @@ fn missing_map_instructions() { for code_fragment in code_fragments { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let message = get_error(output, true); - assert_eq!(message, "At least one 'map'-like struct level instruction is expected."); + assert_eq!(message, "At least one trait instruction is expected."); } } @@ -79,9 +81,9 @@ fn unrecognized_struct_instructions() { for (code_fragment, err) in test_data { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let message = get_error(output, false); - assert_eq!(message, format!("Struct level instruction '{}' is not supported.", err)); + assert_eq!(message, format!("Struct instruction '{}' is not supported.", err)); } } @@ -114,9 +116,9 @@ fn unrecognized_member_instructions() { for (code_fragment, err) in test_data { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let message = get_error(output, false); - assert_eq!(message, format!("Member level instruction '{}' is not supported.", err)); + assert_eq!(message, format!("Member instruction '{}' is not supported.", err)); } } @@ -176,7 +178,7 @@ fn more_than_one_default_instruction() { for (code_fragment, err) in test_data { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let message = get_error(output, true); assert_eq!(message, format!("There can be at most one default #[{}(...)] instruction.", err)); } @@ -231,16 +233,14 @@ fn missing_children_instruction() { for (code_fragment, errs) in test_data { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let errors: Vec = get_error_iter(output).collect(); for (ty, should_contain) in errs { match should_contain { - true => assert!(message.contains(&*format!("Missing #[children(...)] instruction for {}", ty))), - false => assert!(!message.contains(&*format!("Missing #[children(...)] instruction for {}", ty))) + true => assert!(errors.iter().any(|x| x.to_string() == format!("Missing #[children(...)] instruction for {}", ty))), + false => assert!(!errors.iter().any(|x| x.to_string() == format!("Missing #[children(...)] instruction for {}", ty))) } } - - assert!(message.contains("")); } } @@ -307,23 +307,298 @@ fn incomplete_children_instruction() { for (code_fragment, errs) in test_data { let input: DeriveInput = syn::parse2(code_fragment).unwrap(); let output = derive(&input); - let message = get_error(output); + let errors: Vec = get_error_iter(output).collect(); for (field, ty, should_contain) in errs { match should_contain { - true => assert!(message.contains(&*format!("Missing '{}: [Type Path]' instruction for type {}", field, ty))), - false => assert!(!message.contains(&*format!("Missing '{}: [Type Path]' instruction for type {}", field, ty))) + true => assert!(errors.iter().any(|x| x.to_string() == format!("Missing '{}: [Type Path]' instruction for type {}", field, ty))), + false => assert!(!errors.iter().any(|x| x.to_string() == format!("Missing '{}: [Type Path]' instruction for type {}", field, ty))) } } } } -fn get_error(output: Result) -> String { +#[test] +fn incomplete_ghost_instruction() { + let test_data = vec![ + (quote! { + #[into(Entity)] + struct EntityDto { + some_val: i32, + #[ghost] + another_val: i32, + } + }, vec![]), + (quote! { + #[from(Entity)] + struct EntityDto { + some_val: i32, + #[ghost] + another_val: i32, + } + }, vec![ + ("another_val", "Entity") + ]), + (quote! { + #[from(Entity)] + struct EntityDto { + some_val: i32, + #[ghost()] + another_val: i32, + } + }, vec![ + ("another_val", "Entity") + ]), + (quote! { + #[map(Entity)] + struct EntityDto { + some_val: i32, + #[ghost] + another_val: i32, + } + }, vec![ + ("another_val", "Entity") + ]), + (quote! { + #[map(Entity)] + struct EntityDto { + some_val: i32, + #[ghost()] + another_val: i32, + } + }, vec![ + ("another_val", "Entity") + ]), + (quote! { + #[from(Entity)] + #[from(Entity2)] + struct EntityDto { + some_val: i32, + #[ghost] + another_val: i32, + #[ghost(Entity2)] + third_val: i32, + } + }, vec![ + ("another_val", "Entity"), + ("another_val", "Entity2"), + ("third_val", "Entity2") + ]) + ]; + + for (code_fragment, errs) in test_data { + let input: DeriveInput = syn::parse2(code_fragment).unwrap(); + let output = derive(&input); + + if errs.len() > 0 { + let errors: Vec = get_error_iter(output).collect(); + + assert_eq!(errs.len(), errors.len()); + + for (field, ty) in errs { + assert!(errors.iter().any(|x| x.to_string() == format!("Member instruction #[ghost(...)] for member '{}' should provide default value for type {}", field, ty))) + } + } else { + assert!(output.is_ok()) + } + } +} + +#[test] +fn incomplete_field_attr_instruction() { + let test_data = vec![ + (quote! { + #[into(Entity as {})] + struct EntityDto (i32); + }, vec![ + ("owned_into", "Entity", false), + ("ref_into", "Entity", false) + ]), + (quote! { + #[into(Entity as {})] + struct EntityDto (#[from]i32); + }, vec![ + ("owned_into", "Entity", false), + ("ref_into", "Entity", false) + ]), + (quote! { + #[into_existing(Entity as {})] + struct EntityDto (i32); + }, vec![ + ("owned_into_existing", "Entity", false), + ("ref_into_existing", "Entity", false) + ]), + (quote! { + #[from(Entity as {})] + struct EntityDto (i32); + }, vec![ + ("from_owned", "Entity", true), + ("from_ref", "Entity", true) + ]), + (quote! { + #[from(Entity as {})] + struct EntityDto (#[into()]i32); + }, vec![ + ("from_owned", "Entity", true), + ("from_ref", "Entity", true) + ]), + (quote! { + #[map(Entity as {})] + #[map(Entity2 as {})] + struct EntityDto (#[map(Entity2| test)]i32); + }, vec![ + ("from_owned", "Entity", true), + ("from_ref", "Entity", true), + ("owned_into", "Entity", false), + ("ref_into", "Entity", false) + ]), + (quote! { + #[map(Entity as {})] + #[from(Entity2 as {})] + struct EntityDto (#[from(Entity2| {123})]i32); + }, vec![ + ("from_owned", "Entity", true), + ("from_ref", "Entity", true), + ("owned_into", "Entity", false), + ("ref_into", "Entity", false), + ]), + (quote! { + #[map_owned(StuffWrapper as {})] + #[o2o(wrapped(payload))] + struct Stuff(i32); + }, vec![]), + ]; + + for (code_fragment, errs) in test_data { + let input: DeriveInput = syn::parse2(code_fragment).unwrap(); + let output = derive(&input); + + if errs.len() > 0 { + let errors: Vec = get_error_iter(output).collect(); + + assert_eq!(errs.len(), errors.len()); + + for (field, ty, or_action) in errs { + assert!(errors.iter().any(|x| x.to_string() == format!("Member 0 should have member trait instruction with field name{}, that corresponds to #[{}({}...)] trait instruction", if or_action {" or an action"} else {""}, field, ty))) + } + } else { + assert!(output.is_ok()) + } + } +} + +#[test] +fn incomplete_field_attr_instruction_2() { + let test_data = vec![ + (quote! { + #[from(Entity as {})] + struct EntityDto (#[from]i32); + }, vec![ + ("from", "Entity", true) + ]), + (quote! { + #[from(Entity as {})] + struct EntityDto ( + #[from]i32, + #[parent]i16 + ); + }, vec![ + ("from", "Entity", true) + ]), + (quote! { + #[into(Entity as {})] + struct EntityDto (#[into]i32); + }, vec![ + ("into", "Entity", false) + ]), + (quote! { + #[into(Entity as {})] + struct EntityDto ( + #[into]i32, + #[parent]i16, + #[ghost]i8 + ); + }, vec![ + ("into", "Entity", false) + ]), + (quote! { + #[into_existing(Entity as {})] + struct EntityDto (#[into_existing]i32); + }, vec![ + ("into_existing", "Entity", false) + ]), + (quote! { + #[into_existing(Entity as {})] + struct EntityDto ( + #[into_existing]i32, + #[parent]i16, + #[ghost]i8 + ); + }, vec![ + ("into_existing", "Entity", false) + ]), + (quote! { + #[into_existing(Entity as {})] + struct EntityDto ( + #[owned_into_existing(test)] + #[ref_into_existing] + i32 + ); + }, vec![ + ("ref_into_existing", "Entity", false) + ]), + (quote! { + #[into_existing(Entity as {})] + struct EntityDto ( + #[owned_into(test)] + #[ref_into] + i32 + ); + }, vec![ + ("ref_into", "Entity", false) + ]), + ]; + + for (code_fragment, errs) in test_data { + let input: DeriveInput = syn::parse2(code_fragment).unwrap(); + let output = derive(&input); + + if errs.len() > 0 { + let errors: Vec = get_error_iter(output).collect(); + + assert_eq!(errs.len(), errors.len()); + + for (field, ty, or_action) in errs { + assert!(errors.iter().any(|x| x.to_string() == format!("Member trait instruction #[{}(...)] for member 0 should specify corresponding field name of the {}{}", field, ty, if or_action {" or an action"} else {""}))) + } + } else { + assert!(output.is_ok()) + } + } +} + +fn get_error(output: Result, expect_root_error: bool) -> String { assert!(output.is_err()); let mut err_iter = output.unwrap_err().into_iter(); + + if expect_root_error { + let error = err_iter.next(); + assert_eq!(error.expect("Root error expected").to_string(), "Cannot expand o2o macro"); + } + let error = err_iter.next(); - let message = error.expect("One error expected").to_string(); + let message = error.expect("Two errors expected").to_string(); assert!(err_iter.next().is_none()); message +} + +fn get_error_iter(output: Result) -> impl Iterator { + assert!(output.is_err()); + let mut err_iter = output.unwrap_err().into_iter(); + + let error = err_iter.next(); + assert!(error.expect("Root error expected").to_string() == "Cannot expand o2o macro"); + + err_iter } \ No newline at end of file diff --git a/o2o-impl/src/validate.rs b/o2o-impl/src/validate.rs index e857703..a805f34 100644 --- a/o2o-impl/src/validate.rs +++ b/o2o-impl/src/validate.rs @@ -1,52 +1,62 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use proc_macro2::Span; use quote::ToTokens; -use syn::{Result, spanned::Spanned}; -use crate::{ast::{Struct, Field}, attr::{Kind, StructAttrCore, StructAttrs, FieldChildAttr, TypePath, ChildrenAttr, WhereAttr, StructGhostAttr}}; +use syn::{spanned::Spanned, Result}; +use crate::{ast::Struct, attr::{ChildrenAttr, FieldChildAttr, Kind, StructAttrCore, StructAttrs, StructGhostAttr, StructKindHint, TypePath, WhereAttr}}; pub(crate) fn validate(input: &Struct) -> Result<()> { + let mut errors: HashMap = HashMap::new(); + if input.attrs.attrs.is_empty() { - return Err(syn::Error::new(Span::call_site(), "At least one 'map'-like struct level instruction is expected.")) + errors.insert("At least one trait instruction is expected.".into(), Span::call_site()); } - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::FromOwned))?; - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::FromRef))?; - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::OwnedInto))?; - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::RefInto))?; - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::OwnedIntoExisting))?; - validate_struct_attrs(input.attrs.iter_for_kind(&Kind::RefIntoExisting))?; + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::FromOwned), &mut errors); + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::FromRef), &mut errors); + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::OwnedInto), &mut errors); + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::RefInto), &mut errors); + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::OwnedIntoExisting), &mut errors); + validate_struct_attrs(input.attrs.iter_for_kind(&Kind::RefIntoExisting), &mut errors); let type_paths = input.attrs.attrs.iter() .map(|x| &x.attr.ty) .collect::>(); - validate_ghost_attrs(&Kind::FromOwned, &input.attrs.ghost_attrs, &type_paths)?; - validate_ghost_attrs(&Kind::FromRef, &input.attrs.ghost_attrs, &type_paths)?; - validate_ghost_attrs(&Kind::OwnedInto, &input.attrs.ghost_attrs, &type_paths)?; - validate_ghost_attrs(&Kind::RefInto, &input.attrs.ghost_attrs, &type_paths)?; - validate_ghost_attrs(&Kind::OwnedIntoExisting, &input.attrs.ghost_attrs, &type_paths)?; - validate_ghost_attrs(&Kind::RefIntoExisting, &input.attrs.ghost_attrs, &type_paths)?; + validate_ghost_attrs(&Kind::FromOwned, &input.attrs.ghost_attrs, &type_paths, &mut errors); + validate_ghost_attrs(&Kind::FromRef, &input.attrs.ghost_attrs, &type_paths, &mut errors); + validate_ghost_attrs(&Kind::OwnedInto, &input.attrs.ghost_attrs, &type_paths, &mut errors); + validate_ghost_attrs(&Kind::RefInto, &input.attrs.ghost_attrs, &type_paths, &mut errors); + validate_ghost_attrs(&Kind::OwnedIntoExisting, &input.attrs.ghost_attrs, &type_paths, &mut errors); + validate_ghost_attrs(&Kind::RefIntoExisting, &input.attrs.ghost_attrs, &type_paths, &mut errors); + + validate_children_attrs(&input.attrs.children_attrs, &type_paths, &mut errors); + validate_where_attrs(&input.attrs.where_attrs, &type_paths, &mut errors); + + validate_fields(input, &input.attrs, &mut errors); - validate_children_attrs(&input.attrs.children_attrs, &type_paths)?; - validate_where_attrs(&input.attrs.where_attrs, &type_paths)?; + if errors.is_empty() { + Ok(()) + } else { + let mut root_err = syn::Error::new(Span::call_site(), "Cannot expand o2o macro"); - validate_fields(&input.fields, &input.attrs)?; - Ok(()) + errors.iter().for_each(|(err, sp)| root_err.combine(syn::Error::new(*sp, err))); + + Err(root_err) + } } -fn validate_struct_attrs<'a, I: Iterator>(attrs: I) -> Result<()> { +fn validate_struct_attrs<'a, I: Iterator>(attrs: I, errors: &mut HashMap) { let mut unique_ident = HashSet::new(); for attr in attrs { if !unique_ident.insert(&attr.ty) { - return Err(syn::Error::new(attr.ty.span, "Ident here must be unique.")) + errors.insert("Ident here must be unique.".into(), attr.ty.span); } } - Ok(()) } -fn validate_ghost_attrs(kind: &Kind, ghost_attrs: &[StructGhostAttr], type_paths: &HashSet<&TypePath>) -> Result<()> { +fn validate_ghost_attrs(kind: &Kind, ghost_attrs: &[StructGhostAttr], type_paths: &HashSet<&TypePath>, errors: &mut HashMap) { if ghost_attrs.iter().filter(|x|x.applicable_to[kind] && x.attr.container_ty.is_none()).count() > 1 { - return Err(syn::Error::new(Span::call_site(), "There can be at most one default #[ghost(...)] instruction.")) + errors.insert("There can be at most one default #[ghost(...)] instruction.".into(), Span::call_site()); } let mut unique_dedicated_attr_type_path = HashSet::new(); @@ -54,18 +64,17 @@ fn validate_ghost_attrs(kind: &Kind, ghost_attrs: &[StructGhostAttr], type_paths for ghost_attr in ghost_attrs.iter().filter(|x|x.applicable_to[kind] && x.attr.container_ty.is_some()) { let tp = ghost_attr.attr.container_ty.as_ref().unwrap(); if !type_paths.contains(tp) { - return Err(syn::Error::new(tp.span, format!("Type {} doesn't match any type specified in 'map'-like struct level instructions.", tp.path_str))) + errors.insert("Type {} doesn't match any type specified in trait instructions.".into(), tp.span); } if !unique_dedicated_attr_type_path.insert(tp) { - return Err(syn::Error::new(tp.span, format!("Dedicated #[ghost(...)] instruction for type {} is already defined.", tp.path_str))) + errors.insert("Dedicated #[ghost(...)] instruction for type {} is already defined.".into(), tp.span); } } - Ok(()) } -fn validate_children_attrs(children_attrs: &[ChildrenAttr], type_paths: &HashSet<&TypePath>) -> Result<()> { +fn validate_children_attrs(children_attrs: &[ChildrenAttr], type_paths: &HashSet<&TypePath>, errors: &mut HashMap) { if children_attrs.iter().filter(|x| x.container_ty.is_none()).count() > 1 { - return Err(syn::Error::new(Span::call_site(), "There can be at most one default #[children(...)] instruction.")) + errors.insert("There can be at most one default #[children(...)] instruction.".into(), Span::call_site()); } let mut unique_dedicated_attr_type_path = HashSet::new(); @@ -73,10 +82,10 @@ fn validate_children_attrs(children_attrs: &[ChildrenAttr], type_paths: &HashSet for children_attr in children_attrs.iter() { if let Some(tp) = &children_attr.container_ty{ if !type_paths.contains(tp) { - return Err(syn::Error::new(tp.span, format!("Type {} doesn't match any type specified in 'map'-like struct level instructions.", tp.path_str))) + errors.insert("Type {} doesn't match any type specified in trait instructions.".into(), tp.span); } if !unique_dedicated_attr_type_path.insert(tp) { - return Err(syn::Error::new(tp.span, format!("Dedicated #[children(...)] instruction for type {} is already defined.", tp.path_str))) + errors.insert("Dedicated #[children(...)] instruction for type {} is already defined.".into(), tp.span); } } @@ -84,16 +93,15 @@ fn validate_children_attrs(children_attrs: &[ChildrenAttr], type_paths: &HashSet for child_data in &children_attr.children { if !unique_field.insert(child_data) { - return Err(syn::Error::new(child_data.field_path.span(), "Ident here must be unique.")) + errors.insert("Ident here must be unique.".into(), child_data.field_path.span()); } } } - Ok(()) } -fn validate_where_attrs(where_attrs: &[WhereAttr], type_paths: &HashSet<&TypePath>) -> Result<()> { +fn validate_where_attrs(where_attrs: &[WhereAttr], type_paths: &HashSet<&TypePath>, errors: &mut HashMap) { if where_attrs.iter().filter(|x| x.container_ty.is_none()).count() > 1 { - return Err(syn::Error::new(Span::call_site(), "There can be at most one default #[where_clause(...)] instruction.")) + errors.insert("There can be at most one default #[where_clause(...)] instruction.".into(), Span::call_site()); } let mut unique_dedicated_attr_type_path = HashSet::new(); @@ -101,78 +109,99 @@ fn validate_where_attrs(where_attrs: &[WhereAttr], type_paths: &HashSet<&TypePat for where_attr in where_attrs.iter() { if let Some(tp) = &where_attr.container_ty{ if !type_paths.contains(tp) { - return Err(syn::Error::new(tp.span, format!("Type {} doesn't match any type specified in 'map'-like struct level instructions.", tp.path_str))) + errors.insert("Type {} doesn't match any type specified in trait instructions.".into(), tp.span); } if !unique_dedicated_attr_type_path.insert(tp) { - return Err(syn::Error::new(tp.span, format!("Dedicated #[where_clause(...)] instruction for type {} is already defined.", tp.path_str))) + errors.insert("Dedicated #[where_clause(...)] instruction for type {} is already defined.".into(), tp.span); } } } - Ok(()) } -fn validate_fields(fields: &[Field], struct_attrs: &StructAttrs) -> Result<()> { +fn validate_fields(input: &Struct, struct_attrs: &StructAttrs, errors: &mut HashMap) { let into_type_paths = struct_attrs.iter_for_kind(&Kind::OwnedInto) .chain(struct_attrs.iter_for_kind(&Kind::RefInto)) .map(|x| &x.ty) .collect::>(); - let from_type_paths = struct_attrs.iter_for_kind(&Kind::FromOwned) - .chain(struct_attrs.iter_for_kind(&Kind::FromRef)) - .map(|x| &x.ty) - .collect::>(); - - let mut errors = HashSet::new(); - - for child_attr in fields.iter().flat_map(|x| &x.attrs.child_attrs) { + for child_attr in input.fields.iter().flat_map(|x| &x.attrs.child_attrs) { match &child_attr.container_ty { - Some(tp) => check_child_errors(child_attr, struct_attrs, tp, &mut errors), + Some(tp) => check_child_errors(child_attr, struct_attrs, tp, errors), None => for tp in into_type_paths.iter(){ - check_child_errors(child_attr, struct_attrs, tp, &mut errors) + check_child_errors(child_attr, struct_attrs, tp, errors) } } } - for field in fields { + let from_type_paths = struct_attrs.iter_for_kind(&Kind::FromOwned) + .chain(struct_attrs.iter_for_kind(&Kind::FromRef)) + .map(|x| &x.ty) + .collect::>(); + + for field in &input.fields { for ghost_attr in field.attrs.ghost_attrs.iter() { if ghost_attr.attr.action.is_some() { continue; } match &ghost_attr.attr.container_ty { Some(tp) => { - if from_type_paths.contains(tp) { - errors.insert(format!("Member level instruction #[ghost(...)] should provide default value for field '{}' for type {}", field.member.to_token_stream(), tp.path_str)); + if from_type_paths.contains(tp) { + errors.insert(format!("Member instruction #[ghost(...)] for member '{}' should provide default value for type {}", field.member.to_token_stream(), tp.path_str), field.member.span()); } }, None => { let field_name_str = field.member.to_token_stream().to_string(); for tp in from_type_paths.iter(){ - errors.insert(format!("Member level instruction #[ghost(...)] should provide default value for field '{}' for type {}", field_name_str, tp.path_str)); + errors.insert(format!("Member instruction #[ghost(...)] for member '{}' should provide default value for type {}", field_name_str, tp.path_str), field.member.span()); } } } } } - if !errors.is_empty() { - let errors: Vec = errors.into_iter().collect(); - return Err(syn::Error::new(Span::call_site(), errors.join("\n"))) - } + if !input.named_fields { + let struct_attrs = struct_attrs.iter_for_kind(&Kind::OwnedInto).map(|x| (x, Kind::OwnedInto)) + .chain(struct_attrs.iter_for_kind(&Kind::RefInto).map(|x| (x, Kind::RefInto))) + .chain(struct_attrs.iter_for_kind(&Kind::OwnedIntoExisting).map(|x| (x, Kind::OwnedIntoExisting))) + .chain(struct_attrs.iter_for_kind(&Kind::RefIntoExisting).map(|x| (x, Kind::RefIntoExisting))) + .chain(struct_attrs.iter_for_kind(&Kind::FromOwned).map(|x| (x, Kind::FromOwned))) + .chain(struct_attrs.iter_for_kind(&Kind::FromRef).map(|x| (x, Kind::FromRef))); + + for (struct_attr, kind) in struct_attrs { + if input.attrs.wrapped_attr(&struct_attr.ty).is_none() && struct_attr.struct_kind_hint == StructKindHint::Struct { + for field in &input.fields { + if field.attrs.ghost(&struct_attr.ty, &kind).is_some() || field.attrs.has_parent_attr(&struct_attr.ty) { + continue; + } - Ok(()) + if let Some(field_attr) = field.attrs.applicable_field_attr(&kind, &struct_attr.ty) { + if kind == Kind::FromOwned || kind == Kind::FromRef { + if !field_attr.attr.wrapper && field_attr.attr.member.is_none() && field_attr.attr.action.is_none() { + errors.insert(format!("Member trait instruction #[{}(...)] for member {} should specify corresponding field name of the {} or an action", field_attr.original_instr, field.member.to_token_stream(), struct_attr.ty.path), field.member.span()); + } + } else if !field_attr.attr.wrapper && field_attr.attr.member.is_none() { + errors.insert(format!("Member trait instruction #[{}(...)] for member {} should specify corresponding field name of the {}", field_attr.original_instr, field.member.to_token_stream(), struct_attr.ty.path_str), field.member.span()); + } + } else { + errors.insert(format!("Member {} should have member trait instruction with field name{}, that corresponds to #[{}({}...)] trait instruction", field.member.to_token_stream(), if kind == Kind::FromOwned || kind == Kind::FromRef { " or an action" } else { "" }, kind, struct_attr.ty.path_str), field.member.span()); + } + } + } + } + } } -fn check_child_errors(child_attr: &FieldChildAttr, struct_attrs: &StructAttrs, tp: &TypePath, errors: &mut HashSet) { +fn check_child_errors(child_attr: &FieldChildAttr, struct_attrs: &StructAttrs, tp: &TypePath, errors: &mut HashMap) { let children_attr = struct_attrs.children_attr(tp); for (idx, _level) in child_attr.child_path.child_path.iter().enumerate() { let path = child_attr.get_child_path_str(Some(idx)); match children_attr { Some(children_attr) => { if !children_attr.children.iter().any(|x| x.check_match(path)) { - errors.insert(format!("Missing '{}: [Type Path]' instruction for type {}", path, tp.path_str)); + errors.insert(format!("Missing '{}: [Type Path]' instruction for type {}", path, tp.path_str), tp.span); } }, - None => { errors.insert(format!("Missing #[children(...)] instruction for {}", tp.path_str)); } + None => { errors.insert(format!("Missing #[children(...)] instruction for {}", tp.path_str), tp.span); } } } } \ No newline at end of file diff --git a/o2o-macros/Cargo.toml b/o2o-macros/Cargo.toml index c8538cc..8bf95e5 100644 --- a/o2o-macros/Cargo.toml +++ b/o2o-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "o2o-macros" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["Artem Romanenia "] description = "Macro definitions of 'o2o' crate" @@ -11,5 +11,5 @@ repository = "https://github.com/Artem-Romanenia/o2o" proc-macro = true [dependencies] -o2o-impl = { version = "0.3.0", path = "../o2o-impl" } +o2o-impl = { version = "0.3.1", path = "../o2o-impl" } syn = "1.0.3" diff --git a/o2o-macros/src/lib.rs b/o2o-macros/src/lib.rs index 1f00cc5..073eeb8 100644 --- a/o2o-macros/src/lib.rs +++ b/o2o-macros/src/lib.rs @@ -5,236 +5,41 @@ use o2o_impl::expand::derive; use syn::{parse_macro_input, DeriveInput}; /// ### Object to Object mapper for Rust -/// **o2o** can implement `std::convert::From`, `std::convert::Into`, and custom `o2o::traits::IntoExisting` traits via procedural macro. It can be best explained through examples, so... +/// **o2o** can implement `std::convert::From`, `std::convert::Into`, and custom `o2o::traits::IntoExisting` traits via procedural macro. /// -/// ### Simplest Case +/// ### Basic example /// /// ``` rust /// use o2o::o2o; /// -/// struct Entity { -/// some_int: i32, -/// another_int: i16, +/// struct Person { +/// id: u32, +/// name: String, +/// age: u8 /// } /// /// #[derive(o2o)] -/// #[map_owned(Entity)] -/// struct EntityDto { -/// some_int: i32, -/// another_int: i16, -/// } -/// ``` -///
-/// View generated code -/// -/// ``` rust -/// impl std::convert::From for EntityDto { -/// fn from(value: Entity) -> EntityDto { -/// EntityDto { -/// some_int: value.some_int, -/// another_int: value.another_int, -/// } -/// } -/// } -/// impl std::convert::Into for EntityDto { -/// fn into(self) -> Entity { -/// Entity { -/// some_int: self.some_int, -/// another_int: self.another_int, -/// } -/// } -/// } -/// ``` -///
-/// -/// With the above code you should be able to do this: -/// -/// ``` rust -/// let entity = Entity { some_int: 123, another_int: 321 } -/// let dto: EntityDto = entity.into(); -/// // and this: -/// let dto = EntityDto { some_int: 123, another_int: 321 } -/// let entity: Entity = dto.into(); -/// ``` -/// -/// ### Different field name -/// -/// ``` rust -/// struct Entity { -/// some_int: i32, -/// another_int: i16, -/// } -/// -/// #[derive(o2o)] -/// #[from_ref(Entity)] -/// #[ref_into_existing(Entity)] -/// struct EntityDto { -/// some_int: i32, -/// #[map(another_int)] -/// different_int: i16, -/// } -/// ``` -///
-/// View generated code -/// -/// ``` rust -/// impl std::convert::From<&Entity> for EntityDto { -/// fn from(value: &Entity) -> EntityDto { -/// EntityDto { -/// some_int: value.some_int, -/// different_int: value.another_int, -/// } -/// } -/// } -/// impl o2o::traits::IntoExisting for &EntityDto { -/// fn into_existing(self, other: &mut Entity) { -/// other.some_int = self.some_int; -/// other.another_int = self.different_int; -/// } -/// } -/// ``` -///
-/// -/// ### Different field type -/// -/// ``` rust -/// struct Entity { -/// some_int: i32, -/// val: i16, -/// str: String -/// } -/// -/// #[derive(o2o)] -/// #[map(Entity)] -/// struct EntityDto { -/// some_int: i32, -/// #[from(~.to_string())] // Tilde allows to append code at the end of the right side of field initialization for From impls -/// #[into(~.parse::().unwrap())] // here it's the same but for Into impls -/// val: String, -/// // Here Into and From are symmetric, so it has to be only specified once. -/// // Note that .clone() is only needed for borrowing impls, so we use #[map_ref()] -/// #[map_ref(~.clone())] -/// str: String -/// } -/// ``` -///
-/// View generated code -/// -/// ``` rust -/// impl std::convert::From for EntityDto { -/// fn from(value: Entity) -> EntityDto { -/// EntityDto { -/// some_int: value.some_int, -/// val: value.val.to_string(), -/// str: value.str, // no .clone() -/// } -/// } -/// } -/// impl std::convert::From<&Entity> for EntityDto { -/// fn from(value: &Entity) -> EntityDto { -/// EntityDto { -/// some_int: value.some_int, -/// val: value.val.to_string(), -/// str: value.str.clone(), -/// } -/// } -/// } -/// impl std::convert::Into for EntityDto { -/// fn into(self) -> Entity { -/// Entity { -/// some_int: self.some_int, -/// val: self.val.parse::().unwrap(), -/// str: self.str, // no .clone() -/// } -/// } -/// } -/// impl std::convert::Into for &EntityDto { -/// fn into(self) -> Entity { -/// Entity { -/// some_int: self.some_int, -/// val: self.val.parse::().unwrap(), -/// str: self.str.clone(), -/// } -/// } -/// } -/// ``` -///
-/// -/// ### Nested structs -/// -/// ``` rust -/// struct Entity { -/// some_int: i32, -/// child: Child, -/// } -/// struct Child { -/// child_int: i32, -/// } -/// -/// #[derive(o2o)] -/// #[from_owned(Entity)] -/// struct EntityDto { -/// some_int: i32, -/// #[map(~.into())] -/// child: ChildDto +/// #[from_owned(Person)] // This tells o2o to generate 'From for PersonDto' implementation +/// #[owned_into(Person)] // This generates 'Into for PersonDto' +/// struct PersonDto { +/// id: u32, +/// name: String, +/// age: u8 /// } /// -/// #[derive(o2o)] -/// #[from_owned(Child)] -/// struct ChildDto { -/// child_int: i32, -/// } -/// ``` -///
-/// View generated code +/// // Applying #[derive(o2o)] on PersonDto allows you to do this: /// -/// ``` rust -/// impl std::convert::From for EntityDto { -/// fn from(value: Entity) -> EntityDto { -/// EntityDto { -/// some_int: value.some_int, -/// child: value.child.into(), -/// } -/// } -/// } -/// -/// impl std::convert::From for ChildDto { -/// fn from(value: Child) -> ChildDto { -/// ChildDto { -/// child_int: value.child_int, -/// } -/// } -/// } -/// ``` -///
+/// let person = Person { id: 123, name: "John".into(), age: 42 }; +/// let dto = PersonDto::from(person); /// -/// ### Nested collection +/// assert_eq!(dto.id, 123); assert_eq!(dto.name, "John"); assert_eq!(dto.age, 42); /// -/// ``` rust -/// struct Entity { -/// some_int: i32, -/// children: Vec, -/// } -/// struct Child { -/// child_int: i32, -/// } +/// // and this: /// -/// #[derive(o2o)] -/// #[map_owned(Entity)] -/// struct EntityDto { -/// some_int: i32, -/// // Here field name as well as type are different, so we pass in field name and tilde inline expression. -/// // Also, it doesn't hurt to use member level instruction #[map()], -/// // which is broader than struct level instruction #[map_owned] -/// #[map(children, ~.iter().map(|p|p.into()).collect())] -/// children_vec: Vec -/// } +/// let dto = PersonDto { id: 321, name: "Jack".into(), age: 23 }; +/// let person: Person = dto.into(); /// -/// #[derive(o2o)] -/// #[map_ref(Child)] -/// struct ChildDto { -/// child_int: i32, -/// } +/// assert_eq!(person.id, 321); assert_eq!(person.name, "Jack"); assert_eq!(person.age, 23); /// ``` /// /// For more examples, visit [github.com](https://github.com/Artem-Romanenia/o2o) diff --git a/o2o-tests/Cargo.toml b/o2o-tests/Cargo.toml index 035aa72..6676547 100644 --- a/o2o-tests/Cargo.toml +++ b/o2o-tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "o2o-tests" -version = "0.3.0" +version = "0.3.1" edition = "2021" authors = ["Artem Romanenia "] description = "Tests for 'o2o' crate" @@ -8,4 +8,4 @@ license = "MIT OR Apache-2.0" repository = "https://github.com/Artem-Romanenia/o2o" [dependencies] -o2o = { version = "0.3.0", path = "../" } \ No newline at end of file +o2o = { version = "0.3.2", path = "../" } \ No newline at end of file diff --git a/o2o-tests/tests/29_at_and_tilde_inside_inline_expr_tests.rs b/o2o-tests/tests/29_at_and_tilde_inside_inline_expr_tests.rs new file mode 100644 index 0000000..0483a75 --- /dev/null +++ b/o2o-tests/tests/29_at_and_tilde_inside_inline_expr_tests.rs @@ -0,0 +1,401 @@ +use o2o::{o2o, traits::IntoExisting}; + +#[derive(Default)] +struct Entity { + some_int: i32, + another_int: i16, + some_float: f32, + another_float: f64, + + extra_float: f32, + + child: Child +} + +#[derive(Default)] +struct Child { + child_float: f32 +} + +fn another_int_to_string(e: &Entity) -> String { + e.another_int.to_string() +} + +fn another_int_string_to_int(f: &EntityDto) -> i16 { + f.another_int_string.parse::().unwrap() +} + +fn unnamed_another_int_string_to_int(f: &UnnamedEntityDto) -> i16 { + f.1.parse::().unwrap() +} + +fn float_to_string(f: f32) -> String { + f.to_string() +} + +fn string_to_float(f: String) -> f32 { + f.parse::().unwrap() +} + +fn float64_to_string(f: f64) -> String { + f.to_string() +} + +fn string_to_float64(f: String) -> f64 { + f.parse::().unwrap() +} + +fn extra_string(e: &Entity) -> String { + e.extra_float.to_string() +} + +fn extra_float(e: &EntityDto) -> f32 { + e.extra_string.parse::().unwrap() +} + +fn extra_float_2(e: &UnnamedEntityDto) -> f32 { + e.4.parse::().unwrap() +} + +#[derive(o2o)] +#[map(Entity)] +#[into_existing(Entity)] +#[children(child: Child)] +#[ghost(extra_float: { extra_float(&@) })] +struct EntityDto { + some_int: i32, + + #[o2o( + from_ref({ another_int_to_string(@) } ), + from_owned({ another_int_to_string(&@) } ), + ref_into(another_int, { another_int_string_to_int(@) } ), + owned_into(another_int, { another_int_string_to_int(&@) } ) + )] + another_int_string: String, + + #[o2o(from({ float_to_string(~) } ))] + #[o2o(ref_into({ string_to_float(~.clone()) } ))] + #[o2o(owned_into({ string_to_float(~.clone()) } ))] + some_float: String, + + #[from(another_float, { float64_to_string(~) } )] + #[ref_into(another_float, { string_to_float64(~.clone()) } )] + #[owned_into(another_float, { string_to_float64(~.clone()) } )] + another_float_string: String, + + #[ghost({ extra_string(&@) })] + extra_string: String, + + #[child(child)] + #[from(child_float, { float_to_string(~) })] + #[into(child_float, { string_to_float(~.clone()) })] + child_float_string: String, +} + +#[derive(o2o)] +#[map(Entity as {})] +#[into_existing(Entity as {})] +#[children(child: Child as {})] +#[ghost(extra_float: { extra_float_2(&@) })] +struct UnnamedEntityDto ( + #[map(Entity| some_int)] + i32, + + #[o2o( + from_ref({ another_int_to_string(@) } ), + from_owned({ another_int_to_string(&@) } ), + ref_into(another_int, { unnamed_another_int_string_to_int(@) } ), + owned_into(another_int, { unnamed_another_int_string_to_int(&@) } ) + )] + String, + + #[o2o(from(some_float, { float_to_string(~) } ))] + #[o2o(ref_into(some_float, { string_to_float(~.clone()) } ))] + #[o2o(owned_into(some_float, { string_to_float(~.clone()) } ))] + String, + + #[from(another_float, { float64_to_string(~) } )] + #[ref_into(another_float, { string_to_float64(~.clone()) } )] + #[owned_into(another_float, { string_to_float64(~.clone()) } )] + String, + + #[ghost({ extra_string(&@) })] + String, + + #[child(child)] + #[from(child_float, { float_to_string(~) })] + #[into(child_float, { string_to_float(~.clone()) })] + String, +); + +#[test] +fn named2named() { + let dto = EntityDto { + some_int: 123, + another_int_string: "456".into(), + some_float: "789".into(), + another_float_string: "987".into(), + extra_string: "654".into(), + child_float_string: "321".into() + }; + + let entity: Entity = dto.into(); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn named2named_reverse() { + let entity = Entity { + some_int: 123, + another_int: 456, + some_float: 789.0, + another_float: 987.0, + extra_float: 654.0, + child: Child { + child_float: 321.0 + } + }; + + let dto: EntityDto = entity.into(); + + assert_eq!(123, dto.some_int); + assert_eq!("456", dto.another_int_string); + assert_eq!("789", dto.some_float); + assert_eq!("987", dto.another_float_string); + assert_eq!("654", dto.extra_string); + assert_eq!("321", dto.child_float_string); + +} + +#[test] +fn named2named_ref() { + let dto = &EntityDto { + some_int: 123, + another_int_string: "456".into(), + some_float: "789".into(), + another_float_string: "987".into(), + extra_string: "654".into(), + child_float_string: "321".into() + }; + + let entity: Entity = dto.into(); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn named2named_ref_reverse() { + let entity = &Entity { + some_int: 123, + another_int: 456, + some_float: 789.0, + another_float: 987.0, + extra_float: 654.0, + child: Child { + child_float: 321.0 + } + }; + + let dto: EntityDto = entity.into(); + + assert_eq!(123, dto.some_int); + assert_eq!("456", dto.another_int_string); + assert_eq!("789", dto.some_float); + assert_eq!("987", dto.another_float_string); + assert_eq!("654", dto.extra_string); + assert_eq!("321", dto.child_float_string); + +} + +#[test] +fn existing_named2named() { + let dto = EntityDto { + some_int: 123, + another_int_string: "456".into(), + some_float: "789".into(), + another_float_string: "987".into(), + extra_string: "654".into(), + child_float_string: "321".into() + }; + + let mut entity: Entity = Default::default(); + dto.into_existing(&mut entity); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn existing_named2named_ref() { + let dto = &EntityDto { + some_int: 123, + another_int_string: "456".into(), + some_float: "789".into(), + another_float_string: "987".into(), + extra_string: "654".into(), + child_float_string: "321".into() + }; + + let mut entity: Entity = Default::default(); + dto.into_existing(&mut entity); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + + + + + + +#[test] +fn unnamed2named() { + let dto = UnnamedEntityDto ( + 123, + "456".into(), + "789".into(), + "987".into(), + "654".into(), + "321".into() + ); + + let entity: Entity = dto.into(); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn unnamed2named_reverse() { + let entity = Entity { + some_int: 123, + another_int: 456, + some_float: 789.0, + another_float: 987.0, + extra_float: 654.0, + child: Child { + child_float: 321.0 + } + }; + + let dto: UnnamedEntityDto = entity.into(); + + assert_eq!(123, dto.0); + assert_eq!("456", dto.1); + assert_eq!("789", dto.2); + assert_eq!("987", dto.3); + assert_eq!("654", dto.4); + assert_eq!("321", dto.5); +} + +#[test] +fn unnamed2named_ref() { + let dto = &UnnamedEntityDto ( + 123, + "456".into(), + "789".into(), + "987".into(), + "654".into(), + "321".into() + ); + + let entity: Entity = dto.into(); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn unnamed2named_ref_reverse() { + let entity = &Entity { + some_int: 123, + another_int: 456, + some_float: 789.0, + another_float: 987.0, + extra_float: 654.0, + child: Child { + child_float: 321.0 + } + }; + + let dto: UnnamedEntityDto = entity.into(); + + assert_eq!(123, dto.0); + assert_eq!("456", dto.1); + assert_eq!("789", dto.2); + assert_eq!("987", dto.3); + assert_eq!("654", dto.4); + assert_eq!("321", dto.5); + +} + +#[test] +fn existing_unnamed2named() { + let dto = UnnamedEntityDto ( + 123, + "456".into(), + "789".into(), + "987".into(), + "654".into(), + "321".into() + ); + + let mut entity: Entity = Default::default(); + dto.into_existing(&mut entity); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} + +#[test] +fn existing_unnamed2named_ref() { + let dto = &UnnamedEntityDto ( + 123, + "456".into(), + "789".into(), + "987".into(), + "654".into(), + "321".into() + ); + + let mut entity: Entity = Default::default(); + dto.into_existing(&mut entity); + + assert_eq!(123, entity.some_int); + assert_eq!(456, entity.another_int); + assert_eq!(789.0, entity.some_float); + assert_eq!(987.0, entity.another_float); + assert_eq!(654.0, entity.extra_float); + assert_eq!(321.0, entity.child.child_float); +} \ No newline at end of file