Skip to content

Commit

Permalink
Merge pull request #90 from dry-rb/custom-scopes
Browse files Browse the repository at this point in the history
Introduce customizable scope objects
  • Loading branch information
timriley committed Dec 18, 2018
2 parents 4f71a6e + 06df9f0 commit 4acab57
Show file tree
Hide file tree
Showing 28 changed files with 563 additions and 225 deletions.
40 changes: 18 additions & 22 deletions lib/dry/view/context.rb
@@ -1,25 +1,25 @@
require "dry/equalizer"

module Dry
module View
class Context
attr_reader :_options, :_part_builder, :_renderer
include Dry::Equalizer(:_options)

attr_reader :_rendering, :_options

def initialize(part_builder: nil, renderer: nil, **options)
@_part_builder = part_builder
@_renderer = renderer
def initialize(rendering: nil, **options)
@_rendering = rendering
@_options = options
end

def bind(part_builder:, renderer:)
self.class.new(
**_options.merge(
part_builder: part_builder,
renderer: renderer,
)
)
def for_rendering(rendering)
return self if rendering == self._rendering

self.class.new(**_options.merge(rendering: rendering))
end

def bound?
!!(_part_builder && _renderer)
def rendering?
!!_rendering
end

def self.decorate(*names, **options)
Expand All @@ -28,15 +28,11 @@ def self.decorate(*names, **options)
define_method name do
attribute = super()

return attribute unless bound? || !attribute

_part_builder.(
name: name,
value: attribute,
renderer: _renderer,
context: self,
**options,
)
if rendering? && attribute
_rendering.part(name, attribute, **options)
else
attribute
end
end
end
end
Expand Down
70 changes: 20 additions & 50 deletions lib/dry/view/controller.rb
Expand Up @@ -8,7 +8,8 @@
require_relative 'path'
require_relative 'rendered'
require_relative 'renderer'
require_relative 'scope'
require_relative 'rendering'
require_relative 'scope_builder'

module Dry
module View
Expand All @@ -33,18 +34,21 @@ class Controller
end
setting :default_context, DEFAULT_CONTEXT

setting :scope

setting :inflector, Dry::Inflector.new

setting :part_builder, PartBuilder
setting :part_namespace

setting :scope_builder, ScopeBuilder
setting :scope_namespace

attr_reader :config
attr_reader :layout_dir
attr_reader :layout_path
attr_reader :template_path

attr_reader :part_builder

attr_reader :exposures

# @api private
Expand All @@ -60,6 +64,11 @@ def self.paths
Array(config.paths).map { |path| Dry::View::Path.new(path) }
end

# @api private
def self.rendering(format: config.default_format, context: config.default_context)
Rendering.prepare(renderer(format), config, context)
end

