boba 0.2.0 → 0.3.0

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: fc8f00fb09f7dc829a0f4c174ef8a3916068328de2868496e5430c83475c9e02
4
- data.tar.gz: fa0393e8df72dfa4fe170dfe2c6ede7d07f308e257b3a5a347f875a6eba9915a
3
+ metadata.gz: 0a005078d3a776680cbc55c160a934a0460ba4cb7b5dd51de9c446eded2c9cdd
4
+ data.tar.gz: fbed64ed0a040d65e84ffa0836ab74500a3f421d0e20e13b4e00f091037cd917
5
5
  SHA512:
6
- metadata.gz: 948d54418c7e911be046e53504ebf66ebb2c1190d8633643b01aadc5dbd12d137a9b7fc3d1da33ddab2298cdad40d9edcfe03ce4d9b0fe04dc59d83b17d67618
7
- data.tar.gz: 83921a7f8c4285fb0fe36d65dcb565ca3cc01c421df4e123cec5abb97afa17f3393a08dcf10977f4653190511669c1f5211bbda4a324f030804d3ff19d684953
6
+ metadata.gz: 58cbdd3b0b5933e5052dc888dd4c596e3ee854890e6bf4c6dca431b16e71dd098828663c266d0239768c5c2493ac0430fde363cf1020e2f17ea902657d61b2b2
7
+ data.tar.gz: 9b8e2cc9d3c511bbc420152b2f53b73c16c3fbe0653e4adc4e1ca9f67664f90066957e7cf8cf8317b4bbb4553548171e6ebe264bc3e4910e7a9031e27c8ec6c8
@@ -27,10 +27,8 @@ module Boba
27
27
  false
28
28
  end
29
29
 
30
- #: (singleton(::ActiveRecord::Base) constant, String attribute) -> bool
30
+ #: (::ActiveModel::Validations::ClassMethods constant, String attribute) -> bool
31
31
  def has_unconditional_presence_validator?(constant, attribute)
32
- return false unless constant.respond_to?(:validators_on)
33
-
34
32
  constant.validators_on(attribute).any? do |validator|
35
33
  unconditional_presence_validator?(validator)
36
34
  end
@@ -40,7 +38,7 @@ module Boba
40
38
 
41
39
  #: (ActiveModel::Validator validator) -> bool
42
40
  def unconditional_presence_validator?(validator)
43
- return false unless validator.is_a?(::ActiveRecord::Validations::PresenceValidator)
41
+ return false unless validator.is_a?(::ActiveModel::Validations::PresenceValidator)
44
42
 
45
43
  unconditional_validator?(validator)
46
44
  end
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.2.0"
5
+ VERSION = "0.3.0"
6
6
  end
@@ -3,6 +3,8 @@
3
3
 
4
4
  return unless defined?(ActsAsTaggableOn)
5
5
 
6
+ require "tapioca/dsl/helpers/active_record_constants_helper"
7
+
6
8
  module Tapioca
7
9
  module Dsl
8
10
  module Compilers
@@ -68,13 +70,37 @@ module Tapioca
68
70
  # sig { params(limit: Integer).returns(T.untyped) }
69
71
  # def top_tags(limit = 10); end
70
72
  # end
73
+ #
74
+ # module GeneratedAssociationRelationMethods
75
+ # sig { params(context: T.untyped, options: T.untyped).returns(::ActsAsTaggableOn::Tag::PrivateRelation) }
76
+ # def tag_counts_on(context, options = {}); end
77
+ #
78
+ # sig { params(tags: T.untyped, options: T.untyped).returns(PrivateAssociationRelation) }
79
+ # def tagged_with(tags, options = {}); end
80
+ # end
81
+ #
82
+ # module GeneratedRelationMethods
83
+ # sig { params(context: T.untyped, options: T.untyped).returns(::ActsAsTaggableOn::Tag::PrivateRelation) }
84
+ # def tag_counts_on(context, options = {}); end
85
+ #
86
+ # sig { params(tags: T.untyped, options: T.untyped).returns(PrivateRelation) }
87
+ # def tagged_with(tags, options = {}); end
88
+ # end
71
89
  # end
72
90
  # ~~~
73
91
  #
74
92
  # The mixins are declared rather than re-implemented, so `tagged_with`, `tag_list_on` and the rest keep
