1mod 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) templates: BTreeMap<String, RawTemplate>,
50}
51
52#[derive(Debug, Deserialize, schemars::JsonSchema)]
53#[serde(deny_unknown_fields)]
54pub(crate) struct RawTemplate {
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.macros.get(name).filter(|m| {
135 m.pack == pack_ref || m.pack.ends_with(pack_ref) || pack_ref.ends_with(&m.pack)
136 }),
137 None => self.macros.get(target),
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct Macro {
145 pub name: String,
147 pub pack: String,
149 pub params: Vec<String>,
151 pub defaults: BTreeMap<String, String>,
153 pub pattern: Option<String>,
155 pub description: Option<String>,
157 pub tags: Vec<String>,
159 pub body: MacroBody,
161 pub source: Arc<str>,
163 pub span: Option<Span>,
165}
166
167#[derive(Debug, Clone)]
170pub enum MacroBody {
171 Steps(Vec<MacroStep>),
173 Expect(Vec<ExpectItem>),
175}
176
177#[derive(Debug, Clone)]
179pub struct MacroStep {
180 pub name: Option<String>,
182 pub delay_ms: Option<u64>,
184 pub kind: MacroStepKind,
186 pub optional: bool,
188 pub when: Option<String>,
190 pub retry: Option<Retry>,
192 pub save_as: BTreeMap<String, String>,
194}
195
196#[derive(Debug, Clone)]
198pub enum MacroStepKind {
199 Payload {
201 kind: String,
203 payload: PayloadForm,
205 },
206 Use {
208 target: String,
210 with: BTreeMap<String, String>,
212 },
213}
214
215#[derive(Debug, Clone)]
218pub enum PayloadForm {
219 Raw(String),
221 Structured(serde_json::Value),
223}
224
225#[derive(Debug, Clone)]
228pub struct ExpectItem {
229 pub status: Option<String>,
231 pub fragment: Option<String>,
233}
234
235pub fn load(sources: &[PackSource], kinds: &[StepKindSpec]) -> Result<PackSet, FrontError> {
243 let mut diags: Vec<Diag> = Vec::new();
244 let mut set = PackSet::default();
245 let mut raw_packs: Vec<(usize, String, RawPack)> = Vec::new();
246
247 for (index, source) in sources.iter().enumerate() {
248 match serde_norway::from_str::<RawPack>(&source.text) {
249 Ok(raw) => raw_packs.push((index, source.name.clone(), raw)),
250 Err(err) => {
251 let span = err
252 .location()
253 .map(|loc| Span::clamped(loc.index(), loc.index() + 1, source.text.len()));
254 let mut diag = Diag::error(
255 "proef::pack::yaml",
256 format!("pack is not valid YAML for the pack schema: {err}"),
257 )
258 .with_source(source.name.clone(), Arc::clone(&source.text));
259 if let Some(span) = span {
260 diag = diag.with_span(span);
261 }
262 diags.push(diag);
263 }
264 }
265 }
266
267 for (source_index, pack_name, raw) in &raw_packs {
269 let source = &sources[*source_index];
270 for (template_name, template) in &raw.templates {
271 let normalized = validate::normalize_template(
272 template_name,
273 template,
274 pack_name,
275 source,
276 &mut diags,
277 );
278 if let Some(macro_) = normalized {
279 if let Some(existing) = set.macros.get(template_name) {
281 diags.push(
282 Diag::error(
283 "proef::pack::duplicate_macro",
284 format!(
285 "macro `{template_name}` is defined in both `{}` and `{pack_name}`",
286 existing.pack
287 ),
288 )
289 .with_source(source.name.clone(), Arc::clone(&source.text))
290 .maybe_span(macro_.span)
291 .with_help("macro names are global — rename one of the definitions"),
292 );
293 } else {
294 set.macros.insert(template_name.clone(), macro_);
295 }
296 }
297 }
298 }
299
300 validate::run_cross_macro_passes(&set, kinds, &mut diags);
301
302 if diags
303 .iter()
304 .any(|d| d.severity == crate::diag::Severity::Error)
305 {
306 Err(FrontError::Diagnostics(diags))
307 } else {
308 Ok(set)
309 }
310}
311
312impl Diag {
313 #[must_use]
315 pub(crate) fn maybe_span(self, span: Option<Span>) -> Self {
316 match span {
317 Some(span) => self.with_span(span),
318 None => self,
319 }
320 }
321}