# `Coelho.Schema.Attr`
[🔗](https://github.com/nseaSeb/coelho/blob/main/lib/coelho/schema/attr.ex#L1)

Specification and validation of a single node or mark attribute.

An attribute spec declares a default value, whether the attribute is
required, and an optional validator. Validators are declarative terms
rather than closures so that a schema stays inspectable and comparable.

Supported validators:

  * `:string`, `:integer`, `:boolean` — type checks
  * `{:one_of, list}` — value must be a member of `list`
  * `:safe_url` — relative URL, or absolute URL with an allowed scheme
  * `{:nullable, validator}` — accepts `nil`, otherwise delegates
  * a `fun/1` returning `:ok` or `{:error, message}` — escape hatch

## Turning a value into markup

`:render_as` says how the value reaches the DOM, and — being data rather
than a render function — it is applied by the server renderer *and*
exported to the browser, so the editor shows what the page will carry
without an application writing a hook.

  * `{:style, property}` — `style="property:value"`
  * `{:class, %{value => class}}` — the class the value maps to

Alignment is the shipped example:

    align: [
      default: nil,
      validate: {:nullable, {:one_of, ~w(left center right justify)}},
      render_as: {:style, "text-align"}
    ]

Swap that last line for a class map and the same attribute renders as a
class an application's stylesheet can own — an inline style cannot be
overridden by a rule:

    render_as: {:class, %{"center" => "text-center", "right" => "text-right"}}

Both forms are closed over the values they name, which is the point.
`Coelho.Ecto.Type` does not re-validate a stored document, so every
renderer is a security boundary: a row written under a looser schema is
rendered by today's renderer. A class map is its own allow list — a value
that is not a key contributes nothing. `{:style, property}` has no such
list of its own, so it is accepted only on an attribute whose validator is
a `{:one_of, list}` (nullable or not), and the value is checked against
that list again at render time.

An attribute with no `:render_as` is not rendered by itself; a node's
`:render` decides what to do with it, as before. A `:render` that is a
*function* builds the whole element, so it is the one form `:render_as`
cannot reach — the same rule a spec's `:class` follows.

# `render_as`

```elixir
@type render_as() ::
  {:style, String.t()} | {:class, %{optional(term()) =&gt; String.t()}} | nil
```

# `t`

```elixir
@type t() :: %Coelho.Schema.Attr{
  default: term(),
  render_as: render_as(),
  required: boolean(),
  validate: validator()
}
```

# `validator`

```elixir
@type validator() ::
  :string
  | :integer
  | :boolean
  | {:one_of, [term()]}
  | :safe_url
  | {:nullable, validator()}
  | (term() -&gt; :ok | {:error, String.t()})
  | nil
```

# `class_json_key`

```elixir
@spec class_json_key(term()) :: String.t()
```

The key a `{:class, map}` lookup uses for a value, as the browser sees it.

Exported alongside the classes so both halves agree on what a value is
called: JavaScript indexes an object by the string form of the key, and
`null` reaches it as `"null"`.

# `new`

```elixir
@spec new(keyword()) :: t()
```

Builds an attribute spec from a keyword list.

# `render_values`

```elixir
@spec render_values(t()) :: [term()] | nil
```

The values a `{:style, property}` attribute may render, or `nil`.

Read off the validator, which such an attribute is required to have, so
the list exists in one place and the browser can be handed the same one.

# `validate`

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

Runs a validator against a value.

---

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