schemurai 1.0.0 → 2.1.0

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.
data/lib/schemurai.rb CHANGED
@@ -1,28 +1,65 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "schemurai/version"
4
+ require_relative "schemurai/backend"
4
5
  require_relative "schemurai/formats"
5
6
  require_relative "schemurai/schema_graph"
6
7
  require_relative "schemurai/evaluator"
7
8
 
9
+ # Public API for compiling and evaluating JSON Schemas.
8
10
  module Schemurai
11
+ # Base class for errors raised by Schemurai.
9
12
  class Error < StandardError; end
13
+
14
+ # Raised when a schema or URI cannot be resolved from a registry.
10
15
  class ResolutionError < Error; end
16
+
17
+ # Raised when a schema requires a format assertion Schemurai does not support.
11
18
  class UnsupportedFormatError < Error; end
12
19
 
13
- ValidationError = Data.define(:keyword, :instance_path, :schema_path, :message) do
20
+ # Raised when a schema does not conform to its declared meta-schema.
21
+ class InvalidSchemaError < Error
22
+ # The detailed meta-schema validation result.
23
+ attr_reader :result
24
+
25
+ def initialize(result)
26
+ @result = result
27
+ first_error = result.errors.first
28
+ message = if first_error
29
+ "invalid schema at #{first_error.instance_path.inspect}: #{first_error.message}"
30
+ else
31
+ "invalid schema"
32
+ end
33
+ super(message)
34
+ end
35
+ end
36
+
37
+ # Public value type describing one failed JSON Schema assertion.
38
+ #
39
+ # +keyword+ identifies the failed keyword. +instance_path+ and +schema_path+
40
+ # are JSON Pointers; an empty string points to the root instance or schema.
41
+ # +message+ is a human-readable String. Its presence and type are public API,
42
+ # but its exact wording is not. Use +keyword+ and the paths for programmatic
43
+ # error handling.
44
+ class ValidationError < Data.define(:keyword, :instance_path, :schema_path, :message)
45
+ # Returns the error fields as a Hash.
14
46
  def to_h
15
47
  {keyword: keyword, instance_path: instance_path, schema_path: schema_path, message: message}
16
48
  end
17
49
  end
18
50
 
51
+ # The detailed result of validating one instance.
19
52
  class Result
53
+ # The frozen array of ValidationError objects. It is empty when validation
54
+ # succeeds.
20
55
  attr_reader :errors
21
56
 
57
+ # Creates a result from an array of ValidationError objects.
22
58
  def initialize(errors)
23
59
  @errors = errors.freeze
24
60
  end
25
61
 
62
+ # Returns whether the instance passed validation.
26
63
  def valid?
27
64
  errors.empty?
28
65
  end
@@ -30,57 +67,144 @@ module Schemurai
30
67
  alias_method :success?, :valid?
31
68
  end
32
69
 
70
+ # Compiles schemas that share registered external schemas and backend state.
71
+ #
72
+ # Register every external schema with +schemas:+ before compiling. A registry
73
+ # can be made shareable once registration and compilation are complete.
33
74
  class SchemaRegistry
34
- def initialize(schemas: {})
75
+ # The selected backend, either +:ruby+ or +:vm+.
76
+ attr_reader :backend
77
+
78
+ # Creates a registry.
79
+ #
80
+ # +schemas+ maps absolute URI strings to JSON-shaped Ruby schemas. Pass
81
+ # +validate_schema: true+ to validate every registered and compiled schema
82
+ # against its meta-schema. +backend+ accepts +:ruby+, +:vm+, or +:default+.
83
+ def initialize(schemas: {}, validate_schema: false, backend: Backend.requested)
84
+ @backend = Backend.resolve(backend)
85
+ @validate_schema = validate_schema
35
86
  @graph = Internal::SchemaGraph.new(schemas: schemas)
36
- @shareable = false
87
+ @compiler = VM::Compiler.new(@graph) if @backend == :vm
88
+ schemas.each_value { |schema| validate_schema!(schema) } if validate_schema?
37
89
  end
38
90
 
91
+ # Returns whether this registry validates every schema against its meta-schema.
92
+ def validate_schema?
93
+ @validate_schema
94
+ end
95
+
96
+ # Compiles +schema+ and returns a Validator.
97
+ #
98
+ # +base_uri+ supplies the schema's base URI. Pass +content: true+ or
99
+ # +format: true+ to enable the corresponding optional assertions.
39
100
  def compile(schema, base_uri: nil, content: false, format: false)
40
- raise Error, "cannot compile schemas after the registry is made shareable" if @shareable
101
+ raise Error, "cannot compile schemas after the registry is made shareable" if shareable?
41
102
 
103
+ validate_schema!(schema) if validate_schema?
42
104
  root = @graph.compile(schema, base_uri: base_uri)
