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}