# @api private
def self.renderer(format)
renderers.fetch(format) {
Expand Down Expand Up @@ -100,38 +109,31 @@ def initialize
@layout_path = "#{layout_dir}/#{config.layout}"
@template_path = config.template

@part_builder = config.part_builder.new(
namespace: config.part_namespace,
inflector: config.inflector,
)

@exposures = self.class.exposures.bind(self)
end

# @api public
def call(format: config.default_format, context: config.default_context, **input)
raise UndefinedTemplateError, "no +template+ configured" unless template_path

renderer = self.class.renderer(format)
context = context.bind(part_builder: part_builder, renderer: renderer)

locals = locals(renderer.chdir(template_path), context, input)

output = renderer.template(template_path, template_scope(renderer, context, locals))
template_rendering = self.class.rendering(format: format, context: context).chdir(template_path)
locals = locals(template_rendering, input)
output = template_rendering.template(template_path, template_rendering.scope(config.scope, locals))

if layout?
output = renderer.template(layout_path, layout_scope(renderer, context, layout_locals(locals))) { output }
layout_rendering = self.class.rendering(format: format, context: context).chdir(layout_path)
output = layout_rendering.template(layout_path, layout_rendering.scope(config.scope, layout_locals(locals))) { output }
end

Rendered.new(output: output, locals: locals)
end

private

def locals(renderer, context, input)
def locals(rendering, input)
exposures.(input) do |value, exposure|
if exposure.decorate?
decorate_local(renderer, context, exposure.name, value, **exposure.options)
if exposure.decorate? && value
rendering.part(exposure.name, value, **exposure.options)
else
value
end
Expand All @@ -147,38 +149,6 @@ def layout_locals(locals)
def layout?
!!config.layout
end

def layout_scope(renderer, context, locals = EMPTY_LOCALS)
scope(renderer.chdir(layout_dir), context, locals)
end

def template_scope(renderer, context, locals)
scope(renderer.chdir(template_path), context, locals)
end

def scope(renderer, context, locals = EMPTY_LOCALS)
Scope.new(
renderer: renderer,
context: context,
locals: locals,
)
end

def decorate_local(renderer, context, name, value, **options)
if value
# Decorate truthy values only
part_builder.(
name: name,
value: value,
renderer: renderer,
context: context,
namespace: config.part_namespace,
**options,
)
else
value
end
end
end
end
end
15 changes: 0 additions & 15 deletions lib/dry/view/missing_renderer.rb

This file was deleted.

48 changes: 17 additions & 31 deletions lib/dry/view/part.rb
@@ -1,27 +1,22 @@
require 'dry-equalizer'
require 'dry/view/scope'
require 'dry/view/missing_renderer'

module Dry
module View
class Part
CONVENIENCE_METHODS = %i[
context
render
scope
value
].freeze

include Dry::Equalizer(:_name, :_value, :_part_builder, :_context, :_renderer)
include Dry::Equalizer(:_name, :_value, :_rendering)

attr_reader :_name

attr_reader :_value

attr_reader :_context

attr_reader :_renderer

attr_reader :_part_builder
attr_reader :_rendering

attr_reader :_decorated_attributes

Expand All @@ -37,17 +32,20 @@ def self.decorated_attributes
@decorated_attributes ||= {}
end

def initialize(name:, value:, part_builder: Dry::View::PartBuilder.new, renderer: MissingRenderer.new, context: nil)
def initialize(name:, value:, rendering:)
@_name = name
@_value = value
@_context = context
@_renderer = renderer
@_part_builder = part_builder
@_rendering = rendering

@_decorated_attributes = {}
end

def _render(partial_name, as: _name, **locals, &block)
_renderer.partial(partial_name, _render_scope(as, locals), &block)
_rendering.partial(partial_name, _rendering.scope({as => self}.merge(locals)), &block)
end

def _scope(scope_name = nil, **locals)
_rendering.scope(scope_name, {_name => self}.merge(locals))
end

def to_s
Expand All @@ -58,15 +56,17 @@ def new(klass = (self.class), name: (_name), value: (_value), **options)
klass.new(
name: name,
value: value,
context: _context,
renderer: _renderer,
part_builder: _part_builder,
rendering: _rendering,
**options,
)
end

private

def _context
_rendering.context
end

def method_missing(name, *args, &block)
if self.class.decorated_attributes.key?(name)
_resolve_decorated_attribute(name)
Expand All @@ -85,28 +85,14 @@ def respond_to_missing?(name, include_private = false)
d.key?(name) || c.include?(name) || _value.respond_to?(name, include_private) || super
end

def _render_scope(name, **locals)
Scope.new(
locals: locals.merge(name => self),
context: _context,
renderer: _renderer,
)
end

def _resolve_decorated_attribute(name)
_decorated_attributes.fetch(name) {
attribute = _value.__send__(name)

_decorated_attributes[name] =
if attribute
# Decorate truthy attributes only
_part_builder.(
name: name,
value: attribute,
renderer: _renderer,
context: _context,
**self.class.decorated_attributes[name],
)
_rendering.part(name, attribute, **self.class.decorated_attributes[name])
end
}
end
Expand Down
42 changes: 31 additions & 11 deletions lib/dry/view/part_builder.rb
@@ -1,40 +1,56 @@
require 'dry/inflector'
require 'dry/equalizer'
require_relative 'part'

module Dry
module View
class PartBuilder
include Dry::Equalizer(:namespace)

attr_reader :namespace
attr_reader :inflector
attr_reader :rendering

def initialize(namespace: nil, inflector: Dry::Inflector.new)
def initialize(namespace: nil, rendering: nil)
@namespace = namespace
@inflector = inflector
@rendering = rendering
end

def call(name:, value:, renderer:, context:, **options)
def for_rendering(rendering)
return self if rendering == self.rendering

self.class.new(namespace: namespace, rendering: rendering)
end

def rendering?
!!rendering
end

def call(name, value, **options)
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part

send(builder, name: name, value: value, renderer: renderer, context: context, **options)
send(builder, name, value, **options)
end

private

def build_part(name:, value:, renderer:, context:, **options)
def build_part(name, value, **options)
klass = part_class(name: name, **options)

klass.new(name: name, value: value, part_builder: self, renderer: renderer, context: context)
klass.new(
name: name,
value: value,
rendering: rendering,
)
end

def build_collection_part(name:, value:, renderer:, context:, **options)
def build_collection_part(name, value, **options)
collection_as = collection_options(name: name, **options)[:as]
item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)

arr = value.to_ary.map { |obj|
build_part(name: item_name, value: obj, renderer: renderer, context: context, **options.merge(as: item_as))
build_part(item_name, obj, **options.merge(as: item_as))
}

build_part(name: name, value: arr, renderer: renderer, context: context, **options.merge(as: collection_as))
build_part(name, arr, **options.merge(as: collection_as))
end

def collection_options(name:, **options)
Expand Down Expand Up @@ -93,6 +109,10 @@ def resolve_part_class(name:, fallback_class:)
fallback_class
end
end

def inflector
rendering.inflector
end
end
end
end

0 comments on commit 4acab57

Please sign in to comment.