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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -18
  3. data/LICENSE +20 -0
  4. data/README.md +22 -14
  5. data/dry-view.gemspec +29 -21
  6. data/lib/dry-view.rb +3 -1
  7. data/lib/dry/view.rb +514 -2
  8. data/lib/dry/view/context.rb +80 -0
  9. data/lib/dry/view/decorated_attributes.rb +82 -0
  10. data/lib/dry/view/errors.rb +29 -0
  11. data/lib/dry/view/exposure.rb +35 -14
  12. data/lib/dry/view/exposures.rb +18 -6
  13. data/lib/dry/view/part.rb +166 -53
  14. data/lib/dry/view/part_builder.rb +140 -0
  15. data/lib/dry/view/path.rb +35 -7
  16. data/lib/dry/view/render_environment.rb +62 -0
  17. data/lib/dry/view/render_environment_missing.rb +44 -0
  18. data/lib/dry/view/rendered.rb +55 -0
  19. data/lib/dry/view/renderer.rb +36 -29
  20. data/lib/dry/view/scope.rb +160 -14
  21. data/lib/dry/view/scope_builder.rb +98 -0
  22. data/lib/dry/view/tilt.rb +78 -0
  23. data/lib/dry/view/tilt/erb.rb +26 -0
  24. data/lib/dry/view/tilt/erbse.rb +21 -0
  25. data/lib/dry/view/tilt/haml.rb +26 -0
  26. data/lib/dry/view/version.rb +5 -2
  27. metadata +78 -115
  28. data/.gitignore +0 -26
  29. data/.rspec +0 -2
  30. data/.travis.yml +0 -23
  31. data/CONTRIBUTING.md +0 -29
  32. data/Gemfile +0 -22
  33. data/LICENSE.md +0 -10
  34. data/Rakefile +0 -6
  35. data/benchmarks/templates/button.html.erb +0 -1
  36. data/benchmarks/view.rb +0 -24
  37. data/bin/console +0 -7
  38. data/lib/dry/view/controller.rb +0 -155
  39. data/lib/dry/view/decorator.rb +0 -45
  40. data/lib/dry/view/missing_renderer.rb +0 -15
  41. data/spec/fixtures/templates/_hello.html.slim +0 -1
  42. data/spec/fixtures/templates/decorated_parts.html.slim +0 -4
  43. data/spec/fixtures/templates/edit.html.slim +0 -11
  44. data/spec/fixtures/templates/empty.html.slim +0 -1
  45. data/spec/fixtures/templates/greeting.html.slim +0 -2
  46. data/spec/fixtures/templates/hello.html.slim +0 -1
  47. data/spec/fixtures/templates/layouts/app.html.slim +0 -6
  48. data/spec/fixtures/templates/layouts/app.txt.erb +0 -3
  49. data/spec/fixtures/templates/parts_with_args.html.slim +0 -3
  50. data/spec/fixtures/templates/parts_with_args/_box.html.slim +0 -3
  51. data/spec/fixtures/templates/shared/_index_table.html.slim +0 -2
  52. data/spec/fixtures/templates/shared/_shared_hello.html.slim +0 -1
  53. data/spec/fixtures/templates/tasks.html.slim +0 -3
  54. data/spec/fixtures/templates/user.html.slim +0 -2
  55. data/spec/fixtures/templates/users.html.slim +0 -5
  56. data/spec/fixtures/templates/users.txt.erb +0 -3
  57. data/spec/fixtures/templates/users/_row.html.slim +0 -2
  58. data/spec/fixtures/templates/users/_tbody.html.slim +0 -5
  59. data/spec/fixtures/templates/users_with_count.html.slim +0 -5
  60. data/spec/fixtures/templates/users_with_count_inherit.html.slim +0 -6
  61. data/spec/fixtures/templates_override/_hello.html.slim +0 -1
  62. data/spec/fixtures/templates_override/users.html.slim +0 -5
  63. data/spec/integration/decorator_spec.rb +0 -80
  64. data/spec/integration/exposures_spec.rb +0 -392
  65. data/spec/integration/part/decorated_attributes_spec.rb +0 -157
  66. data/spec/integration/view_spec.rb +0 -133
  67. data/spec/spec_helper.rb +0 -46
  68. data/spec/unit/controller_spec.rb +0 -37
  69. data/spec/unit/decorator_spec.rb +0 -61
  70. data/spec/unit/exposure_spec.rb +0 -227
  71. data/spec/unit/exposures_spec.rb +0 -103
  72. data/spec/unit/part_spec.rb +0 -90
  73. data/spec/unit/renderer_spec.rb +0 -57
  74. 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
- module View
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 initialize(dir, options = {})
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(options.fetch(:root, dir))
25
+ @root = Pathname(root)
13
26
  end
14
27
 
15
- def lookup(name, format)
16
- template?(name, format) || template?("shared/#{name}", format) || !root? && chdir('..').lookup(name, format)
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 template?(name, format)
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
@@ -1,41 +1,48 @@
1
- require 'tilt'
2
- require 'dry-equalizer'
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
- module View
9
+ class View
10
+ # @api private
6
11
  class Renderer
7
- PARTIAL_PREFIX = "_".freeze
8
- PATH_DELIMITER = "/".freeze
9
-
10
- include Dry::Equalizer(:paths, :format)
12
+ PARTIAL_PREFIX = "_"
13
+ PATH_DELIMITER = "/"
11
14
 
12
- TemplateNotFoundError = Class.new(StandardError)
15
+ extend Dry::Core::Cache
13
16
 
14
- attr_reader :paths, :format, :engine, :tilts
17
+ include Dry::Equalizer(:paths, :format, :engine_mapping, :options)
15
18
 
16
- def self.tilts
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
- @tilts = self.class.tilts
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
- msg = "Template #{name.inspect} could not be found in paths:\n#{paths.map { |pa| "- #{pa.to_s}" }.join("\n")}"
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(name_for_partial(name), scope, &block)
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
- def lookup(name)
52
- paths.inject(false) { |result, path|
53
- result || path.lookup(name, format)
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
- partial_name = name_segments[0..-2].push("#{PARTIAL_PREFIX}#{name_segments[-1]}").join(PATH_DELIMITER)
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
- tilts.fetch(path) {
67
- tilts[path] = Tilt.new(path, nil, default_encoding: "utf-8")
73
+ fetch_or_store(:engine, path, engine_mapping, options) {
74
+ Tilt[path, engine_mapping, **options]
68
75
  }
69
76
  end
70
77
  end