pinakes-ui: fix plugin page data loading; add as_json helper

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6d80eff06e9ca46f916e643d5d8bb6c86a6a6964
This commit is contained in:
raf 2026-03-10 00:01:56 +03:00
commit e55fd5cc98
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF

View file

@ -10,7 +10,7 @@ use pinakes_plugin_api::{DataSource, HttpMethod};
use crate::client::ApiClient;
/// Cached data for a plugin page
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct PluginPageData {
data: HashMap<String, serde_json::Value>,
loading: HashMap<String, bool>,
@ -62,6 +62,19 @@ impl PluginPageData {
self.errors.insert(source, error);
}
/// Convert all resolved data to a single JSON object for expression
/// evaluation
#[must_use]
pub fn as_json(&self) -> serde_json::Value {
serde_json::Value::Object(
self
.data
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
)
}
/// Clear all data
pub fn clear(&mut self) {
self.data.clear();
@ -114,7 +127,18 @@ pub async fn fetch_page_data(
) -> Result<HashMap<String, serde_json::Value>, String> {
let mut results = HashMap::new();
for (name, source) in data_sources {
// Process non-Transform sources first so Transform sources can reference them
let mut ordered: Vec<(&String, &DataSource)> = data_sources
.iter()
.filter(|(_, s)| !matches!(s, DataSource::Transform { .. }))
.collect();
ordered.extend(
data_sources
.iter()
.filter(|(_, s)| matches!(s, DataSource::Transform { .. })),
);
for (name, source) in ordered {
let value = match source {
DataSource::Endpoint { path, method, .. } => {
// Fetch from endpoint (ignoring params, poll_interval, transform for
@ -168,11 +192,13 @@ pub fn use_plugin_data(
match fetch_page_data(&client.read(), &sources).await {
Ok(results) => {
for (name, value) in results {
data.write().set_loading(&name, false);
data.write().set_data(name, value);
}
},
Err(e) => {
for name in sources.keys() {
data.write().set_loading(name, false);
data.write().set_error(name.clone(), e.clone());
}
},
@ -270,4 +296,118 @@ mod tests {
let data = serde_json::json!({"foo": "bar"});
assert!(get_json_path(&data, "nonexistent").is_none());
}
#[test]
fn test_get_json_path_array_out_of_bounds() {
let data = serde_json::json!({"items": ["a"]});
assert!(get_json_path(&data, "items.5").is_none());
}
#[test]
fn test_get_json_path_non_array_index() {
let data = serde_json::json!({"foo": "bar"});
assert!(get_json_path(&data, "foo.0").is_none());
}
#[test]
fn test_as_json_empty() {
let data = PluginPageData::default();
assert_eq!(data.as_json(), serde_json::json!({}));
}
#[test]
fn test_as_json_with_data() {
let mut data = PluginPageData::default();
data.set_data("users".to_string(), serde_json::json!([{"id": 1}]));
data.set_data("count".to_string(), serde_json::json!(42));
let json = data.as_json();
assert_eq!(json["users"], serde_json::json!([{"id": 1}]));
assert_eq!(json["count"], serde_json::json!(42));
}
#[test]
fn test_set_loading_true_clears_error() {
let mut data = PluginPageData::default();
data.set_error("src".to_string(), "oops".to_string());
assert!(data.error("src").is_some());
data.set_loading("src", true);
assert!(data.error("src").is_none());
assert!(data.is_loading("src"));
}
#[test]
fn test_set_loading_false_removes_flag() {
let mut data = PluginPageData::default();
data.set_loading("src", true);
assert!(data.is_loading("src"));
data.set_loading("src", false);
assert!(!data.is_loading("src"));
}
#[test]
fn test_clear_resets_all_state() {
let mut data = PluginPageData::default();
data.set_data("x".to_string(), serde_json::json!(1));
data.set_loading("x", true);
data.set_error("y".to_string(), "err".to_string());
data.clear();
assert!(!data.has_data("x"));
assert!(!data.is_loading("x"));
assert!(data.error("y").is_none());
}
#[test]
fn test_partial_eq() {
let mut a = PluginPageData::default();
let mut b = PluginPageData::default();
assert_eq!(a, b);
a.set_data("k".to_string(), serde_json::json!(1));
assert_ne!(a, b);
b.set_data("k".to_string(), serde_json::json!(1));
assert_eq!(a, b);
}
#[tokio::test]
async fn test_fetch_page_data_static_only() {
use pinakes_plugin_api::DataSource;
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
sources.insert("nums".to_string(), DataSource::Static {
value: serde_json::json!([1, 2, 3]),
});
sources.insert("flag".to_string(), DataSource::Static {
value: serde_json::json!(true),
});
let results = super::fetch_page_data(&client, &sources).await.unwrap();
assert_eq!(results["nums"], serde_json::json!([1, 2, 3]));
assert_eq!(results["flag"], serde_json::json!(true));
}
#[tokio::test]
async fn test_fetch_page_data_transform_after_static() {
use pinakes_plugin_api::{DataSource, Expression};
use crate::client::ApiClient;
let client = ApiClient::default();
let mut sources = HashMap::new();
// Insert Transform before Static in the map to test ordering
sources.insert("derived".to_string(), DataSource::Transform {
source_name: "raw".to_string(),
expression: Expression::Literal(serde_json::Value::Null),
});
sources.insert("raw".to_string(), DataSource::Static {
value: serde_json::json!({"ok": true}),
});
let results = super::fetch_page_data(&client, &sources).await.unwrap();
// raw must have been processed before derived
assert_eq!(results["raw"], serde_json::json!({"ok": true}));
// derived gets source_data from raw (transform is identity for now)
assert_eq!(results["derived"], serde_json::json!({"ok": true}));
}
}