Skip to main content
The sandbox answers from the spec, from armed faults, and from recorded traffic. None of those can say “when a charge with this reference already exists, answer 422 with this body”, which is what an undocumented provider rule is. A rule can. Rules live beside the imported spec in <data_dir>/apis/<sandbox>.rules.json, one versioned file per sandbox, written atomically. The sandbox loads them with the spec, and a rule that cannot be true of this spec is refused at load, by name.
With that file in place and pikopod up serving examplepay, the first create with reference: order-77 is answered from the spec and the second by the rule:
Every response a rule produced carries x-pikopod-rule with the rule’s id.

When a rule is consulted

For every request, in this order: auth, faults, route, rules, operation. A rule is only consulted once the request is authenticated, no armed fault answered, and the route matched a declared operation. The first matching rule in file order wins, and the operation behind it never runs. A rule cannot fire on a route the spec does not declare.

when

Body matchers: State conditions read the state a resource carries in the store, which a rule’s set_state sets:

respond

provenance is manual for a rule you wrote, promoted:<fingerprint> for one promoted from an observed divergence, and imported:<fingerprint> for one that arrived in an incident bundle. version is stamped when the rule is first saved, and the file’s version advances on every save.

Managing rules

You can edit the file by hand, or let pikopod rule do it and validate every change against the spec first. A rule file is one rule, or rules: [...], in YAML or JSON:
When pikopod up is serving the sandbox, add and drop push the new set to it at once. rule check tells you about a rule that fires on every request to an operation, so its declared success is never served, and about rules that can never fire. See pikopod rule.

Promoting a divergence

A divergence is the provider answering a recorded request differently from the sandbox. rule promote turns one into a rule, so the sandbox keeps the provider’s behaviour from then on:
How the rule is derived, from the recording and nothing else:
  • when is the recorded request’s features. A tokenized identifier becomes exists_in_store on that field, which is the shape of “this value was already used”. A readable value becomes equals. A value the redactor dropped cannot be conditioned on, and the command says so, because a rule missing that condition may fire more broadly than the provider does.
  • respond is the provider’s recorded status and body, redacted as stored.
  • provenance is promoted:<fingerprint>, so rule list shows where it came from.
Without --yes it asks two questions before writing, and either answered no writes nothing:
The first is the question the agent cannot answer: a 422 may be the provider’s rule, or this integration sending something wrong. The second is about scope: a rule answers every caller of the sandbox. In the run above the reference, which was the real reason for the refusal, had been dropped before disk, so the honest answer to the second question is no, and the rule to write by hand is the exists_in_store one at the top of this page. After writing, the command replays the recorded traffic through a fork with and without the new rule and prints both statuses, so you see what changed before anyone else does.

Seeing which rule fired

requests --explain replays a request against a fork and names the rule:
That run had a second rule, reference-required, with "body": { "reference": "absent" }. The fork starts from an empty store, so a rule that depends on stored state, such as exists_in_store, only fires against the served sandbox.

Refused at load

A rule can also supply what the spec left out. When a provider requires a field its spec does not mark required, a rule with "body": { "reference": "absent" } answering the provider’s real 4xx makes the sandbox refuse the way the provider does. A rule naming a route, an event or an example the spec does not declare is refused when the sandbox loads, and the error names the rule:
Rules replay byte for byte under the same seed, like everything else the sandbox answers, and the parity suite holds a transcript that proves it.