Skip to main content

cratestack_axum/rpc/
synthesize.rs

1//! Reverse mapping: `RpcListInput` / `RpcGetInput` → URL query string
2//! for the existing list/fetch handlers' parsers.
3
4use cratestack_core::rpc::{RpcGetInput, RpcListInput};
5
6/// Synthesize a URL query string from an [`RpcListInput`] in exactly the
7/// shape the macro-generated `parse_model_list_query` parses. Returns
8/// `None` when the input has no fields set (the existing handler treats a
9/// missing query the same as an empty one — no point allocating).
10pub fn synthesize_list_query(input: &RpcListInput) -> Option<String> {
11    let mut pairs: Vec<(String, String)> = Vec::new();
12    if let Some(limit) = input.limit {
13        pairs.push(("limit".to_owned(), limit.to_string()));
14    }
15    if let Some(offset) = input.offset {
16        pairs.push(("offset".to_owned(), offset.to_string()));
17    }
18    if let Some(cursor) = &input.cursor {
19        pairs.push(("cursor".to_owned(), cursor.clone()));
20    }
21    if let Some(fields) = &input.fields {
22        pairs.push(("fields".to_owned(), fields.join(",")));
23    }
24    if let Some(include) = &input.include {
25        pairs.push(("include".to_owned(), include.join(",")));
26    }
27    for (relation, fields) in &input.include_fields {
28        pairs.push((format!("includeFields[{relation}]"), fields.join(",")));
29    }
30    if let Some(sort) = &input.sort {
31        pairs.push(("sort".to_owned(), sort.clone()));
32    }
33    if let Some(where_expr) = &input.where_expr {
34        pairs.push(("where".to_owned(), where_expr.clone()));
35    }
36    if let Some(or) = &input.or {
37        pairs.push(("or".to_owned(), or.clone()));
38    }
39    for predicate in &input.filters {
40        pairs.push((predicate.key.clone(), predicate.value.clone()));
41    }
42    if let Some(computed_params) = &input.computed_params {
43        pairs.push(("computedParams".to_owned(), computed_params.clone()));
44    }
45
46    if pairs.is_empty() {
47        return None;
48    }
49
50    Some(
51        url::form_urlencoded::Serializer::new(String::new())
52            .extend_pairs(pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())))
53            .finish(),
54    )
55}
56
57/// Synthesize a URL query string from an [`RpcGetInput`] in exactly the
58/// shape the macro-generated `parse_model_fetch_query` parses — the
59/// direct `get` counterpart to [`synthesize_list_query`], so RPC `get`
60/// and REST `GET /<plural>/{id}` run one and the same validation and
61/// projection path. Returns `None` when no field is set.
62pub fn synthesize_get_query<Pk>(input: &RpcGetInput<Pk>) -> Option<String> {
63    let mut pairs: Vec<(String, String)> = Vec::new();
64    if let Some(fields) = &input.fields {
65        pairs.push(("fields".to_owned(), fields.join(",")));
66    }
67    if let Some(include) = &input.include {
68        pairs.push(("include".to_owned(), include.join(",")));
69    }
70    for (relation, fields) in &input.include_fields {
71        pairs.push((format!("includeFields[{relation}]"), fields.join(",")));
72    }
73    if let Some(computed_params) = &input.computed_params {
74        pairs.push(("computedParams".to_owned(), computed_params.clone()));
75    }
76
77    if pairs.is_empty() {
78        return None;
79    }
80
81    Some(
82        url::form_urlencoded::Serializer::new(String::new())
83            .extend_pairs(pairs.iter().map(|(k, v)| (k.as_str(), v.as_str())))
84            .finish(),
85    )
86}