75
93
  # the signatures they have in the gem RBI. Only the per-context methods, which exist for the contexts of
76
94
  # this model alone, are generated.
95
+ #
96
+ # The relation side is the exception. `ActiveRecord::Relation` reaches the class methods by delegating to
97
+ # the model class, which Sorbet cannot follow, so `Post.published.tagged_with(...)` does not type-check at
98
+ # all. The two finders that answer a relation are re-stated on the relation modules: `tagged_with`
99
+ # narrows the receiver, and `tag_counts_on` answers a relation of `ActsAsTaggableOn::Tag` rather than
100
+ # of the model. Without the relations compiler both stay untyped.
77
101
  class ActsAsTaggableOn < Tapioca::Dsl::Compiler
102
+ include Helpers::ActiveRecordConstantsHelper
103
+
78
104
  ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
79
105
 
80
106
  # Every `acts_as_taggable_on` call installs this set; the names are fixed, so there is nothing to
@@ -96,6 +122,8 @@ module Tapioca
96
122
  tag_types.each do |tag_type|
97
123
  create_context_methods(model, tag_type)
98
124
  end
125
+
126
+ create_relation_methods(model)
99
127
  end
100
128
  end
101
129
 
@@ -111,6 +139,36 @@ module Tapioca
111
139
 
112
140
  private
113
141
 
142
+ #: (RBI::Scope model) -> void
143
+ def create_relation_methods(model)
144
+ relations_typed = compiler_enabled?("ActiveRecordRelations")
145
+ tag_relation = relations_typed ? "::ActsAsTaggableOn::Tag::PrivateRelation" : "T.untyped"
146
+
147
+ [
148
+ [RelationMethodsModuleName, RelationClassName],
149
+ [AssociationRelationMethodsModuleName, AssociationRelationClassName],
150
+ ].each do |module_name, relation_class|
151
+ relation_methods_module = model.create_module(module_name)
152
+
153
+ relation_methods_module.create_method(
154
+ "tagged_with",
155
+ parameters: [
156
+ create_param("tags", type: "T.untyped"),
157
+ create_opt_param("options", type: "T.untyped", default: "{}"),
158
+ ],
159
+ return_type: relations_typed ? relation_class : "T.untyped",
160
+ )
161
+ relation_methods_module.create_method(
162
+ "tag_counts_on",
163
+ parameters: [
164
+ create_param("context", type: "T.untyped"),
165
+ create_opt_param("options", type: "T.untyped", default: "{}"),
166
+ ],
167
+ return_type: tag_relation,
168
+ )
169
+ end
170
+ end
171
+
114
172
  #: (RBI::Scope model, String tag_type) -> void
115
173
  def create_context_methods(model, tag_type)
116
174
  singular = tag_type.singularize
@@ -3,6 +3,8 @@
3
3
 
4
4
  return unless defined?(AttrJson::Record)
5
5
 
6
+ require "boba/active_record/attribute_service"
7
+
6
8
  module Tapioca
7
9
  module Dsl
8
10
  module Compilers
@@ -25,14 +27,20 @@ module Tapioca
25
27
  # extend AttrJson::Record::ClassMethods
26
28
  #
27
29
  # module AttrJsonGeneratedMethods
28
- # sig { returns(::Integer) }
30
+ # sig { returns(T.nilable(::Integer)) }
29
31
  # def price_cents; end
30
32
  #
31
- # sig { params(value: Integer).returns(::Integer) }
33
+ # sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
32
34
  # def price_cents=(value); end
33
35
  # end
34
36
  # end
35
37
  # ~~~
38
+ #
39
+ # A scalar attribute reads as `nil` until it is set, so it is typed as nilable unless it has a non-nil
40
+ # `default:`. As with columns in `ActiveRecordColumnsPersisted`, an unconditional presence validator
41
+ # (no `if:`, `unless:` or `on:`) types it as it is on a valid record instead:
42
+ # `validates :price_cents, presence: true` turns the methods above into `returns(::Integer)` and
43
+ # `params(value: ::Integer)`.
36
44
  class AttrJson < Tapioca::Dsl::Compiler
37
45
  # Class methods module is already defined in the gem rbi, so just reference it here.
38
46
  ClassMethodsModuleName = "AttrJson::Record::ClassMethods"
@@ -63,13 +71,27 @@ module Tapioca
63
71
  private
64
72
 
