Install yard and add initial API docs
This commit is contained in:
parent
571c8441e2
commit
e5049b759d
|
@ -0,0 +1,5 @@
|
|||
--query '@api.text != "private"'
|
||||
--markup-provider=redcarpet
|
||||
--markup=markdown
|
||||
--plugin junk
|
||||
lib/**/*.rb
|
6
Gemfile
6
Gemfile
|
@ -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
|
||||
|
|
428
lib/dry/view.rb
428
lib/dry/view.rb
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ require "dry/view/exposure"
|
|||
|
||||
module Dry
|
||||
class View
|
||||
# @api private
|
||||
class Exposures
|
||||
include Dry::Equalizer(:exposures)
|
||||
include TSort
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2,6 +2,7 @@ require "dry/equalizer"
|
|||
|
||||
module Dry
|
||||
class View
|
||||
# @api private
|
||||
class RenderEnvironment
|
||||
def self.prepare(renderer, config, context)
|
||||
new(
|
||||
|
|
|
@ -2,6 +2,7 @@ require "dry/inflector"
|
|||
|
||||
module Dry
|
||||
class View
|
||||
# @api private
|
||||
class RenderEnvironmentMissing
|
||||
class MissingEnvironmentError < StandardError
|
||||
def message
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -4,6 +4,7 @@ require_relative 'tilt'
|
|||
|
||||
module Dry
|
||||
class View
|
||||
# @api private
|
||||
class Renderer
|
||||
PARTIAL_PREFIX = "_".freeze
|
||||
PATH_DELIMITER = "/".freeze
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -3,6 +3,7 @@ require "tilt"
|
|||
|
||||
module Dry
|
||||
class View
|
||||
# @api private
|
||||
module Tilt
|
||||
extend Dry::Core::Cache
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
module Dry
|
||||
class View
|
||||
# Current release version
|
||||
VERSION = '0.5.4'.freeze
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue