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.
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonStructure
4
+ # Validates JSON instances against JSON Structure schemas
5
+ #
6
+ # This class is thread-safe. Multiple threads can call validate concurrently.
7
+ class InstanceValidator
8
+ # Validate an instance against a schema
9
+ #
10
+ # This method is thread-safe and can be called from multiple threads concurrently.
11
+ #
12
+ # @param instance_json [String] JSON string containing the instance to validate
13
+ # @param schema_json [String] JSON string containing the schema
14
+ # @return [ValidationResult] validation result
15
+ #
16
+ # @example
17
+ # schema = '{"type": "string", "minLength": 1}'
18
+ # instance = '"hello"'
19
+ # result = JsonStructure::InstanceValidator.validate(instance, schema)
20
+ # if result.valid?
21
+ # puts "Instance is valid!"
22
+ # else
23
+ # result.errors.each { |e| puts e.message }
24
+ # end
25
+ def self.validate(instance_json, schema_json)
26
+ raise ArgumentError, 'instance_json must be a String' unless instance_json.is_a?(String)
27
+ raise ArgumentError, 'schema_json must be a String' unless schema_json.is_a?(String)
28
+
29
+ JsonStructure.validation_started
30
+ begin
31
+ result_ptr = ::FFI::MemoryPointer.new(FFI::JSResult.size)
32
+ FFI.js_result_init(result_ptr)
33
+
34
+ FFI.js_validate_instance(instance_json, schema_json, result_ptr)
35
+ ValidationResult.from_ffi(result_ptr)
36
+ ensure
37
+ JsonStructure.validation_completed
38
+ end
39
+ end
40
+
41
+ # Validate an instance against a schema, raising an exception on failure
42
+ #
43
+ # @param instance_json [String] JSON string containing the instance to validate
44
+ # @param schema_json [String] JSON string containing the schema
45
+ # @return [ValidationResult] validation result (only if valid)
46
+ # @raise [InstanceValidationError] if validation fails
47
+ #
48
+ # @example
49
+ # begin
50
+ # result = JsonStructure::InstanceValidator.validate!(instance, schema)
51
+ # puts "Instance is valid!"
52
+ # rescue JsonStructure::InstanceValidationError => e
53
+ # puts "Validation failed: #{e.message}"
54
+ # end
55
+ def self.validate!(instance_json, schema_json)
56
+ result = validate(instance_json, schema_json)
57
+ raise InstanceValidationError.new(result) unless result.valid?
58
+
59
+ result
60
+ end
61
+ end
62
+
63
+ # Exception raised when instance validation fails
64
+ class InstanceValidationError < StandardError
65
+ attr_reader :result
66
+
67
+ def initialize(result)
68
+ @result = result
69
+ super(result.to_s)
70
+ end
71
+
72
+ def errors
73
+ @result.errors
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonStructure
4
+ # Validates JSON Structure schema documents
5
+ #
6
+ # This class is thread-safe. Multiple threads can call validate concurrently.
7
+ class SchemaValidator
8
+ # Validate a schema string
9
+ #
10
+ # This method is thread-safe and can be called from multiple threads concurrently.
11
+ #
12
+ # @param schema_json [String] JSON string containing the schema
13
+ # @return [ValidationResult] validation result
14
+ #
15
+ # @example
16
+ # schema = '{"type": "string", "minLength": 1}'
17
+ # result = JsonStructure::SchemaValidator.validate(schema)
18
+ # if result.valid?
19
+ # puts "Schema is valid!"
20
+ # else
21
+ # result.errors.each { |e| puts e.message }
22
+ # end
23
+ def self.validate(schema_json)
24
+ raise ArgumentError, 'schema_json must be a String' unless schema_json.is_a?(String)
25
+
26
+ JsonStructure.validation_started
27
+ begin
28
+ result_ptr = ::FFI::MemoryPointer.new(FFI::JSResult.size)
29
+ FFI.js_result_init(result_ptr)
30
+
31
+ FFI.js_validate_schema(schema_json, result_ptr)
32
+ ValidationResult.from_ffi(result_ptr)
33
+ ensure
34
+ JsonStructure.validation_completed
35
+ end
36
+ end
37
+
38
+ # Validate a schema string, raising an exception on failure
39
+ #
40
+ # @param schema_json [String] JSON string containing the schema
41
+ # @return [ValidationResult] validation result (only if valid)
42
+ # @raise [SchemaValidationError] if validation fails
43
+ #
44
+ # @example
45
+ # begin
46
+ # JsonStructure::SchemaValidator.validate!(schema)
47
+ # puts "Schema is valid!"
48
+ # rescue JsonStructure::SchemaValidationError => e
49
+ # puts "Validation failed: #{e.message}"
50
+ # end
51
+ def self.validate!(schema_json)
52
+ result = validate(schema_json)
53
+ raise SchemaValidationError.new(result) unless result.valid?
54
+
55
+ result
56
+ end
57
+ end
58
+
59
+ # Exception raised when schema validation fails
60
+ class SchemaValidationError < StandardError
61
+ attr_reader :result
62
+
63
+ def initialize(result)
64
+ @result = result
65
+ super(result.to_s)
66
+ end
67
+
68
+ def errors
69
+ @result.errors
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonStructure
4
+ # Represents a validation error
5
+ class ValidationError
6
+ attr_reader :code, :severity, :path, :message, :location
7
+
8
+ def initialize(code:, severity:, path:, message:, location:)
9
+ @code = code
10
+ @severity = severity
11
+ @path = path
12
+ @message = message
13
+ @location = location
14
+ end
15
+
16
+ def error?
17
+ @severity == FFI::JS_SEVERITY_ERROR
18
+ end
19
+
20
+ def warning?
21
+ @severity == FFI::JS_SEVERITY_WARNING
22
+ end
23
+
24
+ def info?
25
+ @severity == FFI::JS_SEVERITY_INFO
26
+ end
27
+
28
+ def to_s
29
+ if @path && !@path.empty?
30
+ "#{@message} (at #{@path})"
31
+ else
32
+ @message
33
+ end
34
+ end
35
+
36
+ def inspect
37
+ "#<#{self.class.name} @severity=#{@severity} @code=#{@code} @message=#{@message.inspect} @path=#{@path.inspect}>"
38
+ end
39
+ end
40
+
41
+ # Represents the result of a validation operation
42
+ class ValidationResult
43
+ attr_reader :errors
44
+
45
+ def initialize(valid, errors = [])
46
+ @valid = valid
47
+ @errors = errors
48
+ end
49
+
50
+ def valid?
51
+ @valid
52
+ end
53
+
54
+ def invalid?
55
+ !@valid
56
+ end
57
+
58
+ def error_messages
59
+ @errors.select(&:error?).map(&:message)
60
+ end
61
+
62
+ def warning_messages
63
+ @errors.select(&:warning?).map(&:message)
64
+ end
65
+
66
+ def to_s
67
+ if valid?
68
+ 'Validation succeeded'
69
+ else
70
+ "Validation failed with #{@errors.count} error(s):\n" +
71
+ @errors.map { |e| " - #{e}" }.join("\n")
72
+ end
73
+ end
74
+
75
+ # Creates a ValidationResult from an FFI JSResult struct
76
+ # @api private
77
+ def self.from_ffi(result_ptr)
78
+ result = FFI::JSResult.new(result_ptr)
79
+ valid = result[:valid]
80
+
81
+ errors = result.errors_array.map do |ffi_error|
82
+ ValidationError.new(
83
+ code: ffi_error[:code],
84
+ severity: ffi_error[:severity],
85
+ path: ffi_error.path_str,
86
+ message: ffi_error.message_str,
87
+ location: ffi_error.location_hash
88
+ )
89
+ end
90
+
91
+ new(valid, errors)
92
+ ensure
93
+ FFI.js_result_cleanup(result_ptr) if result_ptr
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonStructure
4
+ # Version of the JSON Structure Ruby SDK
5
+ VERSION = '0.5.1'
6
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rbconfig'
4
+ require 'ffi'
5
+
6
+ require_relative 'jsonstructure/version'
7
+ require_relative 'jsonstructure/binary_installer'
8
+ require_relative 'jsonstructure/ffi'
9
+ require_relative 'jsonstructure/validation_result'
10
+ require_relative 'jsonstructure/schema_validator'
11
+ require_relative 'jsonstructure/instance_validator'
12
+
13
+ # JSON Structure SDK for Ruby
14
+ #
15
+ # This gem provides Ruby bindings to the JSON Structure C library
16
+ # via FFI (Foreign Function Interface). It allows you to validate
17
+ # JSON Structure schemas and validate JSON instances against schemas.
18
+ #
19
+ # ## Thread Safety
20
+ #
21
+ # This library is thread-safe. Multiple threads can perform validations
22
+ # concurrently without synchronization. The underlying C library uses
23
+ # proper synchronization primitives to protect shared state.
24
+ #
25
+ # @example Schema Validation
26
+ # schema = '{"type": "string", "minLength": 1}'
27
+ # result = JsonStructure::SchemaValidator.validate(schema)
28
+ # puts "Valid!" if result.valid?
29
+ #
30
+ # @example Instance Validation
31
+ # schema = '{"type": "string"}'
32
+ # instance = '"hello"'
33
+ # result = JsonStructure::InstanceValidator.validate(instance, schema)
34
+ # puts "Valid!" if result.valid?
35
+ #
36
+ # @example Concurrent Validation
37
+ # threads = 10.times.map do |i|
38
+ # Thread.new do
39
+ # result = JsonStructure::SchemaValidator.validate('{"type": "string"}')
40
+ # puts "Thread #{i}: #{result.valid?}"
41
+ # end
42
+ # end
43
+ # threads.each(&:join)
44
+ #
45
+ # @see SchemaValidator
46
+ # @see InstanceValidator
47
+ # @see ValidationResult
48
+ module JsonStructure
49
+ class Error < StandardError; end
50
+
51
+ # Mutex for protecting cleanup coordination
52
+ @cleanup_mutex = Mutex.new
53
+ # Count of active validations (for safe cleanup)
54
+ @active_validations = 0
55
+ # Flag to prevent new validations during shutdown
56
+ @shutting_down = false
57
+
58
+ class << self
59
+ # Track when a validation starts
60
+ # @api private
61
+ def validation_started
62
+ @cleanup_mutex.synchronize do
63
+ raise Error, 'Library is shutting down' if @shutting_down
64
+
65
+ @active_validations += 1
66
+ end
67
+ end
68
+
69
+ # Track when a validation completes
70
+ # @api private
71
+ def validation_completed
72
+ @cleanup_mutex.synchronize do
73
+ @active_validations -= 1
74
+ end
75
+ end
76
+
77
+ # Check if any validations are currently active
78
+ # @api private
79
+ def validations_active?
80
+ @cleanup_mutex.synchronize do
81
+ @active_validations > 0
82
+ end
83
+ end
84
+
85
+ # Safely clean up the library, waiting for active validations
86
+ # @api private
87
+ def safe_cleanup
88
+ @cleanup_mutex.synchronize do
89
+ @shutting_down = true
90
+ end
91
+
92
+ # Wait briefly for active validations to complete (up to 1 second)
93
+ 10.times do
94
+ break unless validations_active?
95
+
96
+ sleep 0.1
97
+ end
98
+
99
+ # Perform cleanup - the C library is thread-safe, so this is safe
100
+ # even if a validation is somehow still running
101
+ FFI.js_cleanup
102
+ end
103
+ end
104
+
105
+ # Initialize the JSON Structure library
106
+ # This is called automatically when the module is loaded
107
+ FFI.js_init
108
+
109
+ # Clean up the JSON Structure library when Ruby exits
110
+ # Uses safe_cleanup to coordinate with active validations
111
+ at_exit do
112
+ JsonStructure.safe_cleanup
113
+ end
114
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe JsonStructure::InstanceValidator do
6
+ describe '.validate' do
7
+ context 'with valid instance' do
8
+ it 'validates string against string schema' do
9
+ schema = '{"type": "string"}'
10
+ instance = '"hello"'
11
+ result = described_class.validate(instance, schema)
12
+
13
+ expect(result).to be_valid
14
+ expect(result.errors).to be_empty
15
+ end
16
+
17
+ it 'validates integer against integer schema' do
18
+ schema = '{"type": "integer"}'
19
+ instance = '42'
20
+ result = described_class.validate(instance, schema)
21
+
22
+ expect(result).to be_valid
23
+ expect(result.errors).to be_empty
24
+ end
25
+
26
+ it 'validates object against object schema' do
27
+ schema = '{"type": "object", "properties": {"name": {"type": "string"}}}'
28
+ instance = '{"name": "Alice"}'
29
+ result = described_class.validate(instance, schema)
30
+
31
+ expect(result).to be_valid
32
+ expect(result.errors).to be_empty
33
+ end
34
+
35
+ it 'validates array against array schema' do
36
+ schema = '{"type": "array", "items": {"type": "integer"}}'
37
+ instance = '[1, 2, 3]'
38
+ result = described_class.validate(instance, schema)
39
+
40
+ expect(result).to be_valid
41
+ expect(result.errors).to be_empty
42
+ end
43
+ end
44
+
45
+ context 'with invalid instance' do
46
+ it 'rejects wrong type' do
47
+ schema = '{"type": "string"}'
48
+ instance = '123'
49
+ result = described_class.validate(instance, schema)
50
+
51
+ expect(result).to be_invalid
52
+ expect(result.errors).not_to be_empty
53
+ end
54
+
55
+ it 'rejects string too short' do
56
+ schema = '{"type": "string", "minLength": 5}'
57
+ instance = '"hi"'
58
+ result = described_class.validate(instance, schema)
59
+
60
+ expect(result).to be_invalid
61
+ expect(result.errors).not_to be_empty
62
+ end
63
+
64
+ it 'rejects number out of range' do
65
+ schema = '{"type": "integer", "minimum": 10}'
66
+ instance = '5'
67
+ result = described_class.validate(instance, schema)
68
+
69
+ expect(result).to be_invalid
70
+ expect(result.errors).not_to be_empty
71
+ end
72
+ end
73
+
74
+ context 'error handling' do
75
+ it 'raises ArgumentError for non-string instance' do
76
+ schema = '{"type": "string"}'
77
+
78
+ expect { described_class.validate(nil, schema) }.to raise_error(ArgumentError)
79
+ expect { described_class.validate(123, schema) }.to raise_error(ArgumentError)
80
+ end
81
+
82
+ it 'raises ArgumentError for non-string schema' do
83
+ instance = '"hello"'
84
+
85
+ expect { described_class.validate(instance, nil) }.to raise_error(ArgumentError)
86
+ expect { described_class.validate(instance, 123) }.to raise_error(ArgumentError)
87
+ end
88
+ end
89
+ end
90
+
91
+ describe '.validate!' do
92
+ context 'with valid instance' do
93
+ it 'returns result without raising' do
94
+ schema = '{"type": "string"}'
95
+ instance = '"hello"'
96
+ result = described_class.validate!(instance, schema)
97
+
98
+ expect(result).to be_valid
99
+ end
100
+ end
101
+
102
+ context 'with invalid instance' do
103
+ it 'raises InstanceValidationError' do
104
+ schema = '{"type": "string"}'
105
+ instance = '123'
106
+
107
+ expect { described_class.validate!(instance, schema) }.to raise_error(JsonStructure::InstanceValidationError)
108
+ end
109
+
110
+ it 'includes validation errors in exception' do
111
+ schema = '{"type": "string"}'
112
+ instance = '123'
113
+
114
+ begin
115
+ described_class.validate!(instance, schema)
116
+ raise 'Expected InstanceValidationError to be raised'
117
+ rescue JsonStructure::InstanceValidationError => e
118
+ expect(e.errors).not_to be_empty
119
+ expect(e.result).to be_invalid
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe JsonStructure::SchemaValidator do
6
+ describe '.validate' do
7
+ context 'with valid schema' do
8
+ it 'returns valid result for simple string schema' do
9
+ schema = '{"type": "string"}'
10
+ result = described_class.validate(schema)
11
+
12
+ expect(result).to be_valid
13
+ expect(result.error_messages).to be_empty # Only check errors, not warnings
14
+ end
15
+
16
+ it 'returns valid result for object schema' do
17
+ schema = '{"type": "object", "properties": {"name": {"type": "string"}}}'
18
+ result = described_class.validate(schema)
19
+
20
+ expect(result).to be_valid
21
+ expect(result.error_messages).to be_empty # Only check errors, not warnings
22
+ end
23
+
24
+ it 'returns valid result for array schema' do
25
+ schema = '{"type": "array", "items": {"type": "integer"}}'
26
+ result = described_class.validate(schema)
27
+
28
+ expect(result).to be_valid
29
+ expect(result.error_messages).to be_empty # Only check errors, not warnings
30
+ end
31
+ end
32
+
33
+ context 'with invalid schema' do
34
+ it 'returns invalid result for malformed JSON' do
35
+ schema = '{invalid json}'
36
+ result = described_class.validate(schema)
37
+
38
+ expect(result).to be_invalid
39
+ expect(result.errors).not_to be_empty
40
+ end
41
+
42
+ it 'returns invalid result for invalid type' do
43
+ schema = '{"type": "not_a_type"}'
44
+ result = described_class.validate(schema)
45
+
46
+ expect(result).to be_invalid
47
+ expect(result.errors).not_to be_empty
48
+ end
49
+ end
50
+
51
+ context 'error handling' do
52
+ it 'raises ArgumentError for non-string input' do
53
+ expect { described_class.validate(nil) }.to raise_error(ArgumentError)
54
+ expect { described_class.validate(123) }.to raise_error(ArgumentError)
55
+ expect { described_class.validate({}) }.to raise_error(ArgumentError)
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '.validate!' do
61
+ context 'with valid schema' do
62
+ it 'returns result without raising' do
63
+ schema = '{"type": "string"}'
64
+ result = described_class.validate!(schema)
65
+
66
+ expect(result).to be_valid
67
+ end
68
+ end
69
+
70
+ context 'with invalid schema' do
71
+ it 'raises SchemaValidationError' do
72
+ schema = '{invalid json}'
73
+
74
+ expect { described_class.validate!(schema) }.to raise_error(JsonStructure::SchemaValidationError)
75
+ end
76
+
77
+ it 'includes validation errors in exception' do
78
+ schema = '{invalid json}'
79
+
80
+ begin
81
+ described_class.validate!(schema)
82
+ raise 'Expected SchemaValidationError to be raised'
83
+ rescue JsonStructure::SchemaValidationError => e
84
+ expect(e.errors).not_to be_empty
85
+ expect(e.result).to be_invalid
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'jsonstructure'
4
+
5
+ RSpec.configure do |config|
6
+ # Enable flags like --only-failures and --next-failure
7
+ config.example_status_persistence_file_path = '.rspec_status'
8
+
9
+ # Disable RSpec exposing methods globally on `Module` and `main`
10
+ config.disable_monkey_patching!
11
+
12
+ config.expect_with :rspec do |c|
13
+ c.syntax = :expect
14
+ end
15
+ end