65
73
  def decorate_attributes(rbi_scope)
74
+ # Both AttrJson::Record and AttrJson::Model bring in ActiveModel validations; the check lets Sorbet see it.
75
+ klass = constant
76
+ validated = klass if klass.is_a?(::ActiveModel::Validations::ClassMethods)
77
+
66
78
  constant.attr_json_registry
67
79
  .definitions
68
80
  .sort_by(&:name) # this is annoying, but we need to sort to force consistent ordering or the rbi checks fail
69
81
  .each do |definition|
70
82
  _, type, options = definition.original_args
71
83
  attribute_name = definition.name.to_s
72
- type_name = sorbet_type(type, array: !!options[:array], nilable: !!options[:nil])
84
+ array = !!options[:array]
85
+ # AttrJson has no option that forbids nil: an attribute reads as nil unless it has a default,
86
+ # which AttrJson also fills in when a stored record lacks the key. An array one defaults to []
87
+ # on its own unless the default is overridden. As with columns, an unconditional presence
88
+ # validator types the attribute as it is on a valid record.
89
+ nilable = array ? options.key?(:default) && options[:default].nil? : options[:default].nil?
90
+ nilable &&= !(validated && Boba::ActiveRecord::AttributeService.has_unconditional_presence_validator?(
91
+ validated,
92
+ attribute_name,
93
+ ))
94
+ type_name = sorbet_type(type, array: array, nilable: nilable)
73
95
 
74
96
  # Model: attr_json(:other_model_id, :string)
75
97
  # => other_model_id
@@ -39,12 +39,19 @@ module Tapioca
39
39
  # # typed: true
40
40
  # class Chat
41
41
  # include RubyLLM::ActiveRecord::ChatMethods
42
+ #
43
+ # Elem = type_member { { fixed: ::RubyLLM::Message } }
42
44
  # end
43
45
  # ~~~
44
46
  #
45
47
  # The module is declared rather than re-implemented, so `ask`, `with_instructions`,
46
48
  # `with_runtime_instructions` and the rest keep the signatures they have in the gem RBI. Models that
47
49
  # never call an `acts_as_*` method are left alone.
50
+ #
51
+ # Since ruby_llm 2.0 `ChatMethods` includes `Enumerable` and hands `each` over to the underlying
52
+ # `RubyLLM::Chat`, which yields `RubyLLM::Message`s. A class that includes a generic module has to
53
+ # re-declare its type members, so the chat model gets `Elem` fixed to that; on older versions,
54
+ # where the module is not enumerable, nothing is added.
48
55
  class RubyLLM < Tapioca::Dsl::Compiler
49
56
  ConstantType = type_member { { fixed: T.class_of(::ActiveRecord::Base) } }
50
57
 
@@ -66,9 +73,20 @@ module Tapioca
66
73
 
67
74
  root.create_path(constant) do |model|
68
75
  mixins.each { |name| model.create_include(name) }
76
+
77
+ add_message_elem_type_member(model, mixins)
69
78
  end
70
79
  end
71
80
 
81
+ private
82
+
83
+ #: (RBI::Scope model, Array[String] mixins) -> void
84
+ def add_message_elem_type_member(model, mixins)
85
+ return unless constant < ::Enumerable && mixins.include?("RubyLLM::ActiveRecord::ChatMethods")
86
+
87
+ model.create_type_variable("Elem", type: "type_member", fixed: "::RubyLLM::Message")
88
+ end
89
+
72
90
  class << self
73
91
  # @override
74
92
  #: -> Enumerable[Module[top]]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boba
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Stathis
@@ -74,9 +74,9 @@ licenses:
74
74
  - MIT
75
75
  metadata:
76
76
  bug_tracker_uri: https://github.com/stathis-alexander/boba/issues
77
- changelog_uri: https://github.com/stathis-alexander/boba/blob/0.2.0/History.md
77
+ changelog_uri: https://github.com/stathis-alexander/boba/blob/0.3.0/History.md
78
78
  homepage_uri: https://github.com/stathis-alexander/boba
79
- source_code_uri: https://github.com/stathis-alexander/boba/tree/0.2.0
79
+ source_code_uri: https://github.com/stathis-alexander/boba/tree/0.3.0
80
80
  rubygems_mfa_required: 'true'
81
81
  rdoc_options: []
82
82
  require_paths: