# `BB.Unit.Option`
[🔗](https://github.com/beam-bots/bb/blob/main/lib/bb/unit/option.ex#L5)

Functions for specifying and validating units in option schemas.

# `schema_option`

```elixir
@type schema_option() ::
  {:compatible, atom() | String.t()}
  | {:min, Localize.Unit.t()}
  | {:max, Localize.Unit.t()}
  | {:eq, Localize.Unit.t()}
```

# `schema_options`

```elixir
@type schema_options() :: [schema_option()]
```

# `unit_type`

```elixir
@spec unit_type(Keyword.t()) :: {:custom, BB.Unit.Option, :validate, [Keyword.t()]}
```

Create a Spark.Options schema type for a unit.

## Examples

Basic usage returns a custom schema type tuple:

    iex> BB.Unit.Option.unit_type()
    {:custom, BB.Unit.Option, :validate, [[]]}

With compatible option to restrict unit category:

    iex> BB.Unit.Option.unit_type(compatible: :meter)
    {:custom, BB.Unit.Option, :validate, [[compatible: :meter]]}

With min constraint:

    iex> BB.Unit.Option.unit_type(min: Localize.Unit.new!(0, "meter"))
    {:custom, BB.Unit.Option, :validate, [[min: Localize.Unit.new!(0, "meter")]]}

With max constraint:

    iex> BB.Unit.Option.unit_type(max: Localize.Unit.new!(100, "meter"))
    {:custom, BB.Unit.Option, :validate, [[max: Localize.Unit.new!(100, "meter")]]}

Combined constraints:

    iex> BB.Unit.Option.unit_type(compatible: :meter, min: Localize.Unit.new!(0, "meter"), max: Localize.Unit.new!(100, "meter"))
    {:custom, BB.Unit.Option, :validate, [[compatible: :meter, min: Localize.Unit.new!(0, "meter"), max: Localize.Unit.new!(100, "meter")]]}

# `validate`

```elixir
@spec validate(any(), Keyword.t()) ::
  {:ok, Localize.Unit.t()} | {:ok, BB.Dsl.ParamRef.t()} | {:error, String.t()}
```

Validate a value against a category.

## Examples

Valid unit passes through:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(5, "meter"))
    {:ok, Localize.Unit.new!(5, "meter")}

Non-unit values are rejected:

    iex> BB.Unit.Option.validate("not a unit")
    {:error, "Value `\"not a unit\"` is not a `Localize.Unit` struct"}

Compatible unit check passes for same category:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(100, "centimeter"), compatible: :meter)
    {:ok, Localize.Unit.new!(100, "centimeter")}

Incompatible units are rejected:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(90, "degree"), compatible: :meter)
    {:error, "The unit `degree` is not compatible with `meter`"}

Min constraint - value must be >= min:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(5, "meter"), min: Localize.Unit.new!(1, "meter"))
    {:ok, Localize.Unit.new!(5, "meter")}

    iex> BB.Unit.Option.validate(Localize.Unit.new!(-45, "degree"), min: Localize.Unit.new!(-30, "degree"))
    {:error, "Expected -45 degree to be greater than or equal to -30 degree"}

Max constraint - value must be <= max:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(5, "meter"), max: Localize.Unit.new!(10, "meter"))
    {:ok, Localize.Unit.new!(5, "meter")}

    iex> BB.Unit.Option.validate(Localize.Unit.new!(45, "degree"), max: Localize.Unit.new!(30, "degree"))
    {:error, "Expected 45 degree to be less than or equal to 30 degree"}

Eq constraint - value must equal exactly:

    iex> BB.Unit.Option.validate(Localize.Unit.new!(5, "meter"), eq: Localize.Unit.new!(5, "meter"))
    {:ok, Localize.Unit.new!(5, "meter")}

    iex> BB.Unit.Option.validate(Localize.Unit.new!(5, "meter"), eq: Localize.Unit.new!(6, "meter"))
    {:error, "Expected 5 meter to equal 6 meter"}

ParamRef values are accepted and annotated with expected unit type:

    iex> ref = BB.Dsl.ParamRef.param([:motion, :max_speed])
    iex> {:ok, validated} = BB.Unit.Option.validate(ref, compatible: :meter)
    iex> validated.expected_unit_type
    :meter

---

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