use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::command::CommandPath;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct OutputEnvelopeMetaV1 {
pub version: String,
pub command: CommandPath,
pub timestamp: String,
}
impl OutputEnvelopeMetaV1 {
pub fn new(version: &str, command: CommandPath, timestamp: &str) -> Result<Self, String> {
if version.trim().is_empty() {
return Err("meta.version cannot be empty".to_string());
}
if timestamp.trim().is_empty() {
return Err("meta.timestamp cannot be empty".to_string());
}
Ok(Self { version: version.to_string(), command, timestamp: timestamp.to_string() })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct OutputEnvelopeV1 {
pub status: String,
pub data: Value,
pub meta: OutputEnvelopeMetaV1,
}
impl OutputEnvelopeV1 {
#[must_use]
pub fn success(data: Value, meta: OutputEnvelopeMetaV1) -> Self {
Self { status: "ok".to_string(), data, meta }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
pub struct ErrorDetailsV1 {
pub failure: Option<String>,
#[serde(default)]
pub context: BTreeMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ErrorPayloadV1 {
pub code: String,
pub message: String,
pub category: String,
#[serde(default)]
pub details: Option<ErrorDetailsV1>,
}
impl ErrorPayloadV1 {
pub fn new(code: &str, message: &str, category: &str) -> Result<Self, String> {
if code.trim().is_empty() {
return Err("error.code cannot be empty".to_string());
}
if message.trim().is_empty() {
return Err("error.message cannot be empty".to_string());
}
if category.trim().is_empty() {
return Err("error.category cannot be empty".to_string());
}
Ok(Self {
code: code.to_string(),
message: message.to_string(),
category: category.to_string(),
details: None,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ErrorEnvelopeV1 {
pub status: String,
pub error: ErrorPayloadV1,
pub meta: OutputEnvelopeMetaV1,
}
impl ErrorEnvelopeV1 {
#[must_use]
pub fn failure(error: ErrorPayloadV1, meta: OutputEnvelopeMetaV1) -> Self {
Self { status: "error".to_string(), error, meta }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandWarningV1 {
pub code: String,
pub message: String,
}
impl CommandWarningV1 {
pub fn new(code: &str, message: &str) -> Result<Self, String> {
if code.trim().is_empty() {
return Err("warning.code cannot be empty".to_string());
}
if message.trim().is_empty() {
return Err("warning.message cannot be empty".to_string());
}
Ok(Self { code: code.to_string(), message: message.to_string() })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CommandFailureClassV1 {
Parse,
Validation,
Runtime,
Io,
Usage,
Internal,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CommandFailureV1 {
pub code: String,
pub failure_class: CommandFailureClassV1,
pub message: String,
pub remediation_hint: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence_pointer: Option<String>,
}
impl CommandFailureV1 {
pub fn new(
code: &str,
failure_class: CommandFailureClassV1,
message: &str,
remediation_hint: &str,
evidence_pointer: Option<&str>,
) -> Result<Self, String> {
if code.trim().is_empty() {
return Err("errors[].code cannot be empty".to_string());
}
if message.trim().is_empty() {
return Err("errors[].message cannot be empty".to_string());
}
if remediation_hint.trim().is_empty() {
return Err("errors[].remediation_hint cannot be empty".to_string());
}
if evidence_pointer.is_some_and(|value| value.trim().is_empty()) {
return Err("errors[].evidence_pointer cannot be empty when present".to_string());
}
Ok(Self {
code: code.to_string(),
failure_class,
message: message.to_string(),
remediation_hint: remediation_hint.to_string(),
evidence_pointer: evidence_pointer.map(ToString::to_string),
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CommandEnvelopeV1 {
pub schema_version: String,
pub command: CommandPath,
pub success: bool,
pub code: String,
pub data: Value,
#[serde(default)]
pub warnings: Vec<CommandWarningV1>,
#[serde(default)]
pub errors: Vec<CommandFailureV1>,
pub timestamp: String,
}
impl CommandEnvelopeV1 {
pub fn new(
schema_version: &str,
command: CommandPath,
success: bool,
code: &str,
data: Value,
warnings: Vec<CommandWarningV1>,
errors: Vec<CommandFailureV1>,
timestamp: &str,
) -> Result<Self, String> {
if schema_version.trim().is_empty() {
return Err("schema_version cannot be empty".to_string());
}
if code.trim().is_empty() {
return Err("code cannot be empty".to_string());
}
if timestamp.trim().is_empty() {
return Err("timestamp cannot be empty".to_string());
}
if success && !errors.is_empty() {
return Err("success envelopes cannot include errors".to_string());
}
if !success && errors.is_empty() {
return Err("failed envelopes must include at least one error".to_string());
}
Ok(Self {
schema_version: schema_version.to_string(),
command,
success,
code: code.to_string(),
data,
warnings,
errors,
timestamp: timestamp.to_string(),
})
}
}