1use improv_core_model::{Value, ValueError, ValueErrorKind, ValueType};
8use serde_json::{json, Value as J};
9
10pub fn value_to_json(v: &Value) -> J {
19 match v {
20 Value::Number(n) => json!(n),
21 Value::Boolean(b) => json!(b),
22 Value::Text(s) => json!(s),
23 Value::DateTime(dt) => json!({
24 "__improv__": "datetime",
25 "value": dt.to_rfc3339(),
26 }),
27 Value::Enum(i) => json!({ "__improv__": "enum", "value": i }),
28 Value::Error(e) => json!({
29 "__improv__": "error",
30 "kind": err_kind_str(&e.kind),
31 "message": err_message(&e.kind),
32 }),
33 }
34}
35
36pub fn json_to_value(j: &J, expected: ValueType) -> Result<Value, String> {
39 match expected {
40 ValueType::Number => j
41 .as_f64()
42 .map(Value::Number)
43 .ok_or_else(|| format!("expected Number, got {}", short(j))),
44 ValueType::Boolean => j
45 .as_bool()
46 .map(Value::Boolean)
47 .ok_or_else(|| format!("expected Boolean, got {}", short(j))),
48 ValueType::Text => j
49 .as_str()
50 .map(|s| Value::Text(s.to_string()))
51 .ok_or_else(|| format!("expected Text, got {}", short(j))),
52 ValueType::DateTime => {
53 let s = tagged_str(j, "datetime")
54 .or_else(|| j.as_str().map(str::to_string))
55 .ok_or_else(|| format!("expected DateTime, got {}", short(j)))?;
56 chrono::DateTime::parse_from_rfc3339(&s)
57 .map(|dt| Value::DateTime(dt.with_timezone(&chrono::Utc)))
58 .map_err(|e| format!("invalid DateTime {s:?}: {e}"))
59 }
60 ValueType::Enum => tagged_u64(j, "enum")
61 .or_else(|| j.as_u64())
62 .and_then(|n| u32::try_from(n).ok())
63 .map(Value::Enum)
64 .ok_or_else(|| format!("expected Enum index, got {}", short(j))),
65 }
66}
67
68pub fn arg_matches(v: &Value, expected: ValueType) -> bool {
72 v.type_of() == Some(expected)
73}
74
75fn tagged_str(j: &J, tag: &str) -> Option<String> {
76 if j.get("__improv__")?.as_str()? == tag {
77 j.get("value")?.as_str().map(str::to_string)
78 } else {
79 None
80 }
81}
82
83fn tagged_u64(j: &J, tag: &str) -> Option<u64> {
84 if j.get("__improv__")?.as_str()? == tag {
85 j.get("value")?.as_u64()
86 } else {
87 None
88 }
89}
90
91fn err_kind_str(k: &ValueErrorKind) -> &'static str {
92 match k {
93 ValueErrorKind::TypeMismatch => "type_mismatch",
94 ValueErrorKind::DimensionMismatch => "dimension_mismatch",
95 ValueErrorKind::DivisionByZero => "division_by_zero",
96 ValueErrorKind::MissingInput => "missing_input",
97 ValueErrorKind::Custom(_) => "custom",
98 ValueErrorKind::Null => "null",
99 ValueErrorKind::Ref => "ref",
100 ValueErrorKind::Name => "name",
101 ValueErrorKind::Num => "num",
102 ValueErrorKind::Na => "na",
103 }
104}
105
106fn err_message(k: &ValueErrorKind) -> String {
107 match k {
108 ValueErrorKind::Custom(m) => m.clone(),
109 other => err_kind_str(other).to_string(),
110 }
111}
112
113fn short(j: &J) -> String {
115 let s = j.to_string();
116 if s.len() > 60 {
117 format!("{}…", &s[..60])
118 } else {
119 s
120 }
121}
122
123pub fn error_value(msg: impl Into<String>) -> Value {
125 Value::Error(ValueError::new(ValueErrorKind::Custom(msg.into())))
126}
127
128#[cfg(test)]
129#[cfg_attr(coverage_nightly, coverage(off))]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn roundtrip_scalars() {
135 assert_eq!(
136 json_to_value(&value_to_json(&Value::Number(2.5)), ValueType::Number).unwrap(),
137 Value::Number(2.5)
138 );
139 assert_eq!(
140 json_to_value(&value_to_json(&Value::Boolean(true)), ValueType::Boolean).unwrap(),
141 Value::Boolean(true)
142 );
143 assert_eq!(
144 json_to_value(&value_to_json(&Value::Text("hi".into())), ValueType::Text).unwrap(),
145 Value::Text("hi".into())
146 );
147 assert_eq!(
148 json_to_value(&value_to_json(&Value::Enum(7)), ValueType::Enum).unwrap(),
149 Value::Enum(7)
150 );
151 }
152
153 #[test]
154 fn roundtrip_datetime() {
155 let dt = chrono::DateTime::parse_from_rfc3339("2025-01-02T03:04:05Z")
156 .unwrap()
157 .with_timezone(&chrono::Utc);
158 let back =
159 json_to_value(&value_to_json(&Value::DateTime(dt)), ValueType::DateTime).unwrap();
160 assert_eq!(back, Value::DateTime(dt));
161 }
162
163 #[test]
164 fn type_mismatch_is_error() {
165 assert!(json_to_value(&json!("nope"), ValueType::Number).is_err());
167 assert!(json_to_value(&json!(1), ValueType::Boolean).is_err());
169 }
170
171 #[test]
172 fn arg_check() {
173 assert!(arg_matches(&Value::Number(1.0), ValueType::Number));
174 assert!(!arg_matches(&Value::Number(1.0), ValueType::Text));
175 assert!(!arg_matches(&error_value("x"), ValueType::Number));
176 }
177
178 #[test]
179 fn error_values_encode_kind_and_message() {
180 use improv_core_model::ValueError;
181 let enc = |k: ValueErrorKind| value_to_json(&Value::Error(ValueError::new(k)));
182 for (k, name) in [
183 (ValueErrorKind::TypeMismatch, "type_mismatch"),
184 (ValueErrorKind::DimensionMismatch, "dimension_mismatch"),
185 (ValueErrorKind::DivisionByZero, "division_by_zero"),
186 (ValueErrorKind::MissingInput, "missing_input"),
187 ] {
188 assert_eq!(
189 enc(k),
190 json!({"__improv__": "error", "kind": name, "message": name})
191 );
192 }
193 assert_eq!(
194 enc(ValueErrorKind::Custom("boom".into())),
195 json!({"__improv__": "error", "kind": "custom", "message": "boom"})
196 );
197 }
198
199 #[test]
200 fn decode_untagged_and_mismatched_shapes() {
201 assert!(matches!(
203 json_to_value(&json!("2025-01-02T03:04:05Z"), ValueType::DateTime),
204 Ok(Value::DateTime(_))
205 ));
206 assert_eq!(
207 json_to_value(&json!(3), ValueType::Enum).unwrap(),
208 Value::Enum(3)
209 );
210 let wrong_tag = json!({"__improv__": "enum", "value": "x"});
212 assert!(json_to_value(&wrong_tag, ValueType::DateTime)
213 .unwrap_err()
214 .contains("expected DateTime"));
215 let wrong_tag = json!({"__improv__": "datetime", "value": 1});
216 assert!(json_to_value(&wrong_tag, ValueType::Enum)
217 .unwrap_err()
218 .contains("expected Enum"));
219 assert!(json_to_value(&json!("not a date"), ValueType::DateTime)
220 .unwrap_err()
221 .contains("invalid DateTime"));
222 assert!(json_to_value(&json!(1), ValueType::Text)
223 .unwrap_err()
224 .contains("expected Text"));
225 assert!(json_to_value(&json!(u64::MAX), ValueType::Enum).is_err());
227 }
228
229 #[test]
230 fn long_values_are_previewed_not_dumped() {
231 let long = json!("y".repeat(200));
232 let msg = json_to_value(&long, ValueType::Number).unwrap_err();
233 assert!(msg.ends_with('…'), "{msg}");
234 assert!(msg.len() < 100, "{msg}");
235 }
236}