dry-schema 1.8.0 → 1.9.3

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -1
  3. data/README.md +4 -4
  4. data/dry-schema.gemspec +2 -2
  5. data/lib/dry/schema/compiler.rb +1 -1
  6. data/lib/dry/schema/dsl.rb +7 -4
  7. data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
  8. data/lib/dry/schema/extensions/hints.rb +11 -9
  9. data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
  10. data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +244 -0
  11. data/lib/dry/schema/extensions/json_schema.rb +29 -0
  12. data/lib/dry/schema/extensions/struct.rb +1 -1
  13. data/lib/dry/schema/extensions.rb +4 -0
  14. data/lib/dry/schema/key.rb +75 -74
  15. data/lib/dry/schema/key_coercer.rb +2 -2
  16. data/lib/dry/schema/key_validator.rb +44 -23
  17. data/lib/dry/schema/macros/array.rb +4 -0
  18. data/lib/dry/schema/macros/core.rb +1 -1
  19. data/lib/dry/schema/macros/dsl.rb +17 -15
  20. data/lib/dry/schema/macros/hash.rb +1 -1
  21. data/lib/dry/schema/macros/key.rb +2 -2
  22. data/lib/dry/schema/macros/schema.rb +2 -0
  23. data/lib/dry/schema/macros/value.rb +7 -0
  24. data/lib/dry/schema/message/or/multi_path.rb +73 -11
  25. data/lib/dry/schema/message/or.rb +2 -2
  26. data/lib/dry/schema/message_compiler.rb +13 -10
  27. data/lib/dry/schema/messages/i18n.rb +98 -96
  28. data/lib/dry/schema/messages/namespaced.rb +6 -0
  29. data/lib/dry/schema/messages/yaml.rb +165 -158
  30. data/lib/dry/schema/predicate.rb +2 -2
  31. data/lib/dry/schema/predicate_inferrer.rb +2 -0
  32. data/lib/dry/schema/primitive_inferrer.rb +2 -0
  33. data/lib/dry/schema/processor.rb +2 -2
  34. data/lib/dry/schema/result.rb +5 -7
  35. data/lib/dry/schema/trace.rb +5 -1
  36. data/lib/dry/schema/type_registry.rb +1 -2
  37. data/lib/dry/schema/version.rb +1 -1
  38. metadata +6 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc91fbbcd77a2535c39ac072f7725e2cc89cf40fb1710c1f7f2f32867d37351e
4
- data.tar.gz: a725bbc8ae7735fb576b827c5ee4dc00235e51287e7fb20882d8235fc61d72f5
3
+ metadata.gz: c2dead9a036c0d9322c68b50938417ebe12a4b62ed2f99a8ae6d4f49f894dabd
4
+ data.tar.gz: ac3b386e3266e8498aa62549cddfbaece9414281beee5710c1930b062eaacde8
5
5
  SHA512:
6
- metadata.gz: 1dcb14a96c4d9e1c38ca7bafaf04a621ee71cffc75bb4f3328d46752ec291f4ee580fea7f21bc4b430277af601ca31e29c5358d3c72d38ec53e128b411921245
7
- data.tar.gz: 27d66147c3be3d089d92b56ddec1b00a230d83e04606a52c14c10cb0f40648b8beb893f874e00916c5450769b1bdaf72c0df54e3e89464f96bc96986904fe465
6
+ metadata.gz: '0831055ac93da2898c55b6194703e87bda1c27040ac23965c5f261c2e59387888f679b9dcd967a4a0665cef220b7c97db823670067267b34b40ed09a3c315b81'
7
+ data.tar.gz: d82ee10469742e83ee412ebe6c06593a86c44c1e0f01fd29067700d9a37ecc461dc3e4ad6e2a09bfe4694d1e95b6c28132810bf243d27338257f66e0c50cc98e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,58 @@
1
1
  <!--- DO NOT EDIT THIS FILE - IT'S AUTOMATICALLY GENERATED VIA DEVTOOLS --->
2
2
 
