boba 0.1.9 → 0.1.10

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: c38ab860f2d0913aef856dc5903505a28fdfe971c30be7d116c3f8b1b6f1b366
4
- data.tar.gz: 16b5fb166214dfd6157efb1c481a4667616ee937c1746d620aef892b457f39be
3
+ metadata.gz: 24108e7a191602ef441626b929c5a846004c36b4af6b6c28c86384577c1b1f4e
4
+ data.tar.gz: 150b997c5bb280a71c228df1a4840d190a29dd8f49d3390f3f1b1999f78488c7
5
5
  SHA512:
6
- metadata.gz: 7a1bed23004b75855101c0c05628d12d3aba8f6f808856402032069310195e3699035659c43337630efbf9d2ac796243ba7c57e83ab633a873e728caab642433
7
- data.tar.gz: 4542bcfde6d1e4f0dd9538dafde159bab762844d18cfa746468ea71831a846953c3f3d6182eed33253736633f8a5f221f1f0d3519f8201159afa2e2830fb368a
6
+ metadata.gz: e89404c6738c73acac5f0c0e0043a883930f6594bc9264d0f48f72ec9ab9bcf554c36163316ebd8ddc335ca3945c9c50bb20f01c2c6fdd490ccca21c072db52e
7
+ data.tar.gz: 42c252740c0258ff9666573bf4f73e67e1de954f86c7a7728cd302490eb34af9eabe1abf0f8956329caaca11574eaef733d127763e00314470ec0df3d6e438b7
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.1.9"
5
+ VERSION = "0.1.10"
6
6
  end
