dry-view 0.5.1 → 0.7.1
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 +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
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/cache"
|
4
|
+
require "dry/core/equalizer"
|
5
|
+
require_relative "part"
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
class View
|
9
|
+
# Decorates exposure values with matching parts
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
class PartBuilder
|
13
|
+
extend Dry::Core::Cache
|
14
|
+
include Dry::Equalizer(:namespace)
|
15
|
+
|
16
|
+
attr_reader :namespace
|
17
|
+
attr_reader :render_env
|
18
|
+
|
19
|
+
# Returns a new instance of PartBuilder
|
20
|
+
#
|
21
|
+
# @api private
|
22
|
+
def initialize(namespace: nil, render_env: nil)
|
23
|
+
@namespace = namespace
|
24
|
+
@render_env = render_env
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def for_render_env(render_env)
|
29
|
+
return self if render_env == self.render_env
|
30
|
+
|
31
|
+
self.class.new(namespace: namespace, render_env: render_env)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Decorates an exposure value
|
35
|
+
#
|
36
|
+
# @param name [Symbol] exposure name
|
37
|
+
# @param value [Object] exposure value
|
38
|
+
# @param options [Hash] exposure options
|
39
|
+
#
|
40
|
+
# @return [Dry::View::Part] decorated value
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
def call(name, value, **options)
|
44
|
+
builder = value.respond_to?(:to_ary) ? :build_collection_part : :build_part
|
45
|
+
|
46
|
+
send(builder, name, value, **options)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def build_part(name, value, **options)
|
52
|
+
klass = part_class(name: name, **options)
|
53
|
+
|
54
|
+
klass.new(
|
55
|
+
name: name,
|
56
|
+
value: value,
|
57
|
+
render_env: render_env
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_collection_part(name, value, **options)
|
62
|
+
collection_as = collection_options(name: name, **options)[:as]
|
63
|
+
item_name, item_as = collection_item_options(name: name, **options).values_at(:name, :as)
|
64
|
+
|
65
|
+
arr = value.to_ary.map { |obj|
|
66
|
+
build_part(item_name, obj, **options.merge(as: item_as))
|
67
|
+
}
|
68
|
+
|
69
|
+
build_part(name, arr, **options.merge(as: collection_as))
|
70
|
+
end
|
71
|
+
|
72
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
73
|
+
def collection_options(name:, **options)
|
74
|
+
collection_as = options[:as].is_a?(Array) ? options[:as].first : nil
|
75
|
+
|
76
|
+
options.merge(as: collection_as)
|
77
|
+
end
|
78
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
79
|
+
|
80
|
+
def collection_item_options(name:, **options)
|
81
|
+
singular_name = inflector.singularize(name).to_sym
|
82
|
+
singular_as =
|
83
|
+
if options[:as].is_a?(Array)
|
84
|
+
options[:as].last if options[:as].length > 1
|
85
|
+
else
|
86
|
+
options[:as]
|
87
|
+
end
|
88
|
+
|
89
|
+
if singular_as && !singular_as.is_a?(Class)
|
90
|
+
singular_as = inflector.singularize(singular_as.to_s)
|
91
|
+
end
|
92
|
+
|
93
|
+
options.merge(
|
94
|
+
name: singular_name,
|
95
|
+
as: singular_as
|
96
|
+
)
|
97
|
+
end
|
98
|
+
|
99
|
+
def part_class(name:, fallback_class: Part, **options)
|
100
|
+
name = options[:as] || name
|
101
|
+
|
102
|
+
if name.is_a?(Class)
|
103
|
+
name
|
104
|
+
else
|
105
|
+
fetch_or_store(namespace, name, fallback_class) do
|
106
|
+
resolve_part_class(name: name, fallback_class: fallback_class)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
112
|
+
def resolve_part_class(name:, fallback_class:)
|
113
|
+
return fallback_class unless namespace
|
114
|
+
|
115
|
+
name = inflector.camelize(name.to_s)
|
116
|
+
|
117
|
+
# Give autoloaders a chance to act
|
118
|
+
begin
|
119
|
+
klass = namespace.const_get(name)
|
120
|
+
rescue NameError # rubocop:disable Lint/HandleExceptions
|
121
|
+
end
|
122
|
+
|
123
|
+
if !klass && namespace.const_defined?(name, false)
|
124
|
+
klass = namespace.const_get(name)
|
125
|
+
end
|
126
|
+
|
127
|
+
if klass && klass < Part
|
128
|
+
klass
|
129
|
+
else
|
130
|
+
fallback_class
|
131
|
+
end
|
132
|
+
end
|
133
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
134
|
+
|
135
|
+
def inflector
|
136
|
+
render_env.inflector
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
data/lib/dry/view/path.rb
CHANGED
@@ -1,19 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "pathname"
|
4
|
+
require "dry/core/cache"
|
2
5
|
|
3
6
|
module Dry
|
4
|
-
|
7
|
+
class View
|
8
|
+
# @api private
|
5
9
|
class Path
|
10
|
+
extend Dry::Core::Cache
|
6
11
|
include Dry::Equalizer(:dir, :root)
|
7
12
|
|
8
13
|
attr_reader :dir, :root
|
9
14
|
|
10
|
-
def
|
15
|
+
def self.[](path)
|
16
|
+
if path.is_a?(self)
|
17
|
+
path
|
18
|
+
else
|
19
|
+
new(path)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(dir, root: dir)
|
11
24
|
@dir = Pathname(dir)
|
12
|
-
@root = Pathname(
|
25
|
+
@root = Pathname(root)
|
13
26
|
end
|
14
27
|
|
15
|
-
def lookup(name, format)
|
16
|
-
|
28
|
+
def lookup(name, format, child_dirs: [], parent_dir: false)
|
29
|
+
fetch_or_store(dir, root, name, format, child_dirs, parent_dir) do
|
30
|
+
lookup_template(name, format) ||
|
31
|
+
lookup_in_child_dirs(name, format, child_dirs: child_dirs) ||
|
32
|
+
parent_dir && lookup_in_parent_dir(name, format, child_dirs: child_dirs)
|
33
|
+
end
|
17
34
|
end
|
18
35
|
|
19
36
|
def chdir(dirname)
|
@@ -21,7 +38,7 @@ module Dry
|
|
21
38
|
end
|
22
39
|
|
23
40
|
def to_s
|
24
|
-
dir
|
41
|
+
dir.to_s
|
25
42
|
end
|
26
43
|
|
27
44
|
private
|
@@ -31,10 +48,21 @@ module Dry
|
|
31
48
|
end
|
32
49
|
|
33
50
|
# Search for a template using a wildcard for the engine extension
|
34
|
-
def
|
51
|
+
def lookup_template(name, format)
|
35
52
|
glob = dir.join("#{name}.#{format}.*")
|
36
53
|
Dir[glob].first
|
37
54
|
end
|
55
|
+
|
56
|
+
def lookup_in_child_dirs(name, format, child_dirs:)
|
57
|
+
child_dirs.reduce(nil) { |_, dir|
|
58
|
+
template = chdir(dir).lookup(name, format)
|
59
|
+
break template if template
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def lookup_in_parent_dir(name, format, child_dirs:)
|
64
|
+
!root? && chdir("..").lookup(name, format, child_dirs: child_dirs, parent_dir: true)
|
65
|
+
end
|
38
66
|
end
|
39
67
|
end
|
40
68
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
class View
|
7
|
+
# @api private
|
8
|
+
class RenderEnvironment
|
9
|
+
def self.prepare(renderer, config, context)
|
10
|
+
new(
|
11
|
+
renderer: renderer,
|
12
|
+
inflector: config.inflector,
|
13
|
+
context: context,
|
14
|
+
scope_builder: config.scope_builder.new(namespace: config.scope_namespace),
|
15
|
+
part_builder: config.part_builder.new(namespace: config.part_namespace)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
include Dry::Equalizer(:renderer, :inflector, :context, :scope_builder, :part_builder)
|
20
|
+
|
21
|
+
attr_reader :renderer, :inflector, :context, :scope_builder, :part_builder
|
22
|
+
|
23
|
+
def initialize(renderer:, inflector:, context:, scope_builder:, part_builder:)
|
24
|
+
@renderer = renderer
|
25
|
+
@inflector = inflector
|
26
|
+
@context = context.for_render_env(self)
|
27
|
+
@scope_builder = scope_builder.for_render_env(self)
|
28
|
+
@part_builder = part_builder.for_render_env(self)
|
29
|
+
end
|
30
|
+
|
31
|
+
def format
|
32
|
+
renderer.format
|
33
|
+
end
|
34
|
+
|
35
|
+
def part(name, value, **options)
|
36
|
+
part_builder.(name, value, **options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def scope(name = nil, locals) # rubocop:disable Style/OptionalArguments
|
40
|
+
scope_builder.(name, locals)
|
41
|
+
end
|
42
|
+
|
43
|
+
def template(name, scope, &block)
|
44
|
+
renderer.template(name, scope, &block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def partial(name, scope, &block)
|
48
|
+
renderer.partial(name, scope, &block)
|
49
|
+
end
|
50
|
+
|
51
|
+
def chdir(dirname)
|
52
|
+
self.class.new(
|
53
|
+
renderer: renderer.chdir(dirname),
|
54
|
+
inflector: inflector,
|
55
|
+
context: context,
|
56
|
+
scope_builder: scope_builder,
|
57
|
+
part_builder: part_builder
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/inflector"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
class View
|
7
|
+
# @api private
|
8
|
+
class RenderEnvironmentMissing
|
9
|
+
class MissingEnvironmentError < StandardError
|
10
|
+
def message
|
11
|
+
"a +render_env+ must be provided"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def format
|
16
|
+
raise MissingEnvironmentError
|
17
|
+
end
|
18
|
+
|
19
|
+
def context
|
20
|
+
raise MissingEnvironmentError
|
21
|
+
end
|
22
|
+
|
23
|
+
def part(_name, _value, **_options)
|
24
|
+
raise MissingEnvironmentError
|
25
|
+
end
|
26
|
+
|
27
|
+
def scope(_name = nil, _locals) # rubocop:disable Style/OptionalArguments
|
28
|
+
raise MissingEnvironmentError
|
29
|
+
end
|
30
|
+
|
31
|
+
def template(_name, _scope)
|
32
|
+
raise MissingEnvironmentError
|
33
|
+
end
|
34
|
+
|
35
|
+
def partial(_name, _scope)
|
36
|
+
raise MissingEnvironmentError
|
37
|
+
end
|
38
|
+
|
39
|
+
def inflector
|
40
|
+
@inflector ||= Dry::Inflector.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/equalizer"
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
class View
|
7
|
+
# Output of a View rendering
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
class Rendered
|
11
|
+
include Dry::Equalizer(:output, :locals)
|
12
|
+
|
13
|
+
# Returns the rendered view
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
attr_reader :output
|
19
|
+
|
20
|
+
# Returns the hash of locals used to render the view
|
21
|
+
#
|
22
|
+
# @return [Hash[<Symbol, Dry::View::Part>] locals hash
|
23
|
+
#
|
24
|
+
# @api public
|
25
|
+
attr_reader :locals
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def initialize(output:, locals:)
|
29
|
+
@output = output
|
30
|
+
@locals = locals
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the local corresponding to the key
|
34
|
+
#
|
35
|
+
# @param name [Symbol] local key
|
36
|
+
#
|
37
|
+
# @return [Dry::View::Part]
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def [](name)
|
41
|
+
locals[name]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the rendered view
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def to_s
|
50
|
+
output
|
51
|
+
end
|
52
|
+
alias_method :to_str, :to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/dry/view/renderer.rb
CHANGED
@@ -1,41 +1,48 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dry/core/cache"
|
4
|
+
require "dry/core/equalizer"
|
5
|
+
require_relative "errors"
|
6
|
+
require_relative "tilt"
|
3
7
|
|
4
8
|
module Dry
|
5
|
-
|
9
|
+
class View
|
10
|
+
# @api private
|
6
11
|
class Renderer
|
7
|
-
PARTIAL_PREFIX = "_"
|
8
|
-
PATH_DELIMITER = "/"
|
9
|
-
|
10
|
-
include Dry::Equalizer(:paths, :format)
|
12
|
+
PARTIAL_PREFIX = "_"
|
13
|
+
PATH_DELIMITER = "/"
|
11
14
|
|
12
|
-
|
15
|
+
extend Dry::Core::Cache
|
13
16
|
|
14
|
-
|
17
|
+
include Dry::Equalizer(:paths, :format, :engine_mapping, :options)
|
15
18
|
|
16
|
-
|
17
|
-
@__engines__ ||= {}
|
18
|
-
end
|
19
|
+
attr_reader :paths, :format, :engine_mapping, :options
|
19
20
|
|
20
|
-
def initialize(paths, format:)
|
21
|
+
def initialize(paths, format:, engine_mapping: nil, **options)
|
21
22
|
@paths = paths
|
22
23
|
@format = format
|
23
|
-
@
|
24
|
+
@engine_mapping = engine_mapping || {}
|
25
|
+
@options = options
|
24
26
|
end
|
25
27
|
|
26
|
-
def template(name, scope, &block)
|
27
|
-
path = lookup(name)
|
28
|
+
def template(name, scope, **lookup_options, &block)
|
29
|
+
path = lookup(name, **lookup_options)
|
28
30
|
|
29
31
|
if path
|
30
32
|
render(path, scope, &block)
|
31
33
|
else
|
32
|
-
|
33
|
-
raise TemplateNotFoundError, msg
|
34
|
+
raise TemplateNotFoundError.new(name, paths)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
def partial(name, scope, &block)
|
38
|
-
template(
|
39
|
+
template(
|
40
|
+
name_for_partial(name),
|
41
|
+
scope,
|
42
|
+
child_dirs: %w[shared],
|
43
|
+
parent_dir: true,
|
44
|
+
&block
|
45
|
+
)
|
39
46
|
end
|
40
47
|
|
41
48
|
def render(path, scope, &block)
|
@@ -45,26 +52,26 @@ module Dry
|
|
45
52
|
def chdir(dirname)
|
46
53
|
new_paths = paths.map { |path| path.chdir(dirname) }
|
47
54
|
|
48
|
-
self.class.new(new_paths, format: format)
|
55
|
+
self.class.new(new_paths, format: format, **options)
|
49
56
|
end
|
50
57
|
|
51
|
-
|
52
|
-
|
53
|
-
|
58
|
+
private
|
59
|
+
|
60
|
+
def lookup(name, **options)
|
61
|
+
paths.inject(nil) { |_, path|
|
62
|
+
result = path.lookup(name, format, **options)
|
63
|
+
break result if result
|
54
64
|
}
|
55
65
|
end
|
56
66
|
|
57
|
-
private
|
58
|
-
|
59
67
|
def name_for_partial(name)
|
60
68
|
name_segments = name.to_s.split(PATH_DELIMITER)
|
61
|
-
|
69
|
+
name_segments[0..-2].push("#{PARTIAL_PREFIX}#{name_segments[-1]}").join(PATH_DELIMITER)
|
62
70
|
end
|
63
71
|
|
64
|
-
# TODO: make default_encoding configurable
|
65
72
|
def tilt(path)
|
66
|
-
|
67
|
-
|
73
|
+
fetch_or_store(:engine, path, engine_mapping, options) {
|
74
|
+
Tilt[path, engine_mapping, **options]
|
68
75
|
}
|
69
76
|
end
|
70
77
|
end
|