1use crate::ctx::{InputRecord, ModelRecord, OutputRecord, WarningRecord};
4use crate::error::Error;
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7use sha2::{Digest, Sha256};
8use std::io::Read;
9use std::path::Path;
10
11pub const SCHEMA_VERSION: u32 = 1;
12pub const FERRAUS_VERSION: &str = env!("CARGO_PKG_VERSION");
13
14#[derive(Clone, Debug, Serialize)]
16pub struct Envelope {
17 pub ok: bool,
18 pub command: String,
19 pub ferraus_version: String,
20 pub schema_version: u32,
21 pub inputs: Vec<InputRecord>,
22 pub outputs: Vec<OutputRecord>,
23 pub params: Map<String, Value>,
24 pub models: Vec<ModelRecord>,
25 pub metrics: Map<String, Value>,
26 pub warnings: Vec<WarningRecord>,
27 #[serde(skip_serializing_if = "Value::is_null")]
28 pub result: Value,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub error: Option<Error>,
31 pub timing_ms: u64,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub dry_run: Option<bool>,
34}
35
36impl Envelope {
37 pub fn error(command: &str, e: Error, timing_ms: u64) -> Envelope {
38 Envelope {
39 ok: false,
40 command: command.to_string(),
41 ferraus_version: FERRAUS_VERSION.to_string(),
42 schema_version: SCHEMA_VERSION,
43 inputs: vec![],
44 outputs: vec![],
45 params: Map::new(),
46 models: vec![],
47 metrics: Map::new(),
48 warnings: vec![],
49 result: Value::Null,
50 error: Some(e),
51 timing_ms,
52 dry_run: None,
53 }
54 }
55
56 pub fn exit_code(&self) -> i32 {
57 self.error.as_ref().map(|e| e.exit_code).unwrap_or(0)
58 }
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct HostInfo {
63 pub os: String,
64 pub arch: String,
65 pub device: String,
66 pub threads: usize,
67}
68
69impl HostInfo {
70 pub fn current(device: &str, threads: usize) -> HostInfo {
71 HostInfo {
72 os: std::env::consts::OS.into(),
73 arch: std::env::consts::ARCH.into(),
74 device: device.into(),
75 threads,
76 }
77 }
78}
79
80#[derive(Clone, Debug, Serialize, Deserialize)]
81pub struct HashedFile {
82 pub path: String,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub sha256: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
86 pub duration_s: Option<f64>,
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub role: Option<String>,
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize)]
93pub struct Sidecar {
94 pub ferraus_version: String,
95 pub schema_version: u32,
96 pub command: String,
97 pub argv: Vec<String>,
98 pub params: Map<String, Value>,
99 pub inputs: Vec<HashedFile>,
100 pub outputs: Vec<HashedFile>,
101 pub models: Vec<Value>,
102 pub plugins: Vec<Value>,
103 pub host: HostInfo,
104 pub deterministic: Value,
106 #[serde(default)]
107 pub warnings: Vec<Value>,
108 pub timing_ms: u64,
109}
110
111impl Sidecar {
112 pub fn path_for(output: &Path) -> std::path::PathBuf {
113 std::path::PathBuf::from(format!("{}.ferraus.json", output.display()))
114 }
115}
116
117pub fn sha256_file(path: &Path) -> std::io::Result<String> {
119 let mut f = std::fs::File::open(path)?;
120 let mut h = Sha256::new();
121 let mut buf = vec![0u8; 1 << 16];
122 loop {
123 let n = f.read(&mut buf)?;
124 if n == 0 {
125 break;
126 }
127 h.update(&buf[..n]);
128 }
129 Ok(hex::encode(h.finalize()))
130}
131
132pub fn sha256_bytes(b: &[u8]) -> String {
133 hex::encode(Sha256::digest(b))
134}