1pub use brep_kernel::feature_pipeline::extension::PluginPin;
3use brep_kernel::feature_pipeline::{self as kernel, FeatureContext, FeatureResult};
4use serde::{Deserialize, Serialize};
5use serde_json::{json, Value};
6use sha2::{Digest, Sha256};
7use std::{
8 collections::{BTreeMap, BTreeSet},
9 rc::Rc,
10};
11mod annotations;
12mod invocation;
13#[derive(Clone, Debug, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct PackageManifest {
16 pub id: String,
17 pub version: String,
18 pub api_version: u32,
19 pub name: String,
20 pub entry: String,
21}
22#[derive(Clone, Debug, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase", deny_unknown_fields)]
24pub struct PackageBundle {
25 pub manifest: PackageManifest,
26 pub modules: BTreeMap<String, String>,
27 #[serde(default)]
28 pub assets: BTreeMap<String, Vec<u8>>,
29}
30fn local_path(s: &str) -> bool {
31 !s.is_empty()
32 && !s.contains('\\')
33 && !s.contains(':')
34 && s.split('/').all(|p| !p.is_empty() && p != "." && p != "..")
35}
36impl PackageBundle {
37 pub fn pin(&self) -> Result<PluginPin, String> {
38 let m = &self.manifest;
39 if m.api_version != 1
40 || m.id.is_empty()
41 || m.id.contains('/')
42 || !m
43 .id
44 .chars()
45 .all(|c| c.is_ascii_alphanumeric() || ".-_".contains(c))
46 || m.version.is_empty()
47 || m.name.is_empty()
48 {
49 return Err(format!(
50 "{}: invalid manifest or unsupported API version",
51 m.id
52 ));
53 }
54 if !local_path(&m.entry)
55 || !self.modules.contains_key(&m.entry)
56 || self.modules.len() > 128
57 || self
58 .modules
59 .keys()
60 .chain(self.assets.keys())
61 .any(|p| !local_path(p))
62 {
63 return Err(format!(
64 "{}: invalid package-local paths or module count",
65 m.id
66 ));
67 }
68 if self.modules.values().map(String::len).sum::<usize>()
69 + self.assets.values().map(Vec::len).sum::<usize>()
70 > 8 * 1024 * 1024
71 {
72 return Err("package exceeds 8 MiB".into());
73 }
74 let digest = format!(
75 "{:x}",
76 Sha256::digest(serde_json::to_vec(self).map_err(|e| e.to_string())?)
77 );
78 Ok(PluginPin {
79 id: m.id.clone(),
80 version: m.version.clone(),
81 api_version: m.api_version,
82 digest,
83 })
84 }
85}
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct FeatureRegistration {
89 pub id: String,
90 pub label: String,
91 pub glyph: String,
92 pub ribbon_path: String,
93 pub command_size: String,
94 pub short_name: String,
95 pub input_params_schema: Value,
96 pub package: PluginPin,
97}
98#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase")]
100pub struct ActionRegistration {
101 pub id: String,
102 pub label: String,
103 pub glyph: String,
104 pub ribbon_path: String,
105 pub command_size: String,
106 pub input_params_schema: Value,
107 pub package: PluginPin,
108}
109#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
110#[serde(rename_all = "camelCase")]
111pub struct WorkbenchRegistration {
112 pub id: String,
113 pub label: String,
114 pub feature_types: Vec<String>,
115 pub actions: Vec<String>,
116 pub panels: Vec<String>,
117 pub package: PluginPin,
118}
119#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct AnnotationRegistration {
122 pub id: String,
123 pub label: String,
124 pub input_params_schema: Value,
125 pub package: PluginPin,
126}
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct PostRegistration {
134 pub id: String,
135 pub label: String,
136 pub module: String,
137 pub package: PluginPin,
138}
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140#[serde(rename_all = "camelCase")]
141pub struct PanelRegistration {
142 pub id: String,
143 pub label: String,
144 pub controls: Vec<PanelControl>,
145 pub package: PluginPin,
146}
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
149pub enum PanelControl {
150 Text {
151 text: String,
152 },
153 Table {
154 columns: Vec<String>,
155 rows: Vec<Vec<String>>,
156 },
157 Action {
158 id: String,
159 label: String,
160 action: String,
161 #[serde(default = "empty_object")]
162 params: Value,
163 },
164 Form {
165 id: String,
166 label: String,
167 action: String,
168 #[serde(default = "empty_object")]
169 params: Value,
170 },
171}
172fn empty_object() -> Value {
173 json!({})
174}
175#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
176pub struct Registry {
177 features: Vec<FeatureRegistration>,
178 actions: Vec<ActionRegistration>,
179 workbenches: Vec<WorkbenchRegistration>,
180 #[serde(default)]
181 panels: Vec<PanelRegistration>,
182 #[serde(default)]
183 annotations: Vec<AnnotationRegistration>,
184 #[serde(default)]
185 posts: Vec<PostRegistration>,
186 packages: Vec<PluginPin>,
187 revision: String,
188}
189impl Registry {
190 pub fn panels(&self) -> &[PanelRegistration] {
191 &self.panels
192 }
193 pub fn posts(&self) -> &[PostRegistration] {
194 &self.posts
195 }
196 pub fn annotations(&self) -> &[AnnotationRegistration] {
197 &self.annotations
198 }
199 pub fn features(&self) -> &[FeatureRegistration] {
200 &self.features
201 }
202 pub fn actions(&self) -> &[ActionRegistration] {
203 &self.actions
204 }
205 pub fn workbenches(&self) -> &[WorkbenchRegistration] {
206 &self.workbenches
207 }
208 pub fn revision(&self) -> &str {
209 &self.revision
210 }
211 pub fn install(packages: &[PackageBundle]) -> Result<Self, String> {
212 let mut result = Self::default();
213 let mut seen = BTreeSet::new();
214 for package in packages {
215 let pin = package.pin()?;
216 if !seen.insert((pin.id.clone(), pin.digest.clone())) {
217 return Err(format!("duplicate package {}", pin.id));
218 }
219 let r = invocation::invoke(package, None, None, None, None)?.0;
220 result.packages.push(pin);
221 result.features.extend(r.features);
222 result.actions.extend(r.actions);
223 result.workbenches.extend(r.workbenches);
224 result.panels.extend(r.panels);
225 result.annotations.extend(r.annotations);
226 result.posts.extend(r.posts);
227 }
228 result.revision = format!("{:x}", Sha256::digest(serde_json::to_vec(&result).unwrap()));
229 Ok(result)
230 }
231 pub fn for_document(&self, pins: &[PluginPin]) -> Result<Self, String> {
232 let mut ids = BTreeSet::new();
233 for pin in pins {
234 if !ids.insert(&pin.id) {
235 return Err(format!("duplicate document pin {}", pin.id));
236 }
237 if !self.packages.contains(pin) {
238 return Err(format!("missing or disabled pinned package {}", pin.id));
239 }
240 }
241 let mut r = self.clone();
242 r.packages = pins.to_vec();
243 r.features.retain(|x| pins.contains(&x.package));
244 r.actions.retain(|x| pins.contains(&x.package));
245 r.workbenches.retain(|x| pins.contains(&x.package));
246 r.panels.retain(|x| pins.contains(&x.package));
247 r.annotations.retain(|x| pins.contains(&x.package));
248 r.posts.retain(|x| pins.contains(&x.package));
249 r.revision = format!("{:x}", Sha256::digest(serde_json::to_vec(&r).unwrap()));
250 Ok(r)
251 }
252}
253#[derive(Clone, Debug, Serialize, Deserialize)]
254#[serde(tag = "op", rename_all = "camelCase")]
255pub enum DocumentCommand {
256 AddFeature {
257 #[serde(rename = "featureType")]
258 feature_type: String,
259 #[serde(rename = "inputParams")]
260 input_params: Value,
261 #[serde(rename = "persistentData")]
262 persistent_data: Value,
263 },
264 UpdateFeature {
265 id: String,
266 #[serde(rename = "inputParams")]
267 input_params: Value,
268 },
269 DeleteFeature {
270 id: String,
271 },
272}
273#[derive(Clone, Debug, Default, Serialize, Deserialize)]
274pub struct ActionPlan {
275 pub commands: Vec<DocumentCommand>,
276 pub notifications: Vec<String>,
277}
278
279pub fn execute_script(source: &str, input: &Value) -> Result<ActionPlan, String> {
283 let package = PackageBundle {
284 manifest: PackageManifest {
285 id: "cad.editor".into(), version: "1".into(), api_version: 1,
286 name: "JavaScript editor".into(), entry: "editor.js".into(),
287 },
288 modules: BTreeMap::from([("editor.js".into(), format!(r#"
289export default function install(app) {{
290 app.registerAction({{id: 'cad.editor/run', label: 'Run script', run(cad) {{
291 const ui = Object.freeze({{notify: cad.notify}});
292 const result = (function(cad, ui) {{
293{source}
294 }})(cad, ui);
295 if (result && typeof result.then === 'function') throw Error('Promise results are unsupported');
296 if (result !== undefined) console.log('Result:', JSON.stringify(result));
297 }} }});
298}}
299"#))]),
300 assets: BTreeMap::new(),
301 };
302 let (_, value, _) = invocation::invoke(
303 &package, Some(("action", "cad.editor/run")), Some(input), None, None,
304 )?;
305 serde_json::from_value(value).map_err(|e| e.to_string())
306}
307#[derive(Clone)]
308pub struct Runtime {
309 packages: Vec<PackageBundle>,
310 registry: Registry,
311}
312impl Runtime {
313 pub fn new(packages: Vec<PackageBundle>) -> Result<Self, String> {
314 let registry = Registry::install(&packages)?;
315 Ok(Self { packages, registry })
316 }
317 pub fn from_validated(
320 packages: Vec<PackageBundle>,
321 registry: Registry,
322 ) -> Result<Self, String> {
323 let pins = packages
324 .iter()
325 .map(PackageBundle::pin)
326 .collect::<Result<Vec<_>, _>>()?;
327 if pins != registry.packages {
328 return Err("validated registry package pins mismatch".into());
329 }
330 for (id, pin) in registry
331 .features
332 .iter()
333 .map(|r| (&r.id, &r.package))
334 .chain(registry.actions.iter().map(|r| (&r.id, &r.package)))
335 .chain(registry.workbenches.iter().map(|r| (&r.id, &r.package)))
336 .chain(registry.panels.iter().map(|r| (&r.id, &r.package)))
337 .chain(registry.annotations.iter().map(|r| (&r.id, &r.package)))
338 .chain(registry.posts.iter().map(|r| (&r.id, &r.package)))
339 {
340 if !pins.contains(pin) || !id.starts_with(&format!("{}/", pin.id)) {
341 return Err("validated registry ownership mismatch".into());
342 }
343 }
344 Ok(Self { packages, registry })
345 }
346 pub fn registry(&self) -> &Registry {
347 &self.registry
348 }
349 pub fn with_provider<R>(&self, f: impl FnOnce() -> R) -> R {
350 kernel::extension::with_provider(Rc::new(self.clone()), f)
351 }
352 pub fn execute_history(&self, request: &kernel::HistoryRequest) -> kernel::HistoryResult {
353 self.with_provider(|| kernel::execute_history(request))
354 }
355 fn package(&self, pins: &[PluginPin], id: &str) -> Result<&PackageBundle, String> {
356 let owner = id.split('/').next().unwrap_or("");
357 let matches: Vec<_> = pins.iter().filter(|p| p.id == owner).collect();
358 if matches.len() != 1 {
359 return Err(format!("{} requires exactly one document plugin pin", id));
360 }
361 self.packages
362 .iter()
363 .find(|p| p.pin().as_ref() == Ok(matches[0]))
364 .ok_or_else(|| format!("missing or disabled pinned package for {id}"))
365 }
366 pub fn execute_action(
367 &self,
368 pins: &[PluginPin],
369 id: &str,
370 input: &Value,
371 ) -> Result<ActionPlan, String> {
372 let p = self.package(pins, id)?;
373 let (r, v, _) = invocation::invoke(
374 p,
375 Some(("action", id)),
376 Some(input),
377 None,
378 Some(&self.registry.for_document(&[p.pin()?])?),
379 )?;
380 self.verify(p, &r)?;
381 serde_json::from_value(v).map_err(|e| e.to_string())
382 }
383 fn verify(&self, p: &PackageBundle, r: &Registry) -> Result<(), String> {
384 let expected = self.registry.for_document(&[p.pin()?])?;
385 if expected.features != r.features
386 || expected.actions != r.actions
387 || expected.workbenches != r.workbenches
388 || expected.panels != r.panels
389 || expected.annotations != r.annotations
390 {
391 return Err("registration metadata changed during invocation".into());
392 }
393 Ok(())
394 }
395}
396impl kernel::extension::ExtensionProvider for Runtime {
397 fn execute_annotation(
398 &self,
399 pins: &[PluginPin],
400 annotation: &kernel::pmi::PmiAnnotation,
401 context: &kernel::pmi::PmiContext<'_>,
402 ) -> Result<kernel::pmi::PluginAnnotationOutput, String> {
403 Runtime::execute_annotation(self, pins, annotation, context)
404 }
405 fn identity(&self, pins: &[PluginPin], kind: &str) -> Result<String, String> {
406 let p = self.package(pins, kind)?;
407 if !self
408 .registry
409 .features
410 .iter()
411 .any(|r| r.id == kind && r.package == p.pin().unwrap())
412 {
413 return Err(format!("unregistered plugin feature {kind}"));
414 }
415 Ok(format!("{}:{}:1", p.pin()?.digest, kind))
416 }
417 fn execute(&self, pins: &[PluginPin], ctx: &FeatureContext<'_>) -> FeatureResult {
418 let run = || -> Result<FeatureResult, String> {
419 let p = self.package(pins, &ctx.feature_type)?;
420 let reg = self
421 .registry
422 .features
423 .iter()
424 .find(|r| r.id == ctx.feature_type && r.package == p.pin().unwrap())
425 .ok_or("feature registration unavailable")?;
426 let mut params = ctx.params.clone();
427 validate_params(®.input_params_schema, &mut params, Some(ctx))?;
428 let input = json!({"params":params,"persistentData":ctx.persistent,"featureId":ctx.id,"units":"mm"});
429 let (r, _, result) = invocation::invoke(
430 p,
431 Some(("feature", &ctx.feature_type)),
432 Some(&input),
433 Some(ctx),
434 Some(&self.registry.for_document(&[p.pin()?])?),
435 )?;
436 self.verify(p, &r)?;
437 result.ok_or_else(|| "missing feature result".into())
439 };
440 match run() {
441 Ok(r) => r,
442 Err(e) => ctx.fail(e),
443 }
444 }
445}
446fn validate_params(
447 schema: &Value,
448 params: &mut Value,
449 ctx: Option<&FeatureContext<'_>>,
450) -> Result<(), String> {
451 let obj = params
452 .as_object_mut()
453 .ok_or("parameters must be an object")?;
454 let fields = schema.as_object().ok_or("schema must be object")?;
455 for key in obj.keys() {
456 if key != "id" && !fields.contains_key(key) {
457 return Err(format!("unknown parameter {key}"));
458 }
459 }
460 if obj.get("id").is_some_and(|v| !v.is_string()) {
461 return Err("feature id must be string".into());
462 }
463 for (key, s) in schema.as_object().ok_or("schema must be object")? {
464 if !obj.contains_key(key) {
465 if let Some(v) = s.get("default_value") {
466 obj.insert(key.clone(), v.clone());
467 }
468 }
469 let v = obj
470 .get_mut(key)
471 .ok_or_else(|| format!("missing parameter {key}"))?;
472 match s["type"].as_str().unwrap_or("") {
473 "number" => {
474 let n = if let Some(n) = v.as_f64() {
475 n
476 } else if let (Some(c), Some(text)) = (ctx, v.as_str()) {
477 c.env.eval(text)?
478 } else {
479 return Err(format!("{key} must be numeric"));
480 };
481 if !n.is_finite()
482 || s["min"].as_f64().is_some_and(|m| n < m)
483 || s["max"].as_f64().is_some_and(|m| n > m)
484 {
485 return Err(format!("{key} outside finite numeric bounds"));
486 }
487 *v = json!(n);
488 }
489 "reference_selection" => {
490 fn reference(v: &Value) -> bool {
491 v.is_string()
492 || v.as_object()
493 .is_some_and(|o| o.get("name").is_some_and(Value::is_string))
494 }
495 if !reference(v) && !v.as_array().is_some_and(|a| a.iter().all(reference)) {
496 return Err(format!("{key} must contain reference names"));
497 }
498 }
499 "string" if !v.is_string() => return Err(format!("{key} must be string")),
500 "boolean" if !v.is_boolean() => return Err(format!("{key} must be boolean")),
501 _ => {}
502 }
503 }
504 Ok(())
505}