fleetforge_common/
validation.rs

1use 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
8/// Validates a step specification against the FleetForge step schema.
9pub 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/// Schema validation failure details.
25#[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/// Error returned when validating step specifications.
45#[derive(Debug, Error)]
46#[error("step specification did not pass schema validation")]
47pub struct StepSpecError {
48    pub details: Vec<StepSpecErrorDetail>,
49}
50
51/// Details about a generic schema validation failure.
52#[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/// Error returned when validating instances against embedded schemas.
72#[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
101/// Validates a run specification instance.
102pub fn validate_run_spec(instance: &Value) -> Result<(), SchemaValidationError> {
103    validate_with_schema("run_spec", run_spec_schema::run_spec_schema(), instance)
104}
105
106/// Validates a stored run record.
107pub fn validate_run(instance: &Value) -> Result<(), SchemaValidationError> {
108    validate_with_schema("run", run_schema::run_schema(), instance)
109}
110
111/// Validates a run event envelope.
112pub fn validate_run_event(instance: &Value) -> Result<(), SchemaValidationError> {
113    validate_with_schema("run_event", run_event_schema::run_event_schema(), instance)
114}
115
116/// Validates artifact metadata.
117pub fn validate_artifact(instance: &Value) -> Result<(), SchemaValidationError> {
118    validate_with_schema("artifact", artifact_schema::artifact_schema(), instance)
119}
120
121/// Validates an AgentRun adapter envelope.
122pub 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}