boba 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cbef69a3715be625d4af0b5bf408b3822cc09fef287e8bcd1acd1a6c6377280
4
- data.tar.gz: f47e14bbf2f23c02f45190053fdcbbfd29aee7de39e3d698919bbb97db96dd8a
3
+ metadata.gz: cb49413d3cfe01c4ea9d7cf283c50f03183f521e198b0af96e30fd0ec95fb7d4
4
+ data.tar.gz: 7f2dd02645cce05f8204a710b6a787e0aa7271fc902bea0f4c0e0174ffe7cf99
5
5
  SHA512:
6
- metadata.gz: feb7409b2a58e5d48e8e2b5fb6efa47adf622f4a74b5d8a0dd52c976118b03142c983949e0996d1535fe3d0da565f2785978c0de5d5524ded192394be15148aa
7
- data.tar.gz: 1f8edb8084ab4115c01c522b0ed0e82d0dd1f3e6720554995df3abbe0842f22865bd08cec7315850482dda117e382cb6954adc976925ec3a1c83de3d03b3c003
6
+ metadata.gz: 87d9819bf315f321c8aaf3e33f6681606cc980384b2a7f61ceda298097cc955a291e91a1b7ef7e0c1e2e3763570cf79d244e1eebfb07766404bb60283732c8bd
7
+ data.tar.gz: 5cd15d8a5d1a16eba4258240ce240c17063dc5b538dfb9346bf1209e5ab0ae18af667ca710b2e2c0e086261c9b110abc7f6dfac78122fbaacc567b53125afe82
data/README.md CHANGED
@@ -36,7 +36,8 @@ If you'd like to use relation types in your sigs that are less broad than `Activ
36
36
  gem 'boba'
37
37
  ```
38
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:
39
+ The railtie will automatically define the `PrivateRelation`, `PrivateAssociationRelation`, and `PrivateCollectionProxy` constants on each model that inherits from `ActiveRecord::Base`. These are defined as their corresponding private `ActiveRecord` classes, so runtime type checking works as expected. They can then be used in typing, like so:
40
+
40
41
  ```ruby
41
42
  class Post < ::ActiveRecord::Base
42
43
  scope :recent -> { where('created_at > ?', Date.current) }
@@ -49,7 +50,7 @@ sig { params(author: Author).returns(Post::PrivateRelation) }
49
50
  def posts_from_author(author); end
50
51
  ```
51
52
 
52
- and the following should not raise an error:
53
+ and the following should not raise a Sorbet error:
53
54
 
54
55
  ```ruby
55
56
  sig { params(author: Author).returns(Post::PrivateRelation) }
@@ -58,6 +59,15 @@ def recent_posts_from_author(author)
58
59
  end
59
60
  ```
60
61
 
62
+ Boba also defines a type alias `RelationType` on each such class, which is defined as the union of the three relation types. This is useful because the relation types are often used interchangeably and so you may expect to return or pass any of the three classes as an argument. To use this, you will also need to use the `ActiveRecordRelationTypes` compiler to generate the type alias in the signatures as well (or define them manually in shims).
63
+
64
+ ```ruby
65
+ sig { params(author: Author).returns(Post::RelationType) }
66
+ def recent_posts_from_author(author)
67
+ posts_from_author(author).recent
68
+ end
69
+ ```
70
+
61
71
  ## Contributing
62
72
 
63
73
  Bugs and feature requests are welcome and should be [filed as issues on github](https://github.com/angellist/boba/issues).
@@ -71,7 +81,3 @@ Since Boba is intended to be used alongside Tapioca and the compilers provided b
71
81
  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
82
 
73
83
  Compilers for Gems, DSLs, or modules that are not publicly available will not be accepted.
74
-
75
- ## Todo
76
-
77
- 1. Specs & spec harness
@@ -7,15 +7,22 @@ module Boba
7
7
  class << self
8
8
  extend T::Sig
9
9
 
10
- sig { params(constant: T.class_of(::ActiveRecord::Base), attribute: String).returns(T::Boolean) }
11
- def has_unconditional_presence_validator?(constant, attribute)
12
- return false unless constant.respond_to?(:validators_on)
10
+ sig do
11
+ params(
12
+ constant: T.class_of(::ActiveRecord::Base),
13
+ attribute: String,
14
+ column_name: String,
15
+ ).returns(T::Boolean)
16
+ end
17
+ def nilable_attribute?(constant, attribute, column_name: attribute)
18
+ return false if has_non_null_database_constraint?(constant, column_name)
13
19
 
14
- constant.validators_on(attribute).any? do |validator|
15
- next false unless validator.is_a?(::ActiveRecord::Validations::PresenceValidator)
20
+ !has_unconditional_presence_validator?(constant, attribute)
21
+ end
16
22
 
17
- !validator.options.key?(:if) && !validator.options.key?(:unless) && !validator.options.key?(:on)
18
- end
23
+ sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
24
+ def virtual_attribute?(constant, column_name)
25
+ constant.columns_hash[column_name].nil?
19
26
  end
20
27
 
21
28
  sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
@@ -28,9 +35,27 @@ module Boba
28
35
  false
29
36
  end
30
37
 
31
- sig { params(constant: T.class_of(::ActiveRecord::Base), column_name: String).returns(T::Boolean) }
32
- def virtual_attribute?(constant, column_name)
33
- constant.columns_hash[column_name].nil?
38
+ sig { params(constant: T.class_of(::ActiveRecord::Base), attribute: String).returns(T::Boolean) }
39
+ def has_unconditional_presence_validator?(constant, attribute)
40
+ return false unless constant.respond_to?(:validators_on)
41
+
42
+ constant.validators_on(attribute).any? do |validator|
43
+ unconditional_presence_validator?(validator)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ sig { params(validator: ActiveModel::Validator).returns(T::Boolean) }
50
+ def unconditional_presence_validator?(validator)
51
+ return false unless validator.is_a?(::ActiveRecord::Validations::PresenceValidator)
52
+
53
+ unconditional_validator?(validator)
54
+ end
55
+
56
+ sig { params(validator: ActiveModel::Validator).returns(T::Boolean) }
57
+ def unconditional_validator?(validator)
58
+ !validator.options.key?(:if) && !validator.options.key?(:unless) && !validator.options.key?(:on)
34
59
  end
35
60
  end
36
61
  end
@@ -13,13 +13,24 @@ module Boba
13
13
  T.any(::ActiveRecord::Reflection::ThroughReflection, ::ActiveRecord::Reflection::AssociationReflection)
14
14
  end
15
15
 
16
+ sig { params(reflection: ReflectionType).returns(T::Boolean) }
17
+ def required_reflection?(reflection)
18
+ return true if has_one_and_required_reflection?(reflection)
19
+
20
+ belongs_to_and_non_optional_reflection?(reflection)
21
+ end
22
+
23
+ private
24
+
16
25
  sig { params(reflection: ReflectionType).returns(T::Boolean) }
17
26
  def has_one_and_required_reflection?(reflection)
18
27
  return false unless reflection.has_one?
19
28
  return true if !!reflection.options[:required]
20
- return true if reflection_required_by_database_constraint?(reflection)
21
29
 
22
- reflection_required_by_validation?(reflection)
30
+ Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
31
+ reflection.active_record,
32
+ reflection.name.to_s,
33
+ )
23
34
  end
24
35
 
25
36
  sig { params(reflection: ReflectionType).returns(T::Boolean) }
@@ -32,36 +43,21 @@ module Boba
32
43
  reflection.options[:optional]
33
44
  end
34
45
  return !optional unless optional.nil?
35
- return true if reflection_required_by_database_constraint?(reflection)
36
- return true if reflection_required_by_validation?(reflection)
37
-
38
- # nothing defined, so fall back to the default active record config
39
- !!reflection.active_record.belongs_to_required_by_default
40
- end
41
-
42
- private
43
-
44
- # check for non-nullable database constraint on the foreign key
45
- sig { params(reflection: ReflectionType).returns(T::Boolean) }
46
- def reflection_required_by_database_constraint?(reflection)
47
- Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
46
+ return true if Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
48
47
  reflection.active_record,
49
- reflection.foreign_key,
48
+ reflection.foreign_key.to_s,
50
49
  )
51
- end
52
-
53
- # check for presence validator on the foreign key or on the association
54
- sig { params(reflection: ReflectionType).returns(T::Boolean) }
55
- def reflection_required_by_validation?(reflection)
56
50
  return true if Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
57
51
  reflection.active_record,
58
- reflection.foreign_key,
52
+ reflection.foreign_key.to_s,
59
53
  )
60
-
61
- Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
54
+ return true if Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
62
55
  reflection.active_record,
63
- reflection.name,
56
+ reflection.name.to_s,
64
57
  )
58
+
59
+ # nothing defined, so fall back to the default active record config
60
+ !!reflection.active_record.belongs_to_required_by_default
65
61
  end
66
62
  end
67
63
  end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Boba
5
+ module Options
6
+ class AssociationTypeOption < T::Enum
7
+ extend T::Sig
8
+
9
+ enums do
10
+ Nilable = new("nilable")
11
+ Persisted = new("persisted")
12
+ end
13
+
14
+ class << self
15
+ extend T::Sig
16
+
17
+ sig do
18
+ params(
19
+ options: T::Hash[String, T.untyped],
20
+ block: T.proc.params(value: String, default_association_type_option: AssociationTypeOption).void,
21
+ ).returns(AssociationTypeOption)
22
+ end
23
+ def from_options(options, &block)
24
+ association_type_option = Nilable
25
+ value = options["ActiveRecordAssociationTypes"]
26
+
27
+ if value
28
+ if has_serialized?(value)
29
+ association_type_option = from_serialized(value)
30
+ else
31
+ block.call(value, association_type_option)
32
+ end
33
+ end
34
+
35
+ association_type_option
36
+ end
37
+ end
38
+
39
+ sig { returns(T::Boolean) }
40
+ def persisted?
41
+ self == AssociationTypeOption::Persisted
42
+ end
43
+
44
+ sig { returns(T::Boolean) }
45
+ def nilable?
46
+ self == AssociationTypeOption::Nilable
47
+ end
48
+ end
49
+ end
50
+ end
@@ -6,15 +6,30 @@ require "rails/railtie"
6
6
  class Boba::RelationsRailtie < Rails::Railtie
7
7
  railtie_name(:boba)
8
8
 
9
- initializer("boba.add_private_relation_constant") do
9
+ initializer("boba.add_private_relation_constants") do
10
10
  ActiveSupport.on_load(:active_record) do
11
11
  module AciveRecordInheritDefineRelationTypes
12
12
  def inherited(child)
13
13
  super(child)
14
14
 
15
- child.const_set("PrivateRelation", Object)
16
- child.const_set("PrivateAssociationRelation", Object)
17
- child.const_set("PrivateCollectionProxy", Object)
15
+ # Tapioca defines these three classes for each active record model as proxies for the actual AR internal
16
+ # classes. In order to be able to use these as types in signatures, we need to expose them as actual constants
17
+ # at runtime. Tapioca intentionally obfuscates these classes because they're private, so exposing them is
18
+ # _slightly_ dangerous in that someone could do something naughty. But we're not super worried about it.
19
+ child.const_set("PrivateRelation", child.const_get(:ActiveRecord_Relation))
20
+ child.const_set("PrivateAssociationRelation", child.const_get(:ActiveRecord_AssociationRelation))
21
+ child.const_set("PrivateCollectionProxy", child.const_get(:ActiveRecord_Associations_CollectionProxy))
22
+
23
+ # Expose a common type so that signatures can be typed to the broader `RelationType` since the three are often
24
+ # used interchangeably.
25
+ relation_type = T.type_alias do
26
+ T.any(
27
+ child.const_get(:PrivateRelation),
28
+ child.const_get(:PrivateAssociationRelation),
29
+ child.const_get(:PrivateCollectionProxy),
30
+ )
31
+ end
32
+ child.const_set(:RelationType, relation_type)
18
33
  end
19
34
  end
20
35
 
data/lib/boba/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Boba
5
- VERSION = "0.0.12"
5
+ VERSION = "0.0.14"
6
6
  end
@@ -6,6 +6,7 @@ require "tapioca/dsl/compilers/active_record_associations"
6
6
  return unless defined?(Tapioca::Dsl::Compilers::ActiveRecordAssociations)
7
7
 
8
8
  require "boba/active_record/reflection_service"
9
+ require "boba/options/association_type_option"
9
10
 
10
11
  module Tapioca
11
12
  module Dsl
@@ -53,62 +54,18 @@ module Tapioca
53
54
  class ActiveRecordAssociationsPersisted < ::Tapioca::Dsl::Compilers::ActiveRecordAssociations
54
55
  extend T::Sig
55
56
 
56
- class AssociationTypeOption < T::Enum
57
- extend T::Sig
58
-
59
- enums do
60
- Nilable = new("nilable")
61
- Persisted = new("persisted")
62
- end
63
-
64
- class << self
65
- extend T::Sig
66
-
67
- sig do
68
- params(
69
- options: T::Hash[String, T.untyped],
70
- block: T.proc.params(value: String, default_association_type_option: AssociationTypeOption).void,
71
- ).returns(AssociationTypeOption)
72
- end
73
- def from_options(options, &block)
74
- association_type_option = Nilable
75
- value = options["ActiveRecordAssociationTypes"]
76
-
77
- if value
78
- if has_serialized?(value)
79
- association_type_option = from_serialized(value)
80
- else
81
- block.call(value, association_type_option)
82
- end
83
- end
84
-
85
- association_type_option
86
- end
87
- end
88
-
89
- sig { returns(T::Boolean) }
90
- def persisted?
91
- self == AssociationTypeOption::Persisted
92
- end
93
-
94
- sig { returns(T::Boolean) }
95
- def nilable?
96
- self == AssociationTypeOption::Nilable
97
- end
98
- end
99
-
100
57
  private
101
58
 
102
- sig { returns(AssociationTypeOption) }
59
+ sig { returns(Boba::Options::AssociationTypeOption) }
103
60
  def association_type_option
104
61
  @association_type_option ||= T.let(
105
- AssociationTypeOption.from_options(options) do |value, default_association_type_option|
62
+ Boba::Options::AssociationTypeOption.from_options(options) do |value, default_association_type_option|
106
63
  add_error(<<~MSG.strip)
107
64
  Unknown value for compiler option `ActiveRecordAssociationTypes` given: `#{value}`.
