cratestack_axum/rpc/inputs.rs
1//! RPC input shapes.
2//!
3//! The RPC binding wraps each model verb's input in a stable, model-agnostic
4//! shape. The macro decodes the body into one of these, then reconstructs
5//! whatever axum extractor the existing CRUD handler expects (`Path(id)`,
6//! `RawQuery(...)`, `Bytes`) and delegates. The handlers themselves are
7//! untouched.
8//!
9//! The list shape mirrors the REST URL query 1:1 — same keys, same semantics —
10//! so REST clients can migrate to RPC without re-learning the filter / order /
11//! pagination vocabulary. Synthesis back to a URL query happens in
12//! [`super::synthesize_list_query`]; the existing list handler parses it via
13//! `parse_model_list_query`.
14
15use serde::{Deserialize, Serialize};
16
17/// RPC input for `model.<X>.get` and `model.<X>.delete`. The PK type is
18/// instantiated per-model at the macro emission site.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RpcPkInput<Pk> {
21 pub id: Pk,
22}
23
24/// RPC input for `model.<X>.update`. Parameterized on both the PK type
25/// and the model's concrete `Update<X>Input` so the patch decodes
26/// straight to its real type — round-tripping through
27/// `serde_json::Value` would corrupt CBOR `Option::None` values (which
28/// `minicbor-serde` encodes as `0xf6` simple-null but `serde_json::Value`
29/// encodes as the CBOR empty-array marker; see comments in
30/// `cratestack-codec-cbor`). The dispatcher re-encodes `patch` through
31/// the same codec before handing it to the existing update handler.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RpcUpdateInput<Pk, Patch> {
34 pub id: Pk,
35 pub patch: Patch,
36}
37
38/// Single arbitrary key/value predicate inside [`RpcListInput::filters`].
39/// Models the REST URL form's "anything that isn't a reserved keyword is a
40/// predicate" rule (e.g. `?published=true&authorId=42`).
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct RpcListPredicate {
43 pub key: String,
44 pub value: String,
45}
46
47/// RPC input for `model.<X>.list`. Mirrors the REST URL query 1:1 — every
48/// optional field maps to a query param of the same name, predicates carry
49/// arbitrary `(key, value)` pairs that aren't reserved keywords.
50#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct RpcListInput {
52 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub limit: Option<i64>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub offset: Option<i64>,
56 /// Selection fields (`?fields=a,b,c`).
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub fields: Option<Vec<String>>,
59 /// Included relations (`?include=author,comments`).
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub include: Option<Vec<String>>,
62 /// Fields per included relation (`?includeFields[author]=id,name`).
63 #[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
64 pub include_fields: std::collections::BTreeMap<String, Vec<String>>,
65 /// Order expression (`?sort=name asc`).
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub sort: Option<String>,
68 /// Top-level filter expression (`?where=...`).
69 #[serde(default, skip_serializing_if = "Option::is_none", rename = "where")]
70 pub where_expr: Option<String>,
71 /// Disjunction filter (`?or=...`).
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub or: Option<String>,
74 /// Arbitrary `key=value` predicates (anything not in the reserved set).
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 pub filters: Vec<RpcListPredicate>,
77}