use crate::plugin::{
CURRENT_WIT_WORLD, CapabilityAtom, PluginCapabilityRef, PluginCapabilityShape,
PluginOperationalImport, WitCapabilityRefError,
};
pub const WIT_PACKAGE: &str = "alma:editor";
pub const WIT_WORLD: &str = "plugin";
pub const WIT_VERSION: &str = "0.2.0";
pub const WIT_WORLD_ID: &str = CURRENT_WIT_WORLD;
pub const WIT_SOURCE: &str = include_str!("../../../wit/alma-editor-plugin.wit");
pub const WIT_HOST_INTERFACE: &str = "host";
pub const WIT_HANDLE_RESOURCES: [&str; 3] =
["buffer-handle", "view-handle", "workspace-observe-task"];
pub const WIT_PLUGIN_EXPORTS: [&str; 2] = ["load", "update"];
pub const WIT_HOST_IMPORTS: [&str; 7] = WitHostImport::names();
pub const WIT_WORKSPACE_OBSERVE_TASK_IMPORTS: [&str; 3] = WitWorkspaceObserveTaskImport::names();
pub const CAPABILITY_NAMES: [&str; 5] = CapabilityAtom::names();
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WitHostImport {
StatusPublish,
BufferObserve,
BufferProposeEdit,
WorkspaceObserveStart,
WorkspaceObservePoll,
WorkspaceObserveTake,
WorkspaceArtifactWrite,
}
impl WitHostImport {
#[must_use]
pub const fn all() -> [Self; 7] {
[
Self::StatusPublish,
Self::BufferObserve,
Self::BufferProposeEdit,
Self::WorkspaceObserveStart,
Self::WorkspaceObservePoll,
Self::WorkspaceObserveTake,
Self::WorkspaceArtifactWrite,
]
}
#[must_use]
pub const fn names() -> [&'static str; 7] {
[
Self::StatusPublish.wit_name(),
Self::BufferObserve.wit_name(),
Self::BufferProposeEdit.wit_name(),
Self::WorkspaceObserveStart.wit_name(),
Self::WorkspaceObservePoll.wit_name(),
Self::WorkspaceObserveTake.wit_name(),
Self::WorkspaceArtifactWrite.wit_name(),
]
}
#[must_use]
pub const fn wit_name(self) -> &'static str {
match self {
Self::StatusPublish => "status-publish",
Self::BufferObserve => "buffer-observe",
Self::BufferProposeEdit => "buffer-propose-edit",
Self::WorkspaceObserveStart => "workspace-observe-start",
Self::WorkspaceObservePoll => "workspace-observe-poll",
Self::WorkspaceObserveTake => "workspace-observe-take",
Self::WorkspaceArtifactWrite => "workspace-artifact-write",
}
}
#[must_use]
pub const fn capability(self) -> CapabilityAtom {
match self {
Self::StatusPublish => CapabilityAtom::StatusPublish,
Self::BufferObserve => CapabilityAtom::BufferObserve,
Self::BufferProposeEdit => CapabilityAtom::BufferProposeEdit,
Self::WorkspaceObserveStart
| Self::WorkspaceObservePoll
| Self::WorkspaceObserveTake => CapabilityAtom::WorkspaceObserve,
Self::WorkspaceArtifactWrite => CapabilityAtom::WorkspaceArtifactWrite,
}
}
#[must_use]
pub const fn operational_import(self) -> PluginOperationalImport {
match self {
Self::StatusPublish => PluginOperationalImport::StatusPublish,
Self::BufferObserve => PluginOperationalImport::BufferObserve,
Self::BufferProposeEdit => PluginOperationalImport::BufferProposeEdit,
Self::WorkspaceObserveStart => PluginOperationalImport::WorkspaceObserveStart,
Self::WorkspaceObservePoll => PluginOperationalImport::WorkspaceObservePoll,
Self::WorkspaceObserveTake => PluginOperationalImport::WorkspaceObserveTake,
Self::WorkspaceArtifactWrite => PluginOperationalImport::WorkspaceArtifactWrite,
}
}
}
impl TryFrom<&str> for WitHostImport {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"status-publish" => Ok(Self::StatusPublish),
"buffer-observe" => Ok(Self::BufferObserve),
"buffer-propose-edit" => Ok(Self::BufferProposeEdit),
"workspace-observe-start" => Ok(Self::WorkspaceObserveStart),
"workspace-observe-poll" => Ok(Self::WorkspaceObservePoll),
"workspace-observe-take" => Ok(Self::WorkspaceObserveTake),
"workspace-artifact-write" => Ok(Self::WorkspaceArtifactWrite),
_unknown => Err(()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WitWorkspaceObserveTaskImport {
Start,
Poll,
Take,
}
impl WitWorkspaceObserveTaskImport {
#[must_use]
pub const fn all() -> [Self; 3] {
[Self::Start, Self::Poll, Self::Take]
}
#[must_use]
pub const fn names() -> [&'static str; 3] {
[
Self::Start.wit_name(),
Self::Poll.wit_name(),
Self::Take.wit_name(),
]
}
#[must_use]
pub const fn wit_name(self) -> &'static str {
match self {
Self::Start => "workspace-observe-start",
Self::Poll => "workspace-observe-poll",
Self::Take => "workspace-observe-take",
}
}
#[must_use]
pub const fn capability(self) -> CapabilityAtom {
CapabilityAtom::WorkspaceObserve
}
#[must_use]
pub const fn operational_import(self) -> PluginOperationalImport {
match self {
Self::Start => PluginOperationalImport::WorkspaceObserveStart,
Self::Poll => PluginOperationalImport::WorkspaceObservePoll,
Self::Take => PluginOperationalImport::WorkspaceObserveTake,
}
}
}
impl TryFrom<&str> for WitWorkspaceObserveTaskImport {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"workspace-observe-start" => Ok(Self::Start),
"workspace-observe-poll" => Ok(Self::Poll),
"workspace-observe-take" => Ok(Self::Take),
_unknown => Err(()),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum WitCapability {
BufferObserve,
BufferProposeEdit,
WorkspaceObserve,
WorkspaceArtifactWrite,
StatusPublish,
}
impl WitCapability {
#[must_use]
pub const fn all() -> [Self; 5] {
[
Self::BufferObserve,
Self::BufferProposeEdit,
Self::WorkspaceObserve,
Self::WorkspaceArtifactWrite,
Self::StatusPublish,
]
}
#[must_use]
pub const fn from_atom(atom: CapabilityAtom) -> Self {
match atom {
CapabilityAtom::BufferObserve => Self::BufferObserve,
CapabilityAtom::BufferProposeEdit => Self::BufferProposeEdit,
CapabilityAtom::WorkspaceObserve => Self::WorkspaceObserve,
CapabilityAtom::WorkspaceArtifactWrite => Self::WorkspaceArtifactWrite,
CapabilityAtom::StatusPublish => Self::StatusPublish,
}
}
#[must_use]
pub const fn atom(self) -> CapabilityAtom {
match self {
Self::BufferObserve => CapabilityAtom::BufferObserve,
Self::BufferProposeEdit => CapabilityAtom::BufferProposeEdit,
Self::WorkspaceObserve => CapabilityAtom::WorkspaceObserve,
Self::WorkspaceArtifactWrite => CapabilityAtom::WorkspaceArtifactWrite,
Self::StatusPublish => CapabilityAtom::StatusPublish,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.atom().as_str()
}
#[must_use]
pub const fn shape(self) -> PluginCapabilityShape {
self.atom().shape()
}
pub fn as_ref(
self,
path: Option<&str>,
) -> Result<PluginCapabilityRef<'_>, WitCapabilityRefError> {
self.atom().authorization_ref(path)
}
}
impl TryFrom<&str> for WitCapability {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
CapabilityAtom::try_from(value).map(Self::from_atom)
}
}
#[cfg(test)]
mod tests {
use super::{
CAPABILITY_NAMES, WIT_HANDLE_RESOURCES, WIT_HOST_IMPORTS, WIT_HOST_INTERFACE, WIT_PACKAGE,
WIT_PLUGIN_EXPORTS, WIT_SOURCE, WIT_VERSION, WIT_WORKSPACE_OBSERVE_TASK_IMPORTS, WIT_WORLD,
WIT_WORLD_ID, WitCapability, WitHostImport, WitWorkspaceObserveTaskImport,
};
use crate::plugin::{CURRENT_WIT_WORLD, CapabilityAtom, PluginOperationalImport};
use wit_parser::{Interface, Resolve, TypeDefKind, WorldId, WorldItem, WorldKey};
const CAPABILITY_EXTENSION_CHECKLIST: [&str; 8] = [
"wit",
"rust-capability",
"json-schema",
"goldens",
"manifest-projection",
"host-authorization",
"redaction",
"docs",
];
const FORBIDDEN_RESOURCE_AUTHORITY_TERMS: [&str; 9] = [
"bevy",
"entity",
"filesystem",
"pathbuf",
"host-path",
"raw-path",
"file-descriptor",
"permission",
"capability",
];
const FORBIDDEN_SCALAR_HANDLE_TERMS: [&str; 6] = [
"handle-id",
"handle-secret",
"view-generation",
"view-secret",
"buffer-generation",
"buffer-secret",
];
#[test]
fn wit_world_id_matches_current_world() {
assert_eq!(WIT_WORLD_ID, CURRENT_WIT_WORLD);
assert_eq!(
WIT_WORLD_ID,
format!("{WIT_PACKAGE}/{WIT_WORLD}@{WIT_VERSION}")
);
}
#[test]
fn capability_names_match_capability_variants() {
let names = WitCapability::all()
.into_iter()
.map(WitCapability::as_str)
.collect::<Vec<_>>();
assert_eq!(names, CAPABILITY_NAMES);
}
#[test]
fn wit_capabilities_adapt_policy_atoms() {
let atoms = WitCapability::all()
.into_iter()
.map(WitCapability::atom)
.collect::<Vec<_>>();
assert_eq!(atoms, CapabilityAtom::all());
for atom in CapabilityAtom::all() {
let capability = WitCapability::from_atom(atom);
assert_eq!(capability.atom(), atom);
assert_eq!(capability.as_str(), atom.as_str());
assert_eq!(capability.shape(), atom.shape());
assert_eq!(capability.as_ref(None), atom.authorization_ref(None));
assert_eq!(WitCapability::try_from(atom.as_str()), Ok(capability));
}
}
#[test]
fn host_import_names_match_host_import_variants() {
let names = WitHostImport::all()
.into_iter()
.map(WitHostImport::wit_name)
.collect::<Vec<_>>();
assert_eq!(names, WIT_HOST_IMPORTS);
}
#[test]
fn host_import_metadata_matches_authority_and_diagnostics() {
for (import, wit_name, capability, operational) in [
(
WitHostImport::StatusPublish,
"status-publish",
"status.publish",
PluginOperationalImport::StatusPublish,
),
(
WitHostImport::BufferObserve,
"buffer-observe",
"buffer.observe",
PluginOperationalImport::BufferObserve,
),
(
WitHostImport::BufferProposeEdit,
"buffer-propose-edit",
"buffer.propose_edit",
PluginOperationalImport::BufferProposeEdit,
),
(
WitHostImport::WorkspaceObserveStart,
"workspace-observe-start",
"workspace.observe",
PluginOperationalImport::WorkspaceObserveStart,
),
(
WitHostImport::WorkspaceObservePoll,
"workspace-observe-poll",
"workspace.observe",
PluginOperationalImport::WorkspaceObservePoll,
),
(
WitHostImport::WorkspaceObserveTake,
"workspace-observe-take",
"workspace.observe",
PluginOperationalImport::WorkspaceObserveTake,
),
(
WitHostImport::WorkspaceArtifactWrite,
"workspace-artifact-write",
"workspace.artifact_write",
PluginOperationalImport::WorkspaceArtifactWrite,
),
] {
assert_eq!(import.wit_name(), wit_name);
assert_eq!(WitHostImport::try_from(wit_name), Ok(import));
assert_eq!(import.capability().as_str(), capability);
assert_eq!(import.operational_import(), operational);
}
}
#[test]
fn workspace_observe_task_import_group_is_exposed_complete() {
let task_imports = WitWorkspaceObserveTaskImport::all()
.into_iter()
.map(WitWorkspaceObserveTaskImport::wit_name)
.collect::<Vec<_>>();
assert_eq!(task_imports, WIT_WORKSPACE_OBSERVE_TASK_IMPORTS);
for (import, wit_name, operational) in [
(
WitWorkspaceObserveTaskImport::Start,
"workspace-observe-start",
PluginOperationalImport::WorkspaceObserveStart,
),
(
WitWorkspaceObserveTaskImport::Poll,
"workspace-observe-poll",
PluginOperationalImport::WorkspaceObservePoll,
),
(
WitWorkspaceObserveTaskImport::Take,
"workspace-observe-take",
PluginOperationalImport::WorkspaceObserveTake,
),
] {
assert_eq!(import.wit_name(), wit_name);
assert_eq!(
WitWorkspaceObserveTaskImport::try_from(wit_name),
Ok(import)
);
assert_eq!(import.capability(), CapabilityAtom::WorkspaceObserve);
assert_eq!(import.operational_import(), operational);
}
let exposed = import_names_from_wit_interface(WIT_SOURCE, WIT_HOST_INTERFACE);
let exposed_task_imports = WIT_WORKSPACE_OBSERVE_TASK_IMPORTS
.into_iter()
.filter(|name| exposed.iter().any(|exposed| exposed.as_str() == *name))
.collect::<Vec<_>>();
assert!(
exposed_task_imports.is_empty()
|| exposed_task_imports.as_slice() == WIT_WORKSPACE_OBSERVE_TASK_IMPORTS.as_slice(),
"workspace.observe task imports must be exposed as a complete start/poll/take group"
);
assert_eq!(exposed_task_imports, WIT_WORKSPACE_OBSERVE_TASK_IMPORTS);
assert!(
!exposed.iter().any(|name| name == "workspace-observe"),
"workspace.observe must not be exposed as a blocking synchronous import"
);
for name in WIT_WORKSPACE_OBSERVE_TASK_IMPORTS {
assert!(WitHostImport::try_from(name).is_ok());
}
assert!(WitWorkspaceObserveTaskImport::try_from("workspace-observe").is_err());
}
#[test]
fn old_v1_capability_names_fail_closed() {
for name in [
"buffer.read",
"buffer.edit",
"workspace.read",
"workspace.write",
"status.write",
] {
assert!(WitCapability::try_from(name).is_err());
}
}
#[test]
fn checked_wit_source_matches_current_world() {
assert!(WIT_SOURCE.contains(&format!("package {WIT_PACKAGE}@{WIT_VERSION};")));
assert!(WIT_SOURCE.contains(&format!("interface {WIT_HOST_INTERFACE}")));
assert!(WIT_SOURCE.contains(&format!("world {WIT_WORLD}")));
for resource in WIT_HANDLE_RESOURCES {
assert!(WIT_SOURCE.contains(&format!("resource {resource}")));
}
}
#[test]
fn checked_wit_capability_enum_matches_rust_vocabulary() {
let wit_cases = capability_cases_from_wit(WIT_SOURCE);
let rust_cases = CAPABILITY_NAMES
.into_iter()
.map(|name| name.replace(['.', '_'], "-"))
.collect::<Vec<_>>();
assert_eq!(wit_cases, rust_cases);
}
#[test]
fn checked_wit_resources_match_host_handle_vocabulary() {
assert_eq!(
resource_names_from_wit_interface(WIT_SOURCE, WIT_HOST_INTERFACE),
WIT_HANDLE_RESOURCES
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>()
);
}
#[test]
fn capability_extension_checklist_covers_authority_boundaries() {
assert_eq!(
CAPABILITY_EXTENSION_CHECKLIST,
[
"wit",
"rust-capability",
"json-schema",
"goldens",
"manifest-projection",
"host-authorization",
"redaction",
"docs",
]
);
}
#[test]
fn checked_wit_resources_do_not_expose_host_authority() {
for declaration in resource_declarations_from_wit_interface(WIT_SOURCE, WIT_HOST_INTERFACE)
{
let declaration = declaration.to_ascii_lowercase();
for term in FORBIDDEN_RESOURCE_AUTHORITY_TERMS {
assert!(
!declaration.contains(term),
"WIT resource declarations must stay opaque: {declaration}"
);
}
}
}
#[test]
fn checked_wit_does_not_reintroduce_scalar_handle_authority() {
for term in FORBIDDEN_SCALAR_HANDLE_TERMS {
assert!(
!WIT_SOURCE.contains(term),
"WIT must use resources instead of scalar handle authority: {term}"
);
}
}
#[test]
fn checked_wit_exports_match_lifecycle_contract() {
assert_eq!(
export_names_from_wit_world(WIT_SOURCE, WIT_WORLD),
WIT_PLUGIN_EXPORTS
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>()
);
}
#[test]
fn checked_wit_host_imports_are_explicit() {
assert_eq!(
import_names_from_wit_interface(WIT_SOURCE, WIT_HOST_INTERFACE),
WIT_HOST_IMPORTS
.into_iter()
.map(str::to_owned)
.collect::<Vec<_>>()
);
}
fn capability_cases_from_wit(source: &str) -> Vec<String> {
let (resolve, world) = parsed_wit_world(source, WIT_WORLD);
let interface = wit_host_interface(&resolve, world, WIT_HOST_INTERFACE);
let capability = interface
.types
.get("capability")
.copied()
.expect("WIT source should define capability enum");
match &resolve.types[capability].kind {
TypeDefKind::Enum(capability) => capability
.cases
.iter()
.map(|case| case.name.clone())
.collect(),
kind => panic!("WIT capability should be an enum, got {}", kind.as_str()),
}
}
fn resource_declarations_from_wit_interface(source: &str, interface: &str) -> Vec<String> {
let (resolve, world) = parsed_wit_world(source, WIT_WORLD);
let interface = wit_host_interface(&resolve, world, interface);
resource_names_from_interface(&resolve, interface)
.into_iter()
.map(|name| format!("resource {name}"))
.collect()
}
fn resource_names_from_wit_interface(source: &str, interface: &str) -> Vec<String> {
let (resolve, world) = parsed_wit_world(source, WIT_WORLD);
let interface = wit_host_interface(&resolve, world, interface);
resource_names_from_interface(&resolve, interface)
}
fn import_names_from_wit_interface(source: &str, interface: &str) -> Vec<String> {
let (resolve, world) = parsed_wit_world(source, WIT_WORLD);
let interface = wit_host_interface(&resolve, world, interface);
interface.functions.keys().cloned().collect()
}
fn resource_names_from_interface(resolve: &Resolve, interface: &Interface) -> Vec<String> {
interface
.types
.iter()
.filter(|(_name, id)| matches!(resolve.types[**id].kind, TypeDefKind::Resource))
.map(|(name, _id)| name.clone())
.collect()
}
fn export_names_from_wit_world(source: &str, world: &str) -> Vec<String> {
let (resolve, world) = parsed_wit_world(source, world);
resolve.worlds[world]
.exports
.iter()
.filter_map(|(key, item)| match (key, item) {
(WorldKey::Name(name), WorldItem::Function(_)) => Some(name.clone()),
(WorldKey::Name(_), WorldItem::Interface { .. } | WorldItem::Type(_))
| (WorldKey::Interface(_), _) => None,
})
.collect()
}
fn parsed_wit_world(source: &str, world: &str) -> (Resolve, WorldId) {
let mut resolve = Resolve::new();
let package = resolve
.push_str("alma-editor-plugin.wit", source)
.expect("WIT source should parse");
let world = resolve
.select_world(&[package], Some(world))
.expect("WIT source should define world");
(resolve, world)
}
fn wit_host_interface<'resolve>(
resolve: &'resolve Resolve,
world: WorldId,
interface: &str,
) -> &'resolve Interface {
resolve.worlds[world]
.imports
.iter()
.find_map(|(key, item)| match item {
WorldItem::Interface { id, .. } => {
let imported_name_matches =
matches!(key, WorldKey::Name(name) if name == interface);
let declared_name_matches =
resolve.interfaces[*id].name.as_deref() == Some(interface);
(imported_name_matches || declared_name_matches)
.then(|| &resolve.interfaces[*id])
}
WorldItem::Function(_) | WorldItem::Type(_) => None,
})
.expect("WIT world should import host interface")
}
}