dry-schema 1.8.0 → 1.9.3

Sign up to get free protection for your applications and to get access to all the features.
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