1mod fragment;
13mod session;
14
15use proef_core::engine::{
16 DoctorCheck, DoctorResult, EngineFactory, EngineSession, FragmentSupport, PayloadProbeError,
17 RawOption, RawOptionValue, ScenarioCtx, StepKindSpec,
18};
19use proef_core::error::EngineError;
20
21pub const EMBEDDED_HURL_VERSION: &str = "8.0.1";
24
25const PROBE_HURL: &str = "GET http://localhost/health\nHTTP 200\n";
27
28const 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 options: Some(recognise_option),
41}];
42
43fn recognise_option(key: &str) -> Option<RawOption> {
50 let (family, value) = match key {
51 "retry" => (Some("retry"), Some(RawOptionValue::Count)),
52 "repeat" => (None, Some(RawOptionValue::Count)),
55 "delay" => (Some("delay"), Some(RawOptionValue::Duration)),
56 "retry-interval" => (Some("retry"), None),
59 _ => return None,
60 };
61 Some(RawOption { family, value })
62}
63
64pub struct HurlEngineFactory;
66
67impl EngineFactory for HurlEngineFactory {
68 fn id(&self) -> &'static str {
69 "hurl"
70 }
71
72 fn step_kinds(&self) -> &'static [StepKindSpec] {
73 STEP_KINDS
74 }
75
76 fn doctor(&self) -> Vec<DoctorCheck> {
77 vec![
78 DoctorCheck {
79 name: "embedded hurl",
80 run: check_embedded_version,
81 },
82 DoctorCheck {
83 name: "hurl parser",
84 run: check_parser,
85 },
86 DoctorCheck {
87 name: "libcurl",
88 run: check_libcurl,
89 },
90 ]
91 }
92
93 fn open(&self, ctx: &ScenarioCtx) -> Result<Box<dyn EngineSession>, EngineError> {
94 Ok(Box::new(session::HurlSession::open(ctx)?))
95 }
96}
97
98fn check_embedded_version() -> DoctorResult {
100 DoctorResult::pass(format!(
101 "hurl {EMBEDDED_HURL_VERSION} (exact pin, ADR-0003)"
102 ))
103}
104
105fn check_parser() -> DoctorResult {
109 match hurl_core::parser::parse_hurl_file(PROBE_HURL) {
110 Ok(file) if file.entries.len() == 1 => DoctorResult::pass(
111 "parsed 1-entry probe file (hurl_core + libxml2 linkage loads)".to_owned(),
112 ),
113 Ok(file) => DoctorResult::warn(format!(
114 "probe parsed with unexpected entry count {}",
115 file.entries.len()
116 )),
117 Err(err) => DoctorResult::fail(format!("cannot parse probe file: {err:?}")),
118 }
119}
120
121fn validate_payload(text: &str) -> Result<(), PayloadProbeError> {
124 let mut normalized = text.to_owned();
125 if !normalized.ends_with('\n') {
126 normalized.push('\n');
127 }
128 match hurl_core::parser::parse_hurl_file(&normalized) {
129 Ok(file) if file.entries.is_empty() => Err(PayloadProbeError {
133 line: 1,
134 column: 1,
135 message: "contains no hurl entries (only comments or blank lines)".to_owned(),
136 }),
137 Ok(_) => Ok(()),
138 Err(err) => Err(PayloadProbeError {
139 line: err.pos.line,
140 column: err.pos.column,
141 message: format!("{:?}", err.kind),
142 }),
143 }
144}
145
146fn check_libcurl() -> DoctorResult {
149 let info = hurl::http::libcurl_version_info();
150 if info.libraries.is_empty() {
151 return DoctorResult::warn("libcurl loaded but reported no libraries".to_owned());
152 }
153 DoctorResult::pass(format!("{} (host {})", info.libraries.join(" "), info.host))
154}
155
156#[cfg(test)]
157mod tests {
158 #![allow(clippy::unwrap_used)]
159
160 use super::*;
161
162 #[test]
165 fn parser_seam_smoke() {
166 let result = check_parser();
167 assert_eq!(
168 result.status,
169 proef_core::engine::DoctorStatus::Pass,
170 "{}",
171 result.detail
172 );
173 }
174
175 #[test]
176 fn factory_claims_the_hurl_step_kind() {
177 let factory = HurlEngineFactory;
178 assert_eq!(factory.id(), "hurl");
179 assert_eq!(factory.step_kinds().len(), 1);
180 assert_eq!(factory.step_kinds()[0].prefix, "hurl");
181 }
182
183 #[test]
187 fn embedded_version_matches_the_cargo_pin() {
188 let manifest = std::fs::read_to_string(
189 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"),
190 )
191 .unwrap();
192 for dep in ["hurl", "hurl_core"] {
193 let needle = format!("{dep} = \"={EMBEDDED_HURL_VERSION}\"");
194 assert!(
195 manifest.contains(&needle),
196 "workspace Cargo.toml must contain `{needle}` (ADR-0003 lockstep)"
197 );
198 }
199 }
200
201 #[test]
202 fn payload_probe_accepts_valid_and_rejects_broken_hurl() {
203 assert!(validate_payload("GET http://x/one\nHTTP 200").is_ok());
204 let err = validate_payload("GET http://x/one\nHTTP 200\n[Wrong]\n").unwrap_err();
206 assert!(
207 err.line >= 2,
208 "position should be near the broken section: {err:?}"
209 );
210 }
211}