Skip to main content

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}