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

editoast: use uom to type units #9928

Merged
merged 12 commits into from
Jan 16, 2025
Merged
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
15 changes: 15 additions & 0 deletions editoast/Cargo.lock

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

3 changes: 3 additions & 0 deletions editoast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ tracing-opentelemetry = { version = "0.28.0", default-features = false, features
"tracing-log",
] }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
uom = { version = "0.36.0", default-features = false, features = ["f64", "si"] }
url = { version = "2.5.4", features = ["serde"] }
urlencoding = "2.1.3"
utoipa = { version = "4.2.3", features = ["chrono", "uuid"] }
uuid = { version = "1.11.1", features = ["serde", "v4"] }

[dependencies]
anyhow = "1.0"
approx = "0.5.1"
async-trait = "0.1.85"
axum = { version = "0.8.1", default-features = false, features = [
"multipart",
Expand Down Expand Up @@ -194,6 +196,7 @@ tower-http = { version = "0.6.2", features = [
tracing.workspace = true
tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
uom.workspace = true
url.workspace = true
utoipa.workspace = true
uuid.workspace = true
Expand Down
1 change: 1 addition & 0 deletions editoast/editoast_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ thiserror.workspace = true
tracing.workspace = true
tracing-opentelemetry.workspace = true
tracing-subscriber.workspace = true
uom.workspace = true
url.workspace = true
utoipa.workspace = true

Expand Down
1 change: 1 addition & 0 deletions editoast/editoast_common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod hash_rounded_float;
pub mod rangemap_utils;
pub mod schemas;
pub mod tracing;
pub mod units;

pub use hash_rounded_float::hash_float;
pub use hash_rounded_float::hash_float_slice;
Expand Down
216 changes: 216 additions & 0 deletions editoast/editoast_common/src/units.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//! Module to allow the use of serde with uom quantities
//!
//! The serde feature of uom doesn’t allow to specify in which unit the value will be serialized.
//!
//! Two helpers are provided for convenience:
//! * `unit::new` (e.g. `meter::new(32)`) to build a new quantity from a f64 in the given unit
//! * `unit::from` (e.g. `millimeter::from(length)`) to have the quantity as f64 in the given unit
//!
//! ## Usage
//!
//! ```ignore
//! use editoast_model::units::*;
//! #[derive(Debug, Serialize, Derivative)]
//! struct Train {
//! // This means that serde with read and write the velocity in meters per second
//! #[serde(with="meter_per_second")]
//! max_speed: Velocity,
//! // When using optional values, we must add `default` and use ::option unit
//! // See https://stackoverflow.com/a/44303505
//! #[serde(default, with="meter::option")]
//! length: Option<Length>,
//! }
//!
//! impl Train {
//! fn from_meter_per_seconds(mps: f64) -> Self {
//! Self {
//! max_speed: meter_per_second::new(mps),
//! }
//! }
//!
//! fn print(&self) {
//! println!("The max speed is: {} km/h", kilometer_per_hour::from(self.max_speed));
//! }
//! }
//! ```

/// Re-export the Quantities that are used in OSRD
pub mod quantities {
pub use uom::si::f64::{Acceleration, Length, Mass, Ratio, Time, Velocity};
pub type SolidFriction = uom::si::f64::Force;
pub type SolidFrictionPerWeight = uom::si::f64::Acceleration;
pub type ViscosityFriction = uom::si::f64::MassRate;
pub type ViscosityFrictionPerWeight = uom::si::f64::Frequency;
pub type AerodynamicDrag = uom::si::f64::LinearMassDensity;
pub type AerodynamicDragPerWeight = uom::si::f64::LinearNumberDensity;
pub type Deceleration = uom::si::f64::Acceleration;
}

macro_rules! quantity_to_path {
(Length, $unit:ident) => {
uom::si::length::$unit
};
(Velocity, $unit:ident) => {
uom::si::velocity::$unit
};
(Acceleration, $unit:ident) => {
uom::si::acceleration::$unit
};
(Mass, $unit:ident) => {
uom::si::mass::$unit
};
(SolidFriction, $unit:ident) => {
uom::si::force::$unit
};
(ViscosityFriction, $unit:ident) => {
uom::si::mass_rate::$unit
};
(ViscosityFrictionPerWeight, $unit:ident) => {
uom::si::frequency::$unit
};
(AerodynamicDrag, $unit:ident) => {
uom::si::linear_mass_density::$unit
};
(AerodynamicDragPerWeight, $unit:ident) => {
uom::si::linear_number_density::$unit
};
(Time, $unit:ident) => {
uom::si::time::$unit
};
(Ratio, $unit:ident) => {
uom::si::ratio::$unit
};
}

macro_rules! define_unit {
($unit:ident, $quantity:ident) => {
pub mod $unit {
use super::*;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
type Unit = quantity_to_path!($quantity, $unit);
pub type ReprType = f64;

pub fn serialize<S>(value: &$quantity, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
value.get::<Unit>().serialize(serializer)
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<$quantity, D::Error>
where
D: Deserializer<'de>,
{
let value = ReprType::deserialize(deserializer)?;
Ok($quantity::new::<Unit>(value))
}

pub fn new(value: ReprType) -> $quantity {
$quantity::new::<Unit>(value)
}

pub fn from(qty: $quantity) -> ReprType {
qty.get::<Unit>()
}

pub fn hash<H: std::hash::Hasher>(value: &$quantity, state: &mut H) {
crate::hash_float::<5, H>(&from(*value), state);
}

pub mod option {
use super::*;
pub type ReprType = Option<super::ReprType>;

pub fn serialize<S>(
value: &Option<$quantity>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
value.map(|value| value.get::<Unit>()).serialize(serializer)
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<$quantity>, D::Error>
where
D: Deserializer<'de>,
{
let value = Option::deserialize(deserializer)?;
Ok(value.map(|value| $quantity::new::<Unit>(value)))
}

pub fn new(value: ReprType) -> Option<$quantity> {
value.map(|v| $quantity::new::<Unit>(v))
}

pub fn from(qty: Option<$quantity>) -> ReprType {
qty.map(|q| q.get::<Unit>())
}

pub fn hash<H: std::hash::Hasher>(value: &Option<$quantity>, state: &mut H) {
super::hash(&value.unwrap_or_default(), state);
}
}

pub mod u64 {
use super::*;

pub fn serialize<S>(value: &$quantity, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
(value.get::<Unit>() as u64).serialize(serializer)
}

pub fn deserialize<'de, D>(deserializer: D) -> Result<$quantity, D::Error>
where
D: Deserializer<'de>,
{
super::deserialize(deserializer)
}

pub mod option {
use super::*;
pub type ReprType = Option<super::ReprType>;

pub fn serialize<S>(
value: &Option<$quantity>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
value
.map(|value| value.get::<Unit>() as u64)
.serialize(serializer)
}

pub fn deserialize<'de, D>(
deserializer: D,
) -> Result<Option<$quantity>, D::Error>
where
D: Deserializer<'de>,
{
super::super::option::deserialize(deserializer)
}
}
}
}
};
}

// Any new value here must also be added in editoast_derive/src/annotate_units.rs
use quantities::*;
define_unit!(meter, Length);
define_unit!(millimeter, Length);
define_unit!(meter_per_second, Velocity);
define_unit!(meter_per_second_squared, Acceleration);
define_unit!(kilogram, Mass);
define_unit!(newton, SolidFriction);
define_unit!(kilogram_per_second, ViscosityFriction);
define_unit!(hertz, ViscosityFrictionPerWeight);
define_unit!(kilogram_per_meter, AerodynamicDrag);
define_unit!(per_meter, AerodynamicDragPerWeight);
define_unit!(second, Time);
define_unit!(millisecond, Time);
define_unit!(basis_point, Ratio);
76 changes: 76 additions & 0 deletions editoast/editoast_derive/src/annotate_units.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use darling::Result;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse_quote, DeriveInput, LitStr};

const OPTIONAL_SUFFIX: &str = "::option";
struct UnitType {
unit: String,
optional: bool,
}

impl UnitType {
fn new(path: LitStr) -> Self {
let path = path.value();
let optional = path.ends_with(OPTIONAL_SUFFIX);
let unit = path
.strip_suffix(OPTIONAL_SUFFIX)
.unwrap_or(&path)
.rsplit("::")
.next()
.expect("String::split to return at least an empty string")
.to_owned();
Self { unit, optional }
}
}

fn get_abbreviation(value: &str) -> Option<&'static str> {
// Any new value here must also be added in editoast_common/src/units.rs
match value {
"second" => Some("Duration in s"),
"millisecond" => Some("Duration in ms"),
"meter" => Some("Length in m"),
"millimeter" => Some("Length in mm"),
"meter_per_second" => Some("Velocity in m·s⁻¹"),
"meter_per_second_squared" => Some("Acceleration in m·s⁻²"),
"kilogram" => Some("Mass in kg"),
"newton" => Some("Solid Friction in N"),
"hertz" => Some("Viscosity friction per weight in s⁻¹"),
"kilogram_per_meter" => Some("Aerodynamic drag in kg·m⁻¹"),
"kilogram_per_second" => Some("Viscosity friction in kg·s⁻¹"),
"per_meter" => Some("Aerodynamic drag per kg in m⁻¹"),
"basis_point" => Some("Ratio 1:1"),
_ => None,
}
}

pub fn annotate_units(input: &mut DeriveInput) -> Result<TokenStream> {
// We look for fields that have #[sered(with="meter")] attributes
// and we push two new attributes to it to improve the OpenAPI
if let syn::Data::Struct(s) = &mut input.data {
for f in s.fields.iter_mut() {
for attr in f.attrs.clone() {
if attr.path().is_ident("serde") {
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("with") {
let value = meta.value()?;
let s: LitStr = value.parse()?;
let unit = UnitType::new(s);
if let Some(abbreviation) = get_abbreviation(&unit.unit) {
if unit.optional {
f.attrs
.push(parse_quote! {#[schema(value_type = Option<f64>)]});
} else {
f.attrs.push(parse_quote! {#[schema(value_type = f64)]});
}
f.attrs.push(parse_quote! {#[doc = #abbreviation]});
}
}
Ok(())
});
}
}
}
}
Ok(quote! {#input})
}
Loading
Loading