boba 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27ee83d22576856c1cc2c546cd1c49bb69ccbe5dc168858be21ffa0bce985e62
4
- data.tar.gz: 146fd90b3e40b8db91b1a71ca8b850594922a0178ef4e8cb4a18d46ba94eaca1
3
+ metadata.gz: 76f99ba4fafb5f1a55c1abb60b0c099569f3f4153c4f0f0b43ced158e02fb59b
4
+ data.tar.gz: 1de40e562d0dbeb3c0b2a987cd4e2898321a62550a239941cb129f5690da042d
5
5
  SHA512:
6
- metadata.gz: c8bbdbedbc56f12ac239dd8ad75c04bc6782d2b8448dc7f289c430d9ea592458f44b3461c304daaff218689bad83c4779a701d449d14f2d6babf7c2bb06c367a
7
- data.tar.gz: '068e9cf37630c8fd608a04280b35a46f769a432ecb6605cedbc7c03991d1cec5d6f67306a38803069e0eaa8be5f27889c362b5fb9aa622714653775540e7c88d'
6
+ metadata.gz: fbc7d9e5ee8248a1175e499f891716d0397136cc20e1e6db65c22c6d61d777ea756c2c992e48694375358a748986bca2bd2c70393601c8185faf49e1ea7f9e39
7
+ data.tar.gz: 23d4d817f6b1a14345599d027c5fbed2203a32709d1cd955568ee69b7475b4551e31308936c02d4e2c26c403af292070f301c9258eed40fdba0b17414ad346f4
data/README.md CHANGED
@@ -6,8 +6,11 @@ Boba is a collection of compilers for Sorbet & Tapioca.
6
6
 
7
7
  Tapioca is very opinionated about what types of compilers or changes are accepted into the repository. See
8
8
  [here](https://github.com/Shopify/tapioca?tab=readme-ov-file#dsl-compilers). Boba come in all
9
- different shapes, sizes, consistencies, etc, and is much less opinionated about what is or is not accepted. Think of
10
- Boba like a collection of compilers that you can pick and choose from.
9
+ different shapes, sizes, consistencies, etc, and is much less opinionated about what is or is not accepted. Boba is a collection of optional compilers that you can pick and choose from.
10
+
11
+ ### Available Compilers
12
+
13
+ See [the compilers manual](https://github.com/angellist/boba/blob/main/manual/compilers.md) for a list of available compilers.
11
14
 
12
15
  ## Usage
13
16
 
@@ -26,10 +29,24 @@ dsl:
26
29
  Compiler1
27
30
  Compiler2
28
31
  ```
32
+ This makes it easy to selectively enable only the compilers you want to use in your project.
33
+
34
+ ## Contributing
35
+
36
+ Bugs and feature requests are welcome and should be [filed as issues on github](https://github.com/angellist/boba/issues).
37
+
38
+ ### New Compilers
39
+
40
+ Compilers for any commonly used Ruby or Rails gems are welcome to be contributed. See the [Writing New Compilers section of the Tapioca docs](https://github.com/Shopify/tapioca?tab=readme-ov-file#writing-custom-dsl-compilers) for an introduction to writing compilers.
41
+
42
+ Since Boba is intended to be used alongside Tapioca and the compilers provided by Boba are intended to be fully optional,
43
+ we will not accept compilers which overwrite the Tapioca default compilers. See the [Tapioca Manual](https://github.com/Shopify/tapioca/blob/main/manual/compilers.md) for a list of these
44
+ compilers. Instead, compilers which extend or overwrite the default Tapioca compilers should be given unique names.
45
+
46
+ Contributed compilers should be well documented, and named after and include a link or reference to the Gem, DSL, or other module they implement RBIs for.
47
+
48
+ Compilers for Gems, DSLs, or modules that are not publicly available will not be accepted.
29
49
 
30
50
  ## Todo
31
51
 
32
- 1. Contributing Section
33
- 2. Specs & spec harness
34
- 3. Docs & doc harness
35
- 4. Rubocop
52
+ 1. Specs & spec harness
data/lib/boba/version.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
1
4
  module Boba
2
- VERSION = '0.0.2'
5
+ VERSION = "0.0.3"
3
6
  end
@@ -0,0 +1,225 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(ActiveRecord::Base)
5
+
6
+ module Tapioca
7
+ module Dsl
8
+ module Compilers
9
+ # `Tapioca::Dsl::Compilers::ActiveRecordAssociationsPersisted` extends the default Tapioca compiler `Tapioca::Dsl::Compilers::ActiveRecordAssociations`
10
+ # to provide an option to generate RBI files for associations on models assuming that the model is persisted. These sigs therefore respect
11
+ # validations and DB constraints, and generate non-nilable types for associations that are required or non-optional.
12
+ #
13
+ # This compiler accepts a `ActiveRecordAssociationTypes` option that can be used to specify
14
+ # how the types of `belongs_to` and `has_one` associations should be generated. The option can be one of the
15
+ # following:
16
+ # - `nilable (_default_)`: All association methods will be generated with `T.nilable` return types. This is
17
+ # strictly the most correct way to type the methods, but it can make working with the models more cumbersome, as
18
+ # you will have to handle the `nil` cases explicitly using `T.must` or the safe navigation operator `&.`, even
19
+ # for valid persisted models.
20
+ # - `persisted`: The methods will be generated with the type that matches validations on the association. If
21
+ # there is a `required: true` or `optional: false`, then the types will be generated as non-nilable. This mode
22
+ # basically treats each model as if it was a valid and persisted model. Note that this makes typing Active Record
23
+ # models easier, but does not match the behaviour of non-persisted or invalid models, which can have `nil`
24
+ # associations.
25
+ #
26
+ # For example, with the following model class:
27
+ #
28
+ # ~~~rb
29
+ # class Post < ActiveRecord::Base
30
+ # belongs_to :category
31
+ # has_many :comments
32
+ # has_one :author, class_name: "User", optional: false
33
+ #
34
+ # accepts_nested_attributes_for :category, :comments, :author
35
+ # end
36
+ # ~~~
37
+ #
38
+ # By default, the compiler will generate types consistent with `Tapioca::Dsl::Compilers::ActiveRecordAssociationsPersisted`.
39
+ # If `ActiveRecordAssociationTypes` is `persisted`, the `author` method will be generated as:
40
+ # ~~~rbi
41
+ # sig { returns(::User) }
42
+ # def author; end
43
+ # ~~~
44
+ # and if the option is set to `untyped`, the `author` method will be generated as:
45
+ # ~~~rbi
46
+ # sig { returns(T.untyped) }
47
+ # def author; end
48
+ # ~~~
49
+ class ActiveRecordAssociationsPersisted < ::Tapioca::Dsl::Compilers::ActiveRecordAssociations
50
+ extend T::Sig
51
+
52
+ class AssociationTypeOption < T::Enum
53
+ extend T::Sig
54
+
55
+ enums do
56
+ Nilable = new("nilable")
57
+ Persisted = new("persisted")
58
+ end
59
+
60
+ class << self
61
+ extend T::Sig
62
+
63
+ sig do
64
+ params(
65
+ options: T::Hash[String, T.untyped],
66
+ block: T.proc.params(value: String, default_association_type_option: AssociationTypeOption).void,
67
+ ).returns(AssociationTypeOption)
68
+ end
69
+ def from_options(options, &block)
70
+ association_type_option = Nilable
71
+ value = options["ActiveRecordAssociationTypes"]
72
+
73
+ if value
74
+ if has_serialized?(value)
75
+ association_type_option = from_serialized(value)
76
+ else
77
+ block.call(value, association_type_option)
78
+ end
79
+ end
80
+
81
+ association_type_option
82
+ end
83
+ end
84
+
85
+ sig { returns(T::Boolean) }
86
+ def persisted?
87
+ self == AssociationTypeOption::Persisted
88
+ end
89
+
90
+ sig { returns(T::Boolean) }
91
+ def nilable?
92
+ self == AssociationTypeOption::Nilable
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ sig { returns(AssociationTypeOption) }
99
+ def association_type_option
100
+ @association_type_option ||= T.let(
101
+ AssociationTypeOption.from_options(options) do |value, default_association_type_option|
102
+ add_error(<<~MSG.strip)
103
+ Unknown value for compiler option `ActiveRecordAssociationTypes` given: `#{value}`.
104
+ Proceeding with the default value: `#{default_association_type_option.serialize}`.
105
+ MSG
106
+ end,
107
+ T.nilable(AssociationTypeOption),
108
+ )
109
+ end
110
+
111
+ sig do
112
+ params(
113
+ klass: RBI::Scope,
114
+ association_name: T.any(String, Symbol),
115
+ reflection: ReflectionType,
116
+ ).void
117
+ end
118
+ def populate_single_assoc_getter_setter(klass, association_name, reflection)
119
+ association_class = type_for(reflection)
120
+ association_type = single_association_type_for(reflection)
121
+ association_methods_module = constant.generated_association_methods
122
+
123
+ klass.create_method(
124
+ association_name.to_s,
125
+ return_type: association_type,
126
+ )
127
+ klass.create_method(
128
+ "#{association_name}=",
129
+ parameters: [create_param("value", type: association_type)],
130
+ return_type: "void",
131
+ )
132
+ klass.create_method(
133
+ "reload_#{association_name}",
134
+ return_type: association_type,
135
+ )
136
+ klass.create_method(
137
+ "reset_#{association_name}",
138
+ return_type: "void",
139
+ )
140
+ if association_methods_module.method_defined?("#{association_name}_changed?")
141
+ klass.create_method(
142
+ "#{association_name}_changed?",
143
+ return_type: "T::Boolean",
144
+ )
145
+ end
146
+ if association_methods_module.method_defined?("#{association_name}_previously_changed?")
147
+ klass.create_method(
148
+ "#{association_name}_previously_changed?",
149
+ return_type: "T::Boolean",
150
+ )
151
+ end
152
+ unless reflection.polymorphic?
153
+ klass.create_method(
154
+ "build_#{association_name}",
155
+ parameters: [
156
+ create_rest_param("args", type: "T.untyped"),
157
+ create_block_param("blk", type: "T.untyped"),
158
+ ],
159
+ return_type: association_class,
160
+ )
161
+ klass.create_method(
162
+ "create_#{association_name}",
163
+ parameters: [
164
+ create_rest_param("args", type: "T.untyped"),
165
+ create_block_param("blk", type: "T.untyped"),
166
+ ],
167
+ return_type: association_class,
168
+ )
169
+ klass.create_method(
170
+ "create_#{association_name}!",
171
+ parameters: [
172
+ create_rest_param("args", type: "T.untyped"),
173
+ create_block_param("blk", type: "T.untyped"),
174
+ ],
175
+ return_type: association_class,
176
+ )
177
+ end
178
+ end
179
+
180
+ sig do
181
+ params(
182
+ reflection: ReflectionType,
183
+ ).returns(String)
184
+ end
185
+ def single_association_type_for(reflection)
186
+ association_class = type_for(reflection)
187
+ return as_nilable_type(association_class) unless association_type_option.persisted?
188
+
189
+ if has_one_and_required_reflection?(reflection) || belongs_to_and_non_optional_reflection?(reflection)
190
+ association_class
191
+ else
192
+ as_nilable_type(association_class)
193
+ end
194
+ end
195
+
196
+ # Note - one can do more here. If the association's attribute has an unconditional presence validation, it
197
+ # should also be considered required.
198
+ sig { params(reflection: ReflectionType).returns(T::Boolean) }
199
+ def has_one_and_required_reflection?(reflection)
200
+ reflection.has_one? && !!reflection.options[:required]
201
+ end
202
+
203
+ # Note - one can do more here. If the FK defining the belongs_to association is non-nullable at the DB level, or
204
+ # if the association's attribute has an unconditional presence validation, it should also be considered
205
+ # non-optional.
206
+ sig { params(reflection: ReflectionType).returns(T::Boolean) }
207
+ def belongs_to_and_non_optional_reflection?(reflection)
208
+ return false unless reflection.belongs_to?
209
+
210
+ optional = if reflection.options.key?(:required)
211
+ !reflection.options[:required]
212
+ else
213
+ reflection.options[:optional]
214
+ end
215
+
216
+ if optional.nil?
217
+ !!reflection.active_record.belongs_to_required_by_default
218
+ else
219
+ !optional
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,358 @@
1
+ # typed: ignore
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(ActiveRecord::Base)
5
+
6
+ require "tapioca/dsl/helpers/active_record_column_type_helper"
7
+
8
+ module Tapioca
9
+ module Dsl
10
+ module Compilers
11
+ # `Tapioca::Dsl::Compilers::ActiveRecordColumnsPersisted` is an extension of the default Tapioca compiler `Tapioca::Dsl::Compilers::ActiveRecordColumns`.
12
+ # It extends the `persisted` option of the `ActiveRecordColumnTypes` to respect not only database constraints, but
13
+ # also validations on the attributes in the model.
14
+ #
15
+ # [`ActiveRecord::Base`](https://api.rubyonrails.org/classes/ActiveRecord/Base.html).
16
+ # This compiler is only responsible for defining the attribute methods that would be
17
+ # created for columns and virtual attributes that are defined in the Active Record
18
+ # model.
19
+ #
20
+ # This compiler accepts a `ActiveRecordColumnTypes` option that can be used to specify
21
+ # how the types of the column related methods should be generated. The option can be one of the following:
22
+ # - `persisted` (_default_): The methods will be generated with the type that matches the actual database
23
+ # column type as the return type. This means that if the column is a string, the method return type
24
+ # will be `String`, but if the column is also nullable, then the return type will be `T.nilable(String)`. This
25
+ # mode basically treats each model as if it was a valid and persisted model. Note that this makes typing
26
+ # Active Record models easier, but does not match the behaviour of non-persisted or invalid models, which can
27
+ # have all kinds of non-sensical values in their column attributes.
28
+ # - `nilable`: All column methods will be generated with `T.nilable` return types. This is strictly the most
29
+ # correct way to type the methods, but it can make working with the models more cumbersome, as you will have to
30
+ # handle the `nil` cases explicitly using `T.must` or the safe navigation operator `&.`, even for valid
31
+ # persisted models.
32
+ # - `untyped`: The methods will be generated with `T.untyped` return types. This mode is practical if you are not
33
+ # ready to start typing your models strictly yet, but still want to generate RBI files for them.
34
+ #
35
+ # For example, with the following model class:
36
+ # ~~~rb
37
+ # class Post < ActiveRecord::Base
38
+ # end
39
+ # ~~~
40
+ #
41
+ # and the following database schema:
42
+ #
43
+ # ~~~rb
44
+ # # db/schema.rb
45
+ # create_table :posts do |t|
46
+ # t.string :title, null: false
47
+ # t.string :body
48
+ # t.boolean :published
49
+ # t.timestamps
50
+ # end
51
+ # ~~~
52
+ #
53
+ # this compiler will, by default, produce the following methods in the RBI file
54
+ # `post.rbi`:
55
+ #
56
+ # ~~~rbi
57
+ # # post.rbi
58
+ # # typed: true
59
+ # class Post
60
+ # include GeneratedAttributeMethods
61
+ #
62
+ # module GeneratedAttributeMethods
63
+ # sig { returns(T.nilable(::String)) }
64
+ # def body; end
65
+ #
66
+ # sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
67
+ # def body=; end
68
+ #
69
+ # sig { returns(T::Boolean) }
70
+ # def body?; end
71
+ #
72
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
73
+ # def created_at; end
74
+ #
75
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
76
+ # def created_at=; end
77
+ #
78
+ # sig { returns(T::Boolean) }
79
+ # def created_at?; end
80
+ #
81
+ # sig { returns(T.nilable(T::Boolean)) }
82
+ # def published; end
83
+ #
84
+ # sig { params(value: T::Boolean).returns(T::Boolean) }
85
+ # def published=; end
86
+ #
87
+ # sig { returns(T::Boolean) }
88
+ # def published?; end
89
+ #
90
+ # sig { returns(::String) }
91
+ # def title; end
92
+ #
93
+ # sig { params(value: ::String).returns(::String) }
94
+ # def title=(value); end
95
+ #
96
+ # sig { returns(T::Boolean) }
97
+ # def title?; end
98
+ #
99
+ # sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
100
+ # def updated_at; end
101
+ #
102
+ # sig { params(value: ::ActiveSupport::TimeWithZone).returns(::ActiveSupport::TimeWithZone) }
103
+ # def updated_at=; end
104
+ #
105
+ # sig { returns(T::Boolean) }
106
+ # def updated_at?; end
107
+ #
108
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/Dirty.html
109
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveModel/Dirty.html
110
+ # ## Also the methods added by https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
111
+ # end
112
+ # end
113
+ # ~~~
114
+ #
115
+ # However, if `ActiveRecordColumnTypes` is set to `nilable`, the `title` method will be generated as:
116
+ # ~~~rbi
117
+ # sig { returns(T.nilable(::String)) }
118
+ # def title; end
119
+ # ~~~
120
+ # and if the option is set to `untyped`, the `title` method will be generated as:
121
+ # ~~~rbi
122
+ # sig { returns(T.untyped) }
123
+ # def title; end
124
+ # ~~~
125
+ class ActiveRecordColumnsPersisted < ::Tapioca::Dsl::Compilers::ActiveRecordColumns
126
+ extend T::Sig
127
+
128
+ private
129
+
130
+ def column_type_helper
131
+ ::Tapioca::Dsl::Helpers::ActiveRecordColumnTypeHelper.new(
132
+ constant,
133
+ column_type_option: column_type_option,
134
+ )
135
+ end
136
+
137
+ sig do
138
+ params(
139
+ attribute_name: String,
140
+ column_name: String,
141
+ ).returns([String, String])
142
+ end
143
+ def type_for(attribute_name, column_name = attribute_name)
144
+ return column_type_helper.send(:id_type) if attribute_name == "id"
145
+
146
+ column_type_for(column_name)
147
+ end
148
+
149
+ sig { params(column_name: String).returns([String, String]) }
150
+ def column_type_for(column_name)
151
+ return ["T.untyped", "T.untyped"] if @column_type_option.untyped?
152
+
153
+ nilable_column = !has_non_null_database_constraint?(column_name) &&
154
+ !has_unconditional_presence_validator?(column_name)
155
+
156
+ column_type = @constant.attribute_types[column_name]
157
+ getter_type = column_type_helper.send(
158
+ :type_for_activerecord_value,
159
+ column_type,
160
+ column_nullability: nilable_column,
161
+ )
162
+ setter_type =
163
+ case column_type
164
+ when ActiveRecord::Enum::EnumType
165
+ column_type_helper.send(:enum_setter_type, column_type)
166
+ else
167
+ getter_type
168
+ end
169
+
170
+ if @column_type_option.persisted? && (virtual_attribute?(column_name) || !nilable_column)
171
+ [getter_type, setter_type]
172
+ else
173
+ getter_type = as_nilable_type(getter_type) unless column_type_helper.send(
174
+ :not_nilable_serialized_column?,
175
+ column_type,
176
+ )
177
+ [getter_type, as_nilable_type(setter_type)]
178
+ end
179
+ end
180
+
181
+ sig { params(column_name: String).returns(T::Boolean) }
182
+ def virtual_attribute?(column_name)
183
+ @constant.columns_hash[column_name].nil?
184
+ end
185
+
186
+ sig { params(column_name: String).returns(T::Boolean) }
187
+ def has_non_null_database_constraint?(column_name)
188
+ column = @constant.columns_hash[column_name]
189
+ return false if column.nil?
190
+
191
+ !column.null
192
+ end
193
+
194
+ sig { params(column_name: String).returns(T::Boolean) }
195
+ def has_unconditional_presence_validator?(column_name)
196
+ return false unless @constant.respond_to?(:validators_on)
197
+
198
+ @constant.validators_on(column_name).any? do |validator|
199
+ next false unless validator.is_a?(ActiveRecord::Validations::PresenceValidator)
200
+
201
+ !validator.options.key?(:if) && !validator.options.key?(:unless) && !validator.options.key?(:on)
202
+ end
203
+ end
204
+
205
+ sig do
206
+ params(
207
+ klass: RBI::Scope,
208
+ attribute_name: String,
209
+ column_name: String,
210
+ methods_to_add: T.nilable(T::Array[String]),
211
+ ).void
212
+ end
213
+ def add_methods_for_attribute(klass, attribute_name, column_name = attribute_name, methods_to_add = nil)
214
+ getter_type, setter_type = type_for(attribute_name, column_name)
215
+
216
+ # Added by ActiveRecord::AttributeMethods::Read
217
+ #
218
+ add_method(
219
+ klass,
220
+ attribute_name.to_s,
221
+ methods_to_add,
222
+ return_type: getter_type,
223
+ )
224
+
225
+ # Added by ActiveRecord::AttributeMethods::Write
226
+ #
227
+ add_method(
228
+ klass,
229
+ "#{attribute_name}=",
230
+ methods_to_add,
231
+ parameters: [create_param("value", type: setter_type)],
232
+ return_type: setter_type,
233
+ )
234
+
235
+ # Added by ActiveRecord::AttributeMethods::Query
236
+ #
237
+ add_method(
238
+ klass,
239
+ "#{attribute_name}?",
240
+ methods_to_add,
241
+ return_type: "T::Boolean",
242
+ )
243
+
244
+ # Added by ActiveRecord::AttributeMethods::Dirty
245
+ #
246
+ add_method(
247
+ klass,
248
+ "#{attribute_name}_before_last_save",
249
+ methods_to_add,
250
+ return_type: as_nilable_type(getter_type),
251
+ )
252
+ add_method(
253
+ klass,
254
+ "#{attribute_name}_change_to_be_saved",
255
+ methods_to_add,
256
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])",
257
+ )
258
+ add_method(
259
+ klass,
260
+ "#{attribute_name}_in_database",
261
+ methods_to_add,
262
+ return_type: as_nilable_type(getter_type),
263
+ )
264
+ add_method(
265
+ klass,
266
+ "saved_change_to_#{attribute_name}",
267
+ methods_to_add,
268
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])",
269
+ )
270
+ add_method(
271
+ klass,
272
+ "saved_change_to_#{attribute_name}?",
273
+ methods_to_add,
274
+ return_type: "T::Boolean",
275
+ )
276
+ add_method(
277
+ klass,
278
+ "will_save_change_to_#{attribute_name}?",
279
+ methods_to_add,
280
+ return_type: "T::Boolean",
281
+ )
282
+
283
+ # Added by ActiveModel::Dirty
284
+ #
285
+ add_method(
286
+ klass,
287
+ "#{attribute_name}_change",
288
+ methods_to_add,
289
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])",
290
+ )
291
+ add_method(
292
+ klass,
293
+ "#{attribute_name}_changed?",
294
+ methods_to_add,
295
+ return_type: "T::Boolean",
296
+ parameters: [
297
+ create_kw_opt_param("from", type: setter_type, default: "T.unsafe(nil)"),
298
+ create_kw_opt_param("to", type: setter_type, default: "T.unsafe(nil)"),
299
+ ],
300
+ )
301
+ add_method(
302
+ klass,
303
+ "#{attribute_name}_will_change!",
304
+ methods_to_add,
305
+ )
306
+ add_method(
307
+ klass,
308
+ "#{attribute_name}_was",
309
+ methods_to_add,
310
+ return_type: as_nilable_type(getter_type),
311
+ )
312
+ add_method(
313
+ klass,
314
+ "#{attribute_name}_previous_change",
315
+ methods_to_add,
316
+ return_type: "T.nilable([#{getter_type}, #{getter_type}])",
317
+ )
318
+ add_method(
319
+ klass,
320
+ "#{attribute_name}_previously_changed?",
321
+ methods_to_add,
322
+ return_type: "T::Boolean",
323
+ parameters: [
324
+ create_kw_opt_param("from", type: setter_type, default: "T.unsafe(nil)"),
325
+ create_kw_opt_param("to", type: setter_type, default: "T.unsafe(nil)"),
326
+ ],
327
+ )
328
+ add_method(
329
+ klass,
330
+ "#{attribute_name}_previously_was",
331
+ methods_to_add,
332
+ return_type: as_nilable_type(getter_type),
333
+ )
334
+ add_method(
335
+ klass,
336
+ "restore_#{attribute_name}!",
337
+ methods_to_add,
338
+ )
339
+
340
+ # Added by ActiveRecord::AttributeMethods::BeforeTypeCast
341
+ #
342
+ add_method(
343
+ klass,
344
+ "#{attribute_name}_before_type_cast",
345
+ methods_to_add,
346
+ return_type: "T.untyped",
347
+ )
348
+ add_method(
349
+ klass,
350
+ "#{attribute_name}_came_from_user?",
351
+ methods_to_add,
352
+ return_type: "T::Boolean",
353
+ )
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -1,18 +1,43 @@
1
- # frozen_string_literal: true
2
1
  # typed: strict
2
+ # frozen_string_literal: true
3
3
 
4
- return unless defined?(::MoneyRails)
4
+ return unless defined?(MoneyRails)
5
5
 
6
- require 'tapioca/helpers/rbi_helper'
6
+ require "tapioca/helpers/rbi_helper"
7
7
 
8
8
  module Tapioca
9
9
  module Dsl
10
10
  module Compilers
11
+ # `Tapioca::Dsl::Compilers::MoneyRails` decorates RBI files for classes that use the `monetize` method provided
12
+ # by the `money-rails` gem.
13
+ # https://github.com/RubyMoney/money-rails
14
+ #
15
+ # For example, with the following ActiveRecord model:
16
+ # ~~~rb
17
+ # class Product < ActiveRecord::Base
18
+ # monetize :price_cents
19
+ # end
20
+ # ~~~
21
+ #
22
+ # This compiler will generate the following RBI:
23
+ # ~~~rbi
24
+ # class Product
25
+ # include MoneyRailsGeneratedMethods
26
+ #
27
+ # module MoneyRailsGeneratedMethods
28
+ # sig { returns(::Money) }
29
+ # def price; end
30
+ #
31
+ # sig { params(value: ::Money).returns(::Money) }
32
+ # def price=(value); end
33
+ # end
34
+ # end
35
+ # ~~~
11
36
  class MoneyRails < Tapioca::Dsl::Compiler
