Validation for parameter type definitions in the DSL.
Parameters can have simple types (:float, :integer, etc.) or unit types
like {:unit, :meter}.
Numeric parameters - :float, :integer and unit types - can also declare
min/max bounds. option_type/3 folds those bounds into the
Spark.Options type generated for the parameter so that they are enforced
wherever a value is validated, and describe/1 recovers them from a
generated type for display.
A unit-typed parameter accepts any value compatible with its declared unit,
so coerce/2 converts a value into that unit before it is stored. Without it
a parameter reports back whichever unit it happened to be written in.
Summary
Functions
Converts a value into the unit its parameter type declares.
Recovers the declared parameter type and its bounds from a generated
Spark.Options type.
Builds the Spark.Options type for a parameter of type, bounded by min
and max.
Validates a parameter type specification.
Validates a value against a bounded numeric parameter's type and bounds.
Types
@type bound() :: number() | Localize.Unit.t() | nil
@type t() :: :float | :integer | :boolean | :string | :atom | {:unit, atom()}
Functions
@spec coerce(t() | Spark.Options.type() | nil, term()) :: term()
Converts a value into the unit its parameter type declares.
A unit-typed parameter accepts any value compatible with its declared unit, so the value that reaches a write is not necessarily in the unit the reader expects. Every parameter write converges on the declared unit by passing the value through here.
Values of any other type are returned unchanged, as is a unit which cannot be converted - a bound already rejects an incompatible unit wherever the value was validated, and a write which skipped validation is left as it was rather than reported wrong.
Examples
iex> BB.Parameter.Type.coerce({:unit, :degree}, Localize.Unit.new!(1, "radian"))
Localize.Unit.new!(57.29577951308232, "degree")A value already in the declared unit is untouched:
iex> BB.Parameter.Type.coerce({:unit, :degree}, Localize.Unit.new!(30, "degree"))
Localize.Unit.new!(30, "degree")So is anything which is not a unit:
iex> BB.Parameter.Type.coerce(:float, 1.5)
1.5
iex> BB.Parameter.Type.coerce(nil, :anything)
:anythingAn incompatible unit is left alone for the caller's validation to reject:
iex> BB.Parameter.Type.coerce({:unit, :meter}, Localize.Unit.new!(90, "degree"))
Localize.Unit.new!(90, "degree")
@spec describe(Spark.Options.type() | nil) :: {t() | Spark.Options.type() | nil, bound(), bound()}
Recovers the declared parameter type and its bounds from a generated
Spark.Options type.
Types which don't carry bounds - including the hand-written schemas of
components which use BB.Parameter - are returned unchanged with nil
bounds.
Examples
iex> BB.Parameter.Type.describe(:float)
{:float, nil, nil}
iex> {:ok, type} = BB.Parameter.Type.option_type(:integer, 0, 127)
iex> BB.Parameter.Type.describe(type)
{:integer, 0, 127}
iex> {:ok, type} = BB.Parameter.Type.option_type({:unit, :meter}, nil, Localize.Unit.new!(1, "meter"))
iex> BB.Parameter.Type.describe(type)
{{:unit, :meter}, nil, Localize.Unit.new!(1, "meter")}
@spec option_type(t(), bound(), bound()) :: {:ok, Spark.Options.type()} | {:error, String.t()}
Builds the Spark.Options type for a parameter of type, bounded by min
and max.
Either bound may be nil. Numeric bounds are plain numbers, bounds on a unit
type are Localize.Unit values compatible with the parameter's unit.
Examples
An unbounded type is used as-is:
iex> BB.Parameter.Type.option_type(:float, nil, nil)
{:ok, :float}Bounds wrap the type in a custom validator:
iex> BB.Parameter.Type.option_type(:integer, 0, 127)
{:ok, {:custom, BB.Parameter.Type, :validate_bounds, [[type: :integer, min: 0, max: 127]]}}Only one of the two is needed:
iex> BB.Parameter.Type.option_type(:float, 0.0, nil)
{:ok, {:custom, BB.Parameter.Type, :validate_bounds, [[type: :float, min: 0.0, max: nil]]}}Bounds on a unit type become unit constraints:
iex> BB.Parameter.Type.option_type({:unit, :meter}, nil, Localize.Unit.new!(1, "meter"))
{:ok, {:custom, BB.Unit.Option, :validate, [[compatible: :meter, max: Localize.Unit.new!(1, "meter")]]}}Non-numeric types cannot be bounded:
iex> BB.Parameter.Type.option_type(:string, 0, nil)
{:error, "`min` and `max` are only supported for numeric parameter types (:float, :integer or {:unit, unit_type}), got: :string"}
Validates a parameter type specification.
Returns {:ok, type} for valid types or {:error, message} for invalid ones.
Valid Types
- Simple types:
:float,:integer,:boolean,:string,:atom - Unit types:
{:unit, unit_type}whereunit_typeis a valid CLDR unit
Examples
iex> BB.Parameter.Type.validate(:float)
{:ok, :float}
iex> BB.Parameter.Type.validate({:unit, :meter})
{:ok, {:unit, :meter}}
iex> BB.Parameter.Type.validate(:invalid)
{:error, "Expected one of [:float, :integer, :boolean, :string, :atom] or {:unit, unit_type}, got: :invalid"}
Validates a value against a bounded numeric parameter's type and bounds.
This is the validator used by the type option_type/3 builds for a bounded
:float or :integer parameter.
Examples
iex> BB.Parameter.Type.validate_bounds(0.5, type: :float, min: 0.0, max: 1.0)
{:ok, 0.5}
iex> BB.Parameter.Type.validate_bounds(2.0, type: :float, min: 0.0, max: 1.0)
{:error, "expected value to be at most 1.0, got: 2.0"}
iex> BB.Parameter.Type.validate_bounds(-1.0, type: :float, min: 0.0, max: nil)
{:error, "expected value to be at least 0.0, got: -1.0"}
iex> BB.Parameter.Type.validate_bounds("nope", type: :float, min: 0.0, max: 1.0)
{:error, "expected float, got: \"nope\""}