1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::field::{DataType, FieldMeaning};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
25pub struct ActionDef {
26 pub name: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub display_name: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub description: Option<String>,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub inputs: Vec<InputDef>,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub preconditions: Vec<String>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
36 pub effects: Vec<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub transition_trigger: Option<String>,
39}
40
41impl ActionDef {
42 pub fn new(name: impl Into<String>) -> Self {
44 Self {
45 name: name.into(),
46 display_name: None,
47 description: None,
48 inputs: Vec::new(),
49 preconditions: Vec::new(),
50 effects: Vec::new(),
51 transition_trigger: None,
52 }
53 }
54
55 pub fn display_name(mut self, name: impl Into<String>) -> Self {
57 self.display_name = Some(name.into());
58 self
59 }
60
61 pub fn description(mut self, desc: impl Into<String>) -> Self {
63 self.description = Some(desc.into());
64 self
65 }
66
67 pub fn input(mut self, input: InputDef) -> Self {
69 self.inputs.push(input);
70 self
71 }
72
73 pub fn precondition(mut self, guard: impl Into<String>) -> Self {
75 self.preconditions.push(guard.into());
76 self
77 }
78
79 pub fn effect(mut self, effect: impl Into<String>) -> Self {
81 self.effects.push(effect.into());
82 self
83 }
84
85 pub fn transition_trigger(mut self, trigger: impl Into<String>) -> Self {
87 self.transition_trigger = Some(trigger.into());
88 self
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
97pub struct InputDef {
98 pub name: String,
99 pub data_type: DataType,
100 pub meaning: FieldMeaning,
101 #[serde(default = "default_true")]
102 pub required: bool,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub description: Option<String>,
105}
106
107fn default_true() -> bool {
108 true
109}
110
111impl InputDef {
112 pub fn new(name: impl Into<String>, data_type: DataType, meaning: FieldMeaning) -> Self {
114 Self {
115 name: name.into(),
116 data_type,
117 meaning,
118 required: true,
119 description: None,
120 }
121 }
122
123 pub fn required(mut self, required: bool) -> Self {
125 self.required = required;
126 self
127 }
128
129 pub fn description(mut self, desc: impl Into<String>) -> Self {
131 self.description = Some(desc.into());
132 self
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
149pub struct GuardDef {
150 pub name: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub display_name: Option<String>,
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub description: Option<String>,
155}
156
157impl GuardDef {
158 pub fn new(name: impl Into<String>) -> Self {
160 Self {
161 name: name.into(),
162 display_name: None,
163 description: None,
164 }
165 }
166
167 pub fn display_name(mut self, name: impl Into<String>) -> Self {
169 self.display_name = Some(name.into());
170 self
171 }
172
173 pub fn description(mut self, desc: impl Into<String>) -> Self {
175 self.description = Some(desc.into());
176 self
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::field::{DataType, FieldMeaning};
184
185 #[test]
188 fn action_def_minimal() {
189 let action = ActionDef::new("submit");
190 assert_eq!(action.name, "submit");
191 assert!(action.display_name.is_none());
192 assert!(action.description.is_none());
193 assert!(action.inputs.is_empty());
194 assert!(action.preconditions.is_empty());
195 assert!(action.effects.is_empty());
196 assert!(action.transition_trigger.is_none());
197 }
198
199 #[test]
200 fn action_def_builder_chain() {
201 let action = ActionDef::new("submit_order")
202 .display_name("Submit Order")
203 .description("Submits a customer order")
204 .input(InputDef::new(
205 "order_id",
206 DataType::Integer,
207 FieldMeaning::Identifier,
208 ))
209 .input(InputDef::new("notes", DataType::String, FieldMeaning::FreeText).required(false))
210 .precondition("has_items")
211 .precondition("payment_valid")
212 .effect("notify_customer")
213 .effect("send_confirmation")
214 .transition_trigger("submit");
215
216 assert_eq!(action.name, "submit_order");
217 assert_eq!(action.display_name.as_deref(), Some("Submit Order"));
218 assert_eq!(
219 action.description.as_deref(),
220 Some("Submits a customer order")
221 );
222 assert_eq!(action.inputs.len(), 2);
223 assert!(action.inputs[0].required);
224 assert!(!action.inputs[1].required);
225 assert_eq!(action.preconditions, vec!["has_items", "payment_valid"]);
226 assert_eq!(action.effects, vec!["notify_customer", "send_confirmation"]);
227 assert_eq!(action.transition_trigger.as_deref(), Some("submit"));
228 }
229
230 #[test]
231 fn action_def_serde_round_trip() {
232 let action = ActionDef::new("submit_order")
233 .display_name("Submit Order")
234 .input(InputDef::new(
235 "order_id",
236 DataType::Integer,
237 FieldMeaning::Identifier,
238 ))
239 .precondition("has_items")
240 .effect("notify")
241 .transition_trigger("submit");
242
243 let json = serde_json::to_string(&action).unwrap();
244 let parsed: ActionDef = serde_json::from_str(&json).unwrap();
245 assert_eq!(action, parsed);
246 }
247
248 #[test]
249 fn action_def_json_omits_empty_vecs_and_none() {
250 let action = ActionDef::new("simple");
251 let json = serde_json::to_string(&action).unwrap();
252 assert!(!json.contains("display_name"));
253 assert!(!json.contains("description"));
254 assert!(!json.contains("inputs"));
255 assert!(!json.contains("preconditions"));
256 assert!(!json.contains("effects"));
257 assert!(!json.contains("transition_trigger"));
258 }
259
260 #[test]
261 fn action_def_json_schema() {
262 let schema = schemars::schema_for!(ActionDef);
263 let value = schema.to_value();
264 let props = value
265 .get("properties")
266 .expect("ActionDef schema must have properties");
267 let obj = props.as_object().unwrap();
268 assert!(obj.contains_key("name"), "missing 'name' property");
269 assert!(obj.contains_key("inputs"), "missing 'inputs' property");
270 assert!(
271 obj.contains_key("preconditions"),
272 "missing 'preconditions' property"
273 );
274 assert!(obj.contains_key("effects"), "missing 'effects' property");
275 }
276
277 #[test]
280 fn input_def_minimal() {
281 let input = InputDef::new("order_id", DataType::Integer, FieldMeaning::Identifier);
282 assert_eq!(input.name, "order_id");
283 assert_eq!(input.data_type, DataType::Integer);
284 assert_eq!(input.meaning, FieldMeaning::Identifier);
285 assert!(input.required);
286 assert!(input.description.is_none());
287 }
288
289 #[test]
290 fn input_def_builder_chain() {
291 let input = InputDef::new("notes", DataType::String, FieldMeaning::FreeText)
292 .required(false)
293 .description("Optional order notes");
294
295 assert_eq!(input.name, "notes");
296 assert!(!input.required);
297 assert_eq!(input.description.as_deref(), Some("Optional order notes"));
298 }
299
300 #[test]
301 fn input_def_serde_round_trip() {
302 let input = InputDef::new("email", DataType::String, FieldMeaning::Email)
303 .description("Customer email");
304
305 let json = serde_json::to_string(&input).unwrap();
306 let parsed: InputDef = serde_json::from_str(&json).unwrap();
307 assert_eq!(input, parsed);
308 }
309
310 #[test]
311 fn input_def_defaults() {
312 let json = r#"{"name":"total","data_type":"float","meaning":"money"}"#;
313 let parsed: InputDef = serde_json::from_str(json).unwrap();
314 assert!(parsed.required);
315 assert!(parsed.description.is_none());
316 }
317
318 #[test]
319 fn input_def_json_schema() {
320 let schema = schemars::schema_for!(InputDef);
321 let value = schema.to_value();
322 let props = value
323 .get("properties")
324 .expect("InputDef schema must have properties");
325 let obj = props.as_object().unwrap();
326 assert!(obj.contains_key("name"), "missing 'name' property");
327 assert!(
328 obj.contains_key("data_type"),
329 "missing 'data_type' property"
330 );
331 assert!(obj.contains_key("meaning"), "missing 'meaning' property");
332 }
333
334 #[test]
337 fn guard_def_minimal() {
338 let guard = GuardDef::new("has_items");
339 assert_eq!(guard.name, "has_items");
340 assert!(guard.display_name.is_none());
341 assert!(guard.description.is_none());
342 }
343
344 #[test]
345 fn guard_def_builder_chain() {
346 let guard = GuardDef::new("payment_valid")
347 .display_name("Payment Valid")
348 .description("Customer payment method has been verified");
349
350 assert_eq!(guard.name, "payment_valid");
351 assert_eq!(guard.display_name.as_deref(), Some("Payment Valid"));
352 assert_eq!(
353 guard.description.as_deref(),
354 Some("Customer payment method has been verified")
355 );
356 }
357
358 #[test]
359 fn guard_def_serde_round_trip() {
360 let guard = GuardDef::new("has_items")
361 .display_name("Has Items")
362 .description("Order must contain at least one item");
363
364 let json = serde_json::to_string(&guard).unwrap();
365 let parsed: GuardDef = serde_json::from_str(&json).unwrap();
366 assert_eq!(guard, parsed);
367 }
368
369 #[test]
370 fn guard_def_json_omits_none() {
371 let guard = GuardDef::new("simple");
372 let json = serde_json::to_string(&guard).unwrap();
373 assert!(!json.contains("display_name"));
374 assert!(!json.contains("description"));
375 }
376
377 #[test]
378 fn guard_def_json_schema() {
379 let schema = schemars::schema_for!(GuardDef);
380 let value = schema.to_value();
381 let props = value
382 .get("properties")
383 .expect("GuardDef schema must have properties");
384 let obj = props.as_object().unwrap();
385 assert!(obj.contains_key("name"), "missing 'name' property");
386 }
387
388 #[test]
391 fn action_def_without_transition_trigger() {
392 let action = ActionDef::new("update_notes")
393 .display_name("Update Notes")
394 .input(InputDef::new(
395 "notes",
396 DataType::String,
397 FieldMeaning::FreeText,
398 ))
399 .effect("log_change");
400
401 assert!(action.transition_trigger.is_none());
402 assert_eq!(action.effects, vec!["log_change"]);
403 assert_eq!(action.inputs.len(), 1);
404 }
405
406 #[test]
407 fn action_def_serde_minimal_round_trip() {
408 let action = ActionDef::new("simple");
409 let json = serde_json::to_string(&action).unwrap();
410 let parsed: ActionDef = serde_json::from_str(&json).unwrap();
411 assert_eq!(action, parsed);
412 }
413}