Skip to content

Commit

Permalink
add: Impl Into/From with default call
Browse files Browse the repository at this point in the history
  • Loading branch information
Nukesor committed Mar 29, 2022
1 parent d0e0594 commit a20ef66
Show file tree
Hide file tree
Showing 8 changed files with 184 additions and 17 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ It's purpose is to implement traits between various structs.
- `merge` attribute for generating `StructMergeInto` and the auto-implemented `StructMerge` implementations.
- `merge_ref` attribute for generating the `StructMergeRefInto` and the auto-implemented `StructMergeRef` implementations.
- `into` attribute for generating `std::convert::From` and the auto-implemented `std::convert::Into` implementations.
- `into_default` attribute for generating `std::convert::From` and the auto-implemented `std::convert::Into` implementations.
This populates all non-matching fields by calling `Default::default` for the target struct.
27 changes: 12 additions & 15 deletions codegen/src/generate/into/normal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,26 @@ use crate::Parameters;

/// Generate the [std::convert::From] for given structs.
pub(crate) fn impl_into(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
let mut functions_tokens = TokenStream::new();
let mut initializer_tokens = TokenStream::new();

// Add `merge_ref` impl.
let stream = merge_ref(params, fields);
functions_tokens.extend(vec![stream]);
// Add `into` impl.
let stream = into(params, fields);
initializer_tokens.extend(vec![stream]);

// Surround functions with `impl` block.
// Surround the function with the correct Default `impl` block.
let src_ident = &params.src_struct.ident;
let target_path = &params.target_path;
quote! {
impl std::convert::From<#src_ident> for #target_path {
#functions_tokens
fn from(src: #src_ident) -> Self {
#initializer_tokens
}
}
}
}

/// Generate the [inter_struct::merge::StructMergeRef::merge_ref] function for given structs.
///
/// All fields must implement `Clone`.
fn merge_ref(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
/// Generate the [std::convert::From] function body for given structs.
fn into(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
let mut assignments = TokenStream::new();

for (src_field, dest_field) in fields {
Expand Down Expand Up @@ -135,14 +135,11 @@ fn merge_ref(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
assignments.extend(vec![snippet]);
}

let src_ident = &params.src_struct.ident;
let target_path = &params.target_path;
let assignment_code = assignments.to_token_stream();
quote! {
fn from(src: #src_ident) -> Self {
#target_path {
#assignment_code
}
#target_path {
#assignment_code
}
}
}
133 changes: 133 additions & 0 deletions codegen/src/generate/into/with_default.rs
Original file line number Diff line number Diff line change
@@ -1 +1,134 @@
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::Field;

use crate::error::*;
use crate::generate::field::*;
use crate::generate::types::*;
use crate::Parameters;

/// Generate the [std::convert::From] for given structs.
pub(crate) fn impl_into_default(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
let mut initializer_tokens = TokenStream::new();

// Add `into_default` impl.
let stream = into_default(params, fields);
initializer_tokens.extend(vec![stream]);

// Surround the function with the correct Default `impl` block.
let src_ident = &params.src_struct.ident;
let target_path = &params.target_path;
let test = quote! {
impl std::convert::From<#src_ident> for #target_path {
fn from(src: #src_ident) -> Self {
#initializer_tokens
}
}
};
test
}

