phlex-hanami 0.2.0.pre.alpha.2 → 0.2.0.pre.alpha.3

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/lib/phlex/hanami/component.rb +41 -0
  4. data/lib/phlex/hanami/context.rb +21 -1
  5. data/lib/phlex/hanami/contextual.rb +294 -0
  6. data/lib/phlex/hanami/errors/mailer_view_error.rb +1 -0
  7. data/lib/phlex/hanami/errors/missing_context_error.rb +1 -0
  8. data/lib/phlex/hanami/errors/relative_path_error.rb +1 -0
  9. data/lib/phlex/hanami/extensions/mailer.rb +6 -0
  10. data/lib/phlex/hanami/extensions/slice.rb +12 -3
  11. data/lib/phlex/hanami/helpers.rb +3 -2
  12. data/lib/phlex/hanami/mailer/renderable.rb +6 -0
  13. data/lib/phlex/hanami/mailer/text.rb +19 -6
  14. data/lib/phlex/hanami/renderable.rb +14 -237
  15. data/lib/phlex/hanami/rspec.rb +14 -0
  16. data/lib/phlex/hanami/slice_configured.rb +5 -1
  17. data/lib/phlex/hanami/slice_configured_context.rb +9 -1
  18. data/lib/phlex/hanami/testing.rb +156 -0
  19. data/lib/phlex/hanami.rb +3 -1
  20. data/sig/external/dry.rbs +9 -0
  21. data/sig/external/hanami.rbs +77 -0
  22. data/sig/external/hanami_view.rbs +9 -0
  23. data/sig/external/phlex.rbs +16 -0
  24. data/sig/phlex/hanami/component.rbs +9 -0
  25. data/sig/phlex/hanami/context.rbs +39 -0
  26. data/sig/phlex/hanami/contextual.rbs +57 -0
  27. data/sig/phlex/hanami/errors/error.rbs +8 -0
  28. data/sig/phlex/hanami/errors/mailer_view_error.rbs +9 -0
  29. data/sig/phlex/hanami/errors/missing_context_error.rbs +9 -0
  30. data/sig/phlex/hanami/errors/relative_path_error.rbs +9 -0
  31. data/sig/phlex/hanami/extensions/mailer.rbs +25 -0
  32. data/sig/phlex/hanami/extensions/slice.rbs +21 -0
  33. data/sig/phlex/hanami/helpers.rbs +13 -0
  34. data/sig/phlex/hanami/layout.rbs +9 -0
  35. data/sig/phlex/hanami/mailer/layout.rbs +11 -0
  36. data/sig/phlex/hanami/mailer/renderable.rbs +27 -0
  37. data/sig/phlex/hanami/mailer/text.rbs +47 -0
  38. data/sig/phlex/hanami/mailer/view.rbs +11 -0
  39. data/sig/phlex/hanami/mailer.rbs +8 -0
  40. data/sig/phlex/hanami/renderable.rbs +27 -0
  41. data/sig/phlex/hanami/rspec.rbs +1 -0
  42. data/sig/phlex/hanami/slice_configured.rbs +19 -0
  43. data/sig/phlex/hanami/slice_configured_context.rbs +27 -0
  44. data/sig/phlex/hanami/testing.rbs +19 -0
  45. data/sig/phlex/hanami/view.rbs +9 -0
  46. data/sig/phlex/hanami.rbs +7 -0
  47. data/sig/phlex-hanami.rbs +1 -2
  48. metadata +32 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb2ded5cb201e8636daeba76f21fc25b297899eba517ce428d6fcc41450460f9
4
- data.tar.gz: f8935646735d5d5e52faf6c48b5713dae2f4169c02e8162fc4356c32e5cf09bd
3
+ metadata.gz: 4adab6a5900c422ff7da10ee21be828ba77c1d7523da39173433ff7749516e73
4
+ data.tar.gz: d44d102a14b331b1262f692f8479418d8f08557815830899c3fb9be093f34e25
5
5
  SHA512:
6
- metadata.gz: 396360b1808f053b39c29047103c6f860c46ac6bf436890a483848c63639c1e382fc0ad34eefb61f58335a4d158783349670c2b0ee30a421473737c5111e2abf
7
- data.tar.gz: 84fbc91b5c45c6ece40da01070cd64bd9b2702cbc19c10c1ba8eeb3d27cc7aab57e3e60434305b18021dc980cc0a1fff983a051d5cce33b09b200fd87667db52
6
+ metadata.gz: 151ad52da0f7b28599a61385974233e109be3a195710f3d2d971783c25202d830cc04b7d85fb8f19c90c6058c2798f0f8e85bc4bc3a81d86003ec242884fb939
7
+ data.tar.gz: 85e34fbfb79b7cb58352575c5d1cdb18066f4034a427a3d55753992b7f5653d3a348e27c98f593b5db2f56f711fc90755965d5c0470a74f0b467192f6368c833
data/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v0.2.0-alpha.3] - 2026-09-06
11
+
12
+ ### Added
13
+
14
+ - `Phlex::Hanami::Component`, the base class for everything below a view. It reads the same view context a view
15
+ does, and has neither a layout nor the `call` contract Hanami renders a view through, so Hanami cannot render one
16
+ by accident.
17
+ - `Phlex::Hanami::Contextual`, the view context and the slice on their own, for a component base class you already
18
+ have. `Phlex::Hanami::Renderable` includes it and adds the view half.
19
+ - Support for [Phlex kits]. Extend a component namespace with `Phlex::Kit`, include it in a base view, and a view
20
+ calls its components as methods. A kit registers in the slice container as the module, so `slice["components"]`
21
+ returns the kit rather than raising `NoMethodError`.
22
+ - `Phlex::Hanami::Testing::ViewHelpers`, for testing a view or a component without a request. `render` renders the
23
+ instance you give it, `view_context` builds the context an action would have, and `view_request` fakes a request
24
+ for the session, the flash and the CSRF token.
25
+ - `require "phlex/hanami/rspec"` includes those helpers into every RSpec example group tagged `type: :view`. Under
26
+ any other framework, require `phlex/hanami/testing` and include the module where your view tests run.
27
+ - RBS signatures for the public API, shipped in `sig`.
28
+ - Documentation for [components](docs/usage/components.md), [testing](docs/usage/testing.md) and
29
+ [responses](docs/usage/responses.md).
30
+
31
+ [Phlex kits]: https://www.phlex.fun/components/kits.html
32
+
10
33
  ## [v0.2.0-alpha.2] - 2026-09-05
11
34
 
12
35
  ### Added
@@ -43,7 +66,8 @@ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aa
43
66
 
44
67
  Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
45
68
 
