diff --git a/Cargo.lock b/Cargo.lock
index e050724..76b84cd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -351,6 +351,7 @@ name = "charming"
version = "0.4.0"
dependencies = [
"assert-json-diff",
+ "charming-derive",
"deno_core",
"handlebars",
"image",
@@ -364,6 +365,17 @@ dependencies = [
"web-sys",
]
+[[package]]
+name = "charming-derive"
+version = "0.1.0"
+dependencies = [
+ "charming",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn",
+]
+
[[package]]
name = "charming-gallery"
version = "0.1.0"
@@ -1589,9 +1601,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.63"
+version = "1.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
dependencies = [
"unicode-ident",
]
@@ -1607,9 +1619,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.29"
+version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -2115,9 +2127,9 @@ dependencies = [
[[package]]
name = "syn"
-version = "2.0.32"
+version = "2.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
+checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
dependencies = [
"proc-macro2",
"quote",
diff --git a/charming-derive/Cargo.toml b/charming-derive/Cargo.toml
new file mode 100644
index 0000000..351f7b1
--- /dev/null
+++ b/charming-derive/Cargo.toml
@@ -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"] }
diff --git a/charming-derive/src/lib.rs b/charming-derive/src/lib.rs
new file mode 100644
index 0000000..c0d7df5
--- /dev/null
+++ b/charming-derive/src/lib.rs
@@ -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
,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tooltip: Option,
+
+ // 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>(mut self, title: T) -> Self {
+ self.title.push(title.into());
+ self
+ }
+ pub fn 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,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ tooltip: Option,
+
+ // 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,
+}
+
+impl Line {
+ pub fn data>(mut self, data: Vec) -> 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::>(),
+ _ => 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 {
+ gather_all_types(node).iter().map(repr).collect()
+}
+
+fn repr(t: &T) -> String {
+ quote::quote!(#t).to_string()
+}
diff --git a/charming-derive/tests/derive_test.rs b/charming-derive/tests/derive_test.rs
new file mode 100644
index 0000000..30c754e
--- /dev/null
+++ b/charming-derive/tests/derive_test.rs
@@ -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,
+
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ title: Vec,
+
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ #[charming_skip_setter]
+ data: Vec,
+}
+
+impl LineComponent {
+ pub fn data>(mut self, data: Vec) -> 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]);
+}
diff --git a/charming/Cargo.toml b/charming/Cargo.toml
index 42423d3..e6d10f4 100644
--- a/charming/Cargo.toml
+++ b/charming/Cargo.toml
@@ -17,11 +17,12 @@ 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"
@@ -29,11 +30,7 @@ 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"]