1use serde::{Serialize, de::DeserializeOwned};
15
16use crate::error::{DbError, DbResult};
17
18pub fn enum_to_text<T: Serialize>(value: &T) -> String {
26 enum_to_text_with(value, |value| serde_json::to_value(value))
27}
28
29fn enum_to_text_with<T, E>(
30 value: &T,
31 serialize: impl FnOnce(&T) -> Result<serde_json::Value, E>,
32) -> String
33where
34 T: Serialize,
35 E: std::fmt::Debug,
36{
37 match serialize(value).expect("enum serialization is infallible") {
38 serde_json::Value::String(s) => s,
39 other => panic!(
40 "enum_to_text called on a type that doesn't serialize to a bare string, got: {other}"
41 ),
42 }
43}
44
45pub fn text_to_enum<T: DeserializeOwned>(column: &'static str, value: &str) -> DbResult<T> {
52 serde_json::from_value(enum_text_value(value)).map_err(|_| invalid_enum_value(column, value))
53}
54
55fn enum_text_value(value: &str) -> serde_json::Value {
56 serde_json::Value::String(value.to_string())
57}
58
59fn invalid_enum_value(column: &'static str, value: &str) -> DbError {
60 DbError::InvalidEnumValue {
61 column,
62 value: value.to_string(),
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::models::{
70 MvActuationKind, MvActuationStatus, RestoreStatus, RollbackState, SampleQuality,
71 TemplateOrigin, TuneDriver, TuneOutcome, WriteKind,
72 };
73 use bhtune_core::{
74 ControllerDirection, ControllerType, DerivativeType, IntegralType, ProcessType,
75 ProportionalType, ResponseLevel, TimeUnit,
76 };
77
78 #[test]
84 fn process_type_round_trips_and_matches_check_constraint() {
85 let cases = [
86 (ProcessType::Flow, "flow"),
87 (ProcessType::PressureLine, "pressure_line"),
88 (ProcessType::PressureVessel, "pressure_vessel"),
89 (ProcessType::Level, "level"),
90 (ProcessType::TemperatureMixing, "temperature_mixing"),
91 (
92 ProcessType::TemperatureHeatExchange,
93 "temperature_heat_exchange",
94 ),
95 ];
96 for (variant, text) in cases {
97 assert_eq!(enum_to_text(&variant), text);
98 assert_eq!(
99 text_to_enum::<ProcessType>("process_type", text).unwrap(),
100 variant
101 );
102 }
103 }
104
105 #[test]
106 #[should_panic(expected = "doesn't serialize to a bare string")]
107 fn enum_to_text_rejects_non_string_serialization() {
108 enum_to_text(&42u8);
109 }
110
111 #[test]
112 #[should_panic(expected = "enum serialization is infallible")]
113 fn enum_to_text_rejects_a_serialization_failure() {
114 enum_to_text_with(&ProcessType::Flow, |_| {
115 Err::<serde_json::Value, _>("injected serialization failure")
116 });
117 }
118
119 #[test]
120 fn controller_type_round_trips_and_matches_check_constraint() {
121 let cases = [
122 (ControllerType::P, "p"),
123 (ControllerType::Pi, "pi"),
124 (ControllerType::Pid, "pid"),
125 ];
126 for (variant, text) in cases {
127 assert_eq!(enum_to_text(&variant), text);
128 assert_eq!(
129 text_to_enum::<ControllerType>("controller_type", text).unwrap(),
130 variant
131 );
132 }
133 }
134
135 #[test]
136 fn controller_direction_round_trips_and_matches_check_constraint() {
137 let cases = [
138 (ControllerDirection::Direct, "direct"),
139 (ControllerDirection::Reverse, "reverse"),
140 ];
141 for (variant, text) in cases {
142 assert_eq!(enum_to_text(&variant), text);
143 assert_eq!(
144 text_to_enum::<ControllerDirection>("controller_direction", text).unwrap(),
145 variant
146 );
147 }
148 }
149
150 #[test]
151 fn response_level_round_trips_and_matches_check_constraint() {
152 let cases = [
153 (ResponseLevel::Aggressive, "aggressive"),
154 (ResponseLevel::Moderate, "moderate"),
155 (ResponseLevel::Sluggish, "sluggish"),
156 ];
157 for (variant, text) in cases {
158 assert_eq!(enum_to_text(&variant), text);
159 assert_eq!(
160 text_to_enum::<ResponseLevel>("response_level", text).unwrap(),
161 variant
162 );
163 }
164 }
165
166 #[test]
167 fn proportional_type_round_trips_and_matches_check_constraint() {
168 let cases = [
169 (ProportionalType::Gain, "gain"),
170 (ProportionalType::Band, "band"),
171 ];
172 for (variant, text) in cases {
173 assert_eq!(enum_to_text(&variant), text);
174 assert_eq!(
175 text_to_enum::<ProportionalType>("proportional_type", text).unwrap(),
176 variant
177 );
178 }
179 }
180
181 #[test]
182 fn integral_type_round_trips_and_matches_check_constraint() {
183 let cases = [
184 (IntegralType::ResetTime, "reset_time"),
185 (IntegralType::ResetRate, "reset_rate"),
186 (IntegralType::ResetGain, "reset_gain"),
187 ];
188 for (variant, text) in cases {
189 assert_eq!(enum_to_text(&variant), text);
190 assert_eq!(
191 text_to_enum::<IntegralType>("integral_type", text).unwrap(),
192 variant
193 );
194 }
195 }
196
197 #[test]
198 fn derivative_type_round_trips_and_matches_check_constraint() {
199 let cases = [
200 (DerivativeType::DerivativeTime, "derivative_time"),
201 (DerivativeType::DerivativeGain, "derivative_gain"),
202 ];
203 for (variant, text) in cases {
204 assert_eq!(enum_to_text(&variant), text);
205 assert_eq!(
206 text_to_enum::<DerivativeType>("derivative_type", text).unwrap(),
207 variant
208 );
209 }
210 }
211
212 #[test]
213 fn time_unit_round_trips_and_matches_check_constraint() {
214 let cases = [
215 (TimeUnit::Seconds, "seconds"),
216 (TimeUnit::Minutes, "minutes"),
217 ];
218 for (variant, text) in cases {
219 assert_eq!(enum_to_text(&variant), text);
220 assert_eq!(
221 text_to_enum::<TimeUnit>("integral_unit", text).unwrap(),
222 variant
223 );
224 }
225 }
226
227 #[test]
228 fn unrecognized_value_is_a_typed_error_not_a_panic() {
229 let err = text_to_enum::<ProcessType>("process_type", "not_a_real_variant").unwrap_err();
230 assert!(matches!(
231 err,
232 DbError::InvalidEnumValue {
233 column: "process_type",
234 value,
235 } if value == "not_a_real_variant"
236 ));
237 }
238
239 #[test]
240 fn database_enum_types_round_trip_through_the_shared_text_codec() {
241 assert_eq!(
242 text_to_enum::<TuneDriver>("driver", "opcda").unwrap(),
243 TuneDriver::Opcda
244 );
245 assert_eq!(
246 text_to_enum::<TuneOutcome>("outcome", "completed").unwrap(),
247 TuneOutcome::Completed
248 );
249 assert_eq!(
250 text_to_enum::<TemplateOrigin>("origin", "builtin").unwrap(),
251 TemplateOrigin::Builtin
252 );
253 assert_eq!(
254 text_to_enum::<RestoreStatus>("restore_status", "confirmed").unwrap(),
255 RestoreStatus::Confirmed
256 );
257 assert_eq!(
258 text_to_enum::<RollbackState>("rollback_state", "succeeded").unwrap(),
259 RollbackState::Succeeded
260 );
261 assert_eq!(
262 text_to_enum::<SampleQuality>("pv_quality", "good").unwrap(),
263 SampleQuality::Good
264 );
265 assert_eq!(
266 text_to_enum::<MvActuationKind>("kind", "relay").unwrap(),
267 MvActuationKind::Relay
268 );
269 assert_eq!(
270 text_to_enum::<MvActuationStatus>("status", "confirmed").unwrap(),
271 MvActuationStatus::Confirmed
272 );
273 assert_eq!(
274 text_to_enum::<WriteKind>("kind", "write").unwrap(),
275 WriteKind::Write
276 );
277 }
278}