dry-view 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 350649d54605ecc359ddcfd199fe13fec79e4d47
4
- data.tar.gz: 7655546ae79c8e42da84aa12df1698a0bdba7f24
3
+ metadata.gz: ba3055d4bb318adbf1cc7718433873c0f8726e26
4
+ data.tar.gz: 9e09c2f6cf9b340010dcda9319c23b6d78d48c61
5
5
  SHA512:
6
- metadata.gz: 0c817743037231ab4455085be9521d0106d8781998fca1727d7c591183e98862349492567fc77ee1f6ea168feaef62055ce4fc6adf1cad881712a6b23c24b42a
7
- data.tar.gz: dfe44787659b229484a65ff231d7afc5c61f6fb3a831e46e03026fefb3bd1f7a6be46e021a34cb02e9010e59a3838c174962bdf1284550d5a55bf74438d34577
6
+ metadata.gz: 0decd3ced65ee18f4f4bd95c18af6866c53fba4ed92c1e6a2774f8e8bdc787183ee90a260c5904c5a9b661cf106f822e60bd48ba4b4d89ff33fad630daf1eccc
7
+ data.tar.gz: 5b7792fd0b18d5c9dcd2c10d53d3f817fd643ee90e97d866f68e8920f543126d4fed444afa9e0cae1193fa7cde5794cb9d54a16d8d63a1f9314228bc23171308
@@ -13,10 +13,9 @@ after_success:
13
13
  # Send coverage report from the job #1 == current MRI release
14
14
  - '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
15
15
  rvm:
16
- - 2.4.0
17
- - 2.3.3
18
- - 2.2.6
19
- - 2.1.10
16
+ - 2.4.2
17
+ - 2.3.5
18
+ - 2.2.8
20
19
  - jruby-9.1.6.0
21
20
  env:
22
21
  global:
@@ -1,3 +1,17 @@
1
+ # 0.4.0 / 2017-11-01
2
+
3
+ ### Added
4
+
5
+ - Raise a helpful error when trying to render a template or partial that cannot be found (GustavoCaso)
6
+ - Raise a helpful error when trying to call a view controller with no template configured (timriley)
7
+ - Allow a default to be specified for pass-through exposures with the `default:` option (GustavoCaso)
8
+
9
+ ### Changed
10
+
11
+ - [BREAKING] Exposures specify the input data they require using keyword arguments. This includes support for providing default values (via the keyword argument) for keys that are missing from the input data (GustavoCaso)
12
+ - Allow `Dry::View::Part` instances to be created without explicitly passing a `renderer`. This is helpful for unit testing view parts that don't need to render anything (dNitza)
13
+ - Partials can be nested within additional sub-directories by rendering them their relative path as their name, e.g. `render(:"foo/bar")` will look for a `foo/_bar.html.slim` template within the normal template lookup paths (timriley)
14
+
1
15
  # 0.3.0 / 2017-05-14
2
16
 
3
17
  This release reintroduces view parts in a more helpful form. You can provide your own custom view part classes to encapsulate your view logic, as well as a decorator for custom, shared behavior arouund view part wrapping.
@@ -0,0 +1,29 @@
1
+ # Issue Guidelines
2
+
3
+ ## Reporting bugs
4
+
5
+ If you found a bug, report an issue and describe what's the expected behavior versus what actually happens. If the bug causes a crash, attach a full backtrace. If possible, a reproduction script showing the problem is highly appreciated.
6
+
7
+ ## Reporting feature requests
8
+
9
+ Report a feature request **only after discussing it first on [discourse.dry-rb.org](https://discourse.dry-rb.org)** where it was accepted. Please provide a concise description of the feature, don't link to a discussion thread, and instead summarize what was discussed.
10
+
11
+ ## Reporting questions, support requests, ideas, concerns etc.
12
+
13
+ **PLEASE DON'T** - use [discourse.dry-rb.org](http://discourse.dry-rb.org) instead.
14
+
15
+ # Pull Request Guidelines
16
+
17
+ A Pull Request will only be accepted if it addresses a specific issue that was reported previously, or fixes typos, mistakes in documentation etc.
18
+
19
+ Other requirements:
20
+
21
+ 1) Do not open a pull request if you can't provide tests along with it. If you have problems writing tests, ask for help in the related issue.
22
+ 2) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style.
23
+ 3) Add API documentation if it's a new feature
24
+ 4) Update API documentation if it changes an existing feature
25
+ 5) Bonus points for sending a PR to [github.com/dry-rb/dry-rb.org](github.com/dry-rb/dry-rb.org) which updates user documentation and guides
26
+
27
+ # Asking for help
28
+
29
+ If these guidelines aren't helpful, and you're stuck, please post a message on [discourse.dry-rb.org](https://discourse.dry-rb.org).
data/Gemfile CHANGED
@@ -5,11 +5,10 @@ gemspec
5
5
  gem 'inflecto'
6
6
 
7
7
  group :tools do
8
- gem 'pry'
8
+ gem 'pry-byebug', platform: :mri
9
9
  end
10
10
 
11
11
  group :test do
12
- gem 'byebug', platform: :mri
13
12
  gem 'rack', '>= 1.0.0', '<= 2.0.0'
14
13
  gem 'slim'
15
14
 
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
20
  spec.require_paths = ["lib"]
21
21
 
22
- spec.required_ruby_version = '>= 2.1.0'
22
+ spec.required_ruby_version = '>= 2.2.0'
23
23
 
24
24
  spec.add_runtime_dependency "tilt", "~> 2.0"
25
25
  spec.add_runtime_dependency "dry-core", "~> 0.2"
@@ -10,6 +10,8 @@ require 'dry/view/scope'
10
10
  module Dry
11
11
  module View
12
12
  class Controller
13
+ UndefinedTemplateError = Class.new(StandardError)
14
+
13
15
  DEFAULT_LAYOUTS_DIR = 'layouts'.freeze
14
16
  DEFAULT_CONTEXT = Object.new.freeze
15
17
  EMPTY_LOCALS = {}.freeze
@@ -51,17 +53,17 @@ module Dry
51
53
  # @api public
52
54
  def self.expose(*names, **options, &block)
53
55
  if names.length == 1
54
- exposures.add(names.first, block, **options)
56
+ exposures.add(names.first, block, options)
55
57
  else
56
58
  names.each do |name|
57
- exposures.add(name, **options)
59
+ exposures.add(name, options)
58
60
  end
59
61
  end
60
62
  end
61
63
 
62
64
  # @api public
63
65
  def self.private_expose(*names, **options, &block)
64
- expose(*names, **options.merge(private: true), &block)
66
+ expose(*names, **options, private: true, &block)
65
67
  end
66
68
 
67
69
  # @api private
@@ -80,13 +82,15 @@ module Dry
80
82
 
81
83
  # @api public
82
84
  def call(format: config.default_format, context: config.context, **input)
85
+ raise UndefinedTemplateError, "no +template+ configured" unless template_path
86
+
83
87
  renderer = self.class.renderer(format)
84
88
 
85
- template_content = renderer.(template_path, template_scope(renderer, context, **input))
89
+ template_content = renderer.template(template_path, template_scope(renderer, context, input))
86
90
 
87
91
  return template_content unless layout?
88
92
 
89
- renderer.(layout_path, layout_scope(renderer, context)) do
93
+ renderer.template(layout_path, layout_scope(renderer, context)) do
90
94
  template_content
91
95
  end
92
96
  end
@@ -107,7 +111,7 @@ module Dry
107
111
  end
108
112
 
109
113
  def template_scope(renderer, context, **input)
110
- scope(renderer.chdir(template_path), context, locals(**input))
114
+ scope(renderer.chdir(template_path), context, locals(input))
111
115
  end
112
116
 
113
117
  def scope(renderer, context, locals = EMPTY_LOCALS)
@@ -8,7 +8,7 @@ module Dry
8
8
 
9
9
  # @api public
10
10
  def call(name, value, renderer:, context:, **options)
11
- klass = part_class(name, value, **options)
11
+ klass = part_class(name, value, options)
12
12
 
13
13
  if value.respond_to?(:to_ary)
14
14
  singular_name = Dry::Core::Inflector.singularize(name).to_sym
@@ -5,7 +5,8 @@ module Dry
5
5
  class Exposure
6
6
  include Dry::Equalizer(:name, :proc, :object, :options)
7
7
 
8
- SUPPORTED_PARAMETER_TYPES = [:req, :opt].freeze
8
+ EXPOSURE_DEPENDENCY_PARAMETER_TYPES = [:req, :opt].freeze
9
+ INPUT_PARAMETER_TYPES = [:key, :keyreq, :keyrest].freeze
9
10
 
10
11
  attr_reader :name
11
12
  attr_reader :proc
@@ -20,34 +21,50 @@ module Dry
20
21
  end
21
22
 
22
23
  def bind(obj)
23
- self.class.new(name, proc, obj, **options)
24
+ self.class.new(name, proc, obj, options)
24
25
  end
25
26
 
26
- def dependencies
27
- proc ? proc.parameters.map(&:last) : []
27
+ def dependency_names
28
+ if proc
29
+ proc.parameters.each_with_object([]) { |(type, name), names|
30
+ names << name if EXPOSURE_DEPENDENCY_PARAMETER_TYPES.include?(type)
31
+ }
32
+ else
33
+ []
34
+ end
35
+ end
36
+
37
+ def input_keys
38
+ if proc
39
+ proc.parameters.each_with_object([]) { |(type, name), keys|
40
+ keys << name if INPUT_PARAMETER_TYPES.include?(type)
41
+ }
42
+ else
43
+ []
44
+ end
28
45
  end
29
46
 
30
47
  def private?
31
48
  options.fetch(:private) { false }
32
49
  end
33
50
 
34
- def call(input, locals = {})
35
- return input[name] unless proc
36
-
37
- args = dependencies.map.with_index { |name, position|
38
- if position.zero?
39
- locals.fetch(name) { input }
40
- else
41
- locals.fetch(name)
42
- end
43
- }
51
+ def default_value
52
+ options[:default]
53
+ end
44
54
 
45
- call_proc(*args)
55
+ def call(input, locals = {})
56
+ if proc
57
+ call_proc(input, locals)
58
+ else
59
+ input.fetch(name) { default_value }
60
+ end
46
61
  end
47
62
 
48
63
  private
49
64
 
50
- def call_proc(*args)
65
+ def call_proc(input, locals)
66
+ args = proc_args(input, locals)
67
+
51
68
  if proc.is_a?(Method)
52
69
  proc.(*args)
53
70
  else
@@ -55,6 +72,27 @@ module Dry
55
72
  end
56
73
  end
57
74
 
75
+ def proc_args(input, locals)
76
+ dependency_args = proc_dependency_args(locals)
77
+ input_args = proc_input_args(input)
78
+
79
+ if input_args.any?
80
+ dependency_args << input_args
81
+ else
82
+ dependency_args
83
+ end
84
+ end
85
+
86
+ def proc_dependency_args(locals)
87
+ dependency_names.map { |name| locals.fetch(name) }
88
+ end
89
+
90
+ def proc_input_args(input)
91
+ input_keys.each_with_object({}) { |key, args|
92
+ args[key] = input[key] if input.key?(key)
93
+ }
94
+ end
95
+
58
96
  def prepare_proc(proc, object)
59
97
  if proc
60
98
  proc
@@ -21,7 +21,7 @@ module Dry
21
21
  end
22
22
 
23
23
  def add(name, proc = nil, **options)
24
- exposures[name] = Exposure.new(name, proc, **options)
24
+ exposures[name] = Exposure.new(name, proc, options)
25
25
  end
26
26
 
27
27
  def bind(obj)
@@ -47,7 +47,7 @@ module Dry
47
47
  end
48
48
 
49
49
  def tsort_each_child(name, &block)
50
- self[name].dependencies.each(&block) if exposures.key?(name)
50
+ self[name].dependency_names.each(&block) if exposures.key?(name)
51
51
  end
52
52
  end
53
53
  end
@@ -0,0 +1,15 @@
1
+ module Dry
2
+ module View
3
+ class MissingRendererError < StandardError
4
+ def initialize(message = "No renderer provided")
5
+ super
6
+ end
7
+ end
8
+
9
+ class MissingRenderer
10
+ def method_missing(name, *args, &block)
11
+ raise MissingRendererError
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,6 @@
1
1
  require 'dry-equalizer'
2
2
  require 'dry/view/scope'
3
+ require 'dry/view/missing_renderer'
3
4
 
4
5
  module Dry
5
6
  module View
@@ -20,7 +21,7 @@ module Dry
20
21
 
21
22
  attr_reader :_renderer
22
23
 
23
- def initialize(name:, value:, renderer:, context: nil)
24
+ def initialize(name:, value:, renderer: MissingRenderer.new, context: nil)
24
25
  @_name = name
25
26
  @_value = value
26
27
  @_context = context
@@ -28,11 +29,7 @@ module Dry
28
29
  end
29
30
 
30
31
  def _render(partial_name, as: _name, **locals, &block)
31
- _renderer.render(
32
- _partial(partial_name),
33
- _render_scope(as, **locals),
34
- &block
35
- )
32
+ _renderer.partial(partial_name, _render_scope(as, locals), &block)
36
33
  end
37
34
 
38
35
  def to_s
@@ -51,10 +48,6 @@ module Dry
51
48
  end
52
49
  end
53
50
 
54
- def _partial(name)
55
- _renderer.lookup("_#{name}")
56
- end
57
-
58
51
  def _render_scope(name, **locals)
59
52
  Scope.new(
60
53
  locals: locals.merge(name => self),
@@ -4,6 +4,9 @@ require 'dry-equalizer'
4
4
  module Dry
5
5
  module View
6
6
  class Renderer
7
+ PARTIAL_PREFIX = "_".freeze
8
+ PATH_DELIMITER = "/".freeze
9
+
7
10
  include Dry::Equalizer(:paths, :format)
8
11
 
9
12
  TemplateNotFoundError = Class.new(StandardError)
@@ -20,17 +23,21 @@ module Dry
20
23
  @tilts = self.class.tilts
21
24
  end
22
25
 
23
- def call(template, scope, &block)
24
- path = lookup(template)
26
+ def template(name, scope, &block)
27
+ path = lookup(name)
25
28
 
26
29
  if path
27
30
  render(path, scope, &block)
28
31
  else
29
- msg = "Template #{template.inspect} could not be found in paths:\n#{paths.map { |pa| "- #{pa.to_s}" }.join("\n")}"
32
+ msg = "Template #{name.inspect} could not be found in paths:\n#{paths.map { |pa| "- #{pa.to_s}" }.join("\n")}"
30
33
  raise TemplateNotFoundError, msg
31
34
  end
32
35
  end
33
36
 
37
+ def partial(name, scope, &block)
38
+ template(name_for_partial(name), scope, &block)
39
+ end
40
+
34
41
  def render(path, scope, &block)
35
42
  tilt(path).render(scope, &block)
36
43
  end
@@ -49,6 +56,11 @@ module Dry
49
56
 
50
57
  private
51
58
 
59
+ def name_for_partial(name)
60
+ name_segments = name.to_s.split(PATH_DELIMITER)
61
+ partial_name = name_segments[0..-2].push("#{PARTIAL_PREFIX}#{name_segments[-1]}").join(PATH_DELIMITER)
62
+ end
63
+
52
64
  # TODO: make default_encoding configurable
53
65
  def tilt(path)
54
66
  tilts.fetch(path) {
@@ -16,11 +16,7 @@ module Dry
16
16
  end
17
17
 
18
18
  def render(partial_name, **locals, &block)
19
- _renderer.render(
20
- __partial(partial_name),
21
- __render_scope(**locals),
22
- &block
23
- )
19
+ _renderer.partial(partial_name, __render_scope(locals), &block)
24
20
  end
25
21
 
26
22
  private
@@ -35,10 +31,6 @@ module Dry
35
31
  end
36
32
  end
37
33
 
38
- def __partial(name)
39
- _renderer.lookup("_#{name}")
40
- end
41
-
42
34
  def __render_scope(**locals)
43
35
  if locals.any?
44
36
  self.class.new(renderer: _renderer, context: _context, locals: locals)
@@ -1,5 +1,5 @@
1
1
  module Dry
2
2
  module View
3
- VERSION = '0.3.0'.freeze
3
+ VERSION = '0.4.0'.freeze
4
4
  end
5
5
  end
@@ -0,0 +1 @@
1
+ h1 Partial hello
@@ -0,0 +1,11 @@
1
+ h1 Edit
2
+
3
+ - if errors.any?
4
+ - errors.each do |error|
5
+ p.error #{error}
6
+ - else
7
+ p
8
+ | No Errors
9
+
10
+ p
11
+ = pretty_id
@@ -0,0 +1,2 @@
1
+ p
2
+ = greeting
@@ -10,8 +10,8 @@ RSpec.describe 'exposures' do
10
10
  config.default_format = :html
11
11
  end
12
12
 
13
- expose :users do |input|
14
- input.fetch(:users).map { |user|
13
+ expose :users do |users:|
14
+ users.map { |user|
15
15
  user.merge(name: user[:name].upcase)
16
16
  }
17
17
  end
@@ -43,8 +43,8 @@ RSpec.describe 'exposures' do
43
43
  @prefix = "My friend "
44
44
  end
45
45
 
46
- expose :users do |input|
47
- input.fetch(:users).map { |user|
46
+ expose :users do |users:|
47
+ users.map { |user|
48
48
  user.merge(name: prefix + user[:name])
49
49
  }
50
50
  end
@@ -73,8 +73,8 @@ RSpec.describe 'exposures' do
73
73
 
74
74
  private
75
75
 
76
- def users(input)
77
- input.fetch(:users).map { |user|
76
+ def users(users:)
77
+ users.map { |user|
78
78
  user.merge(name: user[:name].upcase)
79
79
  }
80
80
  end
@@ -112,6 +112,40 @@ RSpec.describe 'exposures' do
112
112
  )
113
113
  end
114
114
 
115
+ it 'using default values' do
116
+ vc = Class.new(Dry::View::Controller) do
117
+ configure do |config|
118
+ config.paths = SPEC_ROOT.join('fixtures/templates')
119
+ config.layout = 'app'
120
+ config.template = 'users'
121
+ config.default_format = :html
122
+ end
123
+
124
+ expose :users, default: [{name: 'John', email: 'john@william.org'}]
125
+ end.new
126
+
127
+ expect(vc.(context: context)).to eql(
128
+ '<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><div class="users"><table><tbody><tr><td>John</td><td>john@william.org</td></tr></tbody></table></div><img src="mindblown.jpg" /></body></html>'
129
+ )
130
+ end
131
+
132
+ it 'having default values but passing nil as value for exposure' do
133
+ vc = Class.new(Dry::View::Controller) do
134
+ configure do |config|
135
+ config.paths = SPEC_ROOT.join('fixtures/templates')
136
+ config.layout = 'app'
137
+ config.template = 'greeting'
138
+ config.default_format = :html
139
+ end
140
+
141
+ expose :greeting, default: 'Hello Dry-rb'
142
+ end.new
143
+
144
+ expect(vc.(greeting: nil, context: context)).to eql(
145
+ '<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><p></p></body></html>'
146
+ )
147
+ end
148
+
115
149
  it 'allows exposures to depend on each other' do
116
150
  vc = Class.new(Dry::View::Controller) do
117
151
  configure do |config|
@@ -121,11 +155,9 @@ RSpec.describe 'exposures' do
121
155
  config.default_format = :html
122
156
  end
123
157
 
124
- expose :users do |input|
125
- input.fetch(:users)
126
- end
158
+ expose :users
127
159
 
128
- expose :users_count do |users|
160
+ expose :users_count do |users:|
129
161
  "#{users.length} users"
130
162
  end
131
163
  end.new
@@ -140,6 +172,75 @@ RSpec.describe 'exposures' do
140
172
  )
141
173
  end
142
174
 
175
+ it 'allows exposures to depend on each other and access keywords args from input' do
176
+ vc = Class.new(Dry::View::Controller) do
177
+ configure do |config|
178
+ config.paths = SPEC_ROOT.join('fixtures/templates')
179
+ config.layout = 'app'
180
+ config.template = 'greeting'
181
+ config.default_format = :html
182
+ end
183
+
184
+ expose :greeting do |prefix, greeting:|
185
+ "#{prefix} #{greeting}"
186
+ end
187
+
188
+ expose :prefix do
189
+ 'Hello'
190
+ end
191
+ end.new
192
+
193
+ expect(vc.(greeting: 'From dry-view internals', context: context)).to eql(
194
+ '<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><p>Hello From dry-view internals</p></body></html>'
195
+ )
196
+ end
197
+
198
+ it 'set default values for keyword arguments' do
199
+ vc = Class.new(Dry::View::Controller) do
200
+ configure do |config|
201
+ config.paths = SPEC_ROOT.join('fixtures/templates')
202
+ config.layout = 'app'
203
+ config.template = 'greeting'
204
+ config.default_format = :html
205
+ end
206
+
207
+ expose :greeting do |prefix, greeting: 'From the defaults'|
208
+ "#{prefix} #{greeting}"
209
+ end
210
+
211
+ expose :prefix do
212
+ 'Hello'
213
+ end
214
+ end.new
215
+
216
+ expect(vc.(context: context)).to eql(
217
+ '<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><p>Hello From the defaults</p></body></html>'
218
+ )
219
+ end
220
+
221
+ it 'only pass keywords arguments that are needit in the block and allow for default values' do
222
+ vc = Class.new(Dry::View::Controller) do
223
+ configure do |config|
224
+ config.paths = SPEC_ROOT.join('fixtures/templates')
225
+ config.layout = 'app'
226
+ config.template = 'edit'
227
+ config.default_format = :html
228
+ end
229
+
230
+ expose :pretty_id do |id:|
231
+ "Beautiful #{id}"
232
+ end
233
+
234
+ expose :errors do |errors: []|
235
+ errors
236
+ end
237
+ end.new
238
+
239
+ expect(vc.(id: 1, context: context)).to eql(
240
+ '<!DOCTYPE html><html><head><title>dry-view rocks!</title></head><body><h1>Edit</h1><p>No Errors</p><p>Beautiful 1</p></body></html>'
241
+ )
242
+ end
243
+
143
244
  it 'supports defining multiple exposures at once' do
144
245
  vc = Class.new(Dry::View::Controller) do
145
246
  configure do |config|
@@ -153,11 +254,7 @@ RSpec.describe 'exposures' do
153
254
 
154
255
  private
155
256
 
156
- def users(input)
157
- input.fetch(:users)
158
- end
159
-
160
- def users_count(users)
257
+ def users_count(users:)
161
258
  "#{users.length} users"
162
259
  end
163
260
  end.new
@@ -185,11 +282,9 @@ RSpec.describe 'exposures' do
185
282
  "COUNT: "
186
283
  end
187
284
 
188
- expose :users do |input|
189
- input.fetch(:users)
190
- end
285
+ expose :users
191
286
 
192
- expose :users_count do |prefix, users|
287
+ expose :users_count do |prefix, users:|
193
288
  "#{prefix}#{users.length} users"
194
289
  end
195
290
  end.new
@@ -6,7 +6,7 @@ if RUBY_ENGINE == 'ruby'
6
6
  end
7
7
 
8
8
  begin
9
- require 'byebug'
9
+ require 'pry-byebug'
10
10
  rescue LoadError; end
11
11
 
12
12
  SPEC_ROOT = Pathname(__FILE__).dirname
@@ -1,18 +1,13 @@
1
1
  RSpec.describe Dry::View::Controller do
2
- subject(:layout) { layout_class.new }
3
-
4
- let(:layout_class) do
5
- klass = Class.new(Dry::View::Controller)
6
-
7
- klass.configure do |config|
8
- config.paths = SPEC_ROOT.join('fixtures/templates')
9
- config.layout = 'app'
10
- config.template = 'user'
11
- config.default_format = :html
12
- end
13
-
14
- klass
15
- end
2
+ subject(:controller) {
3
+ Class.new(Dry::View::Controller) do
4
+ configure do |config|
5
+ config.paths = SPEC_ROOT.join('fixtures/templates')
6
+ config.layout = 'app'
7
+ config.template = 'user'
8
+ end
9
+ end.new
10
+ }
16
11
 
17
12
  let(:page) do
18
13
  double(:page, title: 'Test')
@@ -22,15 +17,21 @@ RSpec.describe Dry::View::Controller do
22
17
  { context: page, locals: { user: { name: 'Jane' }, header: { title: 'User' } } }
23
18
  end
24
19
 
25
- let(:renderer) do
26
- layout.class.renderers[:html]
27
- end
28
-
29
20
  describe '#call' do
30
21
  it 'renders template within the layout' do
31
- expect(layout.(options)).to eql(
22
+ expect(controller.(options)).to eql(
32
23
  '<!DOCTYPE html><html><head><title>Test</title></head><body><h1>User</h1><p>Jane</p></body></html>'
33
24
  )
34
25
  end
26
+
27
+ it 'provides a meaningful error if the template name is missing' do
28
+ controller = Class.new(Dry::View::Controller) do
29
+ configure do |config|
30
+ config.paths = SPEC_ROOT.join('fixtures/templates')
31
+ end
32
+ end.new
33
+
34
+ expect { controller.(options) }.to raise_error Dry::View::Controller::UndefinedTemplateError
35
+ end
35
36
  end
36
37
  end
@@ -1,8 +1,9 @@
1
1
  RSpec.describe Dry::View::Exposure do
2
- subject(:exposure) { described_class.new(:hello, proc, object) }
2
+ subject(:exposure) { described_class.new(:hello, proc, object, **options) }
3
3
 
4
4
  let(:proc) { -> input { "hi" } }
5
5
  let(:object) { nil }
6
+ let(:options) { {} }
6
7
 
7
8
  describe "initialization and attributes" do
8
9
  describe "#name" do
@@ -42,6 +43,17 @@ RSpec.describe Dry::View::Exposure do
42
43
  expect(described_class.new(:hello, private: true)).to be_private
43
44
  end
44
45
  end
46
+
47
+ describe "#default_value" do
48
+ it "is nil by default" do
49
+ expect(exposure.default_value).to be_nil
50
+ end
51
+
52
+ it "can be set on initialization" do
53
+ exposuse = described_class.new(:hello, default: 'Hi !')
54
+ expect(exposuse.default_value).to eq('Hi !')
55
+ end
56
+ end
45
57
  end
46
58
 
47
59
  describe "#bind" do
@@ -90,12 +102,12 @@ RSpec.describe Dry::View::Exposure do
90
102
  end
91
103
  end
92
104
 
93
- describe "#dependencies" do
105
+ describe "#dependency_names" do
94
106
  context "proc provided" do
95
107
  let(:proc) { -> input, foo, bar { "hi" } }
96
108
 
97
109
  it "returns an array of exposure dependencies derived from the proc's argument names" do
98
- expect(exposure.dependencies).to eql [:input, :foo, :bar]
110
+ expect(exposure.dependency_names).to eql [:input, :foo, :bar]
99
111
  end
100
112
  end
101
113
 
@@ -111,7 +123,7 @@ RSpec.describe Dry::View::Exposure do
111
123
  end
112
124
 
113
125
  it "returns an array of exposure dependencies derived from the instance method's argument names" do
114
- expect(exposure.dependencies).to eql [:input, :bar, :baz]
126
+ expect(exposure.dependency_names).to eql [:input, :bar, :baz]
115
127
  end
116
128
  end
117
129
 
@@ -119,28 +131,56 @@ RSpec.describe Dry::View::Exposure do
119
131
  let(:proc) { nil }
120
132
 
121
133
  it "returns no dependencies" do
122
- expect(exposure.dependencies).to eql []
134
+ expect(exposure.dependency_names).to eql []
123
135
  end
124
136
  end
125
137
  end
126
138
 
127
139
  describe "#call" do
128
- let(:input) { "input" }
140
+ let(:input) { {name: "Jane"} }
129
141
 
130
142
  context "proc expects input only" do
131
- let(:proc) { -> input { input } }
143
+ let(:proc) { -> name: { name } }
132
144
 
133
145
  it "sends the input to the proc" do
134
- expect(exposure.(input)).to eql "input"
146
+ expect(exposure.(input)).to eql "Jane"
135
147
  end
136
148
  end
137
149
 
138
150
  context "proc expects input and dependencies" do
139
- let(:proc) { -> input, greeting { "#{greeting}, #{input}" } }
151
+ let(:proc) { -> greeting, name: { "#{greeting}, #{name}" } }
140
152
  let(:locals) { {greeting: "Hola"} }
141
153
 
142
154
  it "sends the input and dependency values to the proc" do
143
- expect(exposure.(input, locals)).to eq "Hola, input"
155
+ expect(exposure.(input, locals)).to eq "Hola, Jane"
156
+ end
157
+ end
158
+
159
+ context "Default value" do
160
+ let(:options) { { default: "John" } }
161
+
162
+ context "use default value" do
163
+ let(:proc) { nil }
164
+
165
+ it "use the default value" do
166
+ expect(exposure.({})).to eq "John"
167
+ end
168
+ end
169
+
170
+ context "use input value instead of default" do
171
+ let(:proc) { nil }
172
+
173
+ it "use the default value" do
174
+ expect(exposure.({hello: "Jane"})).to eq "Jane"
175
+ end
176
+ end
177
+
178
+ context "use input value over default even when input is nil" do
179
+ let(:proc) { nil }
180
+
181
+ it "use the default value" do
182
+ expect(exposure.({hello: nil})).to eq nil
183
+ end
144
184
  end
145
185
  end
146
186
 
@@ -154,20 +194,20 @@ RSpec.describe Dry::View::Exposure do
154
194
  end
155
195
 
156
196
  context "proc accesses object instance" do
157
- let(:proc) { -> input { "#{input} from #{name}" } }
197
+ let(:proc) { -> name: { "My name is #{name} but call me #{title} #{name}" } }
158
198
 
159
199
  let(:object) do
160
200
  Class.new do
161
- attr_reader :name
201
+ attr_reader :title
162
202
 
163
- def initialize(name)
164
- @name = name
203
+ def initialize(title)
204
+ @title = title
165
205
  end
166
- end.new("Jane")
206
+ end.new("Dr")
167
207
  end
168
208
 
169
209
  it "makes the instance available as self" do
170
- expect(exposure.(input)).to eq "input from Jane"
210
+ expect(exposure.(input)).to eq "My name is Jane but call me Dr Jane"
171
211
  end
172
212
  end
173
213
 
@@ -9,7 +9,7 @@ RSpec.describe Dry::View::Exposures do
9
9
 
10
10
  describe "#add" do
11
11
  it "creates and adds an exposure" do
12
- proc = -> input { "hi" }
12
+ proc = -> **input { "hi" }
13
13
  exposures.add :hello, proc
14
14
 
15
15
  expect(exposures[:hello].name).to eq :hello
@@ -43,7 +43,7 @@ RSpec.describe Dry::View::Exposures do
43
43
 
44
44
  describe "#locals" do
45
45
  before do
46
- exposures.add(:greeting, -> input { input.fetch(:greeting).upcase })
46
+ exposures.add(:greeting, -> greeting: { greeting.upcase })
47
47
  exposures.add(:farewell, -> greeting { "#{greeting} and goodbye" })
48
48
  end
49
49
 
@@ -54,10 +54,40 @@ RSpec.describe Dry::View::Exposures do
54
54
  end
55
55
 
56
56
  it "does not return any values from private exposures" do
57
- exposures.add(:hidden, -> input { "shh" }, private: true)
57
+ exposures.add(:hidden, -> **input { "shh" }, private: true)
58
58
 
59
59
  expect(locals).to include(:greeting, :farewell)
60
60
  expect(locals).not_to include(:hidden)
61
61
  end
62
62
  end
63
+
64
+ describe "#locals default value" do
65
+ it "returns 'default_value' from exposure" do
66
+ exposures.add(:name, default: 'John')
67
+ locals = exposures.locals({})
68
+
69
+ expect(locals).to eq(:name=>"John")
70
+ end
71
+
72
+ it "returns values from arguments" do
73
+ exposures.add(:name, default: 'John')
74
+ locals = exposures.locals(name: 'William')
75
+
76
+ expect(locals).to eq(:name=>"William")
77
+ end
78
+
79
+ it "returns values from arguments even when value is nil" do
80
+ exposures.add(:name, default: 'John')
81
+ locals = exposures.locals(name: nil)
82
+
83
+ expect(locals).to eq(:name=>nil)
84
+ end
85
+
86
+ it "returns value from proc" do
87
+ exposures.add(:name, -> name: { name.upcase }, default: 'John')
88
+ locals = exposures.locals(name: 'William')
89
+
90
+ expect(locals).to eq(:name=>"WILLIAM")
91
+ end
92
+ end
63
93
  end
@@ -5,61 +5,76 @@ RSpec::Matchers.define :template_scope do |locals|
5
5
  end
6
6
 
7
7
  RSpec.describe Dry::View::Part do
8
- subject(:part) { described_class.new(name: name, value: value, renderer: renderer, context: context) }
8
+ context 'with a renderer' do
9
+ subject(:part) { described_class.new(name: name, value: value, renderer: renderer, context: context) }
9
10
 
10
- let(:name) { :user }
11
- let(:value) { double('value') }
12
- let(:context) { double('context') }
13
- let(:renderer) { double('renderer') }
11
+ let(:name) { :user }
12
+ let(:value) { double(:value) }
13
+ let(:context) { double(:context) }
14
+ let(:renderer) { spy(:renderer) }
14
15
 
15
- describe '#render' do
16
- before do
17
- allow(renderer).to receive(:lookup).with('_info').and_return '_info.html.erb'
18
- allow(renderer).to receive(:render)
19
- end
16
+ describe '#render' do
17
+ it 'renders a partial with the part available in its scope' do
18
+ part.render(:info)
19
+ expect(renderer).to have_received(:partial).with(:info, template_scope(user: part))
20
+ end
20
21
 
21
- it 'renders a partial with the part available in its scope' do
22
- part.render(:info)
23
- expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(user: part))
24
- end
22
+ it 'allows the part to be made available on a different name' do
23
+ part.render(:info, as: :admin)
24
+ expect(renderer).to have_received(:partial).with(:info, template_scope(admin: part))
25
+ end
25
26
 
26
- it 'allows the part to be made available on a different name' do
27
- part.render(:info, as: :admin)
28
- expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(admin: part))
27
+ it 'includes extra locals in the scope' do
28
+ part.render(:info, extra_local: "hello")
29
+ expect(renderer).to have_received(:partial).with(:info, template_scope(user: part, extra_local: "hello"))
30
+ end
29
31
  end
30
32
 
31
- it 'includes extra locals in the scope' do
32
- part.render(:info, extra_local: "hello")
33
- expect(renderer).to have_received(:render).with('_info.html.erb', template_scope(user: part, extra_local: "hello"))
34
- end
35
- end
33
+ describe '#to_s' do
34
+ before do
35
+ allow(value).to receive(:to_s).and_return 'to_s on the value'
36
+ end
36
37
 
37
- describe '#to_s' do
38
- before do
39
- allow(value).to receive(:to_s).and_return 'to_s on the value'
38
+ it 'delegates to the wrapped value' do
39
+ expect(part.to_s).to eq 'to_s on the value'
40
+ end
40
41
  end
41
42
 
42
- it 'delegates to the wrapped value' do
43
- expect(part.to_s).to eq 'to_s on the value'
44
- end
45
- end
43
+ describe '#method_missing' do
44
+ let(:value) { double(greeting: 'hello from value') }
46
45
 
47
- describe '#method_missing' do
48
- let(:value) { double(greeting: 'hello from value') }
46
+ it 'calls a matching method on the value' do
47
+ expect(part.greeting).to eq 'hello from value'
48
+ end
49
49
 
50
- it 'calls a matching method on the value' do
51
- expect(part.greeting).to eq 'hello from value'
52
- end
50
+ it 'forwards all arguments to the method' do
51
+ blk = -> { }
52
+ part.greeting 'args', &blk
53
53
 
54
- it 'forwards all arguments to the method' do
55
- blk = -> { }
56
- part.greeting 'args', &blk
54
+ expect(value).to have_received(:greeting).with('args', &blk)
55
+ end
57
56
 
58
- expect(value).to have_received(:greeting).with('args', &blk)
57
+ it 'raises an error if no metho matches' do
58
+ expect { part.farewell }.to raise_error(NoMethodError)
59
+ end
59
60
  end
61
+ end
62
+
63
+ context 'without a renderer' do
64
+ subject(:part) { described_class.new(name: name, value: value, context: context) }
65
+
66
+ let(:name) { :user }
67
+ let(:value) { double('value') }
68
+ let(:context) { double('context') }
69
+
70
+ describe '#new' do
71
+ it 'can be initialized' do
72
+ expect(part).to be_an_instance_of(Dry::View::Part)
73
+ end
60
74
 
61
- it 'raises an error if no metho matches' do
62
- expect { part.farewell }.to raise_error(NoMethodError)
75
+ it 'raises an exception when render is called' do
76
+ expect { part.render(:info) }.to raise_error(Dry::View::MissingRendererError).with_message('No renderer provided')
77
+ end
63
78
  end
64
79
  end
65
80
  end
@@ -3,28 +3,55 @@ require 'dry/view/renderer'
3
3
 
4
4
  RSpec.describe Dry::View::Renderer do
5
5
  subject(:renderer) do
6
- Dry::View::Renderer.new([Dry::View::Path.new(SPEC_ROOT.join('fixtures/templates'))], format: 'html')
6
+ Dry::View::Renderer.new(
7
+ [Dry::View::Path.new(SPEC_ROOT.join('fixtures/templates'))],
8
+ format: 'html'
9
+ )
7
10
  end
8
11
 
9
12
  let(:scope) { double(:scope) }
10
13
 
11
- describe '#call' do
12
- it 'renders template' do
13
- expect(renderer.('hello', scope)).to eql('<h1>Hello</h1>')
14
+ describe '#template' do
15
+ it 'renders template in current directory' do
16
+ expect(renderer.template(:hello, scope)).to eql('<h1>Hello</h1>')
14
17
  end
15
18
 
16
- it 'looks up shared template in current dir' do
17
- expect(renderer.('_shared_hello', scope)).to eql('<h1>Hello</h1>')
19
+ it 'renders template in shared/ subdirectory' do
20
+ expect(renderer.template(:_shared_hello, scope)).to eql('<h1>Hello</h1>')
18
21
  end
19
22
 
20
- it 'looks up shared template in upper dir' do
21
- expect(renderer.chdir('greetings').('_shared_hello', scope)).to eql('<h1>Hello</h1>')
23
+ it 'renders template in upper directory' do
24
+ expect(renderer.chdir('nested').template(:_shared_hello, scope)).to eql('<h1>Hello</h1>')
22
25
  end
23
26
 
24
- it 'raises error when template was not found' do
27
+ it 'raises error when template cannot be found' do
25
28
  expect {
26
- renderer.('not_found', scope)
27
- }.to raise_error(Dry::View::Renderer::TemplateNotFoundError, /not_found/)
29
+ renderer.template(:missing_template, scope)
30
+ }.to raise_error(Dry::View::Renderer::TemplateNotFoundError, /missing_template/)
31
+ end
32
+ end
33
+
34
+ describe '#partial' do
35
+ it 'renders partial in current directory' do
36
+ expect(renderer.partial(:hello, scope)).to eql('<h1>Partial hello</h1>')
37
+ end
38
+
39
+ it 'renders partial in shared/ subdirectory' do
40
+ expect(renderer.partial(:shared_hello, scope)).to eql('<h1>Hello</h1>')
41
+ end
42
+
43
+ it 'renders partial in upper directory' do
44
+ expect(renderer.chdir('nested').partial(:hello, scope)).to eql('<h1>Partial hello</h1>')
45
+ end
46
+
47
+ it 'renders partial in upper shared/ subdirectory' do
48
+ expect(renderer.chdir('nested').partial(:shared_hello, scope)).to eql('<h1>Hello</h1>')
49
+ end
50
+
51
+ it 'raises error when partial cannot be found' do
52
+ expect {
53
+ renderer.partial(:missing_partial, scope)
54
+ }.to raise_error(Dry::View::Renderer::TemplateNotFoundError, /_missing_partial/)
28
55
  end
29
56
  end
30
57
  end
@@ -2,25 +2,20 @@ RSpec.describe Dry::View::Scope do
2
2
  subject(:scope) { described_class.new(renderer: renderer, context: context, locals: locals) }
3
3
 
4
4
  let(:locals) { {} }
5
- let(:context) { double('context') }
6
- let(:renderer) { double('renderer') }
5
+ let(:context) { double(:context) }
6
+ let(:renderer) { spy(:renderer) }
7
7
 
8
8
  describe '#render' do
9
- before do
10
- allow(renderer).to receive(:lookup).with('_info').and_return '_info.html.erb'
11
- allow(renderer).to receive(:render)
12
- end
13
-
14
9
  it 'renders a partial with itself as the scope' do
15
10
  scope.render(:info)
16
- expect(renderer).to have_received(:render).with('_info.html.erb', scope)
11
+ expect(renderer).to have_received(:partial).with(:info, scope)
17
12
  end
18
13
 
19
14
  it 'renders a partial with provided locals' do
20
15
  scope_with_locals = described_class.new(renderer: renderer, context: context, locals: {foo: 'bar'})
21
16
 
22
17
  scope.render(:info, foo: 'bar')
23
- expect(renderer).to have_received(:render).with('_info.html.erb', scope_with_locals)
18
+ expect(renderer).to have_received(:partial).with(:info, scope_with_locals)
24
19
  end
25
20
  end
26
21
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dry-view
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Solnica
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-05-14 00:00:00.000000000 Z
12
+ date: 2017-11-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tilt
@@ -121,6 +121,7 @@ files:
121
121
  - ".rspec"
122
122
  - ".travis.yml"
123
123
  - CHANGELOG.md
124
+ - CONTRIBUTING.md
124
125
  - Gemfile
125
126
  - LICENSE.md
126
127
  - README.md
@@ -135,13 +136,17 @@ files:
135
136
  - lib/dry/view/decorator.rb
136
137
  - lib/dry/view/exposure.rb
137
138
  - lib/dry/view/exposures.rb
139
+ - lib/dry/view/missing_renderer.rb
138
140
  - lib/dry/view/part.rb
139
141
  - lib/dry/view/path.rb
140
142
  - lib/dry/view/renderer.rb
141
143
  - lib/dry/view/scope.rb
142
144
  - lib/dry/view/version.rb
145
+ - spec/fixtures/templates/_hello.html.slim
143
146
  - spec/fixtures/templates/decorated_parts.html.slim
147
+ - spec/fixtures/templates/edit.html.slim
144
148
  - spec/fixtures/templates/empty.html.slim
149
+ - spec/fixtures/templates/greeting.html.slim
145
150
  - spec/fixtures/templates/hello.html.slim
146
151
  - spec/fixtures/templates/layouts/app.html.slim
147
152
  - spec/fixtures/templates/layouts/app.txt.erb
@@ -180,7 +185,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
185
  requirements:
181
186
  - - ">="
182
187
  - !ruby/object:Gem::Version
183
- version: 2.1.0
188
+ version: 2.2.0
184
189
  required_rubygems_version: !ruby/object:Gem::Requirement
185
190
  requirements:
186
191
  - - ">="
@@ -188,13 +193,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
188
193
  version: '0'
189
194
  requirements: []
190
195
  rubyforge_project:
191
- rubygems_version: 2.6.10
196
+ rubygems_version: 2.6.11
192
197
  signing_key:
193
198
  specification_version: 4
194
199
  summary: Functional view rendering system
195
200
  test_files:
201
+ - spec/fixtures/templates/_hello.html.slim
196
202
  - spec/fixtures/templates/decorated_parts.html.slim
203
+ - spec/fixtures/templates/edit.html.slim
197
204
  - spec/fixtures/templates/empty.html.slim
205
+ - spec/fixtures/templates/greeting.html.slim
198
206
  - spec/fixtures/templates/hello.html.slim
199
207
  - spec/fixtures/templates/layouts/app.html.slim
200
208
  - spec/fixtures/templates/layouts/app.txt.erb