Skip to main content

bijux_cli/contracts/
envelope.rs

1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use super::command::CommandPath;
8
9/// Stable output envelope metadata.
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11pub struct OutputEnvelopeMetaV1 {
12    /// Envelope version identifier.
13    pub version: String,
14    /// Canonical command path.
15    pub command: CommandPath,
16    /// RFC3339 timestamp.
17    pub timestamp: String,
18}
19
20impl OutputEnvelopeMetaV1 {
21    /// Build metadata with required fields.
22    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/// Stable success payload envelope.
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
35pub struct OutputEnvelopeV1 {
36    /// Fixed status marker.
37    pub status: String,
38    /// Command-specific payload.
39    pub data: Value,
40    /// Shared metadata.
41    pub meta: OutputEnvelopeMetaV1,
42}
43
44impl OutputEnvelopeV1 {
45    /// Build a success envelope using fixed status.
46    #[must_use]
47    pub fn success(data: Value, meta: OutputEnvelopeMetaV1) -> Self {
48        Self { status: "ok".to_string(), data, meta }
49    }
50}
51
52/// Stable structured error details.
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
54pub struct ErrorDetailsV1 {
55    /// Stable machine failure identifier.
56    pub failure: Option<String>,
57    /// Arbitrary additional context.
58    #[serde(default)]
59    pub context: BTreeMap<String, Value>,
60}
61
62/// Stable structured error payload.
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
64pub struct ErrorPayloadV1 {
65    /// Stable symbolic error code.
66    pub code: String,
67    /// Human-readable message.
68    pub message: String,
69    /// Error category (`usage`, `validation`, `plugin`, `internal`).
70    pub category: String,
71    /// Structured optional details.
72    #[serde(default)]
73    pub details: Option<ErrorDetailsV1>,
74}
75
76impl ErrorPayloadV1 {
77    /// Build a validated error payload.
78    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/// Stable error envelope.
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
99pub struct ErrorEnvelopeV1 {
100    /// Fixed status marker.
101    pub status: String,
102    /// Structured error payload.
103    pub error: ErrorPayloadV1,
104    /// Shared metadata.
105    pub meta: OutputEnvelopeMetaV1,
106}
107
108impl ErrorEnvelopeV1 {
109    /// Build an error envelope using fixed status.
110    #[must_use]
111    pub fn failure(error: ErrorPayloadV1, meta: OutputEnvelopeMetaV1) -> Self {
112        Self { status: "error".to_string(), error, meta }
113    }
114}
115
116/// Stable command warning record.
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
118pub struct CommandWarningV1 {
119    /// Stable warning code.
120    pub code: String,
121    /// Human-readable warning message.
122    pub message: String,
123}
124
125impl CommandWarningV1 {
126    /// Build a validated warning record.
127    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/// Stable failure class used for machine-readable command failures.
139#[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/// Stable command failure record used by machine-readable command envelopes.
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
152pub struct CommandFailureV1 {
153    /// Stable error code.
154    pub code: String,
155    /// Stable failure class.
156    pub failure_class: CommandFailureClassV1,
157    /// Human-readable failure message.
158    pub message: String,
159    /// Actionable remediation hint.
160    pub remediation_hint: String,
161    /// Optional evidence pointer for logs/artifacts.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    pub evidence_pointer: Option<String>,
164}
165
166impl CommandFailureV1 {
167    /// Build a validated command failure record.
168    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/// Stable machine-readable command envelope contract.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
199pub struct CommandEnvelopeV1 {
200    /// Schema identifier and version.
201    pub schema_version: String,
202    /// Canonical command path.
203    pub command: CommandPath,
204    /// Success flag for command execution.
205    pub success: bool,
206    /// Stable status or error code for script consumers.
207    pub code: String,
208    /// Command-specific response payload.
209    pub data: Value,
210    /// Non-fatal warnings.
211    #[serde(default)]
212    pub warnings: Vec<CommandWarningV1>,
213    /// Fatal error summaries (empty on success).
214    #[serde(default)]
215    pub errors: Vec<CommandFailureV1>,
216    /// RFC3339 timestamp for envelope creation.
217    pub timestamp: String,
218}
219
220impl CommandEnvelopeV1 {
221    /// Build a validated command envelope.
222    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}