1pub(crate) mod locate;
11mod schema;
12mod validate;
13
14pub use schema::json_schema;
15
16use std::collections::BTreeMap;
17use std::sync::Arc;
18
19use serde::Deserialize;
20
21use crate::diag::{Diag, FrontError, Span};
22use crate::engine::StepKindSpec;
23use crate::step::Retry;
24
25#[derive(Debug, Clone)]
27pub struct PackSource {
28 pub name: String,
30 pub text: Arc<str>,
32}
33
34pub fn builtin_sources() -> Vec<PackSource> {
36 vec![PackSource {
37 name: "builtin:core.yaml".to_owned(),
38 text: Arc::from(include_str!("../../helpers/core.yaml")),
39 }]
40}
41
42#[derive(Debug, Deserialize, schemars::JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub(crate) struct RawPack {
49 pub(crate) macros: BTreeMap<String, RawMacro>,
50}
51
52#[derive(Debug, Deserialize, schemars::JsonSchema)]
53#[serde(deny_unknown_fields)]
54pub(crate) struct RawMacro {
55 #[serde(default)]
56 pub(crate) params: Vec<String>,
57 #[serde(default)]
58 pub(crate) defaults: BTreeMap<String, String>,
59 #[serde(rename = "match")]
60 pub(crate) match_: Option<String>,
61 pub(crate) description: Option<String>,
62 #[serde(default)]
63 pub(crate) tags: Vec<String>,
64 #[serde(default)]
65 pub(crate) steps: Vec<RawStep>,
66 pub(crate) expect: Option<Vec<RawExpectItem>>,
67}
68
69#[derive(Debug, Deserialize, schemars::JsonSchema)]
70pub(crate) struct RawStep {
71 pub(crate) name: Option<String>,
72 #[serde(default)]
73 pub(crate) optional: bool,
74 pub(crate) when: Option<String>,
75 pub(crate) retry: Option<RawRetry>,
76 pub(crate) delay: Option<u64>,
78 #[serde(rename = "saveAs")]
79 pub(crate) save_as: Option<BTreeMap<String, String>>,
80 #[serde(rename = "use")]
81 pub(crate) use_: Option<String>,
82 pub(crate) with: Option<BTreeMap<String, String>>,
83 #[serde(flatten)]
86 #[schemars(with = "BTreeMap<String, serde_json::Value>")]
87 pub(crate) payload: BTreeMap<String, serde_norway::Value>,
88}
89
90#[derive(Debug, Deserialize, schemars::JsonSchema)]
91#[serde(deny_unknown_fields)]
92pub(crate) struct RawRetry {
93 pub(crate) count: u32,
94 #[serde(default = "default_retry_interval")]
95 pub(crate) interval_ms: u64,
96}
97
98fn default_retry_interval() -> u64 {
99 1000
100}
101
102#[derive(Debug, Deserialize, schemars::JsonSchema)]
103#[serde(deny_unknown_fields)]
104pub(crate) struct RawExpectItem {
105 pub(crate) status: Option<String>,
106 pub(crate) hurl: Option<String>,
108}
109
110#[derive(Debug, Default)]
116pub struct PackSet {
117 pub macros: BTreeMap<String, Macro>,
119}
120
121impl PackSet {
122 pub fn step_defs(&self) -> Vec<(&str, &str)> {
125 self.macros
126 .values()
127 .filter_map(|m| m.pattern.as_deref().map(|p| (p, m.name.as_str())))
128 .collect()
129 }
130
131 pub fn find_use_target(&self, target: &str) -> Option<&Macro> {
133 match target.split_once('#') {
134 Some((pack_ref, name)) => self
135 .macros
136 .get(name)
137 .filter(|m| pack_ref_matches(&m.pack, pack_ref)),
138 None => self.macros.get(target),
139 }
140 }
141}
142
143fn pack_ref_matches(pack: &str, pack_ref: &str) -> bool {
147 let bounded_suffix = |hay: &str, needle: &str| {
148 hay.strip_suffix(needle)
149 .is_some_and(|rest| rest.is_empty() || rest.ends_with('/'))
150 };
151 bounded_suffix(pack, pack_ref) || bounded_suffix(pack_ref, pack)
152}
153
154#[derive(Debug, Clone)]
156pub struct Macro {
157 pub name: String,
159 pub pack: String,
161 pub params: Vec<String>,
163 pub defaults: BTreeMap<String, String>,
165 pub pattern: Option<String>,
167 pub description: Option<String>,
169 pub tags: Vec<String>,
171 pub body: MacroBody,
173 pub source: Arc<str>,
175 pub span: Option<Span>,
177 pub match_span: Option<Span>,
179}
180
181#[derive(Debug, Clone)]
184pub enum MacroBody {
185 Steps(Vec<MacroStep>),
187 Expect(Vec<ExpectItem>),
189}
190
191#[derive(Debug, Clone)]
193pub struct MacroStep {
194 pub name: Option<String>,
196 pub delay_ms: Option<u64>,
198 pub kind: MacroStepKind,
200 pub optional: bool,
202 pub when: Option<String>,
204 pub retry: Option<Retry>,
206 pub save_as: BTreeMap<String, String>,
208}
209
210#[derive(Debug, Clone)]
212pub enum MacroStepKind {
213 Payload {
215 kind: String,
217 payload: PayloadForm,
219 },
220 Use {
222 target: String,
224 with: BTreeMap<String, String>,
226 },
227}
228
229#[derive(Debug, Clone)]
232pub enum PayloadForm {
233 Raw(String),
235 Structured(serde_json::Value),
237}
238
239#[derive(Debug, Clone)]
242pub struct ExpectItem {
243 pub status: Option<String>,
245 pub fragment: Option<String>,
247}
248
249pub(crate) fn load_collecting(
261 sources: &[PackSource],
262 kinds: &[StepKindSpec],
263) -> (PackSet, Vec<Diag>) {
264 let mut diags: Vec<Diag> = Vec::new();
265 let mut set = PackSet::default();
266 let mut raw_packs: Vec<(usize, String, RawPack)> = Vec::new();
267
268 for (index, source) in sources.iter().enumerate() {
269 match serde_norway::from_str::<RawPack>(&source.text) {
270 Ok(raw) => raw_packs.push((index, source.name.clone(), raw)),
271 Err(err) => {
272 let span = err
273 .location()
274 .map(|loc| Span::clamped(loc.index(), loc.index() + 1, source.text.len()));
275 let mut diag = Diag::error(
276 "proef::pack::yaml",
277 format!("pack is not valid YAML for the pack schema: {err}"),
278 )
279 .with_source(source.name.clone(), Arc::clone(&source.text));
280 if let Some(span) = span {
281 diag = diag.with_span(span);
282 }
283 diags.push(diag);
284 }
285 }
286 }
287
288 for (source_index, pack_name, raw) in &raw_packs {
290 let source = &sources[*source_index];
291 for (macro_name, raw_macro) in &raw.macros {
292 let normalized =
293 validate::normalize_macro(macro_name, raw_macro, pack_name, source, &mut diags);
294 if let Some(macro_) = normalized {
295 if let Some(existing) = set.macros.get(macro_name) {
297 diags.push(
298 Diag::error(
299 "proef::pack::duplicate_macro",
300 format!(
301 "macro `{macro_name}` is defined in both `{}` and `{pack_name}`",
302 existing.pack
303 ),
304 )
305 .with_source(source.name.clone(), Arc::clone(&source.text))
306 .maybe_span(macro_.span)
307 .with_help("macro names are global — rename one of the definitions"),
308 );
309 } else {
310 set.macros.insert(macro_name.clone(), macro_);
311 }
312 }
313 }
314 }
315
316 validate::run_cross_macro_passes(&set, kinds, &mut diags);
317 (set, diags)
318}
319
320pub fn load(sources: &[PackSource], kinds: &[StepKindSpec]) -> Result<PackSet, FrontError> {
324 let (set, diags) = load_collecting(sources, kinds);
325 if diags
326 .iter()
327 .any(|d| d.severity == crate::diag::Severity::Error)
328 {
329 Err(FrontError::Diagnostics(diags))
330 } else {
331 Ok(set)
332 }
333}
334
335impl Diag {
336 #[must_use]
338 pub(crate) fn maybe_span(self, span: Option<Span>) -> Self {
339 match span {
340 Some(span) => self.with_span(span),
341 None => self,
342 }
343 }
344}