Skip to main content

proef_engine_hurl/
lib.rs

1//! The proef API engine: [`EngineFactory`]/[`EngineSession`] over **embedded hurl**
2//! (ADR-0001).
3//!
4//! hurl's crates are pinned exactly (`=8.0.1`) and built `--locked` — the crates
5//! break API in minor releases; upgrades go only through the canary + runbook
6//! (ADR-0003, IMPLEMENTATION-PLAN §7).
7//!
8//! The engine claims the `hurl` step kind, contributes doctor checks, and
9//! executes scenario artifacts via the embedded `run_entries` (see the
10//! `session` module source for the adapter mechanics).
11
12mod fragment;
13mod session;
14
15use proef_core::engine::{
16    DoctorCheck, DoctorResult, EngineFactory, EngineSession, FragmentSupport, PayloadProbeError,
17    ScenarioCtx, StepKindSpec,
18};
19use proef_core::error::EngineError;
20
21/// The exact embedded hurl release (kept in lockstep with the Cargo pin —
22/// asserted by `embedded_version_matches_the_cargo_pin`).
23pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
24
25/// A parseable single-entry probe file used by doctor checks and smoke tests.
26const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
27
28/// Step kinds claimed by this engine: the `hurl:` raw block (ADR-0004,
29/// TECH-SPEC §6 — the pack key doubles as the routing kind). The validate hook
30/// is pack validation pass 7's probe parser; the fragment hooks are ADR-0018's
31/// `.hurl` scanner, which reads named entries out of real hurl files.
32const STEP_KINDS: &[StepKindSpec] = &[StepKindSpec {
33    prefix: "hurl",
34    schema: r#"{ "type": "string", "description": "Raw hurl entries; ${…} lowered at author time, {{…}} resolved by hurl at run time" }"#,
35    validate: Some(validate_payload),
36    fragments: Some(FragmentSupport {
37        ext: "hurl",
38        scan: fragment::scan,
39    }),
40}];
41
42/// The compiled-in hurl engine, registered by `proef-cli` (ADR-0002).
43pub struct HurlEngineFactory;
44
45impl EngineFactory for HurlEngineFactory {
46    fn id(&self) -> &'static str {
47        "hurl"
48    }
49
50    fn step_kinds(&self) -> &'static [StepKindSpec] {
51        STEP_KINDS
52    }
53
54    fn doctor(&self) -> Vec<DoctorCheck> {
55        vec![
56            DoctorCheck {
57                name: "embedded hurl",
58                run: check_embedded_version,
59            },
60            DoctorCheck {
61                name: "hurl parser",
62                run: check_parser,
63            },
64            DoctorCheck {
65                name: "libcurl",
66                run: check_libcurl,
67            },
68        ]
69    }
70
71    fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
72        Ok(Box::new(session::HurlSession::open(ctx)?))
73    }
74}
75
76/// Report the pinned hurl release compiled into this binary.
77fn check_embedded_version() -> DoctorResult {
78    DoctorResult::pass(format!(
79        "hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
80    ))
81}
82
83/// Exercise `hurl_core`'s parser on a probe file — proves the parser linkage that
84/// pack loading (M1) and artifact validation (M2) rely on. Because `hurl_core`
85/// links libxml2, a running probe also proves the libxml2 dynamic linkage loads.
86fn check_parser() -> DoctorResult {
87    match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
88        Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
89            "parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
90        ),
91        Ok(file) => DoctorResult::warn(format!(
92            "probe parsed with unexpected entry count {}",
93            file.entries.len()
94        )),
95        Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
96    }
97}
98
99/// Probe-parse a lowered payload with hurl's real parser (pack validation
100/// pass 7 — the seam hook on [`StepKindSpec`]).
101fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
102    let mut normalized = text.to_owned();
103    if !normalized.ends_with('\n') {
104        normalized.push('\n');
105    }
106    match hurl_core::parser::parse_hurl_file(&normalized) {
107        // A parseable payload with zero entries (only comments or blank
108        // lines) would execute nothing while the step reports green — reject
109        // it at load so dry-run and execution agree.
110        Ok(file) if file.entries.is_empty() => Err(PayloadProbeError {
111            line: 1,
112            column: 1,
113            message: "contains no hurl entries (only comments or blank lines)".to_owned(),
114        }),
115        Ok(_) => Ok(()),
116        Err(err) => Err(PayloadProbeError {
117            line: err.pos.line,
118            column: err.pos.column,
119            message: format!("{:?}", err.kind),
120        }),
121    }
122}
123
124/// Report the libcurl this binary is linked against (mirrors `curl --version`) —
125/// the library `run_entries` drives during execution.
126fn check_libcurl() -> DoctorResult {
127    let info = hurl::http::libcurl_version_info();
128    if info.libraries.is_empty() {
129        return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
130    }
131    DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
132}
133
134#[cfg(test)]
135mod tests {
136    #![allow(clippy::unwrap_used)]
137
138    use super::*;
139
140    /// The seam facts this engine relies on are pinned (ADR-0001): parseable
141    /// probe, one entry, exact crate version.
142    #[test]
143    fn parser_seam_smoke() {
144        let result = check_parser();
145        assert_eq!(
146            result.status,
147            proef_core::engine::DoctorStatus::Pass,
148            "{}",
149            result.detail
150        );
151    }
152
153    #[test]
154    fn factory_claims_the_hurl_step_kind() {
155        let factory = HurlEngineFactory;
156        assert_eq!(factory.id(), "hurl");
157        assert_eq!(factory.step_kinds().len(), 1);
158        assert_eq!(factory.step_kinds()[0].prefix, "hurl");
159    }
160
161    /// The doc on [`EMBEDDED_HURL_VERSION`] promises lockstep with the Cargo
162    /// pin — this is that assertion: a runbook pin bump that forgets the
163    /// const (or vice versa) fails here, not at doctor time mid-upgrade.
164    #[test]
165    fn embedded_version_matches_the_cargo_pin() {
166        let manifest = std::fs::read_to_string(
167            std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"),
168        )
169        .unwrap();
170        for dep in ["hurl", "hurl_core"] {
171            let needle = format!("{dep} = \"={EMBEDDED_HURL_VERSION}\"");
172            assert!(
173                manifest.contains(&needle),
174                "workspace Cargo.toml must contain `{needle}` (ADR-0003 lockstep)"
175            );
176        }
177    }
178
179    #[test]
180    fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
181        assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
182        // An unknown response section name is unambiguously invalid hurl.
183        let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
184        assert!(
185            err.line >= 2,
186            "position should be near the broken section: {err:?}"
187        );
188    }
189}