Current section
Files
Jump to
Current section
Files
lib/wpl/validator/pass1.ex
defmodule WPL.Validator.Pass1 do
@moduledoc """
Pass 1: JSON Schema validation against the canonical WPL v1 schema.
Loads and resolves the schema at compile time. Uses `ex_json_schema` v0.11,
which supports Draft 4/6/7. The schema is Draft 2020-12, so we replace
the `$schema` declaration with the Draft 7 URI at load time — the structural
keywords used in the WPL schema (`$defs`, `oneOf`, `if/then/else`,
`additionalProperties`, `enum`, `required`) are all supported under Draft 7.
"""
alias ExJsonSchema.Validator.Error, as: SchemaError
alias WPL.Validator.Error
@schema_path Application.app_dir(:wpl_validator, "priv/schema/v1.schema.json")
@external_resource @schema_path
# Load and resolve the schema once at compile time.
# Replace the $schema declaration with the Draft 7 URI so ex_json_schema accepts it.
# The WPL v1 schema uses only Draft 7-compatible keywords.
@raw_schema @schema_path
|> File.read!()
|> Jason.decode!()
|> Map.put("$schema", "http://json-schema.org/draft-07/schema#")
@resolved_schema ExJsonSchema.Schema.resolve(@raw_schema)
@spec run(any()) :: [Error.t()]
def run(input) do
case ExJsonSchema.Validator.validate(@resolved_schema, input, error_formatter: false) do
:ok ->
[]
{:error, errors} ->
Enum.map(errors, &to_validation_error/1)
end
end
# ex_json_schema returns structured %ExJsonSchema.Validator.Error{error: sub_error, path: path}
# when error_formatter: false. The path is of the form "#/plan/type"; strip the leading "#"
# to produce a valid RFC 6901 JSON Pointer (e.g. "/plan/type") or "" for the root.
defp to_validation_error(%SchemaError{error: sub_error, path: path}) do
json_pointer = String.replace_prefix(path, "#", "")
%Error{
path: json_pointer,
code: :schema_violation,
message: to_string(sub_error),
severity: :error,
meta: %{keyword: keyword_for(sub_error)}
}
end
# Map structured error sub-types to JSON Schema keyword names, matching
# the TS validator's ajv `keyword` field for conformance parity.
defp keyword_for(%SchemaError.AdditionalItems{}), do: "additionalItems"
defp keyword_for(%SchemaError.AdditionalProperties{}), do: "additionalProperties"
defp keyword_for(%SchemaError.AllOf{}), do: "allOf"
defp keyword_for(%SchemaError.AnyOf{}), do: "anyOf"
defp keyword_for(%SchemaError.Const{}), do: "const"
defp keyword_for(%SchemaError.Contains{}), do: "contains"
defp keyword_for(%SchemaError.Dependencies{}), do: "dependencies"
defp keyword_for(%SchemaError.Enum{}), do: "enum"
defp keyword_for(%SchemaError.Format{}), do: "format"
defp keyword_for(%SchemaError.IfThenElse{}), do: "if"
defp keyword_for(%SchemaError.MaxItems{}), do: "maxItems"
defp keyword_for(%SchemaError.MaxLength{}), do: "maxLength"
defp keyword_for(%SchemaError.MaxProperties{}), do: "maxProperties"
defp keyword_for(%SchemaError.Maximum{}), do: "maximum"
defp keyword_for(%SchemaError.MinItems{}), do: "minItems"
defp keyword_for(%SchemaError.MinLength{}), do: "minLength"
defp keyword_for(%SchemaError.MinProperties{}), do: "minProperties"
defp keyword_for(%SchemaError.Minimum{}), do: "minimum"
defp keyword_for(%SchemaError.MultipleOf{}), do: "multipleOf"
defp keyword_for(%SchemaError.Not{}), do: "not"
defp keyword_for(%SchemaError.OneOf{}), do: "oneOf"
defp keyword_for(%SchemaError.Pattern{}), do: "pattern"
defp keyword_for(%SchemaError.PropertyNames{}), do: "propertyNames"
defp keyword_for(%SchemaError.Required{}), do: "required"
defp keyword_for(%SchemaError.Type{}), do: "type"
defp keyword_for(%SchemaError.UniqueItems{}), do: "uniqueItems"
defp keyword_for(_), do: nil
end