High-performance VAST XML validator for the BEAM.
Validates IAB VAST 2.0–4.3 tags against 108 rules covering required elements, schema structure, security (HTTPS), deprecated features, and CTV advisories.
Backed by vastlint-core (Rust) via a
DirtyCpu NIF - validation never blocks BEAM schedulers regardless of tag
size or concurrency. Ships precompiled NIFs for all major platforms; no Rust
toolchain required.
Platforms
Precompiled NIFs are provided for:
| Platform | Target triple |
|---|---|
| macOS Apple Silicon | aarch64-apple-darwin |
| macOS Intel | x86_64-apple-darwin |
| Linux arm64 (glibc) | aarch64-unknown-linux-gnu |
| Linux x86_64 (glibc) | x86_64-unknown-linux-gnu |
Note: musl targets (Alpine Linux) are not supported for precompiled NIFs because Rust cannot produce shared libraries (
cdylib) for musl. Alpine users can build from source - see below.
Installation
Elixir / Mix
# mix.exs
def deps do
[{:vastlint, "~> 0.3"}]
endmix deps.get
Erlang / rebar3
%% rebar.config
{deps, [{vastlint, "0.3.6"}]}.rebar3 get-deps
The correct precompiled NIF for your platform is downloaded automatically at
deps.get / rebar3 get-deps time. No manual steps required.
Building from source
If no precompiled NIF is available for your platform (e.g. Alpine/musl), build from source. Requires Rust ≥ 1.86:
# Elixir - force a source build
VASTLINT_BUILD=true mix deps.compile vastlint
# Erlang - compile the NIF manually then symlink or copy the result
cd native/vastlint_nif
cargo build --release
cp target/release/libvastlint_nif.so ../../priv/vastlint_nif.so # Linux
cp target/release/libvastlint_nif.dylib ../../priv/vastlint_nif.so # macOS
Usage
Elixir
# Basic validation
{:ok, result} = Vastlint.validate(xml)
result.valid #=> true
result.summary.errors #=> 0
result.issues #=> []
# With options
opts = [
wrapper_depth: 2,
max_wrapper_depth: 5,
rule_overrides: %{"VAST-2.0-mediafile-https" => "off"}
]
{:ok, result} = Vastlint.validate(xml, opts)
# Raising variant - returns Result directly, raises ValidationError on NIF failure
result = Vastlint.validate!(xml)
# Batch validation (validates a list of VAST tags in parallel)
results = Vastlint.validate_batch([xml1, xml2, xml3])
# Library version
Vastlint.version() #=> "0.3.6"Result shape
%Vastlint.Result{
version: "4.2", # VAST version from the tag, or nil
valid: true, # true when errors == 0
summary: %Vastlint.Summary{errors: 0, warnings: 1, infos: 0},
issues: [
%Vastlint.Issue{
id: "VAST-2.0-mediafile-https",
severity: :warning,
message: "MediaFile URL should use HTTPS",
path: "/VAST/Ad/InLine/Creatives/Creative/Linear/MediaFiles/MediaFile",
spec_ref: "VAST 2.0 §3.3.2"
}
]
}Erlang
{ok, Result} = vastlint:validate(Xml),
Valid = maps:get(valid, Result),
Issues = maps:get(issues, Result),
Errors = maps:get(errors, Result).
%% With options
{ok, Result} = vastlint:validate_with_opts(Xml, 0, 5,
#{<<"VAST-2.0-mediafile-https">> => <<"off">>}).
%% Batch validation
Results = vastlint:validate_batch([Xml1, Xml2, Xml3]).
%% Version
Version = vastlint:version().Result map shape
#{
version => binary() | undefined,
valid => boolean(),
errors => non_neg_integer(),
warnings => non_neg_integer(),
infos => non_neg_integer(),
issues => [#{
id => binary(),
severity => error | warning | info, %% atom
message => binary(),
path => binary() | undefined,
spec_ref => binary()
}]
}Performance
Benchmarked on production VAST tags (17–44 KB):
| Tag size | Latency (p50) | Latency (p99) |
|---|---|---|
| 17 KB | 363 µs | 480 µs |
| 30 KB | 820 µs | 1,050 µs |
| 44 KB | 1,800 µs | 2,104 µs |
NIFs run on dirty CPU schedulers - concurrent calls from many BEAM processes scale linearly with available cores. A 50-process concurrency test passes with zero scheduler stalls.
validate_batch/1 validates a list of tags in a single NIF call using Rayon
parallelism, achieving ~10,000 validations/second on a single machine.
Architecture
Elixir app Erlang app
| |
Vastlint.validate/1 vastlint:validate/1
| |
:vastlint_nif.validate/1 vastlint_nif:validate/1
\ /
vastlint_nif.so (Rust cdylib, DirtyCpu NIF)
|
vastlint-core (Rust, 108 validation rules)The NIF module is registered as the Erlang atom vastlint_nif - the same atom
is used by both the Elixir and Erlang loaders, so a single compiled .so serves
both ecosystems without any bridging shim.
License
Apache-2.0 - see LICENSE.
High-performance VAST XML validator for the BEAM.
Validates IAB VAST 2.0–4.3 tags against 108 rules covering required elements, schema structure, security (HTTPS), deprecated features, and CTV advisories.
Backed by vastlint-core (Rust) via a
DirtyCpu NIF - validation never blocks BEAM schedulers regardless of tag
size or concurrency. Ships precompiled NIFs for all major platforms; no Rust
toolchain required.
Platforms
Precompiled NIFs are provided for:
| Platform | Target triple |
|---|---|
| macOS Apple Silicon | aarch64-apple-darwin |
| macOS Intel | x86_64-apple-darwin |
| Linux arm64 (glibc) | aarch64-unknown-linux-gnu |
| Linux x86_64 (glibc) | x86_64-unknown-linux-gnu |
| Linux arm64 (musl) | aarch64-unknown-linux-musl |
| Linux x86_64 (musl) | x86_64-unknown-linux-musl |
Installation
Elixir / Mix
# mix.exs
def deps do
[{:vastlint, "~> 0.3"}]
endmix deps.get
Erlang / rebar3
%% rebar.config
{deps, [{vastlint, "0.3.3"}]}.rebar3 get-deps
Place (or symlink) the precompiled NIF for your platform in priv/:
priv/vastlint_nif.so # Linux
priv/vastlint_nif.dylib # macOSDownload tarballs from the GitHub Releases.
Building from source
If no precompiled NIF is available for your platform, build from source (requires Rust ≥ 1.86):
# Elixir - force a source build
VASTLINT_BUILD=true mix deps.compile vastlint
# Erlang - compile the NIF manually
cd native/vastlint_nif
cargo build --release
cp target/release/libvastlint_nif.{so,dylib} ../../priv/vastlint_nif.so
Usage
Elixir
# Basic validation
{:ok, result} = Vastlint.validate(xml)
result.valid #=> true
result.summary.errors #=> 0
result.issues #=> []
# With options
opts = [
wrapper_depth: 2,
max_wrapper_depth: 5,
rule_overrides: %{"VAST-2.0-mediafile-https" => "off"}
]
{:ok, result} = Vastlint.validate(xml, opts)
# Raising variant - returns Result directly, raises ValidationError on NIF failure
result = Vastlint.validate!(xml)
# Library version
Vastlint.version() #=> "0.3.3"Result shape
%Vastlint.Result{
version: "4.2", # VAST version from the tag, or nil
valid: true, # true when errors == 0
summary: %Vastlint.Summary{errors: 0, warnings: 1, infos: 0},
issues: [
%Vastlint.Issue{
id: "VAST-2.0-mediafile-https",
severity: :warning,
message: "MediaFile URL should use HTTPS",
path: "/VAST/Ad/InLine/Creatives/Creative/Linear/MediaFiles/MediaFile",
spec_ref: "VAST 2.0 §3.3.2"
}
]
}Erlang
{ok, Result} = vastlint:validate(Xml),
Valid = maps:get(valid, Result),
Issues = maps:get(issues, Result),
Errors = maps:get(errors, Result).
%% With options
{ok, Result} = vastlint:validate_with_opts(Xml, 0, 5,
#{<<"VAST-2.0-mediafile-https">> => <<"off">>}).
%% Version
Version = vastlint:version().Result map shape
#{
version => binary() | undefined,
valid => boolean(),
errors => non_neg_integer(),
warnings => non_neg_integer(),
infos => non_neg_integer(),
issues => [#{
id => binary(),
severity => error | warning | info, %% atom
message => binary(),
path => binary() | undefined,
spec_ref => binary()
}]
}Performance
Benchmarked on production VAST tags (17–44 KB):
| Tag size | Latency (p50) | Latency (p99) |
|---|---|---|
| 17 KB | 363 µs | 480 µs |
| 30 KB | 820 µs | 1,050 µs |
| 44 KB | 1,800 µs | 2,104 µs |
NIFs run on dirty CPU schedulers - concurrent calls from many BEAM processes scale linearly with available cores. A 50-process concurrency test passes with zero scheduler stalls.
Architecture
Elixir app Erlang app
| |
Vastlint.validate/1 vastlint:validate/1
| |
:vastlint_nif.validate/1 vastlint_nif:validate/1
\ /
vastlint_nif.so (Rust cdylib, DirtyCpu NIF)
|
vastlint-core (Rust, 108 validation rules)The NIF module is registered as the Erlang atom vastlint_nif - the same atom
is used by both the Elixir and Erlang loaders, so a single compiled .so serves
both ecosystems without any bridging shim.
License
Apache-2.0 - see LICENSE.