Repository navigation
Conversation
Re-imported with scripts/livr_import.erl (make livr-spec-import) from koorchik/LIVR@4384f09e; only the generated files under tests/cases/livr change. New cases: equal_to_field on a numeric string vs a number, emails with a newline, and 'or' with required in every branch. They fail until the livr_spec fixes that follow.
validate_term/3 matched the result of validate_map/3 as a map, but with return => proplist it is a proplist. So nested_list, list_of, 'or' and a list as the root schema returned format_error (the case_clause was caught by the rule runner) or crashed when called from validate/3. Read the field with liver_maps:get/2, which handles both forms.
- equal_to_field: LIVR 2.0 compares the two values as strings, so
<<"10">> is equal to 10 (positive/17-equal_to_field).
- email: the pattern had "\s" in an Erlang string, which is a plain
space, so tabs and newlines passed, and $ matched before a trailing
newline. Use the regex class \s, dollar_endonly, and the quoted local
part of the LIVR reference pattern (no @ or newline inside the
quotes); the dotted local part now excludes a backslash as well, as
in the reference pattern (negative/16-email).
- 'or': a missing field only ran required, not_empty_list and default,
so {'or': [[required, ...], [required, ...]]} passed without a value
(negative/29-or). 'or' now runs on a missing field too, and
validate_term/3 validates each branch as an absent field: only its
required-like rules run, and a branch that passes leaves the field
out.
liver:add_aliased_rule/1 registers a rule made of other rules, given as a map with name, rules and error (atom or binary keys, so an alias decoded from LIVR JSON works as is). The aliased_rules option of validate/3 takes the same aliases for one call and wins over registered ones. The alias rules validate the value like a field of their own, with the rule set of the call (livr_spec or erlang_standard), and their output becomes the field value. If the alias has an error, it replaces their errors. Aliases work at the top level, inside nested rules (nested_object, list_of, 'or', nested_map, ...) and other aliases, and run on a missing field, so an alias with required reports a missing value. Registered aliases are kept in a persistent_term. The aliases of a call are resolved once, when its options are normalized, so rules cost one map match more only when there are aliases; without any, the options and the rule path stay as before.
livr_import also imports aliases_positive and aliases_negative; the
aliases of a case go to opts => #{aliased_rules => [...]}, so
livr_rules_SUITE runs them with the existing case runner.
livr_spec_check compares those suites with upstream too. The files
under tests/cases/livr are the importer's output for koorchik/LIVR@4384f09e.
- The erlang.mk build (ci.erlang.mk) covers OTP 25 and later, but nothing builds liver with rebar3, the way most users depend on it (rebar.config.script pulls jsx below OTP 27). Run rebar3 ct on OTP 25 with rebar3 3.24.0, the last release for OTP 25, and on OTP 29 with rebar3 3.28.0. - actions/checkout v4 runs on Node 20, which GitHub Actions has deprecated; v7 runs on Node 24.
LIVR patterns follow JavaScript, where $ matches only at the end of the
value. PCRE also matches it before a trailing newline, so
{like, <<"^[0-9]{5}$">>} accepted <<"12345\n">>. The like rule now
compiles patterns with dollar_endonly.
|
I've added one more commit to this PR: 3a8c5f0 LIVR patterns follow JavaScript, where Tested in Docker: |
Hi @erlangbureau!
Thank you for liver! It's one of the two Erlang implementations listed on livr-spec.org, and its
livr_specrule set together with the JSON Schema and OpenAPI support makes it a really useful library.The LIVR test suite was extended recently, and liver 1.1.0 fails 9 of its 70 cases (see #10):
equal_to_field,email,or, and the six aliased-rule cases, because liver had no way to register aliased rules. This pull request fixes them, so liver passes the full LIVR 2.0 test suite (70/70). It adds a small API for aliased rules, fixes areturn => proplistbug found on the way, and adds arebar3job to CI.About this pull request: it was prepared with the help of an LLM (Claude Code) and then reviewed and tested: the LIVR conformance harness, the repository's tests on OTP 25 and 29 in Docker (with both
makeandrebar3), dialyzer, and a speed comparison withmaster(schemas without aliases run as fast as before). Still, please look through it before merging. You know the code and its users best, and I'm happy to change anything, rename the new API, split the PR, or drop parts you don't want. The commits are grouped by topic, so it may be easiest to review commit by commit.What would help: if it looks good to you, merge it and publish a new release to hex (1.2.0, as the aliases API is new). The LIVR conformance run installs liver from hex, so once the release is out I'll update the LIVR harness to pass aliases through the new
aliased_rulesoption, and #10 will close by itself after the next run.What's changed
LIVR test suite
tests/cases/livrre-imported withscripts/livr_import.erlfrom koorchik/LIVR@4384f09e (LIVR 2.0). Only generated files change; running the importer on that commit reproduces them byte for byte.aliases_positive/aliases_negativesuites (aliases go toopts => #{aliased_rules => [...]}), andlivr_rules_SUITEruns them.livr_spec_checkcompares these suites with upstream too.livr_specfixes for LIVR 2.0equal_to_fieldcompares the two values as strings, so<<"10">>equals10.email: the pattern had"\s"in an Erlang string, which is a plain space, so tabs and newlines (also a trailing newline) passed. It now rejects whitespace outside quotes and@inside a quoted local part, like the LIVR reference pattern.'or'also runs on a missing field.{'or' => [[required, ...], [required, ...]]}returnedokfor a missing field and now returnsREQUIRED. A branch withoutrequiredthat passes still leaves the field out.Aliased rules (new)
liver:add_aliased_rule/1registers a LIVR aliased rule as#{name, rules, error}. Keys can be atoms or binaries, so an alias decoded from JSON works as is. Thealiased_rulesoption ofvalidate/3takes a list of the same terms for a single call, and these win over registered ones.livr_specorerlang_standard), and their output becomes the field value. Iferroris set, it replaces their errors. Aliases work at the top level, inside nested rules and inside other aliases, and they run on a missing field. Documented in the README anddoc/livr_rules.md.persistent_term(register them at start-up). The aliases of a call are resolved once, when its options are normalized. Without any aliases, the options and the rule path stay as before, so schemas that don't use aliases run at the same speed.Other fix
return => proplistcombined withnested_list,list_of,'or'or a list as the root schema returnedformat_error, or crashed with acase_clausewhen the list was the root schema.validate_term/3now reads the result withliver_maps:get/2.CI
rebar3job:rebar3 ct --dir testson OTP 25 with rebar3 3.24.0 (the last rebar3 release for OTP 25) and on OTP 29 with rebar3 3.28.0.actions/checkoutv4 → v7 in the coverage job (v4 runs on the deprecated Node 20).Behaviour changes
livr_specequal_to_field:<<"10">>and10now count as equal. Floats are formatted the Erlang way, so10.0still does not equal<<"10">>.livr_specemail: addresses with tabs, newlines, a backslash in the dotted local part, or@inside quotes are now rejected.'or'on a missing field now runs its branches, so it can returnREQUIREDor apply adefaultfrom a branch. Adefaultplaced before'or'is now validated by the'or'branches. Witherlang_standard, which has no'or'rule, a schema that uses'or'now returns{unimplemented_rule, 'or'}for a missing field too, as it already did for a present one. A custom rule registered under the name'or'now also runs on missing fields.email) replaces that rule for every call in the VM, asadd_rule/2does.How it was tested
Docker
erlang:25anderlang:29:make testspasses: aliased_rules 10/10, erlang_standard 9/9, json_schema 11/11, liver_internals 8/8, livr_rules 140/140, openapi_schema 13/13.rebar3 ct --dir tests: 191 tests pass onerlang:25with rebar3 3.24.0 and onerlang:29with rebar3 3.28.0.make dialyzeonerlang:29passes.actionlint(rhysd/actionlint:latest): no findings.livr_import:run/3on koorchik/LIVR@4384f09e (erlang:27): its output is identical totests/cases/livr, both at the sync commit (64 cases) and at the aliases-import commit (70 cases).LIVR conformance harness (all 70 cases of the LIVR 2.0 suite, aliases passed through the
aliased_rulesoption): 70/70.Speed (
erlang:27, ms per 100kliver:validate/3calls, best of 15 runs, median of 6 interleaved runs):livr_spec, 5 fields, no aliaseserlang_standard, 3 fields, no aliaseslivr_spec, same schema, one field uses an alias from thealiased_rulesoptionlivr_spec,aliased_rulesoption given but no field uses itThe aliases commit costs nothing measurable without aliases. The ~5% on the
livr_specschema comes from the stricteremailpattern in the LIVR 2.0 fixes; the schema has anemailfield.Each commit was built and tested on its own. At the first two commits (the suite sync and the
return => proplistfix), 6 livr_rules cases fail (equal_to_field, email, or, each in the maps and proplists forms). Thelivr_specfix commit makes them pass.Notes for the maintainer
Unreleasedsection lists the changes. The new API (add_aliased_rule/1,aliased_rules) is a minor-version feature (1.2.0). Nothing breaks forerlang_standardusers.add_aliased_rule/1takes the LIVR alias term (likeregisterAliasedRulein the JS and Perl validators), not positional arguments likeadd_rule/2. That way aliases stored as JSON can be loaded as they are. Registered aliases live in apersistent_term, not in the application env likeadd_rule/2rules, because everyvalidate/3call reads them.[{default, X}, integer]the default value is not checked byinteger(in JS it is). Compared with the JS email regex,\shere matches ASCII whitespace only.rebar3CI job is optional extra coverage. Drop it if one build system in CI is enough. The coverage job uploads to Coveralls withCOVERALLS_REPO_TOKEN, which is not available to pull requests from forks. That was already the case before this PR.Thanks again for your work on liver!