hanami 2.0.0.alpha7.1 → 2.0.0.alpha8

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: 8db2d523a265dd089048a2a944ccf8b1d5d46a36d496be5638f4807fe3c451ad
4
- data.tar.gz: fe510075e1e4d362e6c6419aab0d0b001eb8bf80329d51f7546f9955abf08cf6
3
+ metadata.gz: 36c07c10545a327f0eab55e1a23e3a6f1b258dd7c4db890d16155d98a132bb90
4
+ data.tar.gz: 02a8dbd6f028e198dead1d972aef4a78b04246b3201e2c90f94e2827f4582906
5
5
  SHA512:
6
- metadata.gz: 1af19f64f99af6997e772e25d49a5e835ea9e8ebac64aaab637fe56a0c91ea1b87d4a13d04b26145b58573d12478bd435183873e4cf7378e2b2046866e9a0fa4
7
- data.tar.gz: d7be6b435ac445a23ceb616180493c863715dca8a039a81e2ab3af0aa49d60f73806625909dbee05aad4b1bab77b7b77fc6c94600f937cbe7e5af206f67efa82
6
+ metadata.gz: 938be86da6f4c1d5e797db8f0cc185feb2ac070c4d8ba10f48b6bbe922f8b1fd8b618c9871306a7f3d2d75c34a8fab857949d7f18dd04bb833be092fbb8ced27
7
+ data.tar.gz: 459f220765e02c024fe4b1a5e17ec7a13bba10e2cb3f1b64dfa059cd351bea4cffcd47be1ca5a1cd8b2c89757f20dbcaaa7e77fbfdacb05c9fc87345974e7ec0
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
1
  # Hanami
2
2
  The web, with simplicity.
3
3
 
4
+ ## v2.0.0.alpha8 - 2020-05-19
5
+
6
+ ## Added
7
+ - [Tim Riley] Introduced `Hanami::Application::Action` as base class for actions that integrate with Hanami applications. Base action classes in Hanami applications should now inherit from this.
8
+ - [Tim Riley] Introduced `Hanami::Application::View` and `Hanami::Application::View::Context` as base classes for views and view contexts that integrate with Hanami applications. Base view classes in Hanami applications should now inherit from these.
9
+ - [Tim Riley] Introduced `Hanami::Application.application_name`, which returns an `Hanami::SliceName` instance, with methods for representing the application name in various formats.
10
+
11
+ ## Fixed
12
+ - [Andrew Croome] When a request is halted, do not attempt to automatically render any view paired with an `Hanami::Application::Action`
13
+
14
+ ## Changed
15
+ - [Tim Riley] `Hanami::Application.namespace_name`, `.namespace_path` have been removed. These can now be accessed from the `.application_name`.
16
+ - [Tim Riley] `Hanami::Slice.slice_name` now returns an `Hanami::SliceName` instance instead of a Symbol
17
+ - [Tim Riley] `Hanami::Slice.namespace_path` has been removed. This can now be accessed from the `.slice_name`.
18
+
4
19
  ## v2.0.0.alpha7.1 - 2020-03-09
5
20
 
