1use crate::{
5 config::Field,
6 types::{ArtifactName, EvalId, Fingerprint, RequestStatus},
7};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::{collections::BTreeMap, path::PathBuf};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct ChildIdentity {
15 pub pid: crate::process::ProcessId,
16 pub start_time: u64,
17}
18impl From<crate::process::ChildIdentity> for ChildIdentity {
19 fn from(child: crate::process::ChildIdentity) -> Self {
20 Self {
21 pid: child.pid,
22 start_time: child.start_time,
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(try_from = "String", into = "String")]
31pub enum Blocker {
32 Artifact(ArtifactName),
33 Eval(EvalId),
34}
35impl TryFrom<String> for Blocker {
36 type Error = String;
37 fn try_from(value: String) -> Result<Self, String> {
38 if value.contains('/') {
39 value.parse().map(Self::Eval)
40 } else {
41 value.parse().map(Self::Artifact)
42 }
43 }
44}
45impl From<Blocker> for String {
46 fn from(value: Blocker) -> String {
47 value.to_string()
48 }
49}
50impl std::fmt::Display for Blocker {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::Artifact(id) => id.fmt(f),
54 Self::Eval(id) => id.fmt(f),
55 }
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct HumanDefinition {
61 #[serde(
62 default,
63 skip_serializing_if = "Field::missing",
64 with = "super::definitions::path_field"
65 )]
66 pub repo: Field<PathBuf>,
67 pub eval: HumanEval,
68 #[serde(default, skip_serializing_if = "Field::missing")]
69 pub artifacts: Field<BTreeMap<ArtifactName, super::definitions::Artifact>>,
70 #[serde(flatten)]
71 pub extra: Map<String, Value>,
72}
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct HumanEval {
75 #[serde(default, skip_serializing_if = "Field::missing")]
76 pub id: Field<EvalId>,
77 #[serde(default, skip_serializing_if = "Field::missing")]
78 pub target: Field<ArtifactName>,
79 #[serde(default, skip_serializing_if = "Field::missing")]
80 pub references: Field<BTreeMap<crate::config::ReferenceName, ArtifactName>>,
81 #[serde(default, skip_serializing_if = "Field::missing")]
82 pub deps: Field<Vec<ArtifactName>>,
83 #[serde(default, skip_serializing_if = "Field::missing")]
84 pub declaration: Field<super::definitions::Declaration>,
85 #[serde(flatten)]
86 pub extra: Map<String, Value>,
87}
88impl PartialEq for HumanDefinition {
89 fn eq(&self, other: &Self) -> bool {
90 serde_json::to_value(self).expect("Human definition is JSON")
92 == serde_json::to_value(other).expect("Human definition is JSON")
93 }
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize)]
97#[serde(transparent)]
98pub struct Validation(pub Option<ValidationSnapshot>);
99impl Validation {
100 pub fn snapshot(&self) -> Option<&ValidationSnapshot> {
101 self.0.as_ref()
102 }
103}
104
105#[derive(Debug, Clone, Default, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct ValidationSnapshot {
108 #[serde(default, skip_serializing_if = "Field::missing")]
109 pub selection: Field<super::definitions::SavedSelection>,
110 #[serde(default, skip_serializing_if = "Field::missing")]
111 pub recursive: Field<bool>,
112 #[serde(default, skip_serializing_if = "Field::missing")]
113 pub force: Field<bool>,
114 #[serde(default, skip_serializing_if = "Field::missing")]
115 pub selected_eval_ids: Field<Vec<EvalId>>,
116 #[serde(default, skip_serializing_if = "Field::missing")]
117 pub included_eval_ids: Field<Vec<EvalId>>,
118 #[serde(default, skip_serializing_if = "Field::missing")]
119 pub satisfied: Field<bool>,
120 #[serde(default, skip_serializing_if = "Field::missing")]
121 pub obligations: Field<Vec<ArtifactName>>,
122 #[serde(default, skip_serializing_if = "Field::missing")]
123 pub artifacts: Field<Vec<ArtifactValidation>>,
124 #[serde(default, skip_serializing_if = "Field::missing")]
125 pub evals: Field<Vec<EvalValidation>>,
126 #[serde(flatten)]
127 pub extra: Map<String, Value>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct ArtifactValidation {
132 pub id: ArtifactName,
133 #[serde(default, skip_serializing_if = "Field::missing")]
134 pub status: Field<crate::types::ArtifactValidationStatus>,
135 #[serde(default, skip_serializing_if = "Field::missing")]
136 pub passed: Field<u64>,
137 #[serde(default, skip_serializing_if = "Field::missing")]
138 pub total: Field<u64>,
139 #[serde(default, skip_serializing_if = "Field::missing")]
140 pub satisfied: Field<bool>,
141 #[serde(
142 default,
143 rename = "fingerprintKind",
144 skip_serializing_if = "Field::missing"
145 )]
146 pub fingerprint_kind: Field<crate::types::FingerprintKind>,
147 #[serde(default, skip_serializing_if = "Field::missing")]
148 pub fingerprint: Field<Fingerprint>,
149 #[serde(flatten)]
150 pub extra: Map<String, Value>,
151}
152#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(rename_all = "camelCase")]
154pub struct EvalValidation {
155 pub id: EvalId,
156 #[serde(default, skip_serializing_if = "Field::missing")]
157 pub status: Field<RequestStatus>,
158 #[serde(default, skip_serializing_if = "Field::missing")]
159 pub blocked_by: Field<Vec<Blocker>>,
160 #[serde(flatten)]
161 pub extra: Map<String, Value>,
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use serde_json::json;
168
169 #[test]
170 fn human_snapshot_preserves_partial_legacy_and_owner_extension_fields() {
171 for value in [
172 json!({"eval":{"declaration":{"failSchema":{"type":"object"}}}}),
173 json!({"repo":"saved/workspace","eval":{"id":"app/review","target":"app","references":{"spec":"spec"},"deps":["spec"],"declaration":{"payload":{"instruction":"Review.","owner":{"v":1}},"passSchema":{"required":["approved"]},"owner":[1,2]}},"artifacts":{"app":{"path":"app","views":{"humanTools":{}},"extension":true}},"extension":{"v":2}}),
174 ] {
175 let saved: HumanDefinition = serde_json::from_value(value.clone()).unwrap();
176 assert_eq!(serde_json::to_value(saved).unwrap(), value);
177 }
178 assert!(
179 serde_json::from_value::<HumanDefinition>(json!({"eval":{"id":"not-qualified"}}))
180 .is_err()
181 );
182 let child: ChildIdentity = serde_json::from_value(json!({"pid":7,"startTime":9})).unwrap();
183 assert_eq!(
184 serde_json::to_value(child).unwrap(),
185 json!({"pid":7,"startTime":9})
186 );
187 }
188 #[test]
189 fn validation_preserves_legacy_null_missing_and_extension_fields() {
190 for value in [
191 Value::Null,
192 json!({}),
193 json!({"satisfied":null,"extension":{"owner":true}}),
194 json!({"satisfied":false,"artifacts":[{"id":"app","status":"STALE","fingerprintKind":"script","fingerprint":"v1"}],"evals":[{"id":"app/test","status":"GREEN","blockedBy":["dep","dep/test"]}]}),
195 ] {
196 let typed: Validation = serde_json::from_value(value.clone()).unwrap();
197 assert_eq!(serde_json::to_value(typed).unwrap(), value);
198 }
199 assert!(serde_json::from_value::<Validation>(json!({"evals":[{"id":"bad"}]})).is_err());
200 assert!(serde_json::from_value::<Blocker>(json!("../escape")).is_err());
201 assert_eq!(
202 serde_json::to_value(Blocker::try_from("app/test".to_owned()).unwrap()).unwrap(),
203 json!("app/test")
204 );
205 }
206}