108
65
  Proceeding with the default value: `#{default_association_type_option.serialize}`.
109
66
  MSG
110
67
  end,
111
- T.nilable(AssociationTypeOption),
68
+ T.nilable(Boba::Options::AssociationTypeOption),
112
69
  )
113
70
  end
114
71
 
@@ -190,9 +147,7 @@ module Tapioca
190
147
  association_class = type_for(reflection)
191
148
  return as_nilable_type(association_class) unless association_type_option.persisted?
192
149
 
193
- if Boba::ActiveRecord::ReflectionService.has_one_and_required_reflection?(reflection)
194
- association_class
195
- elsif Boba::ActiveRecord::ReflectionService.belongs_to_and_non_optional_reflection?(reflection)
150
+ if Boba::ActiveRecord::ReflectionService.required_reflection?(reflection)
196
151
  association_class
197
152
  else
198
153
  as_nilable_type(association_class)
@@ -153,14 +153,7 @@ module Tapioca
153
153
  def column_type_for(column_name)
154
154
  return ["T.untyped", "T.untyped"] if column_type_option.untyped?
155
155
 
156
- nilable_column = !Boba::ActiveRecord::AttributeService.has_non_null_database_constraint?(
157
- @constant,
158
- column_name,
159
- )
160
- nilable_column &&= !Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
161
- @constant,
162
- column_name,
163
- )
156
+ nilable_column = Boba::ActiveRecord::AttributeService.nilable_attribute?(@constant, column_name)
164
157
 
