use std::collections::HashMap;
use crate::ports::graphql_plugin::{CostThrottleConfig, GraphQlTargetPlugin};
use crate::ports::{GraphQlAuth, GraphQlAuthKind};
#[derive(Debug, Clone)]
pub struct GenericGraphQlPlugin {
name: String,
endpoint: String,
headers: HashMap<String, String>,
auth: Option<GraphQlAuth>,
throttle: Option<CostThrottleConfig>,
page_size: usize,
description: String,
}
impl GenericGraphQlPlugin {
#[must_use]
pub fn builder() -> GenericGraphQlPluginBuilder {
GenericGraphQlPluginBuilder::default()
}
#[must_use]
pub const fn cost_throttle_config(&self) -> Option<&CostThrottleConfig> {
self.throttle.as_ref()
}
}
impl GraphQlTargetPlugin for GenericGraphQlPlugin {
fn name(&self) -> &str {
&self.name
}
fn endpoint(&self) -> &str {
&self.endpoint
}
fn version_headers(&self) -> HashMap<String, String> {
self.headers.clone()
}
fn default_auth(&self) -> Option<GraphQlAuth> {
self.auth.clone()
}
fn default_page_size(&self) -> usize {
self.page_size
}
fn description(&self) -> &str {
&self.description
}
fn supports_cursor_pagination(&self) -> bool {
true
}
fn cost_throttle_config(&self) -> Option<CostThrottleConfig> {
self.throttle.clone()
}
}
#[derive(Debug, Default)]
pub struct GenericGraphQlPluginBuilder {
name: Option<String>,
endpoint: Option<String>,
headers: HashMap<String, String>,
auth: Option<GraphQlAuth>,
throttle: Option<CostThrottleConfig>,
page_size: usize,
description: String,
}
impl GenericGraphQlPluginBuilder {
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
#[must_use]
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers = headers;
self
}
#[must_use]
pub fn auth(mut self, auth: GraphQlAuth) -> Self {
self.auth = Some(auth);
self
}
#[must_use]
pub fn bearer_auth(mut self, token: impl Into<String>) -> Self {
self.auth = Some(GraphQlAuth {
kind: GraphQlAuthKind::Bearer,
token: token.into(),
header_name: None,
});
self
}
#[must_use]
pub const fn cost_throttle(mut self, throttle: CostThrottleConfig) -> Self {
self.throttle = Some(throttle);
self
}
#[must_use]
pub const fn page_size(mut self, page_size: usize) -> Self {
self.page_size = page_size;
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn build(self) -> Result<GenericGraphQlPlugin, BuildError> {
Ok(GenericGraphQlPlugin {
name: self.name.ok_or(BuildError::MissingName)?,
endpoint: self.endpoint.ok_or(BuildError::MissingEndpoint)?,
headers: self.headers,
auth: self.auth,
throttle: self.throttle,
page_size: if self.page_size == 0 {
50
} else {
self.page_size
},
description: self.description,
})
}
}
#[derive(Debug, thiserror::Error)]
pub enum BuildError {
#[error("plugin name is required — call .name(\"...\")")]
MissingName,
#[error("plugin endpoint is required — call .endpoint(\"...\")")]
MissingEndpoint,
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn minimal_plugin() -> GenericGraphQlPlugin {
GenericGraphQlPlugin::builder()
.name("test")
.endpoint("https://api.example.com/graphql")
.build()
.unwrap()
}
#[test]
fn builder_minimal_roundtrip() {
let p = minimal_plugin();
assert_eq!(p.name(), "test");
assert_eq!(p.endpoint(), "https://api.example.com/graphql");
assert_eq!(p.default_page_size(), 50); assert!(p.default_auth().is_none());
assert!(p.cost_throttle_config().is_none());
assert!(p.version_headers().is_empty());
}
#[test]
fn builder_full_roundtrip() {
let plugin = GenericGraphQlPlugin::builder()
.name("github")
.endpoint("https://api.github.com/graphql")
.bearer_auth("ghp_test")
.header("X-Github-Next-Global-ID", "1")
.cost_throttle(CostThrottleConfig::default())
.page_size(30)
.description("GitHub v4")
.build()
.unwrap();
assert_eq!(plugin.name(), "github");
assert_eq!(plugin.default_page_size(), 30);
assert_eq!(plugin.description(), "GitHub v4");
assert!(plugin.default_auth().is_some());
assert!(plugin.cost_throttle_config().is_some());
let headers = plugin.version_headers();
assert_eq!(
headers.get("X-Github-Next-Global-ID").map(String::as_str),
Some("1")
);
}
#[test]
fn builder_error_missing_name() {
let result = GenericGraphQlPlugin::builder()
.endpoint("https://api.example.com/graphql")
.build();
assert!(matches!(result, Err(BuildError::MissingName)));
}
#[test]
fn builder_error_missing_endpoint() {
let result = GenericGraphQlPlugin::builder().name("api").build();
assert!(matches!(result, Err(BuildError::MissingEndpoint)));
}
#[test]
fn page_size_zero_defaults_to_50() {
let plugin = GenericGraphQlPlugin::builder()
.name("api")
.endpoint("https://api.example.com/graphql")
.page_size(0)
.build()
.unwrap();
assert_eq!(plugin.default_page_size(), 50);
}
#[test]
fn headers_map_replacement() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("X-Foo".to_string(), "bar".to_string());
let plugin = GenericGraphQlPlugin::builder()
.name("api")
.endpoint("https://api.example.com/graphql")
.headers(map)
.build()
.unwrap();
assert_eq!(
plugin.version_headers().get("X-Foo").map(String::as_str),
Some("bar")
);
}
}