dry-view 0.5.1 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +143 -18
- data/LICENSE +20 -0
- data/README.md +22 -14
- data/dry-view.gemspec +29 -21
- data/lib/dry-view.rb +3 -1
- data/lib/dry/view.rb +514 -2
- data/lib/dry/view/context.rb +80 -0
- data/lib/dry/view/decorated_attributes.rb +82 -0
- data/lib/dry/view/errors.rb +29 -0
- data/lib/dry/view/exposure.rb +35 -14
- data/lib/dry/view/exposures.rb +18 -6
- data/lib/dry/view/part.rb +166 -53
- data/lib/dry/view/part_builder.rb +140 -0
- data/lib/dry/view/path.rb +35 -7
- data/lib/dry/view/render_environment.rb +62 -0
- data/lib/dry/view/render_environment_missing.rb +44 -0
- data/lib/dry/view/rendered.rb +55 -0
- data/lib/dry/view/renderer.rb +36 -29
- data/lib/dry/view/scope.rb +160 -14
- data/lib/dry/view/scope_builder.rb +98 -0
- data/lib/dry/view/tilt.rb +78 -0
- data/lib/dry/view/tilt/erb.rb +26 -0
- data/lib/dry/view/tilt/erbse.rb +21 -0
- data/lib/dry/view/tilt/haml.rb +26 -0
- data/lib/dry/view/version.rb +5 -2
- metadata +78 -115
- data/.gitignore +0 -26
- data/.rspec +0 -2
- data/.travis.yml +0 -23
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -22
- data/LICENSE.md +0 -10
- data/Rakefile +0 -6
- data/benchmarks/templates/button.html.erb +0 -1
- data/benchmarks/view.rb +0 -24
- data/bin/console +0 -7
- data/lib/dry/view/controller.rb +0 -155
- data/lib/dry/view/decorator.rb +0 -45
- data/lib/dry/view/missing_renderer.rb +0 -15
- data/spec/fixtures/templates/_hello.html.slim +0 -1
- data/spec/fixtures/templates/decorated_parts.html.slim +0 -4
- data/spec/fixtures/templates/edit.html.slim +0 -11
- data/spec/fixtures/templates/empty.html.slim +0 -1
- data/spec/fixtures/templates/greeting.html.slim +0 -2
- data/spec/fixtures/templates/hello.html.slim +0 -1
- data/spec/fixtures/templates/layouts/app.html.slim +0 -6
- data/spec/fixtures/templates/layouts/app.txt.erb +0 -3
- data/spec/fixtures/templates/parts_with_args.html.slim +0 -3
- data/spec/fixtures/templates/parts_with_args/_box.html.slim +0 -3
- data/spec/fixtures/templates/shared/_index_table.html.slim +0 -2
- data/spec/fixtures/templates/shared/_shared_hello.html.slim +0 -1
- data/spec/fixtures/templates/tasks.html.slim +0 -3
- data/spec/fixtures/templates/user.html.slim +0 -2
- data/spec/fixtures/templates/users.html.slim +0 -5
- data/spec/fixtures/templates/users.txt.erb +0 -3
- data/spec/fixtures/templates/users/_row.html.slim +0 -2
- data/spec/fixtures/templates/users/_tbody.html.slim +0 -5
- data/spec/fixtures/templates/users_with_count.html.slim +0 -5
- data/spec/fixtures/templates/users_with_count_inherit.html.slim +0 -6
- data/spec/fixtures/templates_override/_hello.html.slim +0 -1
- data/spec/fixtures/templates_override/users.html.slim +0 -5
- data/spec/integration/decorator_spec.rb +0 -80
- data/spec/integration/exposures_spec.rb +0 -392
- data/spec/integration/part/decorated_attributes_spec.rb +0 -157
- data/spec/integration/view_spec.rb +0 -133
- data/spec/spec_helper.rb +0 -46
- data/spec/unit/controller_spec.rb +0 -37
- data/spec/unit/decorator_spec.rb +0 -61
- data/spec/unit/exposure_spec.rb +0 -227
- data/spec/unit/exposures_spec.rb +0 -103
- data/spec/unit/part_spec.rb +0 -90
- data/spec/unit/renderer_spec.rb +0 -57
- data/spec/unit/scope_spec.rb +0 -53
@@ -1 +0,0 @@
|
|
1
|
-
<a href="/users/1">User</a>
|
data/benchmarks/view.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'pathname'
|
2
|
-
require 'benchmark/ips'
|
3
|
-
require 'dry/view/renderer'
|
4
|
-
require 'action_view'
|
5
|
-
|
6
|
-
class ActionRender
|
7
|
-
include ActionView::Helpers
|
8
|
-
|
9
|
-
def button
|
10
|
-
link_to('User', '/users/1')
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
action_renderer = ActionRender.new
|
15
|
-
dry_view_renderer = Dry::View::Renderer.new(Pathname(__FILE__).dirname.join('templates'), format: :html)
|
16
|
-
|
17
|
-
template = Pathname(__FILE__).dirname.join('templates').join('button.html.erb')
|
18
|
-
SCOPE = {}
|
19
|
-
|
20
|
-
Benchmark.ips do |x|
|
21
|
-
x.report('actionview') { action_renderer.button }
|
22
|
-
x.report('dry-view') { dry_view_renderer.render(template, SCOPE) }
|
23
|
-
x.compare!
|
24
|
-
end
|
data/bin/console
DELETED
data/lib/dry/view/controller.rb
DELETED
@@ -1,155 +0,0 @@
|
|
1
|
-
require 'dry-configurable'
|
2
|
-
require 'dry-equalizer'
|
3
|
-
|
4
|
-
require 'dry/view/path'
|
5
|
-
require 'dry/view/exposures'
|
6
|
-
require 'dry/view/renderer'
|
7
|
-
require 'dry/view/decorator'
|
8
|
-
require 'dry/view/scope'
|
9
|
-
|
10
|
-
module Dry
|
11
|
-
module View
|
12
|
-
class Controller
|
13
|
-
UndefinedTemplateError = Class.new(StandardError)
|
14
|
-
|
15
|
-
DEFAULT_LAYOUTS_DIR = 'layouts'.freeze
|
16
|
-
DEFAULT_CONTEXT = Object.new.freeze
|
17
|
-
EMPTY_LOCALS = {}.freeze
|
18
|
-
|
19
|
-
include Dry::Equalizer(:config)
|
20
|
-
|
21
|
-
extend Dry::Configurable
|
22
|
-
|
23
|
-
setting :paths
|
24
|
-
setting :layout, false
|
25
|
-
setting :template
|
26
|
-
setting :default_format, :html
|
27
|
-
setting :context, DEFAULT_CONTEXT
|
28
|
-
setting :decorator, Decorator.new
|
29
|
-
|
30
|
-
attr_reader :config
|
31
|
-
attr_reader :layout_dir
|
32
|
-
attr_reader :layout_path
|
33
|
-
attr_reader :template_path
|
34
|
-
attr_reader :exposures
|
35
|
-
|
36
|
-
# @api private
|
37
|
-
def self.inherited(klass)
|
38
|
-
super
|
39
|
-
exposures.each do |name, exposure|
|
40
|
-
klass.exposures.import(name, exposure)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# @api public
|
45
|
-
def self.paths
|
46
|
-
Array(config.paths).map { |path| Dry::View::Path.new(path) }
|
47
|
-
end
|
48
|
-
|
49
|
-
# @api private
|
50
|
-
def self.renderer(format)
|
51
|
-
renderers.fetch(format) {
|
52
|
-
renderers[format] = Renderer.new(paths, format: format)
|
53
|
-
}
|
54
|
-
end
|
55
|
-
|
56
|
-
# @api private
|
57
|
-
def self.renderers
|
58
|
-
@renderers ||= {}
|
59
|
-
end
|
60
|
-
|
61
|
-
# @api public
|
62
|
-
def self.expose(*names, **options, &block)
|
63
|
-
if names.length == 1
|
64
|
-
exposures.add(names.first, block, options)
|
65
|
-
else
|
66
|
-
names.each do |name|
|
67
|
-
exposures.add(name, options)
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
# @api public
|
73
|
-
def self.private_expose(*names, **options, &block)
|
74
|
-
expose(*names, **options, private: true, &block)
|
75
|
-
end
|
76
|
-
|
77
|
-
# @api private
|
78
|
-
def self.exposures
|
79
|
-
@exposures ||= Exposures.new
|
80
|
-
end
|
81
|
-
|
82
|
-
# @api public
|
83
|
-
def initialize
|
84
|
-
@config = self.class.config
|
85
|
-
@layout_dir = DEFAULT_LAYOUTS_DIR
|
86
|
-
@layout_path = "#{layout_dir}/#{config.layout}"
|
87
|
-
@template_path = config.template
|
88
|
-
@exposures = self.class.exposures.bind(self)
|
89
|
-
end
|
90
|
-
|
91
|
-
# @api public
|
92
|
-
def call(format: config.default_format, context: config.context, **input)
|
93
|
-
raise UndefinedTemplateError, "no +template+ configured" unless template_path
|
94
|
-
|
95
|
-
renderer = self.class.renderer(format)
|
96
|
-
|
97
|
-
template_content = renderer.template(template_path, template_scope(renderer, context, input))
|
98
|
-
|
99
|
-
return template_content unless layout?
|
100
|
-
|
101
|
-
renderer.template(layout_path, layout_scope(renderer, context)) do
|
102
|
-
template_content
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# @api public
|
107
|
-
def locals(locals: EMPTY_LOCALS, **input)
|
108
|
-
exposures.locals(input).merge(locals)
|
109
|
-
end
|
110
|
-
|
111
|
-
private
|
112
|
-
|
113
|
-
def layout?
|
114
|
-
!!config.layout
|
115
|
-
end
|
116
|
-
|
117
|
-
def layout_scope(renderer, context)
|
118
|
-
scope(renderer.chdir(layout_dir), context)
|
119
|
-
end
|
120
|
-
|
121
|
-
def template_scope(renderer, context, **input)
|
122
|
-
scope(renderer.chdir(template_path), context, locals(input))
|
123
|
-
end
|
124
|
-
|
125
|
-
def scope(renderer, context, locals = EMPTY_LOCALS)
|
126
|
-
Scope.new(
|
127
|
-
renderer: renderer,
|
128
|
-
context: context,
|
129
|
-
locals: decorated_locals(renderer, context, locals)
|
130
|
-
)
|
131
|
-
end
|
132
|
-
|
133
|
-
def decorated_locals(renderer, context, locals)
|
134
|
-
decorator = self.class.config.decorator
|
135
|
-
|
136
|
-
locals.each_with_object({}) { |(key, val), result|
|
137
|
-
# Decorate truthy values only
|
138
|
-
if val
|
139
|
-
options = exposures.key?(key) ? exposures[key].options : {}
|
140
|
-
|
141
|
-
val = decorator.(
|
142
|
-
key,
|
143
|
-
val,
|
144
|
-
renderer: renderer,
|
145
|
-
context: context,
|
146
|
-
**options
|
147
|
-
)
|
148
|
-
end
|
149
|
-
|
150
|
-
result[key] = val
|
151
|
-
}
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
data/lib/dry/view/decorator.rb
DELETED
@@ -1,45 +0,0 @@
|
|
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, decorator: self, renderer: renderer, context: context)
|
22
|
-
else
|
23
|
-
klass.new(name: name, value: value, decorator: self, 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,15 +0,0 @@
|
|
1
|
-
module Dry
|
2
|
-
module View
|
3
|
-
class MissingRendererError < StandardError
|
4
|
-
def initialize(message = "No renderer provided")
|
5
|
-
super
|
6
|
-
end
|
7
|
-
end
|
8
|
-
|
9
|
-
class MissingRenderer
|
10
|
-
def method_missing(name, *args, &block)
|
11
|
-
raise MissingRendererError
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1 +0,0 @@
|
|
1
|
-
h1 Partial hello
|
@@ -1 +0,0 @@
|
|
1
|
-
p This is a view with no locals.
|
@@ -1 +0,0 @@
|
|
1
|
-
h1 Hello
|
@@ -1 +0,0 @@
|
|
1
|
-
h1 Hello
|
@@ -1 +0,0 @@
|
|
1
|
-
h1 Partial new hello
|
@@ -1,80 +0,0 @@
|
|
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
|