Install yard and add initial API docs

This commit is contained in:
Tim Riley 2019-01-29 21:55:17 +11:00
parent 571c8441e2
commit e5049b759d
No known key found for this signature in database
GPG Key ID: 747ABA1282E88BC9
18 changed files with 753 additions and 48 deletions

5
.yardopts Normal file
View File

@ -0,0 +1,5 @@
--query '@api.text != "private"'
--markup-provider=redcarpet
--markup=markdown
--plugin junk
lib/**/*.rb

View File

@ -24,3 +24,9 @@ group :benchmarks do
gem 'actionview'
gem 'actionpack'
end
group :docs do
gem 'yard'
gem 'yard-junk'
gem 'redcarpet', platforms: :mri
end

View File

@ -12,42 +12,210 @@ require_relative 'view/rendered'
require_relative 'view/renderer'
require_relative 'view/scope_builder'
# A collection of next-generation Ruby libraries, helping you to write clear,
# flexible, and more maintainable Ruby code. Each dry-rb gem fulfils a common
# task, and together they make a powerful platform for any kind of Ruby
# application.
module Dry
# A standalone, template-based view rendering system that offers everything
# you need to write well-factored view code.
#
# This represents a single view, holding the configuration and exposures
# necessary for rendering its template.
#
# @abstract Subclass this and provide your own configuration and exposures to
# define your own view (along with a custom `#initialize` if you wish to
# inject dependencies into your subclass)
#
# @see https://dry-rb.org/gems/dry-view/
#
# @api public
class View
extend Dry::Core::Cache
# @api private
UndefinedTemplateError = Class.new(StandardError)
# @api private
DEFAULT_RENDERER_OPTIONS = {default_encoding: 'utf-8'.freeze}.freeze
include Dry::Equalizer(:config, :exposures)
extend Dry::Core::Cache
extend Dry::Configurable
# @!group Configuration
# @overload config.paths=(paths)
# Set an array of directories that will be searched for all templates
# (templates, partials, and layouts).
#
# These will be converted into Path objects and used for template lookup
# when rendering.
#
# This is a **required setting**.
#
# @param paths [String, Path, Array<String, Path>] the paths
#
# @api public
# @!scope class
setting :paths do |paths|
Array(paths).map { |path| Path[path] }
end
setting :layout, false
setting :layouts_dir, "layouts".freeze
# @overload config.template=(name)
# Set the name of the template for rendering this view. Template name
# should be relative to the configured `paths`.
#
# This is a **required setting**.
#
# @param name [String] template name
# @api public
# @!scope class
setting :template
# @overload config.layout=(name)
# Set the name of the layout to render templates within. Layouts will be
# looked up within the configured `layouts_dir`, within the configured
# `paths`.
#
# A false or nil value will use no layout. Defaults to `nil`.
#
# @param name [String, FalseClass, nil] layout name, or false to indicate no layout
# @api public
# @!scope class
setting :layout, false
# @overload config.layouts_dir=(dir)
# Set the name of the directory (within the configured `paths`) holding
# the layouts. Defaults to `"layouts"`
#
# @param dir [String] directory name
# @api public
# @!scope class
setting :layouts_dir, "layouts".freeze
# @overload config.scope=(scope_class)
# Set the scope class to use when rendering the view's template.
#
# Configuring a custom scope class allows you to provide extra behaviour
# (alongside exposures) to the template.
#
# @see https://dry-rb.org/gems/dry-view/scopes/
#
# @param scope_class [Class] scope class (inheriting from `Dry::View::Scope`)
# @api public
# @!scope class
setting :scope
# @overload config.default_context=(context)
# Set the default context object to use when rendering. This will be used
# unless another context object is applied at render-time to `View#call`
#
# Defaults to a frozen instance of `Dry::View::Context`.
#
# @see View#call
#
# @param context [Dry::View::Context] context object
# @api public
# @!scope class
setting :default_context, Context.new.freeze
# @overload config.default_format=(format)
# Set the default format to use when rendering.
#
# Defaults to `:html`.
#
# @param format [Symbol]
# @api public
# @!scope class
setting :default_format, :html
setting :renderer_engine_mapping
# @overload config.scope_namespace=(namespace)
# Set a namespace that will be searched when building scope classes.
#
# @param namespace [Module, Class]
#
# @see Scope
#
# @api public
# @!scope class
setting :part_namespace
# @overload config.part_builder=(part_builder)
# Set a custom part builder class
#
# @see https://dry-rb.org/gems/dry-view/parts/
#
# @param part_builder [Class]
# @api public
# @!scope class
setting :part_builder, PartBuilder
# @overload config.scope_namespace=(namespace)
# Set a namespace that will be searched when building scope classes.
#
# @param namespace [Module, Class]
#
# @see Scope
#
# @api public
# @!scope class
setting :scope_namespace
# @overload config.scope_builder=(scope_builder)
# Set a custom scope builder class
#
# @see https://dry-rb.org/gems/dry-view/scopes/
#
# @param scope_builder [Class]
# @api public
# @!scope class
setting :scope_builder, ScopeBuilder
# @overload config.inflector=(inflector)
# Set an inflector to provide to the part_builder and scope_builder.
#
# Defaults to `Dry::Inflector.new`.
#
# @param inflector
# @api public
# @!scope class
setting :inflector, Dry::Inflector.new
# @overload config.renderer_options=(options)
# A hash of options to pass to the template engine. Template engines are
# provided by Tilt; see Tilt's documentation for what options your
# template engine may support.
#
# Defaults to `{default_encoding: "utf-8"}`. Any options passed will be
# merged onto the defaults.
#
# @see https://github.com/rtomayko/tilt
#
# @param options [Hash] renderer options
# @api public
# @!scope class
setting :renderer_options, DEFAULT_RENDERER_OPTIONS do |options|
DEFAULT_RENDERER_OPTIONS.merge(options.to_h).freeze
end
setting :default_context, Context.new.freeze
# @overload config.renderer_engine_mapping=(mapping)
# A hash specifying the (Tilt-compatible) template engine class to use
# for a given format. Template engine detection is automatic based on
# format; use this setting only if you want to force a non-preferred
# engine.
#
# @example
# config.renderer_engine_mapping = {erb: Tilt::ErubiTemplate}
#
# @see https://github.com/rtomayko/tilt
#
# @param mapping [Hash<Symbol, Class>] engine mapping
# @api public
# @!scope class
setting :renderer_engine_mapping
setting :scope
setting :inflector, Dry::Inflector.new
setting :part_builder, PartBuilder
setting :part_namespace
setting :scope_builder, ScopeBuilder
setting :scope_namespace
# @!endgroup
# @api private
def self.inherited(klass)
@ -57,38 +225,111 @@ module Dry
end
end
# @api private
def self.layout_path
File.join(config.layouts_dir, config.layout)
end
# @!group Exposures
# @api public
def self.render_env(format: config.default_format, context: config.default_context)
RenderEnvironment.prepare(renderer(format), config, context)
end
# @api public
def self.template_env(**args)
render_env(**args).chdir(config.template)
end
# @api public
def self.layout_env(**args)
render_env(**args).chdir(layout_path)
end
# @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
# @!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
@ -105,23 +346,115 @@ module Dry
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 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)
end
# @!endgroup
# The view's bound exposures
#
# @return [Exposures]
# @api private
attr_reader :exposures
# 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
# @api public
# The view's configuration
#
# @api private
def config
self.class.config
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)
raise UndefinedTemplateError, "no +template+ configured" unless config.template
@ -142,6 +475,7 @@ module Dry
private
# @api private
def locals(render_env, input)
exposures.(context: render_env.context, **input) do |value, exposure|
if exposure.decorate? && value
@ -152,12 +486,14 @@ module Dry
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
end

View File

@ -3,23 +3,70 @@ require_relative "decorated_attributes"
module Dry
class View
# Provides a baseline environment across all the templates, parts and scopes
# in a given rendering.
#
# @abstract Subclass this and add your own methods (along with a custom
# `#initialize` if you wish to inject dependencies)
#
# @api public
class Context
include Dry::Equalizer(:_options)
include DecoratedAttributes
attr_reader :_render_env, :_options
# Returns a new instance of Context
#
# In subclasses, you should include an `**options` parameter and pass _all
# arguments_ to `super`. This allows Context to make copies of itself
# while preserving your dependencies.
#
# @example
# class MyContext < Dry::View::Context
# # Injected dependency
# attr_reader :assets
#
# def initialize(assets:, **options)
# @assets = assets
# super
# end
# end
#
# @api public
def initialize(render_env: nil, **options)
@_render_env = render_env
@_options = options
end
# @api private
def for_render_env(render_env)
return self if render_env == self._render_env
self.class.new(**_options.merge(render_env: render_env))
end
# Returns a copy of the Context with new options merged in.
#
# This may be useful to supply values for dependencies that are _optional_
# when initializing your custom Context subclass.
#
# @example
# class MyContext < Dry::View::Context
# # Injected dependencies (request is optional)
# attr_reader :assets, :request
#
# def initialize(assets:, request: nil, **options)
# @assets = assets
# @request = reuqest
# super
# end
# end
#
# my_context = MyContext.new(assets: assets)
# my_context_with_request = my_context.with(request: request)
#
# @api public
def with(**new_options)
self.class.new(
render_env: _render_env,

View File

@ -2,14 +2,32 @@ require "set"
module Dry
class View
# Decorates attributes in Parts.
module DecoratedAttributes
# @api private
def self.included(klass)
klass.extend ClassInterface
end
# Decorated attributes class-level interface.
module ClassInterface
# @api private
MODULE_NAME = :DecoratedAttributes
# Decorates the provided attributes, wrapping them in Parts using the
# current render environment.
#
# @example
# class Article < Dry::View::Part
# decorate :feature_image
# decorate :author as: :person
# end
#
# @param names [Array<Symbol>] the attribute names
# @param options [Hash] the options to pass to the Part Builder
# @option options [Symbol, Class] :as an alternative name or class to use when finding a matching Part
#
# @api public
def decorate(*names, **options)
decorated_attributes.decorate(*names, **options)
end
@ -27,6 +45,7 @@ module Dry
end
end
# @api private
class Attributes < Module
def initialize(*)
@names = Set.new

View File

@ -2,6 +2,9 @@ require 'dry-equalizer'
module Dry
class View
# An exposure defined on a view
#
# @api private
class Exposure
include Dry::Equalizer(:name, :proc, :object, :options)

View File

@ -4,6 +4,7 @@ require "dry/view/exposure"
module Dry
class View
# @api private
class Exposures
include Dry::Equalizer(:exposures)
include TSort

View File

@ -4,7 +4,17 @@ require_relative "render_environment_missing"
module Dry
class View
# Decorates an exposure value and provides a place to encapsulate
# view-specific behavior alongside your application's domain objects.
#
# @abstract Subclass this and provide your own methods adding view-specific
# behavior. You should not override `#initialize`.
#
# @see https://dry-rb.org/gems/dry-view/parts/
#
# @api public
class Part
# @api private
CONVENIENCE_METHODS = %i[
format
context
@ -16,42 +26,146 @@ module Dry
include Dry::Equalizer(:_name, :_value, :_render_env)
include DecoratedAttributes
# The part's name. This comes from the exposure supplying the value.
#
# @return [Symbol] name
#
# @api public
attr_reader :_name
# The decorated value. This is the value returned from the exposure.
#
# @overload _value
# Returns the value.
# @overload value
# A convenience alias for `_value`. Is available unless the value itself
# responds to `#value`.
#
# @return [Object] value
#
# @api public
attr_reader :_value
# The current render environment
#
# @return [RenderEnvironment] render environment
#
# @api private
attr_reader :_render_env
# Determins a part name (when initialized without one). Intended for use
# only while unit testing Parts.
#
# @api private
def self.part_name(inflector)
name ? inflector.underscore(inflector.demodulize(name)) : "part"
end
# Returns a new Part instance
#
# @param name [Symbol] part name
# @param value [Object] the value to decorate
# @param render_env [RenderEnvironment] render environment
#
# @api public
def initialize(render_env: RenderEnvironmentMissing.new, name: self.class.part_name(render_env.inflector), value:)
@_name = name
@_value = value
@_render_env = render_env
end
# The template format for the current render environment.
#
# @overload _format
# Returns the format.
# @overload format
# A convenience alias for `#_format.` Is available unless the value
# itself responds to `#format`.
#
# @return [Symbol] format
#
# @api public
def _format
_render_env.format
end
# The context object for the current render environment
#
# @overload _context
# Returns the context.
# @overload context
# A convenience alias for `#_context`. Is available unless the value
# itself responds to `#context`.
#
# @return [Context] context
#
# @api public
def _context
_render_env.context
end
# Renders a new partial with the part included in its locals.
#
# @overload _render(partial_name, as: _name, **locals, &block)
# Renders the partial.
# @overload render(partial_name, as: _name, **locals, &block)
# A convenience alias for `#_render`. Is available unless the value
# itself responds to `#render`.
#
# @param partial_name [Symbol, String] partial name
# @param as [Symbol] the name for the Part to assume in the partial's locals. Default's to the Part's `_name`.
# @param locals [Hash<Symbol, Object>] other locals to provide the partial
#
# @return [String] rendered partial
#
# @api public
def _render(partial_name, as: _name, **locals, &block)
_render_env.partial(partial_name, _render_env.scope({as => self}.merge(locals)), &block)
end
# Builds a new scope with the part included in its locals.
#
# @overload _scope(scope_name = nil, **locals)
# Builds the scope.
# @overload scope(scope_name = nil, **locals)
# A convenience alias for `#_scope`. Is available unless the value
# itself responds to `#scope`.
#
# @param scope_name [Symbol, nil] scope name, used by the scope builder to determine the scope class
# @param locals [Hash<Symbol, Object>] other locals to provide the partial
#
# @return [Dry::View::Scope] scope
#
# @api public
def _scope(scope_name = nil, **locals)
_render_env.scope(scope_name, {_name => self}.merge(locals))
end
# Returns a string representation of the value
#
# @return [String]
#
# @api public
def to_s
_value.to_s
end
# Builds a new a part with the given parameters
#
# This is helpful for manually constructing a new part object that
# maintains the current render environment.
#
# However, using `.decorate` is preferred for declaring attributes that
# should also be decorated as parts.
#
# @see DecoratedAttributes::ClassInterface#decorate
#
# @param klass [Class] part class to use (defaults to the part's class)
# @param name [Symbol] part name (defaults to the part's name)
# @param value [Object] value to decorate (defaults to the part's value)
# @param options[Hash<Symbol, Object>] other options to provide when initializing the new part
#
# @api public
def new(klass = (self.class), name: (_name), value: (_value), **options)
klass.new(
name: name,
@ -61,12 +175,19 @@ module Dry
)
end
# Returns a string representation of the part
#
# @return [String]
#
# @api public
def inspect
%(#<#{self.class.name} name=#{_name.inspect} value=#{_value.inspect}>)
end
private
# Handles missing methods. If the `_value` responds to the method, then
# the method will be sent to the value.
def method_missing(name, *args, &block)
if _value.respond_to?(name)
_value.public_send(name, *args, &block)

View File

@ -4,6 +4,9 @@ require_relative 'part'
module Dry
class View
# Decorates exposure values with matching parts
#
# @api private
class PartBuilder
extend Dry::Core::Cache
include Dry::Equalizer(:namespace)
@ -11,17 +14,30 @@ module Dry
attr_reader :namespace
attr_reader :render_env
# Returns a new instance of PartBuilder
#
# @api private
def initialize(namespace: nil, render_env: nil)
@namespace = namespace
@render_env = render_env
end
# @api private
def for_render_env(render_env)
return self if render_env == self.render_env
self.class.new(namespace: namespace, render_env: render_env)
end
# Decorates an exposure value
#
# @param name [Symbol] exposure name
# @param value [Object] exposure value
# @param options [Hash] exposure options
#
# @return [Dry::View::Part] decorated value
#
# @api private
def call(name, value, **options)
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part
@ -93,7 +109,7 @@ module Dry
name = inflector.camelize(name.to_s)
# Give autoloaders a change to act
# Give autoloaders a chance to act
begin
klass = namespace.const_get(name)
rescue NameError

View File

@ -3,6 +3,7 @@ require "dry/core/cache"
module Dry
class View
# @api private
class Path
extend Dry::Core::Cache
include Dry::Equalizer(:dir, :root)

View File

@ -2,6 +2,7 @@ require "dry/equalizer"
module Dry
class View
# @api private
class RenderEnvironment
def self.prepare(renderer, config, context)
new(

View File

@ -2,6 +2,7 @@ require "dry/inflector"
module Dry
class View
# @api private
class RenderEnvironmentMissing
class MissingEnvironmentError < StandardError
def message

View File

@ -2,21 +2,48 @@ require 'dry/equalizer'
module Dry
class View
# Output of a View rendering
#
# @api public
class Rendered
include Dry::Equalizer(:output, :locals)
# Returns the rendered view
#
# @return [String]
#
# @api public
attr_reader :output
# Returns the hash of locals used to render the view
#
# @return [Hash[<Symbol, Dry::View::Part>] locals hash
#
# @api public
attr_reader :locals
# @api private
def initialize(output:, locals:)
@output = output
@locals = locals
end
# Returns the local corresponding to the key
#
# @param name [Symbol] local key
#
# @return [Dry::View::Part]
#
# @api public
def [](name)
locals[name]
end
# Returns the rendered view
#
# @return [String]
#
# @api public
def to_s
output
end

View File

@ -4,6 +4,7 @@ require_relative 'tilt'
module Dry
class View
# @api private
class Renderer
PARTIAL_PREFIX = "_".freeze
PATH_DELIMITER = "/".freeze

View File

@ -4,21 +4,81 @@ require_relative "render_environment_missing"
module Dry
class View
# Evaluation context for templates (including layouts and partials) and
# provides a place to encapsulate view-specific behaviour alongside a
# template and its locals.
#
# @abstract Subclass this and provide your own methods adding view-specific
# behavior. You should not override `#initialize`
#
# @see https://dry-rb.org/gems/dry-view/templates/
# @see https://dry-rb.org/gems/dry-view/scopes/
#
# @api public
class Scope
# @api private
CONVENIENCE_METHODS = %i[format context locals].freeze
include Dry::Equalizer(:_name, :_locals, :_render_env)
# The scope's name
#
# @return [Symbol]
#
# @api public
attr_reader :_name
# The scope's locals
#
# @overload _locals
# Returns the locals
# @overload locals
# A convenience alias for `#_format.` Is available unless there is a
# local named `locals`
#
# @return [Hash[<Symbol, Object>]
#
# @api public
attr_reader :_locals
# The current render environment
#
# @return [RenderEnvironment] render environment
#
# @api private
attr_reader :_render_env
# Returns a new Scope instance
#
# @param name [Symbol, nil] scope name
# @param locals [Hash<Symbol, Object>] template locals
# @param render_env [RenderEnvironment] render environment
#
# @return [Scope]
#
# @api public
def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, render_env: RenderEnvironmentMissing.new)
@_name = name
@_locals = locals
@_render_env = render_env
end
# @overload render(partial_name, **locals, &block)
# Renders a partial using the scope
#
# @param partial_name [Symbol, String] partial name
# @param locals [Hash<Symbol, Object>] partial locals
# @yieldreturn [String] string content to include where the partial calls `yield`
#
# @overload render(**locals, &block)
# Renders a partial (named after the scope's own name) using the scope
#
# @param locals[Hash<Symbol, Object>] partial locals
# @yieldreturn [String] string content to include where the partial calls `yield`
#
# @return [String] the rendered partial output
#
# @api public
def render(partial_name = nil, **locals, &block)
partial_name ||= _name
raise ArgumentError, "+partial_name+ must be provided for unnamed scopes" unless partial_name
@ -27,20 +87,56 @@ module Dry
_render_env.partial(partial_name, _render_scope(locals), &block)
end
# Build a new scope using a scope class matching the provided name
#
# @param name [Symbol, Class] scope name (or class)
# @param locals [Hash<Symbol, Object>] scope locals
#
# @return [Scope]
#
# @api public
def scope(name = nil, **locals)
_render_env.scope(name, locals)
end
# The template format for the current render environment.
#
# @overload _format
# Returns the format.
# @overload format
# A convenience alias for `#_format.` Is available unless there is a
# local named `format`
#
# @return [Symbol] format
#
# @api public
def _format
_render_env.format
end
# The context object for the current render environment
#
# @overload _context
# Returns the context.
# @overload context
# A convenience alias for `#_context`. Is available unless there is a
# local named `context`.
#
# @return [Context] context
#
# @api public
def _context
_render_env.context
end
private
# Handles missing methods, according to the following rules:
#
# 1. If there is a local with a name matching the method, it returns the
# local.
# 2. If the `context` responds to the method, then it will be sent the
# method and all its arguments.
def method_missing(name, *args, &block)
if _locals.key?(name)
_locals[name]

View File

@ -4,24 +4,46 @@ require_relative 'scope'
module Dry
class View
# Builds scope objects via matching classes
#
# @api private
class ScopeBuilder
extend Dry::Core::Cache
include Dry::Equalizer(:namespace)
# The view's configured `scope_namespace`
#
# @api private
attr_reader :namespace
# @return [RenderEnvironment]
#
# @api private
attr_reader :render_env
# Returns a new instance of ScopeBuilder
#
# @api private
def initialize(namespace: nil, render_env: nil)
@namespace = namespace
@render_env = render_env
end
# @api private
def for_render_env(render_env)
return self if render_env == self.render_env
self.class.new(namespace: namespace, render_env: render_env)
end
# Returns a new scope using a class matching the name
#
# @param name [Symbol, Class] scope name
# @param locals [Hash<Symbol, Object>] locals hash
#
# @return [Dry::View::Scope]
#
# @api private
def call(name = nil, locals)
scope_class(name).new(
name: name,
@ -49,7 +71,7 @@ module Dry
def resolve_scope_class(name:)
name = inflector.camelize(name.to_s)
# Give autoloaders a change to act
# Give autoloaders a chance to act
begin
klass = namespace.const_get(name)
rescue NameError

View File

@ -3,6 +3,7 @@ require "tilt"
module Dry
class View
# @api private
module Tilt
extend Dry::Core::Cache

View File

@ -1,5 +1,6 @@
module Dry
class View
# Current release version
VERSION = '0.5.4'.freeze
end
end