Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a derive macro for setting fields #105

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
24 changes: 18 additions & 6 deletions Cargo.lock

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

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

[lib]
proc-macro = true

[dependencies]
proc-macro2 = "1.0.87"
quote = "1.0.37"
syn = { version = "2.0.79", features = ["extra-traits", "visit", "derive"] }

[dev-dependencies]
charming = { path = "../charming" }
serde = { version = "1.0", features = ["derive"] }
236 changes: 236 additions & 0 deletions charming-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
/*!
CharmingSetter is a derive macro for the Charming chart rendering library. It is used to remove a lot of boilerplate code
in the library by implementing common methods to set the fields of structs.

Example without the macro
```rust
use serde::Serialize;
use charming::component::Title;
use charming::element::Tooltip;

#[derive(Serialize, Debug, Clone)]
struct Chart {
#[serde(skip_serializing_if = "Vec::is_empty")]
title: Vec<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
tooltip: Option<Tooltip>,

// many fields cut for brevity
}

// For setting these fields with the setter pattern we would need to implement the following
// methods manually when we are not using the derive macro
impl Chart {
pub fn new() -> Self {
Self {
title: Vec::new(),
tooltip: None,
}
}
pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
self.title.push(title.into());
self
}
pub fn tooltip<T: Into<Tooltip>>(mut self, tooltip: T) -> Self {
self.tooltip = Some(tooltip.into());
self
}

// many methods cut for brevity
}

// Example chart creation
let chart = Chart::new().title(Title::new().text("Title")).tooltip(Tooltip::new());
```

Example with the macro
```rust
use serde::Serialize;
use charming::component::Title;
use charming::element::Tooltip;
use charming_derive::CharmingSetter;

#[derive(Serialize, Debug, Clone, CharmingSetter)]
struct Chart {
#[serde(skip_serializing_if = "Vec::is_empty")]
title: Vec<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
tooltip: Option<Tooltip>,

// many fields cut for brevity
}
// The setter methods from the example above now get implemented automatically by CharmingSetter

// Example chart creation
let chart = Chart::new().title(Title::new().text("Title")).tooltip(Tooltip::new());
```

Field attributes charming_type and charming_skip_setter
```rust
use serde::Serialize;
use charming::datatype::DataPoint;
use charming_derive::CharmingSetter;

#[derive(Serialize, Debug, Clone, CharmingSetter)]
#[serde(rename_all = "camelCase")]
pub struct Line {
// charming_type gets used here to provide the value "line" as the default value for
// the field 'type_' when calling Line::new() and also removes the method to set the field
#[serde(rename = "type")]
#[charming_type = "line"]
type_: String,

// cut for brevity

// charming_skip_setter gets used here to remove the default implementation of the setter
// method for this field, a manual method to set this field needs to be provided instead
#[serde(skip_serializing_if = "Vec::is_empty")]
#[charming_skip_setter]
data: Vec<DataPoint>,
}

impl Line {
pub fn data<D: Into<DataPoint>>(mut self, data: Vec<D>) -> Self {
self.data = data.into_iter().map(|d| d.into()).collect();
self
}
}

```
*/

use syn::visit::Visit;

#[proc_macro_derive(CharmingSetter, attributes(charming_skip_setter, charming_type))]
pub fn charming_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = syn::parse_macro_input!(input as syn::DeriveInput);

let ident = input.ident;
let mut field_list = vec![];
let mut method_list = vec![];

let fields = match &input.data {
syn::Data::Struct(ref data) => match data.fields {
syn::Fields::Named(ref fields) => fields.named.iter().collect::<Vec<_>>(),
_ => unimplemented!(
"Can't implement CharmingSetter on a unit struct or a struct with unnamed fields"
),
},
syn::Data::Enum(_data_enum) => unimplemented!("Can't implement CharmingSetter on an Enum"),
syn::Data::Union(_data_union) => {
unimplemented!("Can't implement CharmingSetter on a Union")
}
};

'outer: for field in fields {
let field_ident = field.ident.as_ref().unwrap();
let field_types = gather_all_type_reprs(&field.ty);
let type_ident =
syn::Ident::new(field_types.last().unwrap(), proc_macro2::Span::call_site());
let shorthand_char = field_types.last().unwrap().chars().next().unwrap();
let shorthand = syn::Ident::new(
&shorthand_char.to_string().to_uppercase(),
proc_macro2::Span::call_site(),
);

