1use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11#[cfg(feature = "ts")]
12use ts_rs::TS;
13
14pub type FieldDefs = BTreeMap<String, FieldDef>;
16
17#[cfg_attr(feature = "ts", derive(TS))]
19#[cfg_attr(
20 feature = "ts",
21 ts(
22 export,
23 export_to = "../../../../packages/@bnto/nodes/src/generated/definitionTypes/"
24 )
25)]
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
27#[serde(rename_all = "camelCase", tag = "type")]
28pub enum FieldDef {
29 #[serde(rename = "string")]
30 String {
31 label: String,
32 #[serde(default, skip_serializing_if = "Option::is_none")]
33 #[cfg_attr(feature = "ts", ts(optional))]
34 description: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 #[cfg_attr(feature = "ts", ts(optional))]
37 default: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 #[cfg_attr(feature = "ts", ts(optional))]
40 placeholder: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 #[cfg_attr(feature = "ts", ts(optional))]
43 order: Option<u32>,
44 },
45 #[serde(rename = "number")]
46 Number {
47 label: String,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 #[cfg_attr(feature = "ts", ts(optional))]
50 description: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 #[cfg_attr(feature = "ts", ts(optional))]
53 default: Option<f64>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 #[cfg_attr(feature = "ts", ts(optional))]
56 min: Option<f64>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 #[cfg_attr(feature = "ts", ts(optional))]
59 max: Option<f64>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 #[cfg_attr(feature = "ts", ts(optional))]
62 step: Option<f64>,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 #[cfg_attr(feature = "ts", ts(optional))]
65 suffix: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 #[cfg_attr(feature = "ts", ts(optional))]
68 order: Option<u32>,
69 },
70 #[serde(rename = "boolean")]
71 Boolean {
72 label: String,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 #[cfg_attr(feature = "ts", ts(optional))]
75 description: Option<String>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 #[cfg_attr(feature = "ts", ts(optional))]
78 default: Option<bool>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 #[cfg_attr(feature = "ts", ts(optional))]
81 order: Option<u32>,
82 },
83 #[serde(rename = "enum")]
84 Enum {
85 label: String,
86 options: Vec<FieldOption>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 #[cfg_attr(feature = "ts", ts(optional))]
89 description: Option<String>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 #[cfg_attr(feature = "ts", ts(optional))]
92 default: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 #[cfg_attr(feature = "ts", ts(optional))]
95 order: Option<u32>,
96 },
97}
98
99#[cfg_attr(feature = "ts", derive(TS))]
101#[cfg_attr(
102 feature = "ts",
103 ts(
104 export,
105 export_to = "../../../../packages/@bnto/nodes/src/generated/definitionTypes/"
106 )
107)]
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct FieldOption {
110 pub value: String,
111 pub label: String,
112}
113
114impl FieldDef {
115 pub fn order(&self) -> u32 {
117 match self {
118 Self::String { order, .. }
119 | Self::Number { order, .. }
120 | Self::Boolean { order, .. }
121 | Self::Enum { order, .. } => order.unwrap_or(u32::MAX),
122 }
123 }
124
125 pub fn default_value(&self) -> serde_json::Value {
127 match self {
128 Self::String { default, .. } => default
129 .as_ref()
130 .map(|s| serde_json::Value::String(s.clone()))
131 .unwrap_or(serde_json::Value::Null),
132 Self::Number { default, .. } => default
133 .and_then(serde_json::Number::from_f64)
134 .map(serde_json::Value::Number)
135 .unwrap_or(serde_json::Value::Null),
136 Self::Boolean { default, .. } => default
137 .map(serde_json::Value::Bool)
138 .unwrap_or(serde_json::Value::Null),
139 Self::Enum { default, .. } => default
140 .as_ref()
141 .map(|s| serde_json::Value::String(s.clone()))
142 .unwrap_or(serde_json::Value::Null),
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn string_field_round_trips() {
153 let json = r#"{"type":"string","label":"Name","default":"hello","placeholder":"Enter..."}"#;
154 let field: FieldDef = serde_json::from_str(json).unwrap();
155 let FieldDef::String {
156 label,
157 default,
158 placeholder,
159 ..
160 } = &field
161 else {
162 panic!("expected String variant");
163 };
164 assert_eq!(label, "Name");
165 assert_eq!(default.as_deref(), Some("hello"));
166 assert_eq!(placeholder.as_deref(), Some("Enter..."));
167 let serialized = serde_json::to_string(&field).unwrap();
168 let round_tripped: FieldDef = serde_json::from_str(&serialized).unwrap();
169 assert_eq!(field, round_tripped);
170 }
171
172 #[test]
173 fn number_field_round_trips() {
174 let json = r#"{"type":"number","label":"Quality","default":80,"min":1,"max":100,"step":1,"suffix":"%"}"#;
175 let field: FieldDef = serde_json::from_str(json).unwrap();
176 let FieldDef::Number {
177 label,
178 default,
179 min,
180 max,
181 step,
182 suffix,
183 ..
184 } = &field
185 else {
186 panic!("expected Number variant");
187 };
188 assert_eq!(label, "Quality");
189 assert_eq!(*default, Some(80.0));
190 assert_eq!(*min, Some(1.0));
191 assert_eq!(*max, Some(100.0));
192 assert_eq!(*step, Some(1.0));
193 assert_eq!(suffix.as_deref(), Some("%"));
194 let serialized = serde_json::to_string(&field).unwrap();
195 let round_tripped: FieldDef = serde_json::from_str(&serialized).unwrap();
196 assert_eq!(field, round_tripped);
197 }
198
199 #[test]
200 fn boolean_field_round_trips() {
201 let json = r#"{"type":"boolean","label":"Strip Metadata","default":true}"#;
202 let field: FieldDef = serde_json::from_str(json).unwrap();
203 let FieldDef::Boolean { label, default, .. } = &field else {
204 panic!("expected Boolean variant");
205 };
206 assert_eq!(label, "Strip Metadata");
207 assert_eq!(*default, Some(true));
208 let serialized = serde_json::to_string(&field).unwrap();
209 let round_tripped: FieldDef = serde_json::from_str(&serialized).unwrap();
210 assert_eq!(field, round_tripped);
211 }
212
213 #[test]
214 fn enum_field_round_trips() {
215 let json = r#"{
216 "type": "enum",
217 "label": "Format",
218 "options": [
219 {"value": "mp4", "label": "MP4"},
220 {"value": "webm", "label": "WebM"}
221 ],
222 "default": "mp4"
223 }"#;
224 let field: FieldDef = serde_json::from_str(json).unwrap();
225 let FieldDef::Enum {
226 label,
227 options,
228 default,
229 ..
230 } = &field
231 else {
232 panic!("expected Enum variant");
233 };
234 assert_eq!(label, "Format");
235 assert_eq!(options.len(), 2);
236 assert_eq!(options[0].value, "mp4");
237 assert_eq!(options[0].label, "MP4");
238 assert_eq!(default.as_deref(), Some("mp4"));
239 let serialized = serde_json::to_string(&field).unwrap();
240 let round_tripped: FieldDef = serde_json::from_str(&serialized).unwrap();
241 assert_eq!(field, round_tripped);
242 }
243
244 #[test]
245 fn tagged_union_discriminator() {
246 let json = r#"{"type":"enum","label":"X","options":[]}"#;
247 let field: FieldDef = serde_json::from_str(json).unwrap();
248 assert!(matches!(field, FieldDef::Enum { .. }));
249
250 let json = r#"{"type":"string","label":"X"}"#;
251 let field: FieldDef = serde_json::from_str(json).unwrap();
252 assert!(matches!(field, FieldDef::String { .. }));
253 }
254
255 #[test]
256 fn field_defs_map_round_trips() {
257 let json = r#"{
258 "format": {"type":"enum","label":"Format","options":[{"value":"mp4","label":"MP4"}],"default":"mp4","order":1},
259 "quality": {"type":"number","label":"Quality","default":80,"min":1,"max":100,"order":2}
260 }"#;
261 let fields: FieldDefs = serde_json::from_str(json).unwrap();
262 assert_eq!(fields.len(), 2);
263 assert!(matches!(fields["format"], FieldDef::Enum { .. }));
264 assert!(matches!(fields["quality"], FieldDef::Number { .. }));
265 }
266
267 #[test]
268 fn order_defaults_to_max() {
269 let field = FieldDef::String {
270 label: "X".into(),
271 description: None,
272 default: None,
273 placeholder: None,
274 order: None,
275 };
276 assert_eq!(field.order(), u32::MAX);
277 }
278
279 #[test]
280 fn order_returns_explicit_value() {
281 let field = FieldDef::Enum {
282 label: "X".into(),
283 options: vec![],
284 description: None,
285 default: None,
286 order: Some(3),
287 };
288 assert_eq!(field.order(), 3);
289 }
290
291 #[test]
292 fn default_value_for_each_variant() {
293 let s = FieldDef::String {
294 label: "X".into(),
295 description: None,
296 default: Some("hello".into()),
297 placeholder: None,
298 order: None,
299 };
300 assert_eq!(s.default_value(), serde_json::json!("hello"));
301
302 let n = FieldDef::Number {
303 label: "X".into(),
304 description: None,
305 default: Some(42.0),
306 min: None,
307 max: None,
308 step: None,
309 suffix: None,
310 order: None,
311 };
312 assert_eq!(n.default_value(), serde_json::json!(42.0));
313
314 let b = FieldDef::Boolean {
315 label: "X".into(),
316 description: None,
317 default: Some(false),
318 order: None,
319 };
320 assert_eq!(b.default_value(), serde_json::json!(false));
321
322 let no_default = FieldDef::String {
323 label: "X".into(),
324 description: None,
325 default: None,
326 placeholder: None,
327 order: None,
328 };
329 assert!(no_default.default_value().is_null());
330 }
331
332 #[test]
333 fn optional_fields_omitted_on_serialization() {
334 let field = FieldDef::String {
335 label: "Name".into(),
336 description: None,
337 default: None,
338 placeholder: None,
339 order: None,
340 };
341 let json = serde_json::to_string(&field).unwrap();
342 assert!(!json.contains("description"));
343 assert!(!json.contains("default"));
344 assert!(!json.contains("placeholder"));
345 assert!(!json.contains("order"));
346 }
347}