Skip to main content

nmbrs_workload/
drivers.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! SRD-109 Part 2 — driver manifests.
5//!
6//! A driver manifest gives an SRD-108 implementation library the
7//! ergonomics of a built-in adapter: `driver=<name>` resolves the
8//! manifest, which names the backing adapter, the implementation
9//! library workload, and default params (lowest precedence). The
10//! manifest maps NAMES to templates and defaults — it never
11//! defines op fields; the op surface remains exactly the backing
12//! adapter's, so every request stays literal and
13//! operator-visible.
14//!
15//! ```yaml
16//! # drivers/<name>/driver.yaml
17//! driver: vendorx
18//! adapter: http
19//! library: vector_impl        # sibling workload (stem or file)
20//! description: VendorX REST vector client (native HTTP op forms)
21//! defaults:
22//!   params:
23//!     base_url: "http://localhost:8099"
24//! ```
25//!
26//! Discovery is local-first then bundled, mirroring workload
27//! resolution: `./drivers/<name>/driver.yaml` under the invoking
28//! cwd wins; otherwise the bundled catalog entry
29//! `drivers/<name>/driver`. Resolution policy lives with the
30//! runner: nearest-first — a name resolving both ways favors the
31//! local manifest with a logged warning naming both (shadowing is
32//! allowed but never silent).
33
34use std::collections::BTreeMap;
35
36/// A parsed driver manifest.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct DriverManifest {
39    /// The driver's name — must match the directory it lives in.
40    pub driver: String,
41    /// The backing adapter (`http` for SRD-109 web drivers).
42    pub adapter: String,
43    /// The implementation library workload, as written: a sibling
44    /// stem (`vector_impl`) or file name (`vector_impl.yaml`).
45    /// The runner resolves it against the manifest's home
46    /// (directory for local manifests, catalog namespace for
47    /// bundled ones).
48    pub library: String,
49    /// One-line description for `describe drivers`.
50    pub description: Option<String>,
51    /// Default params, applied at LOWEST precedence: a CLI param
52    /// wins; these in turn overlay the library's own declared
53    /// param defaults.
54    pub default_params: BTreeMap<String, String>,
55}
56
57/// Parse a driver manifest from its yaml source. Unknown
58/// top-level keys and malformed sections are errors — never
59/// silently ignored.
60pub fn parse_driver_manifest(source: &str, origin: &str) -> Result<DriverManifest, String> {
61    let doc: serde_json::Value =
62        serde_yaml::from_str(source).map_err(|e| format!("driver manifest {origin}: {e}"))?;
63    let obj = doc
64        .as_object()
65        .ok_or_else(|| format!("driver manifest {origin}: top level must be a mapping"))?;
66
67    let mut driver = None;
68    let mut adapter = None;
69    let mut library = None;
70    let mut description = None;
71    let mut default_params = BTreeMap::new();
72
73    for (k, v) in obj {
74        match k.as_str() {
75            "driver" => driver = Some(string_field(v, k, origin)?),
76            "adapter" => adapter = Some(string_field(v, k, origin)?),
77            "library" => library = Some(string_field(v, k, origin)?),
78            "description" => description = Some(string_field(v, k, origin)?),
79            "defaults" => {
80                let d = v.as_object().ok_or_else(|| {
81                    format!("driver manifest {origin}: `defaults:` must be a mapping")
82                })?;
83                for (dk, dv) in d {
84                    match dk.as_str() {
85                        "params" => {
86                            let p = dv.as_object().ok_or_else(|| {
87                                format!(
88                                    "driver manifest {origin}: `defaults.params:` \
89                                 must be a mapping"
90                                )
91                            })?;
92                            for (pk, pv) in p {
93                                let s = match pv {
94                                    serde_json::Value::String(s) => s.clone(),
95                                    other => other.to_string(),
96                                };
97                                default_params.insert(pk.clone(), s);
98                            }
99                        }
100                        other => {
101                            return Err(format!(
102                                "driver manifest {origin}: unknown key \
103                             `defaults.{other}` (allowed: params)"
104                            ));
105                        }
106                    }
107                }
108            }
109            other => {
110                return Err(format!(
111                    "driver manifest {origin}: unknown top-level key `{other}` \
112                 (allowed: driver, adapter, library, description, defaults)"
113                ));
114            }
115        }
116    }
117
118    Ok(DriverManifest {
119        driver: driver.ok_or_else(|| format!("driver manifest {origin}: missing `driver:`"))?,
120        adapter: adapter.ok_or_else(|| format!("driver manifest {origin}: missing `adapter:`"))?,
121        library: library.ok_or_else(|| format!("driver manifest {origin}: missing `library:`"))?,
122        description: description.map(|s| s.trim().to_string()),
123        default_params,
124    })
125}
126
127fn string_field(v: &serde_json::Value, key: &str, origin: &str) -> Result<String, String> {
128    v.as_str()
129        .map(str::to_string)
130        .ok_or_else(|| format!("driver manifest {origin}: `{key}:` must be a string, got {v}"))
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parses_full_manifest() {
139        let m = parse_driver_manifest(
140            r#"
141driver: vendorx
142adapter: http
143library: vector_impl
144description: VendorX REST vector client
145defaults:
146  params:
147    base_url: "http://localhost:8099"
148    api_key: ""
149"#,
150            "<test>",
151        )
152        .unwrap();
153        assert_eq!(m.driver, "vendorx");
154        assert_eq!(m.adapter, "http");
155        assert_eq!(m.library, "vector_impl");
156        assert_eq!(
157            m.default_params.get("base_url").map(String::as_str),
158            Some("http://localhost:8099")
159        );
160    }
161
162    #[test]
163    fn unknown_keys_are_rejected() {
164        let err =
165            parse_driver_manifest("driver: x\nadapter: http\nlibrary: l\nops: {}\n", "<test>")
166                .unwrap_err();
167        assert!(err.contains("unknown top-level key `ops`"), "err: {err}");
168    }
169
170    #[test]
171    fn missing_required_fields_are_named() {
172        let err = parse_driver_manifest("driver: x\nadapter: http\n", "<test>").unwrap_err();
173        assert!(err.contains("missing `library:`"), "err: {err}");
174    }
175}