varunsrin / rusty_money

Money library for Rust
MIT License
86 stars 32 forks source link

Support for serde Serialize / Deserialize #33

Open bodymindarts opened 4 years ago

bodymindarts commented 4 years ago

I am using this library to represent Money in the backend of a web-app. Sometimes I need to serialise and need to translate into another struct for that. Could we add a feature to (de-)serialise via Serde?

Alternatively perhaps we could make Iso Copy/Clone to make it easier to pass around and embed in other structs.

varunsrin commented 3 years ago

👍 will add this to the roadmap. we already have a temporary implementation here: https://github.com/varunsrin/rusty_money/pull/27

Iron-E commented 3 years ago

I recently had to implement Serialize / Deserialize for a struct which contained Money, a struct that doesn't have these traits implemented. What I did was create a struct with mock data that did implement those traits, but had enough information for me to regain my initial struct.

Here's the source.

Edit: I ended up writing my own crate for this purpose

RonaldLaBelle-Deckers commented 10 months ago

I came up with a partial solution. Thought I'd share for discussion or perhaps help someone else.

// My use case is ecom, so this toy struct contains normal fields and rusty-money Currency field
// We use an Rc<> to wrap the Currency and abstract away ownership with the smart pointer
#[derive(Debug, Serialize, Deserialize)]
struct Order {
    money: f64, // todo
    #[serde(serialize_with="ser_currency", deserialize_with="de_currency")]
    currency: Rc<Currency>,
}

// Serialize Fn
fn ser_currency<S>(currency_field: &Rc<Currency>, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
    //
    let currency_string = currency_field.as_ref().to_string();
    serializer.serialize_str(&currency_string[..])
}

// Deserialize Fn
fn de_currency<'de, D>(deserializer: D)-> Result<Rc<Currency>, D::Error>
where D: Deserializer<'de> {
    struct V;
    impl <'de> Visitor<'de> for V {
        type Value = Rc<Currency>;

        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
            write!(formatter, "a string or array of bytes")
        }
        fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
        where
            E: serde::de::Error,
        {
            let currency: Rc<Currency> = Rc::new(*iso::find(s)
                .expect(format!("{s} is not a supported currency.").as_str()));
            Ok(currency)
        }
        fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
        where
            A: SeqAccess<'de>,
        {
            let mut res = vec![];
            while let Some(b) = seq.next_element::<u8>()? {
                res.push(b);
            }
            let mut os_string = OsString::from_vec(res);
            os_string.make_ascii_uppercase();
            let s = os_string.to_string_lossy();
            let currency: Rc<Currency> = Rc::new(*iso::find(s.as_ref())
                .expect(format!("{s} is not a supported currency.").as_str()));
            Ok(currency)
        }
    }
    deserializer.deserialize_any(V)
}

This produces behavior close to what we expect.

fn main() {
    let json = json!({
        "money": 42.42,
        "currency": "USD"
    });
    let bad_json = json!({
        "money": 42.42,
        "currency": "AAA"
    });
    let order = Order { money: 42.42, currency: Rc::new(*iso::USD)};
    eprintln!("{:#}", serde_json::to_string_pretty(&order).unwrap());

    let order_rehyd: Order = serde_json::from_value(json).unwrap();
    eprintln!("{:#}", serde_json::to_string_pretty(&order_rehyd).unwrap());

    let bad_order: Order = serde_json::from_value(bad_json).unwrap(); // Properly Panics Here
    // eprintln!("{:#}", serde_json::to_string_pretty(&bad_order).unwrap());
}

I also had success using an enum to represent currencies and impl Into<rusty_money::iso::Currency>

As you can see the money value itself is not yet solved. I might keep it fixed decimal, and only converting to Money when I need to do a calculation. Additional point I mentioned on a different issue, iso is missing DEM and it is proving difficult to deserialize around that.

As I said, just my findings. Hopefully, it helps others. Thanks for the crate!