dry-schema-extensions 1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f043a132971608919006d6b21f62bb0f057b56511b9bc35c25d3eaa5000d0b19
4
+ data.tar.gz: b50c68c80aba6e494769c28cfdf10bacd77b5a777426c899cee91006671b6470
5
+ SHA512:
6
+ metadata.gz: 0acd59392bd0db95ad17013191e7610f21823ef9f5748d822ec89eca1e1bdc4a5e2570e2a6c820797a2736aec02b571bf211ef3504da853926b32474fdfc685d
7
+ data.tar.gz: 75c8127d2c0b38d08bf7da60dd30d339c3df4776ed01fbc3de6f5dfcf16cba88f8b90f305ab1dcd021a8507b270bb072827dc771d87b620ebe5cfb4f71ae31fb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2021 dry-rb team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # dry-schema-extensions
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/dry-schema-extensions.svg)][gem]
4
+ [![CI Status](https://github.com/ianks/dry-schema-extensions/workflows/CI/badge.svg)][actions]
5
+
6
+ ## Extensions Provided
7
+
8
+ * [Open API Extension](docsite/source/extensions/open_api.html.md)
9
+ * [JSON Schema Extension](docsite/source/extensions/json_schema.html.md)
10
+
11
+ ## Supported Ruby versions
12
+
13
+ This library officially supports the following Ruby versions:
14
+
15
+ * MRI `>= 2.6.0`
16
+ * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
17
+
18
+ ## License
19
+
20
+ See `LICENSE` file.
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
6
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "dry-schema-extensions"
10
+ spec.authors = ["Ian Ker-Seymer"]
11
+ spec.email = ["i.kerseymer@gmail.com"]
12
+ spec.license = "MIT"
13
+ spec.version = "1.0.0"
14
+
15
+ spec.summary = "OpenAPI and JSON Schema extension for dry-schema"
16
+ spec.description = <<~TEXT
17
+ Enhances dry-schema with the ability to export convert dry-schemas to OpenAPI definitions.
18
+ TEXT
19
+ spec.homepage = "https://dry-rb.org/gems/dry-schema"
20
+ spec.files = Dir["LICENSE", "README.md", "dry-schema-extensions.gemspec", "lib/**/*"]
21
+ spec.bindir = "bin"
22
+ spec.executables = []
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.required_ruby_version = ">= 2.6.0"
26
+
27
+ spec.add_runtime_dependency "dry-schema", ">= 1.6.1"
28
+
29
+ spec.add_development_dependency "bundler"
30
+ spec.add_development_dependency "rake"
31
+ spec.add_development_dependency "rspec"
32
+ 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
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/extensions/json_schema/schema_compiler"
4
+
5
+ module Dry
6
+ module Schema
7
+ # @api private
8
+ module OpenAPI
9
+ # @api private
10
+ class SchemaCompiler < JSONSchema::SchemaCompiler
11
+ UnknownConversionError = Class.new(StandardError)
12
+
13
+ def to_hash
14
+ transform_json_schema_hash!(super)
15
+ end
16
+
17
+ private
18
+
19
+ def transform_json_schema_hash!(hash)
20
+ hash.delete(:$schema)
21
+ transform_nullables!(hash)
22
+ end
23
+
24
+ def transform_nullables!(hash)
25
+ deep_transform_values!(hash) do |input|
26
+ next input unless input.respond_to?(:key?)
27
+ next input unless input[:type].respond_to?(:each)
28
+
29
+ types = input[:type]
30
+ input[:nullable] = true if types.delete("null")
31
+
32
+ if types.length == 1
33
+ input[:type] = types.first
34
+ input
35
+ else
36
+ raise UnknownConversionError, "cannot map type #{types.inspect}"
37
+ end
38
+ end
39
+ end
40
+
41
+ def deep_transform_values!(hash, &block)
42
+ case hash
43
+ when Hash
44
+ hash.transform_values! { |value| deep_transform_values!(yield(value), &block) }
45
+ when Array
46
+ hash.map! { |e| deep_transform_values!(e, &block) }
47
+ else
48
+ yield(hash)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/extensions/open_api/schema_compiler"
4
+
5
+ module Dry
6
+ module Schema
7
+ # JSONSchema extension
8
+ #
9
+ # @api public
10
+ module OpenAPI
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 open_api(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(OpenAPI::SchemaMethods)
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-schema"
4
+
5
+ Dry::Schema.register_extension(:json_schema) do
6
+ require "dry/schema/extensions/json_schema"
7
+ end
8
+
9
+ Dry::Schema.register_extension(:open_api) do
10
+ require "dry/schema/extensions/open_api"
11
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-schema-extensions
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ian Ker-Seymer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-schema
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.6.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.6.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: 'Enhances dry-schema with the ability to export convert dry-schemas to
70
+ OpenAPI definitions.
71
+
72
+ '
73
+ email:
74
+ - i.kerseymer@gmail.com
75
+ executables: []
76
+ extensions: []
77
+ extra_rdoc_files: []
78
+ files:
79
+ - LICENSE
80
+ - README.md
81
+ - dry-schema-extensions.gemspec
82
+ - lib/dry-schema-extensions.rb
83
+ - lib/dry/schema/extensions/json_schema.rb
84
+ - lib/dry/schema/extensions/json_schema/schema_compiler.rb
85
+ - lib/dry/schema/extensions/open_api.rb
86
+ - lib/dry/schema/extensions/open_api/schema_compiler.rb
87
+ homepage: https://dry-rb.org/gems/dry-schema
88
+ licenses:
89
+ - MIT
90
+ metadata: {}
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: 2.6.0
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.1.2
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: OpenAPI and JSON Schema extension for dry-schema
110
+ test_files: []