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