iliekturtles / uom

Units of measurement -- type-safe zero-cost dimensional analysis
Apache License 2.0
989 stars 89 forks source link

Deserialize references to quantities #448

Closed kylecarow closed 7 months ago

kylecarow commented 7 months ago

Howdy! Thanks for the useful crate! Looking forward to using it. I'm still somewhat a Rust novice, so bear with me.

Summary

I'm trying to understand why deriving Deserialize on a reference to a quantity doesn't work (or, perhaps, why quantities impl Deserialize but references to quantities do not). Is this something that could be changed? I don't really want to impl Deserialize myself, as I expect to add more fields and it would be a maintenance headache. Or, is there a better way to structure my code?

Context

I have a struct Cycle that contains Vecs of quantities, and another struct CycleElement that ought to hold one index of elements from a Cycle. CycleElement can also serve as a way of constructing a Cycle by pushing them sequentially - see the test build_cycle below.

I am unable to derive Deserialize on CycleElement, as it contains references to quantities, rather than owned quantities. I need to impl Deserialize on CycleElement in order for csv serde to work, see the example here which requires the record struct to impl Deserialize.

Error

error[E0277]: the trait bound `&'a Quantity<(dyn Dimension<I = Z0, J = Z0, Kind = (dyn Kind + 'static), L = PInt<UInt<UTerm, B1>>, M = Z0, N = Z0, T = Z0, Th = Z0> + 'static), (dyn uom::si::Units<f64, amount_of_substance = uom::si::amount_of_substance::mole, electric_current = uom::si::electric_current::ampere, length = uom::si::length::meter, luminous_intensity = uom::si::luminous_intensity::candela, mass = uom::si::mass::kilogram, thermodynamic_temperature = uom::si::thermodynamic_temperature::kelvin, time = uom::si::time::second> + 'static), f64>: Deserialize<'_>` is not satisfied
    --> src/main.rs:23:8
     |
23   |     a: &'a Length, // error here
     |        ^^^^^^^^^^ the trait `Deserialize<'_>` is not implemented for `&'a Quantity<(dyn Dimension<I = Z0, J = Z0, Kind = (dyn Kind + 'static), L = PInt<UInt<UTerm, B1>>, M = Z0, N = Z0, T = Z0, Th = Z0> + 'static), (dyn uom::si::Units<f64, amount_of_substance = uom::si::amount_of_substance::mole, electric_current = uom::si::electric_current::ampere, length = uom::si::length::meter, luminous_intensity = uom::si::luminous_intensity::candela, mass = uom::si::mass::kilogram, thermodynamic_temperature = uom::si::thermodynamic_temperature::kelvin, time = uom::si::time::second> + 'static), f64>`
     |
note: required by a bound in `next_element`

Code

Below is a minimal reproducible example that refuses to compile. Remove the Deserialize derive on struct CycleElement and it works fine, but would become incompatible with csv serde. The tests are just there to make it easier to see how this code works, they don't really test anything.

main.rs:

use serde::{Deserialize, Serialize};

use uom::si::f64::Force;
use uom::si::f64::Length;

pub fn main() {}

#[derive(Debug, Deserialize, Serialize)]
struct Cycle {
    a: Vec<Length>,
    b: Vec<Force>,
}

impl Cycle {
    pub fn push(&mut self, cyc_elem: CycleElement) {
        self.a.push(*cyc_elem.a);
        self.b.push(*cyc_elem.b);
    }
}

#[derive(Serialize, Deserialize)] // including Deserialize here breaks it
struct CycleElement<'a> {
    a: &'a Length, // error here
    b: &'a Force,  // error here
}

#[cfg(test)]
mod tests {
    use super::*;
    use uom::si::force::newton;
    use uom::si::length::meter;

    #[test]
    fn build_cycle() {
        let mut cyc = Cycle {
            a: vec![],
            b: vec![],
        };

        cyc.push(CycleElement {
            a: &Length::new::<meter>(7.0),
            b: &Force::new::<newton>(8.0),
        });
        cyc.push(CycleElement {
            a: &Length::new::<meter>(42.0),
            b: &Force::new::<newton>(1.0),
        });
        cyc.push(CycleElement {
            a: &Length::new::<meter>(0.0),
            b: &Force::new::<newton>(0.0),
        });

        dbg!(cyc);
    }

    #[test]
    fn test_serialize() {
        let cycle = Cycle {
            a: vec![Length::new::<meter>(1.0), Length::new::<meter>(2.0)],
            b: vec![Force::new::<newton>(4.0), Force::new::<newton>(5.0)],
        };

        let output = serde_json::to_string(&cycle).unwrap();

        dbg!(output);
    }

    #[test]
    fn test_deserialize() {
        let input = "{\"a\":[1.0,2.0,3.0,7.0],\"b\":[4.0,5.0,6.0,8.0]}";

        let cyc: Cycle = serde_json::from_str(input).unwrap();
        dbg!(cyc);
    }
}

Cargo.toml:

[package]
name = "test-cyc"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.108"
uom = { version = "0.35.0", features = ["use_serde"] }
kylecarow commented 7 months ago

I'm wondering if I'll run into trouble anyway, considering the docs for csv::Reader::deserialize that I linked above state that it requires the trait DeserializeOwned... not sure.

kylecarow commented 7 months ago

Realized this is not possible given these SO posts and is not specific to this crate.

https://stackoverflow.com/questions/52733047/how-to-borrow-a-field-for-serialization-but-create-it-during-deserialization

https://stackoverflow.com/questions/60801133/how-do-i-use-serde-to-deserialize-structs-with-references-from-a-reader