add http-json, rss, and media data source adapters

http-json: generic HTTP+JSON polling adapter, converts serde_json to domain Value. 4 tests.
rss: XML RSS feed parser, extracts items into Value array. 1 test.
media: Navidrome/Subsonic getNowPlaying adapter. 2 tests with fake server.
This commit is contained in:
2026-06-18 22:52:28 +02:00
parent e398c240a0
commit 366d98a1ae
9 changed files with 1076 additions and 2 deletions

View File

@@ -0,0 +1,68 @@
use domain::{DataSource, DataSourcePort, Value};
pub struct HttpJsonAdapter {
client: reqwest::Client,
}
#[derive(Debug)]
pub enum HttpJsonError {
Request(reqwest::Error),
NoUrl,
Parse(String),
}
impl std::fmt::Display for HttpJsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HttpJsonError::Request(e) => write!(f, "request: {e}"),
HttpJsonError::NoUrl => write!(f, "no url configured"),
HttpJsonError::Parse(e) => write!(f, "parse: {e}"),
}
}
}
impl HttpJsonAdapter {
pub fn new() -> Self {
Self {
client: reqwest::Client::new(),
}
}
}
fn json_to_value(json: serde_json::Value) -> Value {
match json {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(b),
serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(0.0)),
serde_json::Value::String(s) => Value::String(s),
serde_json::Value::Array(arr) => {
Value::Array(arr.into_iter().map(json_to_value).collect())
}
serde_json::Value::Object(map) => {
Value::Object(map.into_iter().map(|(k, v)| (k, json_to_value(v))).collect())
}
}
}
impl DataSourcePort for HttpJsonAdapter {
type Error = HttpJsonError;
async fn poll(&self, source: &DataSource) -> Result<Value, Self::Error> {
let url = source.config.url.as_ref().ok_or(HttpJsonError::NoUrl)?;
let mut req = self.client.get(url);
for (key, val) in &source.config.headers {
req = req.header(key, val);
}
if let Some(api_key) = &source.config.api_key {
req = req.header("Authorization", format!("Bearer {api_key}"));
}
let resp = req.send().await.map_err(HttpJsonError::Request)?;
let json: serde_json::Value = resp.json().await.map_err(HttpJsonError::Request)?;
Ok(json_to_value(json))
}
}