hanami 2.0.0.alpha7.1 → 2.0.0.alpha8

Sign up to get free protection for your applications and to get access to all the features.
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