46
- [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...HEAD
69
+ [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.3...HEAD
70
+ [v0.2.0-alpha.3]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...0.2.0-alpha.3
47
71
  [v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
48
72
  [v0.2.0-alpha.1]: https://github.com/aaronmallen/phlex-hanami/releases/tag/0.2.0-alpha.1
49
73
  [v0.1.0]: https://github.com/stephannv/phlex-hanami/releases/tag/v0.1.0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # The base class for everything below a view.
6
+ #
7
+ # A component is an ordinary Phlex class you render with `render`. It reads the same context a
8
+ # view does — `path`, `assets`, `content_for`, `flash`, translations — because Phlex shares that
9
+ # context with every component in the tree, however deep. What it does not have is the half of a
10
+ # view that belongs to Hanami: no layout, and no `call(context:, **input)` entry point, because
11
+ # Hanami never calls a component.
12
+ #
13
+ # Subclass this, or include {Contextual} into a base class you already have.
14
+ #
15
+ # @example
16
+ # # app/components/card.rb
17
+ # module MyApp
18
+ # module Components
19
+ # class Card < Phlex::Hanami::Component
20
+ # def initialize(post:) = @post = post
21
+ #
22
+ # def view_template
23
+ # article do
24
+ # h2 { a(href: path(:post, id: @post.id)) { @post.title } }
25
+ # yield if block_given?
26
+ # end
27
+ # end
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ # A component takes the context of whoever renders it, not its own slice's, so one written in
33
+ # the app namespace works unchanged inside a slice's view.
34
+ #
35
+ # @api public
36
+ # @since 0.2.0
37
+ class Component < Phlex::HTML
38
+ include Contextual
39
+ end
40
+ end
41
+ end
@@ -17,6 +17,7 @@ module Phlex
17
17
 
18
18
  # @api private
19
19
  # @since 0.2.0
20
+ #: (singleton(::Hanami::Slice)) -> void
20
21
  def self.configure_for_slice(slice)
21
22
  extend SliceConfiguredContext.new(slice)
22
23
  end
@@ -27,12 +28,20 @@ module Phlex
27
28
  #
28
29
  # @api public
29
30
  # @since 0.2.0
30
- attr_reader :inflector
31
+ attr_reader :inflector #: Dry::Inflector
31
32
 
32
33
  # @see SliceConfiguredContext#define_new
33
34
  #
34
35
  # @api private
35
36
  # @since 0.2.0
37
+ #: (
38
+ # ?inflector: Dry::Inflector?,
39
+ # ?routes: ::Hanami::Slice::RoutesHelper?,
40
+ # ?assets: ::Hanami::Assets?,
41
+ # ?request: ::Hanami::Action::Request?,
42
+ # ?i18n: ::Hanami::Providers::I18n::Backend?,
43
+ # **untyped
44
+ # ) -> void
36
45
  def initialize(inflector: nil, routes: nil, assets: nil, request: nil, i18n: nil, **)
37
46
  @inflector = inflector
38
47
  @routes = routes
@@ -50,6 +59,7 @@ module Phlex
50
59
  #
51
60
  # @api public
52
61
  # @since 0.2.0
62
+ #: () -> ::Hanami::Assets
53
63
  def assets
54
64
  raise ::Hanami::ComponentLoadError, "Assets not available. #{assets_hint}" unless @assets
55
65
 
@@ -65,6 +75,7 @@ module Phlex
65
75
  #
66
76
  # @api public
67
77
  # @since 0.2.0
78
+ #: (Symbol, ?String?) ?{ () -> String } -> String?
68
79
  def content_for(key, value = nil)
69
80
  if block_given?
70
81
  @content_for[key] = yield
@@ -85,6 +96,7 @@ module Phlex
85
96
  #
86
97
  # @api public
87
98
  # @since 0.2.0
99
+ #: () -> String?
88
100
  def csrf_token
89
101
  request.session[::Hanami::Action::CSRFProtection::CSRF_TOKEN]
90
102
  end
@@ -95,6 +107,7 @@ module Phlex
95
107
  #
96
108
  # @api public
97
109
  # @since 0.2.0
110
+ #: () -> ::Hanami::Action::Flash
98
111
  def flash
99
112
  request.flash
100
113
  end
@@ -105,6 +118,7 @@ module Phlex
105
118
  #
106
119
  # @api public
107
120
  # @since 0.2.0
121
+ #: () -> ::Hanami::Providers::I18n::Backend
108
122
  def i18n
109
123
  raise ::Hanami::ComponentLoadError, "the i18n gem is required to access translations" unless @i18n
110
124
 
@@ -113,6 +127,7 @@ module Phlex
113
127
 
114
128
  # @api private
115
129
  # @since 0.2.0
130
+ #: (self) -> void
116
131
  def initialize_copy(source)
117
132
  super
118
133
  @content_for = source.instance_variable_get(:@content_for).dup
@@ -126,6 +141,7 @@ module Phlex
126
141
  #
127
142
  # @api public
128
143
  # @since 0.2.0
144
+ #: () -> ::Hanami::Action::Request
129
145
  def request
130
146
  unless @request
131
147
  raise ::Hanami::ComponentLoadError, <<~MESSAGE
@@ -142,6 +158,7 @@ module Phlex
142
158
  #
143
159
  # @api public
144
160
  # @since 0.2.0
161
+ #: () -> bool
145
162
  def request?
146
163
  !!@request
147
164
  end
@@ -154,6 +171,7 @@ module Phlex
154
171
  #
155
172
  # @api public
156
173
  # @since 0.2.0
174
+ #: () -> ::Hanami::Slice::RoutesHelper
157
175
  def routes
158
176
  raise ::Hanami::ComponentLoadError, "the hanami-router gem is required to access routes" unless @routes
159
177
 
@@ -166,12 +184,14 @@ module Phlex
166
184
  #
167
185
  # @api public
168
186
  # @since 0.2.0
187
+ #: () -> ::Hanami::Action::Request::Session
169
188
  def session
170
189
  request.session
171
190
  end
172
191
 
173
192
  private
174
193
 
194
+ #: () -> String
175
195
  def assets_hint
176
196
  if ::Hanami.bundled?("hanami-assets")
177
197
  "Have you put files into your assets directory?"
@@ -0,0 +1,294 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phlex
4
+ module Hanami
5
+ # Gives a Phlex class the Hanami view context and the slice it was defined in.
6
+ #
7
+ # This is the half of the integration a component needs: `path`, `assets`, `content_for`,
8
+ # `flash`, translations and the rest, read off the context Hanami builds for the request.
9
+ # {Renderable} includes it and adds the `call` contract Hanami's `Response#render` expects;
10
+ # include this on its own for a class Hanami never calls directly.
11
+ #
12
+ # {Component} subclasses it already. Include this instead when you have your own component base
13
+ # class.
14
+ #
15
+ # @example
16
+ # module MyApp
17
+ # class Component < Phlex::HTML
18
+ # include Phlex::Hanami::Contextual
19
+ # end
20
+ # end
21
+ #
22
+ # @api public
23
+ # @since 0.2.0
24
+ module Contextual
25
+ # @api private
26
+ # @since 0.2.0
27
+ #: (singleton(::Phlex::SGML)) -> void
28
+ def self.included(view_class)
29
+ view_class.extend(::Hanami::SliceConfigurable)
30
+ view_class.extend(ClassMethods)
31
+ return unless defined?(::Hanami::Helpers::I18nHelper)
32
+
33
+ # Order matters: a module included later sits higher in the lookup chain, so the overrides
34
+ # have to go in after Hanami's helper, not alongside it in this module.
35
+ view_class.include(::Hanami::Helpers::I18nHelper)
36
+ view_class.include(I18nOverrides)
37
+ end
38
+
39
+ # Relative-key resolution for Hanami's i18n helper.
40
+ #
41
+ # A Phlex view has no template, so Hanami's implementation — which reads
42
+ # `_context.current_template_name` — cannot work. Included after `Hanami::Helpers::I18nHelper`
43
+ # so that it wins: a module included later sits higher in the lookup chain.
44
+ #
45
+ # @api private
46
+ # @since 0.2.0
47
+ module I18nOverrides
48
+ private
49
+
50
+ # Resolves a relative translation key (`t(".title")`) against the view's own container key, so
51
+ # `MyApp::Views::Posts::Index` looks up `posts.index.title`.
52
+ #
53
+ # Hanami's own implementation uses `_context.current_template_name`, which is hanami-view's
54
+ # template stack and has no meaning for a Phlex view.
55
+ #: (String | Symbol) -> (String | Symbol)
56
+ def _resolve_i18n_key(key)
57
+ return key unless key.to_s.start_with?(".")
58
+
59
+ base = i18n_key_base
60
+ unless base
61
+ raise ::I18n::ArgumentError,
62
+ "Cannot resolve the relative translation key #{key.inspect} for #{self.class}. " \
63
+ "Give the view a name inside a slice, or use an absolute key (without a leading dot)."
64
+ end
65
+
66
+ "#{base}#{key}"
67
+ end
68
+
69
+ # The view's container key, dotted: `MyApp::Views::Posts::Index` in the app slice becomes
70
+ # `posts.index`. Nil for an anonymous view, or one outside any slice.
71
+ #: () -> String?
72
+ def i18n_key_base
73
+ view_slice = self.class.slice
74
+ name = self.class.name
75
+ return nil unless view_slice && name
76
+
77
+ view_slice.inflector
78
+ .underscore(name)
79
+ .sub(%r{^#{view_slice.slice_name.path}/}, "")
80
+ .sub(%r{^views/}, "")
81
+ .tr("/", ".")
82
+ end
83
+ end
84
+
85
+ # @api public
86
+ # @since 0.2.0
87
+ module ClassMethods
88
+ # @api private
89
+ # @since 0.2.0
90
+ #: (singleton(::Hanami::Slice)) -> void
91
+ def configure_for_slice(slice)
92
+ extend SliceConfigured.new(slice)
93
+ end
94
+
95
+ # The slice this class belongs to, or nil when it is defined outside a slice namespace.
96
+ #
97
+ # A class with no slice still renders: the context comes from whoever is rendering it, not
98
+ # from the slice. What it loses is the slice-relative half — `t(".title")` has nothing to
99
+ # resolve against and raises.
100
+ #
101
+ # @return [Hanami::Slice, nil]
102
+ #
103
+ # @api public
104
+ # @since 0.2.0
105
+ #: () -> singleton(::Hanami::Slice)?
106
+ def slice
107
+ nil
108
+ end
109
+ end
110
+
111
+ # The URL for an asset.
112
+ #
113
+ # @example
114
+ # script(src: asset_url("app.js"))
115
+ #
116
+ # @param source [String] the asset's source path
117
+ #
118
+ # @return [String] the URL
119
+ #
120
+ # @api public
121
+ # @since 0.2.0
122
+ #: (String) -> String
123
+ def asset_url(source)
124
+ assets[source].url
125
+ end
126
+
127
+ # The slice's assets.
128
+ #
129
+ # @api public
130
+ # @since 0.2.0
131
+ #: () -> ::Hanami::Assets
132
+ def assets
133
+ hanami_context.assets
134
+ end
135
+
136
+ # Stores a string of markup for later use, or reads back what was stored.
137
+ #
138
+ # @api public
139
+ # @since 0.2.0
140
+ #: (Symbol, ?String?) ?{ () -> String } -> String?
141
+ def content_for(...)
142
+ hanami_context.content_for(...)
143
+ end
144
+
145
+ # The current request's CSRF token.
146
+ #
147
+ # @return [String]
148
+ #
149
+ # @api public
150
+ # @since 0.2.0
151
+ #: () -> String?
152
+ def csrf_token
153
+ hanami_context.csrf_token
154
+ end
155
+
156
+ # The flash hash for the current request.
157
+ #
158
+ # @api public
159
+ # @since 0.2.0
160
+ #: () -> ::Hanami::Action::Flash
161
+ def flash
162
+ hanami_context.flash
163
+ end
164
+
165
+ # The Hanami view context for this render.
166
+ #
167
+ # Read from Phlex's user context, which is shared with every component in the render tree, so
168
+ # a component nested any number of levels deep sees the same context.
169
+ #
170
+ # @return [Object] a `Hanami::View::Context` when hanami-view is bundled, otherwise a {Context}
171
+ #
172
+ # @raise [MissingContextError] when the view was rendered without a context
173
+ #
174
+ # @api public
175
+ # @since 0.2.0
176
+ #: () -> (::Hanami::View::Context | Context)
177
+ def hanami_context
178
+ context[CONTEXT_KEY] || raise(MissingContextError, self.class)
179
+ end
180
+
181
+ # Whether a Hanami view context is available.
182
+ #
183
+ # @return [Boolean]
184
+ #
185
+ # @api public
186
+ # @since 0.2.0
187
+ #: () -> bool
188
+ def hanami_context?
189
+ !context[CONTEXT_KEY].nil?
190
+ end
191
+
192
+ # The slice's i18n backend.
193
+ #
194
+ # @api public
195
+ # @since 0.2.0
196
+ #: () -> ::Hanami::Providers::I18n::Backend
197
+ def i18n
198
+ hanami_context.i18n
199
+ end
200
+
201
+ # The path for a named route.
202
+ #
203
+ # @example
204
+ # a(href: path(:posts)) { "Posts" }
205
+ #
206
+ # @return [String]
207
+ #
208
+ # @api public
209
+ # @since 0.2.0
210
+ #: (*untyped, **untyped) -> String
211
+ def path(...)
212
+ routes.path(...)
213
+ end
214
+
215
+ # The current request.
216
+ #
217
+ # @return [Hanami::Action::Request]
218
+ #
219
+ # @api public
220
+ # @since 0.2.0
221
+ #: () -> ::Hanami::Action::Request
222
+ def request
223
+ hanami_context.request
224
+ end
225
+
226
+ # Whether the view is being rendered from within a request.
227
+ #
228
+ # @return [Boolean]
229
+ #
230
+ # @api public
231
+ # @since 0.2.0
232
+ #: () -> bool
233
+ def request?
234
+ hanami_context? && hanami_context.request?
235
+ end
236
+
237
+ # The app's routes helper.
238
+ #
239
+ # @return [Hanami::Slice::RoutesHelper]
240
+ #
241
+ # @api public
242
+ # @since 0.2.0
243
+ #: () -> ::Hanami::Slice::RoutesHelper
244
+ def routes
245
+ hanami_context.routes
246
+ end
247
+
248
+ # The session for the current request.
249
+ #
250
+ # @api public
251
+ # @since 0.2.0
252
+ #: () -> ::Hanami::Action::Request::Session
253
+ def session
254
+ hanami_context.session
255
+ end
256
+
257
+ # The slice this class belongs to.
258
+ #
259
+ # @return [Hanami::Slice, nil]
260
+ #
261
+ # @api public
262
+ # @since 0.2.0
263
+ #: () -> singleton(::Hanami::Slice)?
264
+ def slice
265
+ self.class.slice
266
+ end
267
+
268
+ # The full URL for a named route.
269
+ #
270
+ # Hanami's routes helper returns a `URI`; Phlex writes strings, and rejects anything else as
271
+ # an attribute value, so this hands back the string.
272
+ #
273
+ # @example
274
+ # a(href: url(:posts)) { "Posts" }
275
+ #
276
+ # @return [String]
277
+ #
278
+ # @api public
279
+ # @since 0.2.0
280
+ #: (*untyped, **untyped) -> String
281
+ def url(...)
282
+ routes.url(...).to_s
283
+ end
284
+
285
+ private
286
+
287
+ # Hanami's helper modules reach for the view context under this name.
288
+ #: () -> (::Hanami::View::Context | Context)
289
+ def _context
290
+ hanami_context
291
+ end
292
+ end
293
+ end
294
+ end
@@ -12,6 +12,7 @@ module Phlex
12
12
  class MailerViewError < Error
13
13
  # @api private
14
14
  # @since 0.2.0
15
+ #: (singleton(::Hanami::Mailer), singleton(::Phlex::SGML)) -> void
15
16
  def initialize(mailer_class, view_class)
16
17
  super(<<~MESSAGE)
17
18
  #{mailer_class} is paired with #{view_class}, which is not a mail view.
@@ -9,6 +9,7 @@ module Phlex
9
9
  class MissingContextError < Error
10
10
  # @api private
11
11
  # @since 0.2.0
12
+ #: (singleton(::Phlex::SGML)) -> void
12
13
  def initialize(view_class)
13
14
  super(<<~MESSAGE)
14
15
  #{view_class} was rendered without a Hanami view context.
@@ -12,6 +12,7 @@ module Phlex
12
12
  class RelativePathError < Error
13
13
  # @api private
14
14
  # @since 0.2.0
15
+ #: (singleton(::Phlex::SGML)) -> void
15
16
  def initialize(view_class)
16
17
  super(<<~MESSAGE)
17
18
  #{view_class} asked for a relative path, and an email needs a full URL.
@@ -21,6 +21,7 @@ module Phlex
21
21
  #
22
22
  # @api private
23
23
  # @since 0.2.0
24
+ #: () -> void
24
25
  def self.install
25
26
  ::Hanami::Mailer.prepend(self)
26
27
  ::Hanami::Mailer.singleton_class.prepend(ClassMethods)
@@ -31,6 +32,7 @@ module Phlex
31
32
  module ClassMethods
32
33
  # @api private
33
34
  # @since 0.2.0
35
+ #: (singleton(::Hanami::Slice)) -> void
34
36
  def configure_for_slice(slice)
35
37
  super
36
38
  extend SliceConfigured.new(slice)
@@ -49,6 +51,7 @@ module Phlex
49
51
  #
50
52
  # @api private
51
53
  # @since 0.2.0
54
+ #: () -> singleton(::Phlex::SGML)?
52
55
  def phlex_view
53
56
  mailer_slice = slice
54
57
  return nil unless mailer_slice && name
@@ -69,12 +72,14 @@ module Phlex
69
72
  #
70
73
  # @api private
71
74
  # @since 0.2.0
75
+ #: () -> singleton(::Hanami::Slice)?
72
76
  def slice
73
77
  nil
74
78
  end
75
79
 
76
80
  private
77
81
 
82
+ #: (singleton(::Hanami::Slice)) -> String
78
83
  def view_name(mailer_slice)
79
84
  mailer_slice.inflector
80
85
  .underscore(name)
@@ -93,6 +98,7 @@ module Phlex
93
98
  #
94
99
  # @api private
95
100
  # @since 0.2.0
101
+ #: () -> untyped
96
102
  def view
97
103
  @view || self.class.phlex_view || super
98
104
  end
@@ -18,26 +18,31 @@ module Phlex
18
18
  # @api private
19
19
  # @since 0.2.0
20
20
  module Slice
21
- # Returns Phlex classes as classes, and everything else as the loader would.
21
+ # Returns Phlex classes and kits as themselves, and everything else as the loader would.
22
22
  #
23
23
  # `component.loader.constant` is what the default autoloading loader already calls, so no
24
24
  # extra loading happens here and a constant that fails to load still raises
25
25
  # `Dry::System::ComponentNotLoadableError`.
26
26
  #
27
+ # A kit is a module rather than a class, and the default loader calls `new` on whatever it
28
+ # resolves, so `slice["components"]` would raise `NoMethodError` for one. Registering it as
29
+ # the module hands back the thing a kit is for.
30
+ #
27
31
  # @api private
28
32
  # @since 0.2.0
29
33
  COMPONENT_INSTANCE = proc { |component, *args, **kwargs|
30
34
  constant = component.loader.constant(component)
31
35
 
32
- if constant.is_a?(Class) && constant < Phlex::SGML
36
+ if (constant.is_a?(Class) && constant < Phlex::SGML) || constant.is_a?(Phlex::Kit)
33
37
  constant
34
38
  else
35
39
  component.loader.call(component, *args, **kwargs)
36
40
  end
37
- }
41
+ } #: ^(untyped, *untyped, **untyped) -> untyped
38
42
 
39
43
  # @api private
40
44
  # @since 0.2.0
45
+ #: (?String?) -> untyped
41
46
  def prepare(provider_name = nil)
42
47
  result = super
43
48
 
@@ -59,6 +64,7 @@ module Phlex
59
64
  #
60
65
  # @api private
61
66
  # @since 0.2.0
67
+ #: (singleton(::Hanami::Slice)) -> singleton(Context)?
62
68
  def define_view_context(slice)
63
69
  return if ::Hanami.bundled?("hanami-view")
64
70
 
@@ -78,6 +84,7 @@ module Phlex
78
84
  #
79
85
  # @api private
80
86
  # @since 0.2.0
87
+ #: (singleton(::Hanami::Slice)) -> void
81
88
  def register_phlex_components(slice)
82
89
  component_dirs = slice.container.config.component_dirs
83
90
  return if component_dirs.configured?(:instance)
@@ -95,6 +102,7 @@ module Phlex
95
102
  #
96
103
  # @api private
97
104
  # @since 0.2.0
105
+ #: (singleton(::Hanami::Slice)) -> (singleton(::Hanami::View::Context) | singleton(Context))
98
106
  def view_context_class(slice)
99
107
  return ::Hanami::Extensions::View::Context.context_class(slice) if ::Hanami.bundled?("hanami-view")
100
108
 
@@ -103,6 +111,7 @@ module Phlex
103
111
 
104
112
  private
105
113
 
114
+ #: (singleton(::Hanami::Slice)) -> Module
106
115
  def views_namespace(slice)
107
116
  if slice.namespace.const_defined?(:Views, false)
108
117
  slice.namespace.const_get(:Views, false)
@@ -46,7 +46,7 @@ module Phlex
46
46
  OVERRIDDEN_METHODS = {
47
47
  raw: Phlex::SGML.instance_method(:raw),
48
48
  tag: Phlex::HTML.instance_method(:tag),
49
- }.freeze
49
+ }.freeze #: Hash[Symbol, UnboundMethod]
50
50
 
51
51
  # Phlex's implementations, mixed back in over Hanami's.
52
52
  #
@@ -56,10 +56,11 @@ module Phlex
56
56
  OVERRIDDEN_METHODS.each do |name, method|
57
57
  define_method(name) { |*args, **options, &block| method.bind_call(self, *args, **options, &block) }
58
58
  end
59
- end
59
+ end #: Module
60
60
 
61
61
  # @api private
62
62
  # @since 0.2.0
63
+ #: (singleton(::Phlex::SGML)) -> void
63
64
  def self.included(view_class)
64
65
  view_class.include(::Hanami::Extensions::View::StandardHelpers)
65
66
  view_class.include(PhlexMethods)