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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f3bbf276bdafe4ad59d597cfa3d481db980d8a1cc0639066289ee72671bee01
4
- data.tar.gz: ed16c9705abe9aaa2997124342b920d748aac6d131bc25b4930a4bdfd8c82e4b
3
+ metadata.gz: 24108e7a191602ef441626b929c5a846004c36b4af6b6c28c86384577c1b1f4e
4
+ data.tar.gz: 150b997c5bb280a71c228df1a4840d190a29dd8f49d3390f3f1b1999f78488c7
5
5
  SHA512:
6
- metadata.gz: 3efa241e2373a7566a2f9a565366a750947854c2af1ac7d26e66b934f6b4c18835ca8c58b64ec22f107871b46a1bde563663b1d30a1fe3dea54ba9c7afe8d82c
7
- data.tar.gz: 59a567e245ad5d424bfccc9c101584500235ef2a71282e65cdcba5cb6448267684bc9b6fe5e85309c56db96ec8d0e088fa3a3bc33500b12a81614e9c122ff95e
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.8"
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
@@ -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.8
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.8/History.md
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.8
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: