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 +4 -4
- data/CHANGELOG.md +13 -0
- data/Gemfile +2 -0
- data/dry-view.gemspec +1 -0
- data/lib/dry/view/controller.rb +48 -12
- data/lib/dry/view/decorator.rb +45 -0
- data/lib/dry/view/exposure.rb +11 -5
- data/lib/dry/view/exposures.rb +8 -4
- data/lib/dry/view/part.rb +67 -0
- data/lib/dry/view/scope.rb +19 -23
- data/lib/dry/view/version.rb +1 -1
- data/spec/fixtures/templates/decorated_parts.html.slim +4 -0
- data/spec/fixtures/templates/parts_with_args.html.slim +1 -1
- data/spec/fixtures/templates/users.html.slim +2 -2
- data/spec/fixtures/templates/users/_tbody.html.slim +1 -1
- data/spec/fixtures/templates_override/users.html.slim +2 -2
- data/spec/integration/decorator_spec.rb +80 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/unit/decorator_spec.rb +61 -0
- data/spec/unit/exposure_spec.rb +5 -5
- data/spec/unit/exposures_spec.rb +1 -1
- data/spec/unit/part_spec.rb +65 -0
- data/spec/unit/scope_spec.rb +33 -73
- metadata +27 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 350649d54605ecc359ddcfd199fe13fec79e4d47
|
4
|
+
data.tar.gz: 7655546ae79c8e42da84aa12df1698a0bdba7f24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0c817743037231ab4455085be9521d0106d8781998fca1727d7c591183e98862349492567fc77ee1f6ea168feaef62055ce4fc6adf1cad881712a6b23c24b42a
|
7
|
+
data.tar.gz: dfe44787659b229484a65ff231d7afc5c61f6fb3a831e46e03026fefb3bd1f7a6be46e021a34cb02e9010e59a3838c174962bdf1284550d5a55bf74438d34577
|
data/CHANGELOG.md
CHANGED
@@ -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
data/dry-view.gemspec
CHANGED
@@ -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
|
|
data/lib/dry/view/controller.rb
CHANGED
@@ -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
|
-
|
57
|
-
|
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
|
-
|
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,
|
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
|
95
|
-
scope(renderer.chdir(layout_dir),
|
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
|
99
|
-
|
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
|
103
|
-
|
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
|
data/lib/dry/view/exposure.rb
CHANGED
@@ -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 :
|
13
|
+
attr_reader :options
|
10
14
|
|
11
|
-
def initialize(name, proc = nil, object = nil,
|
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
|
-
@
|
19
|
+
@options = options
|
16
20
|
end
|
17
21
|
|
18
22
|
def bind(obj)
|
19
|
-
self.class.new(name, proc, obj,
|
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
|
-
|
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
|
data/lib/dry/view/exposures.rb
CHANGED
@@ -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 =
|
25
|
-
[name
|
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
|
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
|
data/lib/dry/view/scope.rb
CHANGED
@@ -3,51 +3,47 @@ require 'dry-equalizer'
|
|
3
3
|
module Dry
|
4
4
|
module View
|
5
5
|
class Scope
|
6
|
-
include Dry::Equalizer(:
|
6
|
+
include Dry::Equalizer(:_locals, :_context, :_renderer)
|
7
7
|
|
8
|
-
attr_reader :
|
9
|
-
attr_reader :_data
|
8
|
+
attr_reader :_locals
|
10
9
|
attr_reader :_context
|
10
|
+
attr_reader :_renderer
|
11
11
|
|
12
|
-
def initialize(renderer
|
13
|
-
@
|
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
|
19
|
-
|
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
|
26
|
-
|
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
|
38
|
+
def __partial(name)
|
37
39
|
_renderer.lookup("_#{name}")
|
38
40
|
end
|
39
41
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
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
|
-
|
46
|
+
self
|
51
47
|
end
|
52
48
|
end
|
53
49
|
end
|
data/lib/dry/view/version.rb
CHANGED
@@ -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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
data/spec/unit/exposure_spec.rb
CHANGED
@@ -33,13 +33,13 @@ RSpec.describe Dry::View::Exposure do
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
describe "#
|
37
|
-
it "is
|
38
|
-
expect(exposure
|
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
|
42
|
-
expect(described_class.new(:hello,
|
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
|
data/spec/unit/exposures_spec.rb
CHANGED
@@ -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" },
|
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
|
data/spec/unit/scope_spec.rb
CHANGED
@@ -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(:
|
9
|
-
let(:
|
10
|
-
let(:
|
4
|
+
let(:locals) { {} }
|
5
|
+
let(:context) { double('context') }
|
6
|
+
let(:renderer) { double('renderer') }
|
11
7
|
|
12
|
-
describe
|
8
|
+
describe '#render' do
|
13
9
|
before do
|
14
|
-
allow(renderer).to receive(:lookup).and_return
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
67
|
-
expect(scope.
|
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
|
-
|
76
|
-
|
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
|
-
|
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
|
87
|
-
|
44
|
+
it 'forwards all arguments to the method' do
|
45
|
+
blk = -> { }
|
46
|
+
scope.greeting 'args', &blk
|
88
47
|
|
89
|
-
expect(
|
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
|
-
|
94
|
-
|
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.
|
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-
|
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.
|
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
|