43
- Validator.new(@graph, root, content: content, format: format)
105
+ build_validator(root, content: content, format: format)
106
+ end
107
+
108
+ # Validates +schema+ against its declared meta-schema and returns a Result.
109
+ # Schemas without +$schema+ are validated as Draft 7.
110
+ def validate_schema(schema)
111
+ raise Error, "cannot validate schemas after the registry is made shareable" if shareable?
112
+
113
+ root = @graph.meta_schema_root(schema)
114
+ build_validator(root, content: false, format: false).validate(schema)
44
115
  end
45
116
 
117
+ # Returns whether +schema+ conforms to its declared meta-schema.
118
+ def valid_schema?(schema)
119
+ validate_schema(schema).valid?
120
+ end
121
+
122
+ # Resolves and freezes registered schemas, then makes the registry Ractor-shareable.
123
+ #
124
+ # Returns +self+. No more schemas can be compiled after this transition.
46
125
  def make_shareable
47
- return self if @shareable
126
+ return self if shareable?
48
127
 
49
128
  @graph.make_shareable
50
- @shareable = true
129
+ @compiler&.compile_all
51
130
  Ractor.make_shareable(self)
52
131
  end
53
132
 
133
+ # Returns whether this registry is Ractor-shareable.
54
134
  def shareable?
55
- @shareable
135
+ Ractor.shareable?(self)
56
136
  end
57
137
 
138
+ # Returns a Validator for the registered resource at +uri+.
139
+ #
140
+ # The registry must first be made shareable with #make_shareable.
58
141
  def validator_for(uri, content: false, format: false)
59
- raise Error, "make_shareable must be called before retrieving validators by URI" unless @shareable
142
+ raise Error, "make_shareable must be called before retrieving validators by URI" unless shareable?
60
143
 
61
144
  root = @graph.node_at(uri)
62
145
  raise ResolutionError, "unregistered schema URI #{uri.inspect}" unless root
63
146
 
64
- Validator.new(@graph, root, content: content, format: format)
147
+ build_validator(root, content: content, format: format)
148
+ end
149
+
150
+ private def build_validator(root, content:, format:)
151
+ evaluator = if @compiler
152
+ program = @compiler.compile(root)
153
+ VM::Evaluator.new(@graph, @compiler, program, content: content, format: format)
154
+ else
155
+ Internal::Evaluator.new(@graph, root, content: content, format: format)
156
+ end
157
+ Validator.new(evaluator)
158
+ end
159
+
160
+ private def validate_schema!(schema)
161
+ result = validate_schema(schema)
162
+ raise InvalidSchemaError, result unless result.valid?
65
163
  end
66
164
  end
67
165
 
166
+ # A reusable compiled JSON Schema validator.
167
+ #
168
+ # Create validators through Schemurai.compile or SchemaRegistry#compile.
169
+ # Validator instances contain mutable evaluation state and must not be used
170
+ # concurrently or shared between Ractors.
68
171
  class Validator
69
- def initialize(graph, root, content:, format:)
70
- @evaluator = Internal::Evaluator.new(graph, root, content: content, format: format)
172
+ # Wraps an evaluator implementation.
173
+ #
174
+ # Applications should normally create validators through Schemurai.compile
175
+ # or SchemaRegistry#compile.
176
+ def initialize(evaluator)
177
+ @evaluator = evaluator
71
178
  end
72
179
 
180
+ # The backend reported by the evaluator, either +:ruby+ or +:vm+.
181
+ def backend
182
+ @evaluator.backend
183
+ end
184
+
185
+ # Validates +instance+ and returns a Result containing all reported errors.
73
186
  def validate(instance)
74
187
  @evaluator.validate(instance)
75
188
  end
76
189
 
190
+ # Returns whether +instance+ is valid without building a detailed error list.
77
191
  def valid?(instance)
78
192
  @evaluator.valid?(instance)
79
193
  end
80
194
  end
81
195
 
