tapioca-compilers-dry_struct 0.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: 9dcbe2532639837d3887debbc6b919d0e6181ce40af168e8c0ef74ab1efdf21e
4
+ data.tar.gz: 81e38049ba86b21f99967514aa1bc9b6f87c60fa7a7d8b5d395633098b265d55
5
+ SHA512:
6
+ metadata.gz: e0df50ffae3ca81b5236ecbb6c9d57b424841454067ffe5ce8f0d453b9e367706527f3940546b417f8055595bd13971cabef2a903f0c298bf28306935f1bbdeb
7
+ data.tar.gz: 5d2ba85b1a2399808f847407803c3b34a55dda5a474c6b766f836a432b05ff6289dc4f46ec75a2582b44ec3c2909595411d43406e6c0f26642d17fe5ad55bdf5
data/CHANGELOG.md ADDED
File without changes
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ **[WARNING] THIS APP IS ALPHA VERSION!!!**
2
+
3
+ This script is a [Tapioca's custom DSL compiler](https://github.com/Shopify/tapioca#writing-custom-dsl-compilers) for [dry-struct](https://dry-rb.org/gems/dry-struct/main/).
4
+
5
+ # References
6
+
7
+ https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961
8
+
9
+ https://github.com/Shopify/tapioca/issues/1210
10
+
11
+ # Installation
12
+
13
+ cp lib/tapioca/dsl/compilers/dry_struct.rb PATH_TO_YOUR_PROJECT/sorbet/tapioca/compilers/dry_struct.rb
14
+
15
+ -----------------------------------
16
+
17
+ Add this line to your application’s Gemfile:
18
+
19
+ ```
20
+ gem 'tapioca-compilers-dry_struct'
21
+ ```
22
+
23
+ And then execute:
24
+
25
+ ```
26
+ $ bundle
27
+ ```
28
+
29
+ > all the gem needs to do is to place the DSL compiler inside the lib/tapioca/dsl/compilers folder and it will be automatically discovered and loaded by Tapioca.
30
+ >
31
+ > https://github.com/Shopify/tapioca/blob/v0.10.2/README.md#writing-custom-dsl-compilers
32
+
33
+ Tapioca automatically load the compiler inside gem, just you need to do is type a command below.
34
+
35
+ ```
36
+ $ bundle exec tapioca dsl
37
+ ```
38
+
39
+ # Development
40
+
41
+ Execute `./bin/dsl` generates rbi files under `sorbet/rbi/dsl/`.
42
+ If you need to test another dry-struct, add or edit rb files under `spec/examples/` and generate rbi files.
43
+
44
+ A compiler itself exists in `lib/tapioca/dsl/compilers/dry_struct.rb`.
45
+
46
+ # Test
47
+
48
+ ```
49
+ bundle exec rake spec
50
+ ```
51
+
52
+ # Environment Variables
53
+
54
+ ## DRY_PREFER_PLAIN_TIME
55
+
56
+ \ | Ruby `Time` is typed as
57
+ ------- | --------
58
+ 0(DEFAULT) | `::ActiveSupport::TimeWithZone`
59
+ 1 | `::Time`
60
+
61
+ ## DRY_USE_EXPERIMENTAL_SHAPE
62
+
63
+ \ | `Types::Hash.schema(name: Types::String, age: Types::Integer)` is typed as
64
+ ------- | --------
65
+ 0(DEFAULT) | `::T::Hash[::T.untyped, ::T.untyped]`
66
+ 1 | `{ name: ::String, age: ::Integer }`
@@ -0,0 +1,328 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+ require 'dry-types'
4
+ require 'dry-struct'
5
+ begin
6
+ require 'dry-monads'
7
+ rescue LoadError
8
+ # NOP
9
+ end
10
+
11
+ module Tapioca
12
+ module Compilers
13
+ # tapioca's [custom DSL compilers](https://github.com/Shopify/tapioca/blob/main/README.md#writing-custom-dsl-compilers)
14
+ # for [dry-struct](https://dry-rb.org/gems/dry-struct/).
15
+ class DryStruct < Tapioca::Dsl::Compiler
16
+ extend T::Sig
17
+
18
+ VERSION = '0.0.0'
19
+
20
+ # Convert Dry AST to type informations
21
+ #
22
+ # @example
23
+ # ```
24
+ # compiler = DryAstCompiler.new
25
+ # DryStructClass.schema.each do |s|
26
+ # attribute_info = compiler.visit(s.to_ast)
27
+ #
28
+ # # => { name: 'attr_name01', required: true, type: String }
29
+ # # => { name: 'attr_name02', required: true, type: Integer }
30
+ # # => { name: 'attr_name03', required: true, type: [String] }
31
+ # # => { name: 'attr_name04', required: true, type: [[String]] }
32
+ # # => { name: 'attr_name05', required: true, type: Hash }
33
+ # # => { name: 'attr_name06', required: true, type: Date }
34
+ # # => { name: 'attr_name07', required: true, type: DateTime }
35
+ # # => { name: 'attr_name08', required: true, type: Time }
36
+ # # => { name: 'attr_name09', required: true, type: Set }
37
+ # # => { name: 'attr_name10', required: true, type: Range }
38
+ # # => { name: 'attr_name11', required: true, type: YourKlass }
39
+ # # => { name: 'attr_name12', required: true, type: #<DryAstCompiler::Undefined> }
40
+ # # => { name: 'attr_name13', required: true, type: #<DryAstCompiler::Sum> }
41
+ # # => { name: 'attr_name14', required: true, type: #<DryAstCompiler::Map> }
42
+ # # => { name: 'attr_name15', required: true, type: #<DryAstCompiler::Schema> }
43
+ # do
44
+ # ```
45
+ class DryAstCompiler
46
+ # Represents dry's any type
47
+ class Undefined
48
+
49
+ def to_s
50
+ 'Undefined'
51
+ end
52
+
53
+ def inspect
54
+ to_s
55
+ end
56
+ end
57
+
58
+ # Represents dry's sum type
59
+ class Sum
60
+ attr_reader :types
61
+
62
+ def initialize
63
+ @types = ::Set.new
64
+ end
65
+
66
+ def size
67
+ @types.size
68
+ end
69
+
70
+ def <<(arg)
71
+ if arg.is_a?(Sum)
72
+ arg.types.each { |t| @types << t }
73
+ else
74
+ @types << arg
75
+ end
76
+ end
77
+
78
+ def map(&block)
79
+ @types.map(&block)
80
+ end
81
+
82
+ def first
83
+ @types.first
84
+ end
85
+
86
+ def include_undefined?
87
+ @types.any? { |t| t.is_a?(Undefined) }
88
+ end
89
+
90
+ def include_nilclass?
91
+ @types.include?(::NilClass)
92
+ end
93
+
94
+ def delete_nilclass!
95
+ @types.reject! { |t| t == ::NilClass }
96
+ end
97
+
98
+ def to_s
99
+ "Sum(#{@types.map(&:to_s).join(',')})"
100
+ end
101
+
102
+ def inspect
103
+ to_s
104
+ end
105
+ end
106
+
107
+ # Represents dry's schema type
108
+ class Schema
109
+ def initialize(attribute_infos)
110
+ @attribute_infos = attribute_infos
111
+ end
112
+
113
+ def map(&block)
114
+ @attribute_infos.map(&block)
115
+ end
116
+
117
+ def empty?
118
+ @attribute_infos.empty?
119
+ end
120
+ end
121
+
122
+ # Represents dry's map type
123
+ class Map
124
+ attr_reader :key, :value
125
+
126
+ def initialize(key, value)
127
+ @key = key
128
+ @value = value
129
+ end
130
+ end
131
+
132
+ def visit(node)
133
+ meth, rest = node
134
+ public_send(:"visit_#{meth}", rest)
135
+ end
136
+
137
+ def visit_key(node)
138
+ name, required, rest = node
139
+ {
140
+ name: name,
141
+ required: required,
142
+ type: visit(rest)
143
+ }
144
+ end
145
+
146
+ def visit_constructor(node)
147
+ a, _ = node
148
+ visit(a)
149
+ end
150
+
151
+ def visit_struct(node)
152
+ type, _ = node
153
+ type
154
+ end
155
+
156
+ def visit_sum(node)
157
+ type = Sum.new
158
+ node.each do |n|
159
+ next if n.is_a?(::Hash)
160
+ type << visit(n)
161
+ end
162
+ type
163
+ end
164
+
165
+ def visit_and(node)
166
+ # NOP
167
+ end
168
+
169
+ def visit_array(node)
170
+ type = visit(node[0])
171
+ [type]
172
+ end
173
+
174
+ def visit_constrained(node)
175
+ types = node.map { |r| visit(r) }.reject(&:nil?)
176
+ types.size == 1 ? types[0] : types
177
+ end
178
+
179
+ def visit_nominal(node)
180
+ type, _option = node
181
+ type
182
+ end
183
+
184
+ def visit_predicate(_node)
185
+ # NOP
186
+ end
187
+
188
+ def visit_schema(node)
189
+ Schema.new(node[0].map { |n| visit(n) })
190
+ end
191
+
192
+ def visit_hash(_node)
193
+ ::Hash
194
+ end
195
+
196
+ def visit_map(node)
197
+ Map.new(visit(node[0]), visit(node[1]))
198
+ end
199
+
200
+ def visit_enum(node)
201
+ visit(node[0][1][0])
202
+ end
203
+
204
+ def visit_any(_node)
205
+ Undefined.new
206
+ end
207
+ end
208
+
209
+ ConstantType = type_member {{ fixed: ::T.class_of(::Dry::Struct) }}
210
+
211
+ sig { override.void }
212
+ def decorate
213
+ compiler = DryAstCompiler.new
214
+ root.create_path(constant) do |klass|
215
+ constant.schema.each do |s|
216
+ attribute_info = compiler.visit(s.to_ast)
217
+ if ::Dry::Types.const_defined?(:Maybe) && s.type.class == ::Dry::Types::Maybe
218
+ sorbet_type = '::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None)'
219
+ else
220
+ sorbet_type = self.class.to_sorbet_type(attribute_info[:type], attribute_info[:required])
221
+ end
222
+ klass.create_method(
223
+ attribute_info[:name].to_s,
224
+ return_type: sorbet_type,
225
+ )
226
+ end
227
+ end
228
+ end
229
+
230
+ sig { override.returns(::T::Enumerable[Module]) }
231
+ def self.gather_constants
232
+ all_classes.select { |c| c < ::Dry::Struct }
233
+ end
234
+
235
+ sig { params(type: T.untyped, required: T::Boolean).returns(String) }
236
+ def self.to_sorbet_type(type, required)
237
+ base = if type.is_a?(DryAstCompiler::Sum)
238
+ sum_to_sorbet_type(type)
239
+ elsif type.is_a?(DryAstCompiler::Schema)
240
+ env('DRY_USE_EXPERIMENTAL_SHAPE') ? experimental_schema_to_sorbet_type(type) : '::T::Hash[::T.untyped, ::T.untyped]'
241
+ elsif type.is_a?(DryAstCompiler::Map)
242
+ map_to_sorbet_type(type)
243
+ elsif type.is_a?(DryAstCompiler::Undefined)
244
+ '::T.untyped'
245
+ elsif type.is_a?(::Array)
246
+ "::T::Array[#{to_sorbet_type(type[0], true)}]"
247
+ elsif type == ::Hash
248
+ '::T::Hash[::T.untyped, ::T.untyped]'
249
+ elsif type == ::Time
250
+ env('DRY_PREFER_PLAIN_TIME') ? '::Time' : '::ActiveSupport::TimeWithZone'
251
+ elsif type == ::Range
252
+ '::T::Range[::T.untyped]'
253
+ elsif type == ::Set
254
+ '::T::Set[::T.untyped]'
255
+ elsif type == ::TrueClass || type == ::FalseClass
256
+ '::T::Boolean'
257
+ elsif type.nil?
258
+ '::NilClass'
259
+ else
260
+ "::#{type.name}"
261
+ end
262
+ if base == '::T.untyped' || base == '::NilClass'
263
+ base
264
+ else
265
+ if required
266
+ base
267
+ else
268
+ if base.start_with?('::T.nilable')
269
+ base
270
+ else
271
+ "::T.nilable(#{base})"
272
+ end
273
+ end
274
+ end
275
+ end
276
+
277
+ sig { params(sum: DryAstCompiler::Sum).returns(String) }
278
+ def self.sum_to_sorbet_type(sum)
279
+ return '::T.untyped' if sum.include_undefined?
280
+
281
+ if sum.include_nilclass?
282
+ sum.delete_nilclass!
283
+ if sum.size < 2
284
+ "::T.nilable(#{to_sorbet_type(sum.first, true)})"
285
+ elsif (sum.types - [::TrueClass, ::FalseClass]).empty?
286
+ '::T.nilable(::T::Boolean)'
287
+ else
288
+ "::T.nilable(::T.any(#{sum.map { |t| to_sorbet_type(t, true) }.join(', ')}))"
289
+ end
290
+ else
291
+ if (sum.types - [::TrueClass, ::FalseClass]).empty?
292
+ '::T::Boolean'
293
+ elsif sum.size == 1
294
+ to_sorbet_type(sum.first, true)
295
+ else
296
+ "::T.any(#{sum.map { |t| to_sorbet_type(t, true) }.join(', ')})"
297
+ end
298
+ end
299
+ end
300
+
301
+ sig { params(map: DryAstCompiler::Map).returns(String) }
302
+ def self.map_to_sorbet_type(map)
303
+ "::T::Hash[#{map.key}, #{map.value}]"
304
+ end
305
+
306
+ sig { params(schema: DryAstCompiler::Schema).returns(String) }
307
+ def self.experimental_schema_to_sorbet_type(schema)
308
+ return '::T::Hash[::T.untyped, ::T.untyped]' if schema.empty?
309
+
310
+ sigs = schema.map do |i|
311
+ sorbet_type = to_sorbet_type(i[:type], i[:required])
312
+ i[:name].is_a?(::String) ? "'#{i[:name]}' => #{sorbet_type}" : "#{i[:name]}: #{sorbet_type}"
313
+ end
314
+
315
+ "{ #{sigs.join(', ')} }"
316
+ end
317
+
318
+ sig { params(key: String).returns(T::Boolean) }
319
+ def self.env(key)
320
+ v = ENV[key]
321
+ return false if v.nil?
322
+ return false if v == '0'
323
+ true
324
+ end
325
+ end
326
+ end
327
+ end
328
+
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "tapioca-compilers-dry_struct"
5
+ spec.authors = ["Yuki Jikumaru"]
6
+ spec.email = ["jikumaruyuki@gmail.com"]
7
+ spec.license = "MIT"
8
+ spec.version = "0.0.0"
9
+
10
+ spec.summary = "Tapioca's compiler for dry-struct"
11
+ spec.description = spec.summary
12
+ spec.homepage = "https://github.com/YukiJikumaru/tapioca-compilers-dry_struct"
13
+ spec.files = Dir["README.md", "CHANGELOG.md", "tapioca-compilers-dry_struct.gemspec", "lib/tapioca/dsl/compilers/dry_struct.rb"]
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["changelog_uri"] = "https://github.com/YukiJikumaru/tapioca-compilers-dry_struct/blob/main/CHANGELOG.md"
18
+ spec.metadata["source_code_uri"] = "https://github.com/YukiJikumaru/tapioca-compilers-dry_struct"
19
+ spec.metadata["bug_tracker_uri"] = "https://github.com/YukiJikumaru/tapioca-compilers-dry_struct/issues"
20
+
21
+ spec.required_ruby_version = ">= 2.7.0"
22
+
23
+ # to update dependencies edit project.yml
24
+ spec.add_runtime_dependency "tapioca", ">= 0.10"
25
+ spec.add_runtime_dependency "dry-struct", ">= 1.2"
26
+ spec.add_runtime_dependency "dry-monads", ">= 1.2"
27
+
28
+ spec.add_development_dependency "bundler"
29
+ spec.add_development_dependency "rake"
30
+ spec.add_development_dependency "rspec"
31
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tapioca-compilers-dry_struct
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Yuki Jikumaru
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: tapioca
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.10'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.10'
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.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-monads
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
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
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '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'
97
+ description: Tapioca's compiler for dry-struct
98
+ email:
99
+ - jikumaruyuki@gmail.com
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - CHANGELOG.md
105
+ - README.md
106
+ - lib/tapioca/dsl/compilers/dry_struct.rb
107
+ - tapioca-compilers-dry_struct.gemspec
108
+ homepage: https://github.com/YukiJikumaru/tapioca-compilers-dry_struct
109
+ licenses:
110
+ - MIT
111
+ metadata:
112
+ allowed_push_host: https://rubygems.org
113
+ changelog_uri: https://github.com/YukiJikumaru/tapioca-compilers-dry_struct/blob/main/CHANGELOG.md
114
+ source_code_uri: https://github.com/YukiJikumaru/tapioca-compilers-dry_struct
115
+ bug_tracker_uri: https://github.com/YukiJikumaru/tapioca-compilers-dry_struct/issues
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: 2.7.0
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubygems_version: 3.1.6
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: Tapioca's compiler for dry-struct
135
+ test_files: []