Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build_and_unit_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Run unit test suites
shell: bash
run: |
cargo test --all-targets --verbose && exit 0
cargo test --all --all-targets --verbose && exit 0
printf '\e[1;33m\t==========================================\n\e[0m'
printf '\e[1;33m\tUNIT TEST SUITE FAILED\n\e[0m'
printf '\e[1;33m\tPLEASE, SOLVE THEM LOCALLY W/ `cargo test`\e[0m\n'
Expand Down
23 changes: 19 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ description = "patch-hub is a TUI that streamlines the interaction of Linux deve
color-eyre = "0.6.3"
mockall = "0.13.0"
derive-getters = { version = "0.5.0", features = ["auto_copy_getters"] }
lazy_static = "1.5.0"
proc_macros = { path = "./proc_macros" }
ratatui = "0.28.1"
regex = "1.10.5"
serde = { version = "1.0.203", features = ["derive"] }
Expand Down Expand Up @@ -47,3 +49,8 @@ unconditional_recursion = "deny"
[lints.clippy]
too-many-arguments = "allow"
map_unwrap_or = "deny"

[workspace]
members = [
"proc_macros",
]
17 changes: 17 additions & 0 deletions proc_macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "proc_macros"
version = "0.1.0"
edition = "2021"

[dependencies]
derive-getters = { version = "0.5.0", features = ["auto_copy_getters"] }
lazy_static = "1.5.0"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.120"
syn = { version = "2.0.100", features = ["full"] }
quote = "1.0.4"
proc-macro2 = "1.0.94"

[lib]
proc-macro = true
doctest = false # otherwise tests will fail
117 changes: 117 additions & 0 deletions proc_macros/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, Data, DeriveInput};

/// This procedural macro create default deserealization functions for each
/// structure attribute based on std::Default impl.
///
/// It applies default values only for fields missing from the deserialized input.
///
/// # Example
///
/// ```rust
/// use serde::Deserialize;
/// use your_proc_macro_crate::serde_individual_default;
///
/// #[derive(Deserialize, Getters)]
/// #[serde_individual_default]
/// struct Example {
/// #[getter(skip)]
/// test_1: i64,
/// test_2: i64,
/// test_3: String,
/// }
/// impl Default for Example {
/// fn default() -> Self {
/// Example {
/// test_1: 3942,
/// test_2: 42390,
/// test_3: "a".to_string(),
/// }
/// }
/// }
///
/// let json_data_1 = serde_json::json!({
/// "test_1": 500,
/// "test_2": 100
/// });
/// let example_struct_1: Example = serde_json::from_value(json_data_1).unwrap();
/// assert_eq!(example_struct_1.test_1, 500);
/// assert_eq!(example_struct_1.test_2, 100);
/// assert_eq!(example_struct_1.test_3, "a".to_string());
/// ```
#[proc_macro_attribute]
pub fn serde_individual_default(_attr: TokenStream, input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let struct_generics = &input.generics;
let struct_fields = match &input.data {
Data::Struct(s) => &s.fields,
_ => panic!("SerdeIndividualDefault can only be used with structs"),
};
let struct_attrs = &input.attrs;
let struct_visibility = &input.vis;

let struct_name_str = struct_name.to_string();
let (struct_impl_generics, struct_ty_generics, struct_where_clause) =
struct_generics.split_for_impl();

// store only one struct::default object in memory
let default_config_struct_name =
format_ident!("DEFAULT_{}_STATIC", struct_name_str.to_ascii_uppercase());
let struct_default_lazy_construction_definition = {
quote! {
lazy_static::lazy_static! {
static ref #default_config_struct_name: #struct_name = #struct_name::default();
}
}
};

// build struct attributes with #[serde(default = "")] and build the default function itself
let (all_field_attrs, default_deserialize_function_definitions) = struct_fields.iter().fold(
(vec![], vec![]),
|(mut all_field_attrs, mut default_deserialize_function_definitions), field| {
let field_name = &field.ident;
let field_type = &field.ty;
let field_vis = &field.vis;
let field_attrs = &field.attrs;
let field_name_str = field_name.as_ref().unwrap().to_string();

// default function name will be named default_{struct_name}_{field_name}
let default_deserialize_function_name =
format_ident!("default_{}_{}", struct_name_str, field_name_str);

let default_deserialize_function_name_str =
default_deserialize_function_name.to_string();

all_field_attrs.push(quote! {
#(#field_attrs)*
#[serde(default = #default_deserialize_function_name_str)]
#field_vis #field_name: #field_type,
});
default_deserialize_function_definitions.push(quote! {
fn #default_deserialize_function_name() -> #field_type {
#default_config_struct_name.#field_name.clone()
}
});

(all_field_attrs, default_deserialize_function_definitions)
},
);