@@ -0,0 +1,188 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ return unless defined?(Draper)
5
+
6
+ module Tapioca
7
+ module Dsl
8
+ module Compilers
9
+ # `Tapioca::Dsl::Compilers::Draper` decorates RBI files for `Draper::Decorator`
10
+ # subclasses and the source classes they decorate, provided by the `draper` gem.
11
+ # https://github.com/drapergem/draper
12
+ #
13
+ # The compiler emits a typed `object` / `model` / underscored-source-name accessor
14
+ # for every decorator, plus a typed `decorate` instance method on the source class.
15
+ #
16
+ # For example, with the following classes:
17
+ # ~~~rb
18
+ # class Post < ActiveRecord::Base
19
+ # end
20
+ #
21
+ # class PostDecorator < Draper::Decorator
22
+ # end
23
+ # ~~~
24
+ #
25
+ # This compiler will generate the following RBI for the decorator:
26
+ # ~~~rbi
27
+ # class PostDecorator
28
+ # include DraperGeneratedInstanceMethods
29
+ #
30
+ # module DraperGeneratedInstanceMethods
31
+ # sig { returns(::Post) }
32
+ # def model; end
33
+ #
34
+ # sig { returns(::Post) }
35
+ # def object; end
36
+ #
37
+ # sig { returns(::Post) }
38
+ # def post; end
39
+ # end
40
+ # end
41
+ # ~~~
42
+ #
43
+ # And the following RBI for the source class:
44
+ # ~~~rbi
45
+ # class Post
46
+ # include DraperGeneratedDecoratableMethods
47
+ #
48
+ # module DraperGeneratedDecoratableMethods
49
+ # sig { params(options: T.untyped).returns(::PostDecorator) }
50
+ # def decorate(options = T.unsafe(nil)); end
51
+ # end
52
+ # end
53
+ # ~~~
54
+ #
55
+ # ## Why `delegate_all` is not supported
56
+ #
57
+ # `delegate_all` forwards every public instance method of the source class via
58
+ # `method_missing`. Reflecting that into RBI requires emitting one explicit method
59
+ # per name — Sorbet ignores `method_missing` for type inference, and there is no
60
+ # other annotation that lets us say "this class has all the methods of that one"
61
+ # without the `is_a?` lie of declaring `class PostDecorator < Post`.
62
+ #
63
+ # Mirroring AR's full instance method set per decorator turned out to be wildly
64
+ # noisy in practice (several thousand lines per decorator on real models, mostly
65
+ # AR-internal methods like `__callbacks` and `_before_commit_callbacks` that no
66
+ # one calls through a decorator). Argument and return types also collapse to
67
+ # `T.untyped`, so the noise doesn't even buy strong typing.
68
+ #
69
+ # The recommended pattern is therefore to access the source through the typed
70
+ # `object` accessor — `decorator.object.title` carries the typing produced by
71
+ # Tapioca's `ActiveRecordColumns` compiler, with no per-decorator bloat.
72
+ #
73
+ # Concretely, with `Post#title` (a string column):
74
+ # ~~~rb
75
+ # post = Post.new(title: "post 1")
76
+ # post.title # ✓ Sorbet sees ::String (from ActiveRecordColumns)
77
+ #
78
+ # decorator = post.decorate
79
+ # decorator.title # ✗ Sorbet errors — even with `delegate_all`, no
80
+ # # `title` is declared on PostDecorator
81
+ # decorator.object.title # ✓ Sorbet sees ::String (via the typed `object`)
82
+ # ~~~
83
+ class Draper < Tapioca::Dsl::Compiler
84
+ InstanceMethodModuleName = "DraperGeneratedInstanceMethods"
85
+ DecoratableMethodModuleName = "DraperGeneratedDecoratableMethods"
86
+
87
+ ConstantType = type_member { { fixed: T.class_of(Object) } }
88
+
89
+ class << self
90
+ # @override
91
+ #: -> Enumerable[Module[top]]
92
+ def gather_constants
93
+ decorators = gather_decorators
94
+ decorators + gather_decoratables(decorators)
95
+ end
96
+
97
+ private
98
+
99
+ # Decorator subclasses with an inferable `object_class` (e.g. `PostDecorator`).
100
+ # Filters out anonymous classes and abstract bases like `ApplicationDecorator`
101
+ # which have no `decorates` call. Uses Draper's own `object_class?` predicate.
102
+ #: -> Array[singleton(::Draper::Decorator)]
103
+ def gather_decorators
104
+ descendants_of(::Draper::Decorator).select do |klass|
105
+ klass.name && klass.object_class?
106
+ end
107
+ end
108
+
109
+ # Source classes (e.g. `Post`) whose Draper-inferred decorator matches one of
110
+ # the gathered decorators. The inference check ensures we only emit
111
+ # `Source#decorate` when this decorator is the one Draper would actually
112
+ # return at runtime, avoiding conflicting return-type RBIs when multiple
113
+ # decorators target the same source.
114
+ #: (Array[singleton(::Draper::Decorator)] decorators) -> Array[Class[top]]
115
+ def gather_decoratables(decorators)
116
+ decorators.filter_map do |decorator|
117
+ source = decorator.object_class
118
+ source if source.is_a?(Class) &&
119
+ source < ::Draper::Decoratable &&
120
+ T.unsafe(source).decorator_class? == decorator
121
+ end
122
+ end
123
+ end
124
+
125
+ # @override
126
+ #: -> void
127
+ def decorate
128
+ if constant.is_a?(Class) && constant < ::Draper::Decorator
129
+ decorate_decorator(T.cast(constant, T.class_of(::Draper::Decorator)))
130
+ else
131
+ decorate_decoratable(T.unsafe(constant))
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ #: (singleton(::Draper::Decorator) decorator) -> void
138
+ def decorate_decorator(decorator)
139
+ object_class = decorator.object_class
140
+ object_class_name = "::#{object_class.name}"
141
+
142
+ # Each accessor is gated on `method_defined?` so the generated RBI never
143
+ # claims a method that Draper hasn't actually installed at runtime. `object`
144
+ # / `model` are stable Draper APIs but the underscore alias is a Draper
145
+ # convention; the runtime check keeps the compiler robust if any of these
146
+ # change in a future Draper release.
147
+ root.create_path(decorator) do |klass|
148
+ instance_module = RBI::Module.new(InstanceMethodModuleName)
149
+
150
+ if decorator.method_defined?(:object)
151
+ instance_module.create_method("object", return_type: object_class_name)
152
+ end
153
+ if decorator.method_defined?(:model)
154
+ instance_module.create_method("model", return_type: object_class_name)
155
+ end
156
+
157
+ underscore_name = object_class.name.to_s.underscore
158
+ if underscore_name != "object" && underscore_name != "model" &&
159
+ decorator.method_defined?(underscore_name.to_sym)
160
+ instance_module.create_method(underscore_name, return_type: object_class_name)
161
+ end
162
+
163
+ klass << instance_module
164
+ klass.create_include(InstanceMethodModuleName)
165
+ end
166
+ end
167
+
168
+ #: (Class[top] source) -> void
169
+ def decorate_decoratable(source)
170
+ decorator = T.unsafe(source).decorator_class
171
+ decorator_class_name = "::#{decorator.name}"
172
+
173
+ root.create_path(source) do |klass|
174
+ decoratable_module = RBI::Module.new(DecoratableMethodModuleName)
175
+ decoratable_module.create_method(
176
+ "decorate",
177
+ parameters: [create_opt_param("options", type: "T.untyped", default: "T.unsafe(nil)")],
178
+ return_type: decorator_class_name,
179
+ )
180
+
181
+ klass << decoratable_module
182
+ klass.create_include(DecoratableMethodModuleName)
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -47,8 +47,6 @@ module Tapioca
47
47
  # end