12
37
  extend T::Sig
13
38
  include RBIHelper
14
39
 
15
- ConstantType = type_member {{ fixed: T.class_of(::MoneyRails::ActiveRecord::Monetizable) }}
40
+ ConstantType = type_member { { fixed: T.class_of(::MoneyRails::ActiveRecord::Monetizable) } }
16
41
 
17
42
  class << self
18
43
  extend T::Sig
@@ -28,13 +53,13 @@ module Tapioca
28
53
  return if constant.monetized_attributes.empty?
29
54
 
30
55
  root.create_path(constant) do |klass|
31
- instance_module_name = 'MoneyRailsGeneratedMethods'
56
+ instance_module_name = "MoneyRailsGeneratedMethods"
32
57
  instance_module = RBI::Module.new(instance_module_name)
33
58
 
34
59
  constant.monetized_attributes.each do |attribute_name, column_name|
35
60
  column = T.unsafe(constant).columns_hash[column_name]
36
61
 
37
- type_name = '::Money'
62
+ type_name = "::Money"
38
63
  type_name = as_nilable_type(type_name) if column.nil? || !!column.null
39
64
 
40
65
  # Model: monetize :amount_cents
@@ -43,7 +68,7 @@ module Tapioca
43
68
  instance_module.create_method(attribute_name, return_type: type_name)
