yuankunzhang / charming

A visualization library for Rust
Apache License 2.0
1.85k stars 74 forks source link

Add a derive macro for setting fields #105

Open LukaOber opened 1 month ago

LukaOber commented 1 month ago

I documented the macro a bit in the charming-derive/src/lib.rs file. A well formatted version can be seen when using cargo doc. You can check what the macro expands to when using cargo expand --test derive_test in the charming-derive folder.

Current implementation

#[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());

Implementation with the macro

#[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());

I also added two field attributes charming_type and charming_skip_setter, their uses can be seen below

#[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
    }
}

This is just a draft. I will implement more tests first and then open another PR where we derive the new macro on all applicable structs. Just want to get some feedback on potential issues and thoughts first.

Furthermore, I expect little to no breaking changes for the user when switching to this implementation.