use faucet_core::{AuthSpec, DEFAULT_BATCH_SIZE};
use reqwest::header::HeaderMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "config", rename_all = "snake_case")]
pub enum GraphqlAuth {
None,
Bearer { token: String },
Custom { headers: HashMap<String, String> },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GraphqlPagination {
pub has_next_page_path: String,
pub cursor_path: String,
pub cursor_variable: String,
pub page_size_variable: String,
}
impl Default for GraphqlPagination {
fn default() -> Self {
Self {
has_next_page_path: "$.data.*.pageInfo.hasNextPage".into(),
cursor_path: "$.data.*.pageInfo.endCursor".into(),
cursor_variable: "after".into(),
page_size_variable: "first".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GraphqlStreamConfig {
pub endpoint: String,
pub query: String,
pub variables: Value,
pub auth: AuthSpec<GraphqlAuth>,
#[serde(skip, default)]
pub headers: HeaderMap,
pub records_path: Option<String>,
pub pagination: Option<GraphqlPagination>,
pub max_pages: Option<usize>,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
}
fn default_batch_size() -> usize {
DEFAULT_BATCH_SIZE
}
impl GraphqlStreamConfig {
pub fn new(endpoint: impl Into<String>, query: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
query: query.into(),
variables: Value::Object(Default::default()),
auth: AuthSpec::Inline(GraphqlAuth::None),
headers: HeaderMap::new(),
records_path: None,
pagination: None,
max_pages: None,
batch_size: DEFAULT_BATCH_SIZE,
}
}
pub fn variables(mut self, vars: Value) -> Self {
self.variables = vars;
self
}
pub fn auth(mut self, auth: GraphqlAuth) -> Self {
self.auth = AuthSpec::Inline(auth);
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn records_path(mut self, path: impl Into<String>) -> Self {
self.records_path = Some(path.into());
self
}
pub fn pagination(mut self, pagination: GraphqlPagination) -> Self {
self.pagination = Some(pagination);
self
}
pub fn max_pages(mut self, max: usize) -> Self {
self.max_pages = Some(max);
self
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = batch_size;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn default_config() {
let config = GraphqlStreamConfig::new(
"https://api.example.com/graphql",
"query { users { id name } }",
);
assert_eq!(config.endpoint, "https://api.example.com/graphql");
assert!(config.records_path.is_none());
assert!(config.pagination.is_none());
assert!(config.max_pages.is_none());
}
#[test]
fn builder_methods() {
let config =
GraphqlStreamConfig::new("https://api.example.com/graphql", "query { users { id } }")
.variables(json!({"org": "acme"}))
.records_path("$.data.users.edges[*].node")
.max_pages(10)
.auth(GraphqlAuth::Bearer {
token: "token".into(),
});
assert_eq!(config.variables["org"], "acme");
assert_eq!(config.records_path.unwrap(), "$.data.users.edges[*].node");
assert_eq!(config.max_pages, Some(10));
}
#[test]
fn default_pagination() {
let pag = GraphqlPagination::default();
assert_eq!(pag.cursor_variable, "after");
assert_eq!(pag.page_size_variable, "first");
}
#[test]
fn batch_size_defaults_to_default_batch_size() {
let config = GraphqlStreamConfig::new("https://api.example.com/graphql", "query { x }");
assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
}
#[test]
fn with_batch_size_overrides_default() {
let config = GraphqlStreamConfig::new("https://api.example.com/graphql", "query { x }")
.with_batch_size(250);
assert_eq!(config.batch_size, 250);
}
#[test]
fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
let config = GraphqlStreamConfig::new("https://api.example.com/graphql", "query { x }")
.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 = GraphqlStreamConfig::new("https://api.example.com/graphql", "query { x }")
.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#"{
"endpoint": "https://api.example.com/graphql",
"query": "query { x }",
"variables": {},
"auth": {"type": "none"},
"records_path": null,
"pagination": null,
"max_pages": null,
"batch_size": 500
}"#;
let config: GraphqlStreamConfig = serde_json::from_str(json).unwrap();
assert_eq!(config.batch_size, 500);
}
}