44
69
  instance_module.create_method(
45
70
  "#{attribute_name}=",
46
- parameters: [create_param('value', type: type_name)],
71
+ parameters: [create_param("value", type: type_name)],
47
72
  return_type: type_name,
48
73
  )
49
74
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Angellist
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-19 00:00:00.000000000 Z
11
+ date: 2024-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sorbet-static-and-runtime
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.16'
33
+ version: 0.16.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.16'
40
+ version: 0.16.2
41
41
  description:
42
42
  email:
43
43
  - alex.stathis@angellist.com
@@ -48,12 +48,17 @@ files:
48
48
  - LICENSE
49
49
  - README.md
50
50
  - lib/boba/version.rb
51
+ - lib/tapioca/dsl/compilers/active_record_associations_persisted.rb
52
+ - lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
51
53
  - lib/tapioca/dsl/compilers/money_rails.rb
52
54
  homepage: https://github.com/angellist/boba
53
55
  licenses:
54
56
  - MIT
55
57
  metadata:
56
- source_code_uri: https://github.com/angellist/boba/tree/v0.0.2
58
+ bug_tracker_uri: https://github.com/angellist/boba/issues
59
+ changelog_uri: https://github.com/angellist/boba/blob/0.0.3/History.md
60
+ homepage_uri: https://github.com/angellist/boba
61
+ source_code_uri: https://github.com/angellist/boba/tree/0.0.3
57
62
  rubygems_mfa_required: 'true'
58
63
  post_install_message:
59
64
  rdoc_options: []