Remove support for Hanami application integrated views (#207)

These integrations have moved into the hanami gem itself.
This commit is contained in:
Tim Riley 2022-04-22 11:39:04 +10:00 committed by GitHub
parent ec90f452fa
commit 198f2daa5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 377 additions and 1630 deletions

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -52,5 +52,3 @@ RSpec::Matchers.define :part_including do |data|
}
}
end
require_relative "support/application_integration"

View File

@ -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

View File

@ -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