use anyhow::bail;
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
pub fn parse_yaml(yaml_str: &str) -> anyhow::Result<Vec<SpliceRule>> {
let config: ConfigFile = serde_yaml::from_str(yaml_str)?;
config.validate()?;
Ok(config.into_splice_rules())
}
#[derive(Debug, Deserialize)]
pub struct ConfigFile {
pub version: u32,
pub rules: Vec<YamlRule>,
}
#[derive(Debug, Deserialize)]
pub struct YamlRule {
before: Option<YamlStrategyBefore>,
between: Option<YamlStrategyBetween>,
inject: Vec<YamlInjection>,
}
#[derive(Debug, Deserialize)]
pub struct YamlInjection {
pub name: Option<String>,
pub path: Option<String>,
pub builtin: Option<BuiltinSpec>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum BuiltinSpec {
Name(String),
Detailed {
name: String,
alias: Option<String>,
#[serde(default)]
config: BTreeMap<String, serde_yaml::Value>,
},
}
impl BuiltinSpec {
fn builtin_name(&self) -> &str {
match self {
BuiltinSpec::Name(n) => n,
BuiltinSpec::Detailed { name, .. } => name,
}
}
fn alias(&self) -> Option<&str> {
match self {
BuiltinSpec::Name(_) => None,
BuiltinSpec::Detailed { alias, .. } => alias.as_deref(),
}
}
fn config(&self) -> &BTreeMap<String, serde_yaml::Value> {
static EMPTY: BTreeMap<String, serde_yaml::Value> = BTreeMap::new();
match self {
BuiltinSpec::Name(_) => &EMPTY,
BuiltinSpec::Detailed { config, .. } => config,
}
}
}
#[derive(Debug, Deserialize)]
pub struct YamlStrategyBefore {
interface: String,
provider: Option<YamlProviderOpt>,
}
#[derive(Debug, Deserialize)]
pub struct YamlStrategyBetween {
inner: YamlProviderReq,
outer: YamlProviderReq,
interface: String,
}
#[derive(Debug, Deserialize)]
pub struct YamlProviderReq {
name: String,
alias: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct YamlProviderOpt {
name: Option<String>,
alias: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct AdapterInjectionInfo {
pub adapter_path: String,
pub matched_hook_interfaces: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct Injection {
pub name: String,
pub path: Option<String>,
#[serde(skip)]
pub builtin: Option<String>,
#[serde(skip)]
pub builtin_config: BTreeMap<String, toml::Value>,
#[serde(skip)]
pub(crate) config_as_wave: Option<BTreeMap<String, String>>,
#[serde(skip)]
pub(crate) config_provider_path: Option<String>,
#[serde(skip)]
pub(crate) adapter_info: Option<AdapterInjectionInfo>,
#[serde(skip)]
pub(crate) tier: Option<builtin_protocol::Tier>,
}
impl PartialEq for Injection {
fn eq(&self, other: &Self) -> bool {
self.name == other.name && self.path == other.path && self.builtin == other.builtin
}
}
impl Eq for Injection {}
impl std::hash::Hash for Injection {
fn hash<H: std::hash::Hasher>(&self, h: &mut H) {
self.name.hash(h);
self.path.hash(h);
self.builtin.hash(h);
}
}
impl PartialOrd for Injection {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Injection {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(&self.name, &self.path, &self.builtin).cmp(&(&other.name, &other.path, &other.builtin))
}
}
impl Injection {
pub fn from_path(name: impl Into<String>, path: impl Into<String>) -> Self {
Self {
name: name.into(),
path: Some(path.into()),
builtin: None,
builtin_config: BTreeMap::new(),
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}
}
pub fn from_name(name: impl Into<String>) -> Self {
Self {
name: name.into(),
path: None,
builtin: None,
builtin_config: BTreeMap::new(),
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}
}
pub fn from_builtin(builtin: impl Into<String>) -> Self {
let name = builtin.into();
Self {
name: name.clone(),
path: None,
builtin: Some(name),
builtin_config: BTreeMap::new(),
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}
}
}
#[derive(Debug)]
pub enum SpliceRule {
Before {
interface: String,
provider_name: Option<String>,
provider_alias: Option<String>,
inject: Vec<Injection>,
},
Between {
interface: String,
inner_name: String,
inner_alias: Option<String>,
outer_name: String,
outer_alias: Option<String>,
inject: Vec<Injection>,
},
}
impl SpliceRule {
pub fn interface(&self) -> &str {
match self {
SpliceRule::Before { interface, .. } | SpliceRule::Between { interface, .. } => {
interface
}
}
}
pub fn inject(&self) -> &[Injection] {
match self {
SpliceRule::Before { inject, .. } | SpliceRule::Between { inject, .. } => inject,
}
}
pub fn inject_mut(&mut self) -> &mut Vec<Injection> {
match self {
SpliceRule::Before { inject, .. } | SpliceRule::Between { inject, .. } => inject,
}
}
}
fn validate_interface_name(rule_num: usize, interface: &str) -> anyhow::Result<()> {
let safe = |c: char| {
c.is_ascii_alphanumeric()
|| matches!(c, '-' | '_' | '.' | ':' | '/' | '@' | '*' | '?' | '[' | ']')
};
if let Some(bad) = interface.chars().find(|c| !safe(*c)) {
bail!(
"rule {rule_num}: 'interface' contains disallowed character {bad:?} in '{interface}'"
);
}
Ok(())
}
impl ConfigFile {
pub fn validate(&self) -> anyhow::Result<()> {
if self.version != 1 {
bail!(
"unsupported config version {}: only version 1 is supported",
self.version
);
}
let mut seen_names: HashMap<&str, usize> = HashMap::new();
for (i, rule) in self.rules.iter().enumerate() {
let rule_num = i + 1;
match (&rule.before, &rule.between) {
(Some(_), Some(_)) => {
bail!("rule {rule_num}: a rule may specify 'before' or 'between', not both")
}
(None, None) => {
bail!("rule {rule_num}: a rule must specify either 'before' or 'between'")
}
_ => {}
}
let interface = if let Some(b) = &rule.before {
&b.interface
} else if let Some(bw) = &rule.between {
&bw.interface
} else {
unreachable!()
};
if interface.is_empty() {
bail!("rule {rule_num}: 'interface' must not be empty");
}
validate_interface_name(rule_num, interface)?;
if let Some(before) = &rule.before {
if let Some(prov) = &before.provider {
if prov.name.as_deref() == Some("") {
bail!(
"rule {rule_num}: provider 'name' must not be empty if specified \
(omit the key to leave it unset)"
);
}
}
}
if let Some(between) = &rule.between {
if between.inner.name == between.outer.name {
bail!(
"rule {rule_num} (between): 'inner' and 'outer' must name different \
instances, but both are '{}'",
between.inner.name
);
}
}
if rule.inject.is_empty() {
bail!("rule {rule_num}: 'inject' list must contain at least one entry");
}
for (j, inj) in rule.inject.iter().enumerate() {
let inj_num = j + 1;
match (&inj.builtin, &inj.name, &inj.path) {
(None, None, _) => {
bail!("rule {rule_num}, injection {inj_num}: missing 'name' or 'builtin'")
}
(Some(_), Some(_), _) => bail!(
"rule {rule_num}, injection {inj_num}: 'builtin' replaces top-level \
'name' — move the WAC-var override to 'builtin.alias'"
),
(Some(_), _, Some(_)) => bail!(
"rule {rule_num}, injection {inj_num}: 'builtin' and 'path' are mutually \
exclusive — drop one"
),
_ => {}
}
if inj.name.as_deref() == Some("") {
bail!("rule {rule_num}, injection {inj_num}: injection name must not be empty");
}
if inj.path.as_deref() == Some("") {
bail!(
"rule {rule_num}, injection {inj_num}: 'path' must not be empty if \
specified (omit the key to leave it unset)"
);
}
if let Some(spec) = &inj.builtin {
if spec.builtin_name().is_empty() {
bail!(
"rule {rule_num}, injection {inj_num}: builtin 'name' must not be \
empty"
);
}
if spec.alias() == Some("") {
bail!(
"rule {rule_num}, injection {inj_num}: builtin 'alias' must not be \
empty if specified (omit the key to leave it unset)"
);
}
yaml_config_to_toml(spec.config()).map_err(|e| {
anyhow::anyhow!("rule {rule_num}, injection {inj_num}: {e}")
})?;
}
let effective_name = if let Some(spec) = &inj.builtin {
spec.alias().unwrap_or_else(|| spec.builtin_name())
} else {
inj.name.as_deref().expect("validated above")
};
if let Some(first_rule) = seen_names.get(effective_name) {
bail!(
"injection name '{effective_name}' is used in rule {rule_num} but was \
already declared in rule {first_rule}; each injection must have a \
globally unique name"
);
}
seen_names.insert(effective_name, rule_num);
}
}
Ok(())
}
pub fn into_splice_rules(self) -> Vec<SpliceRule> {
self.rules
.into_iter()
.map(
|YamlRule {
before,
between,
inject,
}| {
let inject = inject.into_iter().map(into_injection).collect();
if let Some(YamlStrategyBefore {
interface,
provider,
}) = before
{
SpliceRule::Before {
interface,
provider_name: provider.as_ref().and_then(|p| p.name.clone()),
provider_alias: provider.and_then(|p| p.alias),
inject,
}
} else if let Some(YamlStrategyBetween {
interface,
inner,
outer,
}) = between
{
SpliceRule::Between {
interface,
inner_name: inner.name,
inner_alias: inner.alias,
outer_name: outer.name,
outer_alias: outer.alias,
inject,
}
} else {
unreachable!("validate() guarantees exactly one strategy per rule")
}
},
)
.collect()
}
}
fn into_injection(yaml: YamlInjection) -> Injection {
let YamlInjection {
name,
path,
builtin,
} = yaml;
let (wac_name, builtin_name, builtin_config) = match builtin {
Some(spec) => {
let bname = spec.builtin_name().to_string();
let alias = spec.alias().map(str::to_string);
let cfg = yaml_config_to_toml(spec.config()).expect("validate() ran");
(alias.unwrap_or_else(|| bname.clone()), Some(bname), cfg)
}
None => (name.expect("validated"), None, BTreeMap::new()),
};
Injection {
name: wac_name,
path,
builtin: builtin_name,
builtin_config,
config_as_wave: None,
config_provider_path: None,
adapter_info: None,
tier: None,
}
}
fn yaml_config_to_toml(
values: &BTreeMap<String, serde_yaml::Value>,
) -> anyhow::Result<BTreeMap<String, toml::Value>> {
let mut out = BTreeMap::new();
for (key, val) in values {
let v = yaml_to_toml(val).map_err(|e| anyhow::anyhow!("config key '{key}': {e}"))?;
out.insert(key.clone(), v);
}
Ok(out)
}
fn yaml_to_toml(v: &serde_yaml::Value) -> anyhow::Result<toml::Value> {
use serde_yaml::Value as Y;
Ok(match v {
Y::String(s) => toml::Value::String(s.clone()),
Y::Bool(b) => toml::Value::Boolean(*b),
Y::Number(n) => {
if let Some(i) = n.as_i64() {
toml::Value::Integer(i)
} else if let Some(f) = n.as_f64() {
toml::Value::Float(f)
} else {
bail!("number {n} could not be represented as i64 or f64");
}
}
Y::Null => bail!("value is null; omit the key or use an empty string scalar instead"),
Y::Sequence(items) => {
let parts: anyhow::Result<Vec<_>> = items.iter().map(yaml_to_toml).collect();
toml::Value::Array(parts?)
}
Y::Mapping(m) => {
let mut table = toml::map::Map::new();
for (k, v) in m {
let key = match k {
Y::String(s) => s.clone(),
other => bail!("table key must be a string, got {other:?}",),
};
table.insert(key, yaml_to_toml(v)?);
}
toml::Value::Table(table)
}
Y::Tagged(_) => bail!("YAML-tagged value isn't supported by the substrate"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_before_rule() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
provider:
name: srv-b
inject:
- name: middleware-a
"#;
let rules = parse_yaml(yaml).unwrap();
assert_eq!(rules.len(), 1);
let SpliceRule::Before {
interface,
provider_name,
provider_alias,
inject,
} = &rules[0]
else {
panic!("expected Before rule");
};
assert_eq!(interface, "wasi:http/handler@0.3.0");
assert_eq!(provider_name.as_deref(), Some("srv-b"));
assert!(provider_alias.is_none());
assert_eq!(inject.len(), 1);
assert_eq!(inject[0].name, "middleware-a");
assert!(inject[0].path.is_none());
}
#[test]
fn parse_before_rule_no_provider() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- name: mw
"#;
let rules = parse_yaml(yaml).unwrap();
assert_eq!(rules.len(), 1);
let SpliceRule::Before {
provider_name,
provider_alias,
..
} = &rules[0]
else {
panic!("expected Before rule");
};
assert!(provider_name.is_none());
assert!(provider_alias.is_none());
}
#[test]
fn parse_between_rule() {
let yaml = r#"
version: 1
rules:
- between:
interface: wasi:http/handler@0.3.0
inner:
name: srv-b
alias: renamed-b
outer:
name: srv
inject:
- name: mw-a
- name: mw-b
path: /tmp/mw-b.wasm
"#;
let rules = parse_yaml(yaml).unwrap();
assert_eq!(rules.len(), 1);
let SpliceRule::Between {
interface,
inner_name,
inner_alias,
outer_name,
outer_alias,
inject,
} = &rules[0]
else {
panic!("expected Between rule");
};
assert_eq!(interface, "wasi:http/handler@0.3.0");
assert_eq!(inner_name, "srv-b");
assert_eq!(inner_alias.as_deref(), Some("renamed-b"));
assert_eq!(outer_name, "srv");
assert!(outer_alias.is_none());
assert_eq!(inject.len(), 2);
assert_eq!(inject[1].path.as_deref(), Some("/tmp/mw-b.wasm"));
}
#[test]
fn parse_multi_rule() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- name: first
- between:
interface: wasi:http/handler@0.3.0
inner:
name: srv-b
outer:
name: srv
inject:
- name: second
"#;
let rules = parse_yaml(yaml).unwrap();
assert_eq!(rules.len(), 2);
assert!(matches!(rules[0], SpliceRule::Before { .. }));
assert!(matches!(rules[1], SpliceRule::Between { .. }));
let SpliceRule::Before { inject: inj0, .. } = &rules[0] else {
unreachable!()
};
let SpliceRule::Between { inject: inj1, .. } = &rules[1] else {
unreachable!()
};
assert_eq!(inj0[0].name, "first");
assert_eq!(inj1[0].name, "second");
}
#[test]
fn parse_missing_interface() {
let yaml = r#"
version: 1
rules:
- before:
provider:
name: srv-b
inject:
- name: mw
"#;
let result = parse_yaml(yaml);
assert!(
result.is_err(),
"expected parse error for missing interface field"
);
}
#[test]
fn parse_unknown_version() {
let yaml = r#"
version: 99
rules: []
"#;
let err = parse_yaml(yaml).unwrap_err().to_string();
assert!(
err.contains("unsupported config version"),
"unexpected error: {err}"
);
}
fn assert_err(yaml: &str, expected_fragment: &str) {
let err = parse_yaml(yaml).unwrap_err().to_string();
assert!(
err.contains(expected_fragment),
"expected error containing {expected_fragment:?}, got: {err}"
);
}
#[test]
fn validate_both_before_and_between() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
between:
interface: wasi:http/handler
inner:
name: a
outer:
name: b
inject:
- name: mw
"#,
"'before' or 'between', not both",
);
}
#[test]
fn validate_neither_before_nor_between() {
assert_err(
r#"
version: 1
rules:
- inject:
- name: mw
"#,
"either 'before' or 'between'",
);
}
#[test]
fn validate_empty_inject_list() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject: []
"#,
"'inject' list must contain at least one entry",
);
}
#[test]
fn validate_empty_injection_name() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: ""
"#,
"injection name must not be empty",
);
}
#[test]
fn validate_empty_injection_path() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: mw
path: ""
"#,
"'path' must not be empty if specified",
);
}
#[test]
fn validate_empty_interface_name() {
assert_err(
r#"
version: 1
rules:
- before:
interface: ""
inject:
- name: mw
"#,
"'interface' must not be empty",
);
}
#[test]
fn validate_interface_name_glob_pattern() {
let yaml = r#"
version: 1
rules:
- before:
interface: "wasi:http/*"
inject:
- name: mw
"#;
parse_yaml(yaml).expect("glob pattern should parse cleanly");
}
#[test]
fn validate_interface_name_injection() {
assert_err(
"version: 1
rules:
- before:
interface: \"foo;\\nworld evil { import bar/baz; }\\n\"
inject:
- name: mw
",
"disallowed character",
);
}
#[test]
fn validate_interface_name_whitespace() {
assert_err(
"version: 1
rules:
- before:
interface: \"wasi : http / handler\"
inject:
- name: mw
",
"disallowed character",
);
}
#[test]
fn validate_empty_before_provider_name() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
provider:
name: ""
inject:
- name: mw
"#,
"provider 'name' must not be empty if specified",
);
}
#[test]
fn validate_between_same_inner_outer() {
assert_err(
r#"
version: 1
rules:
- between:
interface: wasi:http/handler
inner:
name: srv
outer:
name: srv
inject:
- name: mw
"#,
"'inner' and 'outer' must name different instances",
);
}
#[test]
fn validate_duplicate_injection_name_across_rules() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: mw-a
- before:
interface: wasi:logging/log
inject:
- name: mw-a
"#,
"injection name 'mw-a' is used in rule 2 but was already declared in rule 1",
);
}
#[test]
fn validate_duplicate_injection_name_within_rule() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: mw-a
- name: mw-a
"#,
"injection name 'mw-a' is used in rule 1 but was already declared in rule 1",
);
}
#[test]
fn parse_builtin_short_form() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- builtin: hello-tier1
"#;
let rules = parse_yaml(yaml).unwrap();
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
assert_eq!(inject.len(), 1);
assert_eq!(inject[0].name, "hello-tier1");
assert_eq!(inject[0].builtin.as_deref(), Some("hello-tier1"));
assert!(inject[0].path.is_none());
}
#[test]
fn parse_builtin_long_form_with_alias() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- builtin:
name: hello-tier1
alias: greeter
"#;
let rules = parse_yaml(yaml).unwrap();
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
assert_eq!(inject[0].name, "greeter");
assert_eq!(inject[0].builtin.as_deref(), Some("hello-tier1"));
}
#[test]
fn parse_builtin_long_form_no_alias() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- builtin:
name: hello-tier1
"#;
let rules = parse_yaml(yaml).unwrap();
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
assert_eq!(inject[0].name, "hello-tier1");
assert_eq!(inject[0].builtin.as_deref(), Some("hello-tier1"));
}
#[test]
fn validate_builtin_with_top_level_name_rejected() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: greeter
builtin: hello-tier1
"#,
"'builtin' replaces top-level 'name'",
);
}
#[test]
fn validate_builtin_with_path_rejected() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- builtin: hello-tier1
path: ./mw.wasm
"#,
"'builtin' and 'path' are mutually exclusive",
);
}
#[test]
fn validate_neither_name_nor_builtin() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- path: ./mw.wasm
"#,
"missing 'name' or 'builtin'",
);
}
#[test]
fn validate_builtin_long_form_empty_alias() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- builtin:
name: hello-tier1
alias: ""
"#,
"builtin 'alias' must not be empty if specified",
);
}
#[test]
fn parse_builtin_config_block_stringifies_scalars() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin:
name: hello-tier1
config:
buffer: 100
flush_after_seconds: 10.0
note: "hi there"
enable: true
"#;
let rules = parse_yaml(yaml).expect("parse");
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
let cfg = &inject[0].builtin_config;
assert_eq!(cfg.get("buffer"), Some(&toml::Value::Integer(100)));
assert_eq!(
cfg.get("flush_after_seconds"),
Some(&toml::Value::Float(10.0))
);
assert_eq!(
cfg.get("note"),
Some(&toml::Value::String("hi there".into()))
);
assert_eq!(cfg.get("enable"), Some(&toml::Value::Boolean(true)));
}
#[test]
fn parse_builtin_config_block_defaults_empty() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin:
name: hello-tier1
"#;
let rules = parse_yaml(yaml).expect("parse");
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
assert!(inject[0].builtin_config.is_empty());
}
#[test]
fn parse_builtin_short_form_has_no_config() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin: hello-tier1
"#;
let rules = parse_yaml(yaml).expect("parse");
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
assert!(inject[0].builtin_config.is_empty());
}
#[test]
fn parse_builtin_config_block_preserves_list() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin:
name: hello-tier1
config:
rules:
- "1.2.3.4/32"
- "5.6.7.8/32"
"#;
let rules = parse_yaml(yaml).expect("parse");
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
let toml::Value::Array(xs) = inject[0].builtin_config.get("rules").unwrap() else {
panic!("expected array");
};
assert_eq!(xs.len(), 2);
}
#[test]
fn parse_builtin_config_block_preserves_map() {
let yaml = r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin:
name: hello-tier1
config:
limits:
max: 100
min: 0
"#;
let rules = parse_yaml(yaml).expect("parse");
let SpliceRule::Before { inject, .. } = &rules[0] else {
panic!("expected Before");
};
let toml::Value::Table(t) = inject[0].builtin_config.get("limits").unwrap() else {
panic!("expected table");
};
assert_eq!(t.get("max"), Some(&toml::Value::Integer(100)));
assert_eq!(t.get("min"), Some(&toml::Value::Integer(0)));
}
#[test]
fn validate_builtin_config_rejects_null() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler@0.3.0
inject:
- builtin:
name: hello-tier1
config:
buffer: null
"#,
"is null",
);
}
#[test]
fn validate_duplicate_alias_collides_with_user_name() {
assert_err(
r#"
version: 1
rules:
- before:
interface: wasi:http/handler
inject:
- name: greeter
path: ./greeter.wasm
- builtin:
name: hello-tier1
alias: greeter
"#,
"injection name 'greeter' is used in rule 1 but was already declared in rule 1",
);
}
}