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
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/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 == _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,82 @@
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
31
+ # matching Part
32
+ #
33
+ # @api public
34
+ def decorate(*names, **options)
35
+ decorated_attributes.decorate(*names, **options)
36
+ end
37
+
38
+ private
39
+
40
+ def decorated_attributes
41
+ if const_defined?(MODULE_NAME, false)
42
+ const_get(MODULE_NAME)
43
+ else
44
+ const_set(MODULE_NAME, Attributes.new).tap do |mod|
45
+ prepend mod
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ class Attributes < Module
53
+ def initialize(*)
54
+ @names = Set.new
55
+ super
56
+ end
57
+
58
+ def decorate(*names, **options)
59
+ @names += names
60
+
61
+ class_eval do
62
+ names.each do |name|
63
+ define_method name do
64
+ attribute = super()
65
+
66
+ if _render_env && attribute
67
+ _render_env.part(name, attribute, **options)
68
+ else
69
+ attribute
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ def inspect
77
+ %(#<#{self.class.name}#{@names.to_a.sort.inspect}>)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class View
5
+ # Error raised when critical settings are not configured
6
+ #
7
+ # @api private
8
+ class UndefinedConfigError < StandardError
9
+ def initialize(key)
10
+ super("no +#{key}+ configured")
11
+ end
12
+ end
13
+
14
+ # Error raised when template could not be found within a view's configured
15
+ # paths
16
+ #
17
+ # @api private
18
+ class TemplateNotFoundError < StandardError
19
+ def initialize(template_name, lookup_paths)
20
+ msg = [
21
+ "Template +#{template_name}+ could not be found in paths:",
22
+ lookup_paths.map { |path| " - #{path}" }
23
+ ].join("\n\n")
24
+
25
+ super(msg)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,12 +1,17 @@
1
- require 'dry-equalizer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/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
 
8
- EXPOSURE_DEPENDENCY_PARAMETER_TYPES = [:req, :opt].freeze
9
- INPUT_PARAMETER_TYPES = [:key, :keyreq, :keyrest].freeze
13
+ EXPOSURE_DEPENDENCY_PARAMETER_TYPES = %i[req opt].freeze
14
+ INPUT_PARAMETER_TYPES = %i[key keyreq keyrest].freeze
10
15
 
11
16
  attr_reader :name
12
17
  attr_reader :proc
@@ -21,7 +26,7 @@ module Dry
21
26
  end
22
27
 
23
28
  def bind(obj)
24
- self.class.new(name, proc, obj, options)
29
+ self.class.new(name, proc, obj, **options)
25
30
  end
26
31
 
27
32
  def dependency_names
@@ -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
@@ -63,23 +76,31 @@ module Dry
63
76
  private
64
77
 
65
78
  def call_proc(input, locals)
66
- args = proc_args(input, locals)
67
-
68
- if proc.is_a?(Method)
69
- proc.(*args)
79
+ args, keywords = proc_args(input, locals)
80
+
81
+ if keywords.empty?
82
+ if proc.is_a?(Method)
83
+ proc.(*args)
84
+ else
85
+ object.instance_exec(*args, &proc)
86
+ end
70
87
  else
71
- object.instance_exec(*args, &proc)
88
+ if proc.is_a?(Method)
89
+ proc.(*args, **keywords)
90
+ else
91
+ object.instance_exec(*args, **keywords, &proc)
92
+ end
72
93
  end
73
94
  end
74
95
 
75
96
  def proc_args(input, locals)
76
97
  dependency_args = proc_dependency_args(locals)
77
- input_args = proc_input_args(input)
98
+ keywords = proc_input_args(input)
78
99
 
79
- if input_args.any?
80
- dependency_args << input_args
100
+ if keywords.empty?
101
+ [dependency_args, {}]
81
102
  else
82
- dependency_args
103
+ [dependency_args, keywords]
83
104
  end
84
105
  end
85
106
 
@@ -1,9 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "tsort"
4
+ require "dry/core/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
@@ -25,7 +30,7 @@ module Dry
25
30
  end
26
31
 
27
32
  def add(name, proc = nil, **options)
28
- exposures[name] = Exposure.new(name, proc, options)
33
+ exposures[name] = Exposure.new(name, proc, **options)
29
34
  end
30
35
 
31
36
  def import(name, exposure)
@@ -40,12 +45,19 @@ module Dry
40
45
  self.class.new(bound_exposures)
41
46
  end
42
47
 
43
- def locals(input)
48
+ def call(input)
49
+ # rubocop:disable Style/MultilineBlockChain
44
50
  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?
51
+ next unless (exposure = self[name])
52
+
53
+ value = exposure.(input, memo)
54
+ value = yield(value, exposure) if block_given?
55
+
56
+ memo[name] = value
57
+ }.each_with_object({}) { |(name, value), memo|
58
+ memo[name] = value unless self[name].private?
48
59
  }
60
+ # rubocop:enable Style/MultilineBlockChain
49
61
  end
50
62
 
51
63
  private
data/lib/dry/view/part.rb CHANGED
@@ -1,77 +1,205 @@
1
- require 'dry-equalizer'
2
- require 'dry/view/scope'
3
- require 'dry/view/missing_renderer'
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/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(
74
+ render_env: RenderEnvironmentMissing.new,
75
+ name: self.class.part_name(render_env.inflector),
76
+ value:
77
+ )
78
+ @_name = name
79
+ @_value = value
80
+ @_render_env = render_env
33
81
  end
34
82
 
