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 +4 -4
- data/README.md +52 -10
- data/lib/boba/relations_railtie.rb +26 -0
- data/lib/boba/version.rb +4 -1
- data/lib/boba.rb +7 -0
- data/lib/tapioca/dsl/compilers/active_record_associations_persisted.rb +225 -0
- data/lib/tapioca/dsl/compilers/active_record_columns_persisted.rb +358 -0
- data/lib/tapioca/dsl/compilers/money_rails.rb +32 -7
- metadata +12 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e6b2eb9c09c7bcd8a5050115892945fc23749ddc3b9f0376387c914c089c390b
|
4
|
+
data.tar.gz: b17a134072db6a900007bc4a74cba405ca69383039e710e86862de09c6daa93f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
9
|
-
|
10
|
-
|
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.
|
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
data/lib/boba.rb
ADDED
@@ -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?(
|
4
|
+
return unless defined?(MoneyRails)
|
5
5
|
|
6
|
-
require
|
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 =
|
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 =
|
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(
|
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.
|
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-
|
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:
|
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:
|
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
|
-
|
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: []
|