peat_schema/validation/
command.rs1use super::{ValidationError, ValidationResult};
6use crate::command::v1::HierarchicalCommand;
7
8pub fn validate_hierarchical_command(cmd: &HierarchicalCommand) -> ValidationResult<()> {
17 if cmd.command_id.is_empty() {
19 return Err(ValidationError::MissingField("command_id".to_string()));
20 }
21
22 if cmd.originator_id.is_empty() {
23 return Err(ValidationError::MissingField("originator_id".to_string()));
24 }
25
26 let target = cmd
28 .target
29 .as_ref()
30 .ok_or_else(|| ValidationError::MissingField("target".to_string()))?;
31
32 if target.target_ids.is_empty() && target.scope != 4 {
34 return Err(ValidationError::MissingField(
36 "target.target_ids (required for non-broadcast commands)".to_string(),
37 ));
38 }
39
40 if cmd.issued_at.is_none() {
42 return Err(ValidationError::MissingField("issued_at".to_string()));
43 }
44
45 if let (Some(issued), Some(expires)) = (&cmd.issued_at, &cmd.expires_at) {
47 if expires.seconds < issued.seconds
48 || (expires.seconds == issued.seconds && expires.nanos < issued.nanos)
49 {
50 return Err(ValidationError::ConstraintViolation(
51 "expires_at must be after issued_at".to_string(),
52 ));
53 }
54 }
55
56 if cmd.command_type.is_none() {
58 return Err(ValidationError::MissingField("command_type".to_string()));
59 }
60
61 Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::command::v1::{
68 command_target::Scope, mission_order::MissionType, CommandTarget, MissionOrder,
69 };
70 use crate::common::v1::Timestamp;
71
72 fn valid_command() -> HierarchicalCommand {
73 HierarchicalCommand {
74 command_id: "CMD-001".to_string(),
75 originator_id: "HQ-1".to_string(),
76 target: Some(CommandTarget {
77 scope: Scope::Squad as i32,
78 target_ids: vec!["Alpha".to_string()],
79 }),
80 command_type: Some(
81 crate::command::v1::hierarchical_command::CommandType::MissionOrder(MissionOrder {
82 mission_type: MissionType::Isr as i32,
83 mission_id: "ISR-001".to_string(),
84 description: "Conduct ISR in sector Alpha".to_string(),
85 objective_location: None,
86 start_time: None,
87 end_time: None,
88 roe: None,
89 }),
90 ),
91 priority: 1,
92 buffer_policy: 1,
93 conflict_policy: 1,
94 acknowledgment_policy: 2,
95 leader_change_policy: 1,
96 issued_at: Some(Timestamp {
97 seconds: 1702000000,
98 nanos: 0,
99 }),
100 expires_at: None,
101 version: 1,
102 }
103 }
104
105 #[test]
106 fn test_valid_command() {
107 let cmd = valid_command();
108 assert!(validate_hierarchical_command(&cmd).is_ok());
109 }
110
111 #[test]
112 fn test_missing_command_id() {
113 let mut cmd = valid_command();
114 cmd.command_id = String::new();
115 let err = validate_hierarchical_command(&cmd).unwrap_err();
116 assert!(matches!(err, ValidationError::MissingField(f) if f == "command_id"));
117 }
118
119 #[test]
120 fn test_missing_originator_id() {
121 let mut cmd = valid_command();
122 cmd.originator_id = String::new();
123 let err = validate_hierarchical_command(&cmd).unwrap_err();
124 assert!(matches!(err, ValidationError::MissingField(f) if f == "originator_id"));
125 }
126
127 #[test]
128 fn test_missing_target() {
129 let mut cmd = valid_command();
130 cmd.target = None;
131 let err = validate_hierarchical_command(&cmd).unwrap_err();
132 assert!(matches!(err, ValidationError::MissingField(f) if f == "target"));
133 }
134
135 #[test]
136 fn test_missing_issued_at() {
137 let mut cmd = valid_command();
138 cmd.issued_at = None;
139 let err = validate_hierarchical_command(&cmd).unwrap_err();
140 assert!(matches!(err, ValidationError::MissingField(f) if f == "issued_at"));
141 }
142
143 #[test]
144 fn test_expires_before_issued() {
145 let mut cmd = valid_command();
146 cmd.issued_at = Some(Timestamp {
147 seconds: 1702000000,
148 nanos: 0,
149 });
150 cmd.expires_at = Some(Timestamp {
151 seconds: 1701000000, nanos: 0,
153 });
154 let err = validate_hierarchical_command(&cmd).unwrap_err();
155 assert!(matches!(err, ValidationError::ConstraintViolation(_)));
156 }
157
158 #[test]
159 fn test_missing_command_type() {
160 let mut cmd = valid_command();
161 cmd.command_type = None;
162 let err = validate_hierarchical_command(&cmd).unwrap_err();
163 assert!(matches!(err, ValidationError::MissingField(f) if f == "command_type"));
164 }
165}