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.
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
@@ -1,43 +1,189 @@
1
- require 'dry-equalizer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+ require "dry/core/constants"
5
+ require_relative "render_environment_missing"
2
6
 
3
7
  module Dry
4
- module View
8
+ class View
9
+ # Evaluation context for templates (including layouts and partials) and
10
+ # provides a place to encapsulate view-specific behaviour alongside a
11
+ # template and its locals.
12
+ #
13
+ # @abstract Subclass this and provide your own methods adding view-specific
14
+ # behavior. You should not override `#initialize`
15
+ #
16
+ # @see https://dry-rb.org/gems/dry-view/templates/
17
+ # @see https://dry-rb.org/gems/dry-view/scopes/
18
+ #
19
+ # @api public
5
20
  class Scope
6
- include Dry::Equalizer(:_locals, :_context, :_renderer)
21
+ # @api private
22
+ CONVENIENCE_METHODS = %i[format context locals].freeze
23
+
24
+ include Dry::Equalizer(:_name, :_locals, :_render_env)
25
+
26
+ # The scope's name
27
+ #
28
+ # @return [Symbol]
29
+ #
30
+ # @api public
31
+ attr_reader :_name
7
32
 
33
+ # The scope's locals
34
+ #
35
+ # @overload _locals
36
+ # Returns the locals
37
+ # @overload locals
38
+ # A convenience alias for `#_format.` Is available unless there is a
39
+ # local named `locals`
40
+ #
41
+ # @return [Hash[<Symbol, Object>]
42
+ #
43
+ # @api public
8
44
  attr_reader :_locals
9
- attr_reader :_context
10
- attr_reader :_renderer
11
45
 
12
- def initialize(renderer:, context: nil, locals: {})
46
+ # The current render environment
47
+ #
48
+ # @return [RenderEnvironment] render environment
49
+ #
50
+ # @api private
51
+ attr_reader :_render_env
52
+
53
+ # Returns a new Scope instance
54
+ #
55
+ # @param name [Symbol, nil] scope name
56
+ # @param locals [Hash<Symbol, Object>] template locals
57
+ # @param render_env [RenderEnvironment] render environment
58
+ #
59
+ # @return [Scope]
60
+ #
61
+ # @api public
62
+ def initialize(
63
+ name: nil,
64
+ locals: Dry::Core::Constants::EMPTY_HASH,
65
+ render_env: RenderEnvironmentMissing.new
66
+ )
67
+ @_name = name
13
68
  @_locals = locals
14
- @_context = context
15
- @_renderer = renderer
69
+ @_render_env = render_env
70
+ end
71
+
72
+ # @overload render(partial_name, **locals, &block)
73
+ # Renders a partial using the scope
74
+ #
75
+ # @param partial_name [Symbol, String] partial name
76
+ # @param locals [Hash<Symbol, Object>] partial locals
77
+ # @yieldreturn [String] string content to include where the partial calls `yield`
78
+ #
79
+ # @overload render(**locals, &block)
80
+ # Renders a partial (named after the scope's own name) using the scope
81
+ #
82
+ # @param locals[Hash<Symbol, Object>] partial locals
83
+ # @yieldreturn [String] string content to include where the partial calls `yield`
84
+ #
85
+ # @return [String] the rendered partial output
86
+ #
87
+ # @api public
88
+ def render(partial_name = nil, **locals, &block)
89
+ partial_name ||= _name
90
+
91
+ unless partial_name
92
+ raise ArgumentError, "+partial_name+ must be provided for unnamed scopes"
93
+ end
94
+
95
+ if partial_name.is_a?(Class)
96
+ partial_name = _inflector.underscore(_inflector.demodulize(partial_name.to_s))
97
+ end
98
+
99
+ _render_env.partial(partial_name, _render_scope(**locals), &block)
16
100
  end
17
101
 
