Wymcp. Tool. ValueGate
(Wymcp v0.9.1)
View Source
The value gate: the last dispatch gate,
which checks every value in a call's data against the action's schema.
Where it sits in the chain, and what its refusal carries, is the rule
stated in Wymcp.Tool, "Dispatch errors and self-correction"; this module
builds what the gate validates against and reads data through it.
One root per action, built at registration
Each action's schema is compiled into a JSV.Root once, at the
registration moment, by
build_roots!/2: Wymcp.Router.init/1 stores the roots, and
Wymcp.Methods.ToolsCall hands the called tool's to dispatch on the
Wymcp.Context. A call validates against a built root and never compiles
a schema. A tool called with no stored root — a direct run/2 call with
a context Wymcp.Testing.build_context/1 built — has its action's root
built on the spot by build_root!/3, so it meets the same gate and the
same answer as a call over the wire.
The root is the action's :properties with the closure the unknown-key
gate enforces (Wymcp.Tool.KeySet.close_properties/1), under an object
type: the schema Wymcp.Help and the input_schema digest publish, so
what is validated is what a caller reads. :required stays out of it —
the required gate answers a missing property ahead of this one — and a
root without it is one a :defaults map, which never carries a required
key, validates against whole. A "required" inside a property's own
schema — a nested object's, an array item's — is part of the value it
constrains and stays in the root, so a bulk call one of whose items lacks
such a key is refused whole, as a call with one wrong-typed item is.
format is asserted: a "format": "date" property refuses "tomorrow",
and a format name JSV does not know refuses the schema at registration.
An output root (Wymcp.Tool.build_output_schema_root/1) leaves format
an annotation, because there the server checks its own data rather than
a caller's.
A schema JSV cannot build — an unknown format, a term JSON cannot carry,
such as a keyword list where a property schema belongs, and some keywords
of the wrong type — raises Wymcp.ConfigurationError on
Wymcp.Tool.actions/0, naming the tool, the action and what the build
stopped on. So does a :defaults value its property schema refuses, or
one JSV raises on while checking it, and that is what lets the gate read
data as the caller sent it, before defaults: every value a default adds
was proven here. The build checks no schema against the JSON Schema
metaschema, so a keyword of the wrong type JSV builds anyway —
"enum" => 5 — is met only when a value is checked against it, and
"required" => "a" is not met at all.
The data run_action/3 receives is the data as this gate read it: JSV's
cast, which turns a whole-number float sent where the schema says
"integer" — 2.0 — into the integer the schema promises.
What a refusal says
check/2 reports every offending value in one answer: one entry per
data path, spelled by
Wymcp.Tool.KeySet.render_path/1 from JSV's JSON pointer — the data is
walked to tell an array index from an object key that is all digits —
carrying that path's problems deduplicated and sorted. The entries are
bounded as the unknown-key gate's are: the first ten in sort order, chosen
by Wymcp.Bound.first/1, and a count of them all. A path's problems are
bounded the same way — the first ten, and an "and N more" text after
them when there are more — since a schema can put many texts at one path,
an anyOf whose branches each require a different property among them.
The problems are JSV's own texts at its cause level, read through
Wymcp.JsonRpc, except where JSV's text quotes what the caller sent —
a number has no length bound, and a 301-digit integer comes back whole;
a key's length is the body's; and uniqueItems names every duplicate
pair, so its text grows with the array. Those keywords get a text of
wymcp's that names no sent value:
minimum,maximum,exclusiveMinimum,exclusiveMaximumandmultipleOfname the bound alone ("value is higher than maximum 100"), and a number too large formultipleOf's division, which JSV reports as an arithmetic error, reads "value could not be checked against multipleOf";uniqueItemsreads "value must contain unique items";formatnames the schema's format and drops JSV's parenthesised reason, which foruri-templatequotes the sent string ("value does not respect the 'uri-template' format");additionalProperties,propertyNamesandpatternProperties— reached where the unknown-key gate does not follow, such as ananyOfbranch — say which keyword the object fails and name no key.
The data path already says which value it was. Every other keyword's
text passes through, and every problem, rewritten or not, goes through
Wymcp.Bound.echo/1, so a text no probe has met is cut at the bound
rather than sent whole. So the answer quotes the request only through
its data paths, under the rule Wymcp.Bound states in "What a refusal
quotes".
What a refusal costs
The ten paths are chosen before the data is read: JSV's errors are
grouped by JSON pointer, the pointers are ordered by their tokens alone,
and only the ten Wymcp.Bound.first/1 keeps are walked against the data
and given their problems. So the work past JSV is linear in the number of
errors JSV reports, and a call with a hundred thousand wrong-typed array
items costs what its ten listed paths do to read. The order reads an
all-digit token as an index, which it is under an array; under an object
an all-digit key is spelled as a key, but chosen among the ten as if it
were an index. JSV's own work — collecting and normalizing every error —
is linear in the data, which the body limit (Wymcp.Plugs.Pipeline)
bounds.
Summary
Functions
Builds one action's root from its properties. Raises
Wymcp.ConfigurationError on Wymcp.Tool.actions/0, naming the tool,
the action and what JSV stopped on: where in the schema it failed and why,
or, where JSV raises instead of answering, JSV's own message.
Builds the root of every action in actions — the map module's
Wymcp.Tool.actions/0 returned, already through action-schema
validation — and proves each action's :defaults against it. Returns the
roots keyed by action.
Checks data against an action's root. Returns {:ok, cast_data} —
data as JSV cast it, a whole-number float under "integer" an integer —
or {:error, invalid, count}: count is the number of data paths holding an
offending value, and invalid lists the first ten of them in sort order,
each as %{at: data_path, problems: problems}.
One line naming every entry invalid lists — <data path>: <problems>,
joined by ; — and how many more count holds beyond them, so the prose
never claims a complete list it is not.
Functions
Builds one action's root from its properties. Raises
Wymcp.ConfigurationError on Wymcp.Tool.actions/0, naming the tool,
the action and what JSV stopped on: where in the schema it failed and why,
or, where JSV raises instead of answering, JSV's own message.
Builds the root of every action in actions — the map module's
Wymcp.Tool.actions/0 returned, already through action-schema
validation — and proves each action's :defaults against it. Returns the
roots keyed by action.
Raises Wymcp.ConfigurationError on Wymcp.Tool.actions/0 for a schema
build_root!/3 refuses, or for a default its property schema refuses,
naming the tool, the action, and each offending default's data path with
its problems.
Checks data against an action's root. Returns {:ok, cast_data} —
data as JSV cast it, a whole-number float under "integer" an integer —
or {:error, invalid, count}: count is the number of data paths holding an
offending value, and invalid lists the first ten of them in sort order,
each as %{at: data_path, problems: problems}.
Examples
iex> properties = %{"limit" => %{"type" => "integer", "maximum" => 100}}
iex> root = Wymcp.Tool.ValueGate.build_root!(Tasks, :list, properties)
iex> Wymcp.Tool.ValueGate.check(%{"limit" => 50}, root)
{:ok, %{"limit" => 50}}
iex> Wymcp.Tool.ValueGate.check(%{"limit" => 50.0}, root)
{:ok, %{"limit" => 50}}
iex> Wymcp.Tool.ValueGate.check(%{"limit" => 500}, root)
{:error, [%{at: "limit", problems: ["value is higher than maximum 100"]}], 1}
One line naming every entry invalid lists — <data path>: <problems>,
joined by ; — and how many more count holds beyond them, so the prose
never claims a complete list it is not.
Examples
iex> invalid = [%{at: "limit", problems: ["value is higher than maximum 100"]}]
iex> Wymcp.Tool.ValueGate.summarize(invalid, 3)
"limit: value is higher than maximum 100 and 2 more"