dry-view 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4320e5c4ceacedaf135a9a59ab73bb5cd316ac2f
4
- data.tar.gz: 9ce48d97be8b9c84b3ac473171777783549725f1
3
+ metadata.gz: 350649d54605ecc359ddcfd199fe13fec79e4d47
4
+ data.tar.gz: 7655546ae79c8e42da84aa12df1698a0bdba7f24
5
5
  SHA512:
6
- metadata.gz: 622277285a6cb325c191ce400acd1cf0f200dfef10eeb6c87c5f6bb758c028be6a50b743ac664ff9c9de271c4795df4443ed3a19bcf9aa82c52431c2922957f0
7
- data.tar.gz: a1340d82a48e85e4a9208f139b9cc3d4c746c704ea7b3bd9e9eef98dc2b41e5010f10488b2bfe9b1b86af3115212a377bcfdf6f776a87ffce5585ed6f8c265d1
6
+ metadata.gz: 0c817743037231ab4455085be9521d0106d8781998fca1727d7c591183e98862349492567fc77ee1f6ea168feaef62055ce4fc6adf1cad881712a6b23c24b42a
7
+ data.tar.gz: dfe44787659b229484a65ff231d7afc5c61f6fb3a831e46e03026fefb3bd1f7a6be46e021a34cb02e9010e59a3838c174962bdf1284550d5a55bf74438d34577
@@ -1,3 +1,16 @@
1
+ # 0.3.0 / 2017-05-14
2
+
3
+ This release reintroduces view parts in a more helpful form. You can provide your own custom view part classes to encapsulate your view logic, as well as a decorator for custom, shared behavior arouund view part wrapping.
4
+
5
+ ### Changed
6
+
7
+ - [BREAKING] Partial rendering in templates requires an explicit `render` method call instead of method_missing behaviour usinig the partial's name (e.g. `<%= render :my_partial %>` instead of `<%= my_partial %>`)
8
+
9
+ ### Added
10
+
11
+ - Wrap all values passed to the template in `Dry::View::Part` objects
12
+ - Added a `decorator` config to `Dry::View::Controller`, with a default `Dry::View::Decorator` that wraps the exposure values in `Dry::View::Part` objects (as above). Provide your own part classes by passing an `:as` option to your exposures, e.g. `expose :user, as: MyApp::UserPart`
13
+
1
14
  # 0.2.2 / 2017-01-31
2
15
 
3
16
  ### Changed
data/Gemfile CHANGED
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
+ gem 'inflecto'
6
+
5
7
  group :tools do
6
8
  gem 'pry'
7
9
  end
@@ -22,6 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.required_ruby_version = '>= 2.1.0'
23
23
 
24
24
  spec.add_runtime_dependency "tilt", "~> 2.0"
25
+ spec.add_runtime_dependency "dry-core", "~> 0.2"
25
26
  spec.add_runtime_dependency "dry-configurable", "~> 0.1"
26
27
  spec.add_runtime_dependency "dry-equalizer", "~> 0.2"
27
28
 
@@ -4,6 +4,7 @@ require 'dry-equalizer'
4
4
  require 'dry/view/path'
5
5
  require 'dry/view/exposures'
6
6
  require 'dry/view/renderer'
7
+ require 'dry/view/decorator'
7
8
  require 'dry/view/scope'
8
9
 
9
10
  module Dry
@@ -19,9 +20,10 @@ module Dry
19
20
 
20
21
  setting :paths
21
22
  setting :layout, false
22
- setting :context, DEFAULT_CONTEXT
23
23
  setting :template
24
24
  setting :default_format, :html
25
+ setting :context, DEFAULT_CONTEXT
26
+ setting :decorator, Decorator.new
25
27
 
26
28
  attr_reader :config
27
29
  attr_reader :layout_dir
@@ -29,20 +31,24 @@ module Dry
29
31
  attr_reader :template_path
30
32
  attr_reader :exposures
31
33
 
34
+ # @api public
32
35
  def self.paths
33
36
  Array(config.paths).map { |path| Dry::View::Path.new(path) }
34
37
  end
35
38
 
39
+ # @api private
36
40
  def self.renderer(format)
37
41
  renderers.fetch(format) {
38
42
  renderers[format] = Renderer.new(paths, format: format)
39
43
  }
40
44
  end
41
45
 
