BB.Parameter.Type (bb v0.31.2)

Copy Markdown View Source

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

bound()

@type bound() :: number() | Localize.Unit.t() | nil

t()

@type t() :: :float | :integer | :boolean | :string | :atom | {:unit, atom()}

Functions

coerce(arg1, value)

@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)
:anything

An 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")

describe(type)

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

option_type(type, min, max)

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

validate(type)

@spec validate(term()) :: {:ok, t()} | {:error, String.t()}

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} where unit_type is 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"}

validate_bounds(value, bounds)

@spec validate_bounds(term(), keyword()) :: {:ok, number()} | {:error, String.t()}

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