Skip to main content

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 {}