46
+ # @api private
42
47
  def self.renderers
43
48
  @renderers ||= {}
44
49
  end
45
50
 
51
+ # @api public
46
52
  def self.expose(*names, **options, &block)
47
53
  if names.length == 1
48
54
  exposures.add(names.first, block, **options)
@@ -53,14 +59,17 @@ module Dry
53
59
  end
54
60
  end
55
61
 
56
- def self.private_expose(*names, &block)
57
- expose(*names, to_view: false, &block)
62
+ # @api public
63
+ def self.private_expose(*names, **options, &block)
64
+ expose(*names, **options.merge(private: true), &block)
58
65
  end
59
66
 
67
+ # @api private
60
68
  def self.exposures
61
69
  @exposures ||= Exposures.new
62
70
  end
63
71
 
72
+ # @api public
64
73
  def initialize
65
74
  @config = self.class.config
66
75
  @layout_dir = DEFAULT_LAYOUTS_DIR
@@ -69,18 +78,20 @@ module Dry
69
78
  @exposures = self.class.exposures.bind(self)
70
79
  end
71
80
 
72
- def call(format: config.default_format, **input)
81
+ # @api public
82
+ def call(format: config.default_format, context: config.context, **input)
73
83
  renderer = self.class.renderer(format)
74
84
 
75
- template_content = renderer.(template_path, template_scope(renderer, **input))
85
+ template_content = renderer.(template_path, template_scope(renderer, context, **input))
76
86
 
77
87
  return template_content unless layout?
78
88
 
79
- renderer.(layout_path, layout_scope(renderer, **input)) do
89
+ renderer.(layout_path, layout_scope(renderer, context)) do
80
90
  template_content
81
91
  end
82
92
  end
83
93
 
94
+ # @api public
84
95
  def locals(locals: EMPTY_LOCALS, **input)
85
96
  exposures.locals(input).merge(locals)
86
97
  end
@@ -91,16 +102,41 @@ module Dry
91
102
  !!config.layout
92
103
  end
93
104
 
94
- def layout_scope(renderer, context: config.context, **)
95
- scope(renderer.chdir(layout_dir), EMPTY_LOCALS, context)
105
+ def layout_scope(renderer, context)
106
+ scope(renderer.chdir(layout_dir), context)
107
+ end
108
+
109
+ def template_scope(renderer, context, **input)
110
+ scope(renderer.chdir(template_path), context, locals(**input))
96
111
  end
97
112
 
98
- def template_scope(renderer, context: config.context, **input)
99
- scope(renderer.chdir(template_path), locals(**input), context)
113
+ def scope(renderer, context, locals = EMPTY_LOCALS)
114
+ Scope.new(
115
+ renderer: renderer,
116
+ context: context,
117
+ locals: decorated_locals(renderer, context, locals)
118
+ )
100
119
  end
101
120
 
102
- def scope(renderer, locals, context)
103
- Scope.new(renderer, locals, context)
121
+ def decorated_locals(renderer, context, locals)
122
+ decorator = self.class.config.decorator
123
+
124
+ locals.each_with_object({}) { |(key, val), result|
125
+ # Decorate truthy values only
126
+ if val
127
+ options = exposures.key?(key) ? exposures[key].options : {}
128
+
129
+ val = decorator.(
130
+ key,
131
+ val,
132
+ renderer: renderer,
133
+ context: context,
134
+ **options
135
+ )
136
+ end
137
+
138
+ result[key] = val
139
+ }
104
140
  end
105
141
  end
106
142
  end
@@ -0,0 +1,45 @@
1
+ require 'dry/core/inflector'
2
+ require 'dry/view/part'
3
+
4
+ module Dry
5
+ module View
6
+ class Decorator
7
+ attr_reader :config
8
+
9
+ # @api public
10
+ def call(name, value, renderer:, context:, **options)
11
+ klass = part_class(name, value, **options)
12
+
13
+ if value.respond_to?(:to_ary)
14
+ singular_name = Dry::Core::Inflector.singularize(name).to_sym
15
+ singular_options = singularize_options(options)
16
+
17
+ arr = value.to_ary.map { |obj|
18
+ call(singular_name, obj, renderer: renderer, context: context, **singular_options)
19
+ }
20
+
21
+ klass.new(name: name, value: arr, renderer: renderer, context: context)
22
+ else
23
+ klass.new(name: name, value: value, renderer: renderer, context: context)
24
+ end
25
+ end
26
+
27
+ # @api public
28
+ def part_class(name, value, **options)
29
+ if options[:as].is_a?(Hash)
30
+ options[:as].keys.first
31
+ else
32
+ options.fetch(:as) { Part }
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @api private
39
+ def singularize_options(**options)
40
+ options[:as] = options[:as].values.first if options[:as].is_a?(Hash)
41
+ options
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,29 +1,35 @@
1
+ require 'dry-equalizer'
2
+
1
3
  module Dry
