boba 0.1.8 → 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 +181 -0
- metadata +5 -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
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
return unless defined?(Shrine)
|
|
5
|
+
|
|
6
|
+
module Tapioca
|
|
7
|
+
module Dsl
|
|
8
|
+
module Compilers
|
|
9
|
+
# `Tapioca::Dsl::Compilers::Shrine` decorates RBI files for classes that include
|
|
10
|
+
# a `Shrine::Attachment` module provided by the `shrine` gem.
|
|
11
|
+
# https://github.com/shrinerb/shrine
|
|
12
|
+
#
|
|
13
|
+
# For example, with the following model:
|
|
14
|
+
# ~~~rb
|
|
15
|
+
# class Photo < ActiveRecord::Base
|
|
16
|
+
# include ImageUploader::Attachment(:image)
|
|
17
|
+
# end
|
|
18
|
+
# ~~~
|
|
19
|
+
#
|
|
20
|
+
# This compiler will generate the following RBI:
|
|
21
|
+
# ~~~rbi
|
|
22
|
+
# class Photo
|
|
23
|
+
# include ShrineGeneratedMethods
|
|
24
|
+
# extend ShrineGeneratedClassMethods
|
|
25
|
+
#
|
|
26
|
+
# module ShrineGeneratedClassMethods
|
|
27
|
+
# sig { params(options: T.untyped).returns(::Shrine::Attacher) }
|
|
28
|
+
# def image_attacher(**options); end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# module ShrineGeneratedMethods
|
|
32
|
+
# sig { returns(T.nilable(::Shrine::UploadedFile)) }
|
|
33
|
+
# def image; end
|
|
34
|
+
#
|
|
35
|
+
# sig { params(value: T.untyped).returns(T.untyped) }
|
|
36
|
+
# def image=(value); end
|
|
37
|
+
#
|
|
38
|
+
# sig { params(options: T.untyped).returns(T.nilable(::Shrine::Attacher)) }
|
|
39
|
+
# def image_attacher(**options); end
|
|
40
|
+
#
|
|
41
|
+
# sig { returns(T::Boolean) }
|
|
42
|
+
# def image_changed?; end
|
|
43
|
+
#
|
|
44
|
+
# sig { params(args: T.untyped, options: T.untyped).returns(T.nilable(String)) }
|
|
45
|
+
# def image_url(*args, **options); end
|
|
46
|
+
# end
|
|
47
|
+
# end
|
|
48
|
+
# ~~~
|
|
49
|
+
class Shrine < Tapioca::Dsl::Compiler
|
|
50
|
+
InstanceMethodModuleName = "ShrineGeneratedMethods"
|
|
51
|
+
ClassMethodModuleName = "ShrineGeneratedClassMethods"
|
|
52
|
+
|
|
53
|
+
ConstantType = type_member { { fixed: T.class_of(Object) } }
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
# @override
|
|
57
|
+
#: -> Enumerable[Module[top]]
|
|
58
|
+
def gather_constants
|
|
59
|
+
all_classes.select do |klass|
|
|
60
|
+
klass.ancestors.any? { |ancestor| ancestor.is_a?(::Shrine::Attachment) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @override
|
|
66
|
+
#: -> void
|
|
67
|
+
def decorate
|
|
68
|
+
attachments = shrine_attachments(constant)
|
|
69
|
+
return if attachments.empty?
|
|
70
|
+
|
|
71
|
+
root.create_path(constant) do |klass|
|
|
72
|
+
instance_module = RBI::Module.new(InstanceMethodModuleName)
|
|
73
|
+
class_module = RBI::Module.new(ClassMethodModuleName)
|
|
74
|
+
|
|
75
|
+
attachments.each do |attachment|
|
|
76
|
+
name = attachment.attachment_name
|
|
77
|
+
|
|
78
|
+
# Filter to methods that follow shrine's naming convention (<name> or <name>= or <name>_*).
|
|
79
|
+
# This excludes method overrides like `reload` from the ActiveRecord plugin.
|
|
80
|
+
attachment.instance_methods(false).sort
|
|
81
|
+
.filter { |m| m == name || m == :"#{name}=" || m.start_with?("#{name}_") }
|
|
82
|
+
.each do |method_name|
|
|
83
|
+
method_obj = attachment.instance_method(method_name)
|
|
84
|
+
instance_module.create_method(
|
|
85
|
+
method_name.to_s,
|
|
86
|
+
parameters: compile_parameters(method_obj),
|
|
87
|
+
return_type: return_type_for(name, method_name),
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Class method from entity plugin:
|
|
92
|
+
# .<name>_attacher - returns a class-level attacher instance
|
|
93
|
+
next unless constant.respond_to?(:"#{name}_attacher")
|
|
94
|
+
|
|
95
|
+
class_method_obj = constant.method(:"#{name}_attacher")
|
|
96
|
+
class_module.create_method(
|
|
97
|
+
"#{name}_attacher",
|
|
98
|
+
parameters: compile_parameters(class_method_obj),
|
|
99
|
+
return_type: "::Shrine::Attacher",
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
klass << instance_module
|
|
104
|
+
klass.create_include(InstanceMethodModuleName)
|
|
105
|
+
klass << class_module
|
|
106
|
+
klass.create_extend(ClassMethodModuleName)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
#: (singleton(Object) klass) -> Array[::Shrine::Attachment]
|
|
113
|
+
def shrine_attachments(klass)
|
|
114
|
+
klass.ancestors
|
|
115
|
+
.filter_map { |ancestor| ancestor if ancestor.is_a?(::Shrine::Attachment) }
|
|
116
|
+
.sort_by(&:attachment_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Maps a dynamically discovered method name to its return type.
|
|
120
|
+
#
|
|
121
|
+
# Known method patterns (from shrine's entity/model plugins) are given
|
|
122
|
+
# specific return types. Methods that don't match any known pattern
|
|
123
|
+
# (e.g. those added by third-party shrine plugins) fall back to
|
|
124
|
+
# T.untyped so they still appear in the generated RBI.
|
|
125
|
+
#
|
|
126
|
+
# #<name> -> entity plugin: returns the attached file
|
|
127
|
+
# #<name>_url -> entity plugin: returns the URL to the file
|
|
128
|
+
# #<name>_attacher -> entity plugin: returns the Attacher instance
|
|
129
|
+
# #<name>= -> model plugin: assigns a file (setter)
|
|
130
|
+
# #<name>_changed? -> model plugin: checks if attachment changed
|
|
131
|
+
#
|
|
132
|
+
#: (Symbol attachment_name, Symbol method_name) -> String?
|
|
133
|
+
def return_type_for(attachment_name, method_name)
|
|
134
|
+
case method_name
|
|
135
|
+
when attachment_name
|
|
136
|
+
"T.nilable(::Shrine::UploadedFile)"
|
|
137
|
+
when :"#{attachment_name}_url"
|
|
138
|
+
"T.nilable(::String)"
|
|
139
|
+
when :"#{attachment_name}_attacher"
|
|
140
|
+
"T.nilable(::Shrine::Attacher)"
|
|
141
|
+
when /=$/
|
|
142
|
+
nil
|
|
143
|
+
when /\?$/
|
|
144
|
+
"T::Boolean"
|
|
145
|
+
else
|
|
146
|
+
"T.untyped"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
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]
|
|
156
|
+
def compile_parameters(method_obj)
|
|
157
|
+
method_obj.parameters.each_with_index.filter_map do |(type, name), index|
|
|
158
|
+
name_str = name.to_s
|
|
159
|
+
name_str = "_arg#{index}" unless name_str.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
160
|
+
case type
|
|
161
|
+
when :req
|
|
162
|
+
create_param(name_str, type: "T.untyped")
|
|
163
|
+
when :opt
|
|
164
|
+
create_opt_param(name_str, type: "T.untyped", default: "T.unsafe(nil)")
|
|
165
|
+
when :rest
|
|
166
|
+
create_rest_param(name_str, type: "T.untyped")
|
|
167
|
+
when :keyreq
|
|
168
|
+
create_kw_param(name_str, type: "T.untyped")
|
|
169
|
+
when :key
|
|
170
|
+
create_kw_opt_param(name_str, type: "T.untyped", default: "T.unsafe(nil)")
|
|
171
|
+
when :keyrest
|
|
172
|
+
create_kw_rest_param(name_str, type: "T.untyped")
|
|
173
|
+
when :block
|
|
174
|
+
create_block_param(name_str, type: "T.untyped")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
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,20 +55,22 @@ 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
|
|
61
62
|
- lib/tapioca/dsl/compilers/noticed.rb
|
|
62
63
|
- lib/tapioca/dsl/compilers/paperclip.rb
|
|
64
|
+
- lib/tapioca/dsl/compilers/shrine.rb
|
|
63
65
|
- lib/tapioca/dsl/compilers/state_machines_extended.rb
|
|
64
66
|
homepage: https://github.com/angellist/boba
|
|
65
67
|
licenses:
|
|
66
68
|
- MIT
|
|
67
69
|
metadata:
|
|
68
70
|
bug_tracker_uri: https://github.com/angellist/boba/issues
|
|
69
|
-
changelog_uri: https://github.com/angellist/boba/blob/0.1.
|
|
71
|
+
changelog_uri: https://github.com/angellist/boba/blob/0.1.10/History.md
|
|
70
72
|
homepage_uri: https://github.com/angellist/boba
|
|
71
|
-
source_code_uri: https://github.com/angellist/boba/tree/0.1.
|
|
73
|
+
source_code_uri: https://github.com/angellist/boba/tree/0.1.10
|
|
72
74
|
rubygems_mfa_required: 'true'
|
|
73
75
|
rdoc_options: []
|
|
74
76
|
require_paths:
|