Skip to main content

cruxx_script/
metadata.rs

1//! Handler metadata and lightweight static argument schemas.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6/// Static JSON type accepted by a handler argument.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ArgType {
10    Any,
11    String,
12    Number,
13    Integer,
14    Boolean,
15    Object,
16    Array,
17}
18
19impl ArgType {
20    pub fn matches(self, value: &Value) -> bool {
21        match self {
22            ArgType::Any => true,
23            ArgType::String => value.is_string(),
24            ArgType::Number => value.is_number(),
25            ArgType::Integer => value.as_i64().is_some() || value.as_u64().is_some(),
26            ArgType::Boolean => value.is_boolean(),
27            ArgType::Object => value.is_object(),
28            ArgType::Array => value.is_array(),
29        }
30    }
31}
32
33/// One static argument accepted by a handler under the pipeline `args` object.
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct ArgSpec {
36    pub name: String,
37    pub arg_type: ArgType,
38    pub required: bool,
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub description: Option<String>,
41}
42
43impl ArgSpec {
44    pub fn required(name: impl Into<String>, arg_type: ArgType) -> Self {
45        Self {
46            name: name.into(),
47            arg_type,
48            required: true,
49            description: None,
50        }
51    }
52
53    pub fn optional(name: impl Into<String>, arg_type: ArgType) -> Self {
54        Self {
55            name: name.into(),
56            arg_type,
57            required: false,
58            description: None,
59        }
60    }
61
62    pub fn describe(mut self, description: impl Into<String>) -> Self {
63        self.description = Some(description.into());
64        self
65    }
66}
67
68/// Static `args` schema for a handler.
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct ArgSchema {
71    #[serde(default)]
72    pub args: Vec<ArgSpec>,
73    #[serde(default = "default_allow_extra_args")]
74    pub allow_extra: bool,
75}
76
77impl ArgSchema {
78    pub fn new() -> Self {
79        Self {
80            args: Vec::new(),
81            allow_extra: true,
82        }
83    }
84
85    pub fn strict() -> Self {
86        Self {
87            args: Vec::new(),
88            allow_extra: false,
89        }
90    }
91
92    pub fn required(mut self, name: impl Into<String>, arg_type: ArgType) -> Self {
93        self.args.push(ArgSpec::required(name, arg_type));
94        self
95    }
96
97    pub fn optional(mut self, name: impl Into<String>, arg_type: ArgType) -> Self {
98        self.args.push(ArgSpec::optional(name, arg_type));
99        self
100    }
101
102    pub fn allow_extra(mut self, allow_extra: bool) -> Self {
103        self.allow_extra = allow_extra;
104        self
105    }
106
107    pub fn get(&self, name: &str) -> Option<&ArgSpec> {
108        self.args.iter().find(|spec| spec.name == name)
109    }
110
111    pub fn has_required_args(&self) -> bool {
112        self.args.iter().any(|spec| spec.required)
113    }
114}
115
116impl Default for ArgSchema {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122fn default_allow_extra_args() -> bool {
123    true
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum RiskLevel {
129    Low,
130    Medium,
131    High,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub enum SideEffect {
137    None,
138    ReadFs,
139    WriteFs,
140    Shell,
141    Network,
142    Git,
143    Docker,
144    Llm,
145    Database,
146    Process,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(rename_all = "snake_case")]
151pub enum Capability {
152    ReadFs,
153    WriteFs,
154    Shell,
155    Network,
156    Git,
157    Docker,
158    Llm,
159    Database,
160    Process,
161}
162
163/// Introspection metadata for a registered handler.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct HandlerMetadata {
166    pub name: String,
167    #[serde(default)]
168    pub description: String,
169    #[serde(default)]
170    pub args: ArgSchema,
171    pub risk: RiskLevel,
172    #[serde(default)]
173    pub side_effects: Vec<SideEffect>,
174    #[serde(default)]
175    pub capabilities: Vec<Capability>,
176    #[serde(default = "default_deterministic")]
177    pub deterministic: bool,
178}
179
180impl HandlerMetadata {
181    pub fn new(name: impl Into<String>) -> Self {
182        Self {
183            name: name.into(),
184            description: String::new(),
185            args: ArgSchema::new(),
186            risk: RiskLevel::Low,
187            side_effects: vec![SideEffect::None],
188            capabilities: Vec::new(),
189            deterministic: true,
190        }
191    }
192
193    pub fn describe(mut self, description: impl Into<String>) -> Self {
194        self.description = description.into();
195        self
196    }
197
198    pub fn args(mut self, args: ArgSchema) -> Self {
199        self.args = args;
200        self
201    }
202
203    pub fn risk(mut self, risk: RiskLevel) -> Self {
204        self.risk = risk;
205        self
206    }
207
208    pub fn side_effects(mut self, side_effects: impl Into<Vec<SideEffect>>) -> Self {
209        self.side_effects = side_effects.into();
210        self
211    }
212
213    pub fn capabilities(mut self, capabilities: impl Into<Vec<Capability>>) -> Self {
214        self.capabilities = capabilities.into();
215        self
216    }
217
218    pub fn deterministic(mut self, deterministic: bool) -> Self {
219        self.deterministic = deterministic;
220        self
221    }
222}
223
224fn default_deterministic() -> bool {
225    true
226}