1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum Phase {
15 Input,
17 Mutate,
19 Read,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, schemars::JsonSchema)]
24pub struct Annotations {
25 pub read_only: bool,
26 pub destructive: bool,
27 pub idempotent: bool,
28 pub waits: bool,
30}
31
32impl Annotations {
33 pub const READ: Annotations = Annotations { read_only: true, destructive: false, idempotent: true, waits: false };
34 pub const INPUT: Annotations = Annotations { read_only: false, destructive: false, idempotent: false, waits: false };
35 pub const MUTATE: Annotations = Annotations { read_only: false, destructive: false, idempotent: false, waits: true };
36 pub const MUTATE_NOWAIT: Annotations = Annotations { read_only: false, destructive: false, idempotent: true, waits: false };
37 pub const DESTRUCTIVE: Annotations = Annotations { read_only: false, destructive: true, idempotent: false, waits: true };
38}
39
40pub struct Ctx<'a> {
42 pub app: &'a mut crate::app::BrepApp,
43 pub egui: &'a egui::Context,
44}
45
46pub enum Outcome {
48 Done(Value),
49 Blob { json: Value, bytes: Vec<u8> },
51 AwaitScreenshot { token: u64, region: crate::automation::cmd_capture::Region },
53}
54
55pub type InputHandler = fn(&mut crate::automation::pointer::Pointer, Value, &mut Vec<egui::Event>) -> Result<Value, String>;
56pub type AppHandler = fn(&mut Ctx<'_>, Value) -> Result<Outcome, String>;
57
58pub enum Handler {
59 Input(InputHandler),
60 App(AppHandler),
61}
62
63pub struct CommandSpec {
64 pub name: &'static str,
65 pub group: &'static str,
66 pub doc: &'static str,
67 pub phase: Phase,
68 pub annotations: Annotations,
69 pub args_schema: fn() -> Value,
70 pub result_schema: fn() -> Value,
71 pub handler: Handler,
72}
73
74pub fn schema_of<T: schemars::JsonSchema>() -> Value {
82 let mut schema = serde_json::to_value(schemars::schema_for!(T)).unwrap_or(Value::Null);
83 normalize_bool_subschemas(&mut schema);
84 schema
85}
86
87fn normalize_bool_subschemas(node: &mut Value) {
92 match node {
93 Value::Object(map) => {
94 if let Some(Value::Object(props)) = map.get_mut("properties") {
95 for value in props.values_mut() {
96 if let Some(b) = value.as_bool() {
97 *value = if b { serde_json::json!({}) } else { serde_json::json!({"not": {}}) };
98 }
99 }
100 }
101 if let Some(items) = map.get_mut("items") {
102 if let Some(b) = items.as_bool() {
103 *items = if b { serde_json::json!({}) } else { serde_json::json!({"not": {}}) };
104 }
105 }
106 for value in map.values_mut() {
107 normalize_bool_subschemas(value);
108 }
109 }
110 Value::Array(arr) => {
111 for value in arr.iter_mut() {
112 normalize_bool_subschemas(value);
113 }
114 }
115 _ => {}
116 }
117}
118
119pub fn parse_args<T: serde::de::DeserializeOwned>(args: Value) -> Result<T, String> {
120 let args = if args.is_null() { Value::Object(Default::default()) } else { args };
122 serde_json::from_value(args).map_err(|e| format!("arguments: {e}"))
123}
124
125#[derive(Debug, Default, Deserialize, schemars::JsonSchema)]
127#[serde(deny_unknown_fields)]
128pub struct NoArgs {}
129
130#[derive(Debug, Default, Serialize, schemars::JsonSchema)]
132pub struct Empty {}
133
134pub fn registry() -> Vec<&'static CommandSpec> {
139 use crate::automation::*;
140 let sets: &[&[CommandSpec]] = &[
141 cmd_frame::COMMANDS,
142 cmd_perf::COMMANDS,
143 cmd_input::COMMANDS,
144 cmd_capture::COMMANDS,
145 cmd_state::COMMANDS,
146 cmd_document::COMMANDS,
147 cmd_classes::COMMANDS,
148 cmd_history::COMMANDS,
149 cmd_scene::COMMANDS,
150 cmd_camera::COMMANDS,
151 cmd_settings::COMMANDS,
152 cmd_shell::COMMANDS,
153 cmd_plugins::COMMANDS,
154 cmd_metadata::COMMANDS,
155 cmd_assembly::COMMANDS,
156 cmd_pmi::COMMANDS,
157 cmd_sheets::COMMANDS,
158 cmd_wire_harness::COMMANDS,
159 cmd_cam::COMMANDS,
160 cmd_exploded::COMMANDS,
161 cmd_instructions::COMMANDS,
162 cmd_instructions_motion::COMMANDS,
163 cmd_simulation::COMMANDS,
164 cmd_simulation_play::COMMANDS,
165 cmd_cam_sim::COMMANDS,
166 cmd_ecad::COMMANDS,
167 #[cfg(not(target_arch = "wasm32"))]
168 cmd_plm::COMMANDS,
169 #[cfg(not(target_arch = "wasm32"))]
170 cmd_kicad::COMMANDS,
171 ];
172 sets.iter().flat_map(|s| s.iter()).collect()
173}
174
175pub fn lookup(name: &str) -> Option<&'static CommandSpec> {
176 registry().into_iter().find(|c| c.name == name)
177}
178
179pub fn describe() -> Value {
182 Value::Array(
183 registry()
184 .into_iter()
185 .map(|c| {
186 serde_json::json!({
187 "name": c.name,
188 "group": c.group,
189 "doc": c.doc,
190 "phase": c.phase,
191 "annotations": c.annotations,
192 "argsSchema": (c.args_schema)(),
193 "resultSchema": (c.result_schema)(),
194 })
195 })
196 .collect(),
197 )
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct Envelope {
204 pub id: u64,
205 pub cmd: String,
206 #[serde(default)]
207 pub args: Value,
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
211#[serde(rename_all = "snake_case")]
212pub enum NoticeKind {
213 Panic,
214 LogError,
215 RunnerRefusal,
216 Toast,
217 Console,
218}
219
220#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
221pub struct Notice {
222 pub kind: NoticeKind,
223 pub frame: u64,
224 pub text: String,
225}
226
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
228pub struct Reply {
229 pub id: u64,
230 pub frame: u64,
231 pub ok: bool,
232 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub result: Option<Value>,
234 #[serde(default, skip_serializing_if = "Option::is_none")]
235 pub error: Option<String>,
236 #[serde(default, skip_serializing_if = "Vec::is_empty")]
237 pub notices: Vec<Notice>,
238 #[serde(skip)]
241 pub blob: Option<Vec<u8>>,
242}
243
244impl Reply {
245 pub fn ok(id: u64, frame: u64, result: Value, notices: Vec<Notice>) -> Self {
246 Self { id, frame, ok: true, result: Some(result), error: None, notices, blob: None }
247 }
248 pub fn err(id: u64, frame: u64, error: impl Into<String>, notices: Vec<Notice>) -> Self {
249 Self { id, frame, ok: false, result: None, error: Some(error.into()), notices, blob: None }
250 }
251}
252