18
- def render(partial_name, **locals, &block)
19
- _renderer.partial(partial_name, __render_scope(locals), &block)
102
+ # Build a new scope using a scope class matching the provided name
103
+ #
104
+ # @param name [Symbol, Class] scope name (or class)
105
+ # @param locals [Hash<Symbol, Object>] scope locals
106
+ #
107
+ # @return [Scope]
108
+ #
109
+ # @api public
110
+ def scope(name = nil, **locals)
111
+ _render_env.scope(name, locals)
112
+ end
113
+
114
+ # The template format for the current render environment.
115
+ #
116
+ # @overload _format
117
+ # Returns the format.
118
+ # @overload format
119
+ # A convenience alias for `#_format.` Is available unless there is a
120
+ # local named `format`
121
+ #
122
+ # @return [Symbol] format
123
+ #
124
+ # @api public
125
+ def _format
126
+ _render_env.format
127
+ end
128
+
129
+ # The context object for the current render environment
130
+ #
131
+ # @overload _context
132
+ # Returns the context.
133
+ # @overload context
134
+ # A convenience alias for `#_context`. Is available unless there is a
135
+ # local named `context`.
136
+ #
137
+ # @return [Context] context
138
+ #
139
+ # @api public
140
+ def _context
141
+ _render_env.context
20
142
  end
21
143
 
22
144
  private
23
145
 
146
+ # Handles missing methods, according to the following rules:
147
+ #
148
+ # 1. If there is a local with a name matching the method, it returns the
149
+ # local.
150
+ # 2. If the `context` responds to the method, then it will be sent the
151
+ # method and all its arguments.
24
152
  def method_missing(name, *args, &block)
25
153
  if _locals.key?(name)
26
154
  _locals[name]
27
155
  elsif _context.respond_to?(name)
28
156
  _context.public_send(name, *args, &block)
157
+ elsif CONVENIENCE_METHODS.include?(name)
158
+ __send__(:"_#{name}", *args, &block)
29
159
  else
30
160
  super
31
161
  end
32
162
  end
163
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
33
164
 
34
- def __render_scope(**locals)
35
- if locals.any?
36
- self.class.new(renderer: _renderer, context: _context, locals: locals)
37
- else
165
+ def respond_to_missing?(name, include_private = false)
166
+ _locals.key?(name) ||
167
+ _render_env.context.respond_to?(name) ||
168
+ CONVENIENCE_METHODS.include?(name) ||
169
+ super
170
+ end
171
+
172
+ def _render_scope(**locals)
173
+ if locals.none?
38
174
  self
175
+ else
176
+ self.class.new(
177
+ # FIXME: what about `name`?
178
+ locals: locals,
179
+ render_env: _render_env
180
+ )
39
181
  end
40
182
  end
183
+
184
+ def _inflector
185
+ _render_env.inflector
186
+ end
41
187
  end
42
188
  end