82
- module_function def compile(schema, schemas: {}, base_uri: nil, content: false, format: false)
83
- SchemaRegistry.new(schemas: schemas).compile(
196
+ # Returns the backend selected by SCHEMURAI_BACKEND, either +:ruby+ or +:vm+.
197
+ module_function def backend
198
+ Backend.resolve
199
+ end
200
+
201
+ # Compiles +schema+ and returns a reusable Validator.
202
+ #
203
+ # +schemas+ maps external URI strings to schemas. +base_uri+ supplies the
204
+ # schema's base URI. Optional content and format assertions are disabled by
205
+ # default. +backend+ accepts +:ruby+, +:vm+, or +:default+.
206
+ module_function def compile(schema, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
207
+ SchemaRegistry.new(schemas: schemas, validate_schema: validate_schema, backend: backend).compile(
84
208
  schema,
85
209
  base_uri: base_uri,
86
210
  content: content,
@@ -88,23 +212,48 @@ module Schemurai
88
212
  )
89
213
  end
90
214
 
91
- module_function def validate(schema, instance, schemas: {}, base_uri: nil, content: false, format: false)
215
+ # Validates +schema+ against its declared meta-schema and returns a Result.
216
+ # Schemas without +$schema+ are validated as Draft 7.
217
+ module_function def validate_schema(schema, schemas: {}, backend: Backend.requested)
218
+ SchemaRegistry.new(schemas: schemas, backend: backend).validate_schema(schema)
219
+ end
220
+
221
+ # Returns whether +schema+ conforms to its declared meta-schema.
222
+ module_function def valid_schema?(schema, schemas: {}, backend: Backend.requested)
223
+ validate_schema(schema, schemas: schemas, backend: backend).valid?
224
+ end
225
+
226
+ # Validates +instance+ against +schema+ and returns a Result.
227
+ #
228
+ # This convenience method compiles the schema for each call. Use .compile
229
+ # when validating multiple instances against the same schema.
230
+ module_function def validate(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
92
231
  compile(
93
232
  schema,
94
233
  schemas: schemas,
95
234
  base_uri: base_uri,
96
235
  content: content,
97
- format: format
236
+ format: format,
237
+ validate_schema: validate_schema,
238
+ backend: backend
98
239
  ).validate(instance)
99
240
  end
100
241
 
101
- module_function def valid?(schema, instance, schemas: {}, base_uri: nil, content: false, format: false)
242
+ # Returns whether +instance+ is valid against +schema+.
243
+ #
244
+ # This convenience method compiles the schema for each call. Use .compile
245
+ # when validating multiple instances against the same schema.
246
+ module_function def valid?(schema, instance, schemas: {}, base_uri: nil, content: false, format: false, validate_schema: false, backend: Backend.requested)
102
247
  compile(
103
248
  schema,
104
249
  schemas: schemas,
105
250
  base_uri: base_uri,
106
251
  content: content,
107
- format: format
252
+ format: format,
253
+ validate_schema: validate_schema,
254
+ backend: backend
108
255
  ).valid?(instance)
109
256
  end
257
+
258
+ private_constant :Backend
110
259
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schemurai
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuya KOHARA (oakcask)
@@ -29,16 +29,20 @@ email:
29
29
  - kuya.kohara@gmail.com
30
30
  executables: []
31
31
  extensions: []
32
- extra_rdoc_files: []
32
+ extra_rdoc_files:
33
+ - LICENSE
34
+ - README.md
33
35
  files:
34
36
  - LICENSE
35
37
  - README.md
36
38
  - lib/schemurai.rb
39
+ - lib/schemurai/backend.rb
37
40
  - lib/schemurai/dialect.rb
38
41
  - lib/schemurai/dialect_keywords.rb
39
42
  - lib/schemurai/dialects/draft2019_09.rb
40
43
  - lib/schemurai/dialects/draft2020_12.rb
41
44
  - lib/schemurai/dialects/draft7.rb
45
+ - lib/schemurai/error_message.rb
42
46
  - lib/schemurai/evaluation.rb
43
47
  - lib/schemurai/evaluator.rb
44
48
  - lib/schemurai/formats.rb
@@ -46,24 +50,36 @@ files:
46
50
  - lib/schemurai/meta_schemas/draft2019_09.rb
47
51
  - lib/schemurai/meta_schemas/draft2020_12.rb
48
52
  - lib/schemurai/meta_schemas/draft7.rb
53
+ - lib/schemurai/schema_domain.rb
49
54
  - lib/schemurai/schema_graph.rb
50
55
  - lib/schemurai/schema_node.rb
51
56
  - lib/schemurai/version.rb
57
+ - lib/schemurai/vm/compiler.rb
58
+ - lib/schemurai/vm/evaluator.rb
52
59
  homepage: https://github.com/oakcask/schemurai
53
60
  licenses:
54
61
  - MIT
55
62
  metadata:
56
63
  homepage_uri: https://github.com/oakcask/schemurai
57
64
  source_code_uri: https://github.com/oakcask/schemurai/tree/main
65
+ documentation_uri: https://rubydoc.info/gems/schemurai/2.1.0
58
66
  rubygems_mfa_required: 'true'
59
- rdoc_options: []
67
+ rdoc_options:
68
+ - "--main"
69
+ - README.md
70
+ - "--title"
71
+ - Schemurai API Documentation
72
+ - "--visibility"
73
+ - public
74
+ - "--exclude"
75
+ - lib/schemurai/(?!version\.rb)
60
76
  require_paths:
61
77
  - lib
62
78
  required_ruby_version: !ruby/object:Gem::Requirement
63
79
  requirements:
64
80
  - - ">="
65
81
  - !ruby/object:Gem::Version
66
- version: '3.2'
82
+ version: '3.4'
67
83
  required_rubygems_version: !ruby/object:Gem::Requirement
68
84
  requirements:
69
85
  - - ">="
@@ -72,5 +88,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
72
88
  requirements: []
73
89
  rubygems_version: 4.0.16
74
90
  specification_version: 4
75
- summary: A small, light-weight-dependency JSON Schema validator
91
+ summary: A fast, Ractor-safe, pure Ruby JSON Schema Draft 7+ validator
76
92
  test_files: []