vastlint - Elixir & Erlang VAST XML validator

Copy Markdown View Source

Hex.pm Hex Docs 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:

PlatformTarget triple
macOS Apple Siliconaarch64-apple-darwin
macOS Intelx86_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"}]
end
mix 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 sizeLatency (p50)Latency (p99)
17 KB363 µs480 µs
30 KB820 µs1,050 µs
44 KB1,800 µs2,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.

Hex.pm Hex Docs 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:

PlatformTarget triple
macOS Apple Siliconaarch64-apple-darwin
macOS Intelx86_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"}]
end
mix 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    # macOS

Download 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 sizeLatency (p50)Latency (p99)
17 KB363 µs480 µs
30 KB820 µs1,050 µs
44 KB1,800 µs2,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.