Liver

Build Status Coverage Status

Summary

Liver is a lightweight Erlang/OTP data validator inspired by LIVR. It follows the same ideas: declarative rules per field, all errors at once, stable error codes, and easy custom rules.

Liver ships two rule sets:

Both are first-class. The default changed in 1.0.0 because most Erlang code works with typed terms, not JSON strings; LIVR remains fully available via #{rule_set => livr_spec} (or livr_compatible => true).

Docs
Standard rules Reference for erlang_standard
LIVR rules Reference for livr_spec
Comparing the sets Coercion, naming, when to use which
OpenAPI Export / import Schema Objects (MVP)
Changelog Releases and breaking changes

Table of Contents

Description

From LIVR (design shared by Liver):

  1. Declarative rules, many per field
  2. All field errors returned together
  3. Fields without rules are excluded from the output
  4. Nested structures supported
  5. Stable error codes (not free-form messages)
  6. Easy to add project-specific rules
  7. Rules may transform values (trim, nested validators, converters, …)
  8. Unicode-aware where it matters

Liver extras:

  1. Two built-in rule maps — LIVR-spec and Erlang-oriented — selectable / composable
  2. Maps and proplists as input/output (return => map | proplist | as_is)
  3. strict option — reject fields not described in the schema
  4. List as root value
  5. add_rule/2, add_rule_set/2, custom error messages

Why two rule sets?

LIVR grew up around JSON and HTML forms: numbers and booleans often arrive as binaries (<<"10">>, <<"true">>). Spec rules such as integer therefore parse and coerce as part of validation.

OTP applications more often already have Erlang types (10, true, atoms). Silent coercion there hides bugs. The default erlang_standard set uses predicates (is_integer, …) and explicit to_* converters when you want parsing.

See Comparing the sets for a fuller explanation.

Getting Started

  1. Add as a dependency:
{deps, [{liver, "1.1.0"}]}.
DEPS = liver
dep_liver = hex 1.1.0
{deps, [
{liver, {git, "https://github.com/erlangbureau/liver.git", {tag, "1.1.0"}}}
]}.
DEPS = liver
dep_liver = git https://github.com/erlangbureau/liver.git 1.1.0
  1. Add liver to applications in your .app.src.

  2. Validate data, or register your own rules with liver:add_rule/2.

Upgrading from 0.9.x: Liver was LIVR-oriented from the first version (2017-11-21). In 1.0.0 the default rule set became erlang_standard. Existing LIVR schemas keep working with #{rule_set => livr_spec} or #{livr_compatible => true}. Details: CHANGELOG.md.

Upgrading to 1.1.0: erlang_standard errors are lowercase atoms (not_integer) instead of binaries (<<"NOT_INTEGER">>). livr_spec is unchanged. Details: CHANGELOG.md.

Usage Examples

Erlang-oriented rules (default)

1> Schema = #{
name => [required, is_utf8_binary],
age => [required, is_pos_integer],
role => [{one_of_terms, [[admin, user]]}]
}.
2> liver:validate(Schema, #{name => <<"Ann">>, age => 30, role => admin}).
{ok,#{age => 30,name => <<"Ann">>,role => admin}}
3> %% Binary is not an integer — no silent parse
3> liver:validate(#{n => is_integer}, #{n => <<"10">>}).
{error,#{n => not_integer}}
4> %% Parse explicitly, then check
4> liver:validate(#{n => [to_integer, is_pos_integer]}, #{n => <<"10">>}).
{ok,#{n => 10}}

Nested map

5> Schema = #{
address => [required, {nested_map, #{
country => [required, is_utf8_binary],
zip => is_pos_integer
}}]
}.
6> liver:validate(Schema, #{
address => #{country => <<"UA">>, zip => 12345, extra => ignored}
}).
{ok,#{address => #{country => <<"UA">>,zip => 12345}}}

Unknown fields (strict)

7> liver:validate(#{a => required}, #{a => 1, b => 2}, #{strict => true}).
{error,#{b => <<"UNKNOWN_FIELD">>}}

LIVR rules (same validator, LIVR rule map)

8> Schema = #{
<<"zip">> => [required, positive_integer],
<<"street">> => [required, string]
}.
9> liver:validate(Schema, #{
<<"zip">> => <<"12345">>,
<<"street">> => <<"Main">>
}, #{rule_set => livr_spec}).
{ok,#{<<"street">> => <<"Main">>,<<"zip">> => 12345}}

positive_integer here accepts <<"12345">> because that is how LIVR is specified for JSON-style input.

Rule sets

rule_set Behaviour
erlang_standard (default) liver_standard_rules
livr_spec liver_livr_rules (LIVR 2.0 names)
[Set1, Set2, …] Compose; first wins on the same rule name
#{Rule => Module} Inline custom rule map
{mixed, erlang_standard} Alias for [erlang_standard, livr_spec]
{mixed, livr_spec} Alias for [livr_spec, erlang_standard]
liver:add_rule_set(my_app, #{slug => my_app_rules}).
liver:validate(Schema, Data,
#{rule_set => [my_app, erlang_standard, livr_spec]}).

#{livr_compatible => true} is an alias for #{rule_set => livr_spec}.

References: standard rules, LIVR rules, comparison.

OpenAPI

MVP helpers in liver_openapi_schema:

See doc/openapi.md.

Exports

validate/2

validate(Schema, Input) -> {ok, Output} | {error, Errors}
Schema, Input, Output, Errors = map() | proplist()

Equivalent to validate(Schema, Input, #{}).

validate/3

validate(Schema, Input, Opts) -> {ok, Output} | {error, Errors}
Opts = map() | proplist()
Option Default Description
return as_is as_is | map | proplist
strict false Reject fields not in schema
rule_set erlang_standard See Rule sets
livr_compatible false Alias for rule_set => livr_spec

which/1, which/2

which(Rule) -> module() | undefined_module
which(Rule, Opts) -> module() | undefined_module

add_rule/2

add_rule(Rule, Module) -> ok

Register a custom rule into the erlang_standard application rule map (also used when that set appears in a rule_set list).

add_rule_set/2

add_rule_set(Name, Rules) -> ok
Name = atom()
Rules = #{atom() => module()}

custom_error/2

custom_error(ErrorCode, ErrorMessage) -> ok

Override a built-in error code. From 1.1.0, default erlang_standard errors are lowercase atoms (not_integer); livr_spec uses LIVR binaries (<<"NOT_INTEGER">>).

License

Liver is released under the MIT License