Skip to main content

krabka_sdk_conformance/
vectors.rs

1//! File-backed conformance vector loader.
2
3use std::{path::Path, str::FromStr};
4
5use serde::{Deserialize, Serialize};
6
7use crate::protocol::{Command, Response};
8
9/// Contract major/minor pair used for vector selection.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
11pub struct ContractVersion {
12    /// Contract major.
13    pub major: u16,
14    /// Contract minor.
15    pub minor: u16,
16}
17
18impl ContractVersion {
19    /// Create a contract version.
20    #[must_use]
21    pub const fn new(major: u16, minor: u16) -> Self {
22        Self { major, minor }
23    }
24
25    /// Return true when this version is inside the vector's supported range.
26    #[must_use]
27    pub fn satisfies(self, vector: &Vector) -> bool {
28        if self < vector.since {
29            return false;
30        }
31        if let Some(until) = vector.until {
32            return self <= until;
33        }
34        true
35    }
36}
37
38/// A contract vector composed of ordered adapter commands and expectations.
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct Vector {
41    /// Stable vector id.
42    pub id: String,
43    /// First contract version that must run this vector.
44    pub since: ContractVersion,
45    /// Last contract version that must run this vector.
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub until: Option<ContractVersion>,
48    /// Whether this vector requires the harness's real broker and gateway substrate.
49    #[serde(default)]
50    pub live_only: bool,
51    /// Ordered steps for this vector.
52    pub steps: Vec<VectorStep>,
53}
54
55/// One command/expectation pair inside a vector.
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct VectorStep {
58    /// Human-readable step name used in diagnostics.
59    pub name: String,
60    /// Command sent to the adapter.
61    pub command: Command,
62    /// Exact response expected from the adapter.
63    pub expect: Response,
64}
65
66/// Load all JSON vectors from a directory in deterministic filename order.
67///
68/// # Errors
69///
70/// Returns an error when the directory or a vector file cannot be read, or a
71/// vector file contains invalid JSON.
72pub fn load_vectors(dir: impl AsRef<Path>) -> Result<Vec<Vector>, VectorError> {
73    let dir = dir.as_ref();
74    let mut entries = std::fs::read_dir(dir)?
75        .map(|entry| entry.map(|entry| entry.path()))
76        .collect::<Result<Vec<_>, _>>()?;
77    entries.sort();
78    let mut vectors = Vec::with_capacity(entries.len());
79    for path in entries {
80        if path.extension().and_then(std::ffi::OsStr::to_str) != Some("json") {
81            continue;
82        }
83        let text = std::fs::read_to_string(&path)?;
84        let vector =
85            serde_json::from_str::<Vector>(&text).map_err(|source| VectorError::JsonFile {
86                path: path.clone(),
87                source,
88            })?;
89        vectors.push(vector);
90    }
91    Ok(vectors)
92}
93
94/// Parse an embedded vector JSON document.
95///
96/// # Errors
97///
98/// Returns an error when `text` is not a valid vector JSON document.
99pub fn parse_vector(text: &str) -> Result<Vector, serde_json::Error> {
100    serde_json::from_str(text)
101}
102
103/// Errors returned while loading vectors.
104#[derive(Debug, thiserror::Error)]
105#[non_exhaustive]
106pub enum VectorError {
107    /// Directory or file read failed.
108    #[error("vector io: {0}")]
109    Io(#[from] std::io::Error),
110    /// JSON in a named vector file failed to decode.
111    #[error("vector json in {}: {source}", path.display())]
112    JsonFile {
113        /// File path.
114        path: std::path::PathBuf,
115        /// JSON error.
116        source: serde_json::Error,
117    },
118}
119
120impl FromStr for Vector {
121    type Err = serde_json::Error;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        serde_json::from_str(s)
125    }
126}