2
4
  module View
3
5
  class Exposure
6
+ include Dry::Equalizer(:name, :proc, :object, :options)
7
+
4
8
  SUPPORTED_PARAMETER_TYPES = [:req, :opt].freeze
5
9
 
6
10
  attr_reader :name
7
11
  attr_reader :proc
8
12
  attr_reader :object
9
- attr_reader :to_view
13
+ attr_reader :options
10
14
 
11
- def initialize(name, proc = nil, object = nil, to_view: true)
15
+ def initialize(name, proc = nil, object = nil, **options)
12
16
  @name = name
13
17
  @proc = prepare_proc(proc, object)
14
18
  @object = object
15
- @to_view = to_view
19
+ @options = options
16
20
  end
17
21
 
18
22
  def bind(obj)
19
- self.class.new(name, proc, obj, to_view: to_view)
23
+ self.class.new(name, proc, obj, **options)
20
24
  end
21
25
 
22
26
  def dependencies
23
27
  proc ? proc.parameters.map(&:last) : []
24
28
  end
25
29
 
26
- alias_method :to_view?, :to_view
30
+ def private?
31
+ options.fetch(:private) { false }
32
+ end
27
33
 
28
34
  def call(input, locals = {})
29
35
  return input[name] unless proc
@@ -12,6 +12,10 @@ module Dry
12
12
  @exposures = exposures
13
13
  end
14
14
 
15
+ def key?(name)
16
+ exposures.key?(name)
17
+ end
18
+
15
19
  def [](name)
16
20
  exposures[name]
17
21
  end
@@ -21,9 +25,9 @@ module Dry
21
25
  end
22
26
 
23
27
  def bind(obj)
24
- bound_exposures = Hash[exposures.map { |name, exposure|
25
- [name, exposure.bind(obj)]
26
- }]
28
+ bound_exposures = exposures.each_with_object({}) { |(name, exposure), memo|
29
+ memo[name] = exposure.bind(obj)
30
+ }
27
31
 
28
32
  self.class.new(bound_exposures)
29
33
  end
@@ -32,7 +36,7 @@ module Dry
32
36
  tsort.each_with_object({}) { |name, memo|
33
37
  memo[name] = self[name].(input, memo) if exposures.key?(name)
34
38
  }.each_with_object({}) { |(name, val), memo|
35
- memo[name] = val if self[name].to_view?
39
+ memo[name] = val unless self[name].private?
36
40
  }
37
41
  end
38
42
 