48
48
  # ~~~
49
49
  class Shrine < Tapioca::Dsl::Compiler
50
- include RBIHelper
51
-
52
50
  InstanceMethodModuleName = "ShrineGeneratedMethods"
53
51
  ClassMethodModuleName = "ShrineGeneratedClassMethods"
54
52
 
@@ -149,11 +147,16 @@ module Tapioca
149
147
  end
150
148
  end
151
149
 
152
- #: (UnboundMethod | Method method_obj) -> Array[RBI::TypedParam]
150
+ # Compiles a `Method` / `UnboundMethod#parameters` into the `RBI::TypedParam`
151
+ # array expected by `RBI::Scope#create_method`. Argument types default to
152
+ # `T.untyped` since shrine plugins don't carry sigs. Anonymous parameter names
153
+ # — including empty names and the special tokens `:*`, `:**`, `:&` — are
154
+ # replaced with `_arg{index}` so the generated RBI is syntactically valid.
155
+ #: ((Method | UnboundMethod) method_obj) -> Array[RBI::TypedParam]
153
156
  def compile_parameters(method_obj)
154
- method_obj.parameters.filter_map do |(type, name)|
157
+ method_obj.parameters.each_with_index.filter_map do |(type, name), index|
155
158
  name_str = name.to_s
156
- name_str = "arg" if name_str.empty?
159
+ name_str = "_arg#{index}" unless name_str.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
157
160
  case type
158
161
  when :req
159
162
  create_param(name_str, type: "T.untyped")
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.1.9
4
+ version: 0.1.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Angellist
@@ -55,6 +55,7 @@ files:
55
55
  - lib/tapioca/dsl/compilers/active_record_columns_persisted.rb
56
56
  - lib/tapioca/dsl/compilers/active_record_relation_types.rb
57
57
  - lib/tapioca/dsl/compilers/attr_json.rb
58
+ - lib/tapioca/dsl/compilers/draper.rb
58
59
  - lib/tapioca/dsl/compilers/flag_shih_tzu.rb
59
60
  - lib/tapioca/dsl/compilers/kaminari.rb
60
61
  - lib/tapioca/dsl/compilers/money_rails.rb
@@ -67,9 +68,9 @@ licenses:
67
68
  - MIT
68
69
  metadata:
69
70
  bug_tracker_uri: https://github.com/angellist/boba/issues
70
- changelog_uri: https://github.com/angellist/boba/blob/0.1.9/History.md
71
+ changelog_uri: https://github.com/angellist/boba/blob/0.1.10/History.md
71
72
  homepage_uri: https://github.com/angellist/boba
72
- source_code_uri: https://github.com/angellist/boba/tree/0.1.9
73
+ source_code_uri: https://github.com/angellist/boba/tree/0.1.10
73
74
  rubygems_mfa_required: 'true'
74
75
  rdoc_options: []
75
76
  require_paths: