dry-types-json-schema 0.0.1

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: 33b4fa74b0afcb773080ea49a615812cb32e2d23fc0a498a5f5f8e3a5d729532
4
+ data.tar.gz: d3cddcc29c63e0cdbef859ce7259155cc038e40374690b58c9e5eafaa103dd08
5
+ SHA512:
6
+ metadata.gz: 8b10cc09ab9ff064cf2d1fabaa859e1ca4442e247b368f3aa2c73c8f54f46e2b958898549d91c5bdf0884b97914249646a76280deddec3e0a1d7aa9243961bed
7
+ data.tar.gz: 380965c43395d6742b673f9926d092fe746a593277b027edf4c90ee3f3995893ce94c838d1257d8e9eaa573dfc26ae78f304075e93d4ad4375c753c1a9397668
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ coverage
data/.rubocop.yml ADDED
@@ -0,0 +1,264 @@
1
+ require:
2
+ - rubocop-minitest
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 3.0
6
+ NewCops: disable
7
+
8
+ Layout/SpaceAroundMethodCallOperator:
9
+ Enabled: false
10
+
11
+ Layout/SpaceInLambdaLiteral:
12
+ Enabled: false
13
+
14
+ Layout/MultilineMethodCallIndentation:
15
+ Enabled: true
16
+ EnforcedStyle: indented
17
+
18
+ Layout/FirstArrayElementIndentation:
19
+ EnforcedStyle: consistent
20
+
21
+ Layout/SpaceInsideHashLiteralBraces:
22
+ Enabled: true
23
+
24
+ Layout/LineLength:
25
+ Max: 100
26
+ Exclude:
27
+ - "spec/**/*.rb"
28
+
29
+ Layout/HashAlignment:
30
+ Enabled: false
31
+
32
+ Lint/AmbiguousBlockAssociation:
33
+ Enabled: true
34
+ # because 'expect { foo }.to change { bar }' is fine
35
+ Exclude:
36
+ - "spec/**/*.rb"
37
+
38
+ Lint/BooleanSymbol:
39
+ Enabled: false
40
+
41
+ Lint/ConstantDefinitionInBlock:
42
+ Exclude:
43
+ - "spec/**/*.rb"
44
+
45
+ Lint/RaiseException:
46
+ Enabled: false
47
+
48
+ Lint/StructNewOverride:
49
+ Enabled: false
50
+
51
+ Lint/SuppressedException:
52
+ Exclude:
53
+ - "spec/spec_helper.rb"
54
+
55
+ Lint/LiteralAsCondition:
56
+ Exclude:
57
+ - "spec/**/*.rb"
58
+
59
+ Naming/PredicateName:
60
+ Enabled: false
61
+
62
+ Naming/FileName:
63
+ Exclude:
64
+ - "lib/*-*.rb"
65
+
66
+ Naming/MethodName:
67
+ Enabled: false
68
+
69
+ Naming/MethodParameterName:
70
+ Enabled: false
71
+
72
+ Naming/MemoizedInstanceVariableName:
73
+ Enabled: false
74
+
75
+ Metrics/MethodLength:
76
+ Enabled: false
77
+
78
+ Metrics/ClassLength:
79
+ Enabled: false
80
+
81
+ Metrics/BlockLength:
82
+ Enabled: false
83
+
84
+ Metrics/AbcSize:
85
+ Max: 25
86
+
87
+ Metrics/CyclomaticComplexity:
88
+ Enabled: true
89
+ Max: 12
90
+
91
+ Style/ExponentialNotation:
92
+ Enabled: false
93
+
94
+ Style/HashEachMethods:
95
+ Enabled: false
96
+
97
+ Style/HashTransformKeys:
98
+ Enabled: false
99
+
100
+ Style/HashTransformValues:
101
+ Enabled: false
102
+
103
+ Style/AccessModifierDeclarations:
104
+ Enabled: false
105
+
106
+ Style/Alias:
107
+ Enabled: true
108
+ EnforcedStyle: prefer_alias_method
109
+
110
+ Style/AsciiComments:
111
+ Enabled: false
112
+
113
+ Style/BlockDelimiters:
114
+ Enabled: false
115
+
116
+ Style/ClassAndModuleChildren:
117
+ Exclude:
118
+ - "spec/**/*.rb"
119
+
120
+ Style/ConditionalAssignment:
121
+ Enabled: false
122
+
123
+ Style/DateTime:
124
+ Enabled: false
125
+
126
+ Style/Documentation:
127
+ Enabled: false
128
+
129
+ Style/EachWithObject:
130
+ Enabled: false
131
+
132
+ Style/FormatString:
133
+ Enabled: false
134
+
135
+ Style/FormatStringToken:
136
+ Enabled: false
137
+
138
+ Style/GuardClause:
139
+ Enabled: false
140
+
141
+ Style/IfUnlessModifier:
142
+ Enabled: false
143
+
144
+ Style/Lambda:
145
+ Enabled: false
146
+
147
+ Style/LambdaCall:
148
+ Enabled: false
149
+
150
+ Style/ParallelAssignment:
151
+ Enabled: false
152
+
153
+ Style/RaiseArgs:
154
+ Enabled: false
155
+
156
+ Style/StabbyLambdaParentheses:
157
+ Enabled: false
158
+
159
+ Style/StringLiterals:
160
+ Enabled: true
161
+ EnforcedStyle: double_quotes
162
+ ConsistentQuotesInMultiline: false
163
+
164
+ Style/StringLiteralsInInterpolation:
165
+ Enabled: true
166
+ EnforcedStyle: double_quotes
167
+
168
+ Style/SymbolArray:
169
+ Exclude:
170
+ - "spec/**/*.rb"
171
+
172
+ Style/TrailingUnderscoreVariable:
173
+ Enabled: false
174
+
175
+ Style/MultipleComparison:
176
+ Enabled: false
177
+
178
+ Style/Next:
179
+ Enabled: false
180
+
181
+ Style/AccessorGrouping:
182
+ Enabled: false
183
+
184
+ Style/EmptyLiteral:
185
+ Enabled: false
186
+
187
+ Style/Semicolon:
188
+ Exclude:
189
+ - "spec/**/*.rb"
190
+
191
+ Style/HashAsLastArrayItem:
192
+ Exclude:
193
+ - "spec/**/*.rb"
194
+
195
+ Style/CaseEquality:
196
+ Exclude:
197
+ - "lib/dry/monads/**/*.rb"
198
+ - "lib/dry/struct/**/*.rb"
199
+ - "lib/dry/types/**/*.rb"
200
+ - "spec/**/*.rb"
201
+
202
+ Style/ExplicitBlockArgument:
203
+ Exclude:
204
+ - "lib/dry/types/**/*.rb"
205
+
206
+ Style/CombinableLoops:
207
+ Enabled: false
208
+
209
+ Style/EmptyElse:
210
+ Enabled: false
211
+
212
+ Style/DoubleNegation:
213
+ Enabled: false
214
+
215
+ Style/MultilineBlockChain:
216
+ Enabled: false
217
+
218
+ Style/NumberedParametersLimit:
219
+ Max: 2
220
+
221
+ Lint/UnusedBlockArgument:
222
+ Exclude:
223
+ - "spec/**/*.rb"
224
+
225
+ Lint/Debugger:
226
+ Exclude:
227
+ - "bin/console"
228
+
229
+ Lint/BinaryOperatorWithIdenticalOperands:
230
+ Exclude:
231
+ - "spec/**/*.rb"
232
+
233
+ Metrics/ParameterLists:
234
+ Exclude:
235
+ - "spec/**/*.rb"
236
+
237
+ Lint/EmptyBlock:
238
+ Exclude:
239
+ - "spec/**/*.rb"
240
+
241
+ Lint/EmptyFile:
242
+ Exclude:
243
+ - "spec/**/*.rb"
244
+
245
+ Lint/UselessMethodDefinition:
246
+ Exclude:
247
+ - "spec/**/*.rb"
248
+
249
+ Lint/SelfAssignment:
250
+ Enabled: false
251
+
252
+ Lint/EmptyClass:
253
+ Enabled: false
254
+
255
+ Naming/ConstantName:
256
+ Exclude:
257
+ - "spec/**/*.rb"
258
+
259
+ Naming/VariableNumber:
260
+ Exclude:
261
+ - "spec/**/*.rb"
262
+
263
+ Naming/BinaryOperatorParameterName:
264
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,105 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dry-types-json-schema (0.0.1)
5
+ dry-types (~> 1.7.2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ base64 (0.2.0)
12
+ bigdecimal (3.1.7)
13
+ coderay (1.1.3)
14
+ concurrent-ruby (1.2.3)
15
+ docile (1.4.0)
16
+ dry-core (1.0.1)
17
+ concurrent-ruby (~> 1.0)
18
+ zeitwerk (~> 2.6)
19
+ dry-inflector (1.0.0)
20
+ dry-logic (1.5.0)
21
+ concurrent-ruby (~> 1.0)
22
+ dry-core (~> 1.0, < 2)
23
+ zeitwerk (~> 2.6)
24
+ dry-struct (1.6.0)
25
+ dry-core (~> 1.0, < 2)
26
+ dry-types (>= 1.7, < 2)
27
+ ice_nine (~> 0.11)
28
+ zeitwerk (~> 2.6)
29
+ dry-types (1.7.2)
30
+ bigdecimal (~> 3.0)
31
+ concurrent-ruby (~> 1.0)
32
+ dry-core (~> 1.0)
33
+ dry-inflector (~> 1.0)
34
+ dry-logic (~> 1.4)
35
+ zeitwerk (~> 2.6)
36
+ hana (1.3.7)
37
+ ice_nine (0.11.2)
38
+ json (2.7.1)
39
+ json_schemer (2.2.1)
40
+ base64
41
+ bigdecimal
42
+ hana (~> 1.3)
43
+ regexp_parser (~> 2.0)
44
+ simpleidn (~> 0.2)
45
+ language_server-protocol (3.17.0.3)
46
+ method_source (1.0.0)
47
+ minitest (5.22.3)
48
+ parallel (1.24.0)
49
+ parser (3.3.0.5)
50
+ ast (~> 2.4.1)
51
+ racc
52
+ pry (0.14.2)
53
+ coderay (~> 1.1)
54
+ method_source (~> 1.0)
55
+ racc (1.7.3)
56
+ rainbow (3.1.1)
57
+ regexp_parser (2.9.0)
58
+ rexml (3.2.6)
59
+ rubocop (1.62.1)
60
+ json (~> 2.3)
61
+ language_server-protocol (>= 3.17.0)
62
+ parallel (~> 1.10)
63
+ parser (>= 3.3.0.2)
64
+ rainbow (>= 2.2.2, < 4.0)
65
+ regexp_parser (>= 1.8, < 3.0)
66
+ rexml (>= 3.2.5, < 4.0)
67
+ rubocop-ast (>= 1.31.1, < 2.0)
68
+ ruby-progressbar (~> 1.7)
69
+ unicode-display_width (>= 2.4.0, < 3.0)
70
+ rubocop-ast (1.31.2)
71
+ parser (>= 3.3.0.4)
72
+ rubocop-minitest (0.35.0)
73
+ rubocop (>= 1.61, < 2.0)
74
+ rubocop-ast (>= 1.31.1, < 2.0)
75
+ ruby-progressbar (1.13.0)
76
+ simplecov (0.22.0)
77
+ docile (~> 1.1)
78
+ simplecov-html (~> 0.11)
79
+ simplecov_json_formatter (~> 0.1)
80
+ simplecov-html (0.12.3)
81
+ simplecov_json_formatter (0.1.4)
82
+ simpleidn (0.2.1)
83
+ unf (~> 0.1.4)
84
+ unf (0.1.4)
85
+ unf_ext
86
+ unf_ext (0.0.9.1)
87
+ unicode-display_width (2.5.0)
88
+ zeitwerk (2.6.13)
89
+
90
+ PLATFORMS
91
+ arm64-darwin-22
92
+ ruby
93
+
94
+ DEPENDENCIES
95
+ dry-struct (~> 1.6.0)
96
+ dry-types-json-schema!
97
+ json_schemer (~> 2.2.1)
98
+ minitest (~> 5.22.3)
99
+ pry (~> 0.14.2)
100
+ rubocop (~> 1.62.1)
101
+ rubocop-minitest (~> 0.35.0)
102
+ simplecov (~> 0.22.0)
103
+
104
+ BUNDLED WITH
105
+ 2.5.3
data/Makefile ADDED
@@ -0,0 +1,16 @@
1
+ .PHONY: *
2
+
3
+ default: test
4
+
5
+ build:
6
+ rm -f *.gem
7
+ gem build dry-types-json-schema.gemspec
8
+
9
+ publish: build
10
+ gem push *.gem
11
+
12
+ console:
13
+ irb -Ilib:spec -rspec_helper.rb
14
+
15
+ test:
16
+ ruby -Ilib:spec -rpry spec/**/*_spec.rb
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Dry::Types::JSONSchema
2
+
3
+ ![](https://images.unsplash.com/photo-1642952469120-eed4b65104be?q=80&w=600&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
4
+
5
+ ## Example
6
+
7
+ ```ruby
8
+ Dry::Types.load_extensions(:json_schema)
9
+
10
+ AnnotatedString = Dry::Types["string"].meta(format: :email, title: "Notes")
11
+
12
+ AnnotatedString.json_schema
13
+ #=> {:type=>:string, :title=>"Notes", :format=>:email}
14
+
15
+ module Types
16
+ include Dry.Types()
17
+ end
18
+
19
+ class StructTest < Dry::Struct
20
+ schema schema.meta(title: "Title", description: "description")
21
+
22
+ VariableList = Types::Array
23
+ .of(Types::String | Types::Hash)
24
+ .constrained(min_size: 1)
25
+ .meta(description: "Allow an array of strings or multiple hashes")
26
+
27
+ attribute :data, Types::String | Types::Hash
28
+ attribute :string, Types::String.constrained(min_size: 1, max_size: 255)
29
+ attribute :list, VariableList
30
+ end
31
+
32
+ StructTest.json_schema
33
+ # =>
34
+ # {:type=>:object,
35
+ # :properties=>
36
+ # {:data=>{:anyOf=>[{:type=>:string}, {:type=>:object}]},
37
+ # :string=>{:type=>:string, :minLength=>1, :maxLength=>255},
38
+ # :list=>
39
+ # {:type=>:array,
40
+ # :items=>{:anyOf=>[{:type=>:string}, {:type=>:object}]},
41
+ # :description=>"Allow an array of strings or multiple hashes",
42
+ # :minItems=>1}},
43
+ # :required=>[:data, :string, :list],
44
+ # :title=>"Title",
45
+ # :description=>"description"}
46
+ ```
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "dry-types-json-schema"
5
+ s.version = "0.0.1"
6
+ s.summary = "Generate JSON Schema from dry-types"
7
+ s.authors = ["elcuervo"]
8
+ s.licenses = %w[MIT]
9
+ s.email = ["elcuervo@elcuervo.net"]
10
+ s.homepage = "http://github.com/elcuervo/dry-types-json-schema"
11
+ s.files = `git ls-files`.split("\n")
12
+ s.test_files = `git ls-files test`.split("\n")
13
+
14
+ s.add_dependency("dry-types", "~> 1.7.2")
15
+
16
+ s.add_development_dependency("dry-struct", "~> 1.6.0")
17
+ s.add_development_dependency("minitest", "~> 5.22.3")
18
+ s.add_development_dependency("pry", "~> 0.14.2")
19
+ s.add_development_dependency("json_schemer", "~> 2.2.1")
20
+ s.add_development_dependency("simplecov", "~> 0.22.0")
21
+ s.add_development_dependency("rubocop", "~> 1.62.1")
22
+ s.add_development_dependency("rubocop-minitest", "~> 0.35.0")
23
+ end
data/flake.lock ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "nodes": {
3
+ "flake-utils": {
4
+ "inputs": {
5
+ "systems": "systems"
6
+ },
7
+ "locked": {
8
+ "lastModified": 1710146030,
9
+ "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
10
+ "owner": "numtide",
11
+ "repo": "flake-utils",
12
+ "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
13
+ "type": "github"
14
+ },
15
+ "original": {
16
+ "owner": "numtide",
17
+ "repo": "flake-utils",
18
+ "type": "github"
19
+ }
20
+ },
21
+ "nixpkgs": {
22
+ "locked": {
23
+ "lastModified": 1710889954,
24
+ "narHash": "sha256-Pr6F5Pmd7JnNEMHHmspZ0qVqIBVxyZ13ik1pJtm2QXk=",
25
+ "owner": "nixos",
26
+ "repo": "nixpkgs",
27
+ "rev": "7872526e9c5332274ea5932a0c3270d6e4724f3b",
28
+ "type": "github"
29
+ },
30
+ "original": {
31
+ "owner": "nixos",
32
+ "ref": "nixpkgs-unstable",
33
+ "repo": "nixpkgs",
34
+ "type": "github"
35
+ }
36
+ },
37
+ "root": {
38
+ "inputs": {
39
+ "flake-utils": "flake-utils",
40
+ "nixpkgs": "nixpkgs"
41
+ }
42
+ },
43
+ "systems": {
44
+ "locked": {
45
+ "lastModified": 1681028828,
46
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47
+ "owner": "nix-systems",
48
+ "repo": "default",
49
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50
+ "type": "github"
51
+ },
52
+ "original": {
53
+ "owner": "nix-systems",
54
+ "repo": "default",
55
+ "type": "github"
56
+ }
57
+ }
58
+ },
59
+ "root": "root",
60
+ "version": 7
61
+ }
data/flake.nix ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ inputs = {
3
+ nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
4
+ flake-utils.url = "github:numtide/flake-utils";
5
+ };
6
+
7
+ outputs = { self, nixpkgs, flake-utils }:
8
+ flake-utils.lib.eachDefaultSystem (system:
9
+ let
10
+ pkgs = import nixpkgs { inherit system; };
11
+ in
12
+ {
13
+ devShells.default = pkgs.mkShell {
14
+ buildInputs = with pkgs; [
15
+ ruby_3_3
16
+ ];
17
+ };
18
+ });
19
+ }
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Types
5
+ # The `JSONSchema` class is responsible for converting dry-types type definitions into JSON Schema definitions.
6
+ # This class enables the transformation of complex type constraints into a standardized JSON Schema format,
7
+ # facilitating interoperability with systems that utilize JSON Schema for validation.
8
+ #
9
+ class JSONSchema
10
+ # Error raised when an unknown predicate is encountered during schema generation.
11
+ #
12
+ UnknownPredicateError = Class.new(StandardError)
13
+
14
+ # Constant definitions for various lambdas and mappings used throughout the JSON schema conversion process.
15
+ #
16
+ EMPTY_HASH = {}.freeze
17
+ IDENTITY = ->(v, _) { v }.freeze
18
+ INSPECT = ->(v, _) { v.inspect }.freeze
19
+ TO_INTEGER = ->(v, _) { v.to_i }.freeze
20
+ TO_ARRAY = ->(v, _) { v.to_a }.freeze
21
+ TO_TYPE = ->(v, _) { CLASS_TO_TYPE.fetch(v.to_s.to_sym) }.freeze
22
+
23
+ # Metadata annotations and allowed types overrides for schema generation.
24
+ #
25
+ ANNOTATIONS = %i[title description].freeze
26
+ ALLOWED_TYPES_META_OVERRIDES = ANNOTATIONS.dup.concat([:format]).freeze
27
+
28
+ # Mapping for array predicate overrides.
29
+ #
30
+ ARRAY_PREDICATE_OVERRIDE = {
31
+ min_size?: :min_items?,
32
+ max_size?: :max_items?
33
+ }.freeze
34
+
35
+ # Mapping of Ruby classes to their corresponding JSON Schema types.
36
+ #
37
+ CLASS_TO_TYPE = {
38
+ String: :string,
39
+ Integer: :integer,
40
+ TrueClass: :boolean,
41
+ FalseClass: :boolean,
42
+ NilClass: :null,
43
+ BigDecimal: :number,
44
+ Float: :number,
45
+ Hash: :object,
46
+ Array: :array,
47
+ Date: :string,
48
+ DateTime: :string,
49
+ Time: :string
50
+ }.freeze
51
+
52
+ # Additional properties for specific types, such as formatting options.
53
+ #
54
+ EXTRA_PROPS_FOR_TYPE = {
55
+ Date: { format: :date },
56
+ Time: { format: :time },
57
+ DateTime: { format: :"date-time" }
58
+ }.freeze
59
+
60
+ # Mapping of predicate methods to their corresponding JSON Schema expressions.
61
+ #
62
+ PREDICATE_TO_TYPE = {
63
+ type?: { type: TO_TYPE },
64
+ min_size?: { minLength: TO_INTEGER },
65
+ min_items?: { minItems: TO_INTEGER },
66
+ max_size?: { maxLength: TO_INTEGER },
67
+ max_items?: { maxItems: TO_INTEGER },
68
+ min?: { maxLength: TO_INTEGER },
69
+ gt?: { exclusiveMinimum: IDENTITY },
70
+ gteq?: { minimum: IDENTITY },
71
+ lt?: { exclusiveMaximum: IDENTITY },
72
+ lteq?: { maximum: IDENTITY },
73
+ format?: { format: INSPECT },
74
+ included_in?: { enum: TO_ARRAY },
75
+ }.freeze
76
+
77
+ # @return [Set] the set of required keys for the JSON Schema.
78
+ #
79
+ attr_reader :required
80
+
81
+
82
+ # Initializes a new instance of the JSONSchema class.
83
+ # @param root [Boolean] whether this schema is the root schema.
84
+ # @param loose [Boolean] whether to ignore unknown predicates.
85
+ #
86
+ def initialize(root: false, loose: false)
87
+ @keys = EMPTY_HASH.dup
88
+ @required = Set.new
89
+ @root = root
90
+ @loose = loose
91
+ end
92
+
93
+ # Checks if the schema is the root schema.
94
+ # @return [Boolean] true if this is the root schema; otherwise, false.
95
+ #
96
+ def root? = @root
97
+
98
+ # Checks if unknown predicates are ignored.
99
+ # @return [Boolean] true if ignoring unknown predicates; otherwise, false.
100
+ #
101
+ def loose? = @loose
102
+
103
+ # Processes the abstract syntax tree (AST) and generates the JSON Schema.
104
+ # @param ast [Array] the abstract syntax tree representing type definitions.
105
+ # @return [void]
106
+ #
107
+ def call(ast)
108
+ visit(ast)
109
+ end
110
+
111
+ # Converts the internal schema representation into a hash.
112
+ # @return [Hash] the JSON Schema as a hash.
113
+ #
114
+ def to_hash
115
+ result = @keys.to_hash
116
+ result[:$schema] = "http://json-schema.org/draft-06/schema#" if root?
117
+ result
118
+ end
119
+
120
+ # Visits a node in the abstract syntax tree and processes it according to its type.
121
+ # @param node [Array] the node to process.
122
+ # @param opts [Hash] optional parameters for node processing.
123
+ # @return [void]
124
+ #
125
+ def visit(node, opts = EMPTY_HASH)
126
+ name, rest = node
127
+ public_send(:"visit_#{name}", rest, opts)
128
+ end
129
+
130
+ def visit_constrained(node, opts = EMPTY_HASH)
131
+ node.each { |it| visit(it, opts) }
132
+ end
133
+
134
+ def visit_constructor(node, opts = EMPTY_HASH)
135
+ type, _ = node
136
+
137
+ visit(type, opts)
138
+ end
139
+
140
+ def visit_nominal(node, opts = EMPTY_HASH)
141
+ type, meta = node
142
+
143
+ if opts.fetch(:key, false)
144
+ if meta.any?
145
+ @keys[opts[:key]] ||= {}
146
+ @keys[opts[:key]].merge!(meta.slice(*ALLOWED_TYPES_META_OVERRIDES))
147
+ end
148
+ else
149
+ @keys.merge!(type: CLASS_TO_TYPE[type.to_s.to_sym])
150
+ @keys.merge!(meta.slice(*ALLOWED_TYPES_META_OVERRIDES)) if meta.any?
151
+ end
152
+ end
153
+
154
+ def visit_predicate(node, opts = EMPTY_HASH)
155
+ head, ((_, type),) = node
156
+ ctx = opts[:key]
157
+
158
+ head = ARRAY_PREDICATE_OVERRIDE.fetch(head) if opts[:left_type] == ::Array
159
+
160
+ definition = PREDICATE_TO_TYPE.fetch(head) do
161
+ raise UnknownPredicateError, head unless loose?
162
+
163
+ EMPTY_HASH
164
+ end.dup
165
+
166
+ definition.transform_values! { |v| v.call(type, ctx) }
167
+
168
+ return unless definition.any? && ctx
169
+
170
+ if (extra = EXTRA_PROPS_FOR_TYPE[type.to_s.to_sym])
171
+ definition = definition.merge(extra)
172
+ end
173
+
174
+ @keys[ctx] ||= {}
175
+ @keys[ctx].merge!(definition)
176
+ end
177
+
178
+ def visit_sum(node, opts = EMPTY_HASH)
179
+ *types, _ = node
180
+
181
+ # FIXME: cleaner way to generate individual types
182
+ #
183
+ process = -> (type) do
184
+ self.class.new
185
+ .tap { |target| target.visit(type, opts) }
186
+ .to_hash
187
+ .values
188
+ .first
189
+ end
190
+
191
+ result = types.map(&process).uniq
192
+
193
+ return @keys[opts[:key]] = result.first if result.count == 1
194
+
195
+ return @keys[opts[:key]] = { anyOf: result } unless opts[:array]
196
+
197
+ @keys[opts[:key]] = {
198
+ type: :array,
199
+ items: { anyOf: result }
200
+ }
201
+ end
202
+
203
+ def visit_and(node, opts = EMPTY_HASH)
204
+ left, right = node
205
+ (_, (_, ((_, left_type),))) = left
206
+
207
+ visit(left, opts)
208
+ visit(right, opts.merge(left_type: left_type))
209
+ end
210
+
211
+ def visit_hash(node, opts = EMPTY_HASH)
212
+ _part, _meta = node
213
+
214
+ @keys[opts[:key]] = { type: :object }
215
+ end
216
+
217
+ def visit_struct(node, opts = EMPTY_HASH)
218
+ _, schema = node
219
+ visit(schema, opts)
220
+ end
221
+
222
+ def visit_array(node, opts = EMPTY_HASH)
223
+ type, meta = node
224
+
225
+ visit(type, opts.merge(array: true))
226
+
227
+ @keys[opts[:key]].merge!(meta.slice(*ANNOTATIONS)) if meta.any?
228
+ end
229
+
230
+ def visit_schema(node, opts = EMPTY_HASH)
231
+ keys, _, meta = node
232
+
233
+ target = self.class.new
234
+
235
+ keys.each { |fragment| target.visit(fragment, opts) }
236
+
237
+ definition = { type: :object, properties: target.to_hash }
238
+
239
+ definition[:required] = target.required.to_a if target.required.any?
240
+ definition.merge!(meta.slice(*ANNOTATIONS)) if meta.any?
241
+
242
+ @keys.merge!(definition)
243
+ end
244
+
245
+ def visit_enum(node, opts = EMPTY_HASH)
246
+ enum, _ = node
247
+ visit(enum, opts)
248
+ end
249
+
250
+ def visit_key(node, opts = EMPTY_HASH)
251
+ name, required, rest = node
252
+
253
+ @required << name if required
254
+
255
+ visit(rest, opts.merge(key: name))
256
+ end
257
+ end
258
+
259
+ # The `Builder` module provides a method to generate a JSON Schema hash from dry-types definitions.
260
+ #
261
+ module Builder
262
+ # @overload json_schema(options = {})
263
+ # @param options [Hash] Initialization options passed to `JSONSchema.new`
264
+ # @return [Hash] The generated JSON Schema as a hash.
265
+ #
266
+ def json_schema(**)
267
+ compiler = JSONSchema.new(**)
268
+ compiler.call(to_ast)
269
+ compiler.to_hash
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/types"
4
+
5
+ Dry::Types.register_extension(:json_schema) do
6
+ require "dry/types/extensions/json_schema"
7
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe Dry::Types::JSONSchema do
6
+ module Types
7
+ include Dry.Types()
8
+ end
9
+
10
+ describe "basic" do
11
+ it_conforms_definition do
12
+ let(:title) { "Title" }
13
+ let(:type) { Dry::Types["string"].meta(format: :email, title: title) }
14
+
15
+ let(:definition) do
16
+ { type: :string, format: :email, title: title }
17
+ end
18
+ end
19
+ end
20
+
21
+ describe "hash" do
22
+ let(:type) do
23
+ Dry::Types["hash"]
24
+ .schema(
25
+ name: Dry::Types["string"],
26
+ age: Dry::Types["integer"].constrained(gt: 0, lteq: 99),
27
+ active: Dry::Types["bool"],
28
+ migrated: Dry::Types["nil"],
29
+ views: Dry::Types["decimal"].constrained(gteq: 0, lt: 99_999),
30
+ created_at: Dry::Types["time"]
31
+ ).meta(title: "Hash title")
32
+ end
33
+
34
+ it_conforms_definition do
35
+ let(:definition) do
36
+ {
37
+ type: :object,
38
+ title: type.meta[:title],
39
+
40
+ properties: {
41
+ name: { type: :string },
42
+ age: { type: :integer, exclusiveMinimum: 0, maximum: 99 },
43
+ active: { type: :boolean },
44
+ migrated: { type: :null },
45
+ views: { type: :number, minimum: 0, exclusiveMaximum: 99_999 },
46
+ created_at: { type: :string, format: :time }
47
+ },
48
+ required: %i[name age active migrated views created_at]
49
+ }
50
+ end
51
+ end
52
+ end
53
+
54
+ describe "struct" do
55
+ class StructTest < Dry::Struct
56
+ schema schema.meta(title: "Title", description: "description")
57
+
58
+ VariableList = Types::Array
59
+ .of(Types::String | Types::Hash)
60
+ .constrained(min_size: 1)
61
+ .meta(description: "Allow an array of strings or multiple hashes")
62
+
63
+ # Validate regexp compatibility during inspect
64
+ #
65
+ EmailType = Types::String
66
+ .constrained(format: /\A[\w+\-.]+@[a-z\d-]+(\.[a-z]+)*\.[a-z]+\z/i)
67
+ .meta(description: "The internally used pattern")
68
+
69
+ attribute :data, Types::String | Types::Hash
70
+ attribute :string, Types::String.constrained(min_size: 1, max_size: 255)
71
+ attribute :list, VariableList
72
+ attribute? :email, EmailType
73
+ attribute? :super, Types::Bool
74
+ attribute? :start, Types::Date
75
+ attribute? :end, Types::DateTime
76
+ attribute? :epoch, Types::Time
77
+ attribute? :meta, Types::String.meta(format: :email)
78
+ attribute? :enum, Types::String.enum(*%w[draft published archived])
79
+ end
80
+
81
+ let(:type) { StructTest }
82
+
83
+ it_conforms_definition do
84
+ let(:definition) do
85
+ {
86
+ title: StructTest.schema.meta[:title],
87
+ description: StructTest.schema.meta[:description],
88
+
89
+ type: :object,
90
+ properties: {
91
+ data: {
92
+ anyOf: [
93
+ { type: :string },
94
+ { type: :object }
95
+ ]
96
+ },
97
+
98
+ list: {
99
+ type: :array,
100
+ description: StructTest::VariableList.meta[:description],
101
+ items: {
102
+ anyOf: [
103
+ { type: :string },
104
+ { type: :object }
105
+ ]
106
+ },
107
+ minItems: 1
108
+ },
109
+
110
+ string: {
111
+ type: :string,
112
+ minLength: 1,
113
+ maxLength: 255
114
+ },
115
+
116
+ super: {
117
+ type: :boolean
118
+ },
119
+
120
+ email: {
121
+ type: :string,
122
+ format: "/\\A[\\w+\\-.]+@[a-z\\d-]+(\\.[a-z]+)*\\.[a-z]+\\z/i",
123
+ description: StructTest::EmailType.meta[:description]
124
+ },
125
+
126
+ start: {
127
+ type: :string,
128
+ format: :date
129
+ },
130
+
131
+ end: {
132
+ type: :string,
133
+ format: :"date-time"
134
+ },
135
+
136
+ epoch: {
137
+ type: :string,
138
+ format: :time
139
+ },
140
+
141
+ meta: {
142
+ type: :string,
143
+ format: :email
144
+ },
145
+
146
+ enum: {
147
+ type: :string,
148
+ enum: %w[draft published archived]
149
+ }
150
+ },
151
+
152
+ required: %i[data string list]
153
+ }
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simplecov"
4
+
5
+ SimpleCov.start
6
+
7
+ require "minitest/autorun"
8
+ require "json_schemer"
9
+
10
+ require "dry/struct"
11
+ require "dry/types"
12
+ require "dry/types/extensions"
13
+
14
+ Dry::Types.load_extensions(:json_schema)
15
+
16
+ class Minitest::Spec
17
+ class << self
18
+ def it_conforms_definition(&block)
19
+ instance_exec(&block) if block
20
+
21
+ describe "conforms the schema definition" do
22
+ it { assert_equal type.json_schema, definition }
23
+ it { assert JSONSchemer.schema(type.json_schema.to_json).valid_schema? }
24
+ end
25
+ end
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,168 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-types-json-schema
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - elcuervo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-types
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.7.2
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.7.2
27
+ - !ruby/object:Gem::Dependency
28
+ name: dry-struct
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.6.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.6.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 5.22.3
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 5.22.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.14.2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.14.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: json_schemer
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 2.2.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 2.2.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.22.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.22.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 1.62.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 1.62.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.35.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.35.0
125
+ description:
126
+ email:
127
+ - elcuervo@elcuervo.net
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rubocop.yml"
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - Makefile
137
+ - README.md
138
+ - dry-types-json-schema.gemspec
139
+ - flake.lock
140
+ - flake.nix
141
+ - lib/dry/types/extensions.rb
142
+ - lib/dry/types/extensions/json_schema.rb
143
+ - spec/extensions/json_schema_spec.rb
144
+ - spec/spec_helper.rb
145
+ homepage: http://github.com/elcuervo/dry-types-json-schema
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubygems_version: 3.5.6
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Generate JSON Schema from dry-types
168
+ test_files: []