From 21b7debaf14c6a0f66e46adf888b410830999659 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Wed, 14 Oct 2026 18:10:25 +1100 Subject: [PATCH 1/2] feat(stack-encrypt): declare the JSON operators Contains, ContainedBy and AtPath ADR-0009 decision 1 names three JSON operators. `op::Contains` and `op::ContainedBy` are the containments (`@>` and `<@`), exact, and take one value, the document. `op::AtPath`, written `op::AtPath(op::Eq)`, applies another operator at a JSON path and inherits that operator's fidelity through `TermFor`: an exact term answers `AtPath` and `AtPath`, and only a probabilistic one answers `AtPath`. No index answers them. This crate has no JSON index, and EQL v4 has no JSON type Stack Encrypt produces, so a containment query or a query at a path does not compile on any plan, which the compile-fail tests pin. `AtPath`'s arity is a new `op::Pathed`, a path and the leaf's input, and no `op::Input` of it exists until the JSON index arrives, since the path becomes a selector only that index can derive. The value-selector and selector-plus-leaf term shapes the issue lists wait for the same index. Closes #1187 Claude-Session: https://claude.ai/code/session_01VYKAWbd361xDLmqBdLoUcB --- packages/stack-encrypt/CHANGELOG.md | 7 ++ packages/stack-encrypt/src/op/mod.rs | 71 ++++++++++++++++++- .../tests/ui/op_at_path_inherits_fidelity.rs | 10 +++ .../ui/op_at_path_inherits_fidelity.stderr | 21 ++++++ .../tests/ui/op_json_not_answered.rs | 19 +++++ .../tests/ui/op_json_not_answered.stderr | 29 ++++++++ 6 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.rs create mode 100644 packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.stderr create mode 100644 packages/stack-encrypt/tests/ui/op_json_not_answered.rs create mode 100644 packages/stack-encrypt/tests/ui/op_json_not_answered.stderr diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 22f83342c..4e0ccfa64 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -59,6 +59,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 What a query takes is the operator's `op::Arity` (`op::One` or `op::Pair`), and `op::Input` relates the value given to the plaintext the plan is checked against (ADR-0009, decision 7). +- **The JSON operators are declared.** `op::Contains` and `op::ContainedBy` + (`@>` and `<@`) and the accessor `op::AtPath`, `op::AtPath(op::Eq)`, + which applies another operator at a JSON path and inherits its fidelity + (`op::TermFor`): `AtPath` and `AtPath` are exact. No index + answers them yet, since this crate has no JSON index, so a query with + them does not compile until one exists; `AtPath`'s arity, `op::Pathed`, + has no `op::Input` until then (ADR-0009, decision 1). - **`target_limit` says how many rows a match query should fetch.** `query.target_limit(want, &stats)` is `want` plus the false positives expected at the confidence `stats` names (99% by default), as an diff --git a/packages/stack-encrypt/src/op/mod.rs b/packages/stack-encrypt/src/op/mod.rs index 1b0b913c0..1a36809a9 100644 --- a/packages/stack-encrypt/src/op/mod.rs +++ b/packages/stack-encrypt/src/op/mod.rs @@ -148,9 +148,44 @@ impl Operator for Between { type Arity = Pair; } +/// JSON containment: rows whose document contains the query's document +/// (`@>`). Exact. Declared by ADR-0009 and answered by no index yet: this +/// crate has no JSON index, so a query with it does not compile until one +/// exists. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct Contains; + +/// JSON containment the other way: rows whose document is contained by +/// the query's (`<@`). Exact, and answered by no index yet, as +/// [`Contains`] is. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct ContainedBy; + +/// The accessor: `Op` applied at a JSON path, `op::AtPath(op::Eq)`. It +/// takes a path and `Op`'s input, and inherits the fidelity of `Op` +/// ([`TermFor`]): `AtPath` and `AtPath` are exact, and an accessor +/// over a probabilistic leaf operator would be `Probabilistic`. +/// +/// Its arity is [`Pathed`], which no input has yet: the path becomes a +/// selector only a JSON index can derive, and this crate has none, so a +/// query with `AtPath` does not compile until that index exists. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct AtPath(pub Op); + +impl Operator for Contains { + type Arity = One; +} +impl Operator for ContainedBy { + type Arity = One; +} +impl Operator for AtPath { + type Arity = Pathed; +} + /// How many plaintexts a query for an operator takes: [`One`] or a -/// [`Pair`]. Sealed: the chain derives a term per plaintext and joins them -/// as the arity says, so there are exactly these two. +/// [`Pair`], or for an accessor a path and its leaf's input ([`Pathed`]). +/// Sealed: the chain derives a term per plaintext and joins them as the +/// arity says, so there are exactly these. pub trait Arity: sealed::Sealed + 'static {} /// One plaintext: the query is given an `S` (`Eq`, `Lt`, `Matches`). @@ -161,8 +196,15 @@ pub struct One; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct Pair; +/// A path and a leaf operator's input: what [`AtPath`] takes. No +/// [`Input`] of this arity exists yet; it arrives with the JSON index, +/// whose selector the path becomes. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct Pathed; + impl Arity for One {} impl Arity for Pair {} +impl Arity for Pathed {} /// What a query of arity `A` is given, and the plaintext its index is /// declared over: an `S` is an input of [`One`] over `S`, and an `(S, S)` @@ -382,6 +424,7 @@ mod sealed { impl Sealed for super::CaseSensitive {} impl Sealed for super::One {} impl Sealed for super::Pair {} + impl Sealed for super::Pathed {} } /// The case a text operator is asked in: [`CaseInsensitive`] or @@ -510,7 +553,12 @@ impl TermFor for T {} impl TermFor for T {} impl TermFor for T {} impl TermFor for T {} +impl TermFor for T {} +impl TermFor for T {} impl TermFor> for T {} +/// An accessor inherits its leaf's requirement: what answers `Op` at a +/// path is held to what answers `Op`. +impl> TermFor> for T {} /// An index that answers the operator `Op` for the input `In`, and the /// query term it sends for it. @@ -799,6 +847,25 @@ mod tests { ); assert_eq!(format!("{:?}", Eq), "Eq"); assert_eq!(format!("{:?}", Between), "Between"); + assert_eq!(format!("{:?}", AtPath(Gte)), "AtPath(Gte)"); + assert_eq!(AtPath(Matches.case_sensitive()).0, Matches.case_sensitive()); + } + + /// An accessor is held to its leaf's requirement: an exact term + /// answers `AtPath` and `AtPath`, and only a probabilistic one + /// answers `AtPath`. Containment takes any term. + #[test] + fn an_accessor_inherits_the_fidelity_of_its_leaf() { + fn answers>() {} + answers::, EqualityTerm>(); + answers::, OreTerm>(); + answers::, RangeQuery>>(); + answers::, MatchQuery>(); + answers::>, EqualityTerm>(); + answers::(); + answers::(); + // `answers::, EqualityTerm>()` does not compile: + // `tests/ui/op_at_path_inherits_fidelity.rs`. } /// The ordering operators default to ORE, and a range is refused only diff --git a/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.rs b/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.rs new file mode 100644 index 000000000..c017a4a3e --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.rs @@ -0,0 +1,10 @@ +//! An accessor is held to its leaf's requirement: `AtPath` needs a +//! probabilistic term, as `Matches` does, so an exact term cannot answer it. +use stack_encrypt::op::{self, TermFor}; +use stack_encrypt::sem::EqualityTerm; + +fn answers>() {} + +fn main() { + answers::, EqualityTerm>(); +} diff --git a/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.stderr b/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.stderr new file mode 100644 index 000000000..64fc20e2f --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_at_path_inherits_fidelity.stderr @@ -0,0 +1,21 @@ +error[E0277]: `EqualityTerm` cannot answer `AtPath` + --> tests/ui/op_at_path_inherits_fidelity.rs:9:40 + | +9 | answers::, EqualityTerm>(); + | ^^^^^^^^^^^^ this query term does not meet the operator's requirement + | + = help: the trait `Probabilistic` is not implemented for `EqualityTerm` + = note: a term that answers `op::Matches` must implement `op::Probabilistic` +help: the trait `Probabilistic` is implemented for `MatchQuery` + --> src/sem/mod.rs + | + | impl crate::op::Probabilistic for MatchQuery { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = note: required for `EqualityTerm` to implement `TermFor` + = note: 1 redundant requirement hidden + = note: required for `EqualityTerm` to implement `TermFor>` +note: required by a bound in `answers` + --> tests/ui/op_at_path_inherits_fidelity.rs:6:33 + | +6 | fn answers>() {} + | ^^^^^^^^^^^ required by this bound in `answers` diff --git a/packages/stack-encrypt/tests/ui/op_json_not_answered.rs b/packages/stack-encrypt/tests/ui/op_json_not_answered.rs new file mode 100644 index 000000000..2f208e9fe --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_json_not_answered.rs @@ -0,0 +1,19 @@ +//! The JSON operators are declared and answered by no index: this crate has +//! no JSON index, so a containment query and a query at a path do not +//! compile on any plan. +use stack_encrypt::kms::FakeDataKeySource; +use stack_encrypt::op; +use stack_encrypt::{Equality, Plan, StackCipher}; + +async fn query(cipher: &StackCipher) { + let profile_plan = Plan::context("users/profile") + .with::(Equality) + .build() + .unwrap(); + let _ = cipher.query(op::Contains, "{}").on(&profile_plan); + let _ = cipher + .query(op::AtPath(op::Eq), &("$.address.city", "Sydney")) + .on(&profile_plan); +} + +fn main() {} diff --git a/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr b/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr new file mode 100644 index 000000000..1105d61e6 --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr @@ -0,0 +1,29 @@ +error[E0277]: `Contains` has no default index given `std::string::String` + --> tests/ui/op_json_not_answered.rs:13:46 + | +13 | let _ = cipher.query(op::Contains, "{}").on(&profile_plan); + | ^^ no default index answers this operator here + | + = help: the trait `DefaultIndex` is not implemented for `Contains` + = note: `op::Eq` defaults to `Equality`; `op::Matches` defaults to `Match` with the default configuration, which is case-insensitive and over text only; `op::Lt`, `Lte`, `Gt`, `Gte` and `Between` default to `Ore` over an integer, and `Between` takes a pair; name another index, such as `Ope`, with `.via(index)` + = help: the following other types implement trait `DefaultIndex`: + `Between` implements `DefaultIndex<(S, S)>` + `Gt` implements `DefaultIndex` + `Gte` implements `DefaultIndex` + `Lt` implements `DefaultIndex` + `Lte` implements `DefaultIndex` + `stack_encrypt::op::Eq` implements `DefaultIndex` + `stack_encrypt::op::Matches<::Case>` implements `DefaultIndex` + = note: required for `ValuePlan>` to implement `Chooses` + +error[E0599]: the method `on` exists for struct `QueryBuilder<'_, AtPath, (&str, &str), FakeDataKeySource>`, but its trait bounds were not satisfied + --> tests/ui/op_json_not_answered.rs:16:10 + | +14 | let _ = cipher + | _____________- +15 | | .query(op::AtPath(op::Eq), &("$.address.city", "Sydney")) +16 | | .on(&profile_plan); + | |_________-^^ + | + = note: the following trait bounds were not satisfied: + `(&str, &str): Input` From 6eff76aaa71c3f18ee23f4f6de21f73a1e524d99 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 12:19:22 +1100 Subject: [PATCH 2/2] fix(stack-encrypt): address the second review of the JSON operators - `op::Pathed` carries the leaf operator's arity, `Pathed`, so `AtPath` takes `Pathed` (a path and a value) and `AtPath` takes `Pathed` (a path and a pair of bounds). A JSON index's `Input` impls can then derive one term or two as the leaf needs. A unit test pins the three arities. - `AtPath` takes only a leaf operator, the new `op::Leaf`: `Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` and `Matches`. `AtPath`, `AtPath` and `AtPath>` are no longer operators, since none of them is one selector and value; `op::LeafArity` (sealed: `One` and `Pair`) keeps an accessor from nesting. `tests/ui/op_at_path_not_a_leaf.rs` pins the diagnostic. - The `DefaultIndex` diagnostic says no index answers the JSON operators yet, instead of only pointing at `.via(index)`, which led to a second error. `op::Input` gains a diagnostic naming what each arity takes. - The module intro lists the JSON operators and the `Pathed` arity, and `Operator::Arity`'s doc names all three arities. --- packages/stack-encrypt/CHANGELOG.md | 8 +- packages/stack-encrypt/src/op/mod.rs | 114 +++++++++++++----- .../tests/ui/op_at_path_not_a_leaf.rs | 11 ++ .../tests/ui/op_at_path_not_a_leaf.stderr | 49 ++++++++ .../tests/ui/op_between_takes_a_pair.stderr | 6 +- .../tests/ui/op_json_not_answered.stderr | 29 ++++- 6 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.rs create mode 100644 packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.stderr diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 4e0ccfa64..df56f71ed 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -64,8 +64,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 which applies another operator at a JSON path and inherits its fidelity (`op::TermFor`): `AtPath` and `AtPath` are exact. No index answers them yet, since this crate has no JSON index, so a query with - them does not compile until one exists; `AtPath`'s arity, `op::Pathed`, - has no `op::Input` until then (ADR-0009, decision 1). + them does not compile until one exists. `AtPath` takes a leaf + operator (`op::Leaf`: `Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between`, + `Matches`), so `AtPath` and a nested `AtPath>` do not + compile, and its arity, `op::Pathed`, carries the leaf's (`Pathed` + for `AtPath`, `Pathed` for `AtPath`); it has no + `op::Input` until the JSON index exists (ADR-0009, decision 1). - **`target_limit` says how many rows a match query should fetch.** `query.target_limit(want, &stats)` is `want` plus the false positives expected at the confidence `stats` names (99% by default), as an diff --git a/packages/stack-encrypt/src/op/mod.rs b/packages/stack-encrypt/src/op/mod.rs index 1a36809a9..7bc228e69 100644 --- a/packages/stack-encrypt/src/op/mod.rs +++ b/packages/stack-encrypt/src/op/mod.rs @@ -1,10 +1,14 @@ //! Operators: what a query asks of the server, as a type. //! -//! A query names an *operator* ([`Eq`](struct@Eq), [`Lt`], [`Between`], -//! [`Matches`](struct@Matches)), and the plan it runs on finds the index -//! that answers it. The operator is a unit type passed by value, as an -//! index is (`.via(Ore)`), and it decides what the query takes: one value -//! for most, a pair of bounds for `Between`: +//! A query names an *operator* ([`Eq`](struct@Eq), [`Lt`], [`Lte`], +//! [`Gt`], [`Gte`], [`Between`], [`Matches`](struct@Matches), and the +//! JSON operators [`Contains`], [`ContainedBy`] and [`AtPath`]), and the +//! plan it runs on finds the index that answers it. The operator is a unit +//! type passed by value, as an index is (`.via(Ore)`), and it decides what +//! the query takes: one value for most, a pair of bounds for `Between`, a +//! path and the leaf operator's input for `AtPath`. No index answers the +//! JSON operators yet: this crate has no JSON index, so a query with one +//! does not compile until that index exists. //! //! ``` //! # async fn example() -> Result<(), stack_encrypt::Error> { @@ -42,10 +46,13 @@ //! - [`DefaultIndex`] is on an operator, and names the index a plan that //! declares its indexes, rather than a target type, answers it with. //! -//! What a query takes is the operator's [`Arity`]: [`One`] plaintext, or a -//! [`Pair`] of them as a tuple. [`Input`] says, for the value a query is -//! given, which plaintext the index is declared over (`S` for `(S, S)`), -//! so the plan's type is checked against it. +//! What a query takes is the operator's [`Arity`]: [`One`] plaintext, a +//! [`Pair`] of them as a tuple, or for an accessor a path and its leaf +//! operator's input ([`Pathed`](Pathed), where `A` is the leaf's arity). +//! [`Input`] says, for the value a query is given, which plaintext the +//! index is declared over (`S` for `(S, S)`), so the plan's type is checked +//! against it. No input of a `Pathed` arity exists until the JSON index +//! does. //! //! What the types can see is checked at compile time: a match query on an //! integer, an ordering on text, an operator a target type does not answer, @@ -92,8 +99,9 @@ use crate::{Error, KeysetCipher, Label, NonEmpty, PlanError}; /// The trait is open, as [`Index`] is: an operator defined in another crate /// is answered by the indexes that implement [`Supports`] for it. pub trait Operator: Copy + 'static { - /// What a query for this operator takes: [`One`] plaintext, or a - /// [`Pair`] of them as a tuple. + /// What a query for this operator takes: [`One`] plaintext, a + /// [`Pair`] of them as a tuple, or for [`AtPath`] a path and its leaf + /// operator's input ([`Pathed`]). type Arity: Arity; } @@ -166,7 +174,13 @@ pub struct ContainedBy; /// ([`TermFor`]): `AtPath` and `AtPath` are exact, and an accessor /// over a probabilistic leaf operator would be `Probabilistic`. /// -/// Its arity is [`Pathed`], which no input has yet: the path becomes a +/// `Op` is a [`Leaf`] operator, one that compares a single value at the +/// path: `AtPath` and `AtPath>` are not operators, +/// since neither is one selector and value. +/// +/// Its arity is [`Pathed`](Pathed), so `AtPath` (a path and +/// a value) and `AtPath` (a path and a pair of bounds) take +/// different inputs. No input of it exists yet: the path becomes a /// selector only a JSON index can derive, and this crate has none, so a /// query with `AtPath` does not compile until that index exists. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] @@ -178,16 +192,43 @@ impl Operator for Contains { impl Operator for ContainedBy { type Arity = One; } -impl Operator for AtPath { - type Arity = Pathed; +impl Operator for AtPath { + type Arity = Pathed; } +/// An operator that compares one value, or one pair of bounds, and so can +/// be applied at a JSON path ([`AtPath`]): [`Eq`](struct@Eq), [`Lt`], +/// [`Lte`], [`Gt`], [`Gte`], [`Between`] and [`Matches`](struct@Matches). +/// Containment compares whole documents and an accessor already has a +/// path, so neither is a leaf. +/// +/// The trait is open, as [`Operator`] is, but its arity is [`One`] or +/// [`Pair`] ([`LeafArity`], sealed), so an accessor never nests. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a leaf operator, so it cannot be applied at a path", + label = "`op::AtPath` takes a leaf operator", + note = "`op::AtPath` applies `op::Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` or `Matches` at a path; `op::Contains` and `op::ContainedBy` compare whole documents, and an accessor does not nest" +)] +pub trait Leaf: Operator {} + +impl Leaf for Eq {} +impl Leaf for Lt {} +impl Leaf for Lte {} +impl Leaf for Gt {} +impl Leaf for Gte {} +impl Leaf for Between {} +impl Leaf for Matches {} + /// How many plaintexts a query for an operator takes: [`One`] or a /// [`Pair`], or for an accessor a path and its leaf's input ([`Pathed`]). /// Sealed: the chain derives a term per plaintext and joins them as the /// arity says, so there are exactly these. pub trait Arity: sealed::Sealed + 'static {} +/// The arity of a [`Leaf`] operator: [`One`] or [`Pair`], never +/// [`Pathed`]. Sealed. +pub trait LeafArity: Arity {} + /// One plaintext: the query is given an `S` (`Eq`, `Lt`, `Matches`). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct One; @@ -196,27 +237,32 @@ pub struct One; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] pub struct Pair; -/// A path and a leaf operator's input: what [`AtPath`] takes. No -/// [`Input`] of this arity exists yet; it arrives with the JSON index, -/// whose selector the path becomes. +/// A path and a leaf operator's input, of the leaf's arity `A`: what +/// [`AtPath`] takes. `Pathed` is a path and a value, +/// `Pathed` a path and a pair of bounds, so an input can derive one +/// term or two as the leaf needs. No [`Input`] of this arity exists yet; +/// it arrives with the JSON index, whose selector the path becomes. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] -pub struct Pathed; +pub struct Pathed(PhantomData); impl Arity for One {} impl Arity for Pair {} -impl Arity for Pathed {} +impl Arity for Pathed {} +impl LeafArity for One {} +impl LeafArity for Pair {} /// What a query of arity `A` is given, and the plaintext its index is /// declared over: an `S` is an input of [`One`] over `S`, and an `(S, S)` /// an input of [`Pair`] over `S`. The chain's value type is the input, and /// the plan is checked against the plaintext. /// -/// Those are the two impls. The arity decides how the chain derives terms -/// from the input, which is what [`derive`](Self::derive) does. +/// Those are the two impls; an input of [`Pathed`] arrives with the JSON +/// index. The arity decides how the chain derives terms from the input, +/// which is what [`derive`](Self::derive) does. #[diagnostic::on_unimplemented( - message = "`{Self}` is not what a query of arity `{A}` is given", - label = "the operator takes another shape of value", - note = "`op::Between` takes a pair of bounds, `cipher.query(op::Between, &(lo, hi))`; every other operator takes one value" + message = "`{Self}` is not what a query of arity `{A}` takes", + label = "the operator takes another input", + note = "an operator of arity `One` takes one value, `op::Between` (arity `Pair`) takes a pair, `&(lo, hi)`, and `op::AtPath` (arity `Pathed`) takes no input yet: it arrives with the JSON index" )] pub trait Input: Sized + 'static { /// The plaintext the index is declared over. @@ -424,7 +470,7 @@ mod sealed { impl Sealed for super::CaseSensitive {} impl Sealed for super::One {} impl Sealed for super::Pair {} - impl Sealed for super::Pathed {} + impl Sealed for super::Pathed {} } /// The case a text operator is asked in: [`CaseInsensitive`] or @@ -558,7 +604,7 @@ impl TermFor for T {} impl TermFor> for T {} /// An accessor inherits its leaf's requirement: what answers `Op` at a /// path is held to what answers `Op`. -impl> TermFor> for T {} +impl> TermFor> for T {} /// An index that answers the operator `Op` for the input `In`, and the /// query term it sends for it. @@ -806,7 +852,7 @@ impl, T: Answers> Answers #[diagnostic::on_unimplemented( message = "`{Self}` has no default index given `{In}`", label = "no default index answers this operator here", - note = "`op::Eq` defaults to `Equality`; `op::Matches` defaults to `Match` with the default configuration, which is case-insensitive and over text only; `op::Lt`, `Lte`, `Gt`, `Gte` and `Between` default to `Ore` over an integer, and `Between` takes a pair; name another index, such as `Ope`, with `.via(index)`" + note = "`op::Eq` defaults to `Equality`; `op::Matches` defaults to `Match` with the default configuration, which is case-insensitive and over text only; `op::Lt`, `Lte`, `Gt`, `Gte` and `Between` default to `Ore` over an integer, and `Between` takes a pair; name another index, such as `Ope`, with `.via(index)`; no index answers `op::Contains`, `op::ContainedBy` or `op::AtPath` yet, since this crate has no JSON index" )] pub trait DefaultIndex>: Operator { /// The index. An index is a unit type, so its `Default` is the index. @@ -861,11 +907,21 @@ mod tests { answers::, OreTerm>(); answers::, RangeQuery>>(); answers::, MatchQuery>(); - answers::>, EqualityTerm>(); answers::(); answers::(); // `answers::, EqualityTerm>()` does not compile: - // `tests/ui/op_at_path_inherits_fidelity.rs`. + // `tests/ui/op_at_path_inherits_fidelity.rs`. Nor does an accessor + // over a non-leaf: `tests/ui/op_at_path_not_a_leaf.rs`. + } + + /// An accessor's arity carries its leaf's, so an input can tell a path + /// and a value from a path and a pair of bounds. + #[test] + fn an_accessor_takes_its_leafs_arity_under_a_path() { + fn arity, A: Arity>() {} + arity::, Pathed>(); + arity::, Pathed>(); + arity::, Pathed>(); } /// The ordering operators default to ORE, and a range is refused only diff --git a/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.rs b/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.rs new file mode 100644 index 000000000..59c0a8fba --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.rs @@ -0,0 +1,11 @@ +//! `AtPath` applies a leaf operator at a path: containment compares whole +//! documents, and an accessor already has a path, so neither is an +//! operator under `AtPath`. +use stack_encrypt::op; + +fn operator(_: Op) {} + +fn main() { + operator(op::AtPath(op::Contains)); + operator(op::AtPath(op::AtPath(op::Eq))); +} diff --git a/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.stderr b/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.stderr new file mode 100644 index 000000000..6519c4a31 --- /dev/null +++ b/packages/stack-encrypt/tests/ui/op_at_path_not_a_leaf.stderr @@ -0,0 +1,49 @@ +error[E0277]: `Contains` is not a leaf operator, so it cannot be applied at a path + --> tests/ui/op_at_path_not_a_leaf.rs:9:25 + | +9 | operator(op::AtPath(op::Contains)); + | -------- ^^^^^^^^^^^^ `op::AtPath` takes a leaf operator + | | + | required by a bound introduced by this call + | + = help: the trait `Leaf` is not implemented for `Contains` + = note: `op::AtPath` applies `op::Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` or `Matches` at a path; `op::Contains` and `op::ContainedBy` compare whole documents, and an accessor does not nest + = help: the following other types implement trait `Leaf`: + Between + Gt + Gte + Lt + Lte + stack_encrypt::op::Eq + stack_encrypt::op::Matches + = note: required for `AtPath` to implement `Operator` +note: required by a bound in `operator` + --> tests/ui/op_at_path_not_a_leaf.rs:6:17 + | +6 | fn operator(_: Op) {} + | ^^^^^^^^^^^^ required by this bound in `operator` + +error[E0277]: `AtPath` is not a leaf operator, so it cannot be applied at a path + --> tests/ui/op_at_path_not_a_leaf.rs:10:25 + | +10 | operator(op::AtPath(op::AtPath(op::Eq))); + | -------- ^^^^^^^^^^^^^^^^^^ `op::AtPath` takes a leaf operator + | | + | required by a bound introduced by this call + | + = help: the trait `Leaf` is not implemented for `AtPath` + = note: `op::AtPath` applies `op::Eq`, `Lt`, `Lte`, `Gt`, `Gte`, `Between` or `Matches` at a path; `op::Contains` and `op::ContainedBy` compare whole documents, and an accessor does not nest + = help: the following other types implement trait `Leaf`: + Between + Gt + Gte + Lt + Lte + stack_encrypt::op::Eq + stack_encrypt::op::Matches + = note: required for `AtPath>` to implement `Operator` +note: required by a bound in `operator` + --> tests/ui/op_at_path_not_a_leaf.rs:6:17 + | + 6 | fn operator(_: Op) {} + | ^^^^^^^^^^^^ required by this bound in `operator` diff --git a/packages/stack-encrypt/tests/ui/op_between_takes_a_pair.stderr b/packages/stack-encrypt/tests/ui/op_between_takes_a_pair.stderr index f0e245640..f14672a61 100644 --- a/packages/stack-encrypt/tests/ui/op_between_takes_a_pair.stderr +++ b/packages/stack-encrypt/tests/ui/op_between_takes_a_pair.stderr @@ -1,13 +1,13 @@ -error[E0277]: `u32` is not what a query of arity `Pair` is given +error[E0277]: `u32` is not what a query of arity `Pair` takes --> tests/ui/op_between_takes_a_pair.rs:12:39 | 12 | let _ = cipher.query(op::Between, &40u32).on(&age_plan); - | ----- ^^^^^^ the operator takes another shape of value + | ----- ^^^^^^ the operator takes another input | | | required by a bound introduced by this call | = help: the trait `Input` is not implemented for `u32` - = note: `op::Between` takes a pair of bounds, `cipher.query(op::Between, &(lo, hi))`; every other operator takes one value + = note: an operator of arity `One` takes one value, `op::Between` (arity `Pair`) takes a pair, `&(lo, hi)`, and `op::AtPath` (arity `Pathed`) takes no input yet: it arrives with the JSON index help: the trait `Input` is implemented for `(S, S)` --> src/op/mod.rs | diff --git a/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr b/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr index 1105d61e6..a2feec653 100644 --- a/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr +++ b/packages/stack-encrypt/tests/ui/op_json_not_answered.stderr @@ -5,7 +5,7 @@ error[E0277]: `Contains` has no default index given `std::string::String` | ^^ no default index answers this operator here | = help: the trait `DefaultIndex` is not implemented for `Contains` - = note: `op::Eq` defaults to `Equality`; `op::Matches` defaults to `Match` with the default configuration, which is case-insensitive and over text only; `op::Lt`, `Lte`, `Gt`, `Gte` and `Between` default to `Ore` over an integer, and `Between` takes a pair; name another index, such as `Ope`, with `.via(index)` + = note: `op::Eq` defaults to `Equality`; `op::Matches` defaults to `Match` with the default configuration, which is case-insensitive and over text only; `op::Lt`, `Lte`, `Gt`, `Gte` and `Between` default to `Ore` over an integer, and `Between` takes a pair; name another index, such as `Ope`, with `.via(index)`; no index answers `op::Contains`, `op::ContainedBy` or `op::AtPath` yet, since this crate has no JSON index = help: the following other types implement trait `DefaultIndex`: `Between` implements `DefaultIndex<(S, S)>` `Gt` implements `DefaultIndex` @@ -16,6 +16,31 @@ error[E0277]: `Contains` has no default index given `std::string::String` `stack_encrypt::op::Matches<::Case>` implements `DefaultIndex` = note: required for `ValuePlan>` to implement `Chooses` +error[E0277]: `(&str, &str)` is not what a query of arity `Pathed` takes + --> tests/ui/op_json_not_answered.rs:15:36 + | +15 | .query(op::AtPath(op::Eq), &("$.address.city", "Sydney")) + | ----- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the operator takes another input + | | + | required by a bound introduced by this call + | + = note: an operator of arity `One` takes one value, `op::Between` (arity `Pair`) takes a pair, `&(lo, hi)`, and `op::AtPath` (arity `Pathed`) takes no input yet: it arrives with the JSON index +help: the trait `Input>` is not implemented for `(&str, &str)` + but trait `Input` is implemented for it + --> src/op/mod.rs + | + | impl Input for (S, S) { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + = help: for that trait implementation, expected `Pair`, found `Pathed` +note: required by a bound in `plan::chain::>::query` + --> src/plan/chain.rs + | + | pub fn query<'a, Op, Q>(&'a self, op: Op, value: &Q) -> QueryBuilder<'a, Op, F, K> + | ----- required by a bound in this associated function +... + | F: Input, + | ^^^^^^^^^^^^^^^^ required by this bound in `plan::chain::>::query` + error[E0599]: the method `on` exists for struct `QueryBuilder<'_, AtPath, (&str, &str), FakeDataKeySource>`, but its trait bounds were not satisfied --> tests/ui/op_json_not_answered.rs:16:10 | @@ -26,4 +51,4 @@ error[E0599]: the method `on` exists for struct `QueryBuilder<'_, AtPath` + `(&str, &str): Input>`