6
21
  ## Fixed
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/action"
4
+
5
+ module Hanami
6
+ class Application
7
+ class Action < Hanami::Action
8
+ # Provides slice-specific configuration and behavior for any action class defined
9
+ # within a slice's module namespace.
10
+ #
11
+ # @api private
12
+ # @since 2.0.0
13
+ class SliceConfiguredAction < Module
14
+ attr_reader :slice
15
+
16
+ def initialize(slice)
17
+ super()
18
+ @slice = slice
19
+ end
20
+
21
+ def extended(action_class)
22
+ configure_action(action_class)
23
+ extend_behavior(action_class)
24
+ define_new
25
+ end
26
+
27
+ def inspect
28
+ "#<#{self.class.name}[#{slice.name}]>"
29
+ end
30
+
31
+ private
32
+
33
+ # @see Hanami::Application::Action#initialize
34
+ def define_new
35
+ resolve_view = method(:resolve_paired_view)
36
+ resolve_view_context = method(:resolve_view_context)
37
+ resolve_routes = method(:resolve_routes)
38
+
39
+ define_method(:new) do |**kwargs|
40
+ super(
41
+ view: kwargs.fetch(:view) { resolve_view.(self) },
42
+ view_context: kwargs.fetch(:view_context) { resolve_view_context.(self) },
43
+ routes: kwargs.fetch(:routes) { resolve_routes.() },
44
+ **kwargs,
45
+ )
46
+ end
47
+ end
48
+
49
+ def configure_action(action_class)
50
+ action_class.config.settings.each do |setting|
51
+ action_class.config.public_send :"#{setting}=", actions_config.public_send(:"#{setting}")
52
+ end
53
+ end
54
+
55
+ def extend_behavior(action_class)
56
+ if actions_config.sessions.enabled?
57
+ require "hanami/action/session"
58
+ action_class.include Hanami::Action::Session
59
+ end
60
+
61
+ if actions_config.csrf_protection
62
+ require "hanami/action/csrf_protection"
63
+ action_class.include Hanami::Action::CSRFProtection
64
+ end
65
+
66
+ if actions_config.cookies.enabled?
67
+ require "hanami/action/cookies"
68
+ action_class.include Hanami::Action::Cookies
69
+ end
70
+ end
71
+
72
+ def resolve_paired_view(action_class)
73
+ view_identifiers = actions_config.view_name_inferrer.call(
74
+ action_class_name: action_class.name,
75
+ slice: slice,
76
+ )
77
+
78
+ view_identifiers.detect do |identifier|
79
+ break slice[identifier] if slice.key?(identifier)
80
+ end
81
+ end
82
+
83
+ def resolve_view_context(_action_class)
84
+ identifier = actions_config.view_context_identifier
85
+
86
+ if slice.key?(identifier)
87
+ slice[identifier]
88
+ elsif slice.application.key?(identifier)
89
+ slice.application[identifier]
90
+ end
91
+ end
92
+
93
+ def resolve_routes
94
+ slice.application[:routes_helper] if slice.application.key?(:routes_helper)
95
+ end
96
+
97
+ def actions_config
98
+ slice.application.config.actions
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/action"
4
+ require "hanami/slice_configurable"
5
+ require_relative "action/slice_configured_action"
6
+
7
+ module Hanami
8
+ class Application
9
+ # Superclass for actions intended for use within an Hanami application.
10
+ #
11
+ # @see Hanami::Action
12
+ #
13
+ # @api public
14
+ # @since 2.0.0
15
+ class Action < Hanami::Action
16
+ extend Hanami::SliceConfigurable
17
+
18
+ class << self
19
+ # @api private
20
+ def configure_for_slice(slice)
21
+ extend SliceConfiguredAction.new(slice)
22
+ end
23
+ end
24
+
25
+ attr_reader :view, :view_context, :routes
26
+
27
+ # @see SliceConfiguredAction#define_new
28
+ # @api public
29
+ def initialize(view: nil, view_context: nil, routes: nil, **kwargs)
30
+ @view = view
31
+ @view_context = view_context
32
+ @routes = routes
33
+
34
+ super(**kwargs)
35
+ end
36
+
37
+ private
38
+
39
+ def build_response(**options)
40
+ options = options.merge(view_options: method(:view_options))
41
+ super(**options)
42
+ end
43
+
44
+ def view_options(req, res)
45
+ {context: view_context&.with(**view_context_options(req, res))}.compact
46
+ end
47
+
48
+ def view_context_options(req, res)
49
+ {request: req, response: res}
50
+ end
51
+
52
+ def finish(req, res, halted)
53
+ res.render(view, **req.params) if !halted && auto_render?(res)
54
+ super
55
+ end
56
+
57
+ # Returns true if a view should automatically be rendered onto the response body.
58
+ #
59
+ # This may be overridden to enable/disable automatic rendering.
60
+ #
61
+ # @param res [Hanami::Action::Response]
62
+ #
63
+ # @return [Boolean]
64
+ #
65
+ # @since 2.0.0
66
+ # @api public
67
+ def auto_render?(res)
68
+ view && res.body.empty?
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+ require "hanami/view/context"
5
+ require_relative "../../errors"
6
+ require_relative "../../slice_configurable"
7
+ require_relative "slice_configured_context"
8
+
9
+ module Hanami
10
+ class Application
11
+ class View < Hanami::View
12
+ # View context for views in Hanami applications.
13
+ #
14
+ # @api public
15
+ # @since 2.0.0
16
+ class Context < Hanami::View::Context
17
+ extend Hanami::SliceConfigurable
18
+
19
+ # @api private
20
+ def self.configure_for_slice(slice)
21
+ extend SliceConfiguredContext.new(slice)
22
+ end
23
+
24
+ # @see SliceConfiguredContext#define_new
25
+ def initialize(**kwargs)
26
+ defaults = {content: {}}
27
+
28
+ super(**kwargs, **defaults)
29
+ end
30
+
31
+ def inflector
32
+ _options.fetch(:inflector)
33
+ end
34
+
35
+ def routes
36
+ _options.fetch(:routes)
37
+ end
38
+
39
+ def settings
40
+ _options.fetch(:settings)
41
+ end
42
+
43
+ def assets
44
+ unless _options[:assets]
45
+ raise Hanami::ComponentLoadError, "hanami-assets gem is required to access assets"
46
+ end
47
+
48
+ _options[:assets]
49
+ end
50
+
51
+ def content_for(key, value = nil, &block)
52
+ content = _options[:content]
53
+ output = nil
54
+
55
+ if block
56
+ content[key] = yield
57
+ elsif value
58
+ content[key] = value
59
+ else
60
+ output = content[key]
61
+ end
62
+
63
+ output
64
+ end
65
+
66
+ def current_path
67
+ request.fullpath
68
+ end
69
+
70
+ def csrf_token
71
+ request.session[Hanami::Action::CSRFProtection::CSRF_TOKEN]
72
+ end
73
+
74
+ def request
75
+ _options.fetch(:request)
76
+ end
77
+
78
+ def session
79
+ request.session
80
+ end
81
+
82
+ def flash
83
+ response.flash
84
+ end
85
+
86
+ private
87
+
88
+ # TODO: create `Request#flash` so we no longer need the `response`
89
+ def response
90
+ _options.fetch(:response)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+
5
+ module Hanami
6
+ class Application
7
+ class View < Hanami::View
8
+ # Provides slice-specific configuration and behavior for any view context class
9
+ # defined within a slice's module namespace.
10
+ #
11
+ # @api private
12
+ # @since 2.0.0
13
+ class SliceConfiguredContext < Module
14
+ attr_reader :slice
15
+
16
+ def initialize(slice)
17
+ super()
18
+ @slice = slice
19
+ end
20
+
21
+ def extended(_context_class)
22
+ define_new
23
+ end
24
+
25
+ def inspect
26
+ "#<#{self.class.name}[#{slice.name}]>"
27
+ end
28
+
29
+ private
30
+
31
+ # Defines a {.new} method on the context class that resolves key components from
32
+ # the application container and provides them to {#initialize} as injected
33
+ # dependencies.
34
+ #
35
+ # This includes the following application components:
36
+ #
37
+ # - the configured inflector as `inflector`
38
+ # - "settings" from the application container as `settings`
39
+ # - "routes" from the application container as `routes`
40
+ # - "assets" from the application container as `assets`
41
+ def define_new
42
+ inflector = slice.inflector
43
+ resolve_settings = method(:resolve_settings)
44
+ resolve_routes = method(:resolve_routes)
45
+ resolve_assets = method(:resolve_assets)
46
+
47
+ define_method :new do |**kwargs|
48
+ kwargs[:inflector] ||= inflector
49
+ kwargs[:settings] ||= resolve_settings.()
50
+ kwargs[:routes] ||= resolve_routes.()
51
+ kwargs[:assets] ||= resolve_assets.()
52
+
53
+ super(**kwargs)
54
+ end
55
+ end
56
+
57
+ def resolve_settings
58
+ slice.application[:settings] if slice.application.key?(:settings)
59
+ end
60
+
61
+ def resolve_routes
62
+ slice.application[:routes_helper] if slice.application.key?(:routes_helper)
63
+ end
64
+
65
+ def resolve_assets
66
+ slice.application[:assets] if slice.application.key?(:assets)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+
5
+ module Hanami
6
+ class Application
7
+ class View < Hanami::View
8
+ # Provides slice-specific configuration and behavior for any view class defined
9
+ # within a slice's module namespace.
10
+ #
11
+ # @api private
12
+ # @since 2.0.0
13
+ class SliceConfiguredView < Module
14
+ attr_reader :slice
15
+
16
+ def initialize(slice)
17
+ super()
18
+ @slice = slice
19
+ end
20
+
21
+ def extended(view_class)
22
+ configure_view(view_class)
23
+ define_inherited
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class.name}[#{slice.name}]>"
28
+ end
29
+
30
+ private
31
+
32
+ # rubocop:disable Metrics/AbcSize
33
+ def configure_view(view_class)
34
+ view_class.settings.each do |setting|
35
+ if slice.application.config.views.respond_to?(:"#{setting}")
36
+ view_class.config.public_send(
37
+ :"#{setting}=",
38
+ slice.application.config.views.public_send(:"#{setting}")
39
+ )
40
+ end
41
+ end
42
+
43
+ view_class.config.inflector = inflector
44
+ view_class.config.paths = prepare_paths(slice, view_class.config.paths)
45
+ view_class.config.template = template_name(view_class)
46
+
47
+ if (part_namespace = namespace_from_path(slice.application.config.views.parts_path))
48
+ view_class.config.part_namespace = part_namespace
49
+ end
50
+ end
51
+ # rubocop:enable Metrics/AbcSize
52
+
53
+ def define_inherited
54
+ template_name = method(:template_name)
55
+
56
+ define_method(:inherited) do |subclass|
57
+ super(subclass)
58
+ subclass.config.template = template_name.(subclass)
59
+ end
60
+ end
61
+
62
+ def prepare_paths(slice, configured_paths)
63
+ configured_paths.map { |path|
64
+ if path.dir.relative?
65
+ slice.root.join(path.dir)
66
+ else
67
+ path
68
+ end
69
+ }
70
+ end
71
+
72
+ def namespace_from_path(path)
73
+ path = "#{slice.slice_name.path}/#{path}"
74
+
75
+ begin
76
+ require path
77
+ rescue LoadError => exception
78
+ raise exception unless exception.path == path
79
+ end
80
+
81
+ begin
82
+ inflector.constantize(inflector.camelize(path))
83
+ rescue NameError => exception
84
+ end
85
+ end
86
+
87
+ def template_name(view_class)
88
+ slice
89
+ .inflector
90
+ .underscore(view_class.name)
91
+ .sub(/^#{slice.slice_name.path}\//, "")
92
+ .sub(/^#{view_class.config.template_inference_base}\//, "")
93
+ end
94
+
95
+ def inflector
96
+ slice.inflector
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/view"
4
+ require_relative "../slice_configurable"
5
+ require_relative "view/slice_configured_view"
6
+
7
+ module Hanami
8
+ class Application
9
+ # Superclass for views intended for use within an Hanami application.
10
+ #
11
+ # @see Hanami::View
12
+ #
13
+ # @api public
14
+ # @since 2.0.0
15
+ class View < Hanami::View
16
+ extend Hanami::SliceConfigurable
17
+
18
+ # @api private
19
+ def self.configure_for_slice(slice)
20
+ extend SliceConfiguredView.new(slice)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../constants"
4
+
5
+ module Hanami
6
+ class Application
7
+ # Infers a view name for automatically rendering within actions.
8
+ #
9
+ # @api private
10
+ # @since 2.0.0
11
+ class ViewNameInferrer
12
+ ALTERNATIVE_NAMES = {
13
+ "create" => "new",
14
+ "update" => "edit"
15
+ }.freeze
16
+
17
+ class << self
18
+ # Returns an array of container keys for views matching the given action.
19
+ #
20
+ # Also provides alternative view keys for common RESTful actions.
21
+ #
22
+ # @example
23
+ # ViewNameInferrer.call(action_name: "Main::Actions::Posts::Create", slice: Main::Slice)
24
+ # # => ["views.posts.create", "views.posts.new"]
25
+ #
26
+ # @param action_name [String] action class name
27
+ # @param slice [Hanami::Slice, Hanami::Application] Hanami slice containing the action
28
+ #
29
+ # @return [Array<string>] array of paired view container keys
30
+ def call(action_class_name:, slice:)
31
+ action_key_base = slice.application.config.actions.name_inference_base
32
+ view_key_base = slice.application.config.actions.view_name_inference_base
33
+
34
+ action_name_key = action_name_key(action_class_name, slice, action_key_base)
35
+
36
+ view_key = [view_key_base, action_name_key].compact.join(CONTAINER_KEY_DELIMITER)
37
+
38
+ [view_key, alternative_view_key(view_key)].compact
39
+ end
40
+
41
+ private
42
+
43
+ def action_name_key(action_name, slice, key_base)
44
+ slice
45
+ .inflector
46
+ .underscore(action_name)
47
+ .sub(%r{^#{slice.slice_name.path}#{PATH_DELIMITER}}, "")
48
+ .sub(%r{^#{key_base}#{PATH_DELIMITER}}, "")
49
+ .gsub("/", CONTAINER_KEY_DELIMITER)
50
+ end
51
+
52
+ def alternative_view_key(view_key)
53
+ parts = view_key.split(CONTAINER_KEY_DELIMITER)
54
+
55
+ alternative_name = ALTERNATIVE_NAMES[parts.last]
56
+ return unless alternative_name
57
+
58
+ [parts[0..-2], alternative_name].join(CONTAINER_KEY_DELIMITER)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -7,6 +7,7 @@ require "rack"
7
7
  require "zeitwerk"
8
8
  require_relative "constants"
9
9
  require_relative "slice"
10
+ require_relative "slice_name"
10
11
  require_relative "application/slice_registrar"
11
12
 
12
13
  module Hanami
@@ -17,21 +18,25 @@ module Hanami
17
18
  @_mutex = Mutex.new
18
19
 
19
20
  class << self
20
- def inherited(klass)
21
+ def inherited(subclass)
21
22
  super
23
+
22
24
  @_mutex.synchronize do
23
- klass.class_eval do
25
+ subclass.class_eval do
24
26
  @_mutex = Mutex.new
25
- @_configuration = Hanami::Configuration.new(application_name: name, env: Hanami.env)
27
+ @application_name = SliceName.new(subclass, inflector: -> { subclass.inflector })
28
+ @configuration = Hanami::Configuration.new(application_name: @application_name, env: Hanami.env)
26
29
  @autoloader = Zeitwerk::Loader.new
27
30
  @container = Class.new(Dry::System::Container)
28
31
 
32
+ @prepared = @booted = false
33
+
29
34
  extend ClassMethods
30
35
  end
31
36
 
32
- klass.send :prepare_base_load_path
37
+ subclass.send :prepare_base_load_path
33
38
 
34
- Hanami.application = klass
39
+ Hanami.application = subclass
35
40
  end
36
41
  end
37
42
  end
@@ -40,20 +45,16 @@ module Hanami
40
45
  #
41
46
  # rubocop:disable Metrics/ModuleLength
42
47
  module ClassMethods
43
- attr_reader :autoloader, :container
44
-
45
- def self.extended(klass)
46
- klass.class_eval do
47
- @prepared = @booted = false
48
- end
49
- end
48
+ attr_reader :application_name, :configuration, :autoloader, :container
50
49
 
51
- def configuration
52
- @_configuration
53
- end
50
+ alias_method :slice_name, :application_name
54
51
 
55
52
  alias_method :config, :configuration
56
53
 
54
+ def application
55
+ self
56
+ end
57
+
57
58
  def prepare(provider_name = nil)
58
59
  container.prepare(provider_name) and return self if provider_name
59
60
 
@@ -147,19 +148,7 @@ module Hanami
147
148
  end
148
149
 
149
150
  def namespace
150
- configuration.namespace
151
- end
152
-
153
- def namespace_name
154
- namespace.name
155
- end
156
-
157
- def namespace_path
158
- inflector.underscore(namespace)
159
- end
160
-
161
- def application_name
162
- configuration.application_name
151
+ application_name.namespace
163
152
  end
164
153
 
165
154
  def root
@@ -170,21 +159,6 @@ module Hanami
170
159
  configuration.inflector
171
160
  end
172
161
 
173
- # @api private
174
- def component_provider(component)
175
- raise "Hanami.application must be prepared before detecting providers" unless prepared?
176
-
177
- # e.g. [Admin, Main, MyApp]
178
- providers = slices.to_a + [self]
179
-
180
- component_class = component.is_a?(Class) ? component : component.class
181
- component_name = component_class.name
182
-
183
- return unless component_name
184
-
185
- providers.detect { |provider| component_name.include?(provider.namespace.to_s) }
186
- end
187
-
188
162
  private
189
163
 
190
164
  def prepare_base_load_path
@@ -222,8 +196,8 @@ module Hanami
222
196
 
223
197
  def prepare_autoload_paths
224
198
  # Autoload classes defined in lib/[app_namespace]/
225
- if root.join("lib", namespace_path).directory?
226
- autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
199
+ if root.join("lib", application_name.name).directory?
200
+ autoloader.push_dir(root.join("lib", application_name.name), namespace: namespace)
227
201
  end
228
202
  end
229
203
 
@@ -239,8 +213,8 @@ module Hanami
239
213
 
240
214
  def prepare_autoloader
241
215
  # Autoload classes defined in lib/[app_namespace]/
242
- if root.join("lib", namespace_path).directory?
243
- autoloader.push_dir(root.join("lib", namespace_path), namespace: namespace)
216
+ if root.join("lib", application_name.name).directory?
217
+ autoloader.push_dir(root.join("lib", application_name.name), namespace: namespace)
244
218
  end
245
219
 
246
220
  autoloader.setup
@@ -258,7 +232,7 @@ module Hanami
258
232
  end
259
233
 
260
234
  def autodiscover_application_constant(constants)
261
- inflector.constantize([namespace_name, *constants].join(MODULE_DELIMITER))
235
+ inflector.constantize([application_name.namespace_name, *constants].join(MODULE_DELIMITER))
262
236
  end
263
237
 
264
238
  def load_router