Skip to main content

sift_query_core/protocol/
rpc.rs

1//! Canonical, value-only RPC parameters. Authorization scopes are not inputs.
2use crate::{
3    NodeId, PagingMode, ParamError, ParamNames, ParamParser, PredicateArena, PredicateNode,
4    Projection, QueryBuilder, QueryInput, QueryPolicy, RawParams, SearchEntity, SortOrder,
5    UserQuery,
6};
7
8impl<E: SearchEntity> UserQuery<E> {
9    pub fn to_rpc(&self, policy: &QueryPolicy) -> Result<RawParams, ParamError> {
10        self.to_rpc_with_names(policy, policy.names())
11    }
12
13    /// Same query semantics, using the remote service's control names.
14    pub fn to_rpc_with_names(
15        &self,
16        policy: &QueryPolicy,
17        names: &ParamNames,
18    ) -> Result<RawParams, ParamError> {
19        self.into_user_query(policy)?;
20        let target = policy
21            .clone()
22            .with_param_names(names.clone())
23            .map_err(|error| ParamError::ConflictingParameters(error.to_string()))?;
24        let mut entries = Vec::new();
25        if let Some(root) = self.predicate().root() {
26            let expression = encode_node::<E>(self.predicate(), root, policy, &mut entries, None)?;
27            entries.push((names.group_expr.clone(), expression));
28        }
29        if let Projection::Fields(fields) = self.projection() {
30            entries.push((
31                names.only_select.clone(),
32                fields
33                    .iter()
34                    .map(|id| E::META.field(*id).expect("validated field").public_name)
35                    .collect::<Vec<_>>()
36                    .join(","),
37            ));
38        }
39        // Emit the shortest prefix that reconstructs the final stable order.
40        // Appended stable keys do not consume the explicit-sort budget.
41        for length in 0..=self.order().len() {
42            let mut order = if length == 0 {
43                E::META
44                    .default_order
45                    .iter()
46                    .copied()
47                    .map(Into::into)
48                    .collect::<Vec<_>>()
49            } else {
50                self.order()[..length].to_vec()
51            };
52            for stable in E::META.stable_order {
53                if !order.iter().any(|item| item.field == stable.field) {
54                    order.push((*stable).into());
55                }
56            }
57            if order == self.order() {
58                if length > 0 {
59                    entries.push((
60                        names.order_by.clone(),
61                        self.order()[..length]
62                            .iter()
63                            .map(|order| {
64                                format!(
65                                    "{}:{}",
66                                    E::META
67                                        .field(order.field)
68                                        .expect("validated order")
69                                        .public_name,
70                                    if order.direction == SortOrder::Asc {
71                                        "asc"
72                                    } else {
73                                        "desc"
74                                    }
75                                )
76                            })
77                            .collect::<Vec<_>>()
78                            .join(","),
79                    ));
80                }
81                break;
82            }
83        }
84        let paging = self.paging();
85        if self.is_forced_empty() {
86            entries.push((
87                names.size.clone(),
88                (u64::from(
89                    policy
90                        .limits()
91                        .max_page_size
92                        .min(E::META.limits.max_page_size),
93                ) + 1)
94                    .to_string(),
95            ));
96        } else {
97            if paging.explicit_size || paging.size != 15 {
98                entries.push((names.size.clone(), paging.size.to_string()));
99            }
100            match &paging.mode {
101                PagingMode::Page { page } if paging.explicit_page => {
102                    entries.push((names.page.clone(), page.to_string()));
103                }
104                PagingMode::Offset => {
105                    entries.push((names.offset.clone(), paging.offset.to_string()));
106                }
107                PagingMode::Cursor(cursor) => {
108                    let token = match cursor {
109                        None => String::new(),
110                        Some(cursor) => policy
111                            .cursor_codec()
112                            .ok_or_else(|| invalid("cursor codec missing"))?
113                            .encode::<E>(self.order(), cursor.values())?,
114                    };
115                    entries.push((names.cursor.clone(), token));
116                }
117                PagingMode::Page { .. } => {}
118            }
119        }
120        let raw = RawParams::from_entries(entries, target.limits())?;
121        // Full parser/normalizer validation also catches filters that would
122        // change canonical RPC values. Never silently send a different query.
123        let decoded = ParamParser::new(&target)?.parse::<E>(&raw)?;
124        if signature(self.predicate()) != signature(decoded.predicate())
125            || self.order() != decoded.order()
126            || self.projection() != decoded.projection()
127            || self.paging() != decoded.paging()
128            || self.is_forced_empty() != decoded.is_forced_empty()
129        {
130            return Err(invalid("RPC normalization changes query semantics"));
131        }
132        Ok(raw)
133    }
134}
135
136impl<E: SearchEntity> QueryBuilder<'_, E> {
137    pub fn build_for_rpc(self) -> Result<RawParams, ParamError> {
138        let policy = self.policy;
139        self.build()?.to_rpc(policy)
140    }
141    pub fn build_for_rpc_with_names(self, names: &ParamNames) -> Result<RawParams, ParamError> {
142        let policy = self.policy;
143        self.build()?.to_rpc_with_names(policy, names)
144    }
145}
146
147fn encode_node<E: SearchEntity>(
148    arena: &PredicateArena,
149    id: NodeId,
150    policy: &QueryPolicy,
151    entries: &mut Vec<(String, String)>,
152    parent_is_and: Option<bool>,
153) -> Result<String, ParamError> {
154    match arena.node(id).expect("validated predicate") {
155        PredicateNode::Condition(condition) => {
156            let group = format!("q{}", id.0);
157            let field = E::META.field(condition.field).expect("validated field");
158            let key = format!("{group}.{}", field.public_name);
159            entries.push((
160                format!("{key}-op"),
161                condition.operator.short_name().to_owned(),
162            ));
163            if condition.ignore_case {
164                entries.push((format!("{key}-ic"), "true".to_owned()));
165            }
166            for value in &condition.values {
167                let input_field = crate::FieldMeta {
168                    value_type: condition.operator.input_type(field.value_type),
169                    ..*field
170                };
171                let text = if let Some(codec) = policy.codec_for::<E>(field) {
172                    let text = codec.format_input(&input_field, value).map_err(|reason| {
173                        ParamError::InvalidValue {
174                            field: field.public_name.to_owned(),
175                            reason,
176                        }
177                    })?;
178                    if codec.parse(&input_field, &text).ok().as_ref() != Some(value) {
179                        return Err(invalid("codec has no lossless RPC representation"));
180                    }
181                    text
182                } else {
183                    value.to_external_text()
184                };
185                entries.push((key.clone(), text));
186            }
187            Ok(group)
188        }
189        PredicateNode::And(children) | PredicateNode::Or(children) => {
190            let and = matches!(arena.node(id), Some(PredicateNode::And(_)));
191            let separator = if and { "&" } else { "|" };
192            let expressions = children
193                .iter()
194                .map(|child| encode_node::<E>(arena, *child, policy, entries, Some(and)))
195                .collect::<Result<Vec<_>, _>>()?;
196            let expression = expressions.join(separator);
197            // AND has higher precedence. Extra parentheses would consume the
198            // remote nesting budget without adding query semantics.
199            Ok(if !and && parent_is_and == Some(true) {
200                format!("({expression})")
201            } else {
202                expression
203            })
204        }
205    }
206}
207
208#[derive(PartialEq)]
209enum Expression {
210    Condition(crate::Condition),
211    Junction(bool, Vec<Self>),
212}
213fn signature(arena: &PredicateArena) -> Option<Expression> {
214    fn node(arena: &PredicateArena, id: NodeId) -> Expression {
215        match arena.node(id).expect("validated predicate") {
216            PredicateNode::Condition(condition) => Expression::Condition(condition.clone()),
217            PredicateNode::And(children) | PredicateNode::Or(children) => {
218                let and = matches!(arena.node(id), Some(PredicateNode::And(_)));
219                let mut output = Vec::new();
220                for child in children {
221                    match node(arena, *child) {
222                        Expression::Junction(kind, nested) if kind == and => output.extend(nested),
223                        child => output.push(child),
224                    }
225                }
226                if output.len() == 1 {
227                    output.remove(0)
228                } else {
229                    Expression::Junction(and, output)
230                }
231            }
232        }
233    }
234    arena.root().map(|root| node(arena, root))
235}
236fn invalid(reason: &str) -> ParamError {
237    ParamError::InvalidValue {
238        field: "rpc".to_owned(),
239        reason: reason.to_owned(),
240    }
241}