dry-view 0.5.3 → 0.6.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.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +18 -0
  3. data/.travis.yml +15 -10
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +60 -1
  6. data/Gemfile +15 -5
  7. data/README.md +38 -13
  8. data/bin/setup +5 -0
  9. data/bin/setup_helpers.rb +27 -0
  10. data/dry-view.gemspec +8 -9
  11. data/lib/dry-view.rb +3 -1
  12. data/lib/dry/view.rb +503 -2
  13. data/lib/dry/view/context.rb +80 -0
  14. data/lib/dry/view/decorated_attributes.rb +81 -0
  15. data/lib/dry/view/exposure.rb +15 -2
  16. data/lib/dry/view/exposures.rb +15 -5
  17. data/lib/dry/view/part.rb +154 -61
  18. data/lib/dry/view/part_builder.rb +136 -0
  19. data/lib/dry/view/path.rb +22 -5
  20. data/lib/dry/view/render_environment.rb +62 -0
  21. data/lib/dry/view/render_environment_missing.rb +44 -0
  22. data/lib/dry/view/rendered.rb +55 -0
  23. data/lib/dry/view/renderer.rb +22 -19
  24. data/lib/dry/view/scope.rb +146 -14
  25. data/lib/dry/view/scope_builder.rb +98 -0
  26. data/lib/dry/view/tilt.rb +78 -0
  27. data/lib/dry/view/tilt/erb.rb +26 -0
  28. data/lib/dry/view/tilt/erbse.rb +21 -0
  29. data/lib/dry/view/tilt/haml.rb +26 -0
  30. data/lib/dry/view/version.rb +5 -2
  31. metadata +50 -88
  32. data/benchmarks/templates/button.html.erb +0 -1
  33. data/benchmarks/view.rb +0 -24
  34. data/lib/dry/view/controller.rb +0 -159
  35. data/lib/dry/view/decorator.rb +0 -45
  36. data/lib/dry/view/missing_renderer.rb +0 -15
  37. data/spec/fixtures/templates/_hello.html.slim +0 -1
  38. data/spec/fixtures/templates/controller_renderer_options.html.erb +0 -3
  39. data/spec/fixtures/templates/decorated_parts.html.slim +0 -4
  40. data/spec/fixtures/templates/edit.html.slim +0 -11
  41. data/spec/fixtures/templates/empty.html.slim +0 -1
  42. data/spec/fixtures/templates/greeting.html.slim +0 -2
  43. data/spec/fixtures/templates/hello.html.slim +0 -1
  44. data/spec/fixtures/templates/layouts/app.html.slim +0 -6
  45. data/spec/fixtures/templates/layouts/app.txt.erb +0 -3
  46. data/spec/fixtures/templates/parts_with_args.html.slim +0 -3
  47. data/spec/fixtures/templates/parts_with_args/_box.html.slim +0 -3
  48. data/spec/fixtures/templates/shared/_index_table.html.slim +0 -2
  49. data/spec/fixtures/templates/shared/_shared_hello.html.slim +0 -1
  50. data/spec/fixtures/templates/tasks.html.slim +0 -3
  51. data/spec/fixtures/templates/user.html.slim +0 -2
  52. data/spec/fixtures/templates/users.html.slim +0 -5
  53. data/spec/fixtures/templates/users.txt.erb +0 -3
  54. data/spec/fixtures/templates/users/_row.html.slim +0 -2
  55. data/spec/fixtures/templates/users/_tbody.html.slim +0 -5
  56. data/spec/fixtures/templates/users_with_count.html.slim +0 -5
  57. data/spec/fixtures/templates/users_with_count_inherit.html.slim +0 -6
  58. data/spec/fixtures/templates_override/_hello.html.slim +0 -1
  59. data/spec/fixtures/templates_override/users.html.slim +0 -5
  60. data/spec/integration/decorator_spec.rb +0 -80
  61. data/spec/integration/exposures_spec.rb +0 -392
  62. data/spec/integration/part/decorated_attributes_spec.rb +0 -193
  63. data/spec/integration/view_spec.rb +0 -133
  64. data/spec/spec_helper.rb +0 -46
  65. data/spec/unit/controller_spec.rb +0 -83
  66. data/spec/unit/decorator_spec.rb +0 -61
  67. data/spec/unit/exposure_spec.rb +0 -227
  68. data/spec/unit/exposures_spec.rb +0 -103
  69. data/spec/unit/part_spec.rb +0 -104
  70. data/spec/unit/renderer_spec.rb +0 -57
  71. data/spec/unit/scope_spec.rb +0 -53
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+ require_relative "decorated_attributes"
5
+
6
+ module Dry
7
+ class View
8
+ # Provides a baseline environment across all the templates, parts and scopes
9
+ # in a given rendering.
10
+ #
11
+ # @abstract Subclass this and add your own methods (along with a custom
12
+ # `#initialize` if you wish to inject dependencies)
13
+ #
14
+ # @api public
15
+ class Context
16
+ include Dry::Equalizer(:_options)
17
+ include DecoratedAttributes
18
+
19
+ attr_reader :_render_env, :_options
20
+
21
+ # Returns a new instance of Context
22
+ #
23
+ # In subclasses, you should include an `**options` parameter and pass _all
24
+ # arguments_ to `super`. This allows Context to make copies of itself
25
+ # while preserving your dependencies.
26
+ #
27
+ # @example
28
+ # class MyContext < Dry::View::Context
29
+ # # Injected dependency
30
+ # attr_reader :assets
31
+ #
32
+ # def initialize(assets:, **options)
33
+ # @assets = assets
34
+ # super
35
+ # end
36
+ # end
37
+ #
38
+ # @api public
39
+ def initialize(render_env: nil, **options)
40
+ @_render_env = render_env
41
+ @_options = options
42
+ end
43
+
44
+ # @api private
45
+ def for_render_env(render_env)
46
+ return self if render_env == self._render_env
47
+
48
+ self.class.new(**_options.merge(render_env: render_env))
49
+ end
50
+
51
+ # Returns a copy of the Context with new options merged in.
52
+ #
53
+ # This may be useful to supply values for dependencies that are _optional_
54
+ # when initializing your custom Context subclass.
55
+ #
56
+ # @example
57
+ # class MyContext < Dry::View::Context
58
+ # # Injected dependencies (request is optional)
59
+ # attr_reader :assets, :request
60
+ #
61
+ # def initialize(assets:, request: nil, **options)
62
+ # @assets = assets
63
+ # @request = reuqest
64
+ # super
65
+ # end
66
+ # end
67
+ #
68
+ # my_context = MyContext.new(assets: assets)
69
+ # my_context_with_request = my_context.with(request: request)
70
+ #
71
+ # @api public
72
+ def with(**new_options)
73
+ self.class.new(
74
+ render_env: _render_env,
75
+ **_options.merge(new_options),
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Dry
6
+ class View
7
+ # Decorates attributes in Parts.
8
+ module DecoratedAttributes
9
+ # @api private
10
+ def self.included(klass)
11
+ klass.extend ClassInterface
12
+ end
13
+
14
+ # Decorated attributes class-level interface.
15
+ module ClassInterface
16
+ # @api private
17
+ MODULE_NAME = :DecoratedAttributes
18
+
19
+ # Decorates the provided attributes, wrapping them in Parts using the
20
+ # current render environment.
21
+ #
22
+ # @example
23
+ # class Article < Dry::View::Part
24
+ # decorate :feature_image
25
+ # decorate :author as: :person
26
+ # end
27
+ #
28
+ # @param names [Array<Symbol>] the attribute names
29
+ # @param options [Hash] the options to pass to the Part Builder
30
+ # @option options [Symbol, Class] :as an alternative name or class to use when finding a matching Part
31
+ #
32
+ # @api public
33
+ def decorate(*names, **options)
34
+ decorated_attributes.decorate(*names, **options)
35
+ end
36
+
37
+ private
38
+
39
+ def decorated_attributes
40
+ if const_defined?(MODULE_NAME, false)
41
+ const_get(MODULE_NAME)
42
+ else
43
+ const_set(MODULE_NAME, Attributes.new).tap do |mod|
44
+ prepend mod
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # @api private
51
+ class Attributes < Module
52
+ def initialize(*)
53
+ @names = Set.new
54
+ super
55
+ end
56
+
57
+ def decorate(*names, **options)
58
+ @names += names
59
+
60
+ class_eval do
61
+ names.each do |name|
62
+ define_method name do
63
+ attribute = super()
64
+
65
+ if _render_env && attribute
66
+ _render_env.part(name, attribute, **options)
67
+ else
68
+ attribute
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ def inspect
76
+ %(#<#{self.class.name}#{@names.to_a.sort.inspect}>)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,7 +1,12 @@
1
- require 'dry-equalizer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-equalizer"
2
4
 
3
5
  module Dry
4
- module View
6
+ class View
7
+ # An exposure defined on a view
8
+ #
9
+ # @api private
5
10
  class Exposure
6
11
  include Dry::Equalizer(:name, :proc, :object, :options)
7
12
 
@@ -44,6 +49,14 @@ module Dry
44
49
  end
45
50
  end
46
51
 
52
+ def for_layout?
53
+ options.fetch(:layout) { false }
54
+ end
55
+
56
+ def decorate?
57
+ options.fetch(:decorate) { true }
58
+ end
59
+
47
60
  def private?
48
61
  options.fetch(:private) { false }
49
62
  end
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "tsort"
4
+ require "dry/equalizer"
2
5
  require "dry/view/exposure"
3
6
 
4
7
  module Dry
5
- module View
8
+ class View
9
+ # @api private
6
10
  class Exposures
11
+ include Dry::Equalizer(:exposures)
7
12
  include TSort
8
13
 
9
14
  attr_reader :exposures
@@ -40,11 +45,16 @@ module Dry
40
45
  self.class.new(bound_exposures)
41
46
  end
42
47
 
43
- def locals(input)
48
+ def call(input)
44
49
  tsort.each_with_object({}) { |name, memo|
45
- memo[name] = self[name].(input, memo) if exposures.key?(name)
46
- }.each_with_object({}) { |(name, val), memo|
47
- memo[name] = val unless self[name].private?
50
+ next unless exposure = self[name]
51
+
52
+ value = exposure.(input, memo)
53
+ value = yield(value, exposure) if block_given?
54
+
55
+ memo[name] = value
56
+ }.each_with_object({}) { |(name, value), memo|
57
+ memo[name] = value unless self[name].private?
48
58
  }
49
59
  end
50
60
 
data/lib/dry/view/part.rb CHANGED
@@ -1,77 +1,197 @@
1
- require 'dry-equalizer'
2
- require 'dry/view/scope'
3
- require 'dry/view/missing_renderer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/equalizer"
4
+ require_relative "decorated_attributes"
5
+ require_relative "render_environment_missing"
4
6
 
5
7
  module Dry
6
- module View
8
+ class View
9
+ # Decorates an exposure value and provides a place to encapsulate
10
+ # view-specific behavior alongside your application's domain objects.
11
+ #
12
+ # @abstract Subclass this and provide your own methods adding view-specific
13
+ # behavior. You should not override `#initialize`.
14
+ #
15
+ # @see https://dry-rb.org/gems/dry-view/parts/
16
+ #
17
+ # @api public
7
18
  class Part
19
+ # @api private
8
20
  CONVENIENCE_METHODS = %i[
21
+ format
9
22
  context
10
23
  render
24
+ scope
11
25
  value
12
26
  ].freeze
13
27
 
14
- include Dry::Equalizer(:_name, :_value, :_decorator, :_context, :_renderer)
28
+ include Dry::Equalizer(:_name, :_value, :_render_env)
29
+ include DecoratedAttributes
15
30
 
31
+ # The part's name. This comes from the exposure supplying the value.
32
+ #
33
+ # @return [Symbol] name
34
+ #
35
+ # @api public
16
36
  attr_reader :_name
17
37
 
38
+ # The decorated value. This is the value returned from the exposure.
39
+ #
40
+ # @overload _value
41
+ # Returns the value.
42
+ # @overload value
43
+ # A convenience alias for `_value`. Is available unless the value itself
44
+ # responds to `#value`.
45
+ #
46
+ # @return [Object] value
47
+ #
48
+ # @api public
18
49
  attr_reader :_value
19
50
 
20
- attr_reader :_context
21
-
22
- attr_reader :_renderer
23
-
24
- attr_reader :_decorator
51
+ # The current render environment
52
+ #
53
+ # @return [RenderEnvironment] render environment
54
+ #
55
+ # @api private
56
+ attr_reader :_render_env
25
57
 
26
- attr_reader :_decorated_attributes
58
+ # Determins a part name (when initialized without one). Intended for use
59
+ # only while unit testing Parts.
60
+ #
61
+ # @api private
62
+ def self.part_name(inflector)
63
+ name ? inflector.underscore(inflector.demodulize(name)) : "part"
64
+ end
27
65
 
66
+ # Returns a new Part instance
67
+ #
68
+ # @param name [Symbol] part name
69
+ # @param value [Object] the value to decorate
70
+ # @param render_env [RenderEnvironment] render environment
71
+ #
28
72
  # @api public
29
- def self.decorate(*names, **options)
30
- names.each do |name|
31
- decorated_attributes[name] = options
32
- end
73
+ def initialize(render_env: RenderEnvironmentMissing.new, name: self.class.part_name(render_env.inflector), value:)
74
+ @_name = name
75
+ @_value = value
76
+ @_render_env = render_env
33
77
  end
34
78
 
35
- # @api private
36
- def self.decorated_attributes
37
- @decorated_attributes ||= {}
79
+ # The template format for the current render environment.
80
+ #
81
+ # @overload _format
82
+ # Returns the format.
83
+ # @overload format
84
+ # A convenience alias for `#_format.` Is available unless the value
85
+ # itself responds to `#format`.
86
+ #
87
+ # @return [Symbol] format
88
+ #
89
+ # @api public
90
+ def _format
91
+ _render_env.format
38
92
  end
39
93
 
40
- # FIXME: does MissingRenderer.new lead to needless allocations of MissingRenderer? We only need one globally.
41
- def initialize(name:, value:, decorator: Dry::View::Decorator.new, renderer: MissingRenderer.new, context: nil)
42
- @_name = name
43
- @_value = value
44
- @_context = context
45
- @_renderer = renderer
46
- @_decorator = decorator
47
- @_decorated_attributes = {}
94
+ # The context object for the current render environment
95
+ #
96
+ # @overload _context
97
+ # Returns the context.
98
+ # @overload context
99
+ # A convenience alias for `#_context`. Is available unless the value
100
+ # itself responds to `#context`.
101
+ #
102
+ # @return [Context] context
103
+ #
104
+ # @api public
105
+ def _context
106
+ _render_env.context
48
107
  end
49
108
 
109
+ # Renders a new partial with the part included in its locals.
110
+ #
111
+ # @overload _render(partial_name, as: _name, **locals, &block)
112
+ # Renders the partial.
113
+ # @overload render(partial_name, as: _name, **locals, &block)
114
+ # A convenience alias for `#_render`. Is available unless the value
115
+ # itself responds to `#render`.
116
+ #
117
+ # @param partial_name [Symbol, String] partial name
118
+ # @param as [Symbol] the name for the Part to assume in the partial's locals. Default's to the Part's `_name`.
119
+ # @param locals [Hash<Symbol, Object>] other locals to provide the partial
120
+ #
121
+ # @return [String] rendered partial
122
+ #
123
+ # @api public
50
124
  def _render(partial_name, as: _name, **locals, &block)
51
- _renderer.partial(partial_name, _render_scope(as, locals), &block)
125
+ _render_env.partial(partial_name, _render_env.scope({as => self}.merge(locals)), &block)
126
+ end
127
+
128
+ # Builds a new scope with the part included in its locals.
129
+ #
130
+ # @overload _scope(scope_name = nil, **locals)
131
+ # Builds the scope.
132
+ # @overload scope(scope_name = nil, **locals)
133
+ # A convenience alias for `#_scope`. Is available unless the value
134
+ # itself responds to `#scope`.
135
+ #
136
+ # @param scope_name [Symbol, nil] scope name, used by the scope builder to determine the scope class
137
+ # @param locals [Hash<Symbol, Object>] other locals to provide the partial
138
+ #
139
+ # @return [Dry::View::Scope] scope
140
+ #
141
+ # @api public
142
+ def _scope(scope_name = nil, **locals)
143
+ _render_env.scope(scope_name, {_name => self}.merge(locals))
52
144
  end
53
145
 
146
+ # Returns a string representation of the value
147
+ #
148
+ # @return [String]
149
+ #
150
+ # @api public
54
151
  def to_s
55
152
  _value.to_s
56
153
  end
57
154
 
155
+ # Builds a new a part with the given parameters
156
+ #
157
+ # This is helpful for manually constructing a new part object that
158
+ # maintains the current render environment.
159
+ #
160
+ # However, using `.decorate` is preferred for declaring attributes that
161
+ # should also be decorated as parts.
162
+ #
163
+ # @see DecoratedAttributes::ClassInterface#decorate
164
+ #
165
+ # @param klass [Class] part class to use (defaults to the part's class)
166
+ # @param name [Symbol] part name (defaults to the part's name)
167
+ # @param value [Object] value to decorate (defaults to the part's value)
168
+ # @param options[Hash<Symbol, Object>] other options to provide when initializing the new part
169
+ #
170
+ # @api public
58
171
  def new(klass = (self.class), name: (_name), value: (_value), **options)
59
172
  klass.new(
60
173
  name: name,
61
174
  value: value,
62
- context: _context,
63
- renderer: _renderer,
64
- decorator: _decorator,
175
+ render_env: _render_env,
65
176
  **options,
66
177
  )
67
178
  end
68
179
 
180
+ # Returns a string representation of the part
181
+ #
182
+ # @return [String]
183
+ #
184
+ # @api public
185
+ def inspect
186
+ %(#<#{self.class.name} name=#{_name.inspect} value=#{_value.inspect}>)
187
+ end
188
+
69
189
  private
70
190
 
191
+ # Handles missing methods. If the `_value` responds to the method, then
192
+ # the method will be sent to the value.
71
193
  def method_missing(name, *args, &block)
72
- if self.class.decorated_attributes.key?(name)
73
- _resolve_decorated_attribute(name)
74
- elsif _value.respond_to?(name)
194
+ if _value.respond_to?(name)
75
195
  _value.public_send(name, *args, &block)
76
196
  elsif CONVENIENCE_METHODS.include?(name)
77
197
  __send__(:"_#{name}", *args, &block)
@@ -81,34 +201,7 @@ module Dry
81
201
  end
82
202
 
83
203
  def respond_to_missing?(name, include_private = false)
84
- d = self.class.decorated_attributes
85
- c = CONVENIENCE_METHODS
86
- d.key?(name) || c.include?(name) || _value.respond_to?(name, include_private) || super
87
- end
88
-
89
- def _render_scope(name, **locals)
90
- Scope.new(
91
- locals: locals.merge(name => self),
92
- context: _context,
93
- renderer: _renderer,
94
- )
95
- end
96
-
97
- def _resolve_decorated_attribute(name)
98
- _decorated_attributes.fetch(name) {
99
- attribute = _value.__send__(name)
100
-
101
- _decorated_attributes[name] =
102
- if attribute # Decorate truthy attributes only
103
- _decorator.(
104
- name,
105
- attribute,
106
- renderer: _renderer,
107
- context: _context,
108
- **self.class.decorated_attributes[name],
109
- )
110
- end
111
- }
204
+ CONVENIENCE_METHODS.include?(name) || _value.respond_to?(name, include_private) || super
112
205
  end
113
206
  end
114
207
  end