Packages
nldoc_validation
2.3.1
4.0.42
4.0.41
4.0.40
4.0.39
4.0.38
4.0.37
4.0.36
4.0.35
4.0.34
4.0.33
4.0.32
4.0.31
4.0.30
4.0.29
4.0.28
4.0.27
4.0.26
4.0.25
4.0.24
4.0.23
4.0.22
4.0.21
4.0.20
4.0.19
4.0.18
4.0.17
4.0.16
4.0.15
4.0.14
4.0.12
4.0.11
4.0.10
4.0.9
4.0.8
4.0.7
4.0.6
4.0.5
4.0.4
4.0.3
4.0.2
4.0.1
4.0.0
3.0.0
2.3.17
2.3.16
2.3.15
2.3.14
2.3.13
2.3.12
2.3.11
2.3.10
2.3.9
2.3.8
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.0
2.1.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
1.0.0
WCAG validator for the NLdoc specification.
Current section
Files
Jump to
Current section
Files
lib/nldoc/validation/rule.ex
defmodule NLdoc.Validation.Rule do
@moduledoc """
This module provides a macro for defining validation rules for NLdoc spec objects, i.e. documents and their elements.
Such rules check for semantic correctness and accessibility concerns in the spec objects.
Simply `use NLdoc.Validation.Rule` in your module with all details about the validation rule and implement the `valid?/1`
function to determine whether the spec objects that this rule validates, are valid.
You may also choose to override the `validates/0` and `make_validation/1` functions for more granular control over the
validation process.
If your validation rule requires state to be maintained during the validation process, implement an `update_state/2` function
to update the `NLdoc.Validation.State` struct with the necessary information.
Note that the application of and merging of autofixes to a document as a result of validation is currently not implemented.
Example:
defmodule NLdoc.Validation.Rules.HeadingLevelValid do
use NLdoc.Validation.Rule,
severity: NLdoc.Validation.Severity.error(),
rule: "invalid-heading-level",
ruleset: "https://html.spec.whatwg.org/",
ruleset_version: "5",
validates: [NLdoc.Spec.Heading]
@impl true
def valid?(%NLdoc.Spec.Heading{level: level}, _),
do: level >= 1 and level <= 6
end
"""
alias NLdoc.Spec.Validation
alias NLdoc.Validation.Severity
@callback make_validation(resource_id :: String.t()) :: Validation.t()
@callback validates() :: [module()]
@callback valid?(NLdoc.Spec.object(), NLdoc.Validation.State.t()) :: boolean()
@callback validate(NLdoc.Spec.object(), NLdoc.Validation.State.t()) ::
NLdoc.Validation.State.t()
@callback update_state(NLdoc.Validation.State.t(), NLdoc.Spec.object()) ::
NLdoc.Validation.State.t()
@type t :: module()
@type opt() ::
{:severity, Severity.t()}
| {:rule, String.t()}
| {:ruleset, String.t()}
| {:ruleset_version, String.t()}
| {:validates, [module()]}
@spec __using__([opt()]) :: Macro.t()
defmacro __using__(opts) do
{severity, _} = opts |> Keyword.get(:severity) |> Code.eval_quoted([], __CALLER__)
rule = opts |> Keyword.get(:rule)
ruleset = opts |> Keyword.get(:ruleset)
ruleset_version = opts |> Keyword.get(:ruleset_version)
{validates, _} = opts |> Keyword.get(:validates) |> Code.eval_quoted([], __CALLER__)
if severity not in Severity.values() do
raise """
Property :severity is required on `use NLdoc.Validation.Rule` and must be one of #{inspect(Severity.values())}, but got #{inspect(severity)}.
"""
end
if not is_binary(rule) do
raise """
Property :rule is required on `use NLdoc.Validation.Rule` and must be a string, but got #{inspect(rule)}.
"""
end
if not is_binary(ruleset) do
raise """
Property :ruleset is required on `use NLdoc.Validation.Rule` and must be a string, but got #{inspect(ruleset)}.
"""
end
if not is_binary(ruleset_version) do
raise """
Property :ruleset_version is required on `use NLdoc.Validation.Rule` and must be a string, but got #{inspect(ruleset_version)}.
"""
end
if not is_list(validates) or validates == [] or
not Enum.all?(validates, &NLdoc.Spec.Schema.schema_module?/1) do
message = """
Property :validates is required on `use NLdoc.Validation.Rule` and must be a non-empty list of NLdoc.Spec.* modules,
but got #{inspect(validates)}.
"""
message =
if not is_list(validates) or validates == [] do
"""
#{message}
The :validates property determines which NLdoc spec objects this rule validates.
If that is not defined or an empty list, then this rule validates nothing.
"""
else
"""
#{message}
Validation rules are only ever applied to NLdoc spec objects, so in order for a rule to actually match those,
:validates must contain valid NLdoc.Spec.* modules.
The following entries are not valid NLdoc.Spec.* modules:
- #{validates |> Enum.reject(&NLdoc.Spec.Schema.schema_module?/1) |> Enum.map_join("\n- ", &inspect/1)}
"""
end
raise message
end
quote do
alias NLdoc.Validation.State
@behaviour NLdoc.Validation.Rule
@impl true
def make_validation(resource_id) when is_binary(resource_id) do
%NLdoc.Spec.Validation{
type: NLdoc.Spec.Type.Validation.value(),
resource_id: resource_id,
severity: unquote(severity),
rule: unquote(rule),
ruleset: unquote(ruleset),
ruleset_version: unquote(ruleset_version)
}
end
@impl NLdoc.Validation.Rule
def validates,
do: unquote(validates)
@impl NLdoc.Validation.Rule
def validate(resource, state = %State{}) do
if valid?(resource, state) do
state
|> update_state(resource)
else
state
|> State.prepend(:findings, make_validation(resource.id))
|> update_state(resource)
end
end
# Note: `valid?` and `update_state` are defined here solely for `defoverridable`.
# Their default implementations are defined in the `__before_compile__` macro.
@impl NLdoc.Validation.Rule
def valid?(_resource, _state = %State{}),
do: true
@impl NLdoc.Validation.Rule
def update_state(state = %State{}, resource),
do: state
defoverridable validates: 0, valid?: 2, validate: 2, update_state: 2
@before_compile NLdoc.Validation.Rule
end
end
defmacro __before_compile__(_env) do
quote do
alias NLdoc.Validation.State
@impl NLdoc.Validation.Rule
def valid?(_, _state = %State{}),
do: true
@impl NLdoc.Validation.Rule
def update_state(state = %State{}, _),
do: state
end
end
end