boba 0.0.1 → 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76f99ba4fafb5f1a55c1abb60b0c099569f3f4153c4f0f0b43ced158e02fb59b
|
4
|
+
data.tar.gz: 1de40e562d0dbeb3c0b2a987cd4e2898321a62550a239941cb129f5690da042d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fbc7d9e5ee8248a1175e499f891716d0397136cc20e1e6db65c22c6d61d777ea756c2c992e48694375358a748986bca2bd2c70393601c8185faf49e1ea7f9e39
|
7
|
+
data.tar.gz: 23d4d817f6b1a14345599d027c5fbed2203a32709d1cd955568ee69b7475b4551e31308936c02d4e2c26c403af292070f301c9258eed40fdba0b17414ad346f4
|
data/README.md
CHANGED
@@ -6,15 +6,18 @@ 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.
|
10
|
-
|
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
|
|
14
17
|
Add Boba to your development dependencies in your gemfile:
|
15
18
|
```ruby
|
16
19
|
group :development do
|
17
|
-
gem 'boba'
|
20
|
+
gem 'boba', require: false
|
18
21
|
end
|
19
22
|
```
|
20
23
|
|
@@ -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.
|
33
|
-
2. Specs & spec harness
|
34
|
-
3. Docs & doc harness
|
35
|
-
4. Rubocop
|
52
|
+
1. Specs & spec harness
|
data/lib/boba/version.rb
CHANGED
@@ -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.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-
|
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:
|
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
|
@@ -48,11 +48,18 @@ 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
|
-
metadata:
|
57
|
+
metadata:
|
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
|
62
|
+
rubygems_mfa_required: 'true'
|
56
63
|
post_install_message:
|
57
64
|
rdoc_options: []
|
58
65
|
require_paths:
|