jsonstructure 0.5.1
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 +12 -0
- data/README.md +252 -0
- data/Rakefile +82 -0
- data/lib/jsonstructure/binary_installer.rb +134 -0
- data/lib/jsonstructure/ffi.rb +149 -0
- data/lib/jsonstructure/instance_validator.rb +76 -0
- data/lib/jsonstructure/schema_validator.rb +72 -0
- data/lib/jsonstructure/validation_result.rb +96 -0
- data/lib/jsonstructure/version.rb +6 -0
- data/lib/jsonstructure.rb +114 -0
- data/spec/instance_validator_spec.rb +124 -0
- data/spec/schema_validator_spec.rb +90 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/test_assets_spec.rb +205 -0
- data/spec/thread_safety_spec.rb +257 -0
- data/spec/validation_result_spec.rb +155 -0
- metadata +112 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: db9b0251a2324184e370f6adc96d369922448d7aea9b25f5635667a49bfe48f4
|
|
4
|
+
data.tar.gz: 4eb4c28ec727433e9b7d6f9e255d805c34ec68d7cfa2c01409bb856bf29386a1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 043a453dbb8f2b6fad129f4201001718c9a85b9c9d4268aa573bb7d04a675c6958750778eb9aa80b53c9b631c0f627ef915251144ab9f984225e6ac6d422eb5a
|
|
7
|
+
data.tar.gz: dce72087731cb806299e9bb4dd9f958a035e47c6eb996b50a10f470e87e091504d7f536195ef8b61f27ee66958fdec0a08b5f3d36819868d29bad557fec5c427
|
data/Gemfile
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# JSON Structure Ruby SDK
|
|
2
|
+
|
|
3
|
+
Ruby SDK for JSON Structure schema validation using FFI bindings to the C library.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Schema validation** - Validate JSON Structure schema documents
|
|
8
|
+
- **Instance validation** - Validate JSON instances against schemas
|
|
9
|
+
- **High performance** - FFI bindings to native C library
|
|
10
|
+
- **Idiomatic Ruby API** - Wrapped in clean, Ruby-friendly classes
|
|
11
|
+
- **Cross-platform** - Works on Linux, macOS, and Windows
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add this line to your application's Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem 'jsonstructure'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
And then execute:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bundle install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or install it yourself as:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
gem install jsonstructure
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Binary Distribution
|
|
34
|
+
|
|
35
|
+
**The gem automatically downloads pre-built C library binaries from GitHub releases.** No compilation is required during installation.
|
|
36
|
+
|
|
37
|
+
Supported platforms:
|
|
38
|
+
- Linux (x86_64, arm64)
|
|
39
|
+
- macOS (x86_64, arm64)
|
|
40
|
+
- Windows (x86_64)
|
|
41
|
+
|
|
42
|
+
If you're developing the gem itself or contributing, see the Development section below for building from source.
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
### Schema Validation
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'jsonstructure'
|
|
50
|
+
|
|
51
|
+
schema = '{"type": "string", "minLength": 1}'
|
|
52
|
+
result = JsonStructure::SchemaValidator.validate(schema)
|
|
53
|
+
|
|
54
|
+
if result.valid?
|
|
55
|
+
puts "Schema is valid!"
|
|
56
|
+
else
|
|
57
|
+
result.errors.each do |error|
|
|
58
|
+
puts "Error: #{error.message}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Instance Validation
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
require 'jsonstructure'
|
|
67
|
+
|
|
68
|
+
schema = '{"type": "string", "minLength": 1}'
|
|
69
|
+
instance = '"hello"'
|
|
70
|
+
|
|
71
|
+
result = JsonStructure::InstanceValidator.validate(instance, schema)
|
|
72
|
+
|
|
73
|
+
if result.valid?
|
|
74
|
+
puts "Instance is valid!"
|
|
75
|
+
else
|
|
76
|
+
result.errors.each do |error|
|
|
77
|
+
puts "Error: #{error.message}"
|
|
78
|
+
puts " Path: #{error.path}" if error.path
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Using Exceptions
|
|
84
|
+
|
|
85
|
+
Both validators provide a `validate!` method that raises an exception on validation failure:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require 'jsonstructure'
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
schema = '{"type": "string"}'
|
|
92
|
+
JsonStructure::SchemaValidator.validate!(schema)
|
|
93
|
+
puts "Schema is valid!"
|
|
94
|
+
rescue JsonStructure::SchemaValidationError => e
|
|
95
|
+
puts "Schema validation failed: #{e.message}"
|
|
96
|
+
e.errors.each { |err| puts " - #{err.message}" }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
instance = '123'
|
|
101
|
+
schema = '{"type": "string"}'
|
|
102
|
+
JsonStructure::InstanceValidator.validate!(instance, schema)
|
|
103
|
+
puts "Instance is valid!"
|
|
104
|
+
rescue JsonStructure::InstanceValidationError => e
|
|
105
|
+
puts "Instance validation failed: #{e.message}"
|
|
106
|
+
e.errors.each { |err| puts " - #{err.message}" }
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Working with Validation Results
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
result = JsonStructure::InstanceValidator.validate(instance, schema)
|
|
114
|
+
|
|
115
|
+
# Check validity
|
|
116
|
+
puts result.valid? # => true or false
|
|
117
|
+
puts result.invalid? # => opposite of valid?
|
|
118
|
+
|
|
119
|
+
# Access errors
|
|
120
|
+
result.errors.each do |error|
|
|
121
|
+
puts error.code # Error code
|
|
122
|
+
puts error.severity # Severity level
|
|
123
|
+
puts error.message # Human-readable message
|
|
124
|
+
puts error.path # JSON Pointer path to error location
|
|
125
|
+
puts error.location # Hash with :line, :column, :offset
|
|
126
|
+
|
|
127
|
+
# Check error type
|
|
128
|
+
puts error.error? # true if severity is ERROR
|
|
129
|
+
puts error.warning? # true if severity is WARNING
|
|
130
|
+
puts error.info? # true if severity is INFO
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Get error/warning messages
|
|
134
|
+
error_msgs = result.error_messages # Array of error messages
|
|
135
|
+
warning_msgs = result.warning_messages # Array of warning messages
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API Reference
|
|
139
|
+
|
|
140
|
+
### `JsonStructure::SchemaValidator`
|
|
141
|
+
|
|
142
|
+
- `validate(schema_json)` - Validate a schema, returns `ValidationResult`
|
|
143
|
+
- `validate!(schema_json)` - Validate a schema, raises `SchemaValidationError` on failure
|
|
144
|
+
|
|
145
|
+
### `JsonStructure::InstanceValidator`
|
|
146
|
+
|
|
147
|
+
- `validate(instance_json, schema_json)` - Validate an instance against a schema, returns `ValidationResult`
|
|
148
|
+
- `validate!(instance_json, schema_json)` - Validate an instance, raises `InstanceValidationError` on failure
|
|
149
|
+
|
|
150
|
+
### `JsonStructure::ValidationResult`
|
|
151
|
+
|
|
152
|
+
- `valid?` - Returns true if validation passed
|
|
153
|
+
- `invalid?` - Returns true if validation failed
|
|
154
|
+
- `errors` - Array of `ValidationError` objects
|
|
155
|
+
- `error_messages` - Array of error message strings
|
|
156
|
+
- `warning_messages` - Array of warning message strings
|
|
157
|
+
|
|
158
|
+
### `JsonStructure::ValidationError`
|
|
159
|
+
|
|
160
|
+
- `code` - Error code (integer)
|
|
161
|
+
- `severity` - Severity level (ERROR, WARNING, or INFO)
|
|
162
|
+
- `message` - Human-readable error message
|
|
163
|
+
- `path` - JSON Pointer path to error location (may be nil)
|
|
164
|
+
- `location` - Hash with `:line`, `:column`, `:offset` keys
|
|
165
|
+
- `error?`, `warning?`, `info?` - Check severity level
|
|
166
|
+
|
|
167
|
+
## Thread Safety
|
|
168
|
+
|
|
169
|
+
This library is **thread-safe**. Multiple threads can perform validations concurrently without any external synchronization.
|
|
170
|
+
|
|
171
|
+
### Concurrent Validation
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
require 'jsonstructure'
|
|
175
|
+
|
|
176
|
+
schema = '{"type": "object", "properties": {"name": {"type": "string"}}}'
|
|
177
|
+
|
|
178
|
+
# Safe to validate from multiple threads simultaneously
|
|
179
|
+
threads = 10.times.map do |i|
|
|
180
|
+
Thread.new do
|
|
181
|
+
instance = %Q({"name": "Thread #{i}"})
|
|
182
|
+
result = JsonStructure::InstanceValidator.validate(instance, schema)
|
|
183
|
+
puts "Thread #{i}: #{result.valid? ? 'valid' : 'invalid'}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
threads.each(&:join)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Implementation Details
|
|
190
|
+
|
|
191
|
+
- The underlying C library uses proper synchronization primitives (SRWLOCK on Windows, pthread_mutex on Unix) to protect shared state
|
|
192
|
+
- Each validation call is independent and does not share mutable state with other calls
|
|
193
|
+
- The `at_exit` cleanup hook coordinates with active validations to avoid races during process shutdown
|
|
194
|
+
|
|
195
|
+
### Best Practices
|
|
196
|
+
|
|
197
|
+
1. **Prefer stateless validation**: Each `validate` call is independent. There's no need to create validator instances or manage state.
|
|
198
|
+
|
|
199
|
+
2. **Thread-local results**: ValidationResult objects returned by `validate` are thread-local and can be safely used without synchronization.
|
|
200
|
+
|
|
201
|
+
3. **Exception safety**: If a validation raises an exception (e.g., ArgumentError for invalid input), the library state remains consistent.
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
After checking out the repo, run `bundle install` to install dependencies.
|
|
206
|
+
|
|
207
|
+
### For Local Development (Building from Source)
|
|
208
|
+
|
|
209
|
+
When developing the gem with the C library in the same repository:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
# Build the C library locally
|
|
213
|
+
rake build_c_lib_local
|
|
214
|
+
|
|
215
|
+
# Run the tests
|
|
216
|
+
rake test
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The `rake test` task will attempt to download a pre-built binary first. If that fails (e.g., during development), it will fall back to building the C library from source if available.
|
|
220
|
+
|
|
221
|
+
### For Production Use
|
|
222
|
+
|
|
223
|
+
Production installations automatically download pre-built binaries from GitHub releases. No local build is required:
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
# Just install and use
|
|
227
|
+
gem install jsonstructure
|
|
228
|
+
# The binary will be downloaded automatically on first require
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Architecture
|
|
232
|
+
|
|
233
|
+
This Ruby SDK treats the JSON Structure C library as an external dependency, downloading pre-built binaries from GitHub releases. The architecture consists of:
|
|
234
|
+
|
|
235
|
+
1. **C Library Binaries** - Pre-built shared libraries (`.so`, `.dylib`, `.dll`) distributed via GitHub releases
|
|
236
|
+
2. **Binary Installer** (`lib/jsonstructure/binary_installer.rb`) - Downloads and installs platform-specific binaries
|
|
237
|
+
3. **FFI Bindings** (`lib/jsonstructure/ffi.rb`) - Low-level FFI mappings to C functions
|
|
238
|
+
4. **Ruby Wrappers** - Idiomatic Ruby classes that wrap the FFI calls
|
|
239
|
+
- `SchemaValidator` - Schema validation
|
|
240
|
+
- `InstanceValidator` - Instance validation
|
|
241
|
+
- `ValidationResult` - Result container
|
|
242
|
+
- `ValidationError` - Error information
|
|
243
|
+
|
|
244
|
+
This design allows the Ruby SDK to be distributed independently of the C library source code, treating it as a foreign SDK dependency.
|
|
245
|
+
|
|
246
|
+
## Contributing
|
|
247
|
+
|
|
248
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/json-structure/sdk.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
begin
|
|
7
|
+
require 'rspec/core/rake_task'
|
|
8
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
9
|
+
rescue LoadError
|
|
10
|
+
# RSpec not available, skip test tasks
|
|
11
|
+
desc 'Run tests (requires rspec gem)'
|
|
12
|
+
task :spec do
|
|
13
|
+
puts 'RSpec not available. Install with: gem install rspec'
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc 'Download pre-built C library binary from GitHub releases'
|
|
18
|
+
task :download_binary do
|
|
19
|
+
require_relative 'lib/jsonstructure/binary_installer'
|
|
20
|
+
installer = JsonStructure::BinaryInstaller.new
|
|
21
|
+
installer.install
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc 'Build the C library locally (for development only)'
|
|
25
|
+
task :build_c_lib_local do
|
|
26
|
+
puts 'Building C library locally...'
|
|
27
|
+
c_dir = File.expand_path('../c', __dir__)
|
|
28
|
+
|
|
29
|
+
unless Dir.exist?(c_dir)
|
|
30
|
+
puts 'C library source not found. Skipping local build.'
|
|
31
|
+
puts 'This is normal when installing from a gem.'
|
|
32
|
+
next
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
build_dir = File.join(c_dir, 'build')
|
|
36
|
+
Dir.mkdir(build_dir) unless Dir.exist?(build_dir)
|
|
37
|
+
|
|
38
|
+
Dir.chdir(build_dir) do
|
|
39
|
+
system('cmake', '..', '-DCMAKE_BUILD_TYPE=Release', '-DJS_BUILD_SHARED=ON', '-DJS_BUILD_TESTS=OFF', '-DJS_BUILD_EXAMPLES=OFF') || raise('CMake configuration failed')
|
|
40
|
+
system('cmake', '--build', '.') || raise('Build failed')
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Copy to ext directory
|
|
44
|
+
ext_dir = File.expand_path('ext', __dir__)
|
|
45
|
+
FileUtils.mkdir_p(ext_dir)
|
|
46
|
+
|
|
47
|
+
lib_name = case RbConfig::CONFIG['host_os']
|
|
48
|
+
when /darwin|mac os/
|
|
49
|
+
'libjson_structure.dylib'
|
|
50
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
51
|
+
'json_structure.dll'
|
|
52
|
+
else
|
|
53
|
+
'libjson_structure.so'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
src = File.join(build_dir, lib_name)
|
|
57
|
+
dst = File.join(ext_dir, lib_name)
|
|
58
|
+
FileUtils.cp(src, dst) if File.exist?(src)
|
|
59
|
+
|
|
60
|
+
puts 'C library built successfully!'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc 'Run tests (downloads binary first if needed, falls back to local build for development)'
|
|
64
|
+
task :test do
|
|
65
|
+
# Try to download binary first
|
|
66
|
+
begin
|
|
67
|
+
Rake::Task[:download_binary].invoke
|
|
68
|
+
rescue StandardError => e
|
|
69
|
+
puts "Binary download failed: #{e.message}"
|
|
70
|
+
puts "Attempting local build for development..."
|
|
71
|
+
begin
|
|
72
|
+
Rake::Task[:build_c_lib_local].invoke
|
|
73
|
+
rescue StandardError => build_error
|
|
74
|
+
puts "Local build also failed: #{build_error.message}"
|
|
75
|
+
raise "Cannot obtain C library binary. Please check the error messages above."
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Rake::Task[:spec].invoke
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
task default: :test
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'rbconfig'
|
|
7
|
+
|
|
8
|
+
module JsonStructure
|
|
9
|
+
# Downloads and installs pre-built C library binaries from GitHub releases
|
|
10
|
+
class BinaryInstaller
|
|
11
|
+
REPO = 'json-structure/sdk'
|
|
12
|
+
DEFAULT_VERSION = 'v0.1.0'
|
|
13
|
+
|
|
14
|
+
attr_reader :version, :platform, :lib_dir
|
|
15
|
+
|
|
16
|
+
def initialize(version: nil)
|
|
17
|
+
@version = version || DEFAULT_VERSION
|
|
18
|
+
@platform = detect_platform
|
|
19
|
+
@lib_dir = File.expand_path('../../ext', __dir__)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install
|
|
23
|
+
FileUtils.mkdir_p(lib_dir)
|
|
24
|
+
|
|
25
|
+
if binary_exists?
|
|
26
|
+
puts "Binary already installed at #{binary_path}"
|
|
27
|
+
return true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
puts "Downloading C library binary for #{platform}..."
|
|
31
|
+
download_binary
|
|
32
|
+
puts "Binary installed successfully at #{binary_path}"
|
|
33
|
+
true
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
warn "Failed to download binary: #{e.message}"
|
|
36
|
+
warn "You may need to build the C library manually."
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def binary_exists?
|
|
41
|
+
File.exist?(binary_path)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def binary_path
|
|
45
|
+
File.join(lib_dir, binary_name)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def detect_platform
|
|
51
|
+
os = RbConfig::CONFIG['host_os']
|
|
52
|
+
arch = RbConfig::CONFIG['host_cpu']
|
|
53
|
+
|
|
54
|
+
case os
|
|
55
|
+
when /darwin|mac os/
|
|
56
|
+
"macos-#{normalize_arch(arch)}"
|
|
57
|
+
when /linux/
|
|
58
|
+
"linux-#{normalize_arch(arch)}"
|
|
59
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
60
|
+
"windows-#{normalize_arch(arch)}"
|
|
61
|
+
else
|
|
62
|
+
raise "Unsupported platform: #{os}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def normalize_arch(arch)
|
|
67
|
+
case arch
|
|
68
|
+
when /x86_64|x64|amd64/
|
|
69
|
+
'x86_64'
|
|
70
|
+
when /aarch64|arm64/
|
|
71
|
+
'arm64'
|
|
72
|
+
when /arm/
|
|
73
|
+
'arm'
|
|
74
|
+
else
|
|
75
|
+
arch
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def binary_name
|
|
80
|
+
case RbConfig::CONFIG['host_os']
|
|
81
|
+
when /darwin|mac os/
|
|
82
|
+
'libjson_structure.dylib'
|
|
83
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
84
|
+
'json_structure.dll'
|
|
85
|
+
else
|
|
86
|
+
'libjson_structure.so'
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download_url
|
|
91
|
+
# Try to get from GitHub releases
|
|
92
|
+
# Format: https://github.com/json-structure/sdk/releases/download/v0.1.0/json_structure-macos-x86_64.tar.gz
|
|
93
|
+
"https://github.com/#{REPO}/releases/download/#{version}/json_structure-#{platform}.tar.gz"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def download_binary
|
|
97
|
+
uri = URI(download_url)
|
|
98
|
+
response = fetch_with_redirects(uri)
|
|
99
|
+
|
|
100
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
101
|
+
raise "Failed to download binary from #{download_url}: HTTP #{response.code}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Save and extract tarball
|
|
105
|
+
tarball_path = File.join(lib_dir, 'binary.tar.gz')
|
|
106
|
+
File.binwrite(tarball_path, response.body)
|
|
107
|
+
|
|
108
|
+
# Extract tarball
|
|
109
|
+
Dir.chdir(lib_dir) do
|
|
110
|
+
system('tar', '-xzf', 'binary.tar.gz') || raise('Failed to extract tarball')
|
|
111
|
+
FileUtils.rm('binary.tar.gz')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Verify the binary was extracted
|
|
115
|
+
raise "Binary not found after extraction: #{binary_path}" unless File.exist?(binary_path)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def fetch_with_redirects(uri, limit = 10)
|
|
119
|
+
raise 'Too many HTTP redirects' if limit.zero?
|
|
120
|
+
|
|
121
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
122
|
+
http.use_ssl = (uri.scheme == 'https')
|
|
123
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
124
|
+
response = http.request(request)
|
|
125
|
+
|
|
126
|
+
case response
|
|
127
|
+
when Net::HTTPRedirection
|
|
128
|
+
fetch_with_redirects(URI(response['location']), limit - 1)
|
|
129
|
+
else
|
|
130
|
+
response
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ffi'
|
|
4
|
+
|
|
5
|
+
module JsonStructure
|
|
6
|
+
# Low-level FFI bindings to the C library
|
|
7
|
+
module FFI
|
|
8
|
+
extend ::FFI::Library
|
|
9
|
+
|
|
10
|
+
# Determine library name based on platform
|
|
11
|
+
lib_name = case RbConfig::CONFIG['host_os']
|
|
12
|
+
when /darwin|mac os/
|
|
13
|
+
'libjson_structure.dylib'
|
|
14
|
+
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
|
|
15
|
+
'json_structure.dll'
|
|
16
|
+
else
|
|
17
|
+
'libjson_structure.so'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Try to load from several possible locations
|
|
21
|
+
# Priority: downloaded binaries (ext/), system paths, then local build (fallback for development)
|
|
22
|
+
lib_paths = [
|
|
23
|
+
::File.expand_path("../../../ext/#{lib_name}", __FILE__),
|
|
24
|
+
lib_name, # Let FFI search in standard library paths
|
|
25
|
+
::File.expand_path("../../../../c/build/#{lib_name}", __FILE__) # Fallback for local development
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
loaded = false
|
|
29
|
+
lib_paths.each do |path|
|
|
30
|
+
begin
|
|
31
|
+
ffi_lib path
|
|
32
|
+
loaded = true
|
|
33
|
+
break
|
|
34
|
+
rescue LoadError
|
|
35
|
+
next
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
raise LoadError, "Could not load json_structure library from: #{lib_paths.join(', ')}" unless loaded
|
|
40
|
+
|
|
41
|
+
# Enums
|
|
42
|
+
typedef :int, :js_type_t
|
|
43
|
+
typedef :int, :js_error_code_t
|
|
44
|
+
typedef :int, :js_severity_t
|
|
45
|
+
|
|
46
|
+
# js_severity_t enum values
|
|
47
|
+
JS_SEVERITY_ERROR = 0
|
|
48
|
+
JS_SEVERITY_WARNING = 1
|
|
49
|
+
JS_SEVERITY_INFO = 2
|
|
50
|
+
|
|
51
|
+
# Structs
|
|
52
|
+
class JSLocation < ::FFI::Struct
|
|
53
|
+
layout :line, :int,
|
|
54
|
+
:column, :int,
|
|
55
|
+
:offset, :size_t
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class JSError < ::FFI::Struct
|
|
59
|
+
layout :code, :js_error_code_t,
|
|
60
|
+
:severity, :js_severity_t,
|
|
61
|
+
:location, JSLocation,
|
|
62
|
+
:path, :pointer,
|
|
63
|
+
:message, :pointer
|
|
64
|
+
|
|
65
|
+
def path_str
|
|
66
|
+
ptr = self[:path]
|
|
67
|
+
ptr.null? ? nil : ptr.read_string
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def message_str
|
|
71
|
+
ptr = self[:message]
|
|
72
|
+
ptr.null? ? '' : ptr.read_string
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def location_hash
|
|
76
|
+
loc = self[:location]
|
|
77
|
+
{
|
|
78
|
+
line: loc[:line],
|
|
79
|
+
column: loc[:column],
|
|
80
|
+
offset: loc[:offset]
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class JSResult < ::FFI::Struct
|
|
86
|
+
layout :valid, :bool,
|
|
87
|
+
:errors, :pointer,
|
|
88
|
+
:error_count, :size_t,
|
|
89
|
+
:error_capacity, :size_t
|
|
90
|
+
|
|
91
|
+
def errors_array
|
|
92
|
+
return [] if self[:error_count].zero?
|
|
93
|
+
|
|
94
|
+
errors_ptr = self[:errors]
|
|
95
|
+
(0...self[:error_count]).map do |i|
|
|
96
|
+
JSError.new(errors_ptr + i * JSError.size)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class JSSchemaValidator < ::FFI::Struct
|
|
102
|
+
layout :allow_import, :bool,
|
|
103
|
+
:warnings_enabled, :bool,
|
|
104
|
+
:import_registry, :pointer
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class JSInstanceValidator < ::FFI::Struct
|
|
108
|
+
layout :check_refs, :bool,
|
|
109
|
+
:warnings_enabled, :bool,
|
|
110
|
+
:import_registry, :pointer
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Library functions
|
|
114
|
+
attach_function :js_init, [], :void
|
|
115
|
+
attach_function :js_cleanup, [], :void
|
|
116
|
+
|
|
117
|
+
# Result functions
|
|
118
|
+
attach_function :js_result_init, [:pointer], :void
|
|
119
|
+
attach_function :js_result_cleanup, [:pointer], :void
|
|
120
|
+
attach_function :js_result_to_string, [:pointer], :pointer
|
|
121
|
+
|
|
122
|
+
# Schema validator functions
|
|
123
|
+
attach_function :js_schema_validator_init, [:pointer], :void
|
|
124
|
+
attach_function :js_schema_validate_string, [:pointer, :string, :pointer], :bool
|
|
125
|
+
|
|
126
|
+
# Instance validator functions
|
|
127
|
+
attach_function :js_instance_validator_init, [:pointer], :void
|
|
128
|
+
attach_function :js_instance_validate_strings, [:pointer, :string, :string, :pointer], :bool
|
|
129
|
+
|
|
130
|
+
# Error message function
|
|
131
|
+
attach_function :js_error_message, [:js_error_code_t], :string
|
|
132
|
+
|
|
133
|
+
# Memory management
|
|
134
|
+
attach_function :js_free, [:pointer], :void
|
|
135
|
+
|
|
136
|
+
# Convenience wrappers (since the C inline functions aren't exported)
|
|
137
|
+
def self.js_validate_schema(schema_json, result_ptr)
|
|
138
|
+
validator_ptr = ::FFI::MemoryPointer.new(JSSchemaValidator.size)
|
|
139
|
+
js_schema_validator_init(validator_ptr)
|
|
140
|
+
js_schema_validate_string(validator_ptr, schema_json, result_ptr)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.js_validate_instance(instance_json, schema_json, result_ptr)
|
|
144
|
+
validator_ptr = ::FFI::MemoryPointer.new(JSInstanceValidator.size)
|
|
145
|
+
js_instance_validator_init(validator_ptr)
|
|
146
|
+
js_instance_validate_strings(validator_ptr, instance_json, schema_json, result_ptr)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|