43
189
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/cache"
4
+ require "dry/core/equalizer"
5
+ require_relative "scope"
6
+
7
+ module Dry
8
+ class View
9
+ # Builds scope objects via matching classes
10
+ #
11
+ # @api private
12
+ class ScopeBuilder
13
+ extend Dry::Core::Cache
14
+ include Dry::Equalizer(:namespace)
15
+
16
+ # The view's configured `scope_namespace`
17
+ #
18
+ # @api private
19
+ attr_reader :namespace
20
+
21
+ # @return [RenderEnvironment]
22
+ #
23
+ # @api private
24
+ attr_reader :render_env
25
+
26
+ # Returns a new instance of ScopeBuilder
27
+ #
28
+ # @api private
29
+ def initialize(namespace: nil, render_env: nil)
30
+ @namespace = namespace
31
+ @render_env = render_env
32
+ end
33
+
34
+ # @api private
35
+ def for_render_env(render_env)
36
+ return self if render_env == self.render_env
37
+
38
+ self.class.new(namespace: namespace, render_env: render_env)
39
+ end
40
+
41
+ # Returns a new scope using a class matching the name
42
+ #
43
+ # @param name [Symbol, Class] scope name
44
+ # @param locals [Hash<Symbol, Object>] locals hash
45
+ #
46
+ # @return [Dry::View::Scope]
47
+ #
48
+ # @api private
49
+ def call(name = nil, locals) # rubocop:disable Style/OptionalArguments
50
+ scope_class(name).new(
51
+ name: name,
52
+ locals: locals,
53
+ render_env: render_env
54
+ )
55
+ end
56
+
57
+ private
58
+
59
+ DEFAULT_SCOPE_CLASS = Scope
60
+
61
+ def scope_class(name = nil)
62
+ if name.nil?
63
+ DEFAULT_SCOPE_CLASS
64
+ elsif name.is_a?(Class)
65
+ name
66
+ else
67
+ fetch_or_store(namespace, name) do
68
+ resolve_scope_class(name: name)
69
+ end
70
+ end
71
+ end
72
+
73
+ def resolve_scope_class(name:)
74
+ name = inflector.camelize(name.to_s)
75
+
76
+ # Give autoloaders a chance to act
77
+ begin
78
+ klass = namespace.const_get(name)
79
+ rescue NameError # rubocop:disable Lint/HandleExceptions
80
+ end
81
+
82
+ if !klass && namespace.const_defined?(name, false)
83
+ klass = namespace.const_get(name)
84
+ end
85
+
86
+ if klass && klass < Scope
87
+ klass
88
+ else
89
+ DEFAULT_SCOPE_CLASS
90
+ end
91
+ end
92
+
93
+ def inflector
94
+ render_env.inflector
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/cache"
4
+ require "tilt"
5
+
6
+ module Dry
7
+ class View
8
+ # @api private
9
+ module Tilt
10
+ extend Dry::Core::Cache
11
+
12
+ class << self
13
+ def [](path, mapping, **options)
14
+ ext = File.extname(path).sub(/^./, "").to_sym
15
+ activate_adapter ext
16
+
17
+ with_mapping(mapping).new(path, **options)
18
+ end
19
+
20
+ def default_mapping
21
+ ::Tilt.default_mapping
22
+ end
23
+
24
+ def register_adapter(ext, adapter)
25
+ adapters[ext] = adapter
26
+ end
27
+
28
+ def deregister_adapter(ext)
29
+ adapters.delete(ext)
30
+ end
31
+
32
+ private
33
+
34
+ def adapters
35
+ @adapters ||= {}
36
+ end
37
+
38
+ def activate_adapter(ext)
39
+ fetch_or_store(:adapter, ext) {
40
+ adapter = adapters[ext]
41
+ return unless adapter
42
+
43
+ *requires, error_message = adapter.requirements
44
+
45
+ begin
46
+ requires.each(&method(:require))
47
+ rescue LoadError => e
48
+ raise e, "#{e.message}\n\n#{error_message}"
49
+ end
50
+
51
+ adapter.activate
52
+ }
53
+ end
54
+
55
+ def with_mapping(mapping)
56
+ fetch_or_store(:mapping, mapping) {
57
+ if mapping.any?
58
+ build_mapping(mapping)
59
+ else
60
+ default_mapping
61
+ end
62
+ }
63
+ end
64
+
65
+ def build_mapping(mapping)
66
+ default_mapping.dup.tap do |new_mapping|
67
+ mapping.each do |extension, template_class|
68
+ new_mapping.register template_class, extension
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ require_relative "tilt/erb"
78
+ require_relative "tilt/haml"
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class View
5
+ module Tilt
6
+ module ERB
7
+ def self.requirements
8
+ ["dry/view/tilt/erbse", <<~ERROR]
9
+ dry-view requires erbse for full compatibility when rendering .erb templates (e.g. implicitly capturing block content when yielding)
10
+
11
+ To ignore this and use another engine for .erb templates, deregister this adapter before calling your views:
12
+
13
+ Dry::View::Tilt.deregister_adapter(:erb)
14
+ ERROR
15
+ end
16
+
17
+ def self.activate
18
+ Tilt.default_mapping.register ErbseTemplate, "erb"
19
+ self
20
+ end
21
+ end
22
+
23
+ register_adapter :erb, ERB
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tilt/template"
4
+ require "erbse"
5
+
6
+ module Dry
7
+ class View
8
+ module Tilt
9
+ # Tilt template class copied from cells-erb gem
10
+ class ErbseTemplate < ::Tilt::Template
11
+ def prepare
12
+ @template = ::Erbse::Engine.new
13
+ end
14
+
15
+ def precompiled_template(_locals)
16
+ @template.call(data)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class View
5
+ module Tilt
6
+ module Haml
7
+ def self.requirements
8
+ ["hamlit/block", <<~ERROR]
9
+ dry-view requires hamlit-block for full compatibility when rendering .haml templates (e.g. implicitly capturing block content when yielding)
10
+
11
+ To ignore this and use another engine for .haml templates, dereigster this adapter before calling your views:
12
+
13
+ Dry::View::Tilt.deregister_adatper(:haml)
14
+ ERROR
15
+ end
16
+
17
+ def self.activate
18
+ # Requiring hamlit/block will register the engine with Tilt
19
+ self
20
+ end
21
+ end
22
+
23
+ register_adapter :haml, Haml
24
+ end
25
+ end
26
+ end