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 session;
13
14use proef_core::engine::{
15    DoctorCheck, DoctorResult, EngineFactory, EngineSession, PayloadProbeError, ScenarioCtx,
16    StepKindSpec,
17};
18use proef_core::error::EngineError;
19
20/// The exact embedded hurl release (kept in lockstep with the Cargo pin —
21/// asserted by the seam smoke test).
22pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
23
24/// A parseable single-entry probe file used by doctor checks and smoke tests.
25const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
26
27/// Step kinds claimed by this engine: the `hurl:` raw block (ADR-0004,
28/// TECH-SPEC §6 — the pack key doubles as the routing kind). The validate hook
29/// is pack validation pass 7's probe parser.
30const STEP_KINDS: &[StepKindSpec] = &[StepKindSpec {
31    prefix: "hurl",
32    schema: r#"{ "type": "string", "description": "Raw hurl entries; ${…} lowered at author time, {{…}} resolved by hurl at run time" }"#,
33    validate: Some(validate_payload),
34}];
35
36/// The compiled-in hurl engine, registered by `proef-cli` (ADR-0002).
37pub struct HurlEngineFactory;
38
39impl EngineFactory for HurlEngineFactory {
40    fn id(&self) -> &'static str {
41        "hurl"
42    }
43
44    fn step_kinds(&self) -> &'static [StepKindSpec] {
45        STEP_KINDS
46    }
47
48    fn doctor(&self) -> Vec<DoctorCheck> {
49        vec![
50            DoctorCheck {
51                name: "embedded hurl",
52                run: check_embedded_version,
53            },
54            DoctorCheck {
55                name: "hurl parser",
56                run: check_parser,
57            },
58            DoctorCheck {
59                name: "libcurl",
60                run: check_libcurl,
61            },
62        ]
63    }
64
65    fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
66        Ok(Box::new(session::HurlSession::open(ctx)?))
67    }
68}
69
70/// Report the pinned hurl release compiled into this binary.
71fn check_embedded_version() -> DoctorResult {
72    DoctorResult::pass(format!(
73        "hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
74    ))
75}
76
77/// Exercise `hurl_core`'s parser on a probe file — proves the parser linkage that
78/// pack loading (M1) and artifact validation (M2) rely on. Because `hurl_core`
79/// links libxml2, a running probe also proves the libxml2 dynamic linkage loads.
80fn check_parser() -> DoctorResult {
81    match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
82        Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
83            "parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
84        ),
85        Ok(file) => DoctorResult::warn(format!(
86            "probe parsed with unexpected entry count {}",
87            file.entries.len()
88        )),
89        Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
90    }
91}
92
93/// Probe-parse a lowered payload with hurl's real parser (pack validation
94/// pass 7 — the seam hook on [`StepKindSpec`]).
95fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
96    let mut normalized = text.to_owned();
97    if !normalized.ends_with('\n') {
98        normalized.push('\n');
99    }
100    match hurl_core::parser::parse_hurl_file(&normalized) {
101        Ok(_) => Ok(()),
102        Err(err) => Err(PayloadProbeError {
103            line: err.pos.line,
104            column: err.pos.column,
105            message: format!("{:?}", err.kind),
106        }),
107    }
108}
109
110/// Report the libcurl this binary is linked against (mirrors `curl --version`) —
111/// the library `run_entries` drives at M3.
112fn check_libcurl() -> DoctorResult {
113    let info = hurl::http::libcurl_version_info();
114    if info.libraries.is_empty() {
115        return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
116    }
117    DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
118}
119
120#[cfg(test)]
121mod tests {
122    #![allow(clippy::unwrap_used)]
123
124    use super::*;
125
126    /// The seam facts this engine relies on are pinned (ADR-0001): parseable
127    /// probe, one entry, exact crate version.
128    #[test]
129    fn parser_seam_smoke() {
130        let result = check_parser();
131        assert_eq!(
132            result.status,
133            proef_core::engine::DoctorStatus::Pass,
134            "{}",
135            result.detail
136        );
137    }
138
139    #[test]
140    fn factory_claims_the_hurl_step_kind() {
141        let factory = HurlEngineFactory;
142        assert_eq!(factory.id(), "hurl");
143        assert_eq!(factory.step_kinds().len(), 1);
144        assert_eq!(factory.step_kinds()[0].prefix, "hurl");
145    }
146
147    #[test]
148    fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
149        assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
150        // An unknown response section name is unambiguously invalid hurl.
151        let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
152        assert!(
153            err.line >= 2,
154            "position should be near the broken section: {err:?}"
155        );
156    }
157}