1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::command::CommandPath;
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11pub struct OutputEnvelopeMetaV1 {
12 pub version: String,
14 pub command: CommandPath,
16 pub timestamp: String,
18}
19
20impl OutputEnvelopeMetaV1 {
21 pub fn new(version: &str, command: CommandPath, timestamp: &str) -> Result<Self, String> {
23 if version.trim().is_empty() {
24 return Err("meta.version cannot be empty".to_string());
25 }
26 if timestamp.trim().is_empty() {
27 return Err("meta.timestamp cannot be empty".to_string());
28 }
29 Ok(Self { version: version.to_string(), command, timestamp: timestamp.to_string() })
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
35pub struct OutputEnvelopeV1 {
36 pub status: String,
38 pub data: Value,
40 pub meta: OutputEnvelopeMetaV1,
42}
43
44impl OutputEnvelopeV1 {
45 #[must_use]
47 pub fn success(data: Value, meta: OutputEnvelopeMetaV1) -> Self {
48 Self { status: "ok".to_string(), data, meta }
49 }
50}
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
54pub struct ErrorDetailsV1 {
55 pub failure: Option<String>,
57 #[serde(default)]
59 pub context: BTreeMap<String, Value>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
64pub struct ErrorPayloadV1 {
65 pub code: String,
67 pub message: String,
69 pub category: String,
71 #[serde(default)]
73 pub details: Option<ErrorDetailsV1>,
74}
75
76impl ErrorPayloadV1 {
77 pub fn new(code: &str, message: &str, category: &str) -> Result<Self, String> {
79 if code.trim().is_empty() {
80 return Err("error.code cannot be empty".to_string());
81 }
82 if message.trim().is_empty() {
83 return Err("error.message cannot be empty".to_string());
84 }
85 if category.trim().is_empty() {
86 return Err("error.category cannot be empty".to_string());
87 }
88 Ok(Self {
89 code: code.to_string(),
90 message: message.to_string(),
91 category: category.to_string(),
92 details: None,
93 })
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
99pub struct ErrorEnvelopeV1 {
100 pub status: String,
102 pub error: ErrorPayloadV1,
104 pub meta: OutputEnvelopeMetaV1,
106}
107
108impl ErrorEnvelopeV1 {
109 #[must_use]
111 pub fn failure(error: ErrorPayloadV1, meta: OutputEnvelopeMetaV1) -> Self {
112 Self { status: "error".to_string(), error, meta }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
118pub struct CommandWarningV1 {
119 pub code: String,
121 pub message: String,
123}
124
125impl CommandWarningV1 {
126 pub fn new(code: &str, message: &str) -> Result<Self, String> {
128 if code.trim().is_empty() {
129 return Err("warning.code cannot be empty".to_string());
130 }
131 if message.trim().is_empty() {
132 return Err("warning.message cannot be empty".to_string());
133 }
134 Ok(Self { code: code.to_string(), message: message.to_string() })
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
140#[serde(rename_all = "snake_case")]
141pub enum CommandFailureClassV1 {
142 Parse,
143 Validation,
144 Runtime,
145 Io,
146 Usage,
147 Internal,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152pub struct CommandFailureV1 {
153 pub code: String,
155 pub failure_class: CommandFailureClassV1,
157 pub message: String,
159 pub remediation_hint: String,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub evidence_pointer: Option<String>,
164}
165
166impl CommandFailureV1 {
167 pub fn new(
169 code: &str,
170 failure_class: CommandFailureClassV1,
171 message: &str,
172 remediation_hint: &str,
173 evidence_pointer: Option<&str>,
174 ) -> Result<Self, String> {
175 if code.trim().is_empty() {
176 return Err("errors[].code cannot be empty".to_string());
177 }
178 if message.trim().is_empty() {
179 return Err("errors[].message cannot be empty".to_string());
180 }
181 if remediation_hint.trim().is_empty() {
182 return Err("errors[].remediation_hint cannot be empty".to_string());
183 }
184 if evidence_pointer.is_some_and(|value| value.trim().is_empty()) {
185 return Err("errors[].evidence_pointer cannot be empty when present".to_string());
186 }
187 Ok(Self {
188 code: code.to_string(),
189 failure_class,
190 message: message.to_string(),
191 remediation_hint: remediation_hint.to_string(),
192 evidence_pointer: evidence_pointer.map(ToString::to_string),
193 })
194 }
195}
196
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
199pub struct CommandEnvelopeV1 {
200 pub schema_version: String,
202 pub command: CommandPath,
204 pub success: bool,
206 pub code: String,
208 pub data: Value,
210 #[serde(default)]
212 pub warnings: Vec<CommandWarningV1>,
213 #[serde(default)]
215 pub errors: Vec<CommandFailureV1>,
216 pub timestamp: String,
218}
219
220impl CommandEnvelopeV1 {
221 pub fn new(
223 schema_version: &str,
224 command: CommandPath,
225 success: bool,
226 code: &str,
227 data: Value,
228 warnings: Vec<CommandWarningV1>,
229 errors: Vec<CommandFailureV1>,
230 timestamp: &str,
231 ) -> Result<Self, String> {
232 if schema_version.trim().is_empty() {
233 return Err("schema_version cannot be empty".to_string());
234 }
235 if code.trim().is_empty() {
236 return Err("code cannot be empty".to_string());
237 }
238 if timestamp.trim().is_empty() {
239 return Err("timestamp cannot be empty".to_string());
240 }
241 if success && !errors.is_empty() {
242 return Err("success envelopes cannot include errors".to_string());
243 }
244 if !success && errors.is_empty() {
245 return Err("failed envelopes must include at least one error".to_string());
246 }
247 Ok(Self {
248 schema_version: schema_version.to_string(),
249 command,
250 success,
251 code: code.to_string(),
252 data,
253 warnings,
254 errors,
255 timestamp: timestamp.to_string(),
256 })
257 }
258}