@@ -0,0 +1,67 @@
1
+ require 'dry-equalizer'
2
+ require 'dry/view/scope'
3
+
4
+ module Dry
5
+ module View
6
+ class Part
7
+ CONVENIENCE_METHODS = %i[
8
+ context
9
+ render
10
+ value
11
+ ].freeze
12
+
13
+ include Dry::Equalizer(:_name, :_value, :_context, :_renderer)
14
+
15
+ attr_reader :_name
16
+
17
+ attr_reader :_value
18
+
19
+ attr_reader :_context
20
+
21
+ attr_reader :_renderer
22
+
23
+ def initialize(name:, value:, renderer:, context: nil)
24
+ @_name = name
25
+ @_value = value
26
+ @_context = context
27
+ @_renderer = renderer
28
+ end
29
+
30
+ def _render(partial_name, as: _name, **locals, &block)
31
+ _renderer.render(
32
+ _partial(partial_name),
33
+ _render_scope(as, **locals),
34
+ &block
35
+ )
36
+ end
37
+
38
+ def to_s
39
+ _value.to_s
40
+ end
41
+
42
+ private
43
+
44
+ def method_missing(name, *args, &block)
45
+ if _value.respond_to?(name)
46
+ _value.public_send(name, *args, &block)
47
+ elsif CONVENIENCE_METHODS.include?(name)
48
+ __send__(:"_#{name}", *args, &block)
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ def _partial(name)
55
+ _renderer.lookup("_#{name}")
56
+ end
57
+
58
+ def _render_scope(name, **locals)
59
+ Scope.new(
60
+ locals: locals.merge(name => self),
61
+ context: _context,
62
+ renderer: _renderer,
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
@@ -3,51 +3,47 @@ require 'dry-equalizer'
3
3
  module Dry
4
4
  module View
5
5
  class Scope
6
- include Dry::Equalizer(:_renderer, :_data)
6
+ include Dry::Equalizer(:_locals, :_context, :_renderer)
7
7
 
8
- attr_reader :_renderer
9
- attr_reader :_data
8
+ attr_reader :_locals
10
9
  attr_reader :_context
10
+ attr_reader :_renderer
11
11
 
12
- def initialize(renderer, data, context = nil)
13
- @_renderer = renderer
14
- @_data = data.to_hash
12
+ def initialize(renderer:, context: nil, locals: {})
13
+ @_locals = locals
15
14
  @_context = context
15
+ @_renderer = renderer
16
16
  end
17
17
 
18
- def respond_to_missing?(name, include_private = false)
19
- _template?(name) || _data.key?(name) || _context.respond_to?(name)
18
+ def render(partial_name, **locals, &block)
19
+ _renderer.render(
20
+ __partial(partial_name),
21
+ __render_scope(**locals),
22
+ &block
23
+ )
20
24
  end
21
25
 
22
26
  private
23
27
 
24
28
  def method_missing(name, *args, &block)
25
- if _data.key?(name)
26
- _data[name]
29
+ if _locals.key?(name)
30
+ _locals[name]
27
31
  elsif _context.respond_to?(name)
28
32
  _context.public_send(name, *args, &block)
29
- elsif (template_path = _template?(name))
30
- _render(template_path, *args, &block)
31
33
  else
32
34
  super
33
35
  end
34
36
  end
35
37
 
36
- def _template?(name)
38
+ def __partial(name)
37
39
  _renderer.lookup("_#{name}")
38
40
  end
39
41
 
40
- def _render(path, *args, &block)
41
- _renderer.render(path, _render_args(*args), &block)
42
- end
43
-
44
- def _render_args(*args)
45
- if args.empty?
46
- self
47
- elsif args.length == 1 && args.first.respond_to?(:to_hash)
48
- self.class.new(_renderer, args.first, _context)
42
+ def __render_scope(**locals)
43
+ if locals.any?
44
+ self.class.new(renderer: _renderer, context: _context, locals: locals)
49
45
  else
50
- raise ArgumentError, "render argument must be a Hash"
46
+ self
51
47
  end
52
48
  end
53
49
  end
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module View
3
- VERSION = '0.2.2'.freeze
3
+ VERSION = '0.3.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1,4 @@
1
+ - customs.each do |custom|
2
+ p = custom
3
+ p = custom
4
+ p = ordinary
@@ -1,3 +1,3 @@
1
1
  .users
2
2
  - users.each do |user|
3
- == box user: user, label: "Nombre"
3
+ == user.render :box, label: "Nombre"
@@ -1,5 +1,5 @@
1
1
  .users
2
- == index_table do
3
- == tbody
2
+ == render :index_table do
3
+ == render :tbody
4
4
 
5
5
  img src=assets["mindblown"]
@@ -1,5 +1,5 @@
1
1
  tbody
2
2
  - users.each do |user|
3
- == row do
3
+ == render :row do
4
4
  td = user[:name]
5
5
  td = user[:email]
@@ -1,5 +1,5 @@
1
1
  h1 OVERRIDE
2
2
 
3
3
  .users
4
- == index_table do
5
- == tbody
4
+ == render :index_table do
5
+ == render :tbody
@@ -0,0 +1,80 @@
1
+ RSpec.describe 'decorator' do
2
+ before do
3
+ module Test
4
+ class CustomPart < Dry::View::Part
5
+ def to_s
6
+ "Custom part wrapping #{_value}"
7
+ end
8
+ end
9
+
10
+ class CustomArrayPart < Dry::View::Part
11
+ def each(&block)
12
+ (_value * 2).each(&block)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ describe 'default decorator' do
19
+ it 'supports wrapping array memebers in custom part classes provided to exposure :as option' do
20
+ vc = Class.new(Dry::View::Controller) do
21
+ configure do |config|
22
+ config.paths = SPEC_ROOT.join('fixtures/templates')
23
+ config.layout = nil
24
+ config.template = 'decorated_parts'
25
+ end
26
+
27
+ expose :customs, as: Test::CustomPart
28
+ expose :custom, as: Test::CustomPart
29
+ expose :ordinary
30
+ end.new
31
+
32
+ expect(vc.(customs: ['many things'], custom: 'custom thing', ordinary: 'ordinary thing')).to eql(
33
+ '<p>Custom part wrapping many things</p><p>Custom part wrapping custom thing</p><p>ordinary thing</p>'
34
+ )
35
+ end
36
+
37
+ it 'supports wrapping an array and its members in custom part classes provided to exposure :as option as a hash' do
38
+ vc = Class.new(Dry::View::Controller) do
39
+ configure do |config|
40
+ config.paths = SPEC_ROOT.join('fixtures/templates')
41
+ config.layout = nil
42
+ config.template = 'decorated_parts'
43
+ end
44
+
45
+ expose :customs, as: {Test::CustomArrayPart => Test::CustomPart}
46
+ expose :custom, as: Test::CustomPart
47
+ expose :ordinary
48
+ end.new
49
+
50
+ expect(vc.(customs: ['many things'], custom: 'custom thing', ordinary: 'ordinary thing')).to eql(
51
+ '<p>Custom part wrapping many things</p><p>Custom part wrapping many things</p><p>Custom part wrapping custom thing</p><p>ordinary thing</p>'
52
+ )
53
+ end
54
+ end
55
+
56
+ describe 'custom decorator and part classes' do
57
+ it 'supports wrapping in custom parts based on exposure names' do
58
+ decorator = Class.new(Dry::View::Decorator) do
59
+ def part_class(name, value, **options)
60
+ name == :custom ? Test::CustomPart : super
61
+ end
62
+ end.new
63
+
64
+ vc = Class.new(Dry::View::Controller) do
65
+ configure do |config|
66
+ config.decorator = decorator
67
+ config.paths = SPEC_ROOT.join('fixtures/templates')
68
+ config.layout = nil
69
+ config.template = 'decorated_parts'
70
+ end
71
+
72
+ expose :customs, :custom, :ordinary
73
+ end.new
74
+
75
+ expect(vc.(customs: ['many things'], custom: 'custom thing', ordinary: 'ordinary thing')).to eql(
76
+ '<p>Custom part wrapping many things</p><p>Custom part wrapping custom thing</p><p>ordinary thing</p>'
77
+ )
78
+ end
79
+ end
80
+ end
@@ -20,11 +20,21 @@ Tilt.register 'erb', Tilt::ERBTemplate
20
20
 
21
21
  require 'dry-view'
22
22
 
23
+ module Test
24
+ def self.remove_constants
25
+ constants.each(&method(:remove_const))
26
+ end
27
+ end
28
+
23
29
  RSpec.configure do |config|
24
30
  config.disable_monkey_patching!
25
31
 
26
32
  config.order = :random
27
33
  Kernel.srand config.seed
34
+
35
+ config.after do
36
+ Test.remove_constants
37
+ end
28
38
  end
29
39
 
30
40
  RSpec::Matchers.define :part_including do |data|
@@ -0,0 +1,61 @@
1
+ RSpec.describe Dry::View::Decorator do
2
+ subject(:decorator) { described_class.new }
3
+
4
+ describe '#call' do
5
+ let(:value) { double('value') }
6
+ let(:renderer) { double('renderer') }
7
+ let(:context) { double('context') }
8
+ let(:options) { {} }
9
+
10
+ describe 'returning a part value' do
11
+ subject(:part) { decorator.('user', value, renderer: renderer, context: context, **options) }
12
+
13
+ context 'no options provided' do
14
+ it 'returns a Part' do
15
+ expect(part).to be_a Dry::View::Part
16
+ end
17
+
18
+ it 'wraps the value' do
19
+ expect(part._value).to eq value
20
+ end
21
+ end
22
+
23
+ context 'part class provided via `:as` option' do
24
+ let(:options) { {as: Test::CustomPart} }
25
+
26
+ before do
27
+ module Test
28
+ CustomPart = Class.new(Dry::View::Part)
29
+ end
30
+ end
31
+
32
+ it 'returns an instance of the provided class' do
33
+ expect(part).to be_a Test::CustomPart
34
+ end
35
+
36
+ it 'wraps the value' do
37
+ expect(part._value).to eq value
38
+ end
39
+ end
40
+
41
+ context 'value is an array' do
42
+ let(:child_a) { double('child a') }
43
+ let(:child_b) { double('child a') }
44
+ let(:value) { [child_a, child_b] }
45
+
46
+ it 'returns a part wrapping the array' do
47
+ expect(part).to be_a Dry::View::Part
48
+ expect(part._value).to be_an Array
49
+ end
50
+
51
+ it 'wraps the elements within the array' do
52
+ expect(part[0]).to be_a Dry::View::Part
53
+ expect(part[0]._value).to eq child_a
54
+
55
+ expect(part[1]).to be_a Dry::View::Part
56
+ expect(part[1]._value).to eq child_b
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -33,13 +33,13 @@ RSpec.describe Dry::View::Exposure do
33
33
  end
34
34
  end
35
35
 
36
- describe "#to_view" do
37
- it "is true by default" do
38
- expect(exposure.to_view).to be true
36
+ describe "#private?" do
37
+ it "is false by default" do
38
+ expect(exposure).not_to be_private
39
39
  end
40
40
 
41
- it "can be set to false on initialization" do
42
- expect(described_class.new(:hello, to_view: false).to_view).to be false
41
+ it "can be set on initialization" do
42
+ expect(described_class.new(:hello, private: true)).to be_private
43
43
  end
44
44
  end
45
45
  end
@@ -54,7 +54,7 @@ RSpec.describe Dry::View::Exposures do
54
54
  end
55
55
 
56
56
  it "does not return any values from private exposures" do
57
- exposures.add(:hidden, -> input { "shh" }, to_view: false)
57
+ exposures.add(:hidden, -> input { "shh" }, private: true)
58
58
 
59
59
  expect(locals).to include(:greeting, :farewell)
60
60
  expect(locals).not_to include(:hidden)
@@ -0,0 +1,65 @@
1
+ RSpec::Matchers.define :template_scope do |locals|
2
+ match do |actual|
3
+ locals == locals.map { |k,v| [k, actual.send(k)] }.to_h
4
+ end
5
+ end
6
+
7
+ RSpec.describe Dry::View::Part do
8
+ subject(:part) { described_class.new(name: name, value: value, renderer: renderer, context: context) }
9
+
10
+ let(:name) { :user }
11
+ let(:value) { double('value') }
12
+ let(:context) { double('context') }
13
+ let(:renderer) { double('renderer') }
14
+
15
+ describe '#render' do
16
+ before do
17
+ allow(renderer).to receive(:lookup).with('_info').and_return '_info.html.erb'
18
+ allow(renderer).to receive(:render)
19
+ end
20
+
21
+ it 'renders a partial with the part available in its scope' do
22
+ part.render(:info)
23
+ expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(user: part))
24
+ end
25
+
26
+ it 'allows the part to be made available on a different name' do
27
+ part.render(:info, as: :admin)
28
+ expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(admin: part))
29
+ end
30
+
31
+ it 'includes extra locals in the scope' do
32
+ part.render(:info, extra_local: "hello")
33
+ expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(user: part, extra_local: "hello"))
34
+ end
35
+ end
36
+
37
+ describe '#to_s' do
38
+ before do
39
+ allow(value).to receive(:to_s).and_return 'to_s on the value'
40
+ end
41
+
42
+ it 'delegates to the wrapped value' do
43
+ expect(part.to_s).to eq 'to_s on the value'
44
+ end
45
+ end
46
+
47
+ describe '#method_missing' do
48
+ let(:value) { double(greeting: 'hello from value') }
49
+
50
+ it 'calls a matching method on the value' do
51
+ expect(part.greeting).to eq 'hello from value'
52
+ end
53
+
54
+ it 'forwards all arguments to the method' do
55
+ blk = -> { }
56
+ part.greeting 'args', &blk
57
+
58
+ expect(value).to have_received(:greeting).with('args', &blk)
59
+ end
60
+
61
+ it 'raises an error if no metho matches' do
62
+ expect { part.farewell }.to raise_error(NoMethodError)
63
+ end
64
+ end
65
+ end
@@ -1,97 +1,57 @@
1
- require 'dry/view/scope'
2
-
3
1
  RSpec.describe Dry::View::Scope do
4
- subject(:scope) {
5
- described_class.new(renderer, data, context)
6
- }
2
+ subject(:scope) { described_class.new(renderer: renderer, context: context, locals: locals) }
7
3
 
8
- let(:renderer) { double("renderer") }
9
- let(:data) { {} }
10
- let(:context) { Object.new }
4
+ let(:locals) { {} }
5
+ let(:context) { double('context') }
6
+ let(:renderer) { double('renderer') }
11
7
 
12
- describe "missing method behavior" do
8
+ describe '#render' do
13
9
  before do
14
- allow(renderer).to receive(:lookup).and_return false
10
+ allow(renderer).to receive(:lookup).with('_info').and_return '_info.html.erb'
15
11
  allow(renderer).to receive(:render)
16
12
  end
17
13
 
18
- describe "accessing data" do
19
- let(:data) { {user_name: "Jane Doe", current_user: "data's current_user"} }
20
- let(:context) {
21
- Class.new do
22
- def current_user
23
- "context's current_user"
24
- end
25
- end.new
26
- }
27
-
28
- before do
29
- allow(renderer).to receive(:lookup).with('_current_user').and_return '_current_user.html.slim'
30
- end
31
-
32
- it "returns matching scope data" do
33
- expect(scope.user_name).to eq "Jane Doe"
34
- end
35
-
36
- it "raises an error when no data matches" do
37
- expect { scope.missing }.to raise_error(NoMethodError)
38
- end
39
-
40
- it "returns data in favour of both context methods and partials" do
41
- expect(scope.current_user).to eq "data's current_user"
42
- end
14
+ it 'renders a partial with itself as the scope' do
15
+ scope.render(:info)
16
+ expect(renderer).to have_received(:render).with('_info.html.erb', scope)
43
17
  end
44
18
 
45
- describe "accessing context" do
46
- let(:context) {
47
- Class.new do
48
- def current_user
49
- "context's current_user"
50
- end
51
-
52
- def asset(name)
53
- "#{name}.jpg"
54
- end
55
- end.new
56
- }
19
+ it 'renders a partial with provided locals' do
20
+ scope_with_locals = described_class.new(renderer: renderer, context: context, locals: {foo: 'bar'})
57
21
 
58
- before do
59
- allow(renderer).to receive(:lookup).with('_current_user').and_return '_current_user.html.slim'
60
- end
22
+ scope.render(:info, foo: 'bar')
23
+ expect(renderer).to have_received(:render).with('_info.html.erb', scope_with_locals)
24
+ end
25
+ end
61
26
 
62
- it "forwards to matching methods on the context in favour of partials" do
63
- expect(scope.current_user).to eq "context's current_user"
64
- end
27
+ describe '#method_missing' do
28
+ context 'matching locals' do
29
+ let(:locals) { {greeting: 'hello from locals'} }
30
+ let(:context) { double('context', greeting: 'hello from context') }
65
31
 
66
- it "allows arguments to be passed to those methods as normal" do
67
- expect(scope.asset("mindblown")).to eq "mindblown.jpg"
68
- end
69
-
70
- it "raises an error when no method matches" do
71
- expect { scope.missing }.to raise_error(NoMethodError)
32
+ it 'returns a matching value from the locals, in favour of a matching method on the context' do
33
+ expect(scope.greeting).to eq 'hello from locals'
72
34
  end
