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}