// build final struct.
//We have to explicitly derive Deserialize here so the serde attribute works
let expanded_token_stream = quote! {
#[derive(serde::Deserialize)]
#(#struct_attrs)*
#struct_visibility struct #struct_name #struct_impl_generics {
#(#all_field_attrs)*
} #struct_ty_generics #struct_where_clause

#struct_default_lazy_construction_definition

#(#default_deserialize_function_definitions)*
};
TokenStream::from(expanded_token_stream)
}
132 changes: 132 additions & 0 deletions proc_macros/tests/serde_individual_default.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use proc_macros::serde_individual_default;

use derive_getters::Getters;
use serde::Serialize;

#[derive(Serialize, Getters)]
#[serde_individual_default]
struct Example {
#[getter(skip)]
test_1: i64,
test_2: i64,
test_3: String,
}

impl Default for Example {
fn default() -> Self {
Example {
test_1: 3942,
test_2: 42390,
test_3: "a".to_string(),
}
}
}

#[serde_individual_default]
struct ExampleWithoutSerialize {
test_1: i64,
test_2: i64,
}

impl Default for ExampleWithoutSerialize {
fn default() -> Self {
ExampleWithoutSerialize {
test_1: 765,
test_2: 126,
}
}
}

#[serde_individual_default]
pub struct ExamplePublic {
test_1: i64,
test_2: i64,
}

impl Default for ExamplePublic {
fn default() -> Self {
ExamplePublic {
test_1: 598,
test_2: 403,
}
}
}

#[test]
fn should_have_default_serialization() {
// Case 1: test_3 missing

// Example JSON string that doesn't contain `test_3` but has customized `test_1` and `test_2`
let json_data_1 = serde_json::json!({
"test_1": 500,
"test_2": 100
});

let example_struct_1: Example = serde_json::from_value(json_data_1).unwrap();

// Assert that`test_1` and `test_2` are set to the custom value
assert_eq!(example_struct_1.test_1, 500);
assert_eq!(example_struct_1.test_2, 100);

// Assert that `test_3` is set to the default value (a)
assert_eq!(example_struct_1.test_3, "a".to_string());

// Case 2: test_2 missing

// Example JSON string that doesn't contain `test_2` but has customized `test_1` and `test_3`
let json_data_2 = serde_json::json!({
"test_1": 999,
"test_3": "test".to_string()
});

let example_struct_2: Example = serde_json::from_value(json_data_2).unwrap();

// Assert that`test_1` and `test_3` are set to the custom value
assert_eq!(example_struct_2.test_1, 999);
assert_eq!(example_struct_2.test_3, "test".to_string());

// Assert that `test_2` is set to the default value (42390)
assert_eq!(example_struct_2.test_2, 42390);
}

#[test]
fn should_preserve_other_attributes() {
// Example JSON string that doesn't contain `test_3` but has customized `test_1` and `test_2`
let json_data = serde_json::json!({
"test_1": 500,
"test_2": 100,
"test_3": "b".to_string()
});

let example_struct: Example = serde_json::from_value(json_data).unwrap();

// Assert that`test_2` and `test_3` have getters
assert_eq!(example_struct.test_1, 500);
assert_eq!(example_struct.test_2(), 100);
assert_eq!(example_struct.test_3(), &"b".to_string());
}

#[test]
fn test_struct_without_serialize() {
let json_data = serde_json::json!({
"test_2": 123,
});

let example_without_serialize: ExampleWithoutSerialize =
serde_json::from_value(json_data).unwrap();

assert_eq!(example_without_serialize.test_1, 765);
assert_eq!(example_without_serialize.test_2, 123);
}

#[test]
fn test_public_struct() {
let json_data = serde_json::json!({
"test_1": 345,
});

let example_public: ExamplePublic = serde_json::from_value(json_data).unwrap();

assert_eq!(example_public.test_1, 345);
assert_eq!(example_public.test_2, 403);
}
Loading