Skip to main content

hidane_rules/
eval.rs

1//! Evaluating a ruleset for one request: what the evaluator takes and what it decides.
2//!
3//! [`authorize`] finds the `allow` statements that apply to the request and evaluates their
4//! conditions. What the official emulator does, as measured with
5//! `tools/oracle/rules_conditions.py` (`results/rules-conditions-official-v1.22.0.json`) and
6//! `tools/oracle/rules_eval.py` (`tests/fixtures/rules_eval.json`):
7//!
8//! - Types are checked when a condition runs. Each operator, function and method takes the
9//!   types its error lists (`Unsupported operation error. Received: list + list. Expected:
10//!   int + int, …`); a method a type does not have is a `Function not found error`.
11//! - Errors are absorbed the CEL way: `error || true` and `true || error` are true, `error &&
12//!   false` and `false && error` are false, anything else with an error is the error. An error
13//!   that reaches the top of a condition denies, like `false`.
14//! - `==` between values of different types is false; an int equals a float of the same value,
15//!   but not inside lists and maps. Sets and map diffs compare numbers by value at any depth.
16//! - Strings are Java strings: sizes, indexes and slices count UTF-16 code units, `lower()` and
17//!   `upper()` change ASCII letters only, and regular expressions are RE2.
18//! - The operands of an operator, a call or a list or map literal are all evaluated, the last
19//!   first (as `debug()` shows), and the first error in order wins (for `x in y`, `y`'s; for a
20//!   method, a null receiver or a method it does not have comes before its arguments' errors).
21//! - The arguments of a function (the last first), then its `let`s in order, are evaluated
22//!   before its result, but an error in one only matters if the function uses it.
23//!
24//! Each condition is evaluated once, so a denial lists each statement once; the official
25//! emulator evaluates again after an error and lists everything twice
26//! (`docs/parity-exceptions.md`).
27//!
28//! [`authorize_traced`] also reports what the official emulator logs and counts for its
29//! coverage report (`tools/oracle/rules_coverage.py`, [`Trace`]): each `debug()` value, and
30//! what each expression evaluated to. Which statements it evaluates shows there: those of
31//! every `match` covering the path, each `match`'s until one allows.
32
33mod constraint;
34mod interp;
35mod library;
36mod list;
37mod time;
38mod trace;
39mod value;
40
41pub use constraint::Comparator;
42pub use trace::{Evaluation, KeyStatus, Observed, Outcome, Segment, Trace, Verdict};
43pub(crate) use trace::{NodeKey, Traced, hash_val, observe, shows};
44pub use value::java_double;
45
46use std::rc::Rc;
47
48use indexmap::IndexMap;
49
50use crate::{
51    Ruleset, ast,
52    paths::{Binding, Covering, Part},
53};
54use interp::{Frame, Interp, Level};
55pub(crate) use value::Val;
56
57/// A value as rules see it. Firestore values convert to these (`timestamp`, `bytes`, `latlng`,
58/// a reference as a `path`, maps and lists); the others only come from rules.
59#[derive(Debug, Clone, PartialEq)]
60pub enum Value {
61    Null,
62    Bool(bool),
63    Int(i64),
64    Float(f64),
65    String(String),
66    Bytes(Vec<u8>),
67    Timestamp(Timestamp),
68    /// Seconds and nanoseconds.
69    Duration(i64, i32),
70    /// Latitude and longitude, in degrees.
71    LatLng(f64, f64),
72    /// The segments of a path. A reference holds its document's path from `databases`, like
73    /// [`Request::path`] and [`Document::path`].
74    Path(Vec<String>),
75    List(Vec<Value>),
76    Map(Map),
77}
78
79/// A map's entries, in the order the document or the literal holds them. Equality ignores the
80/// order; where rules list keys in another order, the evaluator sorts.
81pub type Map = IndexMap<String, Value>;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct Timestamp {
85    pub seconds: i64,
86    pub nanos: i32,
87}
88
89/// The method a request is checked against. `read` covers `get` and `list`, `write` covers
90/// `create`, `update` and `delete`.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Method {
93    Get,
94    List,
95    Create,
96    Update,
97    Delete,
98}
99
100impl Method {
101    /// The name rules see in `request.method` and denials print.
102    pub fn name(self) -> &'static str {
103        match self {
104            Self::Get => "get",
105            Self::List => "list",
106            Self::Create => "create",
107            Self::Update => "update",
108            Self::Delete => "delete",
109        }
110    }
111
112    fn statement(self) -> ast::Method {
113        match self {
114            Self::Get => ast::Method::Get,
115            Self::List => ast::Method::List,
116            Self::Create => ast::Method::Create,
117            Self::Update => ast::Method::Update,
118            Self::Delete => ast::Method::Delete,
119        }
120    }
121}
122
123/// A document as `resource`, `request.resource` and `get()` give it.
124#[derive(Debug, Clone, PartialEq)]
125pub struct Document {
126    /// The path from `databases`: `databases`, the database ID, `documents`, then the
127    /// document's segments.
128    pub path: Vec<String>,
129    pub data: Map,
130}
131
132/// `request.auth` for a caller with a token.
133#[derive(Debug, Clone, PartialEq)]
134pub struct Auth {
135    /// The token's `sub`; without one, `request.auth.uid` is null.
136    pub uid: Option<String>,
137    /// Every claim of the token.
138    pub token: Map,
139}
140
141/// `request.query` for a list, and the filter that decides it.
142///
143/// `request.query` holds `limit`, `offset` and `orderBy` from here, and what the official
144/// emulator derives from the path (`tools/oracle/rules_request_fields.py`): `kind`, the
145/// collection ID (null for a query of every collection); `parent`, the document the collection
146/// is in (null at the root), named `ancestor` for a collection group; `allDescendants`; and
147/// `distinct` and `selectOnlyKeys`, always false, and `groupBy`, always empty, which Firestore
148/// queries cannot set.
149#[derive(Debug, Clone, PartialEq, Default)]
150pub struct Query {
151    pub limit: Option<i64>,
152    pub offset: Option<i64>,
153    /// The orderings the query gives (not those completing it), field path to `ASC` or
154    /// `DESC`, in the order `request.query.orderBy` lists them.
155    pub order_by: Vec<(String, String)>,
156    pub filter: Option<Filter>,
157    /// For a collection group, its collection ID: the list covers the collections with that ID
158    /// at any depth below [`Request::path`] (an empty ID: every collection below it).
159    pub group: Option<String>,
160    /// With an empty [`Query::group`]: only the collections directly below [`Request::path`]
161    /// (a query whose `from` has no collection ID and is not `allDescendants`).
162    pub children: bool,
163    /// `request.fields`: the fields the query selects, as the official emulator lists them
164    /// (`None`, null, without a projection).
165    pub fields: Option<Vec<String>>,
166}
167
168/// A query's filter.
169#[derive(Debug, Clone, PartialEq)]
170pub enum Filter {
171    And(Vec<Filter>),
172    Or(Vec<Filter>),
173    /// A field, by its segments (`a.b` is `["a", "b"]`), and the value it is compared with
174    /// (null for the unary operators; a list for `In`, `NotIn` and `ArrayContainsAny`).
175    Field {
176        field: Vec<String>,
177        op: FilterOp,
178        value: Value,
179    },
180    /// The document's name, and a reference ([`Value::Path`]) or a list of them.
181    Name {
182        op: FilterOp,
183        value: Value,
184    },
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum FilterOp {
189    Lt,
190    Le,
191    Gt,
192    Ge,
193    Eq,
194    Ne,
195    ArrayContains,
196    In,
197    ArrayContainsAny,
198    NotIn,
199    IsNan,
200    IsNull,
201    IsNotNan,
202    IsNotNull,
203}
204
205/// One request, as rules see it.
206#[derive(Debug, Clone, PartialEq)]
207pub struct Request {
208    pub method: Method,
209    /// A create or an update without an existence precondition (a set, a patch, a transform):
210    /// whether the document exists chose the method. A denial then also lists the statements of
211    /// the other of the two, which cannot allow the write (`tools/oracle/rules_writes.py`,
212    /// `rules_denials.py`).
213    pub upsert: bool,
214    /// The path checked, from `databases` (a document, or for a list its collection).
215    pub path: Vec<String>,
216    pub auth: Option<Auth>,
217    pub time: Timestamp,
218    /// The document as it would be after a create or an update; `None` otherwise.
219    pub resource: Option<Document>,
220    pub query: Option<Query>,
221    /// `request.inTransaction`: true for every write, which the official emulator commits in
222    /// a transaction of its own, and for a read of documents in a transaction begun before it;
223    /// false for a read that begins one, and for every list, in a transaction or not
224    /// (`tools/oracle/rules_request_fields.py`).
225    pub in_transaction: bool,
226    /// For a write, `request.writeFields`: the paths of its transforms, then those of its
227    /// update mask (`None`, null, for a write with neither).
228    pub write_fields: Option<Vec<String>>,
229    /// For a write, `request.transforms`: the paths its field transforms change, in order
230    /// (`None`, null, without any).
231    pub transforms: Option<Vec<String>>,
232    /// A create whose document ID the server chooses (a `CreateDocument` without one). The
233    /// official emulator decides it without the ID: [`Request::path`] is the collection's,
234    /// the statements are those of the collection's documents, with the document's wildcard
235    /// unbound, and `request.resource` holds only `data`. `request.autoId` says so for a
236    /// create that is not an upsert; other writes do not have it.
237    pub auto_id: bool,
238}
239
240/// The documents `get()` / `exists()` read (as they were before the request's writes) and
241/// `getAfter()` / `existsAfter()` read (as they would be after all of them).
242pub trait Documents {
243    fn get(&self, path: &[String]) -> Option<Document>;
244    fn get_after(&self, path: &[String]) -> Option<Document>;
245}
246
247/// The lookups a request may still make (`tools/oracle/rules_lookups.py`): a single read or
248/// write may look up 10 distinct documents, a query 20, and a batch 10 per write and 20 in all.
249/// A document looked up again costs nothing, across the writes of a batch too, and so does the
250/// document a request reads or writes and every document its batch writes
251/// (`tools/oracle/rules_eval.py`).
252#[derive(Debug, Clone)]
253pub struct Budget {
254    /// Distinct documents looked up so far, by the whole request or batch.
255    pub looked_up: Vec<Vec<String>>,
256    /// How many of them the current write or read looked up first.
257    pub this_part: usize,
258    pub part_limit: usize,
259    pub total_limit: usize,
260    /// The documents the batch writes, from `databases` (see [`Budget::add_written`]).
261    pub written: Vec<Vec<String>>,
262}
263
264impl Budget {
265    fn with(part_limit: usize, total_limit: usize) -> Self {
266        Self {
267            looked_up: Vec::new(),
268            this_part: 0,
269            part_limit,
270            total_limit,
271            written: Vec::new(),
272        }
273    }
274
275    /// Records a document the batch writes (or deletes): any write of the batch may look it up
276    /// for free. The document of the request being decided is free without this.
277    pub fn add_written(&mut self, path: Vec<String>) {
278        if !self.written.contains(&path) {
279            self.written.push(path);
280        }
281    }
282
283    /// A single read or write.
284    pub fn single() -> Self {
285        Self::with(10, 10)
286    }
287
288    /// A query.
289    pub fn query() -> Self {
290        Self::with(20, 20)
291    }
292
293    /// A batch of writes; call [`Budget::next_write`] before each one.
294    pub fn batch() -> Self {
295        Self::with(10, 20)
296    }
297
298    pub fn next_write(&mut self) {
299        self.this_part = 0;
300    }
301}
302
303/// What the rules decided.
304#[derive(Debug, Clone, PartialEq, Eq)]
305pub enum Decision {
306    Allow,
307    /// `PERMISSION_DENIED`, with the official emulator's message.
308    Deny(String),
309}
310
311/// Decides one request against `ruleset` (the `cloud.firestore` service). `resource` is the
312/// stored document (`None` if it does not exist, or for a list). The rules' version decides
313/// how recursive wildcards match.
314///
315/// The `allow` statements of every `match` that covers the path and names the method apply, in
316/// source order; one whose condition is true allows. A denial lists each one's result:
317/// `false for 'get' @ L5` or the error instead of `false`, with the line of the condition, or
318/// says `No matching allow statements` when none applies. For an upsert the statements of the
319/// other method are listed too, `create`'s first: each is `false`, or its condition's error.
320/// Their lookups do not count against `budget`.
321///
322/// A list is decided from its query's filter, not from the documents (`list`).
323pub fn authorize(
324    ruleset: &Ruleset,
325    request: &Request,
326    resource: Option<&Document>,
327    documents: &dyn Documents,
328    budget: &mut Budget,
329) -> Decision {
330    authorize_traced(
331        ruleset,
332        request,
333        resource,
334        documents,
335        budget,
336        &mut Trace::new(),
337    )
338}
339
340/// [`authorize`], reporting into `trace` what the conditions evaluated.
341///
342/// An upsert evaluates the statements of both methods, whichever applies, as the official
343/// emulator does: `create`'s, then `update`'s, as [`authorize`] evaluates those of a method
344/// (`tools/oracle/rules_coverage.py`). Only those of the method that applies decide.
345pub fn authorize_traced(
346    ruleset: &Ruleset,
347    request: &Request,
348    resource: Option<&Document>,
349    documents: &dyn Documents,
350    budget: &mut Budget,
351    trace: &mut Trace,
352) -> Decision {
353    let Some(service) = ruleset.service("cloud.firestore") else {
354        return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
355    };
356    if request.method == Method::List {
357        return list::authorize(ruleset, service, request, documents, budget, trace);
358    }
359    let mut parts: Vec<Part<'_>> = request.path.iter().map(|s| Part::Segment(s)).collect();
360    if request.auto_id {
361        parts.push(Part::UnknownId);
362    }
363    let coverings = crate::paths::covering_parts(ruleset.version, service, &parts);
364    let other = match request.method {
365        Method::Create if request.upsert => Some(Method::Update),
366        Method::Update if request.upsert => Some(Method::Create),
367        _ => None,
368    };
369    let applying = statements(&coverings, request.method);
370    let others = other.map(|other| (other, statements(&coverings, other)));
371    if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
372        return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
373    }
374
375    // The statements of each method see `request.method` as theirs.
376    let context_for = |method| Context {
377        ruleset,
378        service,
379        request: request_value(request, method),
380        resource: resource.map_or(Val::Null, value::resource),
381        target: &request.path,
382        documents,
383    };
384    let context = context_for(request.method);
385    let Some((other, others)) = others else {
386        return match context.decide(&applying, request.method, budget, trace) {
387            Ok(()) => Decision::Allow,
388            Err(entries) => denied(&entries),
389        };
390    };
391    let other_context = context_for(other);
392    // `create`'s statements come first: before the decision when `update` applies. The other
393    // method's lookups count against a copy of the budget.
394    if other == Method::Create {
395        other_context.each(&others, other, &mut budget.clone(), trace);
396    }
397    let decided = context.decide(&applying, request.method, budget, trace);
398    let Err(entries) = decided else {
399        if other == Method::Update {
400            other_context.each(&others, other, &mut budget.clone(), trace);
401        }
402        return Decision::Allow;
403    };
404    // A denial lists every statement of the other method; `create`'s, evaluated already, are
405    // evaluated again without being traced.
406    let listed = match other {
407        Method::Create => {
408            other_context.listed(&others, other, &mut budget.clone(), &mut Trace::new())
409        }
410        _ => other_context.listed(&others, other, &mut budget.clone(), trace),
411    };
412    let entries: Vec<String> = match other {
413        Method::Create => listed.into_iter().chain(entries).collect(),
414        _ => entries.into_iter().chain(listed).collect(),
415    };
416    denied(&entries)
417}
418
419fn no_match() -> Decision {
420    Decision::Deny("No matching allow statements".to_owned())
421}
422
423/// The denial of a request no statement applies to, reported as an evaluation of none.
424fn unmatched(request: &Request, resource: Val, trace: &mut Trace) -> Decision {
425    if trace.evaluations.is_some() {
426        trace.evaluating(
427            request.method,
428            &request_value(request, request.method),
429            &resource,
430        );
431    }
432    no_match()
433}
434
435/// A denial listing `entries`.
436fn denied(entries: &[String]) -> Decision {
437    Decision::Deny(format!("\n{}", entries.join(", ")))
438}
439
440/// The `allow` statements for `method` of every `match` in `coverings`, in source order.
441fn statements<'c, 'a>(
442    coverings: &'c [Covering<'a>],
443    method: Method,
444) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
445    let method = method.statement();
446    let mut statements = Vec::new();
447    for covering in coverings {
448        for allow in &covering.target().body.allows {
449            if allow
450                .methods
451                .iter()
452                .any(|m| m.method.is_some_and(|m| m.covers(method)))
453            {
454                statements.push((allow, covering));
455            }
456        }
457    }
458    statements.sort_by_key(|(allow, _)| allow.span.start);
459    statements
460}
461
462/// What a statement's condition came to.
463fn verdict(result: &Result<bool, String>) -> Verdict {
464    match result {
465        Ok(true) => Verdict::Allow,
466        Ok(false) => Verdict::False,
467        Err(_) => Verdict::Error,
468    }
469}
470
471fn entry(text: &str, method: Method, line: u32) -> String {
472    format!("{text} for '{}' @ L{line}", method.name())
473}
474
475/// What every condition of one request is evaluated with.
476struct Context<'a> {
477    ruleset: &'a Ruleset,
478    service: &'a ast::Service,
479    request: Val,
480    resource: Val,
481    /// The path whose document is free to look up, in the database `get()` may read.
482    target: &'a [String],
483    documents: &'a dyn Documents,
484}
485
486impl Context<'_> {
487    /// Evaluates `statements` in order; if none allows, each one's entry. As the official
488    /// emulator does, the statements of a `match` stop at the first that allows, but those of
489    /// every other `match` are evaluated too (`tools/oracle/rules_coverage.py`).
490    fn decide(
491        &self,
492        statements: &[(&ast::Allow, &Covering<'_>)],
493        method: Method,
494        budget: &mut Budget,
495        trace: &mut Trace,
496    ) -> Result<(), Vec<String>> {
497        let (entries, allowed) = self.each(statements, method, budget, trace);
498        if allowed { Ok(()) } else { Err(entries) }
499    }
500
501    /// The statements of an upsert's method that does not apply, for a denial: each one's
502    /// entry, `false` or its condition's error.
503    fn listed(
504        &self,
505        statements: &[(&ast::Allow, &Covering<'_>)],
506        method: Method,
507        budget: &mut Budget,
508        trace: &mut Trace,
509    ) -> Vec<String> {
510        let mut entries = Vec::with_capacity(statements.len());
511        let mut marks = Vec::with_capacity(statements.len());
512        let at = trace.evaluating(method, &self.request, &self.resource);
513        for (allow, covering) in statements {
514            marks.push(trace.mark());
515            let (result, line) = match &allow.condition {
516                Some(condition) => (
517                    self.evaluate(covering, condition, budget, trace),
518                    condition.span.line,
519                ),
520                None => (Ok(true), allow.span.line),
521            };
522            trace.verdict(at, line, verdict(&result));
523            let text = result.err();
524            entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
525        }
526        trace.repeat(&marks);
527        entries
528    }
529
530    /// Evaluates `statements` but those after one that allowed in the same `match`. Returns the
531    /// entries of those that did not allow, and whether one did.
532    fn each(
533        &self,
534        statements: &[(&ast::Allow, &Covering<'_>)],
535        method: Method,
536        budget: &mut Budget,
537        trace: &mut Trace,
538    ) -> (Vec<String>, bool) {
539        let mut entries = Vec::with_capacity(statements.len());
540        let mut marks = Vec::with_capacity(statements.len());
541        let mut allowed: Vec<*const ast::Match> = Vec::new();
542        let at = trace.evaluating(method, &self.request, &self.resource);
543        for (allow, covering) in statements {
544            let target = std::ptr::from_ref(covering.target());
545            if allowed.contains(&target) {
546                continue;
547            }
548            marks.push(trace.mark());
549            let Some(condition) = &allow.condition else {
550                trace.verdict(at, allow.span.line, Verdict::Allow);
551                allowed.push(target);
552                continue;
553            };
554            let result = self.evaluate(covering, condition, budget, trace);
555            trace.verdict(at, condition.span.line, verdict(&result));
556            let text = match result {
557                Ok(true) => {
558                    allowed.push(target);
559                    continue;
560                }
561                Ok(false) => "false".to_owned(),
562                Err(e) => e,
563            };
564            entries.push(entry(&text, method, condition.span.line));
565        }
566        trace.repeat(&marks);
567        (entries, !allowed.is_empty())
568    }
569
570    /// The condition of a statement at the end of `covering`: true, false or the error's text.
571    fn evaluate(
572        &self,
573        covering: &Covering<'_>,
574        condition: &ast::Expr,
575        budget: &mut Budget,
576        trace: &mut Trace,
577    ) -> Result<bool, String> {
578        let mut interp = Interp::new(
579            levels(self.ruleset, self.service, covering),
580            self.request.clone(),
581            self.resource.clone(),
582            self.target,
583            self.documents,
584            budget,
585            trace,
586            &self.ruleset.parens,
587        );
588        let frame = Frame::condition(covering.chain.len() + 1);
589        interp
590            .eval(condition, frame.scope())
591            .and_then(interp::boolean)
592            .map_err(|e| {
593                if e.message.is_empty() {
594                    // Undecided: the official emulator names the condition's start.
595                    format!(
596                        "evaluation error at L{}:{}",
597                        condition.span.line, condition.span.column
598                    )
599                } else {
600                    e.message
601                }
602            })
603    }
604}
605
606/// The names a condition at the end of `covering` sees: the functions of the file, of the
607/// service and of each `match`, and the variables each `match` binds.
608fn levels<'a>(
609    ruleset: &'a Ruleset,
610    service: &'a ast::Service,
611    covering: &'a Covering<'a>,
612) -> Vec<Level<'a>> {
613    let mut levels = vec![
614        Level {
615            functions: &ruleset.functions,
616            variables: Vec::new(),
617        },
618        Level {
619            functions: &service.body.functions,
620            variables: Vec::new(),
621        },
622    ];
623    let mut bindings = covering.bindings.iter();
624    for m in &covering.chain {
625        let count = m
626            .path
627            .iter()
628            .filter(|s| s.kind.variable().is_some())
629            .count();
630        let variables = bindings
631            .by_ref()
632            .take(count)
633            // A wildcard that took a list's unknown document ID is not bound: using it is a
634            // `Null value error`, as for any unknown name.
635            .filter_map(|(name, binding)| {
636                let value = match binding {
637                    Binding::Segment(s) => Val::str(s),
638                    Binding::Path(p) => Val::path(p.clone()),
639                    Binding::Unbound => return None,
640                };
641                Some((name.as_str(), value))
642            })
643            .collect();
644        levels.push(Level {
645            functions: &m.body.functions,
646            variables,
647        });
648    }
649    levels
650}
651
652/// `request` for the statements of `method`, with the keys the official emulator gives it, in
653/// the order its coverage report lists them (`tools/oracle/rules_request_fields.py`):
654///
655/// - for a `get`: `time`, `auth`, `inTransaction`, `path`, `fields` (null), `method`;
656/// - for a `list`: `time`, `auth`, `inTransaction`, `fields`, `query`, `path`, `method`;
657/// - for a write: `time`, `auth`, `inTransaction`, `path`, `transforms`, `writeFields`,
658///   `readFields` (null), `autoId` (for a create that is not an upsert), `resource` (null for a
659///   delete; only `data` when the ID is the server's), `method`.
660///
661/// `auth` is null without a token, else `uid` and `token`.
662fn request_value(request: &Request, method: Method) -> Val {
663    let strings = |list: &Option<Vec<String>>| {
664        list.as_ref().map_or(Val::Null, |items| {
665            Val::list(items.iter().map(|s| Val::str(s)).collect())
666        })
667    };
668    let mut map = IndexMap::new();
669    map.insert(
670        "time".to_owned(),
671        Val::from(&Value::Timestamp(request.time)),
672    );
673    map.insert("auth".to_owned(), auth_value(request));
674    map.insert(
675        "inTransaction".to_owned(),
676        Val::Bool(request.in_transaction),
677    );
678    let path = Val::path(request.path.clone());
679    match method {
680        Method::Get => {
681            map.insert("path".to_owned(), path);
682            map.insert("fields".to_owned(), Val::Null);
683        }
684        Method::List => {
685            let query = request.query.clone().unwrap_or_default();
686            map.insert("fields".to_owned(), strings(&query.fields));
687            map.insert("query".to_owned(), query_value(request, &query));
688            map.insert("path".to_owned(), path);
689        }
690        Method::Create | Method::Update | Method::Delete => {
691            map.insert("path".to_owned(), path);
692            map.insert("transforms".to_owned(), strings(&request.transforms));
693            map.insert("writeFields".to_owned(), strings(&request.write_fields));
694            map.insert("readFields".to_owned(), Val::Null);
695            if method == Method::Create && !request.upsert {
696                map.insert("autoId".to_owned(), Val::Bool(request.auto_id));
697            }
698            let resource = match &request.resource {
699                None => Val::Null,
700                // The document has no name yet.
701                Some(doc) if request.auto_id => match value::resource(doc) {
702                    Val::Map(map) => {
703                        let data = map.get("data").cloned().unwrap_or(Val::Null);
704                        Val::Map(Rc::new(IndexMap::from([("data".to_owned(), data)])))
705                    }
706                    other => other,
707                },
708                Some(doc) => value::resource(doc),
709            };
710            map.insert("resource".to_owned(), resource);
711        }
712    }
713    map.insert("method".to_owned(), Val::str(method.name()));
714    Val::Map(Rc::new(map))
715}
716
717/// `request.query`, its keys in the order of the official emulator's coverage report.
718fn query_value(request: &Request, query: &Query) -> Val {
719    let all_descendants = query.group.is_some() && !query.children;
720    // The collection ID, and the document holding the collection (or the collections).
721    let (kind, parent) = match &query.group {
722        None => match request.path.split_last() {
723            Some((id, parent)) => (Val::str(id), parent),
724            None => (Val::Null, &[][..]),
725        },
726        Some(id) if id.is_empty() => (Val::Null, request.path.as_slice()),
727        Some(id) => (Val::str(id), request.path.as_slice()),
728    };
729    // `databases/{id}/documents`, the root, is no document.
730    let parent = if parent.len() > 3 {
731        Val::path(parent.to_vec())
732    } else {
733        Val::Null
734    };
735    let order_by = query
736        .order_by
737        .iter()
738        .map(|(field, direction)| (field.clone(), Val::str(direction)))
739        .collect();
740    let mut q = IndexMap::new();
741    q.insert("distinct".to_owned(), Val::Bool(false));
742    q.insert("selectOnlyKeys".to_owned(), Val::Bool(false));
743    q.insert("groupBy".to_owned(), Val::Map(Rc::default()));
744    q.insert("kind".to_owned(), kind);
745    let parent_key = if all_descendants {
746        "ancestor"
747    } else {
748        "parent"
749    };
750    q.insert(parent_key.to_owned(), parent);
751    q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
752    q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
753    q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
754    q.insert("allDescendants".to_owned(), Val::Bool(all_descendants));
755    Val::Map(Rc::new(q))
756}
757
758/// `request.auth`: null without a token, else `uid` and `token`.
759fn auth_value(request: &Request) -> Val {
760    match &request.auth {
761        None => Val::Null,
762        Some(auth) => {
763            let mut a = IndexMap::new();
764            a.insert(
765                "uid".to_owned(),
766                auth.uid.as_deref().map_or(Val::Null, Val::str),
767            );
768            a.insert(
769                "token".to_owned(),
770                Val::from(&Value::Map(auth.token.clone())),
771            );
772            Val::Map(Rc::new(a))
773        }
774    }
775}