1use crate::server_fn::ServerFnError;
7use serde::{Deserialize, Serialize, de::DeserializeOwned};
8use std::marker::PhantomData;
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub enum FilterOp {
13 #[default]
15 Exact,
16 IExact,
18 Contains,
20 IContains,
22 Gt,
24 Gte,
26 Lt,
28 Lte,
30 StartsWith,
32 IStartsWith,
34 EndsWith,
36 IEndsWith,
38 In,
40 IsNull,
42 Range,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct Filter {
49 pub field: String,
51 pub op: FilterOp,
53 pub value: serde_json::Value,
55 pub exclude: bool,
57}
58
59impl Filter {
60 pub fn exact(field: impl Into<String>, value: impl Serialize) -> Self {
62 Self {
63 field: field.into(),
64 op: FilterOp::Exact,
65 value: serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
66 exclude: false,
67 }
68 }
69
70 pub fn with_op(field: impl Into<String>, op: FilterOp, value: impl Serialize) -> Self {
72 Self {
73 field: field.into(),
74 op,
75 value: serde_json::to_value(value).unwrap_or(serde_json::Value::Null),
76 exclude: false,
77 }
78 }
79
80 pub fn negate(mut self) -> Self {
82 self.exclude = !self.exclude;
83 self
84 }
85
86 pub fn to_query_param(&self) -> (String, String) {
88 let key = match self.op {
89 FilterOp::Exact => self.field.clone(),
90 FilterOp::IExact => format!("{}__iexact", self.field),
91 FilterOp::Contains => format!("{}__contains", self.field),
92 FilterOp::IContains => format!("{}__icontains", self.field),
93 FilterOp::Gt => format!("{}__gt", self.field),
94 FilterOp::Gte => format!("{}__gte", self.field),
95 FilterOp::Lt => format!("{}__lt", self.field),
96 FilterOp::Lte => format!("{}__lte", self.field),
97 FilterOp::StartsWith => format!("{}__startswith", self.field),
98 FilterOp::IStartsWith => format!("{}__istartswith", self.field),
99 FilterOp::EndsWith => format!("{}__endswith", self.field),
100 FilterOp::IEndsWith => format!("{}__iendswith", self.field),
101 FilterOp::In => format!("{}__in", self.field),
102 FilterOp::IsNull => format!("{}__isnull", self.field),
103 FilterOp::Range => format!("{}__range", self.field),
104 };
105
106 let value = match &self.value {
107 serde_json::Value::String(s) => s.clone(),
108 serde_json::Value::Number(n) => n.to_string(),
109 serde_json::Value::Bool(b) => b.to_string(),
110 serde_json::Value::Array(arr) => arr
111 .iter()
112 .map(|v| match v {
113 serde_json::Value::String(s) => s.clone(),
114 other => other.to_string(),
115 })
116 .collect::<Vec<_>>()
117 .join(","),
118 serde_json::Value::Null => "null".to_string(),
119 other => other.to_string(),
120 };
121
122 (key, value)
123 }
124}
125
126#[derive(Debug, Clone)]
131pub struct ApiQuerySet<T> {
132 endpoint: String,
134 filters: Vec<Filter>,
136 ordering: Vec<String>,
138 limit: Option<usize>,
140 offset: Option<usize>,
142 fields: Vec<String>,
144 _marker: PhantomData<T>,
146}
147
148impl<T> ApiQuerySet<T>
149where
150 T: Serialize + DeserializeOwned,
151{
152 pub fn new(endpoint: impl Into<String>) -> Self {
154 Self {
155 endpoint: endpoint.into(),
156 filters: Vec::new(),
157 ordering: Vec::new(),
158 limit: None,
159 offset: None,
160 fields: Vec::new(),
161 _marker: PhantomData,
162 }
163 }
164
165 pub fn filter(mut self, field: impl Into<String>, value: impl Serialize) -> Self {
172 self.filters.push(Filter::exact(field, value));
173 self
174 }
175
176 pub fn filter_op(
183 mut self,
184 field: impl Into<String>,
185 op: FilterOp,
186 value: impl Serialize,
187 ) -> Self {
188 self.filters.push(Filter::with_op(field, op, value));
189 self
190 }
191
192 pub fn exclude(mut self, field: impl Into<String>, value: impl Serialize) -> Self {
199 self.filters.push(Filter::exact(field, value).negate());
200 self
201 }
202
203 pub fn order_by(mut self, fields: &[&str]) -> Self {
212 self.ordering = fields.iter().map(|s| (*s).to_string()).collect();
213 self
214 }
215
216 pub fn limit(mut self, n: usize) -> Self {
223 self.limit = Some(n);
224 self
225 }
226
227 pub fn offset(mut self, n: usize) -> Self {
234 self.offset = Some(n);
235 self
236 }
237
238 pub fn only(mut self, fields: &[&str]) -> Self {
245 self.fields = fields.iter().map(|s| (*s).to_string()).collect();
246 self
247 }
248
249 pub fn all_clone(&self) -> Self {
251 Self::new(&self.endpoint)
252 }
253
254 pub fn build_url(&self) -> String {
256 let mut params: Vec<(String, String)> = Vec::new();
257
258 for filter in &self.filters {
260 let (key, value) = filter.to_query_param();
261 if filter.exclude {
262 params.push((format!("exclude__{}", key), value));
263 } else {
264 params.push((key, value));
265 }
266 }
267
268 if !self.ordering.is_empty() {
270 params.push(("ordering".to_string(), self.ordering.join(",")));
271 }
272
273 if let Some(limit) = self.limit {
275 params.push(("limit".to_string(), limit.to_string()));
276 }
277 if let Some(offset) = self.offset {
278 params.push(("offset".to_string(), offset.to_string()));
279 }
280
281 if !self.fields.is_empty() {
283 params.push(("fields".to_string(), self.fields.join(",")));
284 }
285
286 if params.is_empty() {
288 self.endpoint.clone()
289 } else {
290 let query_string = params
291 .iter()
292 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
293 .collect::<Vec<_>>()
294 .join("&");
295 format!("{}?{}", self.endpoint, query_string)
296 }
297 }
298
299 #[cfg(wasm)]
300 async fn fetch_response(
301 &self,
302 method: &str,
303 url: &str,
304 body: Option<&str>,
305 ) -> Result<crate::fetch::FetchResponse, ServerFnError> {
306 use crate::csrf::csrf_headers;
307 use crate::fetch;
308
309 let mut headers = Vec::new();
310 if body.is_some() {
311 headers.push(("Content-Type".to_string(), "application/json".to_string()));
312 }
313 if let Some((header_name, header_value)) = csrf_headers() {
314 headers.push((header_name.to_string(), header_value));
315 }
316
317 let response = fetch::request(method, url, body, headers).await?;
318 if !response.is_success() {
319 return Err(ServerFnError::server(
320 response.status(),
321 response.into_text(),
322 ));
323 }
324 Ok(response)
325 }
326
327 #[cfg(wasm)]
328 async fn fetch_json<R>(
329 &self,
330 method: &str,
331 url: &str,
332 body: Option<&str>,
333 ) -> Result<R, ServerFnError>
334 where
335 R: DeserializeOwned,
336 {
337 self.fetch_response(method, url, body).await?.json()
338 }
339
340 #[cfg(wasm)]
342 pub async fn all(&self) -> Result<Vec<T>, ServerFnError> {
343 let url = self.build_url();
344 self.fetch_json("GET", &url, None).await
345 }
346
347 #[cfg(native)]
349 pub async fn all(&self) -> Result<Vec<T>, ServerFnError> {
350 Err(ServerFnError::Network(
351 "API calls not supported outside WASM".to_string(),
352 ))
353 }
354
355 #[cfg(wasm)]
357 pub async fn first(&self) -> Result<Option<T>, ServerFnError>
358 where
359 T: Clone,
360 {
361 let mut queryset = self.clone();
362 queryset.limit = Some(1);
363 let results = queryset.all().await?;
364 Ok(results.into_iter().next())
365 }
366
367 #[cfg(native)]
369 pub async fn first(&self) -> Result<Option<T>, ServerFnError> {
370 Err(ServerFnError::Network(
371 "API calls not supported outside WASM".to_string(),
372 ))
373 }
374
375 #[cfg(wasm)]
377 pub async fn get(&self, pk: impl std::fmt::Display) -> Result<T, ServerFnError> {
378 let url = format!("{}{}/", self.endpoint.trim_end_matches('/'), pk);
379 self.fetch_json("GET", &url, None).await
380 }
381
382 #[cfg(native)]
384 pub async fn get(&self, _pk: impl std::fmt::Display) -> Result<T, ServerFnError> {
385 Err(ServerFnError::Network(
386 "API calls not supported outside WASM".to_string(),
387 ))
388 }
389
390 #[cfg(wasm)]
392 pub async fn count(&self) -> Result<usize, ServerFnError> {
393 let base_url = self.build_url();
394 let separator = if base_url.contains('?') { '&' } else { '?' };
395 let url = format!("{base_url}{separator}count=true");
396
397 #[derive(Deserialize)]
398 struct CountResponse {
399 count: usize,
400 }
401
402 let result: CountResponse = self.fetch_json("GET", &url, None).await?;
403 Ok(result.count)
404 }
405
406 #[cfg(native)]
408 pub async fn count(&self) -> Result<usize, ServerFnError> {
409 Err(ServerFnError::Network(
410 "API calls not supported outside WASM".to_string(),
411 ))
412 }
413
414 pub async fn exists(&self) -> Result<bool, ServerFnError>
416 where
417 Self: Clone,
418 {
419 let count = self.clone().limit(1).count().await?;
420 Ok(count > 0)
421 }
422
423 #[cfg(wasm)]
425 pub async fn create(&self, data: &T) -> Result<T, ServerFnError> {
426 let body =
427 serde_json::to_string(data).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
428 self.fetch_json("POST", &self.endpoint, Some(&body)).await
429 }
430
431 #[cfg(native)]
433 pub async fn create(&self, _data: &T) -> Result<T, ServerFnError> {
434 Err(ServerFnError::Network(
435 "API calls not supported outside WASM".to_string(),
436 ))
437 }
438
439 #[cfg(wasm)]
441 pub async fn update(&self, pk: impl std::fmt::Display, data: &T) -> Result<T, ServerFnError> {
442 let url = format!("{}{}/", self.endpoint.trim_end_matches('/'), pk);
443 let body =
444 serde_json::to_string(data).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
445 self.fetch_json("PUT", &url, Some(&body)).await
446 }
447
448 #[cfg(native)]
450 pub async fn update(&self, _pk: impl std::fmt::Display, _data: &T) -> Result<T, ServerFnError> {
451 Err(ServerFnError::Network(
452 "API calls not supported outside WASM".to_string(),
453 ))
454 }
455
456 #[cfg(wasm)]
458 pub async fn partial_update(
459 &self,
460 pk: impl std::fmt::Display,
461 data: &serde_json::Value,
462 ) -> Result<T, ServerFnError> {
463 let url = format!("{}{}/", self.endpoint.trim_end_matches('/'), pk);
464 let body =
465 serde_json::to_string(data).map_err(|e| ServerFnError::Serialization(e.to_string()))?;
466 self.fetch_json("PATCH", &url, Some(&body)).await
467 }
468
469 #[cfg(native)]
471 pub async fn partial_update(
472 &self,
473 _pk: impl std::fmt::Display,
474 _data: &serde_json::Value,
475 ) -> Result<T, ServerFnError> {
476 Err(ServerFnError::Network(
477 "API calls not supported outside WASM".to_string(),
478 ))
479 }
480
481 #[cfg(wasm)]
483 pub async fn delete(&self, pk: impl std::fmt::Display) -> Result<(), ServerFnError> {
484 let url = format!("{}{}/", self.endpoint.trim_end_matches('/'), pk);
485 self.fetch_response("DELETE", &url, None).await?;
486 Ok(())
487 }
488
489 #[cfg(native)]
491 pub async fn delete(&self, _pk: impl std::fmt::Display) -> Result<(), ServerFnError> {
492 Err(ServerFnError::Network(
493 "API calls not supported outside WASM".to_string(),
494 ))
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use super::*;
501
502 #[test]
503 fn test_filter_exact() {
504 let filter = Filter::exact("name", "test");
505 assert_eq!(filter.field, "name");
506 assert!(!filter.exclude);
507 let (key, value) = filter.to_query_param();
508 assert_eq!(key, "name");
509 assert_eq!(value, "test");
510 }
511
512 #[test]
513 fn test_filter_with_op() {
514 let filter = Filter::with_op("age", FilterOp::Gte, 18);
515 let (key, value) = filter.to_query_param();
516 assert_eq!(key, "age__gte");
517 assert_eq!(value, "18");
518 }
519
520 #[test]
521 fn test_filter_negate() {
522 let filter = Filter::exact("status", "banned").negate();
523 assert!(filter.exclude);
524 }
525
526 #[test]
527 fn test_queryset_build_url_simple() {
528 let qs: ApiQuerySet<serde_json::Value> = ApiQuerySet::new("/api/users/");
529 assert_eq!(qs.build_url(), "/api/users/");
530 }
531
532 #[test]
533 fn test_queryset_build_url_with_filters() {
534 let qs: ApiQuerySet<serde_json::Value> = ApiQuerySet::new("/api/users/")
535 .filter("is_active", true)
536 .filter_op("age", FilterOp::Gte, 18);
537
538 let url = qs.build_url();
539 assert!(url.contains("is_active=true"));
540 assert!(url.contains("age__gte=18"));
541 }
542
543 #[test]
544 fn test_queryset_build_url_with_ordering() {
545 let qs: ApiQuerySet<serde_json::Value> =
546 ApiQuerySet::new("/api/users/").order_by(&["-created_at", "username"]);
547
548 let url = qs.build_url();
549 assert!(url.contains("ordering=-created_at%2Cusername"));
550 }
551
552 #[test]
553 fn test_queryset_build_url_with_pagination() {
554 let qs: ApiQuerySet<serde_json::Value> =
555 ApiQuerySet::new("/api/users/").limit(10).offset(20);
556
557 let url = qs.build_url();
558 assert!(url.contains("limit=10"));
559 assert!(url.contains("offset=20"));
560 }
561
562 #[test]
563 fn test_queryset_build_url_with_fields() {
564 let qs: ApiQuerySet<serde_json::Value> =
565 ApiQuerySet::new("/api/users/").only(&["id", "username"]);
566
567 let url = qs.build_url();
568 assert!(url.contains("fields=id%2Cusername"));
569 }
570
571 #[test]
572 fn test_queryset_chain() {
573 let qs: ApiQuerySet<serde_json::Value> = ApiQuerySet::new("/api/users/")
574 .filter("is_active", true)
575 .exclude("role", "admin")
576 .order_by(&["-created_at"])
577 .limit(10)
578 .offset(0);
579
580 let url = qs.build_url();
581 assert!(url.starts_with("/api/users/?"));
582 assert!(url.contains("is_active=true"));
583 assert!(url.contains("exclude__role=admin"));
584 assert!(url.contains("ordering=-created_at"));
585 assert!(url.contains("limit=10"));
586 }
587
588 #[test]
589 fn test_filter_in_list() {
590 let filter = Filter::with_op("id", FilterOp::In, vec![1, 2, 3]);
591 let (key, value) = filter.to_query_param();
592 assert_eq!(key, "id__in");
593 assert_eq!(value, "1,2,3");
594 }
595}