/// Generate the [std::convert::From] function body for given structs.
fn into_default(params: &Parameters, fields: Vec<(Field, Field)>) -> TokenStream {
let mut assignments = TokenStream::new();

for (src_field, dest_field) in fields {
let src_field_ident = src_field.ident;
let dest_field_ident = dest_field.ident;

// Find out, whether the fields are optional or not.
let src_field_type = match determine_field_type(src_field.ty) {
Ok(field) => field,
Err(err) => {
assignments.extend(vec![err]);
continue;
}
};
let target_field_type = match determine_field_type(dest_field.ty) {
Ok(field) => field,
Err(err) => {
assignments.extend(vec![err]);
continue;
}
};

let snippet = match (src_field_type, target_field_type) {
// Both fields have the same type
(FieldType::Normal(src_type), FieldType::Normal(target_type)) => {
Some(equal_type_or_err!(
src_type,
target_type,
"",
quote! {
#dest_field_ident: src.#src_field_ident,
}
))
}
// The src is optional and needs to be `Some(T)` to be merged.
(FieldType::Optional { .. }, FieldType::Normal(_)) => None,
// The target is optional and needs to be wrapped in `Some(T)` to be merged.
(
FieldType::Normal(src_type),
FieldType::Optional {
inner: target_type, ..
},
) => Some(equal_type_or_err!(
src_type,
target_type,
"",
quote! {
#dest_field_ident: Some(src.#src_field_ident),
}
)),
// Both fields are optional. It can now be either of these:
// - (Option<T>, Option<T>)
// - (Option<Option<T>>, Option<T>)
// - (Option<T>, Option<Option<T>>)
(
FieldType::Optional {
inner: inner_src_type,
outer: outer_src_type,
},
FieldType::Optional {
inner: inner_target_type,
outer: outer_target_type,
},
) => {
// Handling the (Option<T>, Option<T>) case
if is_equal_type(&inner_src_type, &inner_target_type) {
Some(quote! {
#dest_field_ident: src.#src_field_ident,
})
// Handling the (src: Option<Option<<T>>, dest: Option<T>) case
} else if is_equal_type(&inner_src_type, &outer_target_type) {
None
// Handling the (src: Option<<T>, dest: Option<Option<T>)> case
} else {
Some(equal_type_or_err!(
outer_src_type,
inner_target_type,
"",
quote! {
#dest_field_ident: Some(src.#src_field_ident.clone()),
}
))
}
}
// Skip anything where either of the fields are invalid
(FieldType::Invalid, _) | (_, FieldType::Invalid) => None,
};

if let Some(snippet) = snippet {
assignments.extend(vec![snippet]);
}
}

let target_path = &params.target_path;
let assignment_code = assignments.to_token_stream();
quote! {
#target_path {
#assignment_code
..#target_path::default()
}
}
}
4 changes: 4 additions & 0 deletions codegen/src/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,9 @@ pub(crate) fn generate_impl(mode: &Mode, params: Parameters) -> Result<TokenStre
Mode::Merge => Ok(merge::owned::impl_owned(&params, similar_fields)),
Mode::MergeRef => Ok(merge::borrowed::impl_borrowed(&params, similar_fields)),
Mode::Into => Ok(into::normal::impl_into(&params, similar_fields)),
Mode::IntoDefault => Ok(into::with_default::impl_into_default(
&params,
similar_fields,
)),
}
}
4 changes: 3 additions & 1 deletion codegen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum Mode {
Merge,
MergeRef,
Into,
IntoDefault,
}

pub(crate) struct Parameters {
Expand Down Expand Up @@ -55,7 +56,7 @@ pub(crate) struct Parameters {
///
/// Eiter a single path or a list of paths can be specified.
/// The traits will then be implemented for each given target struct.
#[proc_macro_derive(InterStruct, attributes(merge, merge_ref, into))]
#[proc_macro_derive(InterStruct, attributes(merge, merge_ref, into, into_default))]
pub fn inter_struct(struct_ast: TokenStream) -> TokenStream {
// Parse the main macro input as a struct.
// We work on a clone of the struct ast.
Expand Down Expand Up @@ -84,6 +85,7 @@ pub fn inter_struct(struct_ast: TokenStream) -> TokenStream {
"merge" => Mode::Merge,
"merge_ref" => Mode::MergeRef,
"into" => Mode::Into,
"into_default" => Mode::IntoDefault,
_ => continue,
};

Expand Down
2 changes: 1 addition & 1 deletion testing/src/into_test/into.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod tests {

/// Test the implementation of [std::convert::Into] generated by inter-struct.
#[test]
fn merge_identical() {
fn test_into() {
// The base struct that's going to be merged into.
let from = FromStruct::new();

Expand Down
18 changes: 18 additions & 0 deletions testing/src/into_test/into_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#[cfg(test)]
mod tests {
use crate::into_test::*;

/// Test the implementation of [std::convert::Into] generated by inter-struct.
#[test]
fn test_into_with_default() {
// The base struct that's going to be merged into.
let from = FromStruct::new();

let into = IntoDefaultStruct::from(from);
assert_eq!(into.normal, "from");
assert_eq!(into.optional, Some("from".to_string()));

assert_eq!(into.normal_additional, "");
assert_eq!(into.optional_additional, None);
}
}
11 changes: 11 additions & 0 deletions testing/src/into_test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
use inter_struct::InterStruct;

mod into;
mod into_default;

#[derive(InterStruct)]
#[into("crate::into_test::IntoStruct")]
#[into_default("crate::into_test::IntoDefaultStruct")]
pub struct FromStruct {
pub normal: String,
pub optional: Option<String>,
Expand All @@ -29,3 +31,12 @@ pub struct IntoStruct {
pub normal: String,
pub optional: Option<String>,
}

/// A struct with a few additional fields that should be populated by their [Default] values.
#[derive(Default)]
pub struct IntoDefaultStruct {
pub normal: String,
pub optional: Option<String>,
pub normal_additional: String,
pub optional_additional: Option<String>,
}

0 comments on commit a20ef66

Please sign in to comment.