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_base_url()
43    }
44}
45
46impl Client {
47    pub fn with_default_base_url() -> Self {
48        Self::from_base_url("https://api.cloudflare.com/client/v4")
49    }
50
51    pub fn from_base_url(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 list_zones(&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 zone(&self) -> Zones0GetBuilder<'_> {
108        Zones0GetBuilder {
109            client: self,
110            zone_id: None,
111        }
112    }
113
114    pub fn list_dns_records(&self) -> DnsRecordsForAZoneListDnsRecordsBuilder<'_> {
115        DnsRecordsForAZoneListDnsRecordsBuilder {
116            client: self,
117            zone_id: None,
118            name: None,
119            page: None,
120            per_page: None,
121        }
122    }
123
124    pub fn create_dns_record(&self) -> DnsRecordsForAZoneCreateDnsRecordBuilder<'_> {
125        DnsRecordsForAZoneCreateDnsRecordBuilder {
126            client: self,
127            zone_id: None,
128            body: None,
129        }
130    }
131
132    pub fn delete_dns_record(&self) -> DnsRecordsForAZoneDeleteDnsRecordBuilder<'_> {
133        DnsRecordsForAZoneDeleteDnsRecordBuilder {
134            client: self,
135            zone_id: None,
136            dns_record_id: None,
137            body: None,
138        }
139    }
140
141    pub fn list_worker_routes(&self) -> WorkerRoutesListRoutesBuilder<'_> {
142        WorkerRoutesListRoutesBuilder {
143            client: self,
144            zone_id: None,
145        }
146    }
147
148    pub fn create_worker_route(&self) -> WorkerRoutesCreateRouteBuilder<'_> {
149        WorkerRoutesCreateRouteBuilder {
150            client: self,
151            zone_id: None,
152            body: None,
153        }
154    }
155
156    pub fn delete_worker_route(&self) -> WorkerRoutesDeleteRouteBuilder<'_> {
157        WorkerRoutesDeleteRouteBuilder {
158            client: self,
159            zone_id: None,
160            route_id: None,
161            body: None,
162        }
163    }
164
165    fn endpoint(&self, path: &str) -> String {
166        let base = self.base_url.trim_end_matches('/');
167        let suffix = path.trim_start_matches('/');
168        format!("{base}/{suffix}")
169    }
170
171    fn request(&self, method: Method, path: &str) -> RequestBuilder {
172        self.reqwest_client
173            .request(method, self.endpoint(path))
174            .headers(self.default_headers.clone())
175    }
176
177    async fn send_json(&self, request: RequestBuilder) -> Result<ResponseValue<Value>, Error<()>> {
178        let response = request
179            .send()
180            .await
181            .map_err(|e| Error::new(format!("request failed: {e}")))?;
182        let status = response.status();
183        if !status.is_success() {
184            let body = response
185                .text()
186                .await
187                .unwrap_or_else(|_| "<failed to read response body>".to_string());
188            return Err(Error::new(format!(
189                "Cloudflare API error (HTTP {}): {}",
190                status.as_u16(),
191                body
192            )));
193        }
194
195        response
196            .json::<Value>()
197            .await
198            .map_err(|e| Error::new(format!("invalid JSON response: {e}")))
199    }
200}
201
202fn encode_path(value: &str) -> String {
203    utf8_percent_encode(value, NON_ALPHANUMERIC).to_string()
204}
205
206#[derive(Clone, Debug)]
207pub struct ZonesGetBuilder<'a> {
208    client: &'a Client,
209    account_id: Option<String>,
210    name: Option<String>,
211    status: Option<String>,
212    page: Option<u32>,
213    per_page: Option<u32>,
214}
215
216impl<'a> ZonesGetBuilder<'a> {
217    pub fn account_id(mut self, value: impl Into<String>) -> Self {
218        self.account_id = Some(value.into());
219        self
220    }
221
222    pub fn name(mut self, value: impl Into<String>) -> Self {
223        self.name = Some(value.into());
224        self
225    }
226
227    pub fn status(mut self, value: impl Into<String>) -> Self {
228        self.status = Some(value.into());
229        self
230    }
231
232    pub fn page(mut self, value: u32) -> Self {
233        self.page = Some(value);
234        self
235    }
236
237    pub fn per_page(mut self, value: u32) -> Self {
238        self.per_page = Some(value);
239        self
240    }
241
242    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
243        let mut request = self.client.request(Method::GET, "/zones");
244        if let Some(account_id) = self.account_id {
245            request = request.query(&[("account.id", account_id)]);
246        }
247        if let Some(name) = self.name {
248            request = request.query(&[("name", name)]);
249        }
250        if let Some(status) = self.status {
251            request = request.query(&[("status", status)]);
252        }
253        if let Some(page) = self.page {
254            request = request.query(&[("page", page)]);
255        }
256        if let Some(per_page) = self.per_page {
257            request = request.query(&[("per_page", per_page)]);
258        }
259        self.client.send_json(request).await
260    }
261}
262
263#[derive(Clone, Debug)]
264pub struct Zones0GetBuilder<'a> {
265    client: &'a Client,
266    zone_id: Option<String>,
267}
268
269impl<'a> Zones0GetBuilder<'a> {
270    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
271        self.zone_id = Some(value.into());
272        self
273    }
274
275    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
276        let zone_id = self
277            .zone_id
278            .ok_or_else(|| Error::new("zone_id is required"))?;
279        let path = format!("/zones/{}", encode_path(&zone_id));
280        self.client
281            .send_json(self.client.request(Method::GET, &path))
282            .await
283    }
284}
285
286#[derive(Clone, Debug)]
287pub struct DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
288    client: &'a Client,
289    zone_id: Option<String>,
290    name: Option<String>,
291    page: Option<u32>,
292    per_page: Option<u32>,
293}
294
295impl<'a> DnsRecordsForAZoneListDnsRecordsBuilder<'a> {
296    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
297        self.zone_id = Some(value.into());
298        self
299    }
300
301    pub fn name(mut self, value: impl Into<String>) -> Self {
302        self.name = Some(value.into());
303        self
304    }
305
306    pub fn page(mut self, value: u32) -> Self {
307        self.page = Some(value);
308        self
309    }
310
311    pub fn per_page(mut self, value: u32) -> Self {
312        self.per_page = Some(value);
313        self
314    }
315
316    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
317        let zone_id = self
318            .zone_id
319            .ok_or_else(|| Error::new("zone_id is required"))?;
320        let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
321        let mut request = self.client.request(Method::GET, &path);
322        if let Some(name) = self.name {
323            request = request.query(&[("name", name)]);
324        }
325        if let Some(page) = self.page {
326            request = request.query(&[("page", page)]);
327        }
328        if let Some(per_page) = self.per_page {
329            request = request.query(&[("per_page", per_page)]);
330        }
331        self.client.send_json(request).await
332    }
333}
334
335#[derive(Clone, Debug)]
336pub struct DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
337    client: &'a Client,
338    zone_id: Option<String>,
339    body: Option<Map<String, Value>>,
340}
341
342impl<'a> DnsRecordsForAZoneCreateDnsRecordBuilder<'a> {
343    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
344        self.zone_id = Some(value.into());
345        self
346    }
347
348    pub fn body(mut self, body: Map<String, Value>) -> Self {
349        self.body = Some(body);
350        self
351    }
352
353    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
354        let zone_id = self
355            .zone_id
356            .ok_or_else(|| Error::new("zone_id is required"))?;
357        let body = self.body.unwrap_or_default();
358        let path = format!("/zones/{}/dns_records", encode_path(&zone_id));
359        self.client
360            .send_json(self.client.request(Method::POST, &path).json(&body))
361            .await
362    }
363}
364
365#[derive(Clone, Debug)]
366pub struct DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
367    client: &'a Client,
368    zone_id: Option<String>,
369    dns_record_id: Option<String>,
370    body: Option<Map<String, Value>>,
371}
372
373impl<'a> DnsRecordsForAZoneDeleteDnsRecordBuilder<'a> {
374    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
375        self.zone_id = Some(value.into());
376        self
377    }
378
379    pub fn dns_record_id(mut self, value: impl Into<String>) -> Self {
380        self.dns_record_id = Some(value.into());
381        self
382    }
383
384    pub fn body(mut self, body: Map<String, Value>) -> Self {
385        self.body = Some(body);
386        self
387    }
388
389    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
390        let zone_id = self
391            .zone_id
392            .ok_or_else(|| Error::new("zone_id is required"))?;
393        let dns_record_id = self
394            .dns_record_id
395            .ok_or_else(|| Error::new("dns_record_id is required"))?;
396        let body = self.body.unwrap_or_default();
397        let path = format!(
398            "/zones/{}/dns_records/{}",
399            encode_path(&zone_id),
400            encode_path(&dns_record_id)
401        );
402        self.client
403            .send_json(self.client.request(Method::DELETE, &path).json(&body))
404            .await
405    }
406}
407
408#[derive(Clone, Debug)]
409pub struct WorkerRoutesListRoutesBuilder<'a> {
410    client: &'a Client,
411    zone_id: Option<String>,
412}
413
414impl<'a> WorkerRoutesListRoutesBuilder<'a> {
415    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
416        self.zone_id = Some(value.into());
417        self
418    }
419
420    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
421        let zone_id = self
422            .zone_id
423            .ok_or_else(|| Error::new("zone_id is required"))?;
424        let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
425        self.client
426            .send_json(self.client.request(Method::GET, &path))
427            .await
428    }
429}
430
431#[derive(Clone, Debug)]
432pub struct WorkerRoutesCreateRouteBuilder<'a> {
433    client: &'a Client,
434    zone_id: Option<String>,
435    body: Option<Map<String, Value>>,
436}
437
438impl<'a> WorkerRoutesCreateRouteBuilder<'a> {
439    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
440        self.zone_id = Some(value.into());
441        self
442    }
443
444    pub fn body(mut self, body: Map<String, Value>) -> Self {
445        self.body = Some(body);
446        self
447    }
448
449    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
450        let zone_id = self
451            .zone_id
452            .ok_or_else(|| Error::new("zone_id is required"))?;
453        let body = self.body.unwrap_or_default();
454        let path = format!("/zones/{}/workers/routes", encode_path(&zone_id));
455        self.client
456            .send_json(self.client.request(Method::POST, &path).json(&body))
457            .await
458    }
459}
460
461#[derive(Clone, Debug)]
462pub struct WorkerRoutesDeleteRouteBuilder<'a> {
463    client: &'a Client,
464    zone_id: Option<String>,
465    route_id: Option<String>,
466    body: Option<Map<String, Value>>,
467}
468
469impl<'a> WorkerRoutesDeleteRouteBuilder<'a> {
470    pub fn zone_id(mut self, value: impl Into<String>) -> Self {
471        self.zone_id = Some(value.into());
472        self
473    }
474
475    pub fn route_id(mut self, value: impl Into<String>) -> Self {
476        self.route_id = Some(value.into());
477        self
478    }
479
480    pub fn body(mut self, body: Map<String, Value>) -> Self {
481        self.body = Some(body);
482        self
483    }
484
485    pub async fn send(self) -> Result<ResponseValue<Value>, Error<()>> {
486        let zone_id = self
487            .zone_id
488            .ok_or_else(|| Error::new("zone_id is required"))?;
489        let route_id = self
490            .route_id
491            .ok_or_else(|| Error::new("route_id is required"))?;
492        let body = self.body.unwrap_or_default();
493        let path = format!(
494            "/zones/{}/workers/routes/{}",
495            encode_path(&zone_id),
496            encode_path(&route_id)
497        );
498        self.client
499            .send_json(self.client.request(Method::DELETE, &path).json(&body))
500            .await
501    }
502}