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 +4 -4
- data/lib/boba/version.rb +1 -1
- data/lib/tapioca/dsl/compilers/draper.rb +188 -0
- data/lib/tapioca/dsl/compilers/shrine.rb +8 -5
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 24108e7a191602ef441626b929c5a846004c36b4af6b6c28c86384577c1b1f4e
|
|
4
|
+
data.tar.gz: 150b997c5bb280a71c228df1a4840d190a29dd8f49d3390f3f1b1999f78488c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e89404c6738c73acac5f0c0e0043a883930f6594bc9264d0f48f72ec9ab9bcf554c36163316ebd8ddc335ca3945c9c50bb20f01c2c6fdd490ccca21c072db52e
|
|
7
|
+
data.tar.gz: 42c252740c0258ff9666573bf4f73e67e1de954f86c7a7728cd302490eb34af9eabe1abf0f8956329caaca11574eaef733d127763e00314470ec0df3d6e438b7
|
data/lib/boba/version.rb
CHANGED
|
@@ -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
|
-
|
|
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 = "
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|