pinakes-plugin-api: add required_endpoints and theme_extensions to manifest UI section
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I203fabfe07548106abb5bac760bbfec06a6a6964
This commit is contained in:
parent
6e442065b1
commit
5a0901ba95
2 changed files with 122 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
# For plugin manifest parsing
|
# For plugin manifest parsing
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,33 @@ pub struct UiSection {
|
||||||
/// Widgets to inject into existing host pages
|
/// Widgets to inject into existing host pages
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub widgets: Vec<UiWidget>,
|
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
|
/// 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
|
// Validate UI pages
|
||||||
for (idx, page_entry) in self.ui.pages.iter().enumerate() {
|
for (idx, page_entry) in self.ui.pages.iter().enumerate() {
|
||||||
match page_entry {
|
match page_entry {
|
||||||
|
|
@ -663,4 +699,89 @@ gap = 16
|
||||||
let manifest = PluginManifest::parse_str(toml).unwrap();
|
let manifest = PluginManifest::parse_str(toml).unwrap();
|
||||||
assert_eq!(manifest.ui.pages.len(), 2);
|
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