Skip to main content

faucet_source_graphql/
config.rs

1//! GraphQL source configuration.
2
3use reqwest::header::HeaderMap;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Authentication for GraphQL endpoints.
9#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
10#[serde(tag = "type")]
11pub enum GraphqlAuth {
12    /// No authentication.
13    None,
14    /// Bearer token in the Authorization header.
15    Bearer(String),
16    /// Custom headers (e.g. API keys, cookies).
17    #[serde(skip)]
18    Custom(HeaderMap),
19}
20
21/// Cursor-based pagination configuration for GraphQL.
22///
23/// Most GraphQL APIs use the Relay cursor specification with
24/// `pageInfo { hasNextPage, endCursor }`.
25#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
26pub struct GraphqlPagination {
27    /// JSONPath to the `hasNextPage` boolean in the response.
28    pub has_next_page_path: String,
29    /// JSONPath to the `endCursor` string in the response.
30    pub cursor_path: String,
31    /// Name of the cursor variable in the GraphQL query (default: `"after"`).
32    pub cursor_variable: String,
33    /// Optional page size. Added to variables as `first` (or `page_size_variable`).
34    pub page_size: Option<usize>,
35    /// Name of the page size variable (default: `"first"`).
36    pub page_size_variable: String,
37}
38
39impl Default for GraphqlPagination {
40    fn default() -> Self {
41        Self {
42            has_next_page_path: "$.data.*.pageInfo.hasNextPage".into(),
43            cursor_path: "$.data.*.pageInfo.endCursor".into(),
44            cursor_variable: "after".into(),
45            page_size: None,
46            page_size_variable: "first".into(),
47        }
48    }
49}
50
51/// Configuration for the GraphQL source.
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53pub struct GraphqlStreamConfig {
54    /// GraphQL endpoint URL.
55    pub endpoint: String,
56    /// The GraphQL query string.
57    pub query: String,
58    /// Variables to pass with the query.
59    pub variables: Value,
60    /// Authentication method.
61    pub auth: GraphqlAuth,
62    /// Additional request headers.
63    #[serde(skip, default)]
64    pub headers: HeaderMap,
65    /// JSONPath expression to extract records from the response.
66    pub records_path: Option<String>,
67    /// Pagination configuration. `None` for single-page queries.
68    pub pagination: Option<GraphqlPagination>,
69    /// Maximum number of pages to fetch.
70    pub max_pages: Option<usize>,
71}
72
73impl GraphqlStreamConfig {
74    /// Create a new config with an endpoint and query.
75    pub fn new(endpoint: impl Into<String>, query: impl Into<String>) -> Self {
76        Self {
77            endpoint: endpoint.into(),
78            query: query.into(),
79            variables: Value::Object(Default::default()),
80            auth: GraphqlAuth::None,
81            headers: HeaderMap::new(),
82            records_path: None,
83            pagination: None,
84            max_pages: None,
85        }
86    }
87
88    /// Set the GraphQL variables.
89    pub fn variables(mut self, vars: Value) -> Self {
90        self.variables = vars;
91        self
92    }
93
94    /// Set the authentication method.
95    pub fn auth(mut self, auth: GraphqlAuth) -> Self {
96        self.auth = auth;
97        self
98    }
99
100    /// Set additional headers.
101    pub fn headers(mut self, headers: HeaderMap) -> Self {
102        self.headers = headers;
103        self
104    }
105
106    /// Set the JSONPath expression for record extraction.
107    pub fn records_path(mut self, path: impl Into<String>) -> Self {
108        self.records_path = Some(path.into());
109        self
110    }
111
112    /// Enable cursor-based pagination with the given configuration.
113    pub fn pagination(mut self, pagination: GraphqlPagination) -> Self {
114        self.pagination = Some(pagination);
115        self
116    }
117
118    /// Set the maximum number of pages to fetch.
119    pub fn max_pages(mut self, max: usize) -> Self {
120        self.max_pages = Some(max);
121        self
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use serde_json::json;
129
130    #[test]
131    fn default_config() {
132        let config = GraphqlStreamConfig::new(
133            "https://api.example.com/graphql",
134            "query { users { id name } }",
135        );
136        assert_eq!(config.endpoint, "https://api.example.com/graphql");
137        assert!(config.records_path.is_none());
138        assert!(config.pagination.is_none());
139        assert!(config.max_pages.is_none());
140    }
141
142    #[test]
143    fn builder_methods() {
144        let config =
145            GraphqlStreamConfig::new("https://api.example.com/graphql", "query { users { id } }")
146                .variables(json!({"org": "acme"}))
147                .records_path("$.data.users.edges[*].node")
148                .max_pages(10)
149                .auth(GraphqlAuth::Bearer("token".into()));
150        assert_eq!(config.variables["org"], "acme");
151        assert_eq!(config.records_path.unwrap(), "$.data.users.edges[*].node");
152        assert_eq!(config.max_pages, Some(10));
153    }
154
155    #[test]
156    fn default_pagination() {
157        let pag = GraphqlPagination::default();
158        assert_eq!(pag.cursor_variable, "after");
159        assert_eq!(pag.page_size_variable, "first");
160        assert!(pag.page_size.is_none());
161    }
162}