165
158
  column_type = @constant.attribute_types[column_name]
166
159
  getter_type = column_type_helper.send(
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(ActiveRecord::Base)
5
+
6
+ require "tapioca/dsl/helpers/active_record_constants_helper"
7
+ require "tapioca/dsl/compilers/active_record_relations"
8
+
9
+ return unless defined?(Tapioca::Dsl::Compilers::ActiveRecordRelations)
10
+
11
+ module Tapioca
12
+ module Dsl
13
+ module Compilers
14
+ # `Tapioca::Dsl::Compilers::ActiveRecordRelationTypes` extends
15
+ # `Tapioca::Dsl::Compilers::ActiveRecordRelationTypes` to generate a `RelationType` type alias for each class.
16
+ # This type alias is defined a runtime through the Boba railtie, and is useful for typing signatures to accept or
17
+ # return relations. For instance, with the following `ActiveRecord::Base` subclass:
18
+ # ~~~rb
19
+ # class Post < ApplicationRecord
20
+ # end
21
+ # ~~~
22
+ #
23
+ # This compiler will produce the RBI file `post.rbi` with the following content:
24
+ # ~~~rbi
25
+ # # post.rbi
26
+ # # typed: true
27
+ #
28
+ # class Post
29
+ # RelationType = T.any(PrivateRelation, PrivateAssociationRelation, PrivateCollectionProxy)
30
+ # end
31
+ # ~~~
32
+ # So that the following method will accept any of the private relation types as an argument:
33
+ # ~~~rb
34
+ # sig { params(posts: Post::RelationType).void }
35
+ # def process_posts(posts)
36
+ # # ...
37
+ # end
38
+ # ~~~
39
+ class ActiveRecordRelationTypes < Compiler
40
+ extend T::Sig
41
+
42
+ ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
43
+
44
+ sig { override.void }
45
+ def decorate
46
+ root.create_path(constant) do |rbi_class|
47
+ relation_type_alias = "T.any(" \
48
+ "#{Tapioca::Dsl::Helpers::ActiveRecordConstantsHelper::RelationClassName}, " \
49
+ "#{Tapioca::Dsl::Helpers::ActiveRecordConstantsHelper::AssociationRelationClassName}, " \
50
+ "#{Tapioca::Dsl::Helpers::ActiveRecordConstantsHelper::AssociationsCollectionProxyClassName}" \
51
+ ")"
52
+ rbi_class.create_type_variable("RelationType", type: "T.type_alias { #{relation_type_alias} }")
53
+ end
54
+ end
55
+
56
+ class << self
57
+ extend T::Sig
58
+
59
+ sig { override.returns(T::Enumerable[Module]) }
60
+ def gather_constants
61
+ Tapioca::Dsl::Compilers::ActiveRecordRelations.gather_constants
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -1,7 +1,7 @@
1
- # frozen_string_literal: true
2
1
  # typed: true
2
+ # frozen_string_literal: true
3
3
 
4
- return if !defined?(AttrJson::Record)
4
+ return unless defined?(AttrJson::Record)
5
5
 
6
6
  module Tapioca
7
7
  module Dsl
@@ -39,7 +39,7 @@ module Tapioca
39
39
  # Class methods module is already defined in the gem rbi, so just reference it here.
40
40
  ClassMethodsModuleName = "AttrJson::Record::ClassMethods"
41
41
  InstanceMethodModuleName = "AttrJsonGeneratedMethods"
42
- ConstantType = type_member {{ fixed: T.any(T.class_of(::AttrJson::Record), T.class_of(::AttrJson::Model)) }}
42
+ ConstantType = type_member { { fixed: T.any(T.class_of(::AttrJson::Record), T.class_of(::AttrJson::Model)) } }
43
43
 
44
44
  class << self
45
45
  extend T::Sig
@@ -65,7 +65,7 @@ module Tapioca
65
65
  private
66
66
 
67
67
  def decorate_attributes(rbi_scope)
68
- T.unsafe(constant).attr_json_registry
68
+ constant.attr_json_registry
69
69
  .definitions
70
70
  .sort_by(&:name) # this is annoying, but we need to sort to force consistent ordering or the rbi checks fail
71
71
  .each do |definition|
@@ -122,7 +122,7 @@ module Tapioca
122
122
 
123
123
  sorbet_type = "::#{sorbet_type}"
124
124
  sorbet_type = "T::Array[#{sorbet_type}]" if array
125
- sorbet_type = "T.nilable(#{sorbet_type})" if nilable # todo: improve this
125
+ sorbet_type = "T.nilable(#{sorbet_type})" if nilable # TODO: improve this
126
126
 
127
127
  sorbet_type
128
128
  end
@@ -0,0 +1,85 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Kaminari)
5
+
6
+ require "tapioca/dsl/helpers/active_record_constants_helper"
7
+
8
+ module Tapioca
9
+ module Dsl
10
+ module Compilers
11
+ # `Tapioca::Dsl::Compilers::Kaminari` decorates RBI files for models
12
+ # using Kaminari.
13
+ #
14
+ # For example, with Kaminari installed and the following `ActiveRecord::Base` subclass:
15
+ #
16
+ # ~~~rb
17
+ # class Post < ApplicationRecord
18
+ # end
19
+ # ~~~
20
+ #
21
+ # This compiler will produce the RBI file `post.rbi` with the following content:
22
+ #
23
+ # ~~~rbi
24
+ # # post.rbi
25
+ # # typed: true
26
+ # class Post
27
+ # extend GeneratedRelationMethods
28
+ #
29
+ # module GeneratedRelationMethods
30
+ # sig do
31
+ # params(
32
+ # num: T.any(Integer, String)
33
+ # ).returns(T.all(PrivateRelation, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods))
34
+ # end
35
+ # def page(num = nil); end
36
+ # end
37
+ # end
38
+ # ~~~
39
+ class Kaminari < Tapioca::Dsl::Compiler
40
+ extend T::Sig
41
+ include Helpers::ActiveRecordConstantsHelper
42
+
43
+ ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
44
+
45
+ sig { override.void }
46
+ def decorate
47
+ root.create_path(constant) do |model|
48
+ target_modules.each do |module_name, return_type|
49
+ model.create_module(module_name).create_method(
50
+ ::Kaminari.config.page_method_name.to_s,
51
+ parameters: [create_opt_param("num", type: "T.any(Integer, String)", default: "nil")],
52
+ return_type: "T.all(#{return_type}, Kaminari::PageScopeMethods, Kaminari::ActiveRecordRelationMethods)",
53
+ )
54
+ end
55
+
56
+ model.create_extend(RelationMethodsModuleName)
57
+ end
58
+ end
59
+
60
+ class << self
61
+ extend T::Sig
62
+
63
+ sig { override.returns(T::Enumerable[Module]) }
64
+ def gather_constants
65
+ descendants_of(::ActiveRecord::Base).reject(&:abstract_class?)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ sig { returns(T::Array[[String, String]]) }
72
+ def target_modules
73
+ if compiler_enabled?("ActiveRecordRelations")
74
+ [
75
+ [RelationMethodsModuleName, RelationClassName],
76
+ [AssociationRelationMethodsModuleName, AssociationRelationClassName],
77
+ ]
78
+ else
79
+ [[RelationMethodsModuleName, "T.untyped"]]
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -4,6 +4,8 @@
4
4
  return unless defined?(MoneyRails)
5
5
 
6
6
  require "tapioca/helpers/rbi_helper"
7
+ require "tapioca/dsl/helpers/active_record_column_type_helper"
8
+ require "boba/active_record/attribute_service"
7
9
 
8
10
  module Tapioca
9
11
  module Dsl
@@ -37,7 +39,14 @@ module Tapioca
37
39
  extend T::Sig
38
40
  include RBIHelper
39
41
 
40
- ConstantType = type_member { { fixed: T.class_of(::MoneyRails::ActiveRecord::Monetizable) } }
42
+ ConstantType = type_member do
43
+ {
44
+ fixed: T.all(
45
+ T.class_of(::MoneyRails::ActiveRecord::Monetizable),
46
+ ::MoneyRails::ActiveRecord::Monetizable::ClassMethods,
47
+ ),
48
+ }
49
+ end
41
50
 
42
51
  class << self
43
52
  extend T::Sig
@@ -48,6 +57,21 @@ module Tapioca
48
57
  end
49
58
  end
50
59
 
60
+ ColumnTypeOption = Tapioca::Dsl::Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption
61
+
62
+ sig { returns(ColumnTypeOption) }
63
+ def column_type_option
64
+ @column_type_option ||= T.let(
65
+ ColumnTypeOption.from_options(options) do |value, default_column_type_option|
66
+ add_error(<<~MSG.strip)
67
+ Unknown value for compiler option `ActiveRecordColumnTypes` given: `#{value}`.
68
+ Proceeding with the default value: `#{default_column_type_option.serialize}`.
69
+ MSG
70
+ end,
71
+ T.nilable(ColumnTypeOption),
72
+ )
73
+ end
74
+
51
75
  sig { override.void }
52
76
  def decorate
53
77
  return if constant.monetized_attributes.empty?
@@ -57,10 +81,23 @@ module Tapioca
57
81
  instance_module = RBI::Module.new(instance_module_name)
58
82
 
59
83
  constant.monetized_attributes.each do |attribute_name, column_name|
60
- column = T.unsafe(constant).columns_hash[column_name]
84
+ if column_type_option.untyped?
85
+ type_name = "T.untyped"
86
+ else
87
+ type_name = "::Money"
88
+
89
+ nilable_attribute = if constant < ::ActiveRecord::Base && column_type_option.persisted?
90
+ Boba::ActiveRecord::AttributeService.nilable_attribute?(
91
+ T.cast(constant, T.class_of(::ActiveRecord::Base)),
92
+ attribute_name,
93
+ column_name: column_name,
94
+ )
95
+ else
96
+ true
97
+ end
61
98
 
62
- type_name = "::Money"
63
- type_name = as_nilable_type(type_name) if column.nil? || !!column.null
99
+ type_name = as_nilable_type(type_name) if nilable_attribute
100
+ end
64
101
 
65
102
  # Model: monetize :amount_cents
66
103
  # => amount
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.12
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Angellist
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-11-30 00:00:00.000000000 Z
11
+ date: 2024-12-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sorbet-static-and-runtime
@@ -50,11 +50,14 @@ files:
50
50
  - lib/boba.rb
