From 273a64058fde1bed833a65e45f6c1535676401f5 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Thu, 22 Nov 2018 21:41:03 +1100 Subject: [PATCH 01/10] Introduce customizable scopes Scopes provide a custom rendering context for a specific template or partial --- lib/dry/view/controller.rb | 20 ++- lib/dry/view/part.rb | 21 ++- lib/dry/view/part_builder.rb | 13 +- lib/dry/view/scope.rb | 41 ++++-- lib/dry/view/scope_builder.rb | 62 ++++++++ .../scopes/anonymous_scope.html.slim | 1 + .../custom_view_controller_scope.html.slim | 1 + .../named_scope_with_defaults.html.slim | 1 + ...named_scope_with_explicit_render.html.slim | 1 + ...named_scope_with_implicit_render.html.slim | 1 + .../scopes/scope_from_part.html.slim | 1 + .../scopes/shared/_greeting.html.slim | 1 + .../scopes/shared/_holler.html.slim | 1 + spec/integration/controller/locals_spec.rb | 1 - .../part/decorated_attributes_spec.rb | 11 +- spec/integration/scope_spec.rb | 138 ++++++++++++++++++ spec/unit/context_spec.rb | 4 +- spec/unit/part_builder_spec.rb | 22 ++- spec/unit/part_spec.rb | 39 +++-- spec/unit/scope_spec.rb | 20 ++- 20 files changed, 362 insertions(+), 38 deletions(-) create mode 100644 lib/dry/view/scope_builder.rb create mode 100644 spec/fixtures/integration/scopes/anonymous_scope.html.slim create mode 100644 spec/fixtures/integration/scopes/custom_view_controller_scope.html.slim create mode 100644 spec/fixtures/integration/scopes/named_scope_with_defaults.html.slim create mode 100644 spec/fixtures/integration/scopes/named_scope_with_explicit_render.html.slim create mode 100644 spec/fixtures/integration/scopes/named_scope_with_implicit_render.html.slim create mode 100644 spec/fixtures/integration/scopes/scope_from_part.html.slim create mode 100644 spec/fixtures/integration/scopes/shared/_greeting.html.slim create mode 100644 spec/fixtures/integration/scopes/shared/_holler.html.slim create mode 100644 spec/integration/scope_spec.rb diff --git a/lib/dry/view/controller.rb b/lib/dry/view/controller.rb index e07d8caf..9768cc10 100644 --- a/lib/dry/view/controller.rb +++ b/lib/dry/view/controller.rb @@ -8,7 +8,7 @@ require_relative 'path' require_relative 'rendered' require_relative 'renderer' -require_relative 'scope' +require_relative 'scope_builder' module Dry module View @@ -33,17 +33,23 @@ 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 :scope_builder attr_reader :exposures @@ -100,9 +106,14 @@ def initialize @layout_path = "#{layout_dir}/#{config.layout}" @template_path = config.template + @scope_builder = config.scope_builder.new( + namespace: config.scope_namespace, + inflector: config.inflector, + ) @part_builder = config.part_builder.new( namespace: config.part_namespace, inflector: config.inflector, + scope_builder: scope_builder, ) @exposures = self.class.exposures.bind(self) @@ -157,10 +168,11 @@ def template_scope(renderer, context, locals) end def scope(renderer, context, locals = EMPTY_LOCALS) - Scope.new( - renderer: renderer, - context: context, + scope_builder.( + name: config.scope, locals: locals, + context: context, + renderer: renderer, ) end diff --git a/lib/dry/view/part.rb b/lib/dry/view/part.rb index 9ed73d94..5d41f691 100644 --- a/lib/dry/view/part.rb +++ b/lib/dry/view/part.rb @@ -1,5 +1,4 @@ require 'dry-equalizer' -require 'dry/view/scope' require 'dry/view/missing_renderer' module Dry @@ -8,10 +7,11 @@ class Part CONVENIENCE_METHODS = %i[ context render + scope value ].freeze - include Dry::Equalizer(:_name, :_value, :_part_builder, :_context, :_renderer) + include Dry::Equalizer(:_name, :_value, :_context, :_renderer, :_part_builder, :_scope_builder) attr_reader :_name @@ -23,6 +23,8 @@ class Part attr_reader :_part_builder + attr_reader :_scope_builder + attr_reader :_decorated_attributes # @api public @@ -37,12 +39,13 @@ 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:, context: nil, renderer: MissingRenderer.new, part_builder:, scope_builder:) @_name = name @_value = value @_context = context @_renderer = renderer @_part_builder = part_builder + @_scope_builder = scope_builder @_decorated_attributes = {} end @@ -50,6 +53,15 @@ def _render(partial_name, as: _name, **locals, &block) _renderer.partial(partial_name, _render_scope(as, locals), &block) end + def _scope(name = nil, **locals) + _scope_builder.( + name: name, + locals: locals, + context: _context, + renderer: _renderer, + ) + end + def to_s _value.to_s end @@ -61,6 +73,7 @@ def new(klass = (self.class), name: (_name), value: (_value), **options) context: _context, renderer: _renderer, part_builder: _part_builder, + scope_builder: _scope_builder, **options, ) end @@ -86,7 +99,7 @@ def respond_to_missing?(name, include_private = false) end def _render_scope(name, **locals) - Scope.new( + _scope_builder.( locals: locals.merge(name => self), context: _context, renderer: _renderer, diff --git a/lib/dry/view/part_builder.rb b/lib/dry/view/part_builder.rb index fd56e467..92f94b33 100644 --- a/lib/dry/view/part_builder.rb +++ b/lib/dry/view/part_builder.rb @@ -6,10 +6,12 @@ module View class PartBuilder attr_reader :namespace attr_reader :inflector + attr_reader :scope_builder - def initialize(namespace: nil, inflector: Dry::Inflector.new) + def initialize(namespace: nil, inflector: Dry::Inflector.new, scope_builder:) @namespace = namespace @inflector = inflector + @scope_builder = scope_builder end def call(name:, value:, renderer:, context:, **options) @@ -23,7 +25,14 @@ def call(name:, value:, renderer:, context:, **options) def build_part(name:, value:, renderer:, context:, **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, + context: context, + renderer: renderer, + part_builder: self, + scope_builder: scope_builder, + ) end def build_collection_part(name:, value:, renderer:, context:, **options) diff --git a/lib/dry/view/scope.rb b/lib/dry/view/scope.rb index 54f2d0f8..33ef6dde 100644 --- a/lib/dry/view/scope.rb +++ b/lib/dry/view/scope.rb @@ -1,22 +1,40 @@ -require 'dry-equalizer' +require 'dry/equalizer' +require 'dry/core/constants' module Dry module View class Scope - include Dry::Equalizer(:_locals, :_context, :_renderer) + include Dry::Equalizer(:_name, :_locals, :_context, :_renderer, :_scope_builder) + attr_reader :_name attr_reader :_locals attr_reader :_context attr_reader :_renderer + attr_reader :_scope_builder - def initialize(renderer:, context: nil, locals: {}) + def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, context: nil, renderer:, scope_builder:) + @_name = name @_locals = locals @_context = context @_renderer = renderer + @_scope_builder = scope_builder end - def render(partial_name, **locals, &block) - _renderer.partial(partial_name, __render_scope(locals), &block) + def render(partial_name = nil, **locals, &block) + partial_name ||= _name + + raise ArgumentError, "+partial_name+ must be provided for unnamed scopes" unless partial_name + + _renderer.partial(partial_name, _render_scope(locals), &block) + end + + def scope(name = nil, **locals) + _scope_builder.( + name: name, + locals: locals, + context: _context, + renderer: _renderer, + ) end private @@ -31,11 +49,16 @@ def method_missing(name, *args, &block) end end - def __render_scope(**locals) - if locals.any? - self.class.new(renderer: _renderer, context: _context, locals: locals) - else + def _render_scope(**locals) + if locals.none? self + else + self.class.new( + locals: locals, + context: _context, + renderer: _renderer, + scope_builder: _scope_builder, + ) end end end diff --git a/lib/dry/view/scope_builder.rb b/lib/dry/view/scope_builder.rb new file mode 100644 index 00000000..834c8a18 --- /dev/null +++ b/lib/dry/view/scope_builder.rb @@ -0,0 +1,62 @@ +require 'dry/inflector' +require_relative 'scope' + +module Dry + module View + class ScopeBuilder + attr_reader :namespace + attr_reader :inflector + + def initialize(namespace: nil, inflector: Dry::Inflector.new) + @namespace = namespace + @inflector = inflector + end + + def call(name: nil, locals:, context:, renderer:) + scope_class(name: name).new( + name: name, + locals: locals, + context: context, + renderer: renderer, + scope_builder: self, + ) + end + + private + + DEFAULT_SCOPE_CLASS = Scope + + def scope_class(name: nil) + if name.nil? + DEFAULT_SCOPE_CLASS + elsif name.is_a?(Class) + name + else + resolve_scope_class(name: name) + end + end + + def resolve_scope_class(name:) + return fallback_class unless namespace + + name = inflector.camelize(name.to_s) + + # Give autoloaders a change to act + begin + klass = namespace.const_get(name) + rescue NameError + end + + if !klass && namespace.const_defined?(name, false) + klass = namespace.const_get(name) + end + + if klass && klass < Scope + klass + else + DEFAULT_SCOPE_CLASS + end + end + end + end +end diff --git a/spec/fixtures/integration/scopes/anonymous_scope.html.slim b/spec/fixtures/integration/scopes/anonymous_scope.html.slim new file mode 100644 index 00000000..b82a05d3 --- /dev/null +++ b/spec/fixtures/integration/scopes/anonymous_scope.html.slim @@ -0,0 +1 @@ +== scope(greeting: text).render(:greeting) diff --git a/spec/fixtures/integration/scopes/custom_view_controller_scope.html.slim b/spec/fixtures/integration/scopes/custom_view_controller_scope.html.slim new file mode 100644 index 00000000..05c215a7 --- /dev/null +++ b/spec/fixtures/integration/scopes/custom_view_controller_scope.html.slim @@ -0,0 +1 @@ +== hello diff --git a/spec/fixtures/integration/scopes/named_scope_with_defaults.html.slim b/spec/fixtures/integration/scopes/named_scope_with_defaults.html.slim new file mode 100644 index 00000000..65e5bff0 --- /dev/null +++ b/spec/fixtures/integration/scopes/named_scope_with_defaults.html.slim @@ -0,0 +1 @@ +== scope(:greeting).render diff --git a/spec/fixtures/integration/scopes/named_scope_with_explicit_render.html.slim b/spec/fixtures/integration/scopes/named_scope_with_explicit_render.html.slim new file mode 100644 index 00000000..a14150e6 --- /dev/null +++ b/spec/fixtures/integration/scopes/named_scope_with_explicit_render.html.slim @@ -0,0 +1 @@ +== scope(:greeting, greeting: text).render(:holler) diff --git a/spec/fixtures/integration/scopes/named_scope_with_implicit_render.html.slim b/spec/fixtures/integration/scopes/named_scope_with_implicit_render.html.slim new file mode 100644 index 00000000..106765e9 --- /dev/null +++ b/spec/fixtures/integration/scopes/named_scope_with_implicit_render.html.slim @@ -0,0 +1 @@ +== scope(:greeting, greeting: text).render diff --git a/spec/fixtures/integration/scopes/scope_from_part.html.slim b/spec/fixtures/integration/scopes/scope_from_part.html.slim new file mode 100644 index 00000000..7e4b9df2 --- /dev/null +++ b/spec/fixtures/integration/scopes/scope_from_part.html.slim @@ -0,0 +1 @@ +== message.greeting diff --git a/spec/fixtures/integration/scopes/shared/_greeting.html.slim b/spec/fixtures/integration/scopes/shared/_greeting.html.slim new file mode 100644 index 00000000..5a2d87af --- /dev/null +++ b/spec/fixtures/integration/scopes/shared/_greeting.html.slim @@ -0,0 +1 @@ +| Greeting: #{greeting} diff --git a/spec/fixtures/integration/scopes/shared/_holler.html.slim b/spec/fixtures/integration/scopes/shared/_holler.html.slim new file mode 100644 index 00000000..3256174f --- /dev/null +++ b/spec/fixtures/integration/scopes/shared/_holler.html.slim @@ -0,0 +1 @@ +| Holler: #{greeting} diff --git a/spec/integration/controller/locals_spec.rb b/spec/integration/controller/locals_spec.rb index 3cd42be2..1535f1b8 100644 --- a/spec/integration/controller/locals_spec.rb +++ b/spec/integration/controller/locals_spec.rb @@ -2,7 +2,6 @@ require "dry/view/part" RSpec.describe "locals" do - specify "locals are decorated with parts by default" do vc = Class.new(Dry::View::Controller) do configure do |config| diff --git a/spec/integration/part/decorated_attributes_spec.rb b/spec/integration/part/decorated_attributes_spec.rb index 4782532a..1c359fd0 100644 --- a/spec/integration/part/decorated_attributes_spec.rb +++ b/spec/integration/part/decorated_attributes_spec.rb @@ -1,4 +1,5 @@ require 'dry/core/inflector' +require 'dry/view/scope_builder' RSpec.describe 'Part / Decorated attributes' do let(:article_class) { @@ -53,9 +54,14 @@ def initialize(author:, body:) article_part_class.new( name: :article, value: article, + part_builder: part_builder, + scope_builder: scope_builder, ) } + let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } + let(:scope_builder) { Dry::View::ScopeBuilder.new } + describe 'decorating without options' do describe 'multiple declarations' do let(:article_part_class) { @@ -147,6 +153,7 @@ class CommentPart < Dry::View::Part name: :article, value: article, part_builder: part_builder, + scope_builder: scope_builder, ) } @@ -161,9 +168,11 @@ def part_class(name:, **options) super end end - end.new + end.new(scope_builder: scope_builder) } + let(:scope_builder) { Dry::View::ScopeBuilder.new } + before do module Test class AuthorPart < Dry::View::Part diff --git a/spec/integration/scope_spec.rb b/spec/integration/scope_spec.rb new file mode 100644 index 00000000..fa719a51 --- /dev/null +++ b/spec/integration/scope_spec.rb @@ -0,0 +1,138 @@ +require "dry/view/part" +require "dry/view/scope" + +RSpec.describe "Scopes" do + let(:base_vc) { + Class.new(Dry::View::Controller) do + configure do |config| + config.paths = FIXTURES_PATH.join("integration/scopes") + end + end + } + + specify "Custom scope for a view controller" do + module Test + class ControllerScope < Dry::View::Scope + def hello + "Hello #{_locals[:text]}!" + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.template = "custom_view_controller_scope" + config.scope = Test::ControllerScope + end + + expose :text + end.new + + expect(vc.(text: "world").to_s).to eq "Hello world!" + end + + specify "Rendering a partial via an anonymous scope" do + vc = Class.new(base_vc) do + configure do |config| + config.template = "anonymous_scope" + end + + expose :text + end.new + + expect(vc.(text: "Hello").to_s).to eq "Greeting: Hello" + end + + specify "Rendering a partial implicitly via a custom named scope" do + module Test::Scopes + class Greeting < Dry::View::Scope + def greeting + _locals[:greeting].upcase + "!" + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.scope_namespace = Test::Scopes + config.template = "named_scope_with_implicit_render" + end + + expose :text + end.new + + expect(vc.(text: "Hello").to_s).to eq "Greeting: HELLO!" + end + + specify "Rendering a partial explicitly via a custom named scope" do + module Test::Scopes + class Greeting < Dry::View::Scope + def greeting + _locals[:greeting].upcase + "!" + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.scope_namespace = Test::Scopes + config.template = "named_scope_with_explicit_render" + end + + expose :text + end.new + + expect(vc.(text: "Hello").to_s).to eq "Holler: HELLO!" + end + + specify "Custom named scope providing defaults for missing locals" do + module Test::Scopes + class Greeting < Dry::View::Scope + def greeting + _locals.fetch(:greeting) { "Howdy" } + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.scope_namespace = Test::Scopes + config.template = "named_scope_with_defaults" + end + + expose :text + end.new + + expect(vc.().to_s).to eq "Greeting: Howdy" + end + + specify "Creating a custom scope from a view part" do + module Test::Parts + class Message < Dry::View::Part + def greeting + scope(:greeting, greeting: value[:text]).render + end + end + end + + module Test::Scopes + class Greeting < Dry::View::Scope + def greeting + _locals[:greeting] + "!" + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.part_namespace = Test::Parts + config.scope_namespace = Test::Scopes + config.template = "scope_from_part" + end + + expose :message + end.new + + expect(vc.(message: {text: "Hello from a part"}).to_s).to eq "Greeting: Hello from a part!" + end +end diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index 0e1658b1..b52ef95a 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -1,6 +1,7 @@ require "dry/view/context" require "dry/view/part" require "dry/view/part_builder" +require "dry/view/scope_builder" RSpec.describe Dry::View::Context do let(:context_class) { @@ -18,7 +19,8 @@ def initialize(assets:, **options) let(:assets) { double(:assets) } let(:renderer) { double(:renderer) } - let(:part_builder) { Dry::View::PartBuilder.new } + let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } + let(:scope_builder) { Dry::View::ScopeBuilder.new } it "provides a helpful #inspect on the generated decorated attributes module" do expect(context_class.ancestors[0].inspect).to eq "#" diff --git a/spec/unit/part_builder_spec.rb b/spec/unit/part_builder_spec.rb index 1d4d3e47..d2f422a0 100644 --- a/spec/unit/part_builder_spec.rb +++ b/spec/unit/part_builder_spec.rb @@ -1,15 +1,31 @@ +require 'dry/view/scope_builder' + RSpec.describe Dry::View::PartBuilder do - subject(:part_builder) { described_class.new(namespace: namespace) } + subject(:part_builder) { + described_class.new( + namespace: namespace, + scope_builder: scope_builder, + ) + } let(:namespace) { nil } + let(:scope_builder) { Dry::View::ScopeBuilder.new } describe '#call' do - subject(:part) { part_builder.(name: name, value: value, renderer: renderer, context: context, **options) } + subject(:part) { + part_builder.( + name: name, + value: value, + context: context, + renderer: renderer, + **options, + ) + } let(:name) { :user } let(:value) { double(:user) } - let(:renderer) { double(:renderer) } let(:context) { double(:context) } + let(:renderer) { double(:renderer) } let(:options) { {} } shared_examples 'a view part' do diff --git a/spec/unit/part_spec.rb b/spec/unit/part_spec.rb index a5da4a6c..9d4a6d94 100644 --- a/spec/unit/part_spec.rb +++ b/spec/unit/part_spec.rb @@ -1,3 +1,5 @@ +require 'dry/view/scope_builder' + RSpec::Matchers.define :template_scope do |locals| match do |actual| locals == locals.map { |k,v| [k, actual.send(k)] }.to_h @@ -5,13 +7,24 @@ end RSpec.describe Dry::View::Part do - context 'with a renderer' do - subject(:part) { described_class.new(name: name, value: value, renderer: renderer, context: context) } + let(:name) { :user } + let(:value) { double(:value) } + let(:context) { double(:context) } + let(:renderer) { spy(:renderer) } + let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } + let(:scope_builder) { Dry::View::ScopeBuilder.new } - let(:name) { :user } - let(:value) { double(:value) } - let(:context) { double(:context) } - let(:renderer) { spy(:renderer) } + context 'with a renderer' do + subject(:part) { + described_class.new( + name: name, + value: value, + context: context, + renderer: renderer, + part_builder: part_builder, + scope_builder: scope_builder, + ) + } describe '#render' do it 'renders a partial with the part available in its scope' do @@ -85,11 +98,15 @@ end context 'without a renderer' do - subject(:part) { described_class.new(name: name, value: value, context: context) } - - let(:name) { :user } - let(:value) { double('value') } - let(:context) { double('context') } + subject(:part) { + described_class.new( + name: name, + value: value, + context: context, + part_builder: part_builder, + scope_builder: scope_builder, + ) + } describe '#initialize' do it 'can be initialized' do diff --git a/spec/unit/scope_spec.rb b/spec/unit/scope_spec.rb index 6fc93f98..768bc683 100644 --- a/spec/unit/scope_spec.rb +++ b/spec/unit/scope_spec.rb @@ -1,9 +1,19 @@ +require 'dry/view/scope_builder' + RSpec.describe Dry::View::Scope do - subject(:scope) { described_class.new(renderer: renderer, context: context, locals: locals) } + subject(:scope) { + described_class.new( + locals: locals, + context: context, + renderer: renderer, + scope_builder: scope_builder, + ) + } let(:locals) { {} } let(:context) { double(:context) } let(:renderer) { spy(:renderer) } + let(:scope_builder) { Dry::View::ScopeBuilder.new } describe '#render' do it 'renders a partial with itself as the scope' do @@ -12,9 +22,15 @@ end it 'renders a partial with provided locals' do - scope_with_locals = described_class.new(renderer: renderer, context: context, locals: {foo: 'bar'}) + scope_with_locals = described_class.new( + locals: {foo: 'bar'}, + context: context, + renderer: renderer, + scope_builder: scope_builder, + ) scope.render(:info, foo: 'bar') + expect(renderer).to have_received(:partial).with(:info, scope_with_locals) end end From abe945b3ba08aea9972e55beba7a6eacdbacff46 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Fri, 14 Dec 2018 16:03:52 +1100 Subject: [PATCH 02/10] Bring various objects used for rendering into single Rendering object This provides a unified, coherent API for rendering facilities (such as rendering templates or partials, as well as creating scopes and parts), and makes it easy to pass these interrelated objects around in one go, rather than having to manage them all individually. --- lib/dry/view/context.rb | 36 +++++------ lib/dry/view/controller.rb | 62 +++---------------- lib/dry/view/missing_renderer.rb | 15 ----- lib/dry/view/part.rb | 55 +++++----------- lib/dry/view/part_builder.rb | 39 +++++++----- lib/dry/view/rendering.rb | 51 +++++++++++++++ lib/dry/view/scope.rb | 32 ++++------ lib/dry/view/scope_builder.rb | 30 ++++++--- .../part/decorated_attributes_spec.rb | 51 +++++++-------- spec/unit/context_spec.rb | 24 ++++--- spec/unit/part_builder_spec.rb | 28 ++++----- spec/unit/part_spec.rb | 56 +++++------------ spec/unit/scope_spec.rb | 19 +++--- 13 files changed, 219 insertions(+), 279 deletions(-) delete mode 100644 lib/dry/view/missing_renderer.rb create mode 100644 lib/dry/view/rendering.rb diff --git a/lib/dry/view/context.rb b/lib/dry/view/context.rb index ab44a467..45cc9a2b 100644 --- a/lib/dry/view/context.rb +++ b/lib/dry/view/context.rb @@ -1,25 +1,21 @@ module Dry module View class Context - attr_reader :_options, :_part_builder, :_renderer + 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) @@ -28,15 +24,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 diff --git a/lib/dry/view/controller.rb b/lib/dry/view/controller.rb index 9768cc10..806a01b7 100644 --- a/lib/dry/view/controller.rb +++ b/lib/dry/view/controller.rb @@ -8,6 +8,7 @@ require_relative 'path' require_relative 'rendered' require_relative 'renderer' +require_relative 'rendering' require_relative 'scope_builder' module Dry @@ -48,9 +49,6 @@ class Controller attr_reader :layout_path attr_reader :template_path - attr_reader :part_builder - attr_reader :scope_builder - attr_reader :exposures # @api private @@ -106,16 +104,6 @@ def initialize @layout_path = "#{layout_dir}/#{config.layout}" @template_path = config.template - @scope_builder = config.scope_builder.new( - namespace: config.scope_namespace, - inflector: config.inflector, - ) - @part_builder = config.part_builder.new( - namespace: config.part_namespace, - inflector: config.inflector, - scope_builder: scope_builder, - ) - @exposures = self.class.exposures.bind(self) end @@ -123,15 +111,14 @@ def initialize 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) + rendering = Rendering.prepare(self.class.renderer(format), config, context).chdir(template_path) - locals = locals(renderer.chdir(template_path), context, input) + locals = locals(rendering, input) - output = renderer.template(template_path, template_scope(renderer, context, locals)) + output = rendering.template(template_path, rendering.scope(config.scope, locals)) if layout? - output = renderer.template(layout_path, layout_scope(renderer, context, layout_locals(locals))) { output } + output = rendering.template(layout_path, rendering.chdir(layout_dir).scope(config.scope, layout_locals(locals))) { output } end Rendered.new(output: output, locals: locals) @@ -139,10 +126,10 @@ def call(format: config.default_format, context: config.default_context, **input 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 @@ -158,39 +145,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_builder.( - name: config.scope, - locals: locals, - context: context, - renderer: renderer, - ) - 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 diff --git a/lib/dry/view/missing_renderer.rb b/lib/dry/view/missing_renderer.rb deleted file mode 100644 index 86baa866..00000000 --- a/lib/dry/view/missing_renderer.rb +++ /dev/null @@ -1,15 +0,0 @@ -module Dry - module View - class MissingRendererError < StandardError - def initialize(message = "No renderer provided") - super - end - end - - class MissingRenderer - def method_missing(name, *args, &block) - raise MissingRendererError - end - end - end -end diff --git a/lib/dry/view/part.rb b/lib/dry/view/part.rb index 5d41f691..e4b559fd 100644 --- a/lib/dry/view/part.rb +++ b/lib/dry/view/part.rb @@ -1,5 +1,4 @@ require 'dry-equalizer' -require 'dry/view/missing_renderer' module Dry module View @@ -11,19 +10,13 @@ class Part value ].freeze - include Dry::Equalizer(:_name, :_value, :_context, :_renderer, :_part_builder, :_scope_builder) + include Dry::Equalizer(:_name, :_value, :_rendering) attr_reader :_name attr_reader :_value - attr_reader :_context - - attr_reader :_renderer - - attr_reader :_part_builder - - attr_reader :_scope_builder + attr_reader :_rendering attr_reader :_decorated_attributes @@ -39,27 +32,20 @@ def self.decorated_attributes @decorated_attributes ||= {} end - def initialize(name:, value:, context: nil, renderer: MissingRenderer.new, part_builder:, scope_builder:) + def initialize(name:, value:, rendering:) @_name = name @_value = value - @_context = context - @_renderer = renderer - @_part_builder = part_builder - @_scope_builder = scope_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(name = nil, **locals) - _scope_builder.( - name: name, - locals: locals, - context: _context, - renderer: _renderer, - ) + def _scope(scope_name = nil, **locals) + _rendering.scope(scope_name, {_name => self}.merge(locals)) end def to_s @@ -70,16 +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, - scope_builder: _scope_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) @@ -98,14 +85,6 @@ 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_builder.( - locals: locals.merge(name => self), - context: _context, - renderer: _renderer, - ) - end - def _resolve_decorated_attribute(name) _decorated_attributes.fetch(name) { attribute = _value.__send__(name) @@ -113,13 +92,7 @@ def _resolve_decorated_attribute(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 diff --git a/lib/dry/view/part_builder.rb b/lib/dry/view/part_builder.rb index 92f94b33..e65f5395 100644 --- a/lib/dry/view/part_builder.rb +++ b/lib/dry/view/part_builder.rb @@ -5,45 +5,50 @@ module Dry module View class PartBuilder attr_reader :namespace - attr_reader :inflector - attr_reader :scope_builder + attr_reader :rendering - def initialize(namespace: nil, inflector: Dry::Inflector.new, scope_builder:) + def initialize(namespace: nil, rendering: nil) @namespace = namespace - @inflector = inflector - @scope_builder = scope_builder + @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, - context: context, - renderer: renderer, - part_builder: self, - scope_builder: scope_builder, + 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) @@ -102,6 +107,10 @@ def resolve_part_class(name:, fallback_class:) fallback_class end end + + def inflector + rendering.inflector + end end end end diff --git a/lib/dry/view/rendering.rb b/lib/dry/view/rendering.rb new file mode 100644 index 00000000..caa359c0 --- /dev/null +++ b/lib/dry/view/rendering.rb @@ -0,0 +1,51 @@ +module Dry + module View + class Rendering + def self.prepare(renderer, config, context) + new( + renderer: renderer, + inflector: config.inflector, + context: context, + scope_builder: config.scope_builder.new(namespace: config.scope_namespace), + part_builder: config.part_builder.new(namespace: config.part_namespace), + ) + end + + attr_reader :renderer, :inflector, :context, :scope_builder, :part_builder + + def initialize(renderer:, inflector:, context:, scope_builder:, part_builder:) + @renderer = renderer + @inflector = inflector + @context = context.for_rendering(self) + @scope_builder = scope_builder.for_rendering(self) + @part_builder = part_builder.for_rendering(self) + end + + def part(name, value, **options) + part_builder.(name, value, **options) + end + + def scope(name = nil, locals) + scope_builder.(name, locals) + end + + def template(name, scope, &block) + renderer.template(name, scope, &block) + end + + def partial(name, scope, &block) + renderer.partial(name, scope, &block) + end + + def chdir(dirname) + self.class.new( + renderer: renderer.chdir(dirname), + inflector: inflector, + context: context, + scope_builder: scope_builder, + part_builder: part_builder, + ) + end + end + end +end diff --git a/lib/dry/view/scope.rb b/lib/dry/view/scope.rb index 33ef6dde..0aef581e 100644 --- a/lib/dry/view/scope.rb +++ b/lib/dry/view/scope.rb @@ -4,37 +4,28 @@ module Dry module View class Scope - include Dry::Equalizer(:_name, :_locals, :_context, :_renderer, :_scope_builder) + include Dry::Equalizer(:_name, :_locals, :_rendering) attr_reader :_name attr_reader :_locals - attr_reader :_context - attr_reader :_renderer - attr_reader :_scope_builder + attr_reader :_rendering - def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, context: nil, renderer:, scope_builder:) + def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, rendering:) @_name = name @_locals = locals - @_context = context - @_renderer = renderer - @_scope_builder = scope_builder + @_rendering = rendering end def render(partial_name = nil, **locals, &block) - partial_name ||= _name + partial_name ||= _name unless _name.is_a?(Class) raise ArgumentError, "+partial_name+ must be provided for unnamed scopes" unless partial_name - _renderer.partial(partial_name, _render_scope(locals), &block) + _rendering.partial(partial_name, _render_scope(locals), &block) end def scope(name = nil, **locals) - _scope_builder.( - name: name, - locals: locals, - context: _context, - renderer: _renderer, - ) + _rendering.scope(name, locals) end private @@ -42,8 +33,8 @@ def scope(name = nil, **locals) def method_missing(name, *args, &block) if _locals.key?(name) _locals[name] - elsif _context.respond_to?(name) - _context.public_send(name, *args, &block) + elsif _rendering.context.respond_to?(name) + _rendering.context.public_send(name, *args, &block) else super end @@ -54,10 +45,9 @@ def _render_scope(**locals) self else self.class.new( + # FIXME: what about `name`? locals: locals, - context: _context, - renderer: _renderer, - scope_builder: _scope_builder, + rendering: _rendering, ) end end diff --git a/lib/dry/view/scope_builder.rb b/lib/dry/view/scope_builder.rb index 834c8a18..03a5c495 100644 --- a/lib/dry/view/scope_builder.rb +++ b/lib/dry/view/scope_builder.rb @@ -5,20 +5,28 @@ module Dry module View class ScopeBuilder 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: nil, locals:, context:, renderer:) - scope_class(name: name).new( + 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 = nil, locals) + scope_class(name).new( name: name, locals: locals, - context: context, - renderer: renderer, - scope_builder: self, + rendering: rendering, ) end @@ -26,7 +34,7 @@ def call(name: nil, locals:, context:, renderer:) DEFAULT_SCOPE_CLASS = Scope - def scope_class(name: nil) + def scope_class(name = nil) if name.nil? DEFAULT_SCOPE_CLASS elsif name.is_a?(Class) @@ -57,6 +65,10 @@ def resolve_scope_class(name:) DEFAULT_SCOPE_CLASS end end + + def inflector + rendering.inflector + end end end end diff --git a/spec/integration/part/decorated_attributes_spec.rb b/spec/integration/part/decorated_attributes_spec.rb index 1c359fd0..2e41878a 100644 --- a/spec/integration/part/decorated_attributes_spec.rb +++ b/spec/integration/part/decorated_attributes_spec.rb @@ -49,18 +49,26 @@ def initialize(author:, body:) ) } - describe 'using default part builder' do - subject(:article_part) { - article_part_class.new( - name: :article, - value: article, - part_builder: part_builder, - scope_builder: scope_builder, - ) - } + subject(:article_part) { + article_part_class.new( + name: :article, + value: article, + rendering: rendering, + ) + } - let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } - let(:scope_builder) { Dry::View::ScopeBuilder.new } + let(:rendering) { + Dry::View::Rendering.new( + renderer: Dry::View::Renderer.new([Dry::View::Path.new(FIXTURES_PATH)], format: :html), + inflector: Dry::Inflector.new, + context: Dry::View::Context.new, + scope_builder: Dry::View::ScopeBuilder.new, + part_builder: part_builder, + ) + } + + describe 'using default part builder' do + let(:part_builder) { Dry::View::PartBuilder.new } describe 'decorating without options' do describe 'multiple declarations' do @@ -142,19 +150,10 @@ class CommentPart < Dry::View::Part describe 'using custom part builder' do let(:article_part_class) { - Class.new(Dry::View::Part) do - decorate :author - decorate :comments - end - } - - subject(:article_part) { - article_part_class.new( - name: :article, - value: article, - part_builder: part_builder, - scope_builder: scope_builder, - ) + Class.new(Dry::View::Part) do + decorate :author + decorate :comments + end } let(:part_builder) { @@ -168,11 +167,9 @@ def part_class(name:, **options) super end end - end.new(scope_builder: scope_builder) + end.new } - let(:scope_builder) { Dry::View::ScopeBuilder.new } - before do module Test class AuthorPart < Dry::View::Part diff --git a/spec/unit/context_spec.rb b/spec/unit/context_spec.rb index b52ef95a..6b6683ba 100644 --- a/spec/unit/context_spec.rb +++ b/spec/unit/context_spec.rb @@ -18,9 +18,18 @@ def initialize(assets:, **options) } let(:assets) { double(:assets) } - let(:renderer) { double(:renderer) } - let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } - let(:scope_builder) { Dry::View::ScopeBuilder.new } + + let(:rendering) { + Dry::View::Rendering.new( + inflector: Dry::Inflector.new, + renderer: double(:renderer), + context: Dry::View::Context.new, + part_builder: Dry::View::PartBuilder.new, + scope_builder: Dry::View::ScopeBuilder.new, + ) + } + + # let(:rendering) { double(:rendering) } it "provides a helpful #inspect on the generated decorated attributes module" do expect(context_class.ancestors[0].inspect).to eq "#" @@ -29,7 +38,7 @@ def initialize(assets:, **options) context "unbound" do subject(:context) { context_class.new(assets: assets) } - it { is_expected.not_to be_bound } + it { is_expected.not_to be_rendering } it "returns its attributes" do expect(context.assets).to eql assets @@ -40,13 +49,12 @@ def initialize(assets:, **options) end end - context "bound" do + context "for rendering" do subject(:context) { - context_class.new(assets: assets). - bind(renderer: renderer, part_builder: part_builder) + context_class.new(assets: assets).for_rendering(rendering) } - it { is_expected.to be_bound } + it { is_expected.to be_rendering } it "returns its assets decorated in view parts" do expect(context.assets).to be_a Dry::View::Part diff --git a/spec/unit/part_builder_spec.rb b/spec/unit/part_builder_spec.rb index d2f422a0..dc4879eb 100644 --- a/spec/unit/part_builder_spec.rb +++ b/spec/unit/part_builder_spec.rb @@ -1,31 +1,27 @@ require 'dry/view/scope_builder' RSpec.describe Dry::View::PartBuilder do - subject(:part_builder) { - described_class.new( - namespace: namespace, - scope_builder: scope_builder, + subject(:part_builder) { rendering.part_builder } + + let(:rendering) { + Dry::View::Rendering.new( + renderer: Dry::View::Renderer.new([FIXTURES_PATH], format: :html), + inflector: Dry::Inflector.new, + context: Dry::View::Context.new, + scope_builder: Dry::View::ScopeBuilder.new, + part_builder: Dry::View::PartBuilder.new(namespace: namespace) ) } let(:namespace) { nil } - let(:scope_builder) { Dry::View::ScopeBuilder.new } describe '#call' do subject(:part) { - part_builder.( - name: name, - value: value, - context: context, - renderer: renderer, - **options, - ) + part_builder.(name, value, **options) } let(:name) { :user } let(:value) { double(:user) } - let(:context) { double(:context) } - let(:renderer) { double(:renderer) } let(:options) { {} } shared_examples 'a view part' do @@ -39,8 +35,8 @@ expect(part._value).to eq value end - it 'retains the part builder' do - expect(part._part_builder).to eql part_builder + it 'retains the rendering' do + expect(part._rendering).to eql rendering end end diff --git a/spec/unit/part_spec.rb b/spec/unit/part_spec.rb index 9d4a6d94..47035b1b 100644 --- a/spec/unit/part_spec.rb +++ b/spec/unit/part_spec.rb @@ -1,45 +1,48 @@ require 'dry/view/scope_builder' -RSpec::Matchers.define :template_scope do |locals| +RSpec::Matchers.define :scope do |locals| match do |actual| - locals == locals.map { |k,v| [k, actual.send(k)] }.to_h + locals == actual._locals end end RSpec.describe Dry::View::Part do let(:name) { :user } let(:value) { double(:value) } - let(:context) { double(:context) } + let(:rendering) { + Dry::View::Rendering.new( + renderer: renderer, + inflector: Dry::Inflector.new, + context: Dry::View::Context.new, + scope_builder: Dry::View::ScopeBuilder.new, + part_builder: Dry::View::ScopeBuilder.new, + ) + } let(:renderer) { spy(:renderer) } - let(:part_builder) { Dry::View::PartBuilder.new(scope_builder: scope_builder) } - let(:scope_builder) { Dry::View::ScopeBuilder.new } context 'with a renderer' do subject(:part) { described_class.new( name: name, value: value, - context: context, - renderer: renderer, - part_builder: part_builder, - scope_builder: scope_builder, + rendering: rendering, ) } describe '#render' do it 'renders a partial with the part available in its scope' do part.render(:info) - expect(renderer).to have_received(:partial).with(:info, template_scope(user: part)) + expect(renderer).to have_received(:partial).with(:info, scope(user: part)) end it 'allows the part to be made available on a different name' do part.render(:info, as: :admin) - expect(renderer).to have_received(:partial).with(:info, template_scope(admin: part)) + expect(renderer).to have_received(:partial).with(:info, scope(admin: part)) end it 'includes extra locals in the scope' do part.render(:info, extra_local: "hello") - expect(renderer).to have_received(:partial).with(:info, template_scope(user: part, extra_local: "hello")) + expect(renderer).to have_received(:partial).with(:info, scope(user: part, extra_local: "hello")) end end @@ -54,12 +57,9 @@ end describe '#new' do - it 'preserves renderer, and context, and part_builder' do + it 'preserves rendering' do new_part = part.new(value: 'new value') - - expect(new_part._renderer).to eql part._renderer - expect(new_part._context).to eql part._context - expect(new_part._part_builder).to eql part._part_builder + expect(new_part._rendering).to eql part._rendering end end @@ -96,26 +96,4 @@ end end end - - context 'without a renderer' do - subject(:part) { - described_class.new( - name: name, - value: value, - context: context, - part_builder: part_builder, - scope_builder: scope_builder, - ) - } - - describe '#initialize' do - it 'can be initialized' do - expect(part).to be_an_instance_of(Dry::View::Part) - end - - it 'raises an exception when render is called' do - expect { part.render(:info) }.to raise_error(Dry::View::MissingRendererError).with_message('No renderer provided') - end - end - end end diff --git a/spec/unit/scope_spec.rb b/spec/unit/scope_spec.rb index 768bc683..a759da31 100644 --- a/spec/unit/scope_spec.rb +++ b/spec/unit/scope_spec.rb @@ -4,41 +4,36 @@ subject(:scope) { described_class.new( locals: locals, - context: context, - renderer: renderer, - scope_builder: scope_builder, + rendering: rendering, ) } let(:locals) { {} } + let(:rendering) { spy(:rendering, context: context) } let(:context) { double(:context) } - let(:renderer) { spy(:renderer) } - let(:scope_builder) { Dry::View::ScopeBuilder.new } describe '#render' do it 'renders a partial with itself as the scope' do scope.render(:info) - expect(renderer).to have_received(:partial).with(:info, scope) + expect(rendering).to have_received(:partial).with(:info, scope) end it 'renders a partial with provided locals' do scope_with_locals = described_class.new( locals: {foo: 'bar'}, - context: context, - renderer: renderer, - scope_builder: scope_builder, + rendering: rendering, ) scope.render(:info, foo: 'bar') - expect(renderer).to have_received(:partial).with(:info, scope_with_locals) + expect(rendering).to have_received(:partial).with(:info, scope_with_locals) end end describe '#method_missing' do context 'matching locals' do let(:locals) { {greeting: 'hello from locals'} } - let(:context) { double('context', greeting: 'hello from context') } + let(:context) { double(:context, greeting: 'hello from context') } it 'returns a matching value from the locals, in favour of a matching method on the context' do expect(scope.greeting).to eq 'hello from locals' @@ -46,7 +41,7 @@ end context 'matching context' do - let(:context) { double('context', greeting: 'hello from context') } + let(:context) { double(:context, greeting: 'hello from context') } it 'calls the matching method on the context' do expect(scope.greeting).to eq 'hello from context' From 889ac7bae1b518e76a254abe934f1a9dc7f18df9 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Fri, 14 Dec 2018 15:55:38 +1100 Subject: [PATCH 03/10] Fix bug where renderer options were not preserved when chdir-ing --- lib/dry/view/renderer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dry/view/renderer.rb b/lib/dry/view/renderer.rb index 3a7aca98..c0e09d88 100644 --- a/lib/dry/view/renderer.rb +++ b/lib/dry/view/renderer.rb @@ -46,7 +46,7 @@ def render(path, scope, &block) def chdir(dirname) new_paths = paths.map { |path| path.chdir(dirname) } - self.class.new(new_paths, format: format) + self.class.new(new_paths, format: format, **options) end def lookup(name) From 611ffe58c9aef385d688f70fa81c96691480a426 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Fri, 14 Dec 2018 15:55:44 +1100 Subject: [PATCH 04/10] Tidy specs --- spec/unit/controller_spec.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/unit/controller_spec.rb b/spec/unit/controller_spec.rb index 4468f2e1..0be38b14 100644 --- a/spec/unit/controller_spec.rb +++ b/spec/unit/controller_spec.rb @@ -49,9 +49,7 @@ def title configure do |config| config.paths = SPEC_ROOT.join('fixtures/templates') config.template = 'controller_renderer_options' - config.renderer_options = { - outvar: '@__buf__' - } + config.renderer_options = {outvar: '@__buf__'} end end.new } @@ -82,10 +80,9 @@ def form(action:, &blk) end.new } - it 'uses default encoding' do - klass = Class.new(Dry::View::Controller) - expect(klass.config.renderer_options).to be_a Hash - expect(klass.config.renderer_options[:default_encoding]).to eql 'utf-8' + it 'merges configured options with default encoding' do + expect(controller.class.config.renderer_options[:outvar]).to eq '@__buf__' + expect(controller.class.config.renderer_options[:default_encoding]).to eq 'utf-8' end it 'are passed to renderer' do From 25817ac975ddd9f4f7ccc249e561e37da7823f26 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Mon, 17 Dec 2018 08:26:08 +1100 Subject: [PATCH 05/10] Allow implicit rendering of custom scopes created via class name --- lib/dry/view/scope.rb | 8 +++++-- ...named_scope_with_implicit_render.html.slim | 1 + spec/integration/scope_spec.rb | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/integration/scopes/class_named_scope_with_implicit_render.html.slim diff --git a/lib/dry/view/scope.rb b/lib/dry/view/scope.rb index 0aef581e..970b5046 100644 --- a/lib/dry/view/scope.rb +++ b/lib/dry/view/scope.rb @@ -17,9 +17,9 @@ def initialize(name: nil, locals: Dry::Core::Constants::EMPTY_HASH, rendering:) end def render(partial_name = nil, **locals, &block) - partial_name ||= _name unless _name.is_a?(Class) - + partial_name ||= _name raise ArgumentError, "+partial_name+ must be provided for unnamed scopes" unless partial_name + partial_name = _inflector.underscore(_inflector.demodulize(partial_name.to_s)) if partial_name.is_a?(Class) _rendering.partial(partial_name, _render_scope(locals), &block) end @@ -51,6 +51,10 @@ def _render_scope(**locals) ) end end + + def _inflector + _rendering.inflector + end end end end diff --git a/spec/fixtures/integration/scopes/class_named_scope_with_implicit_render.html.slim b/spec/fixtures/integration/scopes/class_named_scope_with_implicit_render.html.slim new file mode 100644 index 00000000..cc0b9809 --- /dev/null +++ b/spec/fixtures/integration/scopes/class_named_scope_with_implicit_render.html.slim @@ -0,0 +1 @@ +== scope(Test::Scopes::Greeting, greeting: text).render diff --git a/spec/integration/scope_spec.rb b/spec/integration/scope_spec.rb index fa719a51..1b946213 100644 --- a/spec/integration/scope_spec.rb +++ b/spec/integration/scope_spec.rb @@ -64,6 +64,28 @@ def greeting expect(vc.(text: "Hello").to_s).to eq "Greeting: HELLO!" end + specify "Rendering a partial implicitly via a custom named scope (provided via a class)" do + module Test::Scopes + class Greeting < Dry::View::Scope + def greeting + _locals[:greeting].upcase + "!" + end + end + end + + vc = Class.new(base_vc) do + configure do |config| + config.scope_namespace = Test::Scopes + config.template = "class_named_scope_with_implicit_render" + end + + expose :text + end.new + + expect(vc.(text: "Hello").to_s).to eq "Greeting: HELLO!" + end + + specify "Rendering a partial explicitly via a custom named scope" do module Test::Scopes class Greeting < Dry::View::Scope From 2da5c7ce51e900dc6a3a394e8ae6a93e64ba8a5a Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Mon, 17 Dec 2018 08:33:02 +1100 Subject: [PATCH 06/10] Add spec demonstrating error implicitly rendering unnamed scopes --- .../unnamed_named_scope_with_implicit_render.html.slim | 1 + spec/integration/scope_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 spec/fixtures/integration/scopes/unnamed_named_scope_with_implicit_render.html.slim diff --git a/spec/fixtures/integration/scopes/unnamed_named_scope_with_implicit_render.html.slim b/spec/fixtures/integration/scopes/unnamed_named_scope_with_implicit_render.html.slim new file mode 100644 index 00000000..16f8aa91 --- /dev/null +++ b/spec/fixtures/integration/scopes/unnamed_named_scope_with_implicit_render.html.slim @@ -0,0 +1 @@ +== scope(hello: "world").render diff --git a/spec/integration/scope_spec.rb b/spec/integration/scope_spec.rb index 1b946213..1d7782c1 100644 --- a/spec/integration/scope_spec.rb +++ b/spec/integration/scope_spec.rb @@ -85,6 +85,15 @@ def greeting expect(vc.(text: "Hello").to_s).to eq "Greeting: HELLO!" end + specify "Raising an error when an unnamed partial cannot be rendered implicitly" do + vc = Class.new(base_vc) do + configure do |config| + config.template = "unnamed_named_scope_with_implicit_render" + end + end.new + + expect { vc.().to_s }.to raise_error ArgumentError, "+partial_name+ must be provided for unnamed scopes" + end specify "Rendering a partial explicitly via a custom named scope" do module Test::Scopes From dbdd7eccf168642855e06afe50cb5ff4df9d2470 Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Mon, 17 Dec 2018 08:53:09 +1100 Subject: [PATCH 07/10] Move rendering builder into class method This will be helpful when we want to prepare a standalone rendering object for testing purposes --- lib/dry/view/controller.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/dry/view/controller.rb b/lib/dry/view/controller.rb index 806a01b7..eac70527 100644 --- a/lib/dry/view/controller.rb +++ b/lib/dry/view/controller.rb @@ -64,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) { @@ -111,7 +116,7 @@ def initialize def call(format: config.default_format, context: config.default_context, **input) raise UndefinedTemplateError, "no +template+ configured" unless template_path - rendering = Rendering.prepare(self.class.renderer(format), config, context).chdir(template_path) + rendering = self.class.rendering(format: format, context: context).chdir(template_path) locals = locals(rendering, input) From a5c1b6aecf31c1046e927808651ad704d4e2b2ab Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Mon, 17 Dec 2018 21:59:20 +1100 Subject: [PATCH 08/10] Add simple equality spec for Rendering --- lib/dry/view/context.rb | 4 ++++ lib/dry/view/part_builder.rb | 4 +++- lib/dry/view/rendering.rb | 4 ++++ lib/dry/view/scope_builder.rb | 4 +++- spec/unit/rendering_spec.rb | 26 ++++++++++++++++++++++++++ 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 spec/unit/rendering_spec.rb diff --git a/lib/dry/view/context.rb b/lib/dry/view/context.rb index 45cc9a2b..0577aa30 100644 --- a/lib/dry/view/context.rb +++ b/lib/dry/view/context.rb @@ -1,6 +1,10 @@ +require "dry/equalizer" + module Dry module View class Context + include Dry::Equalizer(:_options) + attr_reader :_rendering, :_options def initialize(rendering: nil, **options) diff --git a/lib/dry/view/part_builder.rb b/lib/dry/view/part_builder.rb index e65f5395..11d00577 100644 --- a/lib/dry/view/part_builder.rb +++ b/lib/dry/view/part_builder.rb @@ -1,9 +1,11 @@ -require 'dry/inflector' +require 'dry/equalizer' require_relative 'part' module Dry module View class PartBuilder + include Dry::Equalizer(:namespace) + attr_reader :namespace attr_reader :rendering diff --git a/lib/dry/view/rendering.rb b/lib/dry/view/rendering.rb index caa359c0..4ce5274b 100644 --- a/lib/dry/view/rendering.rb +++ b/lib/dry/view/rendering.rb @@ -1,3 +1,5 @@ +require "dry/equalizer" + module Dry module View class Rendering @@ -11,6 +13,8 @@ def self.prepare(renderer, config, context) ) end + include Dry::Equalizer(:renderer, :inflector, :context, :scope_builder, :part_builder) + attr_reader :renderer, :inflector, :context, :scope_builder, :part_builder def initialize(renderer:, inflector:, context:, scope_builder:, part_builder:) diff --git a/lib/dry/view/scope_builder.rb b/lib/dry/view/scope_builder.rb index 03a5c495..8f8372ed 100644 --- a/lib/dry/view/scope_builder.rb +++ b/lib/dry/view/scope_builder.rb @@ -1,9 +1,11 @@ -require 'dry/inflector' +require 'dry/equalizer' require_relative 'scope' module Dry module View class ScopeBuilder + include Dry::Equalizer(:namespace) + attr_reader :namespace attr_reader :rendering diff --git a/spec/unit/rendering_spec.rb b/spec/unit/rendering_spec.rb new file mode 100644 index 00000000..5097029e --- /dev/null +++ b/spec/unit/rendering_spec.rb @@ -0,0 +1,26 @@ +require "dry/view/rendering" + +require "dry/inflector" +require "dry/view/context" +require "dry/view/part_builder" +require "dry/view/scope_builder" + +RSpec.describe Dry::View::Rendering do + subject(:rendering) { described_class.new(**rendering_options) } + + let(:rendering_options) { + { + inflector: Dry::Inflector.new, + renderer: Dry::View::Renderer.new([Dry::View::Path.new(FIXTURES_PATH)], format: :html), + context: Dry::View::Context.new, + part_builder: Dry::View::PartBuilder.new, + scope_builder: Dry::View::ScopeBuilder.new, + } + } + + describe "#==" do + it "is equal when its options are equal" do + expect(rendering).to eq described_class.new(**rendering_options) + end + end +end From 69bde1b161aecc437bd4fb310676a499b2a691eb Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Mon, 17 Dec 2018 22:18:22 +1100 Subject: [PATCH 09/10] Remove bogus line --- lib/dry/view/scope_builder.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/dry/view/scope_builder.rb b/lib/dry/view/scope_builder.rb index 8f8372ed..dfc91b5d 100644 --- a/lib/dry/view/scope_builder.rb +++ b/lib/dry/view/scope_builder.rb @@ -47,8 +47,6 @@ def scope_class(name = nil) end def resolve_scope_class(name:) - return fallback_class unless namespace - name = inflector.camelize(name.to_s) # Give autoloaders a change to act From 06df9f03431f3c34621db8069a9356a689dce13b Mon Sep 17 00:00:00 2001 From: Tim Riley Date: Tue, 18 Dec 2018 21:48:40 +1100 Subject: [PATCH 10/10] Use properly chdir-ed renderings when generating template/layout scopes --- lib/dry/view/controller.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/dry/view/controller.rb b/lib/dry/view/controller.rb index eac70527..29f015b3 100644 --- a/lib/dry/view/controller.rb +++ b/lib/dry/view/controller.rb @@ -116,14 +116,13 @@ def initialize def call(format: config.default_format, context: config.default_context, **input) raise UndefinedTemplateError, "no +template+ configured" unless template_path - rendering = self.class.rendering(format: format, context: context).chdir(template_path) - - locals = locals(rendering, input) - - output = rendering.template(template_path, rendering.scope(config.scope, 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 = rendering.template(layout_path, rendering.chdir(layout_dir).scope(config.scope, 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)