3
+ ## 1.9.3 2022-06-23
4
+
5
+
6
+ ### Added
7
+
8
+ - Support `anyOf` composition in JSON schema output (@robhanlon22)
9
+
10
+ ### Fixed
11
+
12
+ - Allow composition of multiple ors (issue #307 fixed via #409) (@robhanlon22)
13
+
14
+
15
+ [Compare v1.9.2...v1.9.3](https://github.com/dry-rb/dry-schema/compare/v1.9.2...v1.9.3)
16
+
17
+ ## 1.9.2 2022-05-28
18
+
19
+
20
+ ### Fixed
21
+
22
+ - Fix loose JSON schemas for nested hashes (via #401) (@tomdalling)
23
+ - Correct spelling error 'mininum' to 'minimum' in json-schema extension (via #404) (@svenanderzen)
24
+
25
+ ### Changed
26
+
27
+ - [performance] YAML message backend allocates less strings (via #399) (@casperisfine)
28
+
29
+ [Compare v1.9.1...v1.9.2](https://github.com/dry-rb/dry-schema/compare/v1.9.1...v1.9.2)
30
+
31
+ ## 1.9.1 2022-02-17
32
+
33
+
34
+ ### Fixed
35
+
36
+ - Namespaced messages no longer crashes in certain scenarios (see dry-rb/dry-validation#692 fixed via #398) (@krekoten)
37
+
38
+
39
+ [Compare v1.9.0...v1.9.1](https://github.com/dry-rb/dry-schema/compare/v1.9.0...v1.9.1)
40
+
41
+ ## 1.9.0 2022-02-15
42
+
43
+
44
+ ### Added
45
+
46
+ - [EXPERIMENTAL] `json_schema` extension which allows you to convert a schema into a JSON schema (via #369) (@ianks)
47
+
48
+ ### Fixed
49
+
50
+ - Composing schemas no longer crashes in certain scenarios (issue #342 fixed via #366) (@vsuhachev)
51
+ - Fix info extension for typed arrays (issue #394 fixed via #397) (@CandyFet)
52
+
53
+
54
+ [Compare v1.8.0...v1.9.0](https://github.com/dry-rb/dry-schema/compare/v1.8.0...v1.9.0)
55
+
3
56
  ## 1.8.0 2021-09-12
4
57
 
5
58
 
@@ -121,7 +174,7 @@ This release ships with a bunch of internal refactorings that should improve per
121
174
  - Key validation works correctly with a non-nested maybe hashes (issue #311 fixed via #312) (@svobom57)
122
175
 
123
176
 
124
- [Compare v1.5.3...master](https://github.com/dry-rb/dry-schema/compare/v1.5.3...master)
177
+ [Compare v1.5.3...main](https://github.com/dry-rb/dry-schema/compare/v1.5.3...main)
125
178
 
126
179
  ## 1.5.3 2020-08-21
127
180
 
data/README.md CHANGED
@@ -8,10 +8,10 @@
8
8
  # dry-schema [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
9
9
 
10
10
  [![Gem Version](https://badge.fury.io/rb/dry-schema.svg)][gem]
11
- [![CI Status](https://github.com/dry-rb/dry-schema/workflows/CI/badge.svg)][actions]
11
+ [![CI Status](https://github.com/dry-rb/dry-schema/workflows/ci/badge.svg)][actions]
12
12
  [![Codacy Badge](https://api.codacy.com/project/badge/Grade/961f5c776f1d49218b2cede3745e059c)][codacy]
13
13
  [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/961f5c776f1d49218b2cede3745e059c)][codacy]
14
- [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=master)][inchpages]
14
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-schema.svg?branch=main)][inchpages]
15
15
 
16
16
  ## Links
17
17
 
@@ -22,8 +22,8 @@
22
22
 
23
23
  This library officially supports the following Ruby versions:
24
24
 
25
- * MRI `>= 2.6.0`
26
- * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
25
+ * MRI `>= 2.7.0`
26
+ * jruby `>= 9.3` (postponed until 2.7 is supported)
27
27
 
28
28
  ## License
29
29
 
data/dry-schema.gemspec CHANGED
@@ -27,11 +27,11 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ["lib"]
28
28
 
29
29
  spec.metadata["allowed_push_host"] = "https://rubygems.org"
30
- spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/master/CHANGELOG.md"
30
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-schema/blob/main/CHANGELOG.md"
31
31
  spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
32
32
  spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
33
33
 
34
- spec.required_ruby_version = ">= 2.6.0"
34
+ spec.required_ruby_version = ">= 2.7.0"
35
35
 
36
36
  # to update dependencies edit project.yml
37
37
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
@@ -47,7 +47,7 @@ module Dry
47
47
  # @return [Boolean]
48
48
  #
49
49
  # @api private
50
- def supports?(predicate)
50
+ def support?(predicate)
51
51
  predicates.key?(predicate)
52
52
  end
53
53
  end
@@ -80,8 +80,10 @@ module Dry
80
80
  # Build a new DSL object and evaluate provided block
81
81
  #
82
82
  # @param [Hash] options
83
- # @option options [Class] :processor The processor type (`Params`, `JSON` or a custom sub-class)
84
- # @option options [Compiler] :compiler An instance of a rule compiler (must be compatible with `Schema::Compiler`) (optional)
83
+ # @option options [Class] :processor The processor type
84
+ # (`Params`, `JSON` or a custom sub-class)
85
+ # @option options [Compiler] :compiler An instance of a rule compiler
86
+ # (must be compatible with `Schema::Compiler`) (optional)
85
87
  # @option options [Array[DSL]] :parent One or more instances of the parent DSL (optional)
86
88
  # @option options [Config] :config A configuration object (optional)
87
89
  #
@@ -168,7 +170,8 @@ module Dry
168
170
  # A generic method for defining keys
169
171
  #
170
172
  # @param [Symbol] name The key name
171
- # @param [Class] macro The macro sub-class (ie `Macros::Required` or any other `Macros::Key` subclass)
173
+ # @param [Class] macro The macro sub-class (ie `Macros::Required` or
174
+ # any other `Macros::Key` subclass)
172
175
  #
173
176
  # @return [Macros::Key]
174
177
  #
@@ -383,7 +386,7 @@ module Dry
383
386
  #
384
387
  # @api protected
385
388
  def rules
386
- parent_rules.merge(macros.map { |m| [m.name, m.to_rule] }.to_h.compact)
389
+ parent_rules.merge(macros.to_h { [_1.name, _1.to_rule] }.compact)
387
390
  end
388
391
 
389
392
  # Build a key map from defined types
@@ -35,12 +35,14 @@ module Dry
35
35
  end
36
36
 
37
37
  # @api private
38
+ # rubocop: disable Metrics/AbcSize
39
+ # rubocop: disable Metrics/PerceivedComplexity
40
+ # rubocop: disable Metrics/CyclomaticComplexity
38
41
  def exclude?(messages, opts)
39
42
  Array(messages).all? do |msg|
40
- hints = opts
41
- .hints
42
- .reject { |hint| msg == hint }
43
- .reject { |hint| hint.predicate == :filled? }
43
+ hints = opts.hints.reject { |h|
44
+ msg.eql?(h) || h.predicate.eql?(:filled?)
45
+ }
44
46
 
45
47
  key_failure = opts.key_failure?(msg.path)
46
48
  predicate = msg.predicate
@@ -52,6 +54,9 @@ module Dry
52
54
  HINT_OTHER_EXCLUSION.include?(predicate)
53
55
  end
54
56
  end
57
+ # rubocop: enable Metrics/CyclomaticComplexity
58
+ # rubocop: enable Metrics/PerceivedComplexity
59
+ # rubocop: enable Metrics/AbcSize
55
60
 
56
61
  # @api private
57
62
  def message_type(options)
@@ -22,17 +22,19 @@ module Dry
22
22
  # @see Message::Or
23
23
  #
24
24
  # @api public
25
- class Or::SinglePath
26
- # @api private
27
- def hint?
28
- false
25
+ module Or
26
+ class SinglePath
27
+ # @api private
28
+ def hint?
29
+ false
30
+ end
29
31
  end
30
- end
31
32
 
32
- class Or::MultiPath
33
- # @api private
34
- def hint?
35
- false
33
+ class MultiPath
34
+ # @api private
35
+ def hint?
36
+ false
37
+ end
36
38
  end
37
39
  end
38
40
 
@@ -96,7 +96,16 @@ module Dry
96
96
  keys[rest[0][1]] = {required: opts.fetch(:required, true)}
97
97
  else
98
98
  type = PREDICATE_TO_TYPE[name]
99
- keys[key][:type] = type if type
99
+ assign_type(key, type) if type
100
+ end
101
+ end
102
+
103
+ # @api private
104
+ def assign_type(key, type)
105
+ if keys[key][:type]
106
+ keys[key][:member] = type
107
+ else
108
+ keys[key][:type] = type
100
109
  end
101
110
  end
102
111
  end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/constants"
4
+
5
+ module Dry
6
+ module Schema
7
+ # @api private
8
+ module JSONSchema
9
+ # @api private
10
+ class SchemaCompiler
11
+ # An error raised when a predicate cannot be converted
12
+ UnknownConversionError = Class.new(StandardError)
13
+
14
+ IDENTITY = ->(v, _) { v }.freeze
15
+ TO_INTEGER = ->(v, _) { v.to_i }.freeze
16
+
17
+ PREDICATE_TO_TYPE = {
18
+ array?: {type: "array"},
19
+ bool?: {type: "boolean"},
20
+ date?: {type: "string", format: "date"},
21
+ date_time?: {type: "string", format: "date-time"},
22
+ decimal?: {type: "number"},
23
+ float?: {type: "number"},
24
+ hash?: {type: "object"},
25
+ int?: {type: "integer"},
26
+ nil?: {type: "null"},
27
+ str?: {type: "string"},
28
+ time?: {type: "string", format: "time"},
29
+ min_size?: {minLength: TO_INTEGER},
30
+ max_size?: {maxLength: TO_INTEGER},
31
+ included_in?: {enum: ->(v, _) { v.to_a }},
32
+ filled?: EMPTY_HASH,
33
+ uri?: {format: "uri"},
34
+ uuid_v1?: {
35
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-1[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
36
+ },
37
+ uuid_v2?: {
38
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-2[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
39
+ },
40
+ uuid_v3?: {
41
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
42
+ },
43
+ uuid_v4?: {
44
+ pattern: "^[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}$"
45
+ },
46
+ uuid_v5?: {
47
+ pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"
48
+ },
49
+ gt?: {exclusiveMinimum: IDENTITY},
50
+ gteq?: {minimum: IDENTITY},
51
+ lt?: {exclusiveMaximum: IDENTITY},
52
+ lteq?: {maximum: IDENTITY},
53
+ odd?: {type: "integer", not: {multipleOf: 2}},
54
+ even?: {type: "integer", multipleOf: 2}
55
+ }.freeze
56
+
57
+ # @api private
58
+ attr_reader :keys, :required
59
+
60
+ # @api private
61
+ def initialize(root: false, loose: false)
62
+ @keys = EMPTY_HASH.dup
63
+ @required = Set.new
64
+ @root = root
65
+ @loose = loose
66
+ end
67
+
68
+ # @api private
69
+ def to_hash
70
+ result = {}
71
+ result[:$schema] = "http://json-schema.org/draft-06/schema#" if root?
72
+ result.merge!(type: "object", properties: keys, required: required.to_a)
73
+ result
74
+ end
75
+
76
+ alias_method :to_h, :to_hash
77
+
78
+ # @api private
79
+ def call(ast)
80
+ visit(ast)
81
+ end
82
+
83
+ # @api private
84
+ def visit(node, opts = EMPTY_HASH)
85
+ meth, rest = node
86
+ public_send(:"visit_#{meth}", rest, opts)
87
+ end
88
+
89
+ # @api private
90
+ def visit_set(node, opts = EMPTY_HASH)
91
+ target = (key = opts[:key]) ? self.class.new(loose: loose?) : self
92
+
93
+ node.map { |child| target.visit(child, opts) }
94
+
95
+ return unless key
96
+
97
+ target_info = opts[:member] ? {items: target.to_h} : target.to_h
98
+ type = opts[:member] ? "array" : "object"
99
+
100
+ keys.update(key => {**keys[key], type: type, **target_info})
101
+ end
102
+
103
+ # @api private
104
+ def visit_and(node, opts = EMPTY_HASH)
105
+ left, right = node
106
+
107
+ # We need to know the type first to apply filled macro
108
+ if left[1][0] == :filled?
109
+ visit(right, opts)
110
+ visit(left, opts)
111
+ else
112
+ visit(left, opts)
113
+ visit(right, opts)
114
+ end
115
+ end
116
+
117
+ # @api private
118
+ def visit_or(node, opts = EMPTY_HASH)
119
+ node.each do |child|
120
+ c = self.class.new(loose: loose?)
121
+ c.keys.update(subschema: {})
122
+ c.visit(child, opts.merge(key: :subschema))
123
+
124
+ any_of = (keys[opts[:key]][:anyOf] ||= [])
125
+ any_of << c.keys[:subschema]
126
+ end
127
+ end
128
+
129
+ # @api private
130
+ def visit_implication(node, opts = EMPTY_HASH)
131
+ node.each do |el|
132
+ visit(el, **opts, required: false)
133
+ end
134
+ end
135
+
136
+ # @api private
137
+ def visit_each(node, opts = EMPTY_HASH)
138
+ visit(node, opts.merge(member: true))
139
+ end
140
+
141
+ # @api private
142
+ def visit_key(node, opts = EMPTY_HASH)
143
+ name, rest = node
144
+
145
+ if opts.fetch(:required, :true)
146
+ required << name.to_s
147
+ else
148
+ opts.delete(:required)
149
+ end
150
+
151
+ visit(rest, opts.merge(key: name))
152
+ end
153
+
154
+ # @api private
155
+ def visit_not(node, opts = EMPTY_HASH)
156
+ _name, rest = node
157
+
158
+ visit_predicate(rest, opts)
159
+ end
160
+
161
+ # @api private
162
+ def visit_predicate(node, opts = EMPTY_HASH)
163
+ name, rest = node
164
+
165
+ if name.equal?(:key?)
166
+ prop_name = rest[0][1]
167
+ keys[prop_name] = {}
168
+ else
169
+ target = keys[opts[:key]]
170
+ type_opts = fetch_type_opts_for_predicate(name, rest, target)
171
+
172
+ if target[:type]&.include?("array")
173
+ target[:items] ||= {}
174
+ merge_opts!(target[:items], type_opts)
175
+ else
176
+ merge_opts!(target, type_opts)
177
+ end
178
+ end
179
+ end
180
+
181
+ # @api private
182
+ def fetch_type_opts_for_predicate(name, rest, target)
183
+ type_opts = PREDICATE_TO_TYPE.fetch(name) do
184
+ raise_unknown_conversion_error!(:predicate, name) unless loose?
185
+
186
+ EMPTY_HASH
187
+ end.dup
188
+ type_opts.transform_values! { |v| v.respond_to?(:call) ? v.call(rest[0][1], target) : v }
189
+ type_opts.merge!(fetch_filled_options(target[:type], target)) if name == :filled?
190
+ type_opts
191
+ end
192
+
193
+ # @api private
194
+ def fetch_filled_options(type, _target)
195
+ case type
196
+ when "string"
197
+ {minLength: 1}
198
+ when "array"
199
+ raise_unknown_conversion_error!(:type, :array) unless loose?
200
+
201
+ {not: {type: "null"}}
202
+ else
203
+ {not: {type: "null"}}
204
+ end
205
+ end
206
+
207
+ # @api private
208
+ def merge_opts!(orig_opts, new_opts)
209
+ new_type = new_opts[:type]
210
+ orig_type = orig_opts[:type]
211
+
212
+ if orig_type && new_type && orig_type != new_type
213
+ new_opts[:type] = [orig_type, new_type]
214
+ end
215
+
216
+ orig_opts.merge!(new_opts)
217
+ end
218
+
219
+ # @api private
220
+ def root?
221
+ @root
222
+ end
223
+
224
+ # @api private
225
+ def loose?
226
+ @loose
227
+ end
228
+
229
+ def raise_unknown_conversion_error!(type, name)
230
+ message = <<~MSG
231
+ Could not find an equivalent conversion for #{type} #{name.inspect}.
232
+
233
+ This means that your generated JSON schema may be missing this validation.
234
+
235
+ You can ignore this by generating the schema in "loose" mode, i.e.:
236
+ my_schema.json_schema(loose: true)
237
+ MSG
238
+
239
+ raise UnknownConversionError, message.chomp
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/extensions/json_schema/schema_compiler"
4
+
5
+ module Dry
6
+ module Schema
7
+ # JSONSchema extension
8
+ #
9
+ # @api public
10
+ module JSONSchema
11
+ module SchemaMethods
12
+ # Convert the schema into a JSON schema hash
13
+ #
14
+ # @param [Symbol] loose Compile the schema in "loose" mode
15
+ #
16
+ # @return [Hash<Symbol=>Hash>]
17
+ #
18
+ # @api public
19
+ def json_schema(loose: false)
20
+ compiler = SchemaCompiler.new(root: true, loose: loose)
21
+ compiler.call(to_ast)
22
+ compiler.to_hash
23
+ end
24
+ end
25
+ end
26
+
27
+ Processor.include(JSONSchema::SchemaMethods)
28
+ end
29
+ end
@@ -18,7 +18,7 @@ module Dry
18
18
  end
19
19
 
20
20
  super(args[0].schema, *args.drop(1))
21
- type(schema_dsl.types[name].constructor(args[0]))
21
+ type(schema_dsl.types[name].constructor(args[0].schema))
22
22
  else
23
23
  super
24
24
  end
@@ -15,3 +15,7 @@ end
15
15
  Dry::Schema.register_extension(:info) do
16
16
  require "dry/schema/extensions/info"
17
17
  end
18
+
19
+ Dry::Schema.register_extension(:json_schema) do
20
+ require "dry/schema/extensions/json_schema"
21
+ end