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

check(data, properties)

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}

close_properties(properties)

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"}
}

default_offences(defaults, properties)

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" => true does 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}}]

schema_offences(properties)

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}}]