forked from 1b-agent/opencadstudio-upstream
feat(plugin-api): entity-class display declarations on the manifest (API v10)
Plugins can now declare the entity classes they identify via XDATA, and the
host resolves them to display-only type labels while the declaring plugin is
loaded.
- Append `BuiltinPlugin::entity_classes()` (a guarded trailing vtable slot,
the same additive pattern as `on_load` in v5) returning a &'static
`EntityClasses { app_id, class_prefix, labels }`. The static
`PluginManifest` struct is deliberately untouched: older cdylibs share it
by pointer and its layout is frozen ABI since v1, so adding a field would
read past their allocation.
- `OwnedEntityClasses` travels on the registered manifest as
`OwnedPluginManifest.entity_classes` with `#[serde(default)]`, so manifests
serialized before the field keep decoding (older runners tolerate it
both ways).
- The runner folds the declaration into the GetManifest registration only
when the loaded cdylib's exported `ocs_plugin_api_version` symbol reports
>= 10 — an older binary's vtable predates the slot, so it is never called;
its entities simply keep their native type names.
- `PluginManager::entity_class_declarations` exposes the live set, filtered
to alive, non-disabled plugins in load order. The registered manifests are
the only source and nothing is cached: not installed, disabled,
cap-excluded (`OCS_PLUGIN_MAX_API_VERSION`), failed at load, removed, or a
dead process all contribute nothing.
- Embedded version info `api_version` follows the bump to 10;
`API_VERSION_MIN_SUPPORTED` stays 3.
Tests: runner fold (v10 folds, pre-v10 skipped, `entity_classes()` panic is
contained and reported), manager filtering (disabled / dead / undeclaring
skipped; declarations disappear once the process dies — verified with a real
killed child), bincode registration round-trip, and older-manifest serde
default.
This commit is contained in:
1 parent
859070403c
commit
60eb7b086e
9 files changed
+502
-13
No files matched your search
@@ -158,6 +158,71 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_manifest_roundtrips_entity_classes_over_bincode() {
|
||||
// The registration handshake is length-framed bincode, so the manifest
|
||||
// and its API-v10 entity-class declaration must survive exactly that
|
||||
// wire format.
|
||||
let manifest = crate::ribbon::owned::OwnedPluginManifest {
|
||||
id: "opencad.civil_ifc".to_string(),
|
||||
name: "Civil IFC".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
description: "test".to_string(),
|
||||
api_version: 10,
|
||||
ribbon_order: 50,
|
||||
xdata_apps: vec!["CVIFC".to_string()],
|
||||
command_prefixes: vec![],
|
||||
entity_classes: Some(crate::ribbon::owned::OwnedEntityClasses {
|
||||
app_id: "CVIFC".to_string(),
|
||||
class_prefix: "CVIFC-CLASS:".to_string(),
|
||||
labels: [
|
||||
("LINE".to_string(), "Linear Element".to_string()),
|
||||
(
|
||||
"CIRCULARARC".to_string(),
|
||||
"Circular Arc Element".to_string(),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
let resp = HostResponse::Manifest(manifest);
|
||||
let bytes = bincode::serialize(&resp).unwrap();
|
||||
let got: HostResponse = bincode::deserialize(&bytes).unwrap();
|
||||
match got {
|
||||
HostResponse::Manifest(m) => {
|
||||
let classes = m.entity_classes.expect("declaration survives");
|
||||
assert_eq!(classes.app_id, "CVIFC");
|
||||
assert_eq!(classes.class_prefix, "CVIFC-CLASS:");
|
||||
assert_eq!(
|
||||
classes.labels.get("LINE").map(String::as_str),
|
||||
Some("Linear Element")
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manifest_entity_classes_default_when_absent() {
|
||||
// A manifest serialized before the field existed (an older runner)
|
||||
// decodes with the declaration defaulting to `None`.
|
||||
let older = r#"{
|
||||
"id": "old.plugin",
|
||||
"name": "Old",
|
||||
"version": "0.1.0",
|
||||
"description": "test",
|
||||
"api_version": 9,
|
||||
"ribbon_order": 0,
|
||||
"xdata_apps": [],
|
||||
"command_prefixes": []
|
||||
}"#;
|
||||
let m: crate::ribbon::owned::OwnedPluginManifest =
|
||||
serde_json::from_str(older).expect("older manifest decodes");
|
||||
assert_eq!(m.id, "old.plugin");
|
||||
assert!(m.entity_classes.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_additions_preserve_legacy_discriminants() {
|
||||
fn discriminant<T: serde::Serialize>(value: &T) -> u32 {
|
||||
|
||||
@@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::host::{CommandSource, CommandStep, HostSettingValue};
|
||||
use crate::manifest::ApiVersion;
|
||||
use crate::ribbon::owned::{OwnedPluginManifest, OwnedRibbonGroup};
|
||||
use crate::ribbon::owned::{OwnedEntityClasses, OwnedPluginManifest, OwnedRibbonGroup};
|
||||
|
||||
pub use crate::host::PreviewWire;
|
||||
pub use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||||
@@ -307,6 +307,24 @@ impl From<&'static crate::manifest::PluginManifest> for OwnedPluginManifest {
|
||||
ribbon_order: m.ribbon_order,
|
||||
xdata_apps: m.xdata_apps.iter().map(|s| s.to_string()).collect(),
|
||||
command_prefixes: m.command_prefixes.iter().map(|s| s.to_string()).collect(),
|
||||
// The declaration does not live on the static manifest struct (its
|
||||
// layout is frozen ABI, shared by pointer with older cdylibs); the
|
||||
// runner folds the API-v10 trait accessor in after conversion.
|
||||
entity_classes: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static crate::manifest::EntityClasses> for OwnedEntityClasses {
|
||||
fn from(c: &'static crate::manifest::EntityClasses) -> Self {
|
||||
Self {
|
||||
app_id: c.app_id.to_string(),
|
||||
class_prefix: c.class_prefix.to_string(),
|
||||
labels: c
|
||||
.labels
|
||||
.iter()
|
||||
.map(|(key, label)| (key.to_string(), label.to_string()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user