buildfix_adapter_sdk/lib.rs
1//! SDK for building intake adapters that convert sensor outputs to buildfix receipts.
2//!
3//! This crate provides the `Adapter` trait and utilities for implementing new sensor
4//! intake adapters. An adapter transforms a sensor's native output format into the
5//! standardized `ReceiptEnvelope` format that buildfix expects.
6//!
7//! # Creating a New Adapter
8//!
9//! To create a new adapter, implement the `Adapter` trait for your sensor-specific
10//! adapter struct. The adapter is responsible for:
11//!
12//! 1. **Identifying the sensor** via `sensor_id()` - returns a unique string like
13//! `"cargo-deny"` or `"clippy"`
14//!
15//! 2. **Loading sensor output** via `load()` - reads and parses the sensor's
16//! output file into a `ReceiptEnvelope`
17//!
18//! ## Example
19//!
20//! ```ignore
21//! use buildfix_adapter_sdk::{Adapter, AdapterError, ReceiptBuilder};
22//! use buildfix_types::receipt::{ReceiptEnvelope, Severity, VerdictStatus};
23//! use std::path::Path;
24//!
25//! pub struct MySensorAdapter {
26//! sensor_id: String,
27//! }
28//!
29//! impl MySensorAdapter {
30//! pub fn new() -> Self {
31//! Self {
32//! sensor_id: "my-sensor".to_string(),
33//! }
34//! }
35//! }
36//!
37//! impl Adapter for MySensorAdapter {
38//! fn sensor_id(&self) -> &str {
39//! &self.sensor_id
40//! }
41//!
42//! fn load(&self, path: &Path) -> Result<ReceiptEnvelope, AdapterError> {
43//! // Parse your sensor's output format and convert to ReceiptEnvelope
44//! let output = std::fs::read_to_string(path)
45//! .map_err(AdapterError::Io)?;
46//!
47//! let parsed = serde_json::from_str::<serde_json::Value>(&output)
48//! .map_err(AdapterError::Json)?;
49//!
50//! // Convert to ReceiptEnvelope using ReceiptBuilder
51//! let envelope = ReceiptBuilder::new("my-sensor")
52//! .with_status(VerdictStatus::Fail)
53//! .build();
54//!
55//! Ok(envelope)
56//! }
57//! }
58//! ```
59//!
60//! # Testing Adapters
61//!
62//! Use `AdapterTestHarness` to validate your adapter implementation:
63//!
64//! ```ignore
65//! use buildfix_adapter_sdk::AdapterTestHarness;
66//! use my_adapter::MySensorAdapter;
67//!
68//! #[test]
69//! fn test_adapter_loads_receipt() {
70//! let harness = AdapterTestHarness::new(MySensorAdapter::new());
71//! harness.validate_receipt_fixture("tests/fixtures/my-sensor/report.json")
72//! .expect("receipt should load correctly");
73//! }
74//! ```
75
76pub mod receipt_builder;
77
78pub use receipt_builder::ReceiptBuilder;
79
80mod harness;
81
82pub use harness::{AdapterTestHarness, MetadataValidationError, ValidationResult};
83
84use buildfix_types::receipt::ReceiptEnvelope;
85use std::path::Path;
86use thiserror::Error;
87
88#[derive(Error, Debug)]
89pub enum AdapterError {
90 #[error("IO error: {0}")]
91 Io(#[from] std::io::Error),
92
93 #[error("JSON parse error: {0}")]
94 Json(#[from] serde_json::Error),
95
96 #[error("Invalid sensor output: {0}")]
97 InvalidFormat(String),
98
99 #[error("Required field missing: {0}")]
100 MissingField(String),
101}
102
103pub trait Adapter: Send + Sync {
104 fn sensor_id(&self) -> &str;
105
106 fn load(&self, path: &Path) -> Result<ReceiptEnvelope, AdapterError>;
107}
108
109impl<T: Adapter + ?Sized> Adapter for &T {
110 fn sensor_id(&self) -> &str {
111 (*self).sensor_id()
112 }
113
114 fn load(&self, path: &Path) -> Result<ReceiptEnvelope, AdapterError> {
115 (*self).load(path)
116 }
117}
118
119impl<T: Adapter + ?Sized> Adapter for Box<T> {
120 fn sensor_id(&self) -> &str {
121 (**self).sensor_id()
122 }
123
124 fn load(&self, path: &Path) -> Result<ReceiptEnvelope, AdapterError> {
125 (**self).load(path)
126 }
127}
128
129/// Metadata trait for adapter self-description.
130///
131/// Adapters should implement this trait to provide information about
132/// their capabilities and version compatibility. This enables runtime
133/// discovery and validation of adapter compatibility with the buildfix
134/// system.
135///
136/// # Example
137///
138/// ```
139/// use buildfix_adapter_sdk::AdapterMetadata;
140///
141/// pub struct CargoDenyAdapter;
142///
143/// impl AdapterMetadata for CargoDenyAdapter {
144/// fn name(&self) -> &str {
145/// "cargo-deny"
146/// }
147///
148/// fn version(&self) -> &str {
149/// env!("CARGO_PKG_VERSION")
150/// }
151///
152/// fn supported_schemas(&self) -> &[&str] {
153/// &["cargo-deny.report.v1", "cargo-deny.report.v2"]
154/// }
155/// }
156/// ```
157pub trait AdapterMetadata {
158 /// Returns the adapter name (e.g., "cargo-deny", "sarif").
159 ///
160 /// This should be a unique, stable identifier for the adapter type.
161 /// Convention is to use kebab-case matching the sensor tool name.
162 fn name(&self) -> &str;
163
164 /// Returns the adapter version (e.g., env!("CARGO_PKG_VERSION")).
165 ///
166 /// This should return the semantic version of the adapter crate,
167 /// typically using the `CARGO_PKG_VERSION` environment variable.
168 fn version(&self) -> &str;
169
170 /// Returns the list of schema versions this adapter supports.
171 ///
172 /// Format: "sensor.report.v1" style strings. This allows the system
173 /// to validate that a receipt schema is compatible with this adapter.
174 ///
175 /// Adapters should list all schema versions they can successfully
176 /// parse, enabling backward compatibility checks.
177 fn supported_schemas(&self) -> &[&str];
178}
179
180/// Sealed trait marker for internal use.
181///
182/// This prevents external implementations of [`AdapterExt`] while allowing
183/// blanket implementations for all types that meet the requirements.
184mod sealed {
185 pub trait Sealed {}
186}
187
188use sealed::Sealed;
189
190impl<T: Adapter + AdapterMetadata> Sealed for T {}
191
192/// Extension trait for adapters with metadata.
193///
194/// This trait provides additional functionality for adapters that implement
195/// both [`Adapter`] and [`AdapterMetadata`]. It is automatically implemented
196/// for all qualifying types via a blanket implementation.
197///
198/// # Example
199///
200/// ```ignore
201/// use buildfix_adapter_sdk::{Adapter, AdapterMetadata, AdapterExt};
202///
203/// fn validate_adapter<A>(adapter: &A) -> bool
204/// where
205/// A: Adapter + AdapterMetadata,
206/// {
207/// // Check if adapter supports the required schema
208/// adapter.supports_schema("cargo-deny.report.v1")
209/// }
210/// ```
211pub trait AdapterExt: Adapter + AdapterMetadata + Sealed {
212 /// Validates that this adapter supports the given schema.
213 ///
214 /// # Arguments
215 ///
216 /// * `schema` - The schema version string to check (e.g., "cargo-deny.report.v1")
217 ///
218 /// # Returns
219 ///
220 /// `true` if the schema is in the adapter's supported list, `false` otherwise.
221 ///
222 /// # Example
223 ///
224 /// ```ignore
225 /// let adapter = CargoDenyAdapter::new();
226 /// assert!(adapter.supports_schema("cargo-deny.report.v1"));
227 /// assert!(!adapter.supports_schema("unknown.schema.v1"));
228 /// ```
229 fn supports_schema(&self, schema: &str) -> bool {
230 self.supported_schemas().contains(&schema)
231 }
232}
233
234impl<T: Adapter + AdapterMetadata> AdapterExt for T {}