73
35
  end
74
36
 
75
- describe "rendering" do
76
- before do
77
- allow(renderer).to receive(:lookup).with('_list').and_return '_list.html.slim'
78
- end
79
-
80
- it "renders a matching partial using the existing scope" do
81
- scope.list
37
+ context 'matching context' do
38
+ let(:context) { double('context', greeting: 'hello from context') }
82
39
 
83
- expect(renderer).to have_received(:render).with('_list.html.slim', scope)
40
+ it 'calls the matching method on the context' do
41
+ expect(scope.greeting).to eq 'hello from context'
84
42
  end
85
43
 
86
- it "renders a matching partial using a scope based on arguments passed" do
87
- scope.list(something: 'else')
44
+ it 'forwards all arguments to the method' do
45
+ blk = -> { }
46
+ scope.greeting 'args', &blk
88
47
 
89
- expect(renderer).to have_received(:render)
90
- .with('_list.html.slim', described_class.new(renderer, something: 'else'))
48
+ expect(context).to have_received(:greeting).with('args', &blk)
91
49
  end
50
+ end
92
51
 
93
- it "raises an error if arguments passed are not a hash" do
94
- expect { scope.list('hi') }.to raise_error(ArgumentError)
52
+ describe 'no matches' do
53
+ it 'raises an error' do
54
+ expect { scope.greeting }.to raise_error(NoMethodError)
95
55
  end
