1use serde::{Deserialize, Serialize};
9
10use crate::error::DesignError;
11
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
14#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
15pub enum Density {
16 Bulk {
18 kg_m3: f64,
20 },
21 Surface {
23 kg_m2: f64,
25 },
26 Line {
28 kg_m: f64,
30 },
31}
32
33impl Density {
34 pub fn kind_name(&self) -> &'static str {
36 match self {
37 Self::Bulk { .. } => "bulk",
38 Self::Surface { .. } => "surface",
39 Self::Line { .. } => "line",
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
46#[serde(deny_unknown_fields)]
47pub struct Material {
48 pub name: String,
50 pub density: Density,
52}
53
54impl Material {
55 pub fn bulk(name: impl Into<String>, kg_m3: f64) -> Self {
57 Self {
58 name: name.into(),
59 density: Density::Bulk { kg_m3 },
60 }
61 }
62
63 pub fn surface(name: impl Into<String>, kg_m2: f64) -> Self {
65 Self {
66 name: name.into(),
67 density: Density::Surface { kg_m2 },
68 }
69 }
70
71 pub fn line(name: impl Into<String>, kg_m: f64) -> Self {
73 Self {
74 name: name.into(),
75 density: Density::Line { kg_m },
76 }
77 }
78
79 fn value(&self, part: &'static str, expected: &'static str) -> Result<f64, DesignError> {
81 let value = match (self.density, expected) {
82 (Density::Bulk { kg_m3 }, "bulk") => kg_m3,
83 (Density::Surface { kg_m2 }, "surface") => kg_m2,
84 (Density::Line { kg_m }, "line") => kg_m,
85 _ => {
86 return Err(DesignError::MaterialKind {
87 part,
88 material: self.name.clone(),
89 expected,
90 actual: self.density.kind_name(),
91 });
92 }
93 };
94 if value.is_finite() && value >= 0.0 {
95 Ok(value)
96 } else {
97 Err(DesignError::Domain {
98 what: "material density",
99 value,
100 })
101 }
102 }
103
104 pub fn bulk_kg_m3(&self, part: &'static str) -> Result<f64, DesignError> {
111 self.value(part, "bulk")
112 }
113
114 pub fn surface_kg_m2(&self, part: &'static str) -> Result<f64, DesignError> {
120 self.value(part, "surface")
121 }
122
123 pub fn line_kg_m(&self, part: &'static str) -> Result<f64, DesignError> {
129 self.value(part, "line")
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn kinds_are_checked_and_serialized_with_their_units() {
139 let ply = Material::bulk("Birch plywood", 630.0);
140 assert_eq!(ply.bulk_kg_m3("fin set").unwrap(), 630.0);
141 assert_eq!(
142 ply.surface_kg_m2("parachute canopy"),
143 Err(DesignError::MaterialKind {
144 part: "parachute canopy",
145 material: "Birch plywood".to_owned(),
146 expected: "surface",
147 actual: "bulk",
148 })
149 );
150 let json = serde_json::to_string(&Material::line("Kevlar", 0.0097)).unwrap();
151 assert_eq!(
152 json,
153 r#"{"name":"Kevlar","density":{"kind":"line","kg_m":0.0097}}"#
154 );
155 assert!(Material::bulk("bad", -1.0).bulk_kg_m3("tube").is_err());
156 assert!(
157 serde_json::from_str::<Material>(r#"{"name":"x","density":{"kind":"bulk","kg_m2":1}}"#)
158 .is_err()
159 );
160 }
161}