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
|
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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
|