treewide: cleanup
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ia01590cdeed872cc8ebd16f6ca95f3cc6a6a6964
This commit is contained in:
parent
0ba898c881
commit
185e3b562a
16 changed files with 258 additions and 219 deletions
|
|
@ -2,7 +2,10 @@
|
|||
//!
|
||||
//! Provides data fetching and caching for plugin data sources.
|
||||
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_core::Task;
|
||||
|
|
@ -15,7 +18,7 @@ use crate::client::ApiClient;
|
|||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct PluginPageData {
|
||||
data: HashMap<String, serde_json::Value>,
|
||||
loading: HashMap<String, bool>,
|
||||
loading: HashSet<String>,
|
||||
errors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
|
|
@ -29,13 +32,13 @@ impl PluginPageData {
|
|||
/// Check if a source is currently loading
|
||||
#[must_use]
|
||||
pub fn is_loading(&self, source: &str) -> bool {
|
||||
self.loading.get(source).copied().unwrap_or(false)
|
||||
self.loading.contains(source)
|
||||
}
|
||||
|
||||
/// Get error for a specific source
|
||||
#[must_use]
|
||||
pub fn error(&self, source: &str) -> Option<&String> {
|
||||
self.errors.get(source)
|
||||
pub fn error(&self, source: &str) -> Option<&str> {
|
||||
self.errors.get(source).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Check if there is data for a specific source
|
||||
|
|
@ -52,7 +55,7 @@ impl PluginPageData {
|
|||
/// Set loading state for a source
|
||||
pub fn set_loading(&mut self, source: &str, loading: bool) {
|
||||
if loading {
|
||||
self.loading.insert(source.to_string(), true);
|
||||
self.loading.insert(source.to_string());
|
||||
self.errors.remove(source);
|
||||
} else {
|
||||
self.loading.remove(source);
|
||||
|
|
@ -161,9 +164,10 @@ async fn fetch_endpoint(
|
|||
///
|
||||
/// Endpoint sources are deduplicated by `(path, method, params)`: if multiple
|
||||
/// sources share the same triplet, a single HTTP request is made and the raw
|
||||
/// response is shared, with each source's own `transform` applied independently.
|
||||
/// All unique Endpoint and Static sources are fetched concurrently. Transform
|
||||
/// sources are applied after, in iteration order, against the full result set.
|
||||
/// response is shared, with each source's own `transform` applied
|
||||
/// independently. All unique Endpoint and Static sources are fetched
|
||||
/// concurrently. Transform sources are applied after, in iteration order,
|
||||
/// against the full result set.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
|
|
@ -263,8 +267,15 @@ pub async fn fetch_page_data(
|
|||
..
|
||||
} => {
|
||||
let empty_ctx = serde_json::json!({});
|
||||
fetch_endpoint(&client, path, method.clone(), params, &empty_ctx, &allowed)
|
||||
.await?
|
||||
fetch_endpoint(
|
||||
&client,
|
||||
path,
|
||||
method.clone(),
|
||||
params,
|
||||
&empty_ctx,
|
||||
&allowed,
|
||||
)
|
||||
.await?
|
||||
},
|
||||
DataSource::Static { value } => value.clone(),
|
||||
DataSource::Transform { .. } => unreachable!(),
|
||||
|
|
@ -296,21 +307,60 @@ pub async fn fetch_page_data(
|
|||
}
|
||||
}
|
||||
|
||||
// Process Transform sources sequentially; they reference results above.
|
||||
for (name, source) in data_sources {
|
||||
if let DataSource::Transform {
|
||||
source_name,
|
||||
expression,
|
||||
} = source
|
||||
{
|
||||
let ctx = serde_json::Value::Object(
|
||||
results
|
||||
.iter()
|
||||
.map(|(k, v): (&String, &serde_json::Value)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
// Process Transform sources in dependency order. HashMap iteration order is
|
||||
// non-deterministic, so a Transform referencing another Transform could see
|
||||
// null if the upstream was not yet resolved. The pending loop below defers
|
||||
// any Transform whose upstream is not yet in results, making progress on
|
||||
// each pass until all are resolved. UiPage::validate guarantees no cycles,
|
||||
// so the loop always terminates.
|
||||
let mut pending: Vec<(&String, &String, &Expression)> = data_sources
|
||||
.iter()
|
||||
.filter_map(|(name, source)| {
|
||||
match source {
|
||||
DataSource::Transform {
|
||||
source_name,
|
||||
expression,
|
||||
} => Some((name, source_name, expression)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
while !pending.is_empty() {
|
||||
let prev_len = pending.len();
|
||||
let mut i = 0;
|
||||
while i < pending.len() {
|
||||
let (name, source_name, expression) = pending[i];
|
||||
if results.contains_key(source_name.as_str()) {
|
||||
let ctx = serde_json::Value::Object(
|
||||
results
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
);
|
||||
results.insert(name.clone(), evaluate_expression(expression, &ctx));
|
||||
pending.swap_remove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if pending.len() == prev_len {
|
||||
// No progress: upstream source is missing (should be caught by
|
||||
// UiPage::validate, but handled defensively here).
|
||||
tracing::warn!(
|
||||
"plugin transform dependency unresolvable; processing remaining in \
|
||||
iteration order"
|
||||
);
|
||||
let _ = source_name; // accessible in ctx by its key
|
||||
results.insert(name.clone(), evaluate_expression(expression, &ctx));
|
||||
for (name, _, expression) in pending {
|
||||
let ctx = serde_json::Value::Object(
|
||||
results
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
);
|
||||
results.insert(name.clone(), evaluate_expression(expression, &ctx));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -446,7 +496,7 @@ mod tests {
|
|||
|
||||
// Test error state
|
||||
data.set_error("error".to_string(), "oops".to_string());
|
||||
assert_eq!(data.error("error"), Some(&"oops".to_string()));
|
||||
assert_eq!(data.error("error"), Some("oops"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -522,7 +572,9 @@ mod tests {
|
|||
value: serde_json::json!(true),
|
||||
});
|
||||
|
||||
let results = super::fetch_page_data(&client, &sources, &[]).await.unwrap();
|
||||
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));
|
||||
}
|
||||
|
|
@ -544,7 +596,9 @@ mod tests {
|
|||
value: serde_json::json!({"ok": true}),
|
||||
});
|
||||
|
||||
let results = super::fetch_page_data(&client, &sources, &[]).await.unwrap();
|
||||
let results = super::fetch_page_data(&client, &sources, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(results["raw"], serde_json::json!({"ok": true}));
|
||||
// derived should return the value of "raw" from context
|
||||
assert_eq!(results["derived"], serde_json::json!({"ok": true}));
|
||||
|
|
@ -566,13 +620,13 @@ mod tests {
|
|||
expression: Expression::Literal(serde_json::json!("constant")),
|
||||
});
|
||||
|
||||
let results = super::fetch_page_data(&client, &sources, &[]).await.unwrap();
|
||||
let results = super::fetch_page_data(&client, &sources, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
// A Literal expression returns the literal value, not the source data
|
||||
assert_eq!(results["derived"], serde_json::json!("constant"));
|
||||
}
|
||||
|
||||
// Test: multiple Static sources with the same value each get their own
|
||||
// result; dedup logic does not collapse distinct-named Static sources.
|
||||
#[tokio::test]
|
||||
async fn test_fetch_page_data_deduplicates_identical_endpoints() {
|
||||
use pinakes_plugin_api::DataSource;
|
||||
|
|
@ -589,18 +643,18 @@ mod tests {
|
|||
sources.insert("b".to_string(), DataSource::Static {
|
||||
value: serde_json::json!(1),
|
||||
});
|
||||
let results = super::fetch_page_data(&client, &sources, &[]).await.unwrap();
|
||||
let results = super::fetch_page_data(&client, &sources, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(results["a"], serde_json::json!(1));
|
||||
assert_eq!(results["b"], serde_json::json!(1));
|
||||
assert_eq!(results.len(), 2);
|
||||
}
|
||||
|
||||
// Test: Endpoint sources with identical (path, method, params) but different
|
||||
// transform expressions each get a correctly transformed result. Because the
|
||||
// test runs without a real server the path is checked against the allowlist
|
||||
// before any network call, so we verify the dedup key grouping through the
|
||||
// allowlist rejection path: both names should see the same error message,
|
||||
// proving they were grouped and the single rejection propagates to all names.
|
||||
// Verifies that endpoint sources with identical (path, method, params) are
|
||||
// deduplicated correctly. Because there is no real server, the allowlist
|
||||
// rejection fires before any network call; both names seeing the same error
|
||||
// proves they were grouped and that the single rejection propagated to all.
|
||||
#[tokio::test]
|
||||
async fn test_dedup_groups_endpoint_sources_with_same_key() {
|
||||
use pinakes_plugin_api::{DataSource, Expression, HttpMethod};
|
||||
|
|
@ -640,14 +694,12 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
// Test: multiple Transform sources referencing the same upstream Static source
|
||||
// with different expressions each receive their independently transformed
|
||||
// result. This exercises the transform fan-out behavior that mirrors what
|
||||
// the Endpoint dedup group does after a single shared HTTP request completes:
|
||||
// each member of a group applies its own transform to the shared raw value.
|
||||
// Verifies the transform fan-out behavior: each member of a dedup group
|
||||
// applies its own transform to the shared raw value independently. This
|
||||
// mirrors what Endpoint dedup does after a single shared HTTP request.
|
||||
//
|
||||
// Testing the Endpoint dedup success path with real per-member transforms
|
||||
// requires a mock HTTP server and belongs in an integration test.
|
||||
// Testing Endpoint dedup with real per-member transforms requires a mock HTTP
|
||||
// server and belongs in an integration test.
|
||||
#[tokio::test]
|
||||
async fn test_dedup_transform_applied_per_source() {
|
||||
use pinakes_plugin_api::{DataSource, Expression};
|
||||
|
|
@ -670,8 +722,9 @@ mod tests {
|
|||
expression: Expression::Path("raw_data.name".to_string()),
|
||||
});
|
||||
|
||||
let results =
|
||||
super::fetch_page_data(&client, &sources, &[]).await.unwrap();
|
||||
let results = super::fetch_page_data(&client, &sources, &[])
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
results["raw_data"],
|
||||
serde_json::json!({"count": 42, "name": "test"})
|
||||
|
|
@ -681,8 +734,6 @@ mod tests {
|
|||
assert_eq!(results.len(), 3);
|
||||
}
|
||||
|
||||
// Test: fetch_page_data returns an error when the endpoint data source path is
|
||||
// not listed in the allowed_endpoints slice.
|
||||
#[tokio::test]
|
||||
async fn test_endpoint_blocked_when_not_in_allowlist() {
|
||||
use pinakes_plugin_api::{DataSource, HttpMethod};
|
||||
|
|
@ -705,7 +756,8 @@ mod tests {
|
|||
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"fetch_page_data must return Err when endpoint is not in allowed_endpoints"
|
||||
"fetch_page_data must return Err when endpoint is not in \
|
||||
allowed_endpoints"
|
||||
);
|
||||
let msg = result.unwrap_err();
|
||||
assert!(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue