dry-view 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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