Wymcp. Tool. KeySet
(Wymcp v0.8.7)
View Source
Walks an action's :properties for the object schemas whose key set is
closed, in both directions: check/2 reads a call's data against them
for the unknown-key gate, and close_properties/1 writes the closure into
the schema Wymcp.Help and the input_schema digest publish. Which
objects are closed, and what a refusal carries, is the rule stated in
Wymcp.Tool, "Dispatch errors and self-correction"; this module is where
it is walked. Action-schema validation (Wymcp.Tool.Actions) asks the
same two walks about the author's own maps: schema_offences/1 for the
places the closure walk cannot read a schema, and default_offences/2 for
the keys of :defaults that the closure refuses.
Both directions live here because they must agree on reach. A keyword the
gate descends through but the renderer does not would publish as open an
object whose keys dispatch refuses, and the reverse would publish a
closure nothing enforces. They are two walks — check/2 walks data by its
schema, close_properties/1 walks the schema alone — each spelling the
same reach, kept side by side so a change to it is made in both, and held
to agreement by a test that compares them. default_offences/2 is the
data walk run over :defaults, and schema_offences/1 is the closure
walk itself, collecting what it could not read as it closes, so each
validation answer has the reach of the walk it rides. The walk descends
through "properties", into each declared property; through "items"
when it is a single schema, into each array element; and through an
"additionalProperties" schema, into each value under a key the declared
properties do not name. It follows nothing else — oneOf,
anyOf, allOf, $ref, patternProperties and prefixItems included —
so an object reached only through one of them stays open, and is
published without a closure.
Keywords and property names are strings, the JSON form help publishes
them in, and registration holds a schema to that wherever the closure walk
reads it: every map the walk visits — the :properties map, each schema in
it, and each "properties" map, "items" schema and
"additionalProperties" schema below — is checked, for the offences
schema_offences/1 lists. Nothing else in a schema is checked. A struct
inside a schema — a Date under "default", say — is a value, never a
subschema, so no walk goes into one.
check/2 reads keys and nothing else. It descends into a map only where
the schema declares an object and into a list only where it declares an
array; a value of any other shape reaches Wymcp.Tool.run_action/3 as
it came. It collects every unknown key in one pass. A key that is not a
string is refused in any object the walk reads, unless
"additionalProperties" => true admits it: a call's data never carries
one, since a JSON object key is a string, but a default can.
Locations are data paths, spelled as that
entry states — a plain identifier being a letter or underscore, then
letters, digits and underscores. They sort segment by segment, array
indexes as numbers, so items[2] comes before items[10].
schema_offences/1 and
default_offences/2 locate an offence in the author's map rather than in
data, so they return its location unrendered, for the caller to spell.
Summary
Functions
Checks data against the action's properties, at every depth.
properties is one schema_offences/1 finds nothing in.
Returns properties with "additionalProperties" => false written into
every object schema inside it whose key set is closed, at every depth
check/2 reaches. An object whose author wrote "additionalProperties"
keeps what was written. Nothing else in a schema changes. properties is
one schema_offences/1 finds nothing in, so the closure published is the
one check/2 enforces.
Checks an action's defaults against its properties as check/2 checks
a call's data, and returns every offence, sorted — [] when there is
none. properties is one schema_offences/1 finds nothing in.
Returns every place in properties that the closure walk cannot read as
it closes, sorted — [] when there is none. It is close_properties/1's
own walk, so it checks exactly the maps a closure is written into.
Functions
Checks data against the action's properties, at every depth.
properties is one schema_offences/1 finds nothing in.
Returns :ok, or {:error, unknown, allowed, count}: count is the
number of keys no closed object declares, unknown is the data paths of
the first ten of them in sort order, and allowed maps the data path of
each object holding one of those to that object's property names,
sorted — data itself under "". An object whose additionalProperties
schema admits any string key refuses only a key that is not a string, and
lists "<any string key>" among its names.
The answer is bounded however many keys the caller sent, under the rule
Wymcp.Bound states in "What a refusal quotes": at most ten data paths,
each key in one quoted through Wymcp.Bound.echo/1, which cuts a key
longer than the bound there and marks it with …. A cut key is quoted as
the whole key would be. The work is bounded the same way: the listed
offences are chosen by Wymcp.Bound.first/1, without sorting the rest,
and only their objects' names are read.
Examples
iex> properties = %{
...> "items" => %{
...> "type" => "array",
...> "items" => %{"type" => "object", "properties" => %{"status" => %{}}}
...> }
...> }
iex> Wymcp.Tool.KeySet.check(%{"items" => [%{"status" => "done"}]}, properties)
:ok
iex> Wymcp.Tool.KeySet.check(%{"items" => [%{"stauts" => "done"}], "limt" => 5}, properties)
{:error, ["items[0].stauts", "limt"], %{"" => ["items"], "items[0]" => ["status"]}, 2}
Returns properties with "additionalProperties" => false written into
every object schema inside it whose key set is closed, at every depth
check/2 reaches. An object whose author wrote "additionalProperties"
keeps what was written. Nothing else in a schema changes. properties is
one schema_offences/1 finds nothing in, so the closure published is the
one check/2 enforces.
Examples
iex> Wymcp.Tool.KeySet.close_properties(%{
...> "items" => %{"type" => "array", "items" => %{"properties" => %{"id" => %{}}}},
...> "metadata" => %{"type" => "object"}
...> })
%{
"items" => %{
"type" => "array",
"items" => %{"properties" => %{"id" => %{}}, "additionalProperties" => false}
},
"metadata" => %{"type" => "object"}
}
Checks an action's defaults against its properties as check/2 checks
a call's data, and returns every offence, sorted — [] when there is
none. properties is one schema_offences/1 finds nothing in.
Each offence is {location, offence}. location lists the keys, and the
array indexes, from defaults down to the object holding the key; an
index is an integer, and never a key, because the walk descends only
through string keys. offence is one of:
{:key, key}— a key that is not a string, in an object the walk reads and"additionalProperties" => truedoes not open;{:undeclared, key, declared}— a key the object's closure refuses, with the object's property names, sorted.
Examples
iex> properties = %{"opts" => %{"type" => "object", "properties" => %{"limit" => %{}}}}
iex> Wymcp.Tool.KeySet.default_offences(%{"opts" => %{"limit" => 5}}, properties)
[]
iex> Wymcp.Tool.KeySet.default_offences(%{"opts" => %{"limit" => 5, "stray" => true}}, properties)
[{["opts"], {:undeclared, "stray", ["limit"]}}]
iex> Wymcp.Tool.KeySet.default_offences(%{"opts" => %{limit: 5}}, properties)
[{["opts"], {:key, :limit}}]
Returns every place in properties that the closure walk cannot read as
it closes, sorted — [] when there is none. It is close_properties/1's
own walk, so it checks exactly the maps a closure is written into.
Each offence is {location, offence}. location lists the keys from
properties down to the map holding the offence — [] for properties
itself. offence is one of:
{:key, key}— a key that is not a string;{:additional_properties, value}— an"additionalProperties"that is neither a boolean nor a schema;{:properties, value}— a"properties"that is not a map.
Examples
iex> Wymcp.Tool.KeySet.schema_offences(%{"o" => %{"properties" => %{"id" => %{}}}})
[]
iex> Wymcp.Tool.KeySet.schema_offences(%{
...> "o" => %{"properties" => %{"id" => %{}}, additionalProperties: true},
...> "m" => %{"additionalProperties" => nil}
...> })
[{["m"], {:additional_properties, nil}}, {["o"], {:key, :additionalProperties}}]