96
56
  end
97
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-01-31 00:00:00.000000000 Z
12
+ date: 2017-05-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tilt
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
27
  version: '2.0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: dry-core
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '0.2'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '0.2'
28
42
  - !ruby/object:Gem::Dependency
29
43
  name: dry-configurable
30
44
  requirement: !ruby/object:Gem::Requirement
@@ -118,12 +132,15 @@ files:
118
132
  - lib/dry-view.rb
119
133
  - lib/dry/view.rb
120
134
  - lib/dry/view/controller.rb
135
+ - lib/dry/view/decorator.rb
121
136
  - lib/dry/view/exposure.rb
122
137
  - lib/dry/view/exposures.rb
138
+ - lib/dry/view/part.rb
123
139
  - lib/dry/view/path.rb
124
140
  - lib/dry/view/renderer.rb
125
141
  - lib/dry/view/scope.rb
126
142
  - lib/dry/view/version.rb
143
+ - spec/fixtures/templates/decorated_parts.html.slim
127
144
  - spec/fixtures/templates/empty.html.slim
128
145
  - spec/fixtures/templates/hello.html.slim
129
146
  - spec/fixtures/templates/layouts/app.html.slim
@@ -140,12 +157,15 @@ files:
140
157
  - spec/fixtures/templates/users/_tbody.html.slim
141
158
  - spec/fixtures/templates/users_with_count.html.slim
142
159
  - spec/fixtures/templates_override/users.html.slim
160
+ - spec/integration/decorator_spec.rb
143
161
  - spec/integration/exposures_spec.rb
144
162
  - spec/integration/view_spec.rb
145
163
  - spec/spec_helper.rb
146
164
  - spec/unit/controller_spec.rb
165
+ - spec/unit/decorator_spec.rb
147
166
  - spec/unit/exposure_spec.rb
148
167
  - spec/unit/exposures_spec.rb
168
+ - spec/unit/part_spec.rb
149
169
  - spec/unit/renderer_spec.rb
150
170
  - spec/unit/scope_spec.rb
151
171
  homepage: https://github.com/dry-rb/dry-view
@@ -168,11 +188,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
188
  version: '0'
169
189
  requirements: []
170
190
  rubyforge_project:
171
- rubygems_version: 2.6.8
191
+ rubygems_version: 2.6.10
172
192
  signing_key:
173
193
  specification_version: 4
174
194
  summary: Functional view rendering system
175
195
  test_files:
196
+ - spec/fixtures/templates/decorated_parts.html.slim
176
197
  - spec/fixtures/templates/empty.html.slim
177
198
  - spec/fixtures/templates/hello.html.slim
178
199
  - spec/fixtures/templates/layouts/app.html.slim
@@ -189,11 +210,14 @@ test_files:
189
210
  - spec/fixtures/templates/users/_tbody.html.slim
190
211
  - spec/fixtures/templates/users_with_count.html.slim
191
212
  - spec/fixtures/templates_override/users.html.slim
213
+ - spec/integration/decorator_spec.rb
192
214
  - spec/integration/exposures_spec.rb
193
215
  - spec/integration/view_spec.rb
194
216
  - spec/spec_helper.rb
195
217
  - spec/unit/controller_spec.rb
218
+ - spec/unit/decorator_spec.rb
196
219
  - spec/unit/exposure_spec.rb
197
220
  - spec/unit/exposures_spec.rb
221
+ - spec/unit/part_spec.rb
198
222
  - spec/unit/renderer_spec.rb
199
223
  - spec/unit/scope_spec.rb