use faucet_core::{AuthSpec, DEFAULT_BATCH_SIZE};
use reqwest::header::HeaderMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum XmlAuth {
None,
Bearer { token: String },
Basic { username: String, password: String },
Custom { headers: HashMap<String, String> },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum XmlPagination {
PageNumber {
param_name: String,
start_page: usize,
page_size: Option<usize>,
page_size_param: Option<String>,
},
Offset {
offset_param: String,
limit_param: String,
limit: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct XmlStreamConfig {
pub base_url: String,
pub path: String,
#[serde(with = "crate::serde_helpers::http_method")]
#[schemars(with = "String")]
pub method: reqwest::Method,
pub auth: AuthSpec<XmlAuth>,
#[serde(skip, default)]
pub headers: HeaderMap,
pub body: Option<String>,
pub records_element_path: Option<String>,
pub pagination: Option<XmlPagination>,
pub max_pages: Option<usize>,
pub query_params: std::collections::HashMap<String, String>,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
}
fn default_batch_size() -> usize {
DEFAULT_BATCH_SIZE
}
impl XmlStreamConfig {
pub fn new(base_url: impl Into<String>, path: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
path: path.into(),
method: reqwest::Method::GET,
auth: AuthSpec::Inline(XmlAuth::None),
headers: HeaderMap::new(),
body: None,
records_element_path: None,
pagination: None,
max_pages: None,
query_params: std::collections::HashMap::new(),
batch_size: DEFAULT_BATCH_SIZE,
}
}
pub fn method(mut self, method: reqwest::Method) -> Self {
self.method = method;
self
}
pub fn auth(mut self, auth: XmlAuth) -> Self {
self.auth = AuthSpec::Inline(auth);
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn records_element_path(mut self, path: impl Into<String>) -> Self {
self.records_element_path = Some(path.into());
self
}
pub fn pagination(mut self, pagination: XmlPagination) -> Self {
self.pagination = Some(pagination);
self
}
pub fn max_pages(mut self, max: usize) -> Self {
self.max_pages = Some(max);
self
}
pub fn query_param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.query_params.insert(key.into(), value.into());
self
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let config = XmlStreamConfig::new("https://api.example.com", "/users");
assert_eq!(config.base_url, "https://api.example.com");
assert_eq!(config.path, "/users");
assert_eq!(config.method, reqwest::Method::GET);
assert!(config.records_element_path.is_none());
}
#[test]
fn soap_config() {
let config = XmlStreamConfig::new("https://api.example.com", "/soap")
.method(reqwest::Method::POST)
.body("<Envelope><Body><GetUsers/></Body></Envelope>")
.records_element_path("Envelope.Body.GetUsersResponse.Users.User");
assert_eq!(config.method, reqwest::Method::POST);
assert!(config.body.is_some());
assert_eq!(
config.records_element_path.unwrap(),
"Envelope.Body.GetUsersResponse.Users.User"
);
}
#[test]
fn batch_size_defaults_to_default_batch_size() {
let config = XmlStreamConfig::new("https://api.example.com", "/users");
assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
}
#[test]
fn with_batch_size_overrides_default() {
let config = XmlStreamConfig::new("https://api.example.com", "/users").with_batch_size(500);
assert_eq!(config.batch_size, 500);
}
#[test]
fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
let config = XmlStreamConfig::new("https://api.example.com", "/users").with_batch_size(0);
assert_eq!(config.batch_size, 0);
assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
}
#[test]
fn batch_size_above_max_is_rejected_by_validate_batch_size() {
let config = XmlStreamConfig::new("https://api.example.com", "/users")
.with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
}
#[test]
fn batch_size_deserializes_from_json() {
let json = r#"{
"base_url": "https://api.example.com",
"path": "/users.xml",
"method": "GET",
"auth": { "type": "none" },
"body": null,
"records_element_path": "root.user",
"pagination": null,
"max_pages": null,
"query_params": {},
"batch_size": 250
}"#;
let config: XmlStreamConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.batch_size, 250);
}
#[test]
fn batch_size_defaults_when_missing_from_json() {
let json = r#"{
"base_url": "https://api.example.com",
"path": "/users.xml",
"method": "GET",
"auth": { "type": "none" },
"body": null,
"records_element_path": null,
"pagination": null,
"max_pages": null,
"query_params": {}
}"#;
let config: XmlStreamConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
}
}