# `BB.Parameter.Type`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/parameter/type.ex#L5)

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.

# `bound`

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

# `t`

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

# `coerce`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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\""}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
