Skip to content

Commit

Permalink
Merge pull request #89 from dry-rb/decorated-attributes-in-context
Browse files Browse the repository at this point in the history
Add base Context class with support for decorated attributes
  • Loading branch information
timriley committed Dec 10, 2018
2 parents 24063d5 + 43cda88 commit 4f71a6e
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 19 deletions.
59 changes: 59 additions & 0 deletions lib/dry/view/context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Dry
module View
class Context
attr_reader :_options, :_part_builder, :_renderer

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

def bind(part_builder:, renderer:)
self.class.new(
**_options.merge(
part_builder: part_builder,
renderer: renderer,
)
)
end

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

def self.decorate(*names, **options)
mod = DecoratedAttributes.new(names) do
names.each do |name|
define_method name do
attribute = super()

return attribute unless bound? || !attribute

_part_builder.(
name: name,
value: attribute,
renderer: _renderer,
context: self,
**options,
)
end
end
end

prepend mod
end

class DecoratedAttributes < Module
def initialize(names, &block)
@names = names
super(&block)
end

def inspect
%(#<#{self.class.name}#{@names.inspect}>)
end
end
end
end
end
4 changes: 3 additions & 1 deletion lib/dry/view/controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
require 'dry/equalizer'
require 'dry/inflector'

require_relative 'context'
require_relative 'exposures'
require_relative 'part_builder'
require_relative 'path'
Expand All @@ -15,7 +16,7 @@ class Controller
UndefinedTemplateError = Class.new(StandardError)

DEFAULT_LAYOUTS_DIR = 'layouts'.freeze
DEFAULT_CONTEXT = Object.new.freeze
DEFAULT_CONTEXT = Context.new
DEFAULT_RENDERER_OPTIONS = {default_encoding: 'utf-8'.freeze}.freeze
EMPTY_LOCALS = {}.freeze

Expand Down Expand Up @@ -112,6 +113,7 @@ 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)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
== assets.image_tag("hello.png")
.user
== user.image_tag
65 changes: 65 additions & 0 deletions spec/integration/context_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require "dry/view/context"
require "dry/view/controller"
require "dry/view/part"

RSpec.describe "Context" do
it "Provides decorated attributes for use in templates and parts" do
module Test
class Assets
def [](path)
"hashed/path/to/#{path}"
end
end

class Context < Dry::View::Context
attr_reader :assets
decorate :assets

def initialize(assets:, **options)
@assets = assets
super
end
end

module Parts
class Assets < Dry::View::Part
def image_tag(path)
<<~HTML
<img src="#{value[path]}">
HTML
end
end

class User < Dry::View::Part
def image_tag
value[:image_url] || context.assets.image_tag("default.png")
end
end
end
end

vc = Class.new(Dry::View::Controller) do
configure do |config|
config.paths = FIXTURES_PATH.join("integration/context")
config.template = "decorated_attributes"
config.part_namespace = Test::Parts
end

expose :user
end.new

context = Test::Context.new(assets: Test::Assets.new)

output = vc.(
user: {image_url: nil},
context: context,
).to_s

expect(output.gsub("\n", "")).to eq <<~HTML.gsub("\n", "")
<img src="hashed/path/to/hello.png">
<div class="user">
<img src="hashed/path/to/default.png">
</div>
HTML
end
end
13 changes: 12 additions & 1 deletion spec/integration/exposures_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
require 'dry/view/context'
require 'dry/view/controller'
require 'dry/view/part'

RSpec.describe 'exposures' do
let(:context) { Struct.new(:title, :assets).new('dry-view rocks!', -> input { "#{input}.jpg" }) }
let(:context) {
Class.new(Dry::View::Context) do
def title
'dry-view rocks!'
end

def assets
-> input { "#{input}.jpg" }
end
end.new
}

it 'uses exposures with blocks to build view locals' do
vc = Class.new(Dry::View::Controller) do
Expand Down
14 changes: 11 additions & 3 deletions spec/integration/view_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@
end
end

let(:context) do
Struct.new(:title, :assets).new('dry-view rocks!', -> input { "#{input}.jpg" })
end
let(:context) {
Class.new(Dry::View::Context) do
def title
'dry-view rocks!'
end

def assets
-> input { "#{input}.jpg" }
end
end.new
}

it 'renders within a layout and makes the provided context available everywhere' do
vc = vc_class.new
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
rescue LoadError; end

SPEC_ROOT = Pathname(__FILE__).dirname
FIXTURES_PATH = SPEC_ROOT.join("fixtures")

require 'erb'
require 'slim'
Expand Down
58 changes: 58 additions & 0 deletions spec/unit/context_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
require "dry/view/context"
require "dry/view/part"
require "dry/view/part_builder"

RSpec.describe Dry::View::Context do
let(:context_class) {
Class.new(Dry::View::Context) do
attr_reader :assets

decorate :assets, :invalid_attribute

def initialize(assets:, **options)
@assets = assets
super
end
end
}

let(:assets) { double(:assets) }
let(:renderer) { double(:renderer) }
let(:part_builder) { Dry::View::PartBuilder.new }

it "provides a helpful #inspect on the generated decorated attributes module" do
expect(context_class.ancestors[0].inspect).to eq "#<Dry::View::Context::DecoratedAttributes[:assets, :invalid_attribute]>"
end

context "unbound" do
subject(:context) { context_class.new(assets: assets) }

it { is_expected.not_to be_bound }

it "returns its attributes" do
expect(context.assets).to eql assets
end

it "raises NoMethodError when an invalid attribute is decorated" do
expect { context.invalid_attribute }.to raise_error(NoMethodError)
end
end

context "bound" do
subject(:context) {
context_class.new(assets: assets).
bind(renderer: renderer, part_builder: part_builder)
}

it { is_expected.to be_bound }

it "returns its assets decorated in view parts" do
expect(context.assets).to be_a Dry::View::Part
expect(context.assets.value).to eql assets
end

it "raises NoMethodError when an invalid attribute is decorated" do
expect { context.invalid_attribute }.to raise_error(NoMethodError)
end
end
end
38 changes: 24 additions & 14 deletions spec/unit/controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
}

let(:context) do
double(:page, title: 'Test')
Class.new(Dry::View::Context) do
def title
'Test'
end
end.new
end

describe '#call' do
Expand Down Expand Up @@ -52,24 +56,30 @@
end.new
}

subject(:context) {
Class.new do
def self.form(action:, &blk)
new(action, &blk)
end
before do
module Test
class Form
def initialize(action, &block)
@buf = eval('@__buf__', block.binding)

def initialize(action, &blk)
@buf = eval('@__buf__', blk.binding)
@buf << "<form action=\"#{action}\" method=\"post\">"
block.(self)
@buf << '</form>'
end

@buf << "<form action=\"#{action}\" method=\"post\">"
blk.(self)
@buf << '</form>'
def text(name)
"<input type=\"text\" name=\"#{name}\" />"
end
end
end
end

def text(name)
"<input type=\"text\" name=\"#{name}\" />"
subject(:context) {
Class.new(Dry::View::Context) do
def form(action:, &blk)
Test::Form.new(action, &blk)
end
end
end.new
}

it 'uses default encoding' do
Expand Down

0 comments on commit 4f71a6e

Please sign in to comment.