sorbet-rails 0.1.2

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
+ SHA1:
3
+ metadata.gz: e3f7dd83f8d099f5951a1a8bd13707c4e38d60e5
4
+ data.tar.gz: 4c7e2dc570d7695826c1c88d3f6711c7600dbe57
5
+ SHA512:
6
+ metadata.gz: '0780d48a3e347edd504e6b3b2e11ca88e60118a29e25b3b0b4c931192aec27e8c2f2d1f2ddeff5c111a106a5174ed2feb8e7d462f92a59cc8a91284c9d0c48eb'
7
+ data.tar.gz: 8a3af472623c72b6cf615acd4a19b52b341b82ed18392feb8e25ddcd9a10177b329d3ab77c557b1ca32dacada4216f1ac92e6dcc1503ada0fa9507a087ebf786
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ source 'https://rubygems.org'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Chan Zuckerberg Initiative
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # sorbet-rails
2
+ Set of tools to make sorbet work with rails seamlessly.
3
+
4
+ This gem adds a few rake tasks to generate RBI for dynamic methods generated by Rails. It also includes sigs for related Rails classes. The generated rbis are added to `sorbet/rails-rbi/` folder.
5
+
6
+ Please feel free to send PR requests or file issues to improve the functionality of the gem!
7
+
8
+ ## Initial setup
9
+
10
+ Following the steps below to setup the rbis after `srb tc`
11
+ 1. Include ActiveRecord RBI
12
+ ```
13
+ $ srb tc sorbet-typed
14
+ ```
15
+ 2. Generate routes RBI
16
+ ```
17
+ $ rake rails_rbi:routes
18
+ ```
19
+ 3. Generate models RBI
20
+ ```
21
+ $ rake rails_rbi:models
22
+ ```
23
+ 4. Auto-upgrade the typecheck level of files
24
+ ```
25
+ $ srb tc --suggest-typed --typed=strict --error-white-list=7022 -a
26
+ ```
27
+ Because we've generated RBI files for models & routes, a lot more files should be type-checkable now
28
+
29
+ ## ActiveRecord RBI
30
+
31
+ There are ActiveRecord RBI that we vendor with this gem. Please run `srb tc sorbet-typed` to include the provided RBI in your `sorbet/rbi_list`
32
+
33
+ ## Routes RBI
34
+ The following rake task generates `_path` and `_url` methods for all named routes defined in `routes.rb`
35
+ ```
36
+ rake rails_rbi:routes
37
+ ```
38
+ ## Models RBI
39
+ The following rake task generates rbi files for all models in the Rails App (all descendants of ActiveRecord::Base)
40
+ ```
41
+ rake rails_rbi:models
42
+ ```
43
+ You can also regenerate rbi files for specific models
44
+ ```
45
+ rake rails_rbi:models[ModelName,AnotherOne,...]
46
+ ```
47
+ At the moment, the generation task generate the following signatures
48
+ - Column getters & setters
49
+ - Associations getters & setters
50
+ - Enum values, checkers & scopes
51
+ - Named scopes
52
+ - Model relation class
53
+
54
+ ## Tips & Tricks
55
+ ### `find`, `first` and `last`
56
+ These 3 methods can either return a single nilable record or an array of records. Sorbet does not allow us to define multiple sigs for a function. It doesn't support defining one function sig that has varying returning value depending on the input param type. We opt to define the most commonly used sig for these methods, and monkey-patch new functions for the secondary use.
57
+
58
+ In short:
59
+ - Use `find`, `first` and `last` to fetch a single record
60
+ - Use `find_n`, `first_n`, `last_n` to fetch an array of records.
61
+
62
+ ### `find_by_<attributes>`, `<attribute>_changed?`, etc.
63
+ Rails supports dynamic methods based on attribute names, such as `find_by_<attribute>`, `<attribute>_changed?`, etc. They all have static counterparts. Instead of generating all possible dynamic methods that Rails support, we recommend to use of the static version of these methods instead (also recommended by RuboCop). We've added sigs for th
64
+
65
+ Following are the list of attribute dynamic methods and their static counterpart. The static methods have proper sigs.
66
+ - `find_by_<attributes>` -> `find_by(<attributes>)`
67
+ - `find_by_<attributes>!` -> `find_by!(<attributes>)`
68
+ - `<attribute>_changed?` -> `attribute_changed?(<attribute>)`
69
+ - `saved_change_to_<attribute>?` -> `saved_change_to_attribute?(<attribute>)`
70
+
71
+ ### `after_commit` and other callbacks
72
+ Consider codemod-ing `after_commit` callbacks to use instance method functions. Sorbet doesn't support binding an optional block with a different context. Because of this, when using a callback with a custom block, the block is evaluated in the wrong context (Class-level context).
73
+
74
+ ```
75
+ after_commit do ... end
76
+ ```
77
+ codemod to
78
+ ```
79
+ after_commit :after_commit
80
+ def after_commit
81
+ ...
82
+ end
83
+ ```
84
+
85
+ I've created a gist with codemod commands to convert the code automatically
86
+ https://gist.github.com/manhhung741/d2e0a8f9c4178f328b241dd8b28ccc67
87
+
88
+ See this link for a full list of callbacks available in Rails:
89
+ https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
90
+
91
+ ### Look for `# typed: ignore` files
92
+
93
+ Because sorbet initial setup tries to flag files at the typecheck level that generates 0 errors, there may be files in your repository that is `# typed: ignore`. This is because sometimes Rails allow very dynamic code that Sorbet does not regard as typecheck-able.
94
+
95
+ It is worth going through the list of files that is ignored and resolve them (and auto upgrade the types of other files -- see Initial Setup #4). Usually this will resolve in many more files typecheckable.
@@ -0,0 +1,3 @@
1
+ module SorbetRails
2
+ require 'sorbet-rails/railtie' if defined?(Rails)
3
+ end
@@ -0,0 +1,15 @@
1
+ module SorbetRails
2
+ module CustomFinderMethods
3
+ def find_n(ids)
4
+ find(ids)
5
+ end
6
+
7
+ def first_n(n)
8
+ first(n)
9
+ end
10
+
11
+ def last_n(n)
12
+ last(n)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,367 @@
1
+ # typed: true
2
+ class ModelRbiFormatter
3
+
4
+ RUBY_TO_SORBET_TYPE_MAPPING = {
5
+ boolean: 'T::Boolean',
6
+ date: 'Date',
7
+ datetime: 'DateTime',
8
+ decimal: 'Integer',
9
+ integer: 'Integer',
10
+ string: 'String',
11
+ text: 'String',
12
+ json: 'Hash',
13
+ jsonb: 'Hash',
14
+ }
15
+
16
+ def initialize(model_class, available_classes)
17
+ @model_class = model_class
18
+ @available_classes = available_classes
19
+ @columns_hash = model_class.columns_hash
20
+ @generated_sigs = ActiveSupport::HashWithIndifferentAccess.new
21
+ @generated_class_sigs = ActiveSupport::HashWithIndifferentAccess.new
22
+ @generated_scope_sigs = ActiveSupport::HashWithIndifferentAccess.new
23
+ @generated_querying_sigs = ActiveSupport::HashWithIndifferentAccess.new
24
+ begin
25
+ # Load all dynamic instance methods of this model by instantiating a fake model
26
+ @model_class.new
27
+ rescue StandardError
28
+ puts "Note: Unable to create new instance of #{model_class.name}"
29
+ end
30
+ end
31
+
32
+ def generate_rbi
33
+ puts "-- Generate sigs for #{@model_class.name} --"
34
+ populate_activerecord_querying_methods
35
+ populate_named_scope_methods
36
+ populate_generated_column_methods
37
+ populate_generated_association_methods
38
+ populate_generated_enum_methods
39
+
40
+ @buffer = []
41
+ @buffer << draw_class_header
42
+
43
+ @model_class.instance_methods.sort.each do |method_name|
44
+ expected_sig = @generated_sigs[method_name]
45
+ next unless expected_sig.present?
46
+ method_obj = @model_class.instance_method(method_name)
47
+ draw_method(method_name, method_obj, expected_sig, false)
48
+ end
49
+
50
+ @model_class.methods.sort.each do |method_name|
51
+ expected_sig = @generated_class_sigs[method_name]
52
+ next unless expected_sig.present?
53
+ method_obj = @model_class.method(method_name)
54
+ draw_method(method_name, method_obj, expected_sig, true)
55
+ end
56
+
57
+ @buffer << draw_class_footer
58
+
59
+ # <Model>::NamedScope is a fake module added so that when a method is defined
60
+ # in this module, it'll be added to both the Model class as a class method
61
+ # and to its relation as an instance method.
62
+ #
63
+ # We need to define the NamedScope module after the other classes
64
+ # to work around Sorbet loading order bug
65
+ # https://sorbet-ruby.slack.com/archives/CHN2L03NH/p1556065791047300
66
+ @buffer << "\n"
67
+ @buffer << draw_named_scope_header
68
+ # For simplicity, generate both in the same module for now.
69
+ # We don't need to define two fake modules to share methods between <Model> and <Relation>
70
+ ({}.
71
+ merge(@generated_scope_sigs).
72
+ merge(@generated_querying_sigs)
73
+ ).each do |method_name, expected_sig|
74
+ method_obj = @model_class.method(method_name) if @model_class.methods.include?(method_name.to_sym)
75
+ # this is not a class method because it is added to NamedScope
76
+ draw_method(method_name, method_obj, expected_sig, false)
77
+ end
78
+ @buffer << draw_named_scope_footer
79
+ @buffer << "\n"
80
+ @buffer.join("\n")
81
+ end
82
+
83
+ def draw_method(method_name, method_obj, expected_sig, is_class_method)
84
+ if !method_obj.present?
85
+ # not very actionable because this could be a method in a newer version of Rails
86
+ # puts "Skip method '#{method_name}' because there is no matching method object."
87
+ return
88
+ end
89
+ if !is_method_autogenerated?(method_obj)
90
+ puts "Skip method '#{method_name}' because it is not autogenerated by Rails."
91
+ return
92
+ end
93
+ if !matched_signature?(method_obj, expected_sig)
94
+ puts "Skip method '#{method_name}' because it has different signature from expected."
95
+ return
96
+ end
97
+ @buffer << generate_method_sig(method_name, expected_sig, is_class_method).indent(2)
98
+ end
99
+
100
+ def populate_activerecord_querying_methods
101
+ # All is a named scope that most method from ActiveRecord::Querying delegate to
102
+ # rails/activerecord/lib/active_record/querying.rb:21
103
+ @generated_scope_sigs["all"] = { ret: "#{@model_class.name}::Relation" }
104
+ # It's not possible to typedef all methods in ActiveRecord::Querying module to have the
105
+ # matching type. By generating model-specific sig, we can typedef these methods to return
106
+ # <Model>::Relation class.
107
+ # rails/activerecord/lib/active_record/querying.rb
108
+ model_query_relation_methods = [
109
+ :select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
110
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
111
+ :having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
112
+ ]
113
+ model_query_relation_methods.each do |method_name|
114
+ @generated_querying_sigs[method_name.to_s] = {
115
+ args: [
116
+ {name: :args, arg_type: :rest, value_type: 'T.untyped'},
117
+ {name: :block, arg_type: :block, value_type: 'T.nilable(T.proc.void)'},
118
+ ],
119
+ ret: "#{@model_class.name}::Relation",
120
+ }
121
+ end
122
+ end
123
+
124
+ def populate_named_scope_methods
125
+ @model_class.methods.sort.each do |method_name|
126
+ method_obj = @model_class.method(method_name)
127
+ next unless method_obj.present? && method_obj.source_location.present?
128
+ # we detect sscopes defined in a model by 2 criteria:
129
+ # - they don't have an owner name
130
+ # - they are defined in 'activerecord/lib/active_record/scoping/named.rb'
131
+ # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/scoping/named.rb
132
+ next unless method_obj.owner.name == nil
133
+ source_file = method_obj.source_location[0]
134
+ next unless source_file.include?('lib/active_record/scoping/named.rb')
135
+ @generated_scope_sigs[method_name] = {
136
+ args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
137
+ ret: "#{@model_class.name}::Relation",
138
+ }
139
+ end
140
+ end
141
+
142
+ def populate_generated_column_methods
143
+ @columns_hash.each do |column_name, column_def|
144
+ if @model_class.defined_enums.has_key?(column_name)
145
+ # enum attribute is treated differently
146
+ assignable_type = "T.any(Integer, String, Symbol)"
147
+ assignable_type = "T.nilable(#{assignable_type})" if column_def.null
148
+ @generated_sigs.merge!({
149
+ "#{column_name}" => { ret: "String" },
150
+ "#{column_name}=" => {
151
+ args: [ name: :value, arg_type: :req, value_type: assignable_type],
152
+ },
153
+ })
154
+ else
155
+ column_type = type_for_column_def(column_def)
156
+ @generated_sigs.merge!({
157
+ "#{column_name}" => { ret: column_type },
158
+ "#{column_name}=" => {
159
+ args: [ name: :value, arg_type: :req, value_type: column_type ],
160
+ },
161
+ })
162
+ end
163
+
164
+ if column_def.type == :boolean
165
+ @generated_sigs["#{column_name}?"] = {
166
+ ret: "T::Boolean",
167
+ args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
168
+ }
169
+ end
170
+ end
171
+ end
172
+
173
+ def populate_generated_association_methods
174
+ @model_class.reflections.each do |assoc_name, reflection|
175
+ reflection.collection? ?
176
+ populate_collection_assoc_getter_setter(assoc_name, reflection) :
177
+ populate_single_assoc_getter_setter(assoc_name, reflection)
178
+ end
179
+ end
180
+
181
+ def populate_single_assoc_getter_setter(assoc_name, reflection)
182
+ # TODO allow people to specify the possible values of polymorphic associations
183
+ assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : reflection.class_name
184
+ assoc_type = "T.nilable(#{assoc_class})"
185
+ if reflection.belongs_to?
186
+ # if this is a belongs_to connection, we may be able to detect whether
187
+ # this field is required & use a stronger type
188
+ column_def = @columns_hash[reflection.foreign_key.to_s]
189
+ if column_def
190
+ assoc_type = assoc_class if !column_def.null
191
+ end
192
+ end
193
+
194
+ @generated_sigs.merge!({
195
+ "#{assoc_name}" => { ret: assoc_type },
196
+ "#{assoc_name}=" => {
197
+ args: [ name: :value, arg_type: :req, value_type: assoc_type ],
198
+ },
199
+ })
200
+ end
201
+
202
+ def populate_collection_assoc_getter_setter(assoc_name, reflection)
203
+ # TODO allow people to specify the possible values of polymorphic associations
204
+ assoc_class = assoc_should_be_untyped?(reflection) ? "T.untyped" : reflection.class_name
205
+ relation_class = relation_should_be_untyped?(reflection) ?
206
+ "ActiveRecord::Relation[T.untyped]" :
207
+ "#{assoc_class}::Relation"
208
+ @generated_sigs.merge!({
209
+ "#{assoc_name}" => { ret: relation_class },
210
+ "#{assoc_name}=" => {
211
+ args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], #{relation_class})" ],
212
+ },
213
+ })
214
+ end
215
+
216
+ def populate_generated_enum_methods
217
+ @model_class.defined_enums.each do |enum_name, enum_hash|
218
+ @generated_class_sigs["#{enum_name.pluralize}"] = { ret: "T::Hash[T.any(String, Symbol), Integer]"}
219
+ enum_hash.keys.each do |enum_val|
220
+ @generated_sigs["#{enum_val}?"] = { ret: "T::Boolean" }
221
+ @generated_scope_sigs["#{enum_val}"] = {
222
+ args: [ name: :args, arg_type: :rest, value_type: 'T.untyped' ],
223
+ ret: "#{@model_class.name}::Relation",
224
+ }
225
+ end
226
+ end
227
+ end
228
+
229
+ def assoc_should_be_untyped?(reflection)
230
+ polymorphic_assoc?(reflection) || !Object.const_defined?(reflection.class_name)
231
+ end
232
+
233
+ def relation_should_be_untyped?(reflection)
234
+ # only type the relation we'll generate
235
+ assoc_should_be_untyped?(reflection) || !@available_classes.include?(reflection.class_name)
236
+ end
237
+
238
+ def polymorphic_assoc?(reflection)
239
+ reflection.through_reflection? ?
240
+ polymorphic_assoc?(reflection.source_reflection) :
241
+ reflection.polymorphic?
242
+ end
243
+
244
+ def draw_class_header
245
+ # We define a custom <ModelName>::Relation class so that it can be extended
246
+ # to contain custom scopes for each models
247
+ <<~MESSAGE
248
+ # This is an autogenerated file for dynamic methods in #{@model_class.name}
249
+ # Please rerun rake rails_rbi:models to regenerate.
250
+ # typed: strong
251
+
252
+ class #{@model_class.name}::Relation < ActiveRecord::Relation
253
+ include #{@model_class.name}::NamedScope
254
+ extend T::Generic
255
+ Elem = type_member(fixed: #{@model_class.name})
256
+ end
257
+
258
+ class #{@model_class.name} < #{@model_class.superclass}
259
+ extend T::Sig
260
+ extend T::Generic
261
+ extend #{@model_class.name}::NamedScope
262
+ Elem = type_template(fixed: #{@model_class.name})
263
+ MESSAGE
264
+ end
265
+
266
+ def draw_class_footer
267
+ "end"
268
+ end
269
+
270
+ def draw_named_scope_header
271
+ <<~MESSAGE
272
+ module #{@model_class.name}::NamedScope
273
+ extend T::Sig
274
+ MESSAGE
275
+ end
276
+
277
+ def draw_named_scope_footer
278
+ "end"
279
+ end
280
+
281
+ def generate_column_methods(buffer)
282
+ @columns_hash.each do |column_name, column_def|
283
+ buffer << draw_column_methods(column_name, column_def)
284
+ end
285
+ end
286
+
287
+ def type_for_column_def(column_def)
288
+ strict_type = RUBY_TO_SORBET_TYPE_MAPPING[column_def.type] || 'T.untyped'
289
+ if column_def.array?
290
+ strict_type = "T::Array[#{strict_type}]"
291
+ end
292
+ column_def.null ? "T.nilable(#{strict_type})" : strict_type
293
+ end
294
+
295
+ def is_method_autogenerated?(method_obj)
296
+ # check if this method is autogenerated or overridden
297
+ # Note: sometimes this is a module, sometimes it's an instance of a class
298
+ return false unless method_obj.source_location.present? # BaseObject
299
+
300
+ owner_name = method_obj.owner.to_s
301
+ source_file = method_obj.source_location[0]
302
+
303
+ (
304
+ [
305
+ "ActiveRecord::AttributeMethods::GeneratedAttributeMethods",
306
+ "GeneratedAssociationMethods",
307
+ "ActiveRecord::Querying",
308
+ "ActiveRecord::AttributeMethods::PrimaryKey",
309
+ ].any? { |k| owner_name.include?(k) } ||
310
+ [
311
+ "lib/active_record/enum.rb",
312
+ "lib/active_record/scoping/named.rb",
313
+ ].any? { |k| source_file.include?(k) }
314
+ )
315
+ end
316
+
317
+ def matched_signature?(method_obj, generated_method_def)
318
+ # use parameters reflection to find method arguments
319
+ actual_params = method_obj.parameters
320
+ expected_args = generated_method_def[:args] || []
321
+ expected_params = expected_args.map { |arg| [arg[:arg_type], arg[:name]] }
322
+ actual_params == expected_params
323
+ end
324
+
325
+ def generate_method_sig(method_name, generated_method_def, is_class_method)
326
+ # generated_method_def:
327
+ # {
328
+ # . ret: <return_type>
329
+ # args: [ name: :value, arg_type: :req, value_type: "T.any(T::Array[#{assoc_class}], ActiveRecord::Relation" ]
330
+ # }
331
+ #
332
+ # Generate something like this
333
+ #
334
+ # sig {returns(T.nilable(String))}
335
+ # .def email; end
336
+ # sig {params(record: T.nilable(String)).void}
337
+ # def email=(record); end
338
+
339
+ param_sig = ""
340
+ param_def = ""
341
+ if generated_method_def[:args]
342
+ sig_args_string = generated_method_def[:args].map { |arg_def|
343
+ "#{arg_def[:name]}: #{arg_def[:value_type]}"
344
+ }.join(", ")
345
+ param_sig = "params(#{sig_args_string})."
346
+
347
+ param_def = generated_method_def[:args].map { |arg_def|
348
+ prefix = ""
349
+ prefix = "*" if arg_def[:arg_type] == :rest
350
+ prefix = "**" if arg_def[:arg_type] == :keyrest
351
+
352
+ "#{prefix}#{arg_def[:name]}"
353
+ }.join(", ")
354
+ end
355
+
356
+ return_type = generated_method_def[:ret] ?
357
+ "returns(#{generated_method_def[:ret]})" :
358
+ "void"
359
+
360
+ prefix = is_class_method ? "self." : ""
361
+
362
+ <<~MESSAGE
363
+ sig { #{param_sig}#{return_type} }
364
+ def #{prefix}#{method_name}(#{param_def}); end
365
+ MESSAGE
366
+ end
367
+ end
@@ -0,0 +1,18 @@
1
+ require "rails"
2
+ require "sorbet-rails/custom_finder_methods"
3
+
4
+ class SorbetRails::Railtie < Rails::Railtie
5
+ railtie_name "sorbet-rails"
6
+
7
+ rake_tasks do
8
+ path = File.expand_path(__dir__)
9
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
10
+ end
11
+
12
+ initializer "sorbet-rails.initialize" do
13
+ ActiveSupport.on_load(:active_record) do
14
+ ActiveRecord::Base.extend SorbetRails::CustomFinderMethods
15
+ ActiveRecord::Relation.include SorbetRails::CustomFinderMethods
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,63 @@
1
+ # typed: true
2
+ class RoutesRbiFormatter
3
+ def initialize
4
+ @buffer = []
5
+ end
6
+
7
+ def section_title(title)
8
+ @buffer << "\n# Section #{title}"
9
+ end
10
+
11
+ def section(routes)
12
+ @buffer << draw_section(routes)
13
+ end
14
+
15
+ def header(routes)
16
+ end
17
+
18
+ def no_routes(routes, filter)
19
+ @buffer <<
20
+ if routes.none?
21
+ <<~MESSAGE
22
+ You do not have any routes defined!
23
+ Please add some routes in config/routes.rb.
24
+ MESSAGE
25
+ elsif filter.key?(:controller)
26
+ "No routes were found for this controller."
27
+ elsif filter.key?(:grep)
28
+ "No routes were found for this grep pattern."
29
+ end
30
+
31
+ @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
32
+ end
33
+
34
+ def result
35
+ <<~MESSAGE
36
+ # This is an autogenerated file for routes helper methods
37
+
38
+ # typed: strong
39
+ class ActionController::Base
40
+ extend T::Sig
41
+
42
+ #{@buffer.join("\n").indent(2)}
43
+ end
44
+ MESSAGE
45
+ end
46
+
47
+ private
48
+ def draw_section(routes)
49
+ routes.map do |r|
50
+ if r[:name].present?
51
+ <<~MESSAGE
52
+ # Sigs for route #{r[:path]}
53
+ sig { params(args: T.untyped, kwargs: T.untyped).returns(String) }
54
+ def #{r[:name]}_path(*args, **kwargs); end
55
+ sig { params(args: T.untyped, kwargs: T.untyped).returns(String) }
56
+ def #{r[:name]}_url(*args, **kwargs); end
57
+ MESSAGE
58
+ else
59
+ nil
60
+ end
61
+ end.compact
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sorbet-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Chan Zuckerberg Initiative
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-04-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: opensource@chanzuckerberg.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - LICENSE
21
+ - README.md
22
+ - lib/sorbet-rails.rb
23
+ - lib/sorbet-rails/custom_finder_methods.rb
24
+ - lib/sorbet-rails/model_rbi_formatter.rb
25
+ - lib/sorbet-rails/railtie.rb
26
+ - lib/sorbet-rails/routes_rbi_formatter.rb
27
+ homepage: https://github.com/chanzuckerberg/sorbet-rails
28
+ licenses:
29
+ - MIT
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.6.14
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Set of tools to make Sorbet work with Rails seamlessly.
51
+ test_files: []