Skip to main content

rustev_core/
unparameterized.rs

1//! Parse refusals for spec 034 inputs when this build excludes that feature.
2
3use crate::{Category, Refusal};
4use rustev_contract::{
5    bounded::parse_bounded,
6    limits::{DEFINITION_V1, PLAN_V1},
7    plan::Plan,
8};
9use serde_json::Value;
10
11fn refusal(schema: &str, member: &str) -> Refusal {
12    Refusal {
13        category: Category::Parse,
14        subject: schema.into(),
15        detail: format!("{schema} member {member}: this build has no `parameterized` feature"),
16        shortfalls: vec![],
17    }
18}
19
20fn reference(v: &Value) -> bool {
21    match v {
22        Value::Object(o) => {
23            (o.len() == 1 && o.get("param").is_some_and(Value::is_string))
24                || o.values().any(reference)
25        }
26        Value::Array(a) => a.iter().any(reference),
27        _ => false,
28    }
29}
30
31/// Recognize only spec 034 declarations and item members, not unrelated JSON keys.
32/// Malformed JSON retains the ordinary document parser's diagnostic.
33pub fn definition(bytes: &[u8]) -> Result<(), Refusal> {
34    let Ok(v) = parse_bounded::<Value>(bytes, &DEFINITION_V1) else {
35        return Ok(());
36    };
37    let schema = v["schema"].as_str().unwrap_or("definition");
38    for member in ["parameters", "variants"] {
39        if v.get(member).is_some() {
40            return Err(refusal(schema, member));
41        }
42    }
43    for path in [
44        "/inputs",
45        "/steps",
46        "/policy/rules",
47        "/policy/adjustments",
48        "/policy/on_unresolved",
49    ] {
50        if let Some(items) = v.pointer(path).and_then(Value::as_array) {
51            for item in items {
52                for member in ["only_in", "enabled"] {
53                    if member == "enabled" && path == "/inputs" {
54                        continue;
55                    }
56                    if item.get(member).is_some() {
57                        return Err(refusal(schema, member));
58                    }
59                }
60            }
61        }
62    }
63    if reference(&v) {
64        return Err(refusal(schema, "param"));
65    }
66    Ok(())
67}
68
69/// Refuse the record before decoding it, including malformed records.
70pub fn plan_bytes(bytes: &[u8]) -> Result<(), Refusal> {
71    if let Ok(v) = parse_bounded::<Value>(bytes, &PLAN_V1)
72        && v.get("instantiation").is_some()
73    {
74        return Err(refusal(
75            v["schema"].as_str().unwrap_or("plan"),
76            "instantiation",
77        ));
78    }
79    Ok(())
80}
81
82/// Typed callers cannot load an instantiated plan through a feature-off core.
83pub fn plan(plan: &Plan) -> Result<(), Refusal> {
84    if plan.instantiation.is_some() {
85        return Err(refusal(&plan.schema, "instantiation"));
86    }
87    Ok(())
88}
89
90/// Typed definitions can still contain parameter references in raw exact args.
91#[cfg(not(feature = "parameterized"))]
92pub(crate) fn typed_definition(
93    def: &rustev_contract::definition::Definition,
94) -> Result<(), Refusal> {
95    for step in &def.steps {
96        if let rustev_contract::definition::StepBody::Exact(exact) = &step.body
97            && reference(&exact.args)
98        {
99            return Err(refusal(&def.schema, "param"));
100        }
101    }
102    Ok(())
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use serde_json::json;
109
110    #[test]
111    fn refuses_each_reserved_member_without_reclassifying_unrelated_keys() {
112        for (member, value) in [
113            ("parameters", json!({"parameters": []})),
114            ("variants", json!({"variants": []})),
115            ("only_in", json!({"inputs": [{"only_in": ["a"]}]})),
116            ("enabled", json!({"steps": [{"enabled": true}]})),
117            (
118                "param",
119                json!({"limits": {"deadline_ms": {"param": "deadline"}}}),
120            ),
121        ] {
122            let error = definition(&serde_json::to_vec(&value).unwrap()).unwrap_err();
123            assert_eq!(error.category, Category::Parse);
124            assert!(error.detail.contains(member));
125            assert!(error.detail.contains("no `parameterized` feature"));
126        }
127        assert!(definition(br#"{"schema":"rustev.definition/2","steps":[{"body":{"exact":{"args":{"enabled":true,"variants":[]}}}}]}"#).is_ok());
128        assert!(definition(b"invalid JSON").is_ok());
129    }
130
131    #[test]
132    fn plan_three_without_record_remains_available() {
133        assert!(plan_bytes(br#"{"schema":"rustev.plan/3"}"#).is_ok());
134        let error = plan_bytes(br#"{"schema":"rustev.plan/3","instantiation":{}}"#).unwrap_err();
135        assert_eq!(error.category, Category::Parse);
136        assert!(error.detail.contains("rustev.plan/3 member instantiation"));
137    }
138}