vastlint 0.4.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/README.md +194 -0
- data/Rakefile +10 -0
- data/lib/vastlint/error.rb +6 -0
- data/lib/vastlint/issue.rb +39 -0
- data/lib/vastlint/library.rb +160 -0
- data/lib/vastlint/native/README.md +10 -0
- data/lib/vastlint/native/darwin_amd64/libvastlint.dylib +0 -0
- data/lib/vastlint/native/darwin_arm64/libvastlint.dylib +0 -0
- data/lib/vastlint/native/linux_amd64/libvastlint.so +0 -0
- data/lib/vastlint/native/linux_arm64/libvastlint.so +0 -0
- data/lib/vastlint/result.rb +62 -0
- data/lib/vastlint/summary.rb +37 -0
- data/lib/vastlint/version.rb +5 -0
- data/lib/vastlint.rb +53 -0
- data/scripts/fetch-libs.sh +43 -0
- data/scripts/publish-github-packages.sh +42 -0
- data/scripts/publish-rubygems.sh +37 -0
- data/test/fixtures/invalid.xml +18 -0
- data/test/fixtures/valid.xml +19 -0
- data/test/test_helper.rb +4 -0
- data/test/vastlint_test.rb +34 -0
- metadata +71 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a98849d1f2a31ab7e93a306f409fc6447f122f1b45594d19c339ae3cb40270bc
|
|
4
|
+
data.tar.gz: 3261d865cfdc12e1f3cd776b4a8a68f97f55dcf2e1d70c27b5ddcb31e37047da
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5c6cf05d772b448bd91d1351d9f0ab91a59c1ac582b843d8d7e593956c0f86688309742f4c0c8a24438f7f7160280dc0a5538139aaa21ca1f956f9ffbdcf3dca
|
|
7
|
+
data.tar.gz: 8336a6dd7af6762f49da095fb5ba41c2696bae88979e33a532ce85d34e75ae376e5cb25d4f255e12c176f847a657120ad28cb98f58c0d4c9f6f1eb0ea336740a
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# vastlint-ruby
|
|
2
|
+
|
|
3
|
+
High-performance, in-process VAST XML validation for Ruby backends.
|
|
4
|
+
|
|
5
|
+
This gem wraps the existing `vastlint-ffi` C API, which is backed by the same Rust core used by the CLI, Go binding, Erlang NIF, MCP server, and web validator. The intended use case is a DSP, SSP, ad server, or trafficking backend that needs to validate VAST XML and return structured linting results to a React or other web frontend.
|
|
6
|
+
|
|
7
|
+
## Why this shape
|
|
8
|
+
|
|
9
|
+
- No subprocess management in your Ruby app
|
|
10
|
+
- No network hop to an external validation service
|
|
11
|
+
- Stable JSON-compatible result shape for backend-to-frontend responses
|
|
12
|
+
- Same rule coverage and behavior as the rest of the vastlint ecosystem
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
RubyGems.org is not currently usable for new gem registrations, so the supported distribution target for this package is GitHub Packages.
|
|
17
|
+
|
|
18
|
+
Authenticate Bundler against GitHub Packages:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bundle config https://rubygems.pkg.github.com/aleksUIX USERNAME:TOKEN
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then add the package source in your application:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
source "https://rubygems.org"
|
|
28
|
+
|
|
29
|
+
source "https://rubygems.pkg.github.com/aleksUIX" do
|
|
30
|
+
gem "vastlint"
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then install it:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
bundle install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The gem expects a platform-matched `libvastlint` shared library.
|
|
41
|
+
|
|
42
|
+
For development in this monorepo, it will automatically try the sibling `vastlint/target/debug` and `vastlint/target/release` outputs.
|
|
43
|
+
|
|
44
|
+
For a distributable gem release, vendor the release libraries into `lib/vastlint/native/*` with:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
./scripts/fetch-libs.sh v0.4.14
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can also point the gem at an explicit shared library path:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
export VASTLINT_LIB_PATH=/absolute/path/to/libvastlint.dylib
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If you need anonymous public installs without GitHub auth, GitHub Packages is the wrong host. In that case, use a different gem server such as Gemfury, Cloudsmith, or a private Gemstash instance.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
require "vastlint"
|
|
62
|
+
|
|
63
|
+
result = Vastlint.validate(vast_xml)
|
|
64
|
+
|
|
65
|
+
if result.valid?
|
|
66
|
+
puts "clean tag"
|
|
67
|
+
else
|
|
68
|
+
puts result.summary.errors
|
|
69
|
+
puts result.issues.first.message
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
puts result.to_json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Rails controller example
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
class VastValidationsController < ApplicationController
|
|
79
|
+
def create
|
|
80
|
+
result = Vastlint.validate(
|
|
81
|
+
params.require(:xml),
|
|
82
|
+
wrapper_depth: params[:wrapper_depth].to_i,
|
|
83
|
+
max_wrapper_depth: params[:max_wrapper_depth].presence&.to_i || 5,
|
|
84
|
+
rule_overrides: params[:rule_overrides]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
render json: result.as_json
|
|
88
|
+
rescue ArgumentError, Vastlint::Error => error
|
|
89
|
+
render json: { error: error.message }, status: :unprocessable_entity
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The response shape is stable and frontend-friendly:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"version": "4.2",
|
|
99
|
+
"issues": [
|
|
100
|
+
{
|
|
101
|
+
"id": "VAST-2.0-inline-impression",
|
|
102
|
+
"severity": "error",
|
|
103
|
+
"message": "<InLine> must contain at least one <Impression>",
|
|
104
|
+
"path": "/VAST/Ad[0]/InLine",
|
|
105
|
+
"spec_ref": "IAB VAST 2.0 §2.2.1",
|
|
106
|
+
"line": 4,
|
|
107
|
+
"col": 3
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
"summary": {
|
|
111
|
+
"errors": 1,
|
|
112
|
+
"warnings": 0,
|
|
113
|
+
"infos": 0,
|
|
114
|
+
"valid": false
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
Vastlint.validate(xml, wrapper_depth: 0, max_wrapper_depth: 5, rule_overrides: nil)
|
|
123
|
+
Vastlint.version
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`rule_overrides` accepts a hash of rule IDs to levels:
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
result = Vastlint.validate(
|
|
130
|
+
vast_xml,
|
|
131
|
+
rule_overrides: {
|
|
132
|
+
"VAST-2.0-mediafile-https" => "error",
|
|
133
|
+
"VAST-4.1-mezzanine-recommended" => "off"
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Native library layout
|
|
139
|
+
|
|
140
|
+
Vendored release libraries belong at:
|
|
141
|
+
|
|
142
|
+
- `lib/vastlint/native/darwin_arm64/libvastlint.dylib`
|
|
143
|
+
- `lib/vastlint/native/darwin_amd64/libvastlint.dylib`
|
|
144
|
+
- `lib/vastlint/native/linux_arm64/libvastlint.so`
|
|
145
|
+
- `lib/vastlint/native/linux_amd64/libvastlint.so`
|
|
146
|
+
|
|
147
|
+
The shared libraries come from the `vastlint-ffi-*` tarballs attached to each `vastlint` GitHub Release.
|
|
148
|
+
|
|
149
|
+
## Publish
|
|
150
|
+
|
|
151
|
+
### From GitHub Actions
|
|
152
|
+
|
|
153
|
+
If this gem lives in its own GitHub repository, you do not need to add a separate publish token just to push to GitHub Packages. GitHub injects `GITHUB_TOKEN` automatically, and the workflow only needs `packages: write` permission.
|
|
154
|
+
|
|
155
|
+
The included workflow at `.github/workflows/publish-github-packages.yml`:
|
|
156
|
+
|
|
157
|
+
- derives the gem version from the tag or `lib/vastlint/version.rb`
|
|
158
|
+
- downloads the matching `vastlint-ffi-*` shared libraries from the main `aleksUIX/vastlint` release
|
|
159
|
+
- runs the Ruby tests
|
|
160
|
+
- builds the gem
|
|
161
|
+
- pushes it to `rubygems.pkg.github.com/<owner>` using `GITHUB_TOKEN`
|
|
162
|
+
|
|
163
|
+
That means the normal release path can be fully automated from CI.
|
|
164
|
+
|
|
165
|
+
### From a local machine
|
|
166
|
+
|
|
167
|
+
Publish to GitHub Packages with a classic personal access token that has `write:packages`.
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
export GITHUB_PACKAGES_OWNER=aleksUIX
|
|
171
|
+
export GITHUB_PACKAGES_TOKEN=YOUR_CLASSIC_PAT
|
|
172
|
+
./scripts/publish-github-packages.sh
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The script:
|
|
176
|
+
|
|
177
|
+
- builds `vastlint-<version>.gem` if needed
|
|
178
|
+
- writes a temporary `~/.gem/credentials` with `:github: Bearer TOKEN`
|
|
179
|
+
- pushes to `https://rubygems.pkg.github.com/$GITHUB_PACKAGES_OWNER`
|
|
180
|
+
|
|
181
|
+
The publish script also accepts `GITHUB_TOKEN`, so the exact same script works inside GitHub Actions without adding a separate secret.
|
|
182
|
+
|
|
183
|
+
Install from the registry with:
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
gem install vastlint \
|
|
187
|
+
--clear-sources \
|
|
188
|
+
--source https://USERNAME:TOKEN@rubygems.pkg.github.com/aleksUIX/
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
Apache 2.0.
|
|
194
|
+
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vastlint
|
|
6
|
+
class Issue
|
|
7
|
+
attr_reader :id, :severity, :message, :path, :spec_ref, :line, :col
|
|
8
|
+
|
|
9
|
+
def initialize(id:, severity:, message:, path:, spec_ref:, line:, col:)
|
|
10
|
+
@id = id
|
|
11
|
+
@severity = severity
|
|
12
|
+
@message = message
|
|
13
|
+
@path = path
|
|
14
|
+
@spec_ref = spec_ref
|
|
15
|
+
@line = line
|
|
16
|
+
@col = col
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def as_json(*)
|
|
20
|
+
{
|
|
21
|
+
id: id,
|
|
22
|
+
severity: severity,
|
|
23
|
+
message: message,
|
|
24
|
+
path: path,
|
|
25
|
+
spec_ref: spec_ref,
|
|
26
|
+
line: line,
|
|
27
|
+
col: col
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
as_json
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_json(*args)
|
|
36
|
+
JSON.generate(as_json, *args)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fiddle"
|
|
4
|
+
require "fiddle/import"
|
|
5
|
+
require "json"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "singleton"
|
|
8
|
+
|
|
9
|
+
module Vastlint
|
|
10
|
+
class Library
|
|
11
|
+
include Singleton
|
|
12
|
+
|
|
13
|
+
def validate(xml, wrapper_depth:, max_wrapper_depth:, rule_overrides:)
|
|
14
|
+
raw_result = if default_call?(wrapper_depth, max_wrapper_depth, rule_overrides)
|
|
15
|
+
api.vastlint_validate(xml, xml.bytesize)
|
|
16
|
+
else
|
|
17
|
+
api.vastlint_validate_with_options(
|
|
18
|
+
xml,
|
|
19
|
+
xml.bytesize,
|
|
20
|
+
wrapper_depth,
|
|
21
|
+
max_wrapper_depth,
|
|
22
|
+
serialize_rule_overrides(rule_overrides)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
raise LibraryError, "vastlint returned NULL" if null_pointer?(raw_result)
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
json_payload = read_c_string(api.vastlint_result_json(raw_result))
|
|
30
|
+
raise LibraryError, "vastlint_result_json returned NULL" if json_payload.nil? || json_payload.empty?
|
|
31
|
+
|
|
32
|
+
json_payload
|
|
33
|
+
ensure
|
|
34
|
+
api.vastlint_result_free(raw_result) unless null_pointer?(raw_result)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def version
|
|
39
|
+
read_c_string(api.vastlint_version()) || raise(LibraryError, "vastlint_version returned NULL")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def path
|
|
43
|
+
self.class.resolve_path
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
def resolve_path
|
|
48
|
+
candidates = [ENV["VASTLINT_LIB_PATH"], vendored_path, *development_paths].compact
|
|
49
|
+
match = candidates.find { |candidate| File.file?(candidate) }
|
|
50
|
+
return match if match
|
|
51
|
+
|
|
52
|
+
raise LibraryError, <<~MESSAGE.strip
|
|
53
|
+
unable to find libvastlint for #{platform_label}
|
|
54
|
+
looked in:
|
|
55
|
+
#{candidates.map { |candidate| " - #{candidate}" }.join("\n")}
|
|
56
|
+
set VASTLINT_LIB_PATH or vendor a release library under lib/vastlint/native
|
|
57
|
+
MESSAGE
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def vendored_path
|
|
63
|
+
File.join(repo_root, "lib", "vastlint", "native", platform_directory, library_filename)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def development_paths
|
|
67
|
+
sibling_vastlint_root = File.expand_path("../vastlint", repo_root)
|
|
68
|
+
extension = shared_library_extension
|
|
69
|
+
|
|
70
|
+
[
|
|
71
|
+
File.join(sibling_vastlint_root, "target", "debug", "libvastlint_ffi.#{extension}"),
|
|
72
|
+
File.join(sibling_vastlint_root, "target", "release", "libvastlint_ffi.#{extension}")
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def repo_root
|
|
77
|
+
File.expand_path("../..", __dir__)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def library_filename
|
|
81
|
+
"libvastlint.#{shared_library_extension}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def shared_library_extension
|
|
85
|
+
macos? ? "dylib" : "so"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def platform_directory
|
|
89
|
+
cpu = RbConfig::CONFIG.fetch("host_cpu")
|
|
90
|
+
|
|
91
|
+
if macos?
|
|
92
|
+
return "darwin_arm64" if cpu.match?(/arm64|aarch64/)
|
|
93
|
+
return "darwin_amd64" if cpu.match?(/x86_64|amd64/)
|
|
94
|
+
elsif linux?
|
|
95
|
+
return "linux_arm64" if cpu.match?(/arm64|aarch64/)
|
|
96
|
+
return "linux_amd64" if cpu.match?(/x86_64|amd64/)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
raise LibraryError, "unsupported platform #{platform_label}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def platform_label
|
|
103
|
+
"#{RbConfig::CONFIG.fetch("host_os")}/#{RbConfig::CONFIG.fetch("host_cpu")}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def macos?
|
|
107
|
+
RbConfig::CONFIG.fetch("host_os").include?("darwin")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def linux?
|
|
111
|
+
RbConfig::CONFIG.fetch("host_os").include?("linux")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def api
|
|
118
|
+
@api ||= begin
|
|
119
|
+
library_path = self.class.resolve_path
|
|
120
|
+
|
|
121
|
+
Module.new.tap do |mod|
|
|
122
|
+
mod.extend(Fiddle::Importer)
|
|
123
|
+
mod.dlload(library_path)
|
|
124
|
+
mod.extern "void* vastlint_validate(const char*, size_t)"
|
|
125
|
+
mod.extern "void* vastlint_validate_with_options(const char*, size_t, unsigned int, unsigned int, const char*)"
|
|
126
|
+
mod.extern "char* vastlint_result_json(void*)"
|
|
127
|
+
mod.extern "void vastlint_result_free(void*)"
|
|
128
|
+
mod.extern "char* vastlint_version()"
|
|
129
|
+
end
|
|
130
|
+
rescue Fiddle::DLError => error
|
|
131
|
+
raise LibraryError, "failed to load #{library_path}: #{error.message}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def default_call?(wrapper_depth, max_wrapper_depth, rule_overrides)
|
|
136
|
+
wrapper_depth.zero? && max_wrapper_depth == 5 && (rule_overrides.nil? || rule_overrides.empty?)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def serialize_rule_overrides(rule_overrides)
|
|
140
|
+
return nil if rule_overrides.nil? || rule_overrides.empty?
|
|
141
|
+
|
|
142
|
+
JSON.generate(rule_overrides)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def null_pointer?(pointer)
|
|
146
|
+
pointer.nil? || pointer.to_i.zero?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def read_c_string(value)
|
|
150
|
+
case value
|
|
151
|
+
when nil
|
|
152
|
+
nil
|
|
153
|
+
when String
|
|
154
|
+
value
|
|
155
|
+
else
|
|
156
|
+
Fiddle::Pointer.new(value.to_i).to_s
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Place release shared libraries here before publishing the gem.
|
|
2
|
+
|
|
3
|
+
Expected layout:
|
|
4
|
+
|
|
5
|
+
- darwin_arm64/libvastlint.dylib
|
|
6
|
+
- darwin_amd64/libvastlint.dylib
|
|
7
|
+
- linux_arm64/libvastlint.so
|
|
8
|
+
- linux_amd64/libvastlint.so
|
|
9
|
+
|
|
10
|
+
Use ../../scripts/fetch-libs.sh to download the platform tarballs from a tagged vastlint GitHub Release and copy the shared libraries into this directory.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vastlint
|
|
6
|
+
class Result
|
|
7
|
+
attr_reader :version, :issues, :summary
|
|
8
|
+
|
|
9
|
+
def self.from_json(json_payload)
|
|
10
|
+
parsed = JSON.parse(json_payload)
|
|
11
|
+
|
|
12
|
+
new(
|
|
13
|
+
version: parsed["version"],
|
|
14
|
+
issues: Array(parsed["issues"]).map do |issue|
|
|
15
|
+
Issue.new(
|
|
16
|
+
id: issue["id"],
|
|
17
|
+
severity: issue["severity"],
|
|
18
|
+
message: issue["message"],
|
|
19
|
+
path: issue["path"],
|
|
20
|
+
spec_ref: issue["spec_ref"],
|
|
21
|
+
line: issue["line"],
|
|
22
|
+
col: issue["col"]
|
|
23
|
+
)
|
|
24
|
+
end,
|
|
25
|
+
summary: Summary.new(
|
|
26
|
+
errors: parsed.fetch("summary").fetch("errors"),
|
|
27
|
+
warnings: parsed.fetch("summary").fetch("warnings"),
|
|
28
|
+
infos: parsed.fetch("summary").fetch("infos"),
|
|
29
|
+
valid: parsed.fetch("summary").fetch("valid")
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
rescue JSON::ParserError => error
|
|
33
|
+
raise LibraryError, "failed to parse vastlint result JSON: #{error.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(version:, issues:, summary:)
|
|
37
|
+
@version = version
|
|
38
|
+
@issues = issues
|
|
39
|
+
@summary = summary
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def valid?
|
|
43
|
+
summary.valid?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def as_json(*)
|
|
47
|
+
{
|
|
48
|
+
version: version,
|
|
49
|
+
issues: issues.map(&:as_json),
|
|
50
|
+
summary: summary.as_json
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def to_h
|
|
55
|
+
as_json
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_json(*args)
|
|
59
|
+
JSON.generate(as_json, *args)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Vastlint
|
|
6
|
+
class Summary
|
|
7
|
+
attr_reader :errors, :warnings, :infos
|
|
8
|
+
|
|
9
|
+
def initialize(errors:, warnings:, infos:, valid:)
|
|
10
|
+
@errors = errors
|
|
11
|
+
@warnings = warnings
|
|
12
|
+
@infos = infos
|
|
13
|
+
@valid = valid
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def valid?
|
|
17
|
+
@valid
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def as_json(*)
|
|
21
|
+
{
|
|
22
|
+
errors: errors,
|
|
23
|
+
warnings: warnings,
|
|
24
|
+
infos: infos,
|
|
25
|
+
valid: valid?
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
as_json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_json(*args)
|
|
34
|
+
JSON.generate(as_json, *args)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/vastlint.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "vastlint/version"
|
|
4
|
+
require_relative "vastlint/error"
|
|
5
|
+
require_relative "vastlint/library"
|
|
6
|
+
require_relative "vastlint/issue"
|
|
7
|
+
require_relative "vastlint/summary"
|
|
8
|
+
require_relative "vastlint/result"
|
|
9
|
+
|
|
10
|
+
module Vastlint
|
|
11
|
+
class << self
|
|
12
|
+
def validate(xml, wrapper_depth: 0, max_wrapper_depth: 5, rule_overrides: nil)
|
|
13
|
+
normalized_xml = normalize_xml(xml)
|
|
14
|
+
validate_options!(wrapper_depth, max_wrapper_depth, rule_overrides)
|
|
15
|
+
|
|
16
|
+
Result.from_json(
|
|
17
|
+
Library.instance.validate(
|
|
18
|
+
normalized_xml,
|
|
19
|
+
wrapper_depth: wrapper_depth,
|
|
20
|
+
max_wrapper_depth: max_wrapper_depth,
|
|
21
|
+
rule_overrides: rule_overrides
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def version
|
|
27
|
+
Library.instance.version
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def normalize_xml(xml)
|
|
33
|
+
raise ArgumentError, "xml must be a String" unless xml.is_a?(String)
|
|
34
|
+
raise ArgumentError, "xml must not be empty" if xml.empty?
|
|
35
|
+
|
|
36
|
+
xml
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_options!(wrapper_depth, max_wrapper_depth, rule_overrides)
|
|
40
|
+
unless wrapper_depth.is_a?(Integer) && wrapper_depth >= 0
|
|
41
|
+
raise ArgumentError, "wrapper_depth must be an Integer >= 0"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless max_wrapper_depth.is_a?(Integer) && max_wrapper_depth >= 0
|
|
45
|
+
raise ArgumentError, "max_wrapper_depth must be an Integer >= 0"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return if rule_overrides.nil? || rule_overrides.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "rule_overrides must be a Hash or nil"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
REPO="aleksUIX/vastlint"
|
|
6
|
+
RELEASE_TAG="${1:-}"
|
|
7
|
+
|
|
8
|
+
if [[ -z "$RELEASE_TAG" ]]; then
|
|
9
|
+
echo "Usage: $0 <release-tag> (e.g. v0.4.14)" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
BASE_URL="https://github.com/${REPO}/releases/download/${RELEASE_TAG}"
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
17
|
+
TMPDIR="$(mktemp -d)"
|
|
18
|
+
trap 'rm -rf "$TMPDIR"' EXIT
|
|
19
|
+
|
|
20
|
+
while IFS='|' read -r TARBALL PLATFORM_DIR LIB_NAME; do
|
|
21
|
+
[[ -n "$TARBALL" ]] || continue
|
|
22
|
+
|
|
23
|
+
URL="${BASE_URL}/${TARBALL}"
|
|
24
|
+
DEST_DIR="${REPO_ROOT}/lib/vastlint/native/${PLATFORM_DIR}"
|
|
25
|
+
UNPACK_DIR="${TMPDIR}/${PLATFORM_DIR}"
|
|
26
|
+
|
|
27
|
+
echo "→ Downloading ${TARBALL}"
|
|
28
|
+
mkdir -p "$UNPACK_DIR" "$DEST_DIR"
|
|
29
|
+
curl -fsSL "$URL" -o "${TMPDIR}/${TARBALL}"
|
|
30
|
+
tar xzf "${TMPDIR}/${TARBALL}" -C "$UNPACK_DIR"
|
|
31
|
+
|
|
32
|
+
cp "${UNPACK_DIR}/${LIB_NAME}" "${DEST_DIR}/${LIB_NAME}"
|
|
33
|
+
chmod 755 "${DEST_DIR}/${LIB_NAME}"
|
|
34
|
+
echo " ✓ ${DEST_DIR}/${LIB_NAME}"
|
|
35
|
+
done <<'PLATFORMS'
|
|
36
|
+
vastlint-ffi-macos-aarch64.tar.gz|darwin_arm64|libvastlint.dylib
|
|
37
|
+
vastlint-ffi-macos-x86_64.tar.gz|darwin_amd64|libvastlint.dylib
|
|
38
|
+
vastlint-ffi-linux-aarch64.tar.gz|linux_arm64|libvastlint.so
|
|
39
|
+
vastlint-ffi-linux-x86_64.tar.gz|linux_amd64|libvastlint.so
|
|
40
|
+
PLATFORMS
|
|
41
|
+
|
|
42
|
+
echo
|
|
43
|
+
echo "Done. Vendored shared libraries updated to ${RELEASE_TAG}."
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
OWNER="${GITHUB_PACKAGES_OWNER:-${GITHUB_REPOSITORY_OWNER:-aleksUIX}}"
|
|
6
|
+
TOKEN="${GITHUB_PACKAGES_TOKEN:-${GITHUB_TOKEN:-}}"
|
|
7
|
+
|
|
8
|
+
if [[ -z "$TOKEN" ]]; then
|
|
9
|
+
echo "GITHUB_PACKAGES_TOKEN or GITHUB_TOKEN is required" >&2
|
|
10
|
+
echo "Use GITHUB_TOKEN in GitHub Actions, or a classic GitHub personal access token with write:packages locally" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
15
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
16
|
+
|
|
17
|
+
cd "$REPO_ROOT"
|
|
18
|
+
|
|
19
|
+
VERSION="$(ruby -Ilib -e 'require "vastlint/version"; print Vastlint::VERSION')"
|
|
20
|
+
GEM_FILE="vastlint-${VERSION}.gem"
|
|
21
|
+
|
|
22
|
+
if [[ ! -f "$GEM_FILE" ]]; then
|
|
23
|
+
gem build vastlint.gemspec
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
TMP_HOME="$(mktemp -d)"
|
|
27
|
+
trap 'rm -rf "$TMP_HOME"' EXIT
|
|
28
|
+
|
|
29
|
+
mkdir -p "$TMP_HOME/.gem"
|
|
30
|
+
chmod 700 "$TMP_HOME/.gem"
|
|
31
|
+
|
|
32
|
+
cat > "$TMP_HOME/.gem/credentials" <<EOF
|
|
33
|
+
---
|
|
34
|
+
:github: Bearer ${TOKEN}
|
|
35
|
+
EOF
|
|
36
|
+
|
|
37
|
+
chmod 600 "$TMP_HOME/.gem/credentials"
|
|
38
|
+
|
|
39
|
+
HOME="$TMP_HOME" gem push \
|
|
40
|
+
--key github \
|
|
41
|
+
--host "https://rubygems.pkg.github.com/${OWNER}" \
|
|
42
|
+
"$GEM_FILE"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
API_KEY="${RUBYGEMS_API_KEY:-}"
|
|
6
|
+
|
|
7
|
+
if [[ -z "$API_KEY" ]]; then
|
|
8
|
+
echo "RUBYGEMS_API_KEY is required (an API key from https://rubygems.org/profile/api_keys with Push rights)" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
14
|
+
|
|
15
|
+
cd "$REPO_ROOT"
|
|
16
|
+
|
|
17
|
+
VERSION="$(ruby -Ilib -e 'require "vastlint/version"; print Vastlint::VERSION')"
|
|
18
|
+
GEM_FILE="vastlint-${VERSION}.gem"
|
|
19
|
+
|
|
20
|
+
if [[ ! -f "$GEM_FILE" ]]; then
|
|
21
|
+
gem build vastlint.gemspec
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
TMP_HOME="$(mktemp -d)"
|
|
25
|
+
trap 'rm -rf "$TMP_HOME"' EXIT
|
|
26
|
+
|
|
27
|
+
mkdir -p "$TMP_HOME/.gem"
|
|
28
|
+
chmod 700 "$TMP_HOME/.gem"
|
|
29
|
+
|
|
30
|
+
cat > "$TMP_HOME/.gem/credentials" <<EOF
|
|
31
|
+
---
|
|
32
|
+
:rubygems_api_key: ${API_KEY}
|
|
33
|
+
EOF
|
|
34
|
+
|
|
35
|
+
chmod 600 "$TMP_HOME/.gem/credentials"
|
|
36
|
+
|
|
37
|
+
HOME="$TMP_HOME" gem push "$GEM_FILE"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<VAST version="4.2">
|
|
2
|
+
<Ad>
|
|
3
|
+
<InLine>
|
|
4
|
+
<AdSystem>Broken DSP Creative</AdSystem>
|
|
5
|
+
<AdTitle>Invalid Ad</AdTitle>
|
|
6
|
+
<Creatives>
|
|
7
|
+
<Creative>
|
|
8
|
+
<Linear>
|
|
9
|
+
<Duration>30</Duration>
|
|
10
|
+
<MediaFiles>
|
|
11
|
+
<MediaFile delivery="progressive" type="video/mp4" width="640" height="360">http://example.com/video.mp4</MediaFile>
|
|
12
|
+
</MediaFiles>
|
|
13
|
+
</Linear>
|
|
14
|
+
</Creative>
|
|
15
|
+
</Creatives>
|
|
16
|
+
</InLine>
|
|
17
|
+
</Ad>
|
|
18
|
+
</VAST>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<VAST version="2.0">
|
|
2
|
+
<Ad>
|
|
3
|
+
<InLine>
|
|
4
|
+
<AdSystem>Test</AdSystem>
|
|
5
|
+
<AdTitle>Test Ad</AdTitle>
|
|
6
|
+
<Impression><![CDATA[https://example.com/pixel]]></Impression>
|
|
7
|
+
<Creatives>
|
|
8
|
+
<Creative>
|
|
9
|
+
<Linear>
|
|
10
|
+
<Duration>00:00:30</Duration>
|
|
11
|
+
<MediaFiles>
|
|
12
|
+
<MediaFile delivery="progressive" type="video/mp4" width="640" height="360"><![CDATA[https://example.com/video.mp4]]></MediaFile>
|
|
13
|
+
</MediaFiles>
|
|
14
|
+
</Linear>
|
|
15
|
+
</Creative>
|
|
16
|
+
</Creatives>
|
|
17
|
+
</InLine>
|
|
18
|
+
</Ad>
|
|
19
|
+
</VAST>
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
class VastlintTest < Minitest::Test
|
|
6
|
+
def test_version_returns_a_string
|
|
7
|
+
refute_empty Vastlint.version
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_valid_fixture_returns_no_errors
|
|
11
|
+
result = Vastlint.validate(read_fixture("valid.xml"))
|
|
12
|
+
|
|
13
|
+
assert result.valid?
|
|
14
|
+
assert_equal 0, result.summary.errors
|
|
15
|
+
assert_equal true, result.summary.valid?
|
|
16
|
+
assert_equal "2.0", result.version
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_invalid_fixture_returns_structured_issues
|
|
20
|
+
result = Vastlint.validate(read_fixture("invalid.xml"))
|
|
21
|
+
|
|
22
|
+
refute result.valid?
|
|
23
|
+
assert_operator result.summary.errors, :>, 0
|
|
24
|
+
assert_operator result.issues.length, :>, 0
|
|
25
|
+
assert_includes %w[error warning info], result.issues.first.severity
|
|
26
|
+
assert_kind_of Hash, result.as_json
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def read_fixture(name)
|
|
32
|
+
File.read(File.expand_path("fixtures/#{name}", __dir__))
|
|
33
|
+
end
|
|
34
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: vastlint
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.14
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alex Sekowski
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Ruby bindings for the vastlint Rust core via the stable C FFI. Validate
|
|
14
|
+
VAST XML in a DSP backend and return structured results directly to a React frontend.
|
|
15
|
+
email:
|
|
16
|
+
- alex@vastlint.org
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- Gemfile
|
|
22
|
+
- README.md
|
|
23
|
+
- Rakefile
|
|
24
|
+
- lib/vastlint.rb
|
|
25
|
+
- lib/vastlint/error.rb
|
|
26
|
+
- lib/vastlint/issue.rb
|
|
27
|
+
- lib/vastlint/library.rb
|
|
28
|
+
- lib/vastlint/native/README.md
|
|
29
|
+
- lib/vastlint/native/darwin_amd64/libvastlint.dylib
|
|
30
|
+
- lib/vastlint/native/darwin_arm64/libvastlint.dylib
|
|
31
|
+
- lib/vastlint/native/linux_amd64/libvastlint.so
|
|
32
|
+
- lib/vastlint/native/linux_arm64/libvastlint.so
|
|
33
|
+
- lib/vastlint/result.rb
|
|
34
|
+
- lib/vastlint/summary.rb
|
|
35
|
+
- lib/vastlint/version.rb
|
|
36
|
+
- scripts/fetch-libs.sh
|
|
37
|
+
- scripts/publish-github-packages.sh
|
|
38
|
+
- scripts/publish-rubygems.sh
|
|
39
|
+
- test/fixtures/invalid.xml
|
|
40
|
+
- test/fixtures/valid.xml
|
|
41
|
+
- test/test_helper.rb
|
|
42
|
+
- test/vastlint_test.rb
|
|
43
|
+
homepage: https://vastlint.org
|
|
44
|
+
licenses:
|
|
45
|
+
- Apache-2.0
|
|
46
|
+
metadata:
|
|
47
|
+
homepage_uri: https://vastlint.org
|
|
48
|
+
source_code_uri: https://github.com/aleksUIX/vastlint-ruby
|
|
49
|
+
changelog_uri: https://github.com/aleksUIX/vastlint-ruby/releases
|
|
50
|
+
documentation_uri: https://vastlint.org/docs/rules
|
|
51
|
+
github_repo: ssh://github.com/aleksUIX/vastlint-ruby
|
|
52
|
+
post_install_message:
|
|
53
|
+
rdoc_options: []
|
|
54
|
+
require_paths:
|
|
55
|
+
- lib
|
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.1'
|
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
requirements: []
|
|
67
|
+
rubygems_version: 3.5.22
|
|
68
|
+
signing_key:
|
|
69
|
+
specification_version: 4
|
|
70
|
+
summary: In-process Ruby bindings for vastlint VAST XML validation
|
|
71
|
+
test_files: []
|