rustev_core/
unparameterized.rs1use 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
31pub 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
69pub 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
82pub 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#[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}