pub(crate) mod locate;
mod schema;
mod validate;
pub use schema::json_schema;
use std::collections::BTreeMap;
use std::sync::Arc;
use serde::Deserialize;
use crate::diag::{Diag, FrontError, Span};
use crate::engine::StepKindSpec;
use crate::step::Retry;
#[derive(Debug, Clone)]
pub struct PackSource {
pub name: String,
pub text: Arc<str>,
}
pub fn builtin_sources() -> Vec<PackSource> {
vec![PackSource {
name: "builtin:core.yaml".to_owned(),
text: Arc::from(include_str!("../../helpers/core.yaml")),
}]
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct RawPack {
pub(crate) macros: BTreeMap<String, RawMacro>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct RawMacro {
#[serde(default)]
pub(crate) params: Vec<String>,
#[serde(default)]
pub(crate) defaults: BTreeMap<String, String>,
#[serde(rename = "match")]
pub(crate) match_: Option<String>,
pub(crate) description: Option<String>,
#[serde(default)]
pub(crate) tags: Vec<String>,
#[serde(default)]
pub(crate) steps: Vec<RawStep>,
pub(crate) expect: Option<Vec<RawExpectItem>>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub(crate) struct RawStep {
pub(crate) name: Option<String>,
#[serde(default)]
pub(crate) optional: bool,
pub(crate) when: Option<String>,
pub(crate) retry: Option<RawRetry>,
pub(crate) delay: Option<u64>,
#[serde(rename = "saveAs")]
pub(crate) save_as: Option<BTreeMap<String, String>>,
#[serde(rename = "use")]
pub(crate) use_: Option<String>,
pub(crate) with: Option<BTreeMap<String, String>>,
#[serde(flatten)]
#[schemars(with = "BTreeMap<String, serde_json::Value>")]
pub(crate) payload: BTreeMap<String, serde_norway::Value>,
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct RawRetry {
pub(crate) count: u32,
#[serde(default = "default_retry_interval")]
pub(crate) interval_ms: u64,
}
fn default_retry_interval() -> u64 {
1000
}
#[derive(Debug, Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct RawExpectItem {
pub(crate) status: Option<String>,
pub(crate) hurl: Option<String>,
}
#[derive(Debug, Default)]
pub struct PackSet {
pub macros: BTreeMap<String, Macro>,
}
impl PackSet {
pub fn step_defs(&self) -> Vec<(&str, &str)> {
self.macros
.values()
.filter_map(|m| m.pattern.as_deref().map(|p| (p, m.name.as_str())))
.collect()
}
pub fn find_use_target(&self, target: &str) -> Option<&Macro> {
match target.split_once('#') {
Some((pack_ref, name)) => self
.macros
.get(name)
.filter(|m| pack_ref_matches(&m.pack, pack_ref)),
None => self.macros.get(target),
}
}
}
fn pack_ref_matches(pack: &str, pack_ref: &str) -> bool {
let bounded_suffix = |hay: &str, needle: &str| {
hay.strip_suffix(needle)
.is_some_and(|rest| rest.is_empty() || rest.ends_with('/'))
};
bounded_suffix(pack, pack_ref) || bounded_suffix(pack_ref, pack)
}
#[derive(Debug, Clone)]
pub struct Macro {
pub name: String,
pub pack: String,
pub params: Vec<String>,
pub defaults: BTreeMap<String, String>,
pub pattern: Option<String>,
pub description: Option<String>,
pub tags: Vec<String>,
pub body: MacroBody,
pub source: Arc<str>,
pub span: Option<Span>,
pub match_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub enum MacroBody {
Steps(Vec<MacroStep>),
Expect(Vec<ExpectItem>),
}
#[derive(Debug, Clone)]
pub struct MacroStep {
pub name: Option<String>,
pub delay_ms: Option<u64>,
pub kind: MacroStepKind,
pub optional: bool,
pub when: Option<String>,
pub retry: Option<Retry>,
pub save_as: BTreeMap<String, String>,
}
#[derive(Debug, Clone)]
pub enum MacroStepKind {
Payload {
kind: String,
payload: PayloadForm,
},
Use {
target: String,
with: BTreeMap<String, String>,
},
}
#[derive(Debug, Clone)]
pub enum PayloadForm {
Raw(String),
Structured(serde_json::Value),
}
#[derive(Debug, Clone)]
pub struct ExpectItem {
pub status: Option<String>,
pub fragment: Option<String>,
}
pub(crate) fn load_collecting(
sources: &[PackSource],
kinds: &[StepKindSpec],
) -> (PackSet, Vec<Diag>) {
let mut diags: Vec<Diag> = Vec::new();
let mut set = PackSet::default();
let mut raw_packs: Vec<(usize, String, RawPack)> = Vec::new();
for (index, source) in sources.iter().enumerate() {
match serde_norway::from_str::<RawPack>(&source.text) {
Ok(raw) => raw_packs.push((index, source.name.clone(), raw)),
Err(err) => {
let span = err
.location()
.map(|loc| Span::clamped(loc.index(), loc.index() + 1, source.text.len()));
let mut diag = Diag::error(
"proef::pack::yaml",
format!("pack is not valid YAML for the pack schema: {err}"),
)
.with_source(source.name.clone(), Arc::clone(&source.text));
if let Some(span) = span {
diag = diag.with_span(span);
}
diags.push(diag);
}
}
}
for (source_index, pack_name, raw) in &raw_packs {
let source = &sources[*source_index];
for (macro_name, raw_macro) in &raw.macros {
let normalized =
validate::normalize_macro(macro_name, raw_macro, pack_name, source, &mut diags);
if let Some(macro_) = normalized {
if let Some(existing) = set.macros.get(macro_name) {
diags.push(
Diag::error(
"proef::pack::duplicate_macro",
format!(
"macro `{macro_name}` is defined in both `{}` and `{pack_name}`",
existing.pack
),
)
.with_source(source.name.clone(), Arc::clone(&source.text))
.maybe_span(macro_.span)
.with_help("macro names are global — rename one of the definitions"),
);
} else {
set.macros.insert(macro_name.clone(), macro_);
}
}
}
}
validate::run_cross_macro_passes(&set, kinds, &mut diags);
(set, diags)
}
pub fn load(sources: &[PackSource], kinds: &[StepKindSpec]) -> Result<PackSet, FrontError> {
let (set, diags) = load_collecting(sources, kinds);
if diags
.iter()
.any(|d| d.severity == crate::diag::Severity::Error)
{
Err(FrontError::Diagnostics(diags))
} else {
Ok(set)
}
}
impl Diag {
#[must_use]
pub(crate) fn maybe_span(self, span: Option<Span>) -> Self {
match span {
Some(span) => self.with_span(span),
None => self,
}
}
}