51
51
  - lib/boba/active_record/attribute_service.rb
52
52
  - lib/boba/active_record/reflection_service.rb
53
+ - lib/boba/options/association_type_option.rb
53
54
  - lib/boba/relations_railtie.rb
54
55
  - lib/boba/version.rb
55
56
  - lib/tapioca/dsl/compilers/active_record_associations_persisted.rb
56
57
  - lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
58
+ - lib/tapioca/dsl/compilers/active_record_relation_types.rb
57
59
  - lib/tapioca/dsl/compilers/attr_json.rb
60
+ - lib/tapioca/dsl/compilers/kaminari.rb
58
61
  - lib/tapioca/dsl/compilers/money_rails.rb
59
62
  - lib/tapioca/dsl/compilers/paperclip.rb
60
63
  - lib/tapioca/dsl/compilers/state_machines_extended.rb
@@ -63,9 +66,9 @@ licenses:
63
66
  - MIT
64
67
  metadata:
65
68
  bug_tracker_uri: https://github.com/angellist/boba/issues
66
- changelog_uri: https://github.com/angellist/boba/blob/0.0.12/History.md
69
+ changelog_uri: https://github.com/angellist/boba/blob/0.0.14/History.md
67
70
  homepage_uri: https://github.com/angellist/boba
68
- source_code_uri: https://github.com/angellist/boba/tree/0.0.12
71
+ source_code_uri: https://github.com/angellist/boba/tree/0.0.14
69
72
  rubygems_mfa_required: 'true'
70
73
  post_install_message:
71
74
  rdoc_options: []