Remove support for Hanami application integrated views (#207)
These integrations have moved into the hanami gem itself.
This commit is contained in:
parent
ec90f452fa
commit
198f2daa5c
2
Gemfile
2
Gemfile
|
@ -19,7 +19,7 @@ group :test do
|
|||
gem "erubi"
|
||||
gem "hamlit"
|
||||
gem "hamlit-block"
|
||||
gem "dry-files", github: "dry-rb/dry-files", branch: "main"
|
||||
gem "dry-files", github: "dry-rb/dry-files"
|
||||
gem "hanami-cli", github: "hanami/cli", branch: "main"
|
||||
gem "hanami", github: "hanami/hanami", branch: "main"
|
||||
gem "hanami-controller", github: "hanami/controller", branch: "main"
|
||||
|
|
|
@ -15,7 +15,6 @@ require_relative "view/render_environment"
|
|||
require_relative "view/rendered"
|
||||
require_relative "view/renderer"
|
||||
require_relative "view/scope_builder"
|
||||
require_relative "view/standalone_view"
|
||||
|
||||
module Hanami
|
||||
# A standalone, template-based view rendering system that offers everything
|
||||
|
@ -230,24 +229,388 @@ module Hanami
|
|||
|
||||
# @!endgroup
|
||||
|
||||
include StandaloneView
|
||||
|
||||
def self.inherited(subclass)
|
||||
# @api private
|
||||
def self.inherited(klass)
|
||||
super
|
||||
|
||||
# When inheriting within an Hanami app, and the application provider has
|
||||
# changed from the superclass, (re-)configure the action for the provider,
|
||||
# i.e. for the slice and/or the application itself
|
||||
if (provider = application_provider(subclass)) && provider != application_provider(subclass.superclass)
|
||||
subclass.include ApplicationView.new(provider)
|
||||
exposures.each do |name, exposure|
|
||||
klass.exposures.import(name, exposure)
|
||||
end
|
||||
end
|
||||
|
||||
def self.application_provider(subclass)
|
||||
if Hanami.respond_to?(:application?) && Hanami.application?
|
||||
Hanami.application.component_provider(subclass)
|
||||
# @!group Exposures
|
||||
|
||||
# @!macro [new] exposure_options
|
||||
# @param options [Hash] the exposure's options
|
||||
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
||||
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
||||
# true)
|
||||
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
||||
# matching Part
|
||||
|
||||
# @overload expose(name, **options, &block)
|
||||
# Define a value to be passed to the template. The return value of the
|
||||
# block will be decorated by a matching Part and passed to the template.
|
||||
#
|
||||
# The block will be evaluated with the view instance as its `self`. The
|
||||
# block's parameters will determine what it is given:
|
||||
#
|
||||
# - To receive other exposure values, provide positional parameters
|
||||
# matching the exposure names. These exposures will already by decorated
|
||||
# by their Parts.
|
||||
# - To receive the view's input arguments (whatever is passed to
|
||||
# `View#call`), provide matching keyword parameters. You can provide
|
||||
# default values for these parameters to make the corresponding input
|
||||
# keys optional
|
||||
# - To receive the Context object, provide a `context:` keyword parameter
|
||||
# - To receive the view's input arguments in their entirety, provide a
|
||||
# keywords splat parameter (i.e. `**input`)
|
||||
#
|
||||
# @example Accessing input arguments
|
||||
# expose :article do |slug:|
|
||||
# article_repo.find_by_slug(slug)
|
||||
# end
|
||||
#
|
||||
# @example Accessing other exposures
|
||||
# expose :articles do
|
||||
# article_repo.listing
|
||||
# end
|
||||
#
|
||||
# expose :featured_articles do |articles|
|
||||
# articles.select(&:featured?)
|
||||
# end
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
#
|
||||
# @overload expose(name, **options)
|
||||
# Define a value to be passed to the template, provided by an instance
|
||||
# method matching the name. The method's return value will be decorated by
|
||||
# a matching Part and passed to the template.
|
||||
#
|
||||
# The method's parameters will determine what it is given:
|
||||
#
|
||||
# - To receive other exposure values, provide positional parameters
|
||||
# matching the exposure names. These exposures will already by decorated
|
||||
# by their Parts.
|
||||
# - To receive the view's input arguments (whatever is passed to
|
||||
# `View#call`), provide matching keyword parameters. You can provide
|
||||
# default values for these parameters to make the corresponding input
|
||||
# keys optional
|
||||
# - To receive the Context object, provide a `context:` keyword parameter
|
||||
# - To receive the view's input arguments in their entirey, provide a
|
||||
# keywords splat parameter (i.e. `**input`)
|
||||
#
|
||||
# @example Accessing input arguments
|
||||
# expose :article
|
||||
#
|
||||
# def article(slug:)
|
||||
# article_repo.find_by_slug(slug)
|
||||
# end
|
||||
#
|
||||
# @example Accessing other exposures
|
||||
# expose :articles
|
||||
# expose :featured_articles
|
||||
#
|
||||
# def articles
|
||||
# article_repo.listing
|
||||
# end
|
||||
#
|
||||
# def featured_articles
|
||||
# articles.select(&:featured?)
|
||||
# end
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
#
|
||||
# @overload expose(name, **options)
|
||||
# Define a single value to pass through from the input data (when there is
|
||||
# no instance method matching the `name`). This value will be decorated by
|
||||
# a matching Part and passed to the template.
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
# @option options [Boolean] :default a default value to provide if there is no matching
|
||||
# input data
|
||||
#
|
||||
# @overload expose(*names, **options)
|
||||
# Define multiple values to pass through from the input data (when there
|
||||
# is no instance methods matching their names). These values will be
|
||||
# decorated by matching Parts and passed through to the template.
|
||||
#
|
||||
# The provided options will be applied to all the exposures.
|
||||
#
|
||||
# @param names [Symbol] names for the exposures
|
||||
# @macro exposure_options
|
||||
# @option options [Boolean] :default a default value to provide if there is no matching
|
||||
# input data
|
||||
#
|
||||
# @see https://dry-rb.org/gems/dry-view/exposures/
|
||||
#
|
||||
# @api public
|
||||
def self.expose(*names, **options, &block)
|
||||
if names.length == 1
|
||||
exposures.add(names.first, block, **options)
|
||||
else
|
||||
names.each do |name|
|
||||
exposures.add(name, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
private_class_method :application_provider
|
||||
|
||||
# @api public
|
||||
def self.private_expose(*names, **options, &block)
|
||||
expose(*names, **options, private: true, &block)
|
||||
end
|
||||
|
||||
# Returns the defined exposures. These are unbound, since bound exposures
|
||||
# are only created when initializing a View instance.
|
||||
#
|
||||
# @return [Exposures]
|
||||
# @api private
|
||||
def self.exposures
|
||||
@exposures ||= Exposures.new
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
|
||||
# @!group Scope
|
||||
|
||||
# Creates and assigns a scope for the current view.
|
||||
#
|
||||
# The newly created scope is useful to add custom logic that is specific
|
||||
# to the view.
|
||||
#
|
||||
# The scope has access to locals, exposures, and inherited scope (if any)
|
||||
#
|
||||
# If the view already has an explicit scope the newly created scope will
|
||||
# inherit from the explicit scope.
|
||||
#
|
||||
# There are two cases when this may happen:
|
||||
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
||||
# 2. The scope has been inherited by the view superclass
|
||||
#
|
||||
# If the view doesn't have an already existing scope, the newly scope
|
||||
# will inherit from `Hanami::View::Scope` by default.
|
||||
#
|
||||
# However, you can specify any base class for it. This is not
|
||||
# recommended, unless you know what you're doing.
|
||||
#
|
||||
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
||||
# default base class will be `Hanami::View::Scope`
|
||||
# @param block [Proc] the scope logic definition
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Basic usage
|
||||
# class MyView < Hanami::View
|
||||
# config.scope = MyScope
|
||||
#
|
||||
# scope do
|
||||
# def greeting
|
||||
# _locals[:message].upcase + "!"
|
||||
# end
|
||||
#
|
||||
# def copyright(time)
|
||||
# "Copy #{time.year}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # my_view.html.erb
|
||||
# # <%= greeting %>
|
||||
# # <%= copyright(Time.now.utc) %>
|
||||
#
|
||||
# MyView.new.(message: "Hello") # => "HELLO!"
|
||||
#
|
||||
# @example Inherited scope
|
||||
# class MyScope < Hanami::View::Scope
|
||||
# private
|
||||
#
|
||||
# def shout(string)
|
||||
# string.upcase + "!"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class MyView < Hanami::View
|
||||
# config.scope = MyScope
|
||||
#
|
||||
# scope do
|
||||
# def greeting
|
||||
# shout(_locals[:message])
|
||||
# end
|
||||
#
|
||||
# def copyright(time)
|
||||
# "Copy #{time.year}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # my_view.html.erb
|
||||
# # <%= greeting %>
|
||||
# # <%= copyright(Time.now.utc) %>
|
||||
#
|
||||
# MyView.new.(message: "Hello") # => "HELLO!"
|
||||
def self.scope(base: config.scope || Hanami::View::Scope, &block)
|
||||
config.scope = Class.new(base, &block)
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
|
||||
# @!group Render environment
|
||||
|
||||
# Returns a render environment for the view and the given options. This
|
||||
# environment isn't chdir'ed into any particular directory.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @see View.template_env render environment for the view's template
|
||||
# @see View.layout_env render environment for the view's layout
|
||||
#
|
||||
# @return [RenderEnvironment]
|
||||
# @api public
|
||||
def self.render_env(format: config.default_format, context: config.default_context)
|
||||
RenderEnvironment.prepare(renderer(format), config, context)
|
||||
end
|
||||
|
||||
# @overload template_env(format: config.default_format, context: config.default_context)
|
||||
# Returns a render environment for the view and the given options,
|
||||
# chdir'ed into the view's template directory. This is the environment
|
||||
# used when rendering the template, and is useful to to fetch
|
||||
# independently when unit testing Parts and Scopes.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @return [RenderEnvironment]
|
||||
# @api public
|
||||
def self.template_env(**args)
|
||||
render_env(**args).chdir(config.template)
|
||||
end
|
||||
|
||||
# @overload layout_env(format: config.default_format, context: config.default_context)
|
||||
# Returns a render environment for the view and the given options,
|
||||
# chdir'ed into the view's layout directory. This is the environment used
|
||||
# when rendering the view's layout.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @return [RenderEnvironment] @api public
|
||||
def self.layout_env(**args)
|
||||
render_env(**args).chdir(layout_path)
|
||||
end
|
||||
|
||||
# Returns renderer for the view and provided format
|
||||
#
|
||||
# @api private
|
||||
def self.renderer(format)
|
||||
fetch_or_store(:renderer, config, format) {
|
||||
Renderer.new(
|
||||
config.paths,
|
||||
format: format,
|
||||
engine_mapping: config.renderer_engine_mapping,
|
||||
**config.renderer_options
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.layout_path
|
||||
File.join(*[config.layouts_dir, config.layout].compact)
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
|
||||
# Returns an instance of the view. This binds the defined exposures to the
|
||||
# view instance.
|
||||
#
|
||||
# Subclasses can define their own `#initialize` to accept injected
|
||||
# dependencies, but must call `super()` to ensure the standard view
|
||||
# initialization can proceed.
|
||||
#
|
||||
# @api public
|
||||
def initialize
|
||||
@exposures = self.class.exposures.bind(self)
|
||||
end
|
||||
|
||||
# The view's configuration
|
||||
#
|
||||
# @api private
|
||||
def config
|
||||
self.class.config
|
||||
end
|
||||
|
||||
# The view's bound exposures
|
||||
#
|
||||
# @return [Exposures]
|
||||
# @api private
|
||||
def exposures
|
||||
@exposures
|
||||
end
|
||||
|
||||
# Render the view
|
||||
#
|
||||
# @param format [Symbol] template format to use
|
||||
# @param context [Context] context object to use
|
||||
# @param input input data for preparing exposure values
|
||||
#
|
||||
# @return [Rendered] rendered view object
|
||||
# @api public
|
||||
def call(format: config.default_format, context: config.default_context, **input)
|
||||
ensure_config
|
||||
|
||||
env = self.class.render_env(format: format, context: context)
|
||||
template_env = self.class.template_env(format: format, context: context)
|
||||
|
||||
locals = locals(template_env, input)
|
||||
output = env.template(config.template, template_env.scope(config.scope, locals))
|
||||
|
||||
if layout?
|
||||
layout_env = self.class.layout_env(format: format, context: context)
|
||||
begin
|
||||
output = env.template(
|
||||
self.class.layout_path,
|
||||
layout_env.scope(config.scope, layout_locals(locals))
|
||||
) { output }
|
||||
rescue TemplateNotFoundError
|
||||
raise LayoutNotFoundError.new(config.layout, config.paths)
|
||||
end
|
||||
end
|
||||
|
||||
Rendered.new(output: output, locals: locals)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @api private
|
||||
def ensure_config
|
||||
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
||||
raise UndefinedConfigError, :template unless config.template
|
||||
end
|
||||
|
||||
# @api private
|
||||
def locals(render_env, input)
|
||||
exposures.(context: render_env.context, **input) do |value, exposure|
|
||||
if exposure.decorate? && value
|
||||
render_env.part(exposure.name, value, **exposure.options)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def layout_locals(locals)
|
||||
locals.each_with_object({}) do |(key, value), layout_locals|
|
||||
layout_locals[key] = value if exposures[key].for_layout?
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def layout?
|
||||
!!config.layout # rubocop:disable Style/DoubleNegation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/configurable"
|
||||
require_relative "../view"
|
||||
|
||||
module Hanami
|
||||
class View
|
||||
class ApplicationConfiguration
|
||||
include Dry::Configurable
|
||||
|
||||
setting :parts_path, default: "view/parts"
|
||||
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
@base_config = View.config.dup
|
||||
|
||||
configure_defaults
|
||||
end
|
||||
|
||||
# Returns the list of available settings
|
||||
#
|
||||
# @return [Set]
|
||||
#
|
||||
# @since 2.0.0
|
||||
# @api private
|
||||
def settings
|
||||
self.class.settings + View.settings - NON_FORWARDABLE_METHODS
|
||||
end
|
||||
|
||||
def finalize!
|
||||
return self if frozen?
|
||||
|
||||
base_config.finalize!
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_config
|
||||
|
||||
def configure_defaults
|
||||
self.paths = ["templates"]
|
||||
self.template_inference_base = "views"
|
||||
self.layout = "application"
|
||||
end
|
||||
|
||||
# An inflector for views is not configurable via `config.views.inflector` on an
|
||||
# `Hanami::Application`. The application-wide inflector is already configurable
|
||||
# there as `config.inflector` and will be used as the default inflector for views.
|
||||
#
|
||||
# A custom inflector may still be provided in an `Hanami::View` subclass, via
|
||||
# `config.inflector=`.
|
||||
NON_FORWARDABLE_METHODS = [:inflector, :inflector=].freeze
|
||||
private_constant :NON_FORWARDABLE_METHODS
|
||||
|
||||
def method_missing(name, *args, &block)
|
||||
return super if NON_FORWARDABLE_METHODS.include?(name)
|
||||
|
||||
if config.respond_to?(name)
|
||||
config.public_send(name, *args, &block)
|
||||
elsif base_config.respond_to?(name)
|
||||
base_config.public_send(name, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def respond_to_missing?(name, _include_all = false)
|
||||
return false if NON_FORWARDABLE_METHODS.include?(name)
|
||||
|
||||
config.respond_to?(name) || base_config.respond_to?(name) || super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,98 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami/view/errors"
|
||||
|
||||
module Hanami
|
||||
class View
|
||||
class ApplicationContext < Module
|
||||
attr_reader :provider
|
||||
attr_reader :application
|
||||
|
||||
def initialize(provider)
|
||||
@provider = provider
|
||||
@application = provider.respond_to?(:application) ? provider.application : Hanami.application
|
||||
end
|
||||
|
||||
def included(context_class)
|
||||
define_initialize
|
||||
context_class.include(InstanceMethods)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def define_initialize
|
||||
inflector = application.inflector
|
||||
settings = application[:settings] if application.key?(:settings)
|
||||
routes = application[:routes_helper] if application.key?(:routes_helper)
|
||||
assets = application[:assets] if application.key?(:assets)
|
||||
|
||||
define_method :initialize do |**options|
|
||||
@inflector = options[:inflector] || inflector
|
||||
@settings = options[:settings] || settings
|
||||
@routes = options[:routes] || routes
|
||||
@assets = options[:assets] || assets
|
||||
super(**options)
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
attr_reader :inflector
|
||||
attr_reader :routes
|
||||
attr_reader :settings
|
||||
|
||||
def initialize(**args)
|
||||
defaults = {content: {}}
|
||||
|
||||
super(**defaults.merge(args))
|
||||
end
|
||||
|
||||
def content_for(key, value = nil, &block)
|
||||
content = _options[:content]
|
||||
output = nil
|
||||
|
||||
if block
|
||||
content[key] = yield
|
||||
elsif value
|
||||
content[key] = value
|
||||
else
|
||||
output = content[key]
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def current_path
|
||||
request.fullpath
|
||||
end
|
||||
|
||||
def csrf_token
|
||||
request.session[Hanami::Action::CSRFProtection::CSRF_TOKEN]
|
||||
end
|
||||
|
||||
def request
|
||||
_options.fetch(:request)
|
||||
end
|
||||
|
||||
def session
|
||||
request.session
|
||||
end
|
||||
|
||||
def flash
|
||||
response.flash
|
||||
end
|
||||
|
||||
def assets
|
||||
@assets or
|
||||
raise Hanami::View::MissingProviderError.new("hanami-assets")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: create `Request#flash` so we no longer need the `response`
|
||||
def response
|
||||
_options.fetch(:response)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "dry/core/equalizer"
|
||||
require_relative "application_context"
|
||||
require_relative "decorated_attributes"
|
||||
|
||||
module Hanami
|
||||
|
@ -19,23 +18,6 @@ module Hanami
|
|||
|
||||
attr_reader :_render_env, :_options
|
||||
|
||||
def self.inherited(subclass)
|
||||
super
|
||||
|
||||
# When inheriting within an Hanami app, add application context behavior
|
||||
provider = application_provider(subclass)
|
||||
if provider
|
||||
subclass.include ApplicationContext.new(provider)
|
||||
end
|
||||
end
|
||||
|
||||
def self.application_provider(subclass)
|
||||
if Hanami.respond_to?(:application?) && Hanami.application?
|
||||
Hanami.application.component_provider(subclass)
|
||||
end
|
||||
end
|
||||
private_class_method :application_provider
|
||||
|
||||
# Returns a new instance of Context
|
||||
#
|
||||
# In subclasses, you should include an `**options` parameter and pass _all
|
||||
|
|
|
@ -45,13 +45,5 @@ module Hanami
|
|||
super(msg)
|
||||
end
|
||||
end
|
||||
|
||||
# @since 2.0.0
|
||||
# @api public
|
||||
class MissingProviderError < Error
|
||||
def initialize(provider)
|
||||
super("#{provider.inspect} is missing")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,400 +0,0 @@
|
|||
require_relative "scope"
|
||||
|
||||
module Hanami
|
||||
class View
|
||||
module StandaloneView
|
||||
def self.included(klass)
|
||||
klass.extend ClassMethods
|
||||
klass.include InstanceMethods
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# @api private
|
||||
def inherited(klass)
|
||||
super
|
||||
|
||||
exposures.each do |name, exposure|
|
||||
klass.exposures.import(name, exposure)
|
||||
end
|
||||
end
|
||||
|
||||
# @!group Exposures
|
||||
|
||||
# @!macro [new] exposure_options
|
||||
# @param options [Hash] the exposure's options
|
||||
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
||||
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
||||
# true)
|
||||
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
||||
# matching Part
|
||||
|
||||
# @overload expose(name, **options, &block)
|
||||
# Define a value to be passed to the template. The return value of the
|
||||
# block will be decorated by a matching Part and passed to the template.
|
||||
#
|
||||
# The block will be evaluated with the view instance as its `self`. The
|
||||
# block's parameters will determine what it is given:
|
||||
#
|
||||
# - To receive other exposure values, provide positional parameters
|
||||
# matching the exposure names. These exposures will already by decorated
|
||||
# by their Parts.
|
||||
# - To receive the view's input arguments (whatever is passed to
|
||||
# `View#call`), provide matching keyword parameters. You can provide
|
||||
# default values for these parameters to make the corresponding input
|
||||
# keys optional
|
||||
# - To receive the Context object, provide a `context:` keyword parameter
|
||||
# - To receive the view's input arguments in their entirety, provide a
|
||||
# keywords splat parameter (i.e. `**input`)
|
||||
#
|
||||
# @example Accessing input arguments
|
||||
# expose :article do |slug:|
|
||||
# article_repo.find_by_slug(slug)
|
||||
# end
|
||||
#
|
||||
# @example Accessing other exposures
|
||||
# expose :articles do
|
||||
# article_repo.listing
|
||||
# end
|
||||
#
|
||||
# expose :featured_articles do |articles|
|
||||
# articles.select(&:featured?)
|
||||
# end
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
#
|
||||
# @overload expose(name, **options)
|
||||
# Define a value to be passed to the template, provided by an instance
|
||||
# method matching the name. The method's return value will be decorated by
|
||||
# a matching Part and passed to the template.
|
||||
#
|
||||
# The method's parameters will determine what it is given:
|
||||
#
|
||||
# - To receive other exposure values, provide positional parameters
|
||||
# matching the exposure names. These exposures will already by decorated
|
||||
# by their Parts.
|
||||
# - To receive the view's input arguments (whatever is passed to
|
||||
# `View#call`), provide matching keyword parameters. You can provide
|
||||
# default values for these parameters to make the corresponding input
|
||||
# keys optional
|
||||
# - To receive the Context object, provide a `context:` keyword parameter
|
||||
# - To receive the view's input arguments in their entirey, provide a
|
||||
# keywords splat parameter (i.e. `**input`)
|
||||
#
|
||||
# @example Accessing input arguments
|
||||
# expose :article
|
||||
#
|
||||
# def article(slug:)
|
||||
# article_repo.find_by_slug(slug)
|
||||
# end
|
||||
#
|
||||
# @example Accessing other exposures
|
||||
# expose :articles
|
||||
# expose :featured_articles
|
||||
#
|
||||
# def articles
|
||||
# article_repo.listing
|
||||
# end
|
||||
#
|
||||
# def featured_articles
|
||||
# articles.select(&:featured?)
|
||||
# end
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
#
|
||||
# @overload expose(name, **options)
|
||||
# Define a single value to pass through from the input data (when there is
|
||||
# no instance method matching the `name`). This value will be decorated by
|
||||
# a matching Part and passed to the template.
|
||||
#
|
||||
# @param name [Symbol] name for the exposure
|
||||
# @macro exposure_options
|
||||
# @option options [Boolean] :default a default value to provide if there is no matching
|
||||
# input data
|
||||
#
|
||||
# @overload expose(*names, **options)
|
||||
# Define multiple values to pass through from the input data (when there
|
||||
# is no instance methods matching their names). These values will be
|
||||
# decorated by matching Parts and passed through to the template.
|
||||
#
|
||||
# The provided options will be applied to all the exposures.
|
||||
#
|
||||
# @param names [Symbol] names for the exposures
|
||||
# @macro exposure_options
|
||||
# @option options [Boolean] :default a default value to provide if there is no matching
|
||||
# input data
|
||||
#
|
||||
# @see https://dry-rb.org/gems/dry-view/exposures/
|
||||
#
|
||||
# @api public
|
||||
def expose(*names, **options, &block)
|
||||
if names.length == 1
|
||||
exposures.add(names.first, block, **options)
|
||||
else
|
||||
names.each do |name|
|
||||
exposures.add(name, **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @api public
|
||||
def private_expose(*names, **options, &block)
|
||||
expose(*names, **options, private: true, &block)
|
||||
end
|
||||
|
||||
# Returns the defined exposures. These are unbound, since bound exposures
|
||||
# are only created when initializing a View instance.
|
||||
#
|
||||
# @return [Exposures]
|
||||
# @api private
|
||||
def exposures
|
||||
@exposures ||= Exposures.new
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
|
||||
# @!group Scope
|
||||
|
||||
# Creates and assigns a scope for the current view.
|
||||
#
|
||||
# The newly created scope is useful to add custom logic that is specific
|
||||
# to the view.
|
||||
#
|
||||
# The scope has access to locals, exposures, and inherited scope (if any)
|
||||
#
|
||||
# If the view already has an explicit scope the newly created scope will
|
||||
# inherit from the explicit scope.
|
||||
#
|
||||
# There are two cases when this may happen:
|
||||
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
||||
# 2. The scope has been inherited by the view superclass
|
||||
#
|
||||
# If the view doesn't have an already existing scope, the newly scope
|
||||
# will inherit from `Hanami::View::Scope` by default.
|
||||
#
|
||||
# However, you can specify any base class for it. This is not
|
||||
# recommended, unless you know what you're doing.
|
||||
#
|
||||
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
||||
# default base class will be `Hanami::View::Scope`
|
||||
# @param block [Proc] the scope logic definition
|
||||
#
|
||||
# @api public
|
||||
#
|
||||
# @example Basic usage
|
||||
# class MyView < Hanami::View
|
||||
# config.scope = MyScope
|
||||
#
|
||||
# scope do
|
||||
# def greeting
|
||||
# _locals[:message].upcase + "!"
|
||||
# end
|
||||
#
|
||||
# def copyright(time)
|
||||
# "Copy #{time.year}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # my_view.html.erb
|
||||
# # <%= greeting %>
|
||||
# # <%= copyright(Time.now.utc) %>
|
||||
#
|
||||
# MyView.new.(message: "Hello") # => "HELLO!"
|
||||
#
|
||||
# @example Inherited scope
|
||||
# class MyScope < Hanami::View::Scope
|
||||
# private
|
||||
#
|
||||
# def shout(string)
|
||||
# string.upcase + "!"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class MyView < Hanami::View
|
||||
# config.scope = MyScope
|
||||
#
|
||||
# scope do
|
||||
# def greeting
|
||||
# shout(_locals[:message])
|
||||
# end
|
||||
#
|
||||
# def copyright(time)
|
||||
# "Copy #{time.year}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # my_view.html.erb
|
||||
# # <%= greeting %>
|
||||
# # <%= copyright(Time.now.utc) %>
|
||||
#
|
||||
# MyView.new.(message: "Hello") # => "HELLO!"
|
||||
def scope(base: config.scope || Hanami::View::Scope, &block)
|
||||
config.scope = Class.new(base, &block)
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
|
||||
# @!group Render environment
|
||||
|
||||
# Returns a render environment for the view and the given options. This
|
||||
# environment isn't chdir'ed into any particular directory.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @see View.template_env render environment for the view's template
|
||||
# @see View.layout_env render environment for the view's layout
|
||||
#
|
||||
# @return [RenderEnvironment]
|
||||
# @api public
|
||||
def render_env(format: config.default_format, context: config.default_context)
|
||||
RenderEnvironment.prepare(renderer(format), config, context)
|
||||
end
|
||||
|
||||
# @overload template_env(format: config.default_format, context: config.default_context)
|
||||
# Returns a render environment for the view and the given options,
|
||||
# chdir'ed into the view's template directory. This is the environment
|
||||
# used when rendering the template, and is useful to to fetch
|
||||
# independently when unit testing Parts and Scopes.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @return [RenderEnvironment]
|
||||
# @api public
|
||||
def template_env(**args)
|
||||
render_env(**args).chdir(config.template)
|
||||
end
|
||||
|
||||
# @overload layout_env(format: config.default_format, context: config.default_context)
|
||||
# Returns a render environment for the view and the given options,
|
||||
# chdir'ed into the view's layout directory. This is the environment used
|
||||
# when rendering the view's layout.
|
||||
#
|
||||
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
||||
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
||||
#
|
||||
# @return [RenderEnvironment] @api public
|
||||
def layout_env(**args)
|
||||
render_env(**args).chdir(layout_path)
|
||||
end
|
||||
|
||||
# Returns renderer for the view and provided format
|
||||
#
|
||||
# @api private
|
||||
def renderer(format)
|
||||
fetch_or_store(:renderer, config, format) {
|
||||
Renderer.new(
|
||||
config.paths,
|
||||
format: format,
|
||||
engine_mapping: config.renderer_engine_mapping,
|
||||
**config.renderer_options
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
# @api private
|
||||
def layout_path
|
||||
File.join(*[config.layouts_dir, config.layout].compact)
|
||||
end
|
||||
|
||||
# @!endgroup
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
# Returns an instance of the view. This binds the defined exposures to the
|
||||
# view instance.
|
||||
#
|
||||
# Subclasses can define their own `#initialize` to accept injected
|
||||
# dependencies, but must call `super()` to ensure the standard view
|
||||
# initialization can proceed.
|
||||
#
|
||||
# @api public
|
||||
def initialize
|
||||
@exposures = self.class.exposures.bind(self)
|
||||
end
|
||||
|
||||
# The view's configuration
|
||||
#
|
||||
# @api private
|
||||
def config
|
||||
self.class.config
|
||||
end
|
||||
|
||||
# The view's bound exposures
|
||||
#
|
||||
# @return [Exposures]
|
||||
# @api private
|
||||
def exposures
|
||||
@exposures
|
||||
end
|
||||
|
||||
# Render the view
|
||||
#
|
||||
# @param format [Symbol] template format to use
|
||||
# @param context [Context] context object to use
|
||||
# @param input input data for preparing exposure values
|
||||
#
|
||||
# @return [Rendered] rendered view object
|
||||
# @api public
|
||||
def call(format: config.default_format, context: config.default_context, **input)
|
||||
ensure_config
|
||||
|
||||
env = self.class.render_env(format: format, context: context)
|
||||
template_env = self.class.template_env(format: format, context: context)
|
||||
|
||||
locals = locals(template_env, input)
|
||||
output = env.template(config.template, template_env.scope(config.scope, locals))
|
||||
|
||||
if layout?
|
||||
layout_env = self.class.layout_env(format: format, context: context)
|
||||
begin
|
||||
output = env.template(
|
||||
self.class.layout_path,
|
||||
layout_env.scope(config.scope, layout_locals(locals))
|
||||
) { output }
|
||||
rescue TemplateNotFoundError
|
||||
raise LayoutNotFoundError.new(config.layout, config.paths)
|
||||
end
|
||||
end
|
||||
|
||||
Rendered.new(output: output, locals: locals)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @api private
|
||||
def ensure_config
|
||||
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
||||
raise UndefinedConfigError, :template unless config.template
|
||||
end
|
||||
|
||||
# @api private
|
||||
def locals(render_env, input)
|
||||
exposures.(context: render_env.context, **input) do |value, exposure|
|
||||
if exposure.decorate? && value
|
||||
render_env.part(exposure.name, value, **exposure.options)
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def layout_locals(locals)
|
||||
locals.each_with_object({}) do |(key, value), layout_locals|
|
||||
layout_locals[key] = value if exposures[key].for_layout?
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def layout?
|
||||
!!config.layout # rubocop:disable Style/DoubleNegation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,70 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
require "hanami/view"
|
||||
|
||||
RSpec.describe "Application view / Inflector", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.application.instance_eval(&application_class_config)
|
||||
Hanami.application.register_slice :main
|
||||
Hanami.application.prepare
|
||||
|
||||
module TestApp
|
||||
module View
|
||||
class Base < Hanami::View
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:application_class_config) { proc {} }
|
||||
|
||||
subject(:view_class) {
|
||||
module Main
|
||||
module View
|
||||
class Base < TestApp::View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::View::Base
|
||||
}
|
||||
|
||||
context "no application inflector configured" do
|
||||
it "configures the view with the default application inflector" do
|
||||
expect(view_class.config.inflector).to be TestApp::Application.config.inflector
|
||||
end
|
||||
end
|
||||
|
||||
context "custom inflections configured" do
|
||||
let(:application_class_config) {
|
||||
proc do
|
||||
config.inflections do |inflections|
|
||||
inflections.acronym "NBA"
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
it "configures the view with the customized application inflector" do
|
||||
expect(view_class.config.inflector).to be TestApp::Application.config.inflector
|
||||
expect(view_class.config.inflector.camelize("nba_jam")).to eq "NBAJam"
|
||||
end
|
||||
end
|
||||
|
||||
context "custom inflector configured on view class" do
|
||||
let(:custom_inflector) { Object.new }
|
||||
|
||||
before do
|
||||
view_class.config.inflector = custom_inflector
|
||||
end
|
||||
|
||||
it "overrides the default application inflector" do
|
||||
expect(view_class.config.inflector).to be custom_inflector
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,172 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
require "hanami/view"
|
||||
|
||||
RSpec.describe "Application view / Part namespace", :application_integration do
|
||||
subject(:part_namespace) { view_class.config.part_namespace }
|
||||
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.application.instance_eval(&application_hook) if respond_to?(:application_hook)
|
||||
Hanami.application.register_slice :main
|
||||
Hanami.application.prepare
|
||||
end
|
||||
|
||||
context "view in slice" do
|
||||
let(:view_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Base < Hanami::View; end
|
||||
end
|
||||
end
|
||||
|
||||
module Main
|
||||
module View
|
||||
class Base < TestApp::View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::View::Base
|
||||
}
|
||||
|
||||
context "parts_path configured" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.parts_path = "view/custom_parts"
|
||||
end
|
||||
}
|
||||
|
||||
context "namespace exists" do
|
||||
before do
|
||||
module Main
|
||||
module View
|
||||
module CustomParts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is the matching module within the slice" do
|
||||
is_expected.to eq Main::View::CustomParts
|
||||
end
|
||||
end
|
||||
|
||||
context "namespace exists, but needs requiring" do
|
||||
before do
|
||||
allow_any_instance_of(Object).to receive(:require).and_call_original
|
||||
allow_any_instance_of(Object).to receive(:require).with("main/view/custom_parts") {
|
||||
module Main
|
||||
module View
|
||||
module CustomParts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
}
|
||||
end
|
||||
|
||||
it "is the matching module within the slice" do
|
||||
is_expected.to eq Main::View::CustomParts
|
||||
end
|
||||
end
|
||||
|
||||
context "namespace does not exist" do
|
||||
it "is nil" do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "nil parts_path configured" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.parts_path = nil
|
||||
end
|
||||
}
|
||||
|
||||
it "is nil" do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "view in application" do
|
||||
let(:view_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Base < Hanami::View; end
|
||||
end
|
||||
end
|
||||
|
||||
TestApp::View::Base
|
||||
}
|
||||
|
||||
context "parts_path configured" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.parts_path = "view/custom_parts"
|
||||
end
|
||||
}
|
||||
|
||||
context "namespace exists" do
|
||||
before do
|
||||
module TestApp
|
||||
module View
|
||||
module CustomParts
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is the matching module within the slice" do
|
||||
is_expected.to eq TestApp::View::CustomParts
|
||||
end
|
||||
end
|
||||
|
||||
context "namespace exists, but needs requiring" do
|
||||
before do
|
||||
allow_any_instance_of(Object).to receive(:require).and_call_original
|
||||
allow_any_instance_of(Object).to receive(:require).with("test_app/view/custom_parts") {
|
||||
module TestApp
|
||||
module View
|
||||
module CustomParts
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
}
|
||||
end
|
||||
|
||||
it "is the matching module within the slice" do
|
||||
is_expected.to eq TestApp::View::CustomParts
|
||||
end
|
||||
end
|
||||
|
||||
context "namespace does not exist" do
|
||||
it "is nil" do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "nil parts_path configured" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.parts_path = nil
|
||||
end
|
||||
}
|
||||
|
||||
it "is nil" do
|
||||
is_expected.to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,93 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
require "hanami/view"
|
||||
|
||||
RSpec.describe "Application view / Template", :application_integration do
|
||||
subject(:template) { view_class.config.template }
|
||||
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.application.instance_eval(&application_hook) if respond_to?(:application_hook)
|
||||
Hanami.application.register_slice :main
|
||||
Hanami.application.prepare
|
||||
|
||||
module TestApp
|
||||
module View
|
||||
class Base < Hanami::View
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Main
|
||||
module View
|
||||
class Base < TestApp::View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "Application base view" do
|
||||
let(:view_class) { TestApp::View::Base }
|
||||
|
||||
it "configures the template to match the class name" do
|
||||
expect(template).to eq "view/base"
|
||||
end
|
||||
end
|
||||
|
||||
context "Slice base view" do
|
||||
let(:view_class) { Main::View::Base }
|
||||
|
||||
it "configures the template to match the class name" do
|
||||
expect(template).to eq "main/view/base"
|
||||
end
|
||||
end
|
||||
|
||||
context "Slice view" do
|
||||
let(:view_class) {
|
||||
module Main
|
||||
module Views
|
||||
module Article
|
||||
class Index < View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::Views::Article::Index
|
||||
}
|
||||
|
||||
it "configures the tempalte to match the class name" do
|
||||
expect(template).to eq "article/index"
|
||||
end
|
||||
end
|
||||
|
||||
context "Slice view with namespace matching template inference base" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.template_inference_base = "my_views"
|
||||
end
|
||||
}
|
||||
|
||||
let(:view_class) {
|
||||
module Main
|
||||
module MyViews
|
||||
module Users
|
||||
class Show < View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::MyViews::Users::Show
|
||||
}
|
||||
|
||||
it "configures the tempalte to match the class name" do
|
||||
expect(template).to eq "users/show"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,214 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
require "hanami/view"
|
||||
|
||||
RSpec.describe "Application views" do
|
||||
context "Outside Hanami app" do
|
||||
subject(:view_class) { Class.new(Hanami::View) }
|
||||
|
||||
before do
|
||||
allow(Hanami).to receive(:respond_to?).with(:application?) { nil }
|
||||
end
|
||||
|
||||
it "is not an application view" do
|
||||
expect(view_class.ancestors).not_to include(a_kind_of(Hanami::View::ApplicationView))
|
||||
end
|
||||
|
||||
it "does not configure the view" do
|
||||
expect(view_class.config.paths).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "Inside Hanami app", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
config.root = "/path/to/app"
|
||||
config.views.paths = ["templates"]
|
||||
config.views.layouts_dir = "test_app_layouts"
|
||||
config.views.layout = "testing"
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.application.instance_eval(&application_hook) if respond_to?(:application_hook)
|
||||
end
|
||||
|
||||
context "Base view defined inside slice" do
|
||||
before do
|
||||
Hanami.application.register_slice :main
|
||||
Hanami.prepare
|
||||
|
||||
module TestApp
|
||||
module View
|
||||
class Base < Hanami::View
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
let(:base_view_class) {
|
||||
module Main
|
||||
module View
|
||||
class Base < TestApp::View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::View::Base
|
||||
}
|
||||
|
||||
describe "base view class" do
|
||||
subject(:view_class) { base_view_class }
|
||||
|
||||
it "is an application view" do
|
||||
expect(view_class.ancestors).to include(a_kind_of(Hanami::View::ApplicationView))
|
||||
end
|
||||
|
||||
describe "config" do
|
||||
subject(:config) { view_class.config }
|
||||
|
||||
describe "path" do
|
||||
context "relative path provided in application config" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.paths = ["templates"]
|
||||
end
|
||||
}
|
||||
|
||||
it "configures the path as the relative path appended onto the slice's root path" do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/path/to/app/slices/main/templates"]
|
||||
end
|
||||
end
|
||||
|
||||
context "absolute path provided in application config" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.paths = ["/absolute/path"]
|
||||
end
|
||||
}
|
||||
|
||||
it "leaves the absolute path in place" do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/absolute/path"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "applies standard view configuration from the application" do
|
||||
aggregate_failures do
|
||||
expect(config.layouts_dir).to eq "test_app_layouts"
|
||||
expect(config.layout).to eq "testing"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "subclass of base view class" do
|
||||
subject(:view_class) {
|
||||
base_view_class
|
||||
|
||||
module Main
|
||||
module Views
|
||||
module Articles
|
||||
class Index < Main::View::Base
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Main::Views::Articles::Index
|
||||
}
|
||||
|
||||
it "inherits the application-specific configuration from the base class" do
|
||||
config = view_class.config
|
||||
|
||||
aggregate_failures do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/path/to/app/slices/main/templates"]
|
||||
expect(config.layouts_dir).to eq "test_app_layouts"
|
||||
expect(config.layout).to eq "testing"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "Base view defined directly inside application" do
|
||||
before do
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
let(:base_view_class) {
|
||||
module TestApp
|
||||
class View < Hanami::View
|
||||
end
|
||||
end
|
||||
|
||||
TestApp::View
|
||||
}
|
||||
|
||||
describe "base view class" do
|
||||
subject(:view_class) { base_view_class }
|
||||
|
||||
it "is an application view" do
|
||||
expect(view_class.ancestors).to include(a_kind_of(Hanami::View::ApplicationView))
|
||||
end
|
||||
|
||||
describe "config" do
|
||||
subject(:config) { view_class.config }
|
||||
|
||||
describe "paths" do
|
||||
context "relative path provided in application config" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.paths = ["templates"]
|
||||
end
|
||||
}
|
||||
|
||||
it "configures the path as the relative path appended onto the slice's root path" do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/path/to/app/templates"]
|
||||
end
|
||||
end
|
||||
|
||||
context "absolute path provided in application config" do
|
||||
let(:application_hook) {
|
||||
proc do
|
||||
config.views.paths = ["/absolute/path"]
|
||||
end
|
||||
}
|
||||
|
||||
it "leaves the absolute path in place" do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/absolute/path"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "subclass of base view class" do
|
||||
subject(:view_class) {
|
||||
base_view_class
|
||||
|
||||
module TestApp
|
||||
module Views
|
||||
module Articles
|
||||
class Index < TestApp::View
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
TestApp::Views::Articles::Index
|
||||
}
|
||||
|
||||
it "inherits the application-specific configuration from the base class" do
|
||||
config = view_class.config
|
||||
|
||||
aggregate_failures do
|
||||
expect(config.paths.map { |path| path.dir.to_s }).to eq ["/path/to/app/templates"]
|
||||
expect(config.layouts_dir).to eq "test_app_layouts"
|
||||
expect(config.layout).to eq "testing"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,39 +0,0 @@
|
|||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Activation", :application_integration do
|
||||
context "Inside Hanami app" do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
subject(:context_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
TestApp::View::Context
|
||||
}
|
||||
|
||||
it "is an ApplicationContext" do
|
||||
expect(context_class.ancestors).to include Hanami::View::ApplicationContext
|
||||
end
|
||||
end
|
||||
|
||||
context "Outside Hanami app" do
|
||||
subject(:context_class) {
|
||||
Class.new(Hanami::View::Context)
|
||||
}
|
||||
|
||||
it "is not an ApplicationContext" do
|
||||
expect(context_class.ancestors).not_to include Hanami::View::ApplicationContext
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,69 +0,0 @@
|
|||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Assets", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
let(:context_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
TestApp::View::Context
|
||||
}
|
||||
|
||||
subject(:context) {
|
||||
context_class.new
|
||||
}
|
||||
|
||||
describe "#assets" do
|
||||
context "without assets provider" do
|
||||
it "raises error" do
|
||||
expect { context.assets }.to raise_error(Hanami::View::MissingProviderError, /hanami-assets/)
|
||||
end
|
||||
end
|
||||
|
||||
context "with assets provider" do
|
||||
before do
|
||||
Hanami.application.register_provider(:assets) do
|
||||
start do
|
||||
register "assets", Object.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "is the application assets by default" do
|
||||
expect(context.assets).to be TestApp::Application[:assets]
|
||||
end
|
||||
|
||||
context "injected assets" do
|
||||
subject(:context) {
|
||||
context_class.new(assets: assets)
|
||||
}
|
||||
|
||||
let(:assets) { double(:assets) }
|
||||
|
||||
it "is the injected assets" do
|
||||
expect(context.assets).to be assets
|
||||
end
|
||||
|
||||
context "rebuilt context" do
|
||||
subject(:new_context) { context.with }
|
||||
|
||||
it "retains the injected assets" do
|
||||
expect(new_context.assets).to be assets
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Inflector", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
let(:context_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
TestApp::View::Context
|
||||
}
|
||||
|
||||
subject(:context) {
|
||||
context_class.new
|
||||
}
|
||||
|
||||
describe "#inflector" do
|
||||
it "is the application inflector by default" do
|
||||
expect(context.inflector).to be TestApp::Application.inflector
|
||||
end
|
||||
|
||||
context "injected inflector" do
|
||||
subject(:context) {
|
||||
context_class.new(inflector: inflector)
|
||||
}
|
||||
|
||||
let(:inflector) { double(:inflector) }
|
||||
|
||||
it "is the injected inflector" do
|
||||
expect(context.inflector).to be inflector
|
||||
end
|
||||
|
||||
context "rebuilt context" do
|
||||
subject(:new_context) { context.with }
|
||||
|
||||
it "retains the injected inflector" do
|
||||
expect(new_context.inflector).to be inflector
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,63 +0,0 @@
|
|||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Request", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
let(:context_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
TestApp::View::Context
|
||||
}
|
||||
|
||||
subject(:context) {
|
||||
context_class.new(
|
||||
request: request,
|
||||
response: response,
|
||||
)
|
||||
}
|
||||
|
||||
let(:request) { double(:request) }
|
||||
let(:response) { double(:response) }
|
||||
|
||||
describe "#request" do
|
||||
it "is the provided request" do
|
||||
expect(context.request).to be request
|
||||
end
|
||||
end
|
||||
|
||||
describe "#sesion" do
|
||||
let(:session) { double(:session) }
|
||||
|
||||
before do
|
||||
allow(request).to receive(:session) { session }
|
||||
end
|
||||
|
||||
it "is the request's session" do
|
||||
expect(context.session).to be session
|
||||
end
|
||||
end
|
||||
|
||||
describe "#flash" do
|
||||
let(:flash) { double(:flash) }
|
||||
|
||||
before do
|
||||
allow(response).to receive(:flash) { flash }
|
||||
end
|
||||
|
||||
it "is the response's flash" do
|
||||
expect(context.flash).to be flash
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,65 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Routes", :application_integration do
|
||||
it "accesses application routes" do
|
||||
with_tmp_directory(Dir.mktmpdir) do
|
||||
write "config/application.rb", <<~RUBY
|
||||
require "hanami"
|
||||
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
register_slice :main
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
write "config/routes.rb", <<~RUBY
|
||||
module TestApp
|
||||
class Routes < Hanami::Application::Routes
|
||||
define do
|
||||
slice :main, at: "/" do
|
||||
root to: "test_action"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
write "lib/test_app/view/context.rb", <<~RUBY
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
require "hanami/prepare"
|
||||
|
||||
context = TestApp::View::Context.new
|
||||
expect(context.routes.path(:root)).to eq "/"
|
||||
end
|
||||
end
|
||||
|
||||
it "can inject routes" do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
Hanami.prepare
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
routes = double(:routes)
|
||||
|
||||
context = TestApp::View::Context.new(routes: routes)
|
||||
|
||||
expect(context.routes).to be(routes)
|
||||
end
|
||||
end
|
|
@ -1,53 +0,0 @@
|
|||
require "hanami"
|
||||
require "hanami/view/context"
|
||||
|
||||
RSpec.describe "Application context / Settings", :application_integration do
|
||||
before do
|
||||
module TestApp
|
||||
class Application < Hanami::Application
|
||||
end
|
||||
end
|
||||
|
||||
Hanami.prepare
|
||||
end
|
||||
|
||||
let(:context_class) {
|
||||
module TestApp
|
||||
module View
|
||||
class Context < Hanami::View::Context
|
||||
end
|
||||
end
|
||||
end
|
||||
TestApp::View::Context
|
||||
}
|
||||
|
||||
subject(:context) {
|
||||
context_class.new
|
||||
}
|
||||
|
||||
describe "#settings" do
|
||||
it "is the application settings by default" do
|
||||
expect(context.settings).to be TestApp::Application.settings
|
||||
end
|
||||
|
||||
context "injected settings" do
|
||||
subject(:context) {
|
||||
context_class.new(settings: settings)
|
||||
}
|
||||
|
||||
let(:settings) { double(:settings) }
|
||||
|
||||
it "is the injected settings" do
|
||||
expect(context.settings).to be settings
|
||||
end
|
||||
|
||||
context "rebuilt context" do
|
||||
subject(:new_context) { context.with }
|
||||
|
||||
it "retains the injected settings" do
|
||||
expect(new_context.settings).to be settings
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -52,5 +52,3 @@ RSpec::Matchers.define :part_including do |data|
|
|||
}
|
||||
}
|
||||
end
|
||||
|
||||
require_relative "support/application_integration"
|
||||
|
|
|
@ -1,46 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "tmpdir"
|
||||
require "hanami/devtools/integration/files"
|
||||
require "hanami/devtools/integration/with_tmp_directory"
|
||||
|
||||
RSpec.shared_context "Application integration" do
|
||||
let(:application_modules) { %i[TestApp Main] }
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
config.include RSpec::Support::Files, :application_integration
|
||||
config.include RSpec::Support::WithTmpDirectory, :application_integration
|
||||
config.include_context "Application integration", :application_integration
|
||||
|
||||
config.before :each, :application_integration do
|
||||
@load_paths = $LOAD_PATH.dup
|
||||
|
||||
application_modules.each do |app_module|
|
||||
Object.const_set(app_module, Module.new { |m| m.extend(TestNamespace) })
|
||||
end
|
||||
end
|
||||
|
||||
config.after :each, :application_integration do
|
||||
# Tear down Zeitwerk (from zeitwerk's own test/support/loader_test)
|
||||
Zeitwerk::Registry.loaders.each(&:unload)
|
||||
Zeitwerk::Registry.loaders.clear
|
||||
Zeitwerk::Registry.loaders_managing_gems.clear
|
||||
Zeitwerk::ExplicitNamespace.cpaths.clear
|
||||
Zeitwerk::ExplicitNamespace.tracer.disable
|
||||
|
||||
$LOAD_PATH.replace(@load_paths)
|
||||
$LOADED_FEATURES.delete_if do |feature_path|
|
||||
feature_path =~ %r{hanami/(setup|prepare|boot|application/container/providers)}
|
||||
end
|
||||
|
||||
application_modules.each do |app_module|
|
||||
Object.const_get(app_module).remove_constants
|
||||
Object.send :remove_const, app_module
|
||||
end
|
||||
|
||||
%i[@_application @_app].each do |ivar|
|
||||
Hanami.remove_instance_variable(ivar) if Hanami.instance_variable_defined?(ivar)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,76 +0,0 @@
|
|||
require "hanami/view/application_configuration"
|
||||
require "saharspec/matchers/dont"
|
||||
|
||||
RSpec.describe Hanami::View::ApplicationConfiguration do
|
||||
subject(:configuration) { described_class.new }
|
||||
|
||||
it "includes base view configuration" do
|
||||
expect(configuration).to respond_to(:paths)
|
||||
expect(configuration).to respond_to(:paths=)
|
||||
end
|
||||
|
||||
it "does not include the inflector setting" do
|
||||
expect(configuration).not_to respond_to(:inflector)
|
||||
expect(configuration).not_to respond_to(:inflector=)
|
||||
end
|
||||
|
||||
it "preserves default values from the base view configuration" do
|
||||
expect(configuration.layouts_dir).to eq Hanami::View.config.layouts_dir
|
||||
end
|
||||
|
||||
it "allows settings to be configured independently of the base view configuration" do
|
||||
expect { configuration.layouts_dir = "custom_layouts" }
|
||||
.to change { configuration.layouts_dir }.to("custom_layouts")
|
||||
.and dont.change { Hanami::View.config.layouts_dir }
|
||||
end
|
||||
|
||||
describe "specialised default values" do
|
||||
describe "paths" do
|
||||
it 'is ["templates"]' do
|
||||
expect(configuration.paths).to match [
|
||||
an_object_satisfying { |path| path.dir.to_s == "templates" }
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
describe "template_inference_base" do
|
||||
it 'is "views"' do
|
||||
expect(configuration.template_inference_base).to eq "views"
|
||||
end
|
||||
end
|
||||
|
||||
describe "layout" do
|
||||
it 'is "application"' do
|
||||
expect(configuration.layout).to eq "application"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#settings" do
|
||||
it "includes locally defined settings" do
|
||||
expect(configuration.settings).to include :parts_path
|
||||
end
|
||||
|
||||
it "includes all view settings apart from inflector" do
|
||||
expect(configuration.settings).to include (Hanami::View.settings - [:inflector])
|
||||
end
|
||||
end
|
||||
|
||||
describe "finalized configuration" do
|
||||
before do
|
||||
configuration.finalize!
|
||||
end
|
||||
|
||||
it "is frozen" do
|
||||
expect(configuration).to be_frozen
|
||||
end
|
||||
|
||||
it "does not allow changes to locally defined settings" do
|
||||
expect { configuration.parts_path = "parts" }.to raise_error(Dry::Configurable::FrozenConfig)
|
||||
end
|
||||
|
||||
it "does not allow changes to base view settings" do
|
||||
expect { configuration.paths = [] }.to raise_error(Dry::Configurable::FrozenConfig)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue