boba 0.0.2 → 0.0.4

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: e6b2eb9c09c7bcd8a5050115892945fc23749ddc3b9f0376387c914c089c390b
4
+ data.tar.gz: b17a134072db6a900007bc4a74cba405ca69383039e710e86862de09c6daa93f
5
5
  SHA512:
6
- metadata.gz: c8bbdbedbc56f12ac239dd8ad75c04bc6782d2b8448dc7f289c430d9ea592458f44b3461c304daaff218689bad83c4779a701d449d14f2d6babf7c2bb06c367a
7
- data.tar.gz: '068e9cf37630c8fd608a04280b35a46f769a432ecb6605cedbc7c03991d1cec5d6f67306a38803069e0eaa8be5f27889c362b5fb9aa622714653775540e7c88d'
6
+ metadata.gz: 6d1bd6b43cc6ad2104ad6bcb96f78a53eb7c70068ef948ba0ab172fb8821537963e3a4bf5fe2b810613f9a2e4e9c0a30364bca266e243af1b8cf2800f0d8772a
7
+ data.tar.gz: 38e5e55631671d0bf64c7c8f64b3d21d8d7c8dbd393b4ffc960f2e958766302bb67dea0c8aeef65ec606f2f395d1194ac1367551a795ae66c7f455ce412f287e
data/README.md CHANGED
@@ -4,10 +4,11 @@
4
4
 
5
5
  Boba is a collection of compilers for Sorbet & Tapioca.
6
6
 
7
- Tapioca is very opinionated about what types of compilers or changes are accepted into the repository. See
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.
7
+ Tapioca is very opinionated about what types of compilers or changes are accepted into the repository. See [here](https://github.com/Shopify/tapioca?tab=readme-ov-file#dsl-compilers). Boba come in all 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.
8
+
9
+ ### Available Compilers
10
+
11
+ See [the compilers manual](https://github.com/angellist/boba/blob/main/manual/compilers.md) for a list of available compilers.
11
12
 
12
13
  ## Usage
13
14
 
@@ -18,18 +19,59 @@ group :development do
18
19
  end
19
20
  ```
20
21
 
21
- We recommend you also use the `only` configuration option in your Tapioca config (typically `sorbet/tapioca/config.yml`)
22
- to specify only the Tapioca compilers you wish to use.
22
+ We recommend you also use the `only` configuration option in your Tapioca config (typically `sorbet/tapioca/config.yml`) to specify only the Tapioca compilers you wish to use.
23
23
  ```yml
24
24
  dsl:
25
25
  only:
26
26
  Compiler1
27
27
  Compiler2
28
28
  ```
29
+ This makes it easy to selectively enable only the compilers you want to use in your project.
30
+
31
+ ### Typing Relations
32
+
33
+ If you'd like to use relation types in your sigs that are less broad than `ActiveRecord::Relation`, such as those specific to a model, Boba provides a railtie to initialize these constants for each class. Move Boba in your gemfile out of the development group:
34
+
35
+ ```ruby
36
+ gem 'boba'
37
+ ```
38
+
39
+ The railtie will automatically define the `PrivateRelation` constant on each model that inherits from `ActiveRecord::Base`. It can then be used in typing, like thus:
40
+ ```ruby
41
+ class Post < ::ActiveRecord::Base
42
+ scope :recent -> { where('created_at > ?', Date.current) }
43
+
44
+ belongs_to :author
45
+ has_many :comments
46
+ end
47
+
48
+ sig { params(author: Author).returns(Post::PrivateRelation) }
49
+ def posts_from_author(author); end
50
+ ```
51
+
52
+ and the following should not raise an error:
53
+
54
+ ```ruby
55
+ sig { params(author: Author).returns(Post::PrivateRelation) }
56
+ def recent_posts_from_author(author)
57
+ posts_from_author(author).recent
58
+ end
59
+ ```
60
+
61
+ ## Contributing
62
+
63
+ Bugs and feature requests are welcome and should be [filed as issues on github](https://github.com/angellist/boba/issues).
64
+
65
+ ### New Compilers
66
+
67
+ 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.
68
+
69
+ Since Boba is intended to be used alongside Tapioca and the compilers provided by Boba are intended to be fully optional, 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 compilers. Instead, compilers which extend or overwrite the default Tapioca compilers should be given unique names.
70
+
71
+ 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.
72
+
73
+ Compilers for Gems, DSLs, or modules that are not publicly available will not be accepted.
29
74
 
30
75
  ## Todo
31
76
 
32
- 1. Contributing Section
33
- 2. Specs & spec harness
34
- 3. Docs & doc harness
35
- 4. Rubocop
77
+ 1. Specs & spec harness
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "rails/railtie"
5
+
6
+ class Boba::RelationsRailtie < Rails::Railtie
7
+ railtie_name(:boba)
8
+
9
+ initializer("boba.add_private_relation_constant") do
10
+ ActiveSupport.on_load(:active_record) do
11
+ module AciveRecordInheritDefineRelationTypes
12
+ def inherited(child)
13
+ super(child)
14
+
15
+ child.const_set("PrivateRelation", Object)
16
+ end
17
+ end
18
+
19
+ class ::ActiveRecord::Base
20
+ class << self
21
+ prepend AciveRecordInheritDefineRelationTypes
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
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.4"
3
6
  end
data/lib/boba.rb ADDED
@@ -0,0 +1,7 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Boba
5
+ require "boba/version"
6
+ require "boba/relations_railtie" if defined?(Rails)
7
+ 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.4
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-22 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
@@ -47,13 +47,20 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - LICENSE
49
49
  - README.md
50
+ - lib/boba.rb
51
+ - lib/boba/relations_railtie.rb
50
52
  - lib/boba/version.rb
53
+ - lib/tapioca/dsl/compilers/active_record_associations_persisted.rb
54
+ - lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
51
55
  - lib/tapioca/dsl/compilers/money_rails.rb
52
56
  homepage: https://github.com/angellist/boba
53
57
  licenses:
54
58
  - MIT
55
59
  metadata:
56
- source_code_uri: https://github.com/angellist/boba/tree/v0.0.2
60
+ bug_tracker_uri: https://github.com/angellist/boba/issues
61
+ changelog_uri: https://github.com/angellist/boba/blob/0.0.4/History.md
62
+ homepage_uri: https://github.com/angellist/boba
63
+ source_code_uri: https://github.com/angellist/boba/tree/0.0.4
57
64
  rubygems_mfa_required: 'true'
58
65
  post_install_message:
59
66
  rdoc_options: []