1use std::collections::HashMap;
4use std::panic::{AssertUnwindSafe, catch_unwind};
5use std::sync::OnceLock;
6
7use serde_json::Value;
8
9use crate::{CommandError, CommandInfo, CommandSpec, Dispatch, ExecError, Host, Params, params, shortcut};
10
11pub const MAX_DEPTH: usize = 64;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19pub enum Validation {
20 #[default]
23 Strict,
24 TypedOnly,
27 Off,
31}
32
33pub struct Registry<S: Host> {
36 specs: Vec<CommandSpec<S>>,
37 index: HashMap<&'static str, usize>,
38 common: &'static [&'static str],
39 aliases: &'static [(&'static str, &'static str)],
40 schemas: OnceLock<Vec<Value>>,
41 validation: Validation,
42}
43
44impl<S: Host> Registry<S> {
45 pub fn new(specs: Vec<CommandSpec<S>>) -> Self {
47 let mut index = HashMap::with_capacity(specs.len());
48 for (i, c) in specs.iter().enumerate() {
49 index.entry(c.id).or_insert(i);
50 }
51 Self { specs, index, common: &[], aliases: &[], schemas: OnceLock::new(), validation: Validation::Strict }
52 }
53
54 pub fn with_common_params(mut self, keys: &'static [&'static str]) -> Self {
56 self.common = keys;
57 self
58 }
59
60 pub fn with_param_aliases(mut self, aliases: &'static [(&'static str, &'static str)]) -> Self {
62 self.aliases = aliases;
63 self
64 }
65
66 pub fn with_validation(mut self, validation: Validation) -> Self {
68 self.validation = validation;
69 self
70 }
71
72 pub fn validation(&self) -> Validation {
73 self.validation
74 }
75
76 pub fn find(&self, id: &str) -> Option<&CommandSpec<S>> {
77 self.index.get(id).and_then(|&i| self.specs.get(i))
78 }
79
80 pub fn specs(&self) -> &[CommandSpec<S>] {
81 &self.specs
82 }
83
84 pub fn len(&self) -> usize {
85 self.specs.len()
86 }
87
88 pub fn is_empty(&self) -> bool {
89 self.specs.is_empty()
90 }
91
92 pub fn duplicates(&self) -> Vec<&'static str> {
94 self.specs.iter().enumerate().filter(|(i, c)| self.index.get(c.id) != Some(i)).map(|(_, c)| c.id).collect()
95 }
96
97 pub fn validate(&self, spec: &CommandSpec<S>, params: &Value) -> Result<(), CommandError> {
99 params::validate_params(spec.id, &spec.params, params, self.common, self.aliases)
100 }
101
102 pub fn schema(&self, id: &str) -> Option<&Value> {
104 let i = *self.index.get(id)?;
105 self.schemas().get(i)
106 }
107
108 fn schemas(&self) -> &[Value] {
109 self.schemas.get_or_init(|| self.specs.iter().map(|c| params::params_schema(&c.params)).collect())
110 }
111
112 pub fn info(&self, spec: &CommandSpec<S>, state: &S) -> CommandInfo {
114 let enabled = (spec.enabled)(state);
115 let schema = self.schema(spec.id).cloned().unwrap_or(Value::Null);
116 CommandInfo {
117 id: spec.id.to_string(),
118 label: spec.label.to_string(),
119 menu: spec.menu.iter().map(|m| (*m).to_string()).collect(),
120 shortcut: spec.shortcut.map(|s| shortcut::normalize(s).unwrap_or_else(|| s.to_string())),
121 params_schema: schema,
122 params_text: match spec.params {
123 crate::Params::Text(t) => Some(t.to_string()),
124 _ => None,
125 },
126 enabled: enabled.is_ok(),
127 disabled_reason: enabled.err(),
128 mutates: spec.mutates,
129 }
130 }
131
132 pub fn infos(&self, state: &S) -> Vec<CommandInfo> {
134 self.specs.iter().map(|c| self.info(c, state)).collect()
135 }
136
137 pub fn execute(&self, state: &mut S, id: &str, params: Value) -> Result<Value, CommandError> {
141 let converted = |e: S::Error| {
142 let e = e.into().or_id(id);
143 (e.clone(), e)
144 };
145 self.chokepoint(state, id, params, converted).map_err(|e| match e {
146 ExecError::Command(e) | ExecError::Host(e) => e,
147 })
148 }
149
150 pub fn execute_host(&self, state: &mut S, id: &str, params: Value) -> Result<Value, ExecError<S::Error>>
156 where
157 S::Error: Clone,
158 {
159 self.chokepoint(state, id, params, |e: S::Error| (e.clone().into().or_id(id), e))
160 }
161
162 fn chokepoint<E>(
165 &self,
166 state: &mut S,
167 id: &str,
168 params: Value,
169 host_error: impl FnOnce(S::Error) -> (CommandError, E),
170 ) -> Result<Value, ExecError<E>> {
171 let depth = state.journal().depth();
172 let Some(spec) = self.find(id) else {
173 return reject(state, id, CommandError::Unknown { id: id.to_string() }, depth);
174 };
175 if depth >= MAX_DEPTH {
176 let e = CommandError::failed(id, format!("commands are nested more than {MAX_DEPTH} deep (a command that runs itself?)"));
177 return reject(state, id, e, depth);
178 }
179 let mut params = params;
180 if let Err(e) = state.before(spec, &mut params, depth) {
181 let (seen, host) = host_error(e);
182 state.rejected(id, &seen, depth);
183 return Err(ExecError::Host(host));
184 }
185 if let Err(e) = self.validate_by_policy(spec, ¶ms) {
186 return reject(state, id, e, depth);
187 }
188 if let Err(e) = state.validate(spec, ¶ms, depth) {
189 return reject(state, id, e.or_id(id), depth);
190 }
191 if let Err(reason) = state.enabled_for(spec, ¶ms) {
192 return reject(state, id, CommandError::Disabled { id: id.to_string(), reason }, depth);
193 }
194 state.journal().set_depth(depth + 1);
195 let started = (depth == 0 && state.journal().observer().is_some()).then(web_time::Instant::now);
197 let run = catch_unwind(AssertUnwindSafe(|| (spec.run)(state, ¶ms)));
200 state.journal().set_depth(depth);
202 let mut kept = None;
204 let result = match run {
205 Ok(Ok(v)) => Ok(v),
206 Ok(Err(e)) => {
207 let (seen, host) = host_error(e);
208 kept = Some(host);
209 Err(seen)
210 }
211 Err(_) => Err(CommandError::Panicked { id: id.to_string() }),
212 };
213 state.after(spec, ¶ms, &result, depth);
214 if let Some(started) = started
215 && let Some(observe) = state.journal().observer().cloned()
216 {
217 observe(crate::CommandRun { id: spec.id, ok: result.is_ok(), took: started.elapsed() });
218 }
219 if result.is_ok() && spec.mutates && state.journal().records(depth) {
220 state.scrub(id, &mut params);
222 state.journal().push(id, params);
223 }
224 result.map_err(|e| kept.map_or(ExecError::Command(e), ExecError::Host))
225 }
226
227 fn validate_by_policy(&self, spec: &CommandSpec<S>, params: &Value) -> Result<(), CommandError> {
228 match (self.validation, &spec.params) {
229 (Validation::Strict, _) | (Validation::TypedOnly, Params::Typed(_) | Params::Static(_)) => self.validate(spec, params),
230 (Validation::TypedOnly | Validation::Off, _) => Ok(()),
231 }
232 }
233
234 pub fn bind<'a>(&'a self, state: &'a mut S) -> Bound<'a, S> {
236 Bound { registry: self, state }
237 }
238}
239
240fn reject<S: Host, E>(state: &mut S, id: &str, e: CommandError, depth: usize) -> Result<Value, ExecError<E>> {
241 state.rejected(id, &e, depth);
242 Err(ExecError::Command(e))
243}
244
245pub struct Bound<'a, S: Host> {
247 pub registry: &'a Registry<S>,
248 pub state: &'a mut S,
249}
250
251impl<S: Host> Dispatch for Bound<'_, S> {
252 fn commands(&self) -> Vec<CommandInfo> {
253 self.registry.infos(self.state)
254 }
255 fn execute(&mut self, id: &str, params: Value) -> Result<Value, CommandError> {
256 self.registry.execute(self.state, id, params)
257 }
258}