Skip to main content

cloudflare_api/
lib.rs

1use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
2use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderName, HeaderValue};
3use reqwest::{Method, RequestBuilder};
4use serde_json::{Map, Value};
5use std::fmt;
6use std::marker::PhantomData;
7
8pub type ResponseValue<T> = T;
9
10#[derive(Debug, Clone)]
11pub struct Error<E = ()> {
12    message: String,
13    _marker: PhantomData<E>,
14}
15
16impl<E> Error<E> {
17    fn new(message: impl Into<String>) -> Self {
18        Self {
19            message: message.into(),
20            _marker: PhantomData,
21        }
22    }
23}
24
25impl<E> fmt::Display for Error<E> {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(&self.message)
28    }
29}
30
31impl<E: fmt::Debug> std::error::Error for Error<E> {}
32
33#[derive(Clone, Debug)]
34pub struct Client {
35    reqwest_client: reqwest::Client,
36    base_url: String,
37    default_headers: HeaderMap,
38}
39
40impl Default for Client {
41    fn default() -> Self {
42        Self::with_default_baseurl()
43    }
44}
45
46impl Client {
47    pub fn with_default_baseurl() -> Self {
48        Self::from_baseurl("https://api.cloudflare.com/client/v4")
49    }
50
51    pub fn from_baseurl(base_url: impl Into<String>) -> Self {
52        Self {
53            reqwest_client: reqwest::Client::new(),
54            base_url: base_url.into(),
55            default_headers: HeaderMap::new(),
56        }
57    }
58
59    pub fn set_bearer_token(
60        &mut self,
61        token: impl AsRef<str>,
62    ) -> Result<(), reqwest::header::InvalidHeaderValue> {
63        let value = format!("Bearer {}", token.as_ref());
64        self.default_headers
65            .insert(AUTHORIZATION, HeaderValue::from_str(&value)?);
66        Ok(())
67    }
68
69    pub fn set_api_key_auth(
70        &mut self,
71        email: impl AsRef<str>,
72        key: impl AsRef<str>,
73    ) -> Result<(), reqwest::header::InvalidHeaderValue> {
74        self.default_headers.insert(
75            HeaderName::from_static("x-auth-email"),
76            HeaderValue::from_str(email.as_ref())?,
77        );
78        self.default_headers.insert(
79            HeaderName::from_static("x-auth-key"),
80            HeaderValue::from_str(key.as_ref())?,
81        );
82        Ok(())
83    }
84
85    pub fn set_service_key(
86        &mut self,
87        key: impl AsRef<str>,
88    ) -> Result<(), reqwest::header::InvalidHeaderValue> {
89        self.default_headers.insert(
90            HeaderName::from_static("x-auth-user-service-key"),
91            HeaderValue::from_str(key.as_ref())?,
92        );
93        Ok(())
94    }
95
96    pub fn zones_get(&self) -> ZonesGetBuilder<'_> {
97        ZonesGetBuilder {
98            client: self,
99            account_id: None,
100            name: None,
101            status: None,
102            page: None,
103            per_page: None,
104        }
105    }
106
107    pub fn zones_0_get(&self) -> Zones0GetBuilder<'_> {
108        Zones0GetBuilder {
109            client: self,
110            zone_id: None,
111        }
112    }
113
114    pub fn dns_records_for_a_zone_list_dns_records(
115        &self,
116    ) -> DnsRecordsForAZoneListDnsRecordsBuilder<'_> {
117        DnsRecordsForAZoneListDnsRecordsBuilder {
118            client: self,
119            zone_id: None,
120            name: None,
121            page: None,
122            per_page: None,
123        }
124    }
125
126    pub fn dns_records_for_a_zone_create_dns_record(
127        &self,
128    ) -> DnsRecordsForAZoneCreateDnsRecordBuilder<'_> {
129        DnsRecordsForAZoneCreateDnsRecordBuilder {
130            client: self,
131            zone_id: None,
132            body: None,
133        }
134    }
135
136    pub fn dns_records_for_a_zone_delete_dns_record(
137        &self,
138    ) -> DnsRecordsForAZoneDeleteDnsRecordBuilder<'_> {
139        DnsRecordsForAZoneDeleteDnsRecordBuilder {
140            client: self,
141            zone_id: None,
142            dns_record_id: None,
143            body: None,
144        }
145    }
146
147    pub fn worker_routes_list_routes(&self) -> WorkerRoutesListRoutesBuilder<'_> {
148        WorkerRoutesListRoutesBuilder {
149            client: self,
150            zone_id: None,
151        }
152    }
153
154    pub fn worker_routes_create_route(&self) -> WorkerRoutesCreateRouteBuilder<'_> {
155        WorkerRoutesCreateRouteBuilder {
156            client: self,
157            zone_id: None,
158            body: None,
159        }
160    }
161
162    pub fn worker_routes_delete_route(&self) -> WorkerRoutesDeleteRouteBuilder<'_> {
163        WorkerRoutesDeleteRouteBuilder {
164            client: self,
165            zone_id: None,
166            route_id: None,
167            body: None,
168        }
169    }
170
171    fn endpoint(&self, path: &str) -> String {
172        let base = self.base_url.trim_end_matches('/');
173        let suffix = path.trim_start_matches('/');
174        format!("{base}/{suffix}")
175    }
176
177    fn request(&self, method: Method, path: &str) -> RequestBuilder {
178        self.reqwest_client
179            .request(method, self.endpoint(path))
180            .headers(self.default_headers.clone())
181    }
182
183    async fn send_json(&self, request: RequestBuilder) -> Result<ResponseValue<Value>, Error<()>> {
184        let response = request
185            .send()
186            .await
187            .map_err(|e| Error::new(format!("request failed: {e}")))?;
188        let status = response.status();
189        if !status.is_success() {
190            let body = response
191                .text()
192                .await
193                .unwrap_or_else(|_| "<failed to read response body>".to_string());
194            return Err(Error::new(format!(
195                "Cloudflare API error (HTTP {}): {}",
196                status.as_u16(),
197                body
198            )));
199        }
200
201        response
202            .json::<Value>()
203            .await
204            .map_err(|e| Error::new(format!("invalid JSON response: {e}")))
205    }
206}
207
208fn encode_path(value: &str) -> String {
209    utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
210}
211
212#[derive(Clone, Debug)]
213pub struct ZonesGetBuilder<'a> {
214    client: &'a Client,
215    account_id: Option<String>,
216    name: Option<String>,
217    status: Option<String>,
218    page: Option<u32>,
219    per_page: Option<u32>,
220}
221
222impl<'a> ZonesGetBuilder<'a> {
223    pub fn account_id(mut self, value: impl Into<String>) -> Self {
224        self.account_id = Some(value.into());
225        self
226    }
227
228    pub fn name(mut self, value: impl Into<String>) -> Self {
229        self.name = Some(value.into());
230        self
231    }
232
233    pub fn status(mut self, value: impl Into<String>) -> Self {
234        self.status = Some(value.into());
235        self
236    }
237
238    pub fn page(mut self, value: u32) -> Self {
239        self.page = Some(value);
240        self
241    }
242
243    pub fn per_page(mut self, value: u32) -> Self {
244        self.per_page = Some(value);
245        self
246    }
247
248    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
249        let mut request = self.client.request(Method::GET, "/zones");
250        if let Some(account_id) = self.account_id {
251            request = request.query(&[("account.id", account_id)]);
252        }
253        if let Some(name) = self.name {
254            request = request.query(&[("name", name)]);
255        }
256        if let Some(status) = self.status {
257            request = request.query(&[("status", status)]);
258        }
259        if let Some(page) = self.page {
260            request = request.query(&[("page", page)]);
261        }
262        if let Some(per_page) = self.per_page {
263            request = request.query(&[("per_page", per_page)]);
264        }
265        self.client.send_json(request).await
266    }
267}
268
269#[derive(Clone, Debug)]
270pub struct Zones0GetBuilder<'a> {
271    client: &'a Client,
272    zone_id: Option<String>,
273}
274
275impl<'a> Zones0GetBuilder<'a> {
276    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
277        self.zone_id = Some(value.into());
278        self
279    }
280
281    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
282        let zone_id = self
283            .zone_id
284            .ok_or_else(|| Error::new("zone_id is required"))?;
285        let path = format!("/zones/{}", encode_path(&zone_id));
286        self.client
287            .send_json(self.client.request(Method::GET, &path))
288            .await
289    }
290}
291
292#[derive(Clone, Debug)]
293pub struct DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
294    client: &'a Client,
295    zone_id: Option<String>,
296    name: Option<String>,
297    page: Option<u32>,
298    per_page: Option<u32>,
299}
300
301impl<'a> DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
302    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
303        self.zone_id = Some(value.into());
304        self
305    }
306
307    pub fn name(mut self, value: impl Into<String>) -> Self {
308        self.name = Some(value.into());
309        self
310    }
311
312    pub fn page(mut self, value: u32) -> Self {
313        self.page = Some(value);
314        self
315    }
316
317    pub fn per_page(mut self, value: u32) -> Self {
318        self.per_page = Some(value);
319        self
320    }
321
322    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
323        let zone_id = self
324            .zone_id
325            .ok_or_else(|| Error::new("zone_id is required"))?;
326        let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
327        let mut request = self.client.request(Method::GET, &path);
328        if let Some(name) = self.name {
329            request = request.query(&[("name", name)]);
330        }
331        if let Some(page) = self.page {
332            request = request.query(&[("page", page)]);
333        }
334        if let Some(per_page) = self.per_page {
335            request = request.query(&[("per_page", per_page)]);
336        }
337        self.client.send_json(request).await
338    }
339}
340
341#[derive(Clone, Debug)]
342pub struct DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
343    client: &'a Client,
344    zone_id: Option<String>,
345    body: Option<Map<String, Value>>,
346}
347
348impl<'a> DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
349    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
350        self.zone_id = Some(value.into());
351        self
352    }
353
354    pub fn body_map(mut self, body: Map<String, Value>) -> Self {
355        self.body = Some(body);
356        self
357    }
358
359    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
360        let zone_id = self
361            .zone_id
362            .ok_or_else(|| Error::new("zone_id is required"))?;
363        let body = self.body.unwrap_or_default();
364        let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
365        self.client
366            .send_json(self.client.request(Method::POST, &path).json(&body))
367            .await
368    }
369}
370
371#[derive(Clone, Debug)]
372pub struct DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
373    client: &'a Client,
374    zone_id: Option<String>,
375    dns_record_id: Option<String>,
376    body: Option<Map<String, Value>>,
377}
378
379impl<'a> DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
380    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
381        self.zone_id = Some(value.into());
382        self
383    }
384
385    pub fn dns_record_id(mut self, value: impl Into<String>) -> Self {
386        self.dns_record_id = Some(value.into());
387        self
388    }
389
390    pub fn body_map(mut self, body: Map<String, Value>) -> Self {
391        self.body = Some(body);
392        self
393    }
394
395    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
396        let zone_id = self
397            .zone_id
398            .ok_or_else(|| Error::new("zone_id is required"))?;
399        let dns_record_id = self
400            .dns_record_id
401            .ok_or_else(|| Error::new("dns_record_id is required"))?;
402        let body = self.body.unwrap_or_default();
403        let path = format!(
404            "/zones/{}/dns_records/{}",
405            encode_path(&zone_id),
406            encode_path(&dns_record_id)
407        );
408        self.client
409            .send_json(self.client.request(Method::DELETE, &path).json(&body))
410            .await
411    }
412}
413
414#[derive(Clone, Debug)]
415pub struct WorkerRoutesListRoutesBuilder<'a> {
416    client: &'a Client,
417    zone_id: Option<String>,
418}
419
420impl<'a> WorkerRoutesListRoutesBuilder<'a> {
421    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
422        self.zone_id = Some(value.into());
423        self
424    }
425
426    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
427        let zone_id = self
428            .zone_id
429            .ok_or_else(|| Error::new("zone_id is required"))?;
430        let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
431        self.client
432            .send_json(self.client.request(Method::GET, &path))
433            .await
434    }
435}
436
437#[derive(Clone, Debug)]
438pub struct WorkerRoutesCreateRouteBuilder<'a> {
439    client: &'a Client,
440    zone_id: Option<String>,
441    body: Option<Map<String, Value>>,
442}
443
444impl<'a> WorkerRoutesCreateRouteBuilder<'a> {
445    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
446        self.zone_id = Some(value.into());
447        self
448    }
449
450    pub fn body_map(mut self, body: Map<String, Value>) -> Self {
451        self.body = Some(body);
452        self
453    }
454
455    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
456        let zone_id = self
457            .zone_id
458            .ok_or_else(|| Error::new("zone_id is required"))?;
459        let body = self.body.unwrap_or_default();
460        let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
461        self.client
462            .send_json(self.client.request(Method::POST, &path).json(&body))
463            .await
464    }
465}
466
467#[derive(Clone, Debug)]
468pub struct WorkerRoutesDeleteRouteBuilder<'a> {
469    client: &'a Client,
470    zone_id: Option<String>,
471    route_id: Option<String>,
472    body: Option<Map<String, Value>>,
473}
474
475impl<'a> WorkerRoutesDeleteRouteBuilder<'a> {
476    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
477        self.zone_id = Some(value.into());
478        self
479    }
480
481    pub fn route_id(mut self, value: impl Into<String>) -> Self {
482        self.route_id = Some(value.into());
483        self
484    }
485
486    pub fn body_map(mut self, body: Map<String, Value>) -> Self {
487        self.body = Some(body);
488        self
489    }
490
491    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
492        let zone_id = self
493            .zone_id
494            .ok_or_else(|| Error::new("zone_id is required"))?;
495        let route_id = self
496            .route_id
497            .ok_or_else(|| Error::new("route_id is required"))?;
498        let body = self.body.unwrap_or_default();
499        let path = format!(
500            "/zones/{}/workers/routes/{}",
501            encode_path(&zone_id),
502            encode_path(&route_id)
503        );
504        self.client
505            .send_json(self.client.request(Method::DELETE, &path).json(&body))
506            .await
507    }
508}