GUI plugins #9
2 changed files with 122 additions and 0 deletions
pinakes-plugin-api: add required_endpoints and theme_extensions to manifest UI section
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I203fabfe07548106abb5bac760bbfec06a6a6964
commit
5a0901ba95
|
|
@ -10,6 +10,7 @@ serde = { workspace = true }
|
|||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
# For plugin manifest parsing
|
||||
toml = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -40,6 +40,33 @@ pub struct UiSection {
|
|||
/// Widgets to inject into existing host pages
|
||||
#[serde(default)]
|
||||
pub widgets: Vec<UiWidget>,
|
||||
|
||||
/// API endpoint paths this plugin's UI requires.
|
||||
/// Each must start with `/api/`. Informational; host may check availability.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub required_endpoints: Vec<String>,
|
||||
|
||||
/// CSS custom property overrides provided by this plugin.
|
||||
/// Keys are property names (e.g. `--accent-color`), values are CSS values.
|
||||
/// The host applies these to `document.documentElement` on startup.
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub theme_extensions: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl UiSection {
|
||||
/// Validate that all declared required endpoints start with `/api/`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string for the first invalid endpoint found.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
for ep in &self.required_endpoints {
|
||||
if !ep.starts_with("/api/") {
|
||||
return Err(format!("required_endpoint must start with '/api/': {ep}"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry for a UI page in the manifest - can be inline or file reference
|
||||
|
|
@ -267,6 +294,15 @@ impl PluginManifest {
|
|||
));
|
||||
}
|
||||
|
||||
// Validate UI section (required_endpoints format); non-fatal: warn only
|
||||
if let Err(e) = self.ui.validate() {
|
||||
tracing::warn!(
|
||||
plugin = %self.plugin.name,
|
||||
error = %e,
|
||||
"plugin UI section has invalid required_endpoints"
|
||||
);
|
||||
}
|
||||
|
||||
// Validate UI pages
|
||||
for (idx, page_entry) in self.ui.pages.iter().enumerate() {
|
||||
match page_entry {
|
||||
|
|
@ -663,4 +699,89 @@ gap = 16
|
|||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||
assert_eq!(manifest.ui.pages.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_section_validate_accepts_api_paths() {
|
||||
let section = UiSection {
|
||||
pages: vec![],
|
||||
widgets: vec![],
|
||||
required_endpoints: vec![
|
||||
"/api/v1/media".to_string(),
|
||||
"/api/plugins/my-plugin/data".to_string(),
|
||||
],
|
||||
theme_extensions: HashMap::new(),
|
||||
};
|
||||
assert!(section.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_section_validate_rejects_non_api_path() {
|
||||
let section = UiSection {
|
||||
pages: vec![],
|
||||
widgets: vec![],
|
||||
required_endpoints: vec!["/not-api/something".to_string()],
|
||||
theme_extensions: HashMap::new(),
|
||||
};
|
||||
assert!(section.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ui_section_validate_rejects_empty_sections_with_bad_path() {
|
||||
let section = UiSection {
|
||||
pages: vec![],
|
||||
widgets: vec![],
|
||||
required_endpoints: vec!["/api/ok".to_string(), "no-slash".to_string()],
|
||||
theme_extensions: HashMap::new(),
|
||||
};
|
||||
let err = section.validate().unwrap_err();
|
||||
assert!(
|
||||
err.contains("no-slash"),
|
||||
"error should mention the bad endpoint"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_extensions_roundtrip() {
|
||||
let toml = r##"
|
||||
[plugin]
|
||||
name = "theme-plugin"
|
||||
version = "1.0.0"
|
||||
api_version = "1.0"
|
||||
kind = ["theme_provider"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "plugin.wasm"
|
||||
|
||||
[ui.theme_extensions]
|
||||
"--accent-color" = "#ff6b6b"
|
||||
"--sidebar-width" = "280px"
|
||||
"##;
|
||||
|
||||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||
assert_eq!(
|
||||
manifest.ui.theme_extensions.get("--accent-color").map(String::as_str),
|
||||
Some("#ff6b6b")
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.ui.theme_extensions.get("--sidebar-width").map(String::as_str),
|
||||
Some("280px")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_extensions_empty_by_default() {
|
||||
let toml = r#"
|
||||
[plugin]
|
||||
name = "no-theme"
|
||||
version = "1.0.0"
|
||||
api_version = "1.0"
|
||||
kind = ["media_type"]
|
||||
|
||||
[plugin.binary]
|
||||
wasm = "plugin.wasm"
|
||||
"#;
|
||||
|
||||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||
assert!(manifest.ui.theme_extensions.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue