sorbet_factory_bot 0.1.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: 9c5aa26a1fd0c9aef25e9659068f31a9f43cc62c77b4bf7a335a36ee3ce096bc
4
+ data.tar.gz: bed138de6e79f20761c6868245c5e4ddb7504f037c7fc10c36a3e006cd78facc
5
+ SHA512:
6
+ metadata.gz: 899341bbebbd6762aeef1971cd80d4d93de5b59d964456628c627733518542b1ec1b2550911fee262eccb1870218384a880ca6d8bef6814c08b1cfab0de32786
7
+ data.tar.gz: 6980e7e3e13067b700f132ae611838267b0626f6064b08430a0d8565bfb374d661b703a4437e2b1af4be8481871ee8f06d0945d6358c072ea0c6e445a87662af
data/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # SorbetFactoryBot
2
+
3
+ ## Installation
4
+
5
+ Install the gem and add to the application's Gemfile by executing:
6
+
7
+ ```bash
8
+ bundle add sorbet_factory_bot
9
+ ```
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ ```bash
14
+ gem install sorbet_factory_bot
15
+ ```
16
+
17
+ ## Setup
18
+
19
+ Add `sorbet_factory_bot` to your Tapioca require.rb file. When running `tapioca dsl`
20
+ you should see a new `sorbet/rbi/dsl/sorbet_factory_bot/helpers.rbi` file generated.
21
+
22
+ ```ruby
23
+ # sorbet/tapioca/require.rb
24
+ require "sorbet_factory_bot"
25
+ ```
26
+
27
+ In your test helper, include:
28
+
29
+ ```ruby
30
+ # test/test_helper.rb
31
+ require_relative "../path/to/factories.rb"
32
+
33
+ module ActiveSupport
34
+ class TestCase
35
+ include SorbetFactoryBot::Helpers
36
+ SorbetFactoryBot::Helpers.generate_module_helpers!(FactoryBot.factories)
37
+ end
38
+ end
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ `sorbet_factory_bot` generates typesafe shims for each FactoryBot factory:
44
+
45
+ ```ruby
46
+ # Before
47
+ build(:user)
48
+ build_list(:user)
49
+ create(:user, name: "Astrid")
50
+ create_list(:user)
51
+
52
+ # After
53
+ build_user()
54
+ build_user_list()
55
+ create_user(name: "Astrid")
56
+ create_user_list()
57
+ ```
58
+
59
+ ### Traits
60
+
61
+ FactoryBot allows passing trait names as symbols directly into the builder methods.
62
+ This is not compatible with how Sorbet defines signatures, so use the `traits_`
63
+ keyword argument to pass a list of traits.
64
+
65
+
66
+ ```ruby
67
+ # Before
68
+ create(:user, :trait1, name: "Astrid")
69
+
70
+ # After
71
+ create_user(traits_: [:trait_1], name: "Astrid")
72
+ ```
73
+
74
+ ## Development
75
+
76
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
77
+
78
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
79
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1,37 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "factory_bot"
5
+ require "sorbet-runtime"
6
+
7
+ module SorbetFactoryBot
8
+ module Helpers
9
+ extend ::T::Helpers
10
+ extend ::T::Sig
11
+
12
+ sig { params(registry: FactoryBot::Registry).void }
13
+ def generate_model_helpers!(registry)
14
+ registry.each do |factory|
15
+ name = factory.name
16
+
17
+ # For each factory, generate a corresponding wrapper that has Tapioca-generated
18
+ # signatures.
19
+ T.unsafe(self).define_method(:"build_#{name}") do |traits_: [], **overrides, &blk|
20
+ T.unsafe(FactoryBot).build(name, *traits_, **overrides, &blk)
21
+ end
22
+ T.unsafe(self).define_method(:"create_#{name}") do |traits_: [], **overrides, &blk|
23
+ T.unsafe(FactoryBot).create(name, *traits_, **overrides, &blk)
24
+ end
25
+
26
+ T.unsafe(self).define_method(:"build_#{name}_list") do |count_, traits_: [], **overrides, &blk|
27
+ T.unsafe(FactoryBot).build_list(name, count_, *traits_, **overrides, &blk)
28
+ end
29
+ T.unsafe(self).define_method(:"create_#{name}_list") do |count_, traits_: [], **overrides, &blk|
30
+ T.unsafe(FactoryBot).create_list(name, count_, *traits_, **overrides, &blk)
31
+ end
32
+ end
33
+ end
34
+
35
+ extend self
36
+ end
37
+ end
@@ -0,0 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module SorbetFactoryBot
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,8 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "sorbet_factory_bot/helpers"
5
+ require_relative "sorbet_factory_bot/version"
6
+ require_relative "tapioca/runtime/reflection_monkey_patch"
7
+
8
+ module SorbetFactoryBot; end
@@ -0,0 +1,250 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "tapioca/dsl"
5
+ require "tapioca/dsl/compiler"
6
+
7
+ require_relative "../../../sorbet_factory_bot/helpers"
8
+
9
+ module Tapioca
10
+ module Dsl
11
+ module Compilers
12
+ # This Tapioca compiler generates RBI for FactoryBot factories that back onto Rails models.
13
+ # The wrapper methods generated in SorbetFactoryBot::Helpers have a few key differences from
14
+ # the built-in interface:
15
+ # * Traits are specified via an optional `trait_` kwarg for all methods.
16
+ # Example: build_person(traits_: [:active], name: "Edgar")
17
+ class SorbetFactoryBotCompiler < Tapioca::Dsl::Compiler
18
+ extend T::Sig
19
+ extend T::Generic
20
+
21
+ ConstantType = type_member { { fixed: T.class_of(::SorbetFactoryBot::Helpers) } }
22
+
23
+ RBI_MODULE_NAME = "SorbetFactoryBotMethods"
24
+
25
+ class << self
26
+ extend T::Sig
27
+
28
+ sig { override.returns(T::Enumerable[T::Module[T.anything]]) }
29
+ def gather_constants
30
+ Array(SorbetFactoryBot::Helpers)
31
+ end
32
+ end
33
+
34
+ sig { override.void }
35
+ def decorate
36
+ root.create_path(constant) do |klass|
37
+ klass.create_module(RBI_MODULE_NAME) do |methods_mod|
38
+ if FactoryBot.factories.count == 0 # rubocop:disable Style/NumericPredicate
39
+ if defined?(::Rails)
40
+ # If there are no factories, check in the Rails root for some common paths
41
+ [
42
+ Rails.root.join("factories"),
43
+ Rails.root.join("factories.rb"),
44
+ Rails.root.join("test", "factories"),
45
+ Rails.root.join("test", "factories.rb"),
46
+ ].each do |path|
47
+ if path.exist?
48
+ if path.ftype == "file"
49
+ require_relative path.to_s
50
+ break
51
+ elsif path.ftype == "directory"
52
+ require_relative File.join(path, "require")
53
+ break
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ FactoryBot.factories.each do |factory|
61
+ model = model_klass(factory)
62
+ next if model.nil?
63
+
64
+ name = factory.name
65
+ base_params = generate_parameters(factory, model)
66
+ return_type = model.name
67
+
68
+ methods_mod.create_method(
69
+ "build_#{name}",
70
+ parameters: base_params,
71
+ return_type: return_type,
72
+ )
73
+ methods_mod.create_method(
74
+ "create_#{name}",
75
+ parameters: base_params,
76
+ return_type: return_type,
77
+ )
78
+
79
+ methods_mod.create_method(
80
+ "build_#{name}_list",
81
+ parameters: [
82
+ create_param("count_", type: "Integer"),
83
+ ] + base_params,
84
+ return_type: "T::Array[#{return_type}]",
85
+ )
86
+ methods_mod.create_method(
87
+ "create_#{name}_list",
88
+ parameters: [
89
+ create_param("count_", type: "Integer"),
90
+ ] + base_params,
91
+ return_type: "T::Array[#{return_type}]",
92
+ )
93
+ end
94
+ end
95
+ klass.create_include(RBI_MODULE_NAME)
96
+ end
97
+ end
98
+
99
+ sig { params(factory: FactoryBot::Factory).returns(T.nilable(FactoryBot::Factory)) }
100
+ def parent_factory(factory)
101
+ parent = factory.send(:parent)
102
+ if !parent.is_a?(FactoryBot::NullFactory)
103
+ parent
104
+ end
105
+ end
106
+
107
+ sig { params(factory: FactoryBot::Factory).returns(FactoryBot::Factory) }
108
+ def root_factory(factory)
109
+ if parent = parent_factory(factory)
110
+ root_factory(parent)
111
+ else
112
+ factory
113
+ end
114
+ end
115
+
116
+ sig { params(factory: FactoryBot::Factory).returns(T.nilable(T.class_of(ActiveRecord::Base))) }
117
+ def model_klass(factory)
118
+ root = root_factory(factory)
119
+
120
+ begin
121
+ klass = Object.const_get(root.name.to_s.classify) # rubocop:disable Sorbet/ConstantsFromStrings
122
+ klass if klass < ActiveRecord::Base
123
+ rescue NameError
124
+ nil
125
+ end
126
+ end
127
+
128
+ sig do
129
+ params(
130
+ factory: FactoryBot::Factory,
131
+ model: T.class_of(ActiveRecord::Base),
132
+ ).returns(T::Array[RBI::TypedParam])
133
+ end
134
+ def generate_parameters(factory, model)
135
+ [ create_kw_opt_param("traits_", type: "T::Array[Symbol]", default: "[]") ] +
136
+ generate_parameters_hash(factory, model).values +
137
+ [ create_block_param("blk", type: "T.nilable(T.proc.params(arg0: #{model}).void)") ]
138
+ end
139
+
140
+ sig do
141
+ params(
142
+ factory: FactoryBot::Factory,
143
+ model: T.class_of(ActiveRecord::Base),
144
+ ).returns(T::Hash[Symbol, RBI::TypedParam])
145
+ end
146
+ def generate_parameters_hash(factory, model)
147
+ params = generate_parameters_from_model(model, factory.definition.declarations)
148
+
149
+ parent = parent_factory(factory)
150
+ parent_params =
151
+ if parent
152
+ generate_parameters_hash(parent, model)
153
+ else
154
+ {}
155
+ end
156
+
157
+ # Clobber any collisions in parent in case fields are redeclared
158
+ parent_params.merge(params)
159
+ end
160
+
161
+ sig do
162
+ params(
163
+ model_klass: T.class_of(ActiveRecord::Base),
164
+ declarations: FactoryBot::DeclarationList,
165
+ ).returns(
166
+ T::Hash[Symbol, RBI::TypedParam],
167
+ )
168
+ end
169
+ def generate_parameters_from_model(model_klass, declarations)
170
+ declarations.map do |d|
171
+ type = type_for_declaration(model_klass, d)
172
+ [ d.name, create_kw_opt_param(d.name, type: nilable(type), default: "nil") ]
173
+ end.to_h
174
+ end
175
+
176
+ sig do
177
+ params(
178
+ model_klass: T.class_of(ActiveRecord::Base),
179
+ declaration: FactoryBot::Declaration,
180
+ ).returns(String)
181
+ end
182
+ def type_for_declaration(model_klass, declaration)
183
+ # If the model uses RailsSorbetEnum, use the enum class rather than the column type
184
+ if model_klass.respond_to?(:sorbet_enum_attributes)
185
+ enum_klass = T.unsafe(model_klass).sorbet_enum_attributes.to_h[declaration.name]
186
+ return enum_klass if enum_klass
187
+ end
188
+
189
+ if col_type = model_klass.column_for_attribute(declaration.name.to_s).type
190
+ convert_db_type_to_sorbet(col_type)
191
+ elsif col_klass = model_klass.reflect_on_association(declaration.name.to_s)
192
+ convert_association_to_sorbet(col_klass)
193
+ else
194
+ "T.untyped"
195
+ end
196
+ end
197
+
198
+ sig { params(type: Symbol).returns(String) }
199
+ def convert_db_type_to_sorbet(type)
200
+ case type
201
+ when :big_integer
202
+ "Integer"
203
+ when :binary
204
+ "T.untyped"
205
+ when :boolean
206
+ "T::Boolean"
207
+ when :date
208
+ "T.untyped"
209
+ when :datetime
210
+ "ActiveSupport::TimeWithZone"
211
+ when :decimal
212
+ "Float"
213
+ when :float
214
+ "Float"
215
+ when :integer
216
+ "Integer"
217
+ when :string
218
+ "String"
219
+ else
220
+ "T.untyped"
221
+ end
222
+ end
223
+
224
+ sig { params(col_klass: ActiveRecord::Reflection::AbstractReflection).returns(String) }
225
+ def convert_association_to_sorbet(col_klass)
226
+ if T.unsafe(col_klass).polymorphic?
227
+ return "T.untyped"
228
+ end
229
+
230
+ case col_klass
231
+ when ActiveRecord::Reflection::HasManyReflection,
232
+ ActiveRecord::Reflection::HasAndBelongsToManyReflection
233
+ "T::Array[#{col_klass.klass.name}]"
234
+ else
235
+ T.unsafe(col_klass).klass.name
236
+ end
237
+ end
238
+
239
+ sig { params(type: String).returns(String) }
240
+ def nilable(type)
241
+ if type == "T.untyped"
242
+ type
243
+ else
244
+ "T.nilable(#{type})"
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,27 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ # rubocop:disable Sorbet
5
+ module Tapioca
6
+ module Runtime
7
+ # This just monkey patches the constantize method to rescue ActiveRecord::AdapterNotSpecified
8
+ # This is needed as a workaround for: https://github.com/Shopify/tapioca/issues/2004
9
+ #
10
+ # See also ./sorbet/rbi/shims/gems/tapioca.rbi
11
+ module ReflectionMonkeyPatch
12
+ # @without_runtime
13
+ # (String symbol, ?inherit: bool, ?namespace: Module) -> BasicObject
14
+ def constantize(symbol, inherit: false, namespace: Object)
15
+ namespace.const_get(symbol, inherit)
16
+ rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError, ActiveRecord::AdapterNotSpecified, ActiveRecord::ConnectionNotEstablished
17
+ ::Tapioca::Runtime::Reflection::UNDEFINED_CONSTANT
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # https://github.com/Shopify/tapioca/blob/main/lib/tapioca/runtime/reflection.rb
24
+ if defined?(::Tapioca::Runtime::Reflection)
25
+ Tapioca::Runtime::Reflection.prepend(Tapioca::Runtime::ReflectionMonkeyPatch)
26
+ end
27
+ # rubocop:enable Sorbet
@@ -0,0 +1,4 @@
1
+ module SorbetFactoryBot
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sorbet_factory_bot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Franklin Hu
8
+ - Jay Shirley
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: factory_bot
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: tapioca
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0.17'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sorbet-static-and-runtime
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0.5'
55
+ description: For projects that use FactoryBot and Sorbet, this generates type signatures
56
+ for FactoryBot factories
57
+ email:
58
+ - franklin@thisisfranklin.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - Rakefile
65
+ - lib/sorbet_factory_bot.rb
66
+ - lib/sorbet_factory_bot/helpers.rb
67
+ - lib/sorbet_factory_bot/version.rb
68
+ - lib/tapioca/dsl/compilers/sorbet_factory_bot_compiler.rb
69
+ - lib/tapioca/runtime/reflection_monkey_patch.rb
70
+ - sig/sorbet_factory_bot.rbs
71
+ homepage: https://github.com/jointbits/sorbet_factory_bot
72
+ licenses: []
73
+ metadata:
74
+ homepage_uri: https://github.com/jointbits/sorbet_factory_bot
75
+ source_code_uri: https://github.com/jointbits/sorbet_factory_bot
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 3.2.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.6.9
91
+ specification_version: 4
92
+ summary: Helpers for generating Sorbet RBI via Tapioca for FactoryBot factories
93
+ test_files: []