use serde::Serialize;
pub const SCHEMA: &str = "enprot/v1";
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
pub enum OutputFormat {
#[default]
Text,
Json,
}
#[derive(Serialize)]
pub struct Envelope<'a, T: Serialize> {
#[serde(rename = "$schema")]
schema: &'a str,
#[serde(flatten)]
data: &'a T,
}
pub fn to_json<T: Serialize>(data: &T) -> crate::error::Result<String> {
let env = Envelope {
schema: SCHEMA,
data,
};
serde_json::to_string_pretty(&env).map_err(crate::error::Error::json)
}
#[derive(Serialize)]
pub struct CapabilityDto {
pub tier: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub word: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_fp: Option<String>,
}
#[derive(Serialize)]
pub struct CapabilitiesOutput {
pub capabilities: Vec<CapabilityDto>,
}
#[derive(Serialize)]
pub struct ListNode {
#[serde(rename = "type")]
pub kind: &'static str,
pub word: String,
pub depth: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub cipher: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pbkdf: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cas: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub payload: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub children: Vec<ListNode>,
}
#[derive(Serialize)]
pub struct FileListing {
pub path: String,
pub nodes: Vec<ListNode>,
}
#[derive(Serialize)]
pub struct ListOutput {
pub files: Vec<FileListing>,
}
#[derive(Serialize)]
pub struct ForkPoint {
pub anchor: String,
pub parents: Vec<String>,
}
#[derive(Serialize)]
pub struct VerifyError {
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor: Option<String>,
pub message: String,
}
#[derive(Serialize)]
pub struct VerifyChainFileReport {
pub path: String,
pub ok: bool,
pub anchors_total: usize,
pub signers: Vec<String>,
pub forks: Vec<ForkPoint>,
pub errors: Vec<VerifyError>,
}
#[derive(Serialize)]
pub struct VerifyChainOutput {
pub ok: bool,
pub files: Vec<VerifyChainFileReport>,
}
#[derive(Serialize)]
pub struct ConflictEntry {
pub word: String,
pub ours_nodes: usize,
pub theirs_nodes: usize,
}
#[derive(Serialize)]
pub struct ConflictsOutput {
pub conflicts: Vec<ConflictEntry>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn envelope_stamps_schema() {
let caps = CapabilitiesOutput {
capabilities: vec![CapabilityDto {
tier: "viewer",
word: None,
key_fp: None,
}],
};
let s = to_json(&caps).unwrap();
assert!(s.contains("\"$schema\": \"enprot/v1\""), "got: {s}");
assert!(s.contains("\"tier\": \"viewer\""));
}
#[test]
fn capability_dto_omits_empty_optional_fields() {
let dto = CapabilityDto {
tier: "viewer",
word: None,
key_fp: None,
};
let s = serde_json::to_string(&dto).unwrap();
assert!(!s.contains("word"));
assert!(!s.contains("key_fp"));
}
#[test]
fn list_node_omits_empty_children() {
let n = ListNode {
kind: "encrypted",
word: "Agent_007".into(),
depth: 0,
cipher: Some("aes-256-siv".into()),
pbkdf: None,
cas: None,
signer: None,
payload: None,
children: vec![],
};
let s = serde_json::to_string(&n).unwrap();
assert!(!s.contains("children"), "got: {s}");
}
#[test]
fn verify_chain_output_includes_overall_ok() {
let out = VerifyChainOutput {
ok: true,
files: vec![VerifyChainFileReport {
path: "a.ept".into(),
ok: true,
anchors_total: 1,
signers: vec!["ed25519:abcd".into()],
forks: vec![],
errors: vec![],
}],
};
let s = to_json(&out).unwrap();
assert!(s.contains("\"ok\": true"));
assert!(s.contains("\"anchors_total\": 1"));
}
}