for attribute in &field.attrs {
match &attribute.meta {
syn::Meta::Path(path) => {
if let Some(segment) = path.segments.last() {
if let "charming_skip_setter" = segment.ident.to_string().as_str() {
if field_types[0].starts_with("Option") {
field_list.push(quote::quote! { #field_ident: None, });
} else if field_types[0].starts_with("Vec") {
field_list
.push(quote::quote! { #field_ident: std::vec::Vec::new(), });
}
continue 'outer;
}
}
}
syn::Meta::List(_meta_list) => {}
syn::Meta::NameValue(meta_name_value) => {
if let Some(segment) = meta_name_value.path.segments.last() {
if segment.ident.to_string().starts_with("charming_type") {
match &meta_name_value.value {
syn::Expr::Lit(expr_lit) => {
if let syn::Lit::Str(string_lit) = &expr_lit.lit {
let value = string_lit.value();
field_list.push(
quote::quote! { #field_ident: #value.to_string(), },
);
} else {
unimplemented!("Expected string literal for charming_type")
}
continue 'outer;
}
_ => {
unimplemented!("Can't implement non Literals for charming_type")
}
}
}
}
}
}
}
if field_types[0].starts_with("Option") {
field_list.push(quote::quote! { #field_ident: None, });
method_list.push(quote::quote! {
pub fn #field_ident<#shorthand: Into<#type_ident>>(mut self, #field_ident: #shorthand) -> Self {
self.#field_ident = Some(#field_ident.into());
self
}
});
} else if field_types[0].starts_with("Vec") {
field_list.push(quote::quote! { #field_ident: std::vec::Vec::new(), });
method_list.push(quote::quote! {
pub fn #field_ident<#shorthand: Into<#type_ident>>(mut self, #field_ident: #shorthand) -> Self {
self.#field_ident.push(#field_ident.into());
self
}
});
}
}

quote::quote!(
impl #ident {
pub fn new() -> Self {
Self {
#(#field_list)*
}
}
#(#method_list)*
}
)
.into()
}

// Following code is from enso_macro_utils but supports syn2.0

#[derive(Default)]
struct TypeGatherer<'ast> {
/// Observed types accumulator.
pub types: Vec<&'ast syn::TypePath>,
}

impl<'ast> syn::visit::Visit<'ast> for TypeGatherer<'ast> {
fn visit_type_path(&mut self, node: &'ast syn::TypePath) {
self.types.push(node);
syn::visit::visit_type_path(self, node);
}
}

fn gather_all_types(node: &syn::Type) -> Vec<&syn::TypePath> {
let mut type_gatherer = TypeGatherer::default();
type_gatherer.visit_type(node);
type_gatherer.types
}

fn gather_all_type_reprs(node: &syn::Type) -> Vec<String> {
gather_all_types(node).iter().map(repr).collect()
}

fn repr<T: quote::ToTokens>(t: &T) -> String {
quote::quote!(#t).to_string()
}
36 changes: 36 additions & 0 deletions charming-derive/tests/derive_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use charming_derive::CharmingSetter;
use serde::Serialize;

#[derive(Serialize, Debug, Clone, CharmingSetter)]
#[serde(rename_all = "camelCase")]
struct LineComponent {
#[serde(rename = "type")]
#[charming_type = "line"]
type_: String,

#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,

#[serde(skip_serializing_if = "Vec::is_empty")]
title: Vec<String>,

#[serde(skip_serializing_if = "Vec::is_empty")]
#[charming_skip_setter]
data: Vec<i32>,
}

impl LineComponent {
pub fn data<I: Into<i32>>(mut self, data: Vec<I>) -> Self {
self.data = data.into_iter().map(|d| d.into()).collect();
self
}
}

#[test]
fn expand_test() {
LineComponent::new()
.name("LineComponent")
.title("Title 1")
.title("Title 2")
.data(vec![1, 2, 3]);
}
9 changes: 3 additions & 6 deletions charming/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,20 @@ handlebars = { version = "4.3", optional = true }
image = { version = "0.24", optional = true }
resvg = { version = "0.36", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = {version = "0.6", optional = true }
serde-wasm-bindgen = { version = "0.6", optional = true }
serde_json = "1.0"
serde_v8 = { version = "0.220", optional = true }
serde_with = "3.11.0"
wasm-bindgen = { version = "0.2", optional = true }
charming-derive = { path = "../charming-derive" }

[dev-dependencies]
assert-json-diff = "2.0.2"

[dependencies.web-sys]
version = "0.3.64"
optional = true
features = [
"Window",
"Document",
"Element",
]
features = ["Window", "Document", "Element"]

[features]
default = ["html"]
Expand Down
Loading