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, or for a verify the stored
219 /// one; `None` otherwise.
220 pub resource: Option<Document>,
221 pub query: Option<Query>,
222 /// `request.inTransaction`: true for every write, which the official emulator commits in
223 /// a transaction of its own, and for a read of documents in a transaction begun before it;
224 /// false for a read that begins one, and for every list, in a transaction or not
225 /// (`tools/oracle/rules_request_fields.py`).
226 pub in_transaction: bool,
227 /// For a write, `request.writeFields`: the paths of its transforms, then those of its
228 /// update mask (`None`, null, for a write with neither).
229 pub write_fields: Option<Vec<String>>,
230 /// For a write, `request.transforms`: the paths its field transforms change, in order
231 /// (`None`, null, without any).
232 pub transforms: Option<Vec<String>>,
233 /// A create whose document ID the server chooses (a `CreateDocument` without one). The
234 /// official emulator decides it without the ID: [`Request::path`] is the collection's,
235 /// the statements are those of the collection's documents, with the document's wildcard
236 /// unbound, and `request.resource` holds only `data`. `request.autoId` says so for a
237 /// create that is not an upsert; other writes do not have it.
238 pub auto_id: bool,
239 /// A verify of a commit (`Write.verify`), checked as a `get` of its document. `request` has
240 /// the keys of a write's, `request.resource` the stored document (null when there is none),
241 /// as in the official emulator's second pass, where its first has the keys of a read's
242 /// (`tools/oracle/rules_verify.py`, `docs/parity-exceptions.md`).
243 pub verify: bool,
244}
245
246/// The documents `get()` / `exists()` read (as they were before the request's writes) and
247/// `getAfter()` / `existsAfter()` read (as they would be after all of them).
248pub trait Documents {
249 fn get(&self, path: &[String]) -> Option<Document>;
250 fn get_after(&self, path: &[String]) -> Option<Document>;
251}
252
253/// The lookups a request may still make (`tools/oracle/rules_lookups.py`): a single read or
254/// write may look up 10 distinct documents, a query 20, and a batch 10 per write and 20 in all.
255/// A document looked up again costs nothing, across the writes of a batch too, and so does the
256/// document a request reads or writes and every document its batch writes
257/// (`tools/oracle/rules_eval.py`).
258///
259/// The verifies of a commit count as its writes (`tools/oracle/rules_verify.py`). The documents
260/// they verify cost nothing through `getAfter()` and `existsAfter()` only, and in a verify's
261/// condition so do the documents the batch writes.
262#[derive(Debug, Clone)]
263pub struct Budget {
264 /// Distinct documents looked up so far, by the whole request or batch.
265 pub looked_up: Vec<Vec<String>>,
266 /// How many of them the current write or read looked up first.
267 pub this_part: usize,
268 pub part_limit: usize,
269 pub total_limit: usize,
270 /// The documents the batch writes, from `databases` (see [`Budget::add_written`]).
271 pub written: Vec<Vec<String>>,
272 /// The documents the batch verifies, from `databases` (see [`Budget::add_verified`]).
273 pub verified: Vec<Vec<String>>,
274 /// Whether a verify is being decided (see [`Budget::next_verify`]).
275 pub verifying: bool,
276}
277
278impl Budget {
279 fn with(part_limit: usize, total_limit: usize) -> Self {
280 Self {
281 looked_up: Vec::new(),
282 this_part: 0,
283 part_limit,
284 total_limit,
285 written: Vec::new(),
286 verified: Vec::new(),
287 verifying: false,
288 }
289 }
290
291 /// Records a document the batch writes (or deletes): any write of the batch may look it up
292 /// for free. The document of the request being decided is free without this.
293 pub fn add_written(&mut self, path: Vec<String>) {
294 if !self.written.contains(&path) {
295 self.written.push(path);
296 }
297 }
298
299 /// Records a document the batch verifies: any write or verify of the batch may look it up
300 /// with `getAfter()` or `existsAfter()` for free.
301 pub fn add_verified(&mut self, path: Vec<String>) {
302 if !self.verified.contains(&path) {
303 self.verified.push(path);
304 }
305 }
306
307 /// Whether `path` costs nothing to look up, `after` the writes or before them, besides the
308 /// document of the request being decided and those looked up already.
309 pub fn free(&self, path: &[String], after: bool) -> bool {
310 let has = |list: &[Vec<String>]| list.iter().any(|p| p.as_slice() == path);
311 if after {
312 has(&self.written) || has(&self.verified)
313 } else {
314 !self.verifying && has(&self.written)
315 }
316 }
317
318 /// A single read or write.
319 pub fn single() -> Self {
320 Self::with(10, 10)
321 }
322
323 /// A query.
324 pub fn query() -> Self {
325 Self::with(20, 20)
326 }
327
328 /// A batch of writes; call [`Budget::next_write`] before each one.
329 pub fn batch() -> Self {
330 Self::with(10, 20)
331 }
332
333 pub fn next_write(&mut self) {
334 self.this_part = 0;
335 self.verifying = false;
336 }
337
338 /// Like [`Budget::next_write`], before a verify of the batch.
339 pub fn next_verify(&mut self) {
340 self.this_part = 0;
341 self.verifying = true;
342 }
343}
344
345/// What the rules decided.
346#[derive(Debug, Clone, PartialEq, Eq)]
347pub enum Decision {
348 Allow,
349 /// `PERMISSION_DENIED`, with the official emulator's message.
350 Deny(String),
351}
352
353/// Decides one request against `ruleset` (the `cloud.firestore` service). `resource` is the
354/// stored document (`None` if it does not exist, or for a list). The rules' version decides
355/// how recursive wildcards match.
356///
357/// The `allow` statements of every `match` that covers the path and names the method apply, in
358/// source order; one whose condition is true allows. A denial lists each one's result:
359/// `false for 'get' @ L5` or the error instead of `false`, with the line of the condition, or
360/// says `No matching allow statements` when none applies. For an upsert the statements of the
361/// other method are listed too, `create`'s first: each is `false`, or its condition's error.
362/// Their lookups do not count against `budget`.
363///
364/// A list is decided from its query's filter, not from the documents (`list`).
365pub fn authorize(
366 ruleset: &Ruleset,
367 request: &Request,
368 resource: Option<&Document>,
369 documents: &dyn Documents,
370 budget: &mut Budget,
371) -> Decision {
372 authorize_traced(
373 ruleset,
374 request,
375 resource,
376 documents,
377 budget,
378 &mut Trace::new(),
379 )
380}
381
382/// [`authorize`], reporting into `trace` what the conditions evaluated.
383///
384/// An upsert evaluates the statements of both methods, whichever applies, as the official
385/// emulator does: `create`'s, then `update`'s, as [`authorize`] evaluates those of a method
386/// (`tools/oracle/rules_coverage.py`). Only those of the method that applies decide.
387pub fn authorize_traced(
388 ruleset: &Ruleset,
389 request: &Request,
390 resource: Option<&Document>,
391 documents: &dyn Documents,
392 budget: &mut Budget,
393 trace: &mut Trace,
394) -> Decision {
395 let Some(service) = ruleset.service("cloud.firestore") else {
396 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
397 };
398 if request.method == Method::List {
399 return list::authorize(ruleset, service, request, documents, budget, trace);
400 }
401 let mut parts: Vec<Part<'_>> = request.path.iter().map(|s| Part::Segment(s)).collect();
402 if request.auto_id {
403 parts.push(Part::UnknownId);
404 }
405 let coverings = crate::paths::covering_parts(ruleset.version, service, &parts);
406 let other = match request.method {
407 Method::Create if request.upsert => Some(Method::Update),
408 Method::Update if request.upsert => Some(Method::Create),
409 _ => None,
410 };
411 let applying = statements(&coverings, request.method);
412 let others = other.map(|other| (other, statements(&coverings, other)));
413 if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
414 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
415 }
416
417 // The statements of each method see `request.method` as theirs.
418 let context_for = |method| Context {
419 ruleset,
420 service,
421 request: request_value(request, method),
422 resource: resource.map_or(Val::Null, value::resource),
423 target: &request.path,
424 documents,
425 };
426 let context = context_for(request.method);
427 let Some((other, others)) = others else {
428 return match context.decide(&applying, request.method, budget, trace) {
429 Ok(()) => Decision::Allow,
430 Err(entries) => denied(&entries),
431 };
432 };
433 let other_context = context_for(other);
434 // `create`'s statements come first: before the decision when `update` applies. The other
435 // method's lookups count against a copy of the budget.
436 if other == Method::Create {
437 other_context.each(&others, other, &mut budget.clone(), trace);
438 }
439 let decided = context.decide(&applying, request.method, budget, trace);
440 let Err(entries) = decided else {
441 if other == Method::Update {
442 other_context.each(&others, other, &mut budget.clone(), trace);
443 }
444 return Decision::Allow;
445 };
446 // A denial lists every statement of the other method; `create`'s, evaluated already, are
447 // evaluated again without being traced.
448 let listed = match other {
449 Method::Create => {
450 other_context.listed(&others, other, &mut budget.clone(), &mut Trace::new())
451 }
452 _ => other_context.listed(&others, other, &mut budget.clone(), trace),
453 };
454 let entries: Vec<String> = match other {
455 Method::Create => listed.into_iter().chain(entries).collect(),
456 _ => entries.into_iter().chain(listed).collect(),
457 };
458 denied(&entries)
459}
460
461fn no_match() -> Decision {
462 Decision::Deny("No matching allow statements".to_owned())
463}
464
465/// The denial of a request no statement applies to, reported as an evaluation of none.
466fn unmatched(request: &Request, resource: Val, trace: &mut Trace) -> Decision {
467 if trace.evaluations.is_some() {
468 trace.evaluating(
469 request.method,
470 &request_value(request, request.method),
471 &resource,
472 );
473 }
474 no_match()
475}
476
477/// A denial listing `entries`.
478fn denied(entries: &[String]) -> Decision {
479 Decision::Deny(format!("\n{}", entries.join(", ")))
480}
481
482/// The `allow` statements for `method` of every `match` in `coverings`, in source order.
483fn statements<'c, 'a>(
484 coverings: &'c [Covering<'a>],
485 method: Method,
486) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
487 let method = method.statement();
488 let mut statements = Vec::new();
489 for covering in coverings {
490 for allow in &covering.target().body.allows {
491 if allow
492 .methods
493 .iter()
494 .any(|m| m.method.is_some_and(|m| m.covers(method)))
495 {
496 statements.push((allow, covering));
497 }
498 }
499 }
500 statements.sort_by_key(|(allow, _)| allow.span.start);
501 statements
502}
503
504/// What a statement's condition came to.
505fn verdict(result: &Result<bool, String>) -> Verdict {
506 match result {
507 Ok(true) => Verdict::Allow,
508 Ok(false) => Verdict::False,
509 Err(_) => Verdict::Error,
510 }
511}
512
513fn entry(text: &str, method: Method, line: u32) -> String {
514 format!("{text} for '{}' @ L{line}", method.name())
515}
516
517/// What every condition of one request is evaluated with.
518struct Context<'a> {
519 ruleset: &'a Ruleset,
520 service: &'a ast::Service,
521 request: Val,
522 resource: Val,
523 /// The path whose document is free to look up, in the database `get()` may read.
524 target: &'a [String],
525 documents: &'a dyn Documents,
526}
527
528impl Context<'_> {
529 /// Evaluates `statements` in order; if none allows, each one's entry. As the official
530 /// emulator does, the statements of a `match` stop at the first that allows, but those of
531 /// every other `match` are evaluated too (`tools/oracle/rules_coverage.py`).
532 fn decide(
533 &self,
534 statements: &[(&ast::Allow, &Covering<'_>)],
535 method: Method,
536 budget: &mut Budget,
537 trace: &mut Trace,
538 ) -> Result<(), Vec<String>> {
539 let (entries, allowed) = self.each(statements, method, budget, trace);
540 if allowed { Ok(()) } else { Err(entries) }
541 }
542
543 /// The statements of an upsert's method that does not apply, for a denial: each one's
544 /// entry, `false` or its condition's error.
545 fn listed(
546 &self,
547 statements: &[(&ast::Allow, &Covering<'_>)],
548 method: Method,
549 budget: &mut Budget,
550 trace: &mut Trace,
551 ) -> Vec<String> {
552 let mut entries = Vec::with_capacity(statements.len());
553 let mut marks = Vec::with_capacity(statements.len());
554 let at = trace.evaluating(method, &self.request, &self.resource);
555 for (allow, covering) in statements {
556 marks.push(trace.mark());
557 let (result, line) = match &allow.condition {
558 Some(condition) => (
559 self.evaluate(covering, condition, budget, trace),
560 condition.span.line,
561 ),
562 None => (Ok(true), allow.span.line),
563 };
564 trace.verdict(at, line, verdict(&result));
565 let text = result.err();
566 entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
567 }
568 trace.repeat(&marks);
569 entries
570 }
571
572 /// Evaluates `statements` but those after one that allowed in the same `match`. Returns the
573 /// entries of those that did not allow, and whether one did.
574 fn each(
575 &self,
576 statements: &[(&ast::Allow, &Covering<'_>)],
577 method: Method,
578 budget: &mut Budget,
579 trace: &mut Trace,
580 ) -> (Vec<String>, bool) {
581 let mut entries = Vec::with_capacity(statements.len());
582 let mut marks = Vec::with_capacity(statements.len());
583 let mut allowed: Vec<*const ast::Match> = Vec::new();
584 let at = trace.evaluating(method, &self.request, &self.resource);
585 for (allow, covering) in statements {
586 let target = std::ptr::from_ref(covering.target());
587 if allowed.contains(&target) {
588 continue;
589 }
590 marks.push(trace.mark());
591 let Some(condition) = &allow.condition else {
592 trace.verdict(at, allow.span.line, Verdict::Allow);
593 allowed.push(target);
594 continue;
595 };
596 let result = self.evaluate(covering, condition, budget, trace);
597 trace.verdict(at, condition.span.line, verdict(&result));
598 let text = match result {
599 Ok(true) => {
600 allowed.push(target);
601 continue;
602 }
603 Ok(false) => "false".to_owned(),
604 Err(e) => e,
605 };
606 entries.push(entry(&text, method, condition.span.line));
607 }
608 trace.repeat(&marks);
609 (entries, !allowed.is_empty())
610 }
611
612 /// The condition of a statement at the end of `covering`: true, false or the error's text.
613 fn evaluate(
614 &self,
615 covering: &Covering<'_>,
616 condition: &ast::Expr,
617 budget: &mut Budget,
618 trace: &mut Trace,
619 ) -> Result<bool, String> {
620 let mut interp = Interp::new(
621 levels(self.ruleset, self.service, covering),
622 self.request.clone(),
623 self.resource.clone(),
624 self.target,
625 self.documents,
626 budget,
627 trace,
628 &self.ruleset.parens,
629 );
630 let frame = Frame::condition(covering.chain.len() + 1);
631 interp
632 .eval(condition, frame.scope())
633 .and_then(interp::boolean)
634 .map_err(|e| {
635 if e.message.is_empty() {
636 // Undecided: the official emulator names the condition's start.
637 format!(
638 "evaluation error at L{}:{}",
639 condition.span.line, condition.span.column
640 )
641 } else {
642 e.message
643 }
644 })
645 }
646}
647
648/// The names a condition at the end of `covering` sees: the functions of the file, of the
649/// service and of each `match`, and the variables each `match` binds.
650fn levels<'a>(
651 ruleset: &'a Ruleset,
652 service: &'a ast::Service,
653 covering: &'a Covering<'a>,
654) -> Vec<Level<'a>> {
655 let mut levels = vec![
656 Level {
657 functions: &ruleset.functions,
658 variables: Vec::new(),
659 },
660 Level {
661 functions: &service.body.functions,
662 variables: Vec::new(),
663 },
664 ];
665 let mut bindings = covering.bindings.iter();
666 for m in &covering.chain {
667 let count = m
668 .path
669 .iter()
670 .filter(|s| s.kind.variable().is_some())
671 .count();
672 let variables = bindings
673 .by_ref()
674 .take(count)
675 // A wildcard that took a list's unknown document ID is not bound: using it is a
676 // `Null value error`, as for any unknown name.
677 .filter_map(|(name, binding)| {
678 let value = match binding {
679 Binding::Segment(s) => Val::str(s),
680 Binding::Path(p) => Val::path(p.clone()),
681 Binding::Unbound => return None,
682 };
683 Some((name.as_str(), value))
684 })
685 .collect();
686 levels.push(Level {
687 functions: &m.body.functions,
688 variables,
689 });
690 }
691 levels
692}
693
694/// `request` for the statements of `method`, with the keys the official emulator gives it, in
695/// the order its coverage report lists them (`tools/oracle/rules_request_fields.py`):
696///
697/// - for a `get`: `time`, `auth`, `inTransaction`, `path`, `fields` (null), `method`;
698/// - for a `list`: `time`, `auth`, `inTransaction`, `fields`, `query`, `path`, `method`;
699/// - for a write: `time`, `auth`, `inTransaction`, `path`, `transforms`, `writeFields`,
700/// `readFields` (null), `autoId` (for a create that is not an upsert), `resource` (null for a
701/// delete; only `data` when the ID is the server's), `method`; for a verify, a `get`, the
702/// same but `autoId` (`tools/oracle/rules_verify.py`).
703///
704/// `auth` is null without a token, else `uid` and `token`.
705fn request_value(request: &Request, method: Method) -> Val {
706 let strings = |list: &Option<Vec<String>>| {
707 list.as_ref().map_or(Val::Null, |items| {
708 Val::list(items.iter().map(|s| Val::str(s)).collect())
709 })
710 };
711 let mut map = IndexMap::new();
712 map.insert(
713 "time".to_owned(),
714 Val::from(&Value::Timestamp(request.time)),
715 );
716 map.insert("auth".to_owned(), auth_value(request));
717 map.insert(
718 "inTransaction".to_owned(),
719 Val::Bool(request.in_transaction),
720 );
721 let path = Val::path(request.path.clone());
722 match method {
723 Method::Get if !request.verify => {
724 map.insert("path".to_owned(), path);
725 map.insert("fields".to_owned(), Val::Null);
726 }
727 Method::List => {
728 let query = request.query.clone().unwrap_or_default();
729 map.insert("fields".to_owned(), strings(&query.fields));
730 map.insert("query".to_owned(), query_value(request, &query));
731 map.insert("path".to_owned(), path);
732 }
733 Method::Get | Method::Create | Method::Update | Method::Delete => {
734 map.insert("path".to_owned(), path);
735 map.insert("transforms".to_owned(), strings(&request.transforms));
736 map.insert("writeFields".to_owned(), strings(&request.write_fields));
737 map.insert("readFields".to_owned(), Val::Null);
738 if method == Method::Create && !request.upsert {
739 map.insert("autoId".to_owned(), Val::Bool(request.auto_id));
740 }
741 let resource = match &request.resource {
742 None => Val::Null,
743 // The document has no name yet.
744 Some(doc) if request.auto_id => match value::resource(doc) {
745 Val::Map(map) => {
746 let data = map.get("data").cloned().unwrap_or(Val::Null);
747 Val::Map(Rc::new(IndexMap::from([("data".to_owned(), data)])))
748 }
749 other => other,
750 },
751 Some(doc) => value::resource(doc),
752 };
753 map.insert("resource".to_owned(), resource);
754 }
755 }
756 map.insert("method".to_owned(), Val::str(method.name()));
757 Val::Map(Rc::new(map))
758}
759
760/// `request.query`, its keys in the order of the official emulator's coverage report.
761fn query_value(request: &Request, query: &Query) -> Val {
762 let all_descendants = query.group.is_some() && !query.children;
763 // The collection ID, and the document holding the collection (or the collections).
764 let (kind, parent) = match &query.group {
765 None => match request.path.split_last() {
766 Some((id, parent)) => (Val::str(id), parent),
767 None => (Val::Null, &[][..]),
768 },
769 Some(id) if id.is_empty() => (Val::Null, request.path.as_slice()),
770 Some(id) => (Val::str(id), request.path.as_slice()),
771 };
772 // `databases/{id}/documents`, the root, is no document.
773 let parent = if parent.len() > 3 {
774 Val::path(parent.to_vec())
775 } else {
776 Val::Null
777 };
778 let order_by = query
779 .order_by
780 .iter()
781 .map(|(field, direction)| (field.clone(), Val::str(direction)))
782 .collect();
783 let mut q = IndexMap::new();
784 q.insert("distinct".to_owned(), Val::Bool(false));
785 q.insert("selectOnlyKeys".to_owned(), Val::Bool(false));
786 q.insert("groupBy".to_owned(), Val::Map(Rc::default()));
787 q.insert("kind".to_owned(), kind);
788 let parent_key = if all_descendants {
789 "ancestor"
790 } else {
791 "parent"
792 };
793 q.insert(parent_key.to_owned(), parent);
794 q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
795 q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
796 q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
797 q.insert("allDescendants".to_owned(), Val::Bool(all_descendants));
798 Val::Map(Rc::new(q))
799}
800
801/// `request.auth`: null without a token, else `uid` and `token`.
802fn auth_value(request: &Request) -> Val {
803 match &request.auth {
804 None => Val::Null,
805 Some(auth) => {
806 let mut a = IndexMap::new();
807 a.insert(
808 "uid".to_owned(),
809 auth.uid.as_deref().map_or(Val::Null, Val::str),
810 );
811 a.insert(
812 "token".to_owned(),
813 Val::from(&Value::Map(auth.token.clone())),
814 );
815 Val::Map(Rc::new(a))
816 }
817 }
818}