fleetforge_common/
validation.rs1use serde_json::Value;
2use thiserror::Error;
3
4use crate::{
5 agent_run_schema, artifact_schema, run_event_schema, run_schema, run_spec_schema, step_schema,
6};
7
8pub fn validate_step_spec(instance: &Value) -> Result<(), StepSpecError> {
10 if let Err(errors) = step_schema::step_schema().validate(instance) {
11 let details = errors
12 .map(|error| StepSpecErrorDetail {
13 instance_path: error.instance_path.to_string(),
14 schema_path: error.schema_path.to_string(),
15 message: error.to_string(),
16 })
17 .collect();
18 Err(StepSpecError { details })
19 } else {
20 Ok(())
21 }
22}
23
24#[derive(Debug, Clone)]
26pub struct StepSpecErrorDetail {
27 pub instance_path: String,
28 pub schema_path: String,
29 pub message: String,
30}
31
32impl std::fmt::Display for StepSpecErrorDetail {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 write!(
35 f,
36 "{} (instance path: {}, schema path: {})",
37 self.message, self.instance_path, self.schema_path
38 )
39 }
40}
41
42impl std::error::Error for StepSpecErrorDetail {}
43
44#[derive(Debug, Error)]
46#[error("step specification did not pass schema validation")]
47pub struct StepSpecError {
48 pub details: Vec<StepSpecErrorDetail>,
49}
50
51#[derive(Debug, Clone)]
53pub struct SchemaErrorDetail {
54 pub instance_path: String,
55 pub schema_path: String,
56 pub message: String,
57}
58
59impl std::fmt::Display for SchemaErrorDetail {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(
62 f,
63 "{} (instance path: {}, schema path: {})",
64 self.message, self.instance_path, self.schema_path
65 )
66 }
67}
68
69impl std::error::Error for SchemaErrorDetail {}
70
71#[derive(Debug, Error)]
73#[error("{schema} schema validation failed")]
74pub struct SchemaValidationError {
75 pub schema: &'static str,
76 pub details: Vec<SchemaErrorDetail>,
77}
78
79fn validate_with_schema(
80 schema_name: &'static str,
81 schema: &jsonschema::JSONSchema,
82 instance: &Value,
83) -> Result<(), SchemaValidationError> {
84 if let Err(errors) = schema.validate(instance) {
85 let details = errors
86 .map(|error| SchemaErrorDetail {
87 instance_path: error.instance_path.to_string(),
88 schema_path: error.schema_path.to_string(),
89 message: error.to_string(),
90 })
91 .collect();
92 Err(SchemaValidationError {
93 schema: schema_name,
94 details,
95 })
96 } else {
97 Ok(())
98 }
99}
100
101pub fn validate_run_spec(instance: &Value) -> Result<(), SchemaValidationError> {
103 validate_with_schema("run_spec", run_spec_schema::run_spec_schema(), instance)
104}
105
106pub fn validate_run(instance: &Value) -> Result<(), SchemaValidationError> {
108 validate_with_schema("run", run_schema::run_schema(), instance)
109}
110
111pub fn validate_run_event(instance: &Value) -> Result<(), SchemaValidationError> {
113 validate_with_schema("run_event", run_event_schema::run_event_schema(), instance)
114}
115
116pub fn validate_artifact(instance: &Value) -> Result<(), SchemaValidationError> {
118 validate_with_schema("artifact", artifact_schema::artifact_schema(), instance)
119}
120
121pub fn validate_agent_run(instance: &Value) -> Result<(), SchemaValidationError> {
123 validate_with_schema("agent_run", agent_run_schema::agent_run_schema(), instance)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use serde_json::json;
130
131 #[test]
132 fn run_spec_validation_passes_for_minimal_payload() {
133 let spec = json!({
134 "dag_json": {
135 "steps": [
136 { "id": "11111111-1111-4111-8111-111111111111", "type": "tool" }
137 ]
138 },
139 "inputs": {},
140 "labels": {},
141 "seed": 1,
142 "breakpoints": []
143 });
144 validate_run_spec(&spec).expect("run spec should validate");
145 }
146
147 #[test]
148 fn run_spec_validation_fails_without_steps() {
149 let spec = json!({
150 "dag_json": {},
151 "inputs": {},
152 "labels": {},
153 "seed": 1
154 });
155 let err = validate_run_spec(&spec).expect_err("missing steps must fail validation");
156 assert_eq!(err.schema, "run_spec");
157 }
158
159 #[test]
160 fn agent_run_validation_enforces_known_actions() {
161 let envelope = json!({
162 "adapter": "langgraph",
163 "version": "0.1.0",
164 "action": "emit",
165 "run_id": "11111111-1111-4111-8111-111111111111",
166 "step_id": "11111111-1111-4111-8111-111111111112",
167 "timestamp": "2024-01-01T00:00:00Z",
168 "payload": {}
169 });
170 validate_agent_run(&envelope).expect("agent run envelope should validate");
171
172 let bad = json!({
173 "adapter": "langgraph",
174 "version": "0.1.0",
175 "action": "noop",
176 "run_id": "11111111-1111-4111-8111-111111111111",
177 "timestamp": "2024-01-01T00:00:00Z"
178 });
179 assert!(
180 validate_agent_run(&bad).is_err(),
181 "unknown action must fail validation"
182 );
183 }
184}