35
- # @api private
36
- def self.decorated_attributes
37
- @decorated_attributes ||= {}
83
+ # The template format for the current render environment.
84
+ #
85
+ # @overload _format
86
+ # Returns the format.
87
+ # @overload format
88
+ # A convenience alias for `#_format.` Is available unless the value
89
+ # itself responds to `#format`.
90
+ #
91
+ # @return [Symbol] format
92
+ #
93
+ # @api public
94
+ def _format
95
+ _render_env.format
38
96
  end
39
97
 
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 = {}
98
+ # The context object for the current render environment
99
+ #
100
+ # @overload _context
101
+ # Returns the context.
102
+ # @overload context
103
+ # A convenience alias for `#_context`. Is available unless the value
104
+ # itself responds to `#context`.
105
+ #
106
+ # @return [Context] context
107
+ #
108
+ # @api public
109
+ def _context
110
+ _render_env.context
48
111
  end
49
112
 
113
+ # Renders a new partial with the part included in its locals.
114
+ #
115
+ # @overload _render(partial_name, as: _name, **locals, &block)
116
+ # Renders the partial.
117
+ # @overload render(partial_name, as: _name, **locals, &block)
118
+ # A convenience alias for `#_render`. Is available unless the value
119
+ # itself responds to `#render`.
120
+ #
121
+ # @param partial_name [Symbol, String] partial name
122
+ # @param as [Symbol] the name for the Part to assume in the partial's locals. Defaults to
123
+ # the Part's `_name`.
124
+ # @param locals [Hash<Symbol, Object>] other locals to provide the partial
125
+ #
126
+ # @return [String] rendered partial
127
+ #
128
+ # @api public
129
+ # rubocop:disable Naming/UncommunicativeMethodParamName
50
130
  def _render(partial_name, as: _name, **locals, &block)
51
- _renderer.partial(partial_name, _render_scope(as, locals), &block)
131
+ _render_env.partial(partial_name, _render_env.scope({as => self}.merge(locals)), &block)
132
+ end
133
+ # rubocop:enable Naming/UncommunicativeMethodParamName
134
+
135
+ # Builds a new scope with the part included in its locals.
136
+ #
137
+ # @overload _scope(scope_name = nil, **locals)
138
+ # Builds the scope.
139
+ # @overload scope(scope_name = nil, **locals)
140
+ # A convenience alias for `#_scope`. Is available unless the value
141
+ # itself responds to `#scope`.
142
+ #
143
+ # @param scope_name [Symbol, nil] scope name, used by the scope builder to determine the
144
+ # scope class
145
+ # @param locals [Hash<Symbol, Object>] other locals to provide the partial
146
+ #
147
+ # @return [Dry::View::Scope] scope
148
+ #
149
+ # @api public
150
+ def _scope(scope_name = nil, **locals)
151
+ _render_env.scope(scope_name, {_name => self}.merge(locals))
52
152
  end
53
153
 
154
+ # Returns a string representation of the value
155
+ #
156
+ # @return [String]
157
+ #
158
+ # @api public
54
159
  def to_s
55
160
  _value.to_s
56
161
  end
57
162
 
58
- def new(klass = (self.class), name: (_name), value: (_value), **options)
163
+ # Builds a new a part with the given parameters
164
+ #
165
+ # This is helpful for manually constructing a new part object that
166
+ # maintains the current render environment.
167
+ #
168
+ # However, using `.decorate` is preferred for declaring attributes that
169
+ # should also be decorated as parts.
170
+ #
171
+ # @see DecoratedAttributes::ClassInterface#decorate
172
+ #
173
+ # @param klass [Class] part class to use (defaults to the part's class)
174
+ # @param name [Symbol] part name (defaults to the part's name)
175
+ # @param value [Object] value to decorate (defaults to the part's value)
176
+ # @param options[Hash<Symbol, Object>] other options to provide when initializing the new part
177
+ #
178
+ # @api public
179
+ def new(klass = self.class, name: _name, value: _value, **options)
59
180
  klass.new(
60
181
  name: name,
61
182
  value: value,
62
- context: _context,
63
- renderer: _renderer,
64
- decorator: _decorator,
65
- **options,
183
+ render_env: _render_env,
184
+ **options
66
185
  )
67
186
  end
68
187
 
188
+ # Returns a string representation of the part
189
+ #
190
+ # @return [String]
191
+ #
192
+ # @api public
193
+ def inspect
194
+ %(#<#{self.class.name} name=#{_name.inspect} value=#{_value.inspect}>)
195
+ end
196
+
69
197
  private
70
198
 
199
+ # Handles missing methods. If the `_value` responds to the method, then
200
+ # the method will be sent to the value.
71
201
  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)
202
+ if _value.respond_to?(name)
75
203
  _value.public_send(name, *args, &block)
76
204
  elsif CONVENIENCE_METHODS.include?(name)
77
205
  __send__(:"_#{name}", *args, &block)
@@ -79,25 +207,10 @@ module Dry
79
207
  super
80
208
  end
81
209
  end
210
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
82
211
 
83
- def _render_scope(name, **locals)
84
- Scope.new(
85
- locals: locals.merge(name => self),
86
- context: _context,
87
- renderer: _renderer,
88
- )
89
- end
90
-
91
- def _resolve_decorated_attribute(name)
92
- _decorated_attributes.fetch(name) {
93
- _decorated_attributes[name] = _decorator.(
94
- name,
95
- _value.__send__(name),
96
- renderer: _renderer,
97
- context: _context,
98
- **self.class.decorated_attributes[name],
99
- )
100
- }
212
+ def respond_to_missing?(name, include_private = false)
213
+ CONVENIENCE_METHODS.include?(name) || _value.respond_to?(name, include_private) || super
101
214
  end
102
215
  end
103
216
  end