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 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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in jsonstructure.gemspec
6
+ gemspec
7
+
8
+ group :development, :test do
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'rubocop', '~> 1.50'
12
+ end
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