From ffecd9ea2a772089a52fa9fe26b2169ff37d103a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Busqu=C3=A9?= Date: Tue, 24 Oct 2023 12:31:50 +0200 Subject: [PATCH] Update hanami-view extension to work with the 2.1 beta `Hanami::View::Context` no longer supports `#with`, as it's encouraged for a single instance to be used for the whole request [1]. We use the same approach as hanami [2], where users can configure a default context class and it's expected to be initialized with keyword arguments. In our case, the default context class is a noop and we expect users to implement it. It can be configured in the `view_context_class` setting, while the initialize options can be created from the connection struct through a lambda configured in the `view_context_options` setting. [1] - https://github.com/hanami/view/pull/223 [2] - https://github.com/hanami/hanami/pull/1359 --- Gemfile | 2 +- docs/extensions/hanami_view.md | 33 ++++--------- .../extensions/hanami_view/hanami_view.rb | 46 ++++++++++--------- .../hanami_view/hanami_view/context.rb | 14 ++++++ .../hanami_view/hanami_view_spec.rb | 24 +++++----- 5 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 lib/web_pipe/extensions/hanami_view/hanami_view/context.rb diff --git a/Gemfile b/Gemfile index d7a7328..fba6b0e 100644 --- a/Gemfile +++ b/Gemfile @@ -9,5 +9,5 @@ gemspec group :development do # TODO: Move to gemspec when hanami-view 2.0 is available - gem 'hanami-view', github: 'hanami/view', tag: 'v2.0.0.alpha2' + gem 'hanami-view', github: 'hanami/view', tag: 'v2.1.0.beta2' end diff --git a/docs/extensions/hanami_view.md b/docs/extensions/hanami_view.md index c828bdf..4fa4818 100644 --- a/docs/extensions/hanami_view.md +++ b/docs/extensions/hanami_view.md @@ -1,6 +1,6 @@ # Hanami View -This extension currently works with `hanami-view` v2.0.0.alpha2, which is not +This extension currently works with `hanami-view` v2.1.0.beta, which is not still released but available on the gem repository. This extension integrates with [hanami-view](https://github.com/hanami/view) @@ -20,7 +20,6 @@ WebPipe.load_extensions(:hanami_view) class SayHelloView < Hanami::View config.paths = [File.join(__dir__, '..', 'templates')] config.template = 'say_hello' - config.default_context = MyContext expose :name end @@ -62,34 +61,22 @@ class MyApp end ``` -As in a standard call to `Hanami::View#call`, you can override the context -(`Hanami::View::Context`) to use through the `context:` option. However, it is still possible to leverage the configured default context while injecting specific data to it. - -To work, you have to specify required dependencies (in this case, -request specific data) to your hanami-view's context. A very convenient way to do that is with [`dry-auto_inject`](https://dry-rb.org/gems/dry-auto_inject): +You can configure the view context class to use through the `:view_context_class` configuration option. The only requirement for it is to implement an initialize method accepting keyword arguments: ```ruby -require 'hanami/view/context' +require 'hanami/view' require 'my_import' class MyContext < Hanami::View::Context - include MyImport::Import[:current_path] - - # Without `dry-auto_inject` you have to manually specify dependencies and - # override the initializer: - # - # attr_reader :current_path - # - # def initialize(current_path:, **options) - # @current_path = current_path - # super - # end + def initialize(current_path:) + @current_path = current_path + end end ``` -Then, you have to configure a `:view_context` setting, which must be a lambda -accepting a `WebPipe::Conn` instance and returning a hash matching required -dependencies: +Then, you also need to configure a `:view_context_options` setting, which must be a lambda +accepting a `WebPipe::Conn` instance and returning a hash matching required arguments for +the view context class: ```ruby require 'web_pipe' @@ -101,11 +88,11 @@ class MyApp include WebPipe plug :config, WebPipe::Plugs::Config.( + view_context_class: MyContext, view_context: ->(conn) { { current_path: conn.full_path} } ) plug(:render) do |conn| conn.view(SayHelloView.new, name: 'Joe') - # `:current_path` will be provided to the context end end ``` diff --git a/lib/web_pipe/extensions/hanami_view/hanami_view.rb b/lib/web_pipe/extensions/hanami_view/hanami_view.rb index 7ea2144..9ba89cc 100644 --- a/lib/web_pipe/extensions/hanami_view/hanami_view.rb +++ b/lib/web_pipe/extensions/hanami_view/hanami_view.rb @@ -2,17 +2,24 @@ require 'web_pipe/types' require 'web_pipe/conn' +require 'web_pipe' +require 'web_pipe/extensions/hanami_view/hanami_view/context' require 'hanami/view' # :nodoc: module WebPipe - # See the docs for the extension linked from the README. - module DryView - # Where to find in {#config} request's view context generator. - VIEW_CONTEXT_KEY = :view_context + # See the docs for the extension linked in the README. + module HanamiView + VIEW_CONTEXT_CLASS_KEY = :view_context_class + private_constant :VIEW_CONTEXT_CLASS_KEY - # Default request's view context - DEFAULT_VIEW_CONTEXT = ->(_conn) { Types::EMPTY_HASH } + DEFAULT_VIEW_CONTEXT_CLASS = Class.new(WebPipe::HanamiView::Context) + private_constant :DEFAULT_VIEW_CONTEXT_CLASS + + VIEW_CONTEXT_OPTIONS_KEY = :view_context_options + private_constant :VIEW_CONTEXT_OPTIONS_KEY + + DEFAULT_VIEW_CONTEXT_OPTIONS = ->(_conn) { {} } # Sets string output of a view as response body. # @@ -20,10 +27,7 @@ module DryView # the configured container. # # `kwargs` is used as the input for the view (the arguments that - # {Hanami::View#call} receives). If they doesn't contain an explicit - # `context:` key, it can be added through the injection of the - # result of a lambda present in context's `:view_context`.(see - # {Hanami::View::Context#with}). + # {Hanami::View#call} receives). # # @param view_spec [Hanami::View, Any] # @param kwargs [Hash] Arguments to pass along to `Hanami::View#call` @@ -31,11 +35,10 @@ module DryView # @return WebPipe::Conn def view(view_spec, **kwargs) view_instance = view_instance(view_spec) - view_input = view_input(kwargs, view_instance) set_response_body( view_instance.call( - **view_input + **view_input(kwargs) ).to_str ) end @@ -48,20 +51,19 @@ def view_instance(view_spec) fetch_config(:container)[view_spec] end - def view_input(kwargs, view_instance) + def view_input(kwargs) return kwargs if kwargs.key?(:context) - context = view_instance - .config - .default_context - .with( - **fetch_config( - VIEW_CONTEXT_KEY, DEFAULT_VIEW_CONTEXT - ).call(self) - ) + context = fetch_config( + VIEW_CONTEXT_CLASS_KEY, DEFAULT_VIEW_CONTEXT_CLASS + ).new( + **fetch_config( + VIEW_CONTEXT_OPTIONS_KEY, DEFAULT_VIEW_CONTEXT_OPTIONS + ).call(self) + ) kwargs.merge(context: context) end end - Conn.include(DryView) + Conn.include(HanamiView) end diff --git a/lib/web_pipe/extensions/hanami_view/hanami_view/context.rb b/lib/web_pipe/extensions/hanami_view/hanami_view/context.rb new file mode 100644 index 0000000..acb1cfb --- /dev/null +++ b/lib/web_pipe/extensions/hanami_view/hanami_view/context.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'hanami/view' + +module WebPipe + module HanamiView + # Noop context class for Hanami::View used by default. + class Context < Hanami::View::Context + def initialize(**_kwargs) + super() + end + end + end +end diff --git a/spec/extensions/hanami_view/hanami_view_spec.rb b/spec/extensions/hanami_view/hanami_view_spec.rb index d08b086..73c2fc1 100644 --- a/spec/extensions/hanami_view/hanami_view_spec.rb +++ b/spec/extensions/hanami_view/hanami_view_spec.rb @@ -55,28 +55,29 @@ expect(new_conn.response_body).to eq(['Hello world']) end - it 'injects configured view_context to the context' do + it 'initializes view_context class with the generated view_context_options hash' do view = Class.new(view_class) do config.template = 'template_with_input' - config.default_context = Class.new(Hanami::View::Context) do - attr_reader :name - - def initialize(name: nil, **options) - @name = name - super - end - end.new + end + context_class = Class.new(Hanami::View::Context) do + attr_reader :name + + def initialize(name:) + super + @name = name + end end conn = build_conn(default_env) .add(:name, 'Joe') - .add_config(:view_context, ->(c) { { name: c.fetch(:name) } }) + .add_config(:view_context_class, context_class) + .add_config(:view_context_options, ->(c) { { name: c.fetch(:name) } }) new_conn = conn.view(view.new) expect(new_conn.response_body).to eq(['Hello Joe']) end - it 'does not inject configured view_context when it is explicit' do + it 'respects context given at render time if given' do view = Class.new(view_class) do config.template = 'template_with_input' end @@ -86,7 +87,6 @@ def name end end.new conn = build_conn(default_env) - .add_config(:view_context, ->(_conn) { { name: 'Joe' } }) new_conn = conn.view(view.new, context: context)