dry-schema 1.6.2 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -0
- data/README.md +4 -3
- data/dry-schema.gemspec +16 -14
- data/lib/dry/schema/compiler.rb +1 -1
- data/lib/dry/schema/config.rb +9 -9
- data/lib/dry/schema/dsl.rb +7 -4
- data/lib/dry/schema/extensions/hints/message_compiler_methods.rb +9 -4
- data/lib/dry/schema/extensions/hints.rb +11 -9
- data/lib/dry/schema/extensions/info/schema_compiler.rb +10 -1
- data/lib/dry/schema/extensions/json_schema/schema_compiler.rb +232 -0
- data/lib/dry/schema/extensions/json_schema.rb +29 -0
- data/lib/dry/schema/extensions/struct.rb +1 -1
- data/lib/dry/schema/extensions.rb +4 -0
- data/lib/dry/schema/key.rb +75 -70
- data/lib/dry/schema/key_coercer.rb +2 -2
- data/lib/dry/schema/key_validator.rb +46 -20
- data/lib/dry/schema/macros/array.rb +4 -0
- data/lib/dry/schema/macros/core.rb +1 -1
- data/lib/dry/schema/macros/dsl.rb +17 -15
- data/lib/dry/schema/macros/hash.rb +1 -1
- data/lib/dry/schema/macros/key.rb +2 -2
- data/lib/dry/schema/macros/schema.rb +2 -0
- data/lib/dry/schema/macros/value.rb +13 -1
- data/lib/dry/schema/message/or/multi_path.rb +7 -5
- data/lib/dry/schema/message_compiler.rb +13 -10
- data/lib/dry/schema/messages/abstract.rb +9 -9
- data/lib/dry/schema/messages/i18n.rb +98 -96
- data/lib/dry/schema/messages/namespaced.rb +1 -0
- data/lib/dry/schema/messages/yaml.rb +165 -151
- data/lib/dry/schema/path.rb +10 -60
- data/lib/dry/schema/predicate.rb +2 -2
- data/lib/dry/schema/predicate_inferrer.rb +2 -0
- data/lib/dry/schema/primitive_inferrer.rb +2 -0
- data/lib/dry/schema/processor.rb +6 -6
- data/lib/dry/schema/processor_steps.rb +7 -3
- data/lib/dry/schema/result.rb +38 -31
- data/lib/dry/schema/step.rb +14 -33
- data/lib/dry/schema/trace.rb +5 -1
- data/lib/dry/schema/type_registry.rb +1 -2
- data/lib/dry/schema/version.rb +1 -1
- metadata +11 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 630c8c7019a02d6f22386ef4ac1ca68321af93bd3a5e34f8b62b21d28a7eee69
|
4
|
+
data.tar.gz: e53baa861af349395446deb0183e7358438e4ca4ea7fa78b25d4c4f04e4ea2a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ed11d1eba31d808c75018d5ea78fc649f4f59486c2047554aeabac5cd71e9b2d6e64b88661c8f0615b9a9402c177d2c51f49cd49e3ae6e697c45f9f952dab18
|
7
|
+
data.tar.gz: 5c085c4994f662a44eb372e92a894c76d83eefdce88389ed6c1947004c7bd39204b843ea5eb408af86ad986e6978dde67227b9bdcf7d888d1a8e277b457c4345
|
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.0 2022-02-15
|
4
|
+
|
5
|
+
|
6
|
+
### Added
|
7
|
+
|
8
|
+
- [EXPERIMENTAL] `json_schema` extension which allows you to convert a schema into a JSON schema (via #369) (@ianks)
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
|
12
|
+
- Composing schemas no longer crashes in certain scenarios (issue #342 fixed via #366) (@vsuhachev)
|
13
|
+
- Fix info extension for typed arrays (issue #394 fixed via #397) (@CandyFet)
|
14
|
+
|
15
|
+
|
16
|
+
[Compare v1.8.0...v1.9.0](https://github.com/dry-rb/dry-schema/compare/v1.8.0...v1.9.0)
|
17
|
+
|
18
|
+
## 1.8.0 2021-09-12
|
19
|
+
|
20
|
+
|
21
|
+
### Changed
|
22
|
+
|
23
|
+
- [internal] Upgraded to new `setting` API provided in dry-configurable 0.13.0 (@timriley in #356)
|
24
|
+
|
25
|
+
[Compare v1.7.1...v1.8.0](https://github.com/dry-rb/dry-schema/compare/v1.7.1...v1.8.0)
|
26
|
+
|
27
|
+
## 1.7.1 2021-08-29
|
28
|
+
|
29
|
+
|
30
|
+
### Changed
|
31
|
+
|
32
|
+
- [internal] Use explicit `#to_h` conversion of Dry::Configurable::Config, to ensure compatibility with upcoming dry-configurable 0.13.0 release (via #371) (@timriley)
|
33
|
+
|
34
|
+
[Compare v1.7.0...v1.7.1](https://github.com/dry-rb/dry-schema/compare/v1.7.0...v1.7.1)
|
35
|
+
|
36
|
+
## 1.7.0 2021-06-29
|
37
|
+
|
38
|
+
This release ships with a bunch of internal refactorings that should improve performance but if you see any unexpected behavior please do report issues.
|
39
|
+
|
40
|
+
### Fixed
|
41
|
+
|
42
|
+
- Handle arrays of hashes where Array constructor coerces non-Hash input (#351 fixed via #354) (@ojab)
|
43
|
+
- Run outer schema processor steps before inner ones (issue #350 fixed via #361) (@ojab)
|
44
|
+
- Fix key validator false negatives on empty collections (see #363) (@Drenmi)
|
45
|
+
- Prevent error message YAML files from being parsed multiple times (issue #352 via #364) (@alassek)
|
46
|
+
- Using constructor types should work fine now ie `required(:foo).filled(Types::Params::Integer.constructor(&:succ))` (issue #280 fixed via #365) (@solnic)
|
47
|
+
- Handle non-Hash to Hash transformation in `before(:key_coercer)` (issue #350 fixed via #362) (@ojab)
|
48
|
+
|
49
|
+
### Changed
|
50
|
+
|
51
|
+
- [internal] `Dry::Schema::Path` clean up and performance improvements (via #358) (@ojab)
|
52
|
+
- [internal] simplify and speed up handling of steps in nested schemas (via #360) (@ojab)
|
53
|
+
|
54
|
+
[Compare v1.6.2...v1.7.0](https://github.com/dry-rb/dry-schema/compare/v1.6.2...v1.7.0)
|
55
|
+
|
3
56
|
## 1.6.2 2021-04-15
|
4
57
|
|
5
58
|
|
data/README.md
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
<!--- this file is synced from dry-rb/template-gem project -->
|
1
2
|
[gem]: https://rubygems.org/gems/dry-schema
|
2
3
|
[actions]: https://github.com/dry-rb/dry-schema/actions
|
3
4
|
[codacy]: https://www.codacy.com/gh/dry-rb/dry-schema
|
@@ -14,15 +15,15 @@
|
|
14
15
|
|
15
16
|
## Links
|
16
17
|
|
17
|
-
* [User documentation](
|
18
|
+
* [User documentation](https://dry-rb.org/gems/dry-schema)
|
18
19
|
* [API documentation](http://rubydoc.info/gems/dry-schema)
|
19
20
|
|
20
21
|
## Supported Ruby versions
|
21
22
|
|
22
23
|
This library officially supports the following Ruby versions:
|
23
24
|
|
24
|
-
* MRI
|
25
|
-
* jruby
|
25
|
+
* MRI `>= 2.7.0`
|
26
|
+
* jruby `>= 9.3` (postponed until 2.7 is supported)
|
26
27
|
|
27
28
|
## License
|
28
29
|
|
data/dry-schema.gemspec
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# this file is managed by dry-rb/devtools project
|
3
2
|
|
4
|
-
|
3
|
+
# this file is synced from dry-rb/template-gem project
|
4
|
+
|
5
|
+
lib = File.expand_path("lib", __dir__)
|
5
6
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
-
require
|
7
|
+
require "dry/schema/version"
|
7
8
|
|
8
9
|
Gem::Specification.new do |spec|
|
9
|
-
spec.name =
|
10
|
+
spec.name = "dry-schema"
|
10
11
|
spec.authors = ["Piotr Solnica"]
|
11
12
|
spec.email = ["piotr.solnica@gmail.com"]
|
12
|
-
spec.license =
|
13
|
+
spec.license = "MIT"
|
13
14
|
spec.version = Dry::Schema::VERSION.dup
|
14
15
|
|
15
16
|
spec.summary = "Coercion and validation for data structures"
|
@@ -17,23 +18,24 @@ Gem::Specification.new do |spec|
|
|
17
18
|
dry-schema provides a DSL for defining schemas with keys and rules that should be applied to
|
18
19
|
values. It supports coercion, input sanitization, custom types and localized error messages
|
19
20
|
(with or without I18n gem). It's also used as the schema engine in dry-validation.
|
21
|
+
|
20
22
|
TEXT
|
21
|
-
spec.homepage =
|
23
|
+
spec.homepage = "https://dry-rb.org/gems/dry-schema"
|
22
24
|
spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-schema.gemspec", "lib/**/*", "config/*.yml"]
|
23
|
-
spec.bindir =
|
25
|
+
spec.bindir = "bin"
|
24
26
|
spec.executables = []
|
25
|
-
spec.require_paths = [
|
27
|
+
spec.require_paths = ["lib"]
|
26
28
|
|
27
|
-
spec.metadata[
|
28
|
-
spec.metadata[
|
29
|
-
spec.metadata[
|
30
|
-
spec.metadata[
|
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"
|
31
|
+
spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-schema"
|
32
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-schema/issues"
|
31
33
|
|
32
|
-
spec.required_ruby_version = ">= 2.
|
34
|
+
spec.required_ruby_version = ">= 2.7.0"
|
33
35
|
|
34
36
|
# to update dependencies edit project.yml
|
35
37
|
spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
|
36
|
-
spec.add_runtime_dependency "dry-configurable", "~> 0.
|
38
|
+
spec.add_runtime_dependency "dry-configurable", "~> 0.13", ">= 0.13.0"
|
37
39
|
spec.add_runtime_dependency "dry-core", "~> 0.5", ">= 0.5"
|
38
40
|
spec.add_runtime_dependency "dry-initializer", "~> 3.0"
|
39
41
|
spec.add_runtime_dependency "dry-logic", "~> 1.0"
|
data/lib/dry/schema/compiler.rb
CHANGED
data/lib/dry/schema/config.rb
CHANGED
@@ -25,7 +25,7 @@ module Dry
|
|
25
25
|
# @return [Schema::PredicateRegistry]
|
26
26
|
#
|
27
27
|
# @api public
|
28
|
-
setting
|
28
|
+
setting :predicates, default: Schema::PredicateRegistry.new
|
29
29
|
|
30
30
|
# @!method types
|
31
31
|
#
|
@@ -34,7 +34,7 @@ module Dry
|
|
34
34
|
# @return [Hash]
|
35
35
|
#
|
36
36
|
# @api public
|
37
|
-
setting
|
37
|
+
setting :types, default: Dry::Types
|
38
38
|
|
39
39
|
# @!method messages
|
40
40
|
#
|
@@ -43,12 +43,12 @@ module Dry
|
|
43
43
|
# @return [Dry::Configurable::Config]
|
44
44
|
#
|
45
45
|
# @api public
|
46
|
-
setting
|
47
|
-
setting
|
48
|
-
setting
|
49
|
-
setting
|
50
|
-
setting
|
51
|
-
setting
|
46
|
+
setting :messages do
|
47
|
+
setting :backend, default: :yaml
|
48
|
+
setting :namespace
|
49
|
+
setting :load_paths, default: Set[DEFAULT_MESSAGES_PATH], constructor: :dup.to_proc
|
50
|
+
setting :top_namespace, default: DEFAULT_MESSAGES_ROOT
|
51
|
+
setting :default_locale
|
52
52
|
end
|
53
53
|
|
54
54
|
# @!method validate_keys
|
@@ -58,7 +58,7 @@ module Dry
|
|
58
58
|
# @return [Boolean]
|
59
59
|
#
|
60
60
|
# @api public
|
61
|
-
setting
|
61
|
+
setting :validate_keys, default: false
|
62
62
|
|
63
63
|
# @api private
|
64
64
|
def respond_to_missing?(meth, include_private = false)
|
data/lib/dry/schema/dsl.rb
CHANGED
@@ -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
|
84
|
-
#
|
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
|
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.
|
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
|
-
.
|
42
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
module Or
|
26
|
+
class SinglePath
|
27
|
+
# @api private
|
28
|
+
def hint?
|
29
|
+
false
|
30
|
+
end
|
29
31
|
end
|
30
|
-
end
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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,232 @@
|
|
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?: {mininum: 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 : 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_implication(node, opts = EMPTY_HASH)
|
119
|
+
node.each do |el|
|
120
|
+
visit(el, **opts, required: false)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# @api private
|
125
|
+
def visit_each(node, opts = EMPTY_HASH)
|
126
|
+
visit(node, opts.merge(member: true))
|
127
|
+
end
|
128
|
+
|
129
|
+
# @api private
|
130
|
+
def visit_key(node, opts = EMPTY_HASH)
|
131
|
+
name, rest = node
|
132
|
+
|
133
|
+
if opts.fetch(:required, :true)
|
134
|
+
required << name.to_s
|
135
|
+
else
|
136
|
+
opts.delete(:required)
|
137
|
+
end
|
138
|
+
|
139
|
+
visit(rest, opts.merge(key: name))
|
140
|
+
end
|
141
|
+
|
142
|
+
# @api private
|
143
|
+
def visit_not(node, opts = EMPTY_HASH)
|
144
|
+
_name, rest = node
|
145
|
+
|
146
|
+
visit_predicate(rest, opts)
|
147
|
+
end
|
148
|
+
|
149
|
+
# @api private
|
150
|
+
def visit_predicate(node, opts = EMPTY_HASH)
|
151
|
+
name, rest = node
|
152
|
+
|
153
|
+
if name.equal?(:key?)
|
154
|
+
prop_name = rest[0][1]
|
155
|
+
keys[prop_name] = {}
|
156
|
+
else
|
157
|
+
target = keys[opts[:key]]
|
158
|
+
type_opts = fetch_type_opts_for_predicate(name, rest, target)
|
159
|
+
|
160
|
+
if target[:type]&.include?("array")
|
161
|
+
target[:items] ||= {}
|
162
|
+
merge_opts!(target[:items], type_opts)
|
163
|
+
else
|
164
|
+
merge_opts!(target, type_opts)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# @api private
|
170
|
+
def fetch_type_opts_for_predicate(name, rest, target)
|
171
|
+
type_opts = PREDICATE_TO_TYPE.fetch(name) do
|
172
|
+
raise_unknown_conversion_error!(:predicate, name) unless loose?
|
173
|
+
|
174
|
+
EMPTY_HASH
|
175
|
+
end.dup
|
176
|
+
type_opts.transform_values! { |v| v.respond_to?(:call) ? v.call(rest[0][1], target) : v }
|
177
|
+
type_opts.merge!(fetch_filled_options(target[:type], target)) if name == :filled?
|
178
|
+
type_opts
|
179
|
+
end
|
180
|
+
|
181
|
+
# @api private
|
182
|
+
def fetch_filled_options(type, _target)
|
183
|
+
case type
|
184
|
+
when "string"
|
185
|
+
{minLength: 1}
|
186
|
+
when "array"
|
187
|
+
raise_unknown_conversion_error!(:type, :array) unless loose?
|
188
|
+
|
189
|
+
{not: {type: "null"}}
|
190
|
+
else
|
191
|
+
{not: {type: "null"}}
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# @api private
|
196
|
+
def merge_opts!(orig_opts, new_opts)
|
197
|
+
new_type = new_opts[:type]
|
198
|
+
orig_type = orig_opts[:type]
|
199
|
+
|
200
|
+
if orig_type && new_type && orig_type != new_type
|
201
|
+
new_opts[:type] = [orig_type, new_type]
|
202
|
+
end
|
203
|
+
|
204
|
+
orig_opts.merge!(new_opts)
|
205
|
+
end
|
206
|
+
|
207
|
+
# @api private
|
208
|
+
def root?
|
209
|
+
@root
|
210
|
+
end
|
211
|
+
|
212
|
+
# @api private
|
213
|
+
def loose?
|
214
|
+
@loose
|
215
|
+
end
|
216
|
+
|
217
|
+
def raise_unknown_conversion_error!(type, name)
|
218
|
+
message = <<~MSG
|
219
|
+
Could not find an equivalent conversion for #{type} #{name.inspect}.
|
220
|
+
|
221
|
+
This means that your generated JSON schema may be missing this validation.
|
222
|
+
|
223
|
+
You can ignore this by generating the schema in "loose" mode, i.e.:
|
224
|
+
my_schema.json_schema(loose: true)
|
225
|
+
MSG
|
226
|
+
|
227
|
+
raise UnknownConversionError, message.chomp
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
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
|