dry-schema-extensions 1.0.0

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