curlybars 0.9.13

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/lib/curlybars.rb +108 -0
  3. data/lib/curlybars/configuration.rb +41 -0
  4. data/lib/curlybars/dependency_tracker.rb +8 -0
  5. data/lib/curlybars/error/base.rb +18 -0
  6. data/lib/curlybars/error/compile.rb +11 -0
  7. data/lib/curlybars/error/lex.rb +22 -0
  8. data/lib/curlybars/error/parse.rb +41 -0
  9. data/lib/curlybars/error/presenter/not_found.rb +23 -0
  10. data/lib/curlybars/error/render.rb +11 -0
  11. data/lib/curlybars/error/validate.rb +18 -0
  12. data/lib/curlybars/lexer.rb +60 -0
  13. data/lib/curlybars/method_whitelist.rb +69 -0
  14. data/lib/curlybars/node/block_helper_else.rb +108 -0
  15. data/lib/curlybars/node/boolean.rb +24 -0
  16. data/lib/curlybars/node/each_else.rb +69 -0
  17. data/lib/curlybars/node/if_else.rb +33 -0
  18. data/lib/curlybars/node/item.rb +31 -0
  19. data/lib/curlybars/node/literal.rb +28 -0
  20. data/lib/curlybars/node/option.rb +25 -0
  21. data/lib/curlybars/node/output.rb +24 -0
  22. data/lib/curlybars/node/partial.rb +24 -0
  23. data/lib/curlybars/node/path.rb +137 -0
  24. data/lib/curlybars/node/root.rb +29 -0
  25. data/lib/curlybars/node/string.rb +24 -0
  26. data/lib/curlybars/node/template.rb +32 -0
  27. data/lib/curlybars/node/text.rb +24 -0
  28. data/lib/curlybars/node/unless_else.rb +33 -0
  29. data/lib/curlybars/node/variable.rb +34 -0
  30. data/lib/curlybars/node/with_else.rb +54 -0
  31. data/lib/curlybars/parser.rb +183 -0
  32. data/lib/curlybars/position.rb +7 -0
  33. data/lib/curlybars/presenter.rb +288 -0
  34. data/lib/curlybars/processor/tilde.rb +31 -0
  35. data/lib/curlybars/processor/token_factory.rb +9 -0
  36. data/lib/curlybars/railtie.rb +18 -0
  37. data/lib/curlybars/rendering_support.rb +222 -0
  38. data/lib/curlybars/safe_buffer.rb +11 -0
  39. data/lib/curlybars/template_handler.rb +93 -0
  40. data/lib/curlybars/version.rb +3 -0
  41. data/spec/acceptance/application_layout_spec.rb +60 -0
  42. data/spec/acceptance/collection_blocks_spec.rb +28 -0
  43. data/spec/acceptance/global_helper_spec.rb +25 -0
  44. data/spec/curlybars/configuration_spec.rb +57 -0
  45. data/spec/curlybars/error/base_spec.rb +41 -0
  46. data/spec/curlybars/error/compile_spec.rb +19 -0
  47. data/spec/curlybars/error/lex_spec.rb +25 -0
  48. data/spec/curlybars/error/parse_spec.rb +74 -0
  49. data/spec/curlybars/error/render_spec.rb +19 -0
  50. data/spec/curlybars/error/validate_spec.rb +19 -0
  51. data/spec/curlybars/lexer_spec.rb +466 -0
  52. data/spec/curlybars/method_whitelist_spec.rb +168 -0
  53. data/spec/curlybars/processor/tilde_spec.rb +60 -0
  54. data/spec/curlybars/rendering_support_spec.rb +426 -0
  55. data/spec/curlybars/safe_buffer_spec.rb +46 -0
  56. data/spec/curlybars/template_handler_spec.rb +222 -0
  57. data/spec/integration/cache_spec.rb +124 -0
  58. data/spec/integration/comment_spec.rb +60 -0
  59. data/spec/integration/exception_spec.rb +31 -0
  60. data/spec/integration/node/block_helper_else_spec.rb +422 -0
  61. data/spec/integration/node/each_else_spec.rb +204 -0
  62. data/spec/integration/node/each_spec.rb +291 -0
  63. data/spec/integration/node/escape_spec.rb +27 -0
  64. data/spec/integration/node/helper_spec.rb +176 -0
  65. data/spec/integration/node/if_else_spec.rb +129 -0
  66. data/spec/integration/node/if_spec.rb +143 -0
  67. data/spec/integration/node/output_spec.rb +68 -0
  68. data/spec/integration/node/partial_spec.rb +66 -0
  69. data/spec/integration/node/path_spec.rb +286 -0
  70. data/spec/integration/node/root_spec.rb +15 -0
  71. data/spec/integration/node/template_spec.rb +86 -0
  72. data/spec/integration/node/unless_else_spec.rb +129 -0
  73. data/spec/integration/node/unless_spec.rb +130 -0
  74. data/spec/integration/node/with_spec.rb +116 -0
  75. data/spec/integration/processor/tilde_spec.rb +38 -0
  76. data/spec/integration/processors_spec.rb +30 -0
  77. metadata +358 -0
@@ -0,0 +1,168 @@
1
+ describe Curlybars::MethodWhitelist do
2
+ let(:dummy_class) { Class.new { extend Curlybars::MethodWhitelist } }
3
+
4
+ describe "#allowed_methods" do
5
+ it "returns an empty array as default" do
6
+ expect(dummy_class.new.allowed_methods).to eq([])
7
+ end
8
+ end
9
+
10
+ describe ".allow_methods" do
11
+ before do
12
+ link_presenter = Class.new
13
+ article_presenter = Class.new
14
+
15
+ dummy_class.class_eval do
16
+ allow_methods :cook, link: link_presenter, article: article_presenter
17
+ end
18
+ end
19
+
20
+ it "sets the allowed methods" do
21
+ expect(dummy_class.new.allowed_methods).to eq([:cook, :link, :article])
22
+ end
23
+
24
+ it "raises when collection is not of presenters" do
25
+ expect do
26
+ dummy_class.class_eval { allow_methods :cook, links: ["foobar"] }
27
+ end.to raise_error(RuntimeError)
28
+ end
29
+
30
+ it "raises when collection cardinality is greater than one" do
31
+ stub_const("OnePresenter", Class.new { extend Curlybars::MethodWhitelist })
32
+ stub_const("OtherPresenter", Class.new { extend Curlybars::MethodWhitelist })
33
+
34
+ expect do
35
+ dummy_class.class_eval { allow_methods :cook, links: [OnePresenter, OtherPresenter] }
36
+ end.to raise_error(RuntimeError)
37
+ end
38
+ end
39
+
40
+ describe "inheritance and composition" do
41
+ let(:base_presenter) do
42
+ stub_const("LinkPresenter", Class.new)
43
+
44
+ Class.new do
45
+ extend Curlybars::MethodWhitelist
46
+ allow_methods :cook, link: LinkPresenter
47
+ end
48
+ end
49
+
50
+ let(:helpers) do
51
+ Module.new do
52
+ extend Curlybars::MethodWhitelist
53
+ allow_methods :form
54
+ end
55
+ end
56
+
57
+ let(:post_presenter) do
58
+ Class.new(base_presenter) do
59
+ extend Curlybars::MethodWhitelist
60
+ include Helpers
61
+ allow_methods :wave
62
+ end
63
+ end
64
+
65
+ before do
66
+ stub_const("Helpers", helpers)
67
+ end
68
+
69
+ it "allows methods from inheritance and composition" do
70
+ expect(post_presenter.new.allowed_methods).to eq([:cook, :link, :form, :wave])
71
+ end
72
+
73
+ it "returns a dependency_tree with inheritance and composition" do
74
+ expect(post_presenter.dependency_tree).
75
+ to eq(
76
+ cook: nil,
77
+ link: LinkPresenter,
78
+ form: nil,
79
+ wave: nil
80
+ )
81
+ end
82
+ end
83
+
84
+ describe ".methods_schema" do
85
+ it "setups a schema propagating nil" do
86
+ stub_const("LinkPresenter", Class.new { extend Curlybars::MethodWhitelist })
87
+ dummy_class.class_eval { allow_methods :cook }
88
+
89
+ expect(dummy_class.methods_schema).to eq(cook: nil)
90
+ end
91
+
92
+ it "setups a schema any random type" do
93
+ stub_const("LinkPresenter", Class.new { extend Curlybars::MethodWhitelist })
94
+ dummy_class.class_eval { allow_methods something: :foobar }
95
+
96
+ expect(dummy_class.methods_schema).to eq(something: :foobar)
97
+ end
98
+
99
+ it "setups a schema propagating the return type of the method" do
100
+ stub_const("ArticlePresenter", Class.new { extend Curlybars::MethodWhitelist })
101
+ dummy_class.class_eval { allow_methods article: ArticlePresenter }
102
+
103
+ expect(dummy_class.methods_schema).to eq(article: ArticlePresenter)
104
+ end
105
+
106
+ it "setups a schema propagating a collection" do
107
+ stub_const("LinkPresenter", Class.new { extend Curlybars::MethodWhitelist })
108
+ dummy_class.class_eval { allow_methods links: [LinkPresenter] }
109
+
110
+ expect(dummy_class.methods_schema).to eq(links: [LinkPresenter])
111
+ end
112
+
113
+ it "supports procs in schema" do
114
+ dummy_class.class_eval { allow_methods settings: ->() { { color_1: nil } } }
115
+
116
+ expect(dummy_class.methods_schema).to eq(settings: { color_1: nil })
117
+ end
118
+
119
+ it "supports procs with arguments in schema" do
120
+ dummy_class.class_eval { allow_methods settings: ->(name) { Hash[name, nil] } }
121
+
122
+ expect(dummy_class.methods_schema(:background_color)).to eq(settings: { background_color: nil })
123
+ end
124
+ end
125
+
126
+ describe ".dependency_tree" do
127
+ it "returns a dependencies tree" do
128
+ link_presenter = Class.new do
129
+ extend Curlybars::MethodWhitelist
130
+ allow_methods :url
131
+ end
132
+
133
+ article_presenter = Class.new do
134
+ extend Curlybars::MethodWhitelist
135
+ allow_methods :title, :body
136
+ end
137
+
138
+ stub_const("ArticlePresenter", article_presenter)
139
+ stub_const("LinkPresenter", link_presenter)
140
+
141
+ dummy_class.class_eval do
142
+ allow_methods links: [LinkPresenter], article: ArticlePresenter
143
+ end
144
+
145
+ expect(dummy_class.dependency_tree).
146
+ to eq(
147
+ links: [{ url: nil }],
148
+ article: {
149
+ title: nil,
150
+ body: nil
151
+ }
152
+ )
153
+ end
154
+
155
+ it "propagates arguments" do
156
+ dummy_class.class_eval do
157
+ allow_methods label: ->(label) { Hash[label, nil] }
158
+ end
159
+
160
+ expect(dummy_class.dependency_tree(:some_label)).
161
+ to eq(
162
+ label: {
163
+ some_label: nil
164
+ }
165
+ )
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,60 @@
1
+ describe Curlybars::Processor::Tilde do
2
+ let(:tilde_start_token) { double(:tilde_start, type: :TILDE_START, value: nil, position: nil) }
3
+ let(:tilde_end_token) { double(:tilde_end, type: :TILDE_END, value: nil, position: nil) }
4
+ let(:start_token) { double(:start, type: :START, value: nil, position: nil) }
5
+ let(:end_token) { double(:end, type: :END, value: nil, position: nil) }
6
+
7
+ describe ":TILDE_START" do
8
+ it "trims the previous text token" do
9
+ tokens = [
10
+ text_token("text \t\r\n"),
11
+ tilde_start_token
12
+ ]
13
+ Curlybars::Processor::Tilde.process!(tokens, 'identifier')
14
+
15
+ expect(tokens.first.value).to eq 'text'
16
+ end
17
+
18
+ it "doesn't trim the previous text token when not right before" do
19
+ tokens = [
20
+ text_token("text \t\r\n"),
21
+ start_token,
22
+ end_token,
23
+ tilde_start_token
24
+ ]
25
+ Curlybars::Processor::Tilde.process!(tokens, 'identifier')
26
+
27
+ expect(tokens.first.value).to eq "text \t\r\n"
28
+ end
29
+ end
30
+
31
+ describe ":TILDE_END" do
32
+ it "trims the following text token" do
33
+ tokens = [
34
+ tilde_end_token,
35
+ text_token("\t\r\n text")
36
+ ]
37
+ Curlybars::Processor::Tilde.process!(tokens, 'identifier')
38
+
39
+ expect(tokens.last.value).to eq 'text'
40
+ end
41
+
42
+ it "doesn't trim the following text token when not right after" do
43
+ tokens = [
44
+ tilde_end_token,
45
+ start_token,
46
+ end_token,
47
+ text_token("\t\r\n text")
48
+ ]
49
+ Curlybars::Processor::Tilde.process!(tokens, 'identifier')
50
+
51
+ expect(tokens.last.value).to eq "\t\r\n text"
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def text_token(value)
58
+ double(:text, type: :TEXT, value: value, position: nil)
59
+ end
60
+ end
@@ -0,0 +1,426 @@
1
+ describe Curlybars::RenderingSupport do
2
+ let(:file_name) { '/app/views/template.hbs' }
3
+ let(:presenter) { double(:presenter, allows_method?: true, meth: :value) }
4
+ let(:contexts) { [presenter] }
5
+ let(:variables) { [{}] }
6
+ let(:rendering) { Curlybars::RenderingSupport.new(1.second, contexts, variables, file_name) }
7
+ let(:position) do
8
+ double(:position, file_name: 'template.hbs', line_number: 1, line_offset: 0)
9
+ end
10
+ let(:block) { -> {} }
11
+
12
+ describe "#check_timeout!" do
13
+ it "skips checking if timeout is nil" do
14
+ rendering = Curlybars::RenderingSupport.new(nil, contexts, variables, file_name)
15
+
16
+ sleep 0.1.seconds
17
+ expect { rendering.check_timeout! }.not_to raise_error
18
+ end
19
+
20
+ it "doesn't happen when rendering is < rendering_timeout" do
21
+ rendering = Curlybars::RenderingSupport.new(10.seconds, contexts, variables, file_name)
22
+ expect { rendering.check_timeout! }.not_to raise_error
23
+ end
24
+
25
+ it "happens and raises when rendering >= rendering_timeout" do
26
+ rendering = Curlybars::RenderingSupport.new(0.01.seconds, contexts, variables, file_name)
27
+
28
+ sleep 0.1.seconds
29
+ expect { rendering.check_timeout! }.to raise_error(Curlybars::Error::Render)
30
+ end
31
+ end
32
+
33
+ describe "#to_bool" do
34
+ describe "returns true" do
35
+ it "with `true`" do
36
+ expect(rendering.to_bool(true)).to be_truthy
37
+ end
38
+
39
+ it "with `[:non_empty]`" do
40
+ expect(rendering.to_bool([:non_empty])).to be_truthy
41
+ end
42
+
43
+ it "with `1`" do
44
+ expect(rendering.to_bool(1)).to be_truthy
45
+ end
46
+ end
47
+
48
+ describe "returns false" do
49
+ it "with `false`" do
50
+ expect(rendering.to_bool(false)).to be_falsey
51
+ end
52
+
53
+ it "with `[]`" do
54
+ expect(rendering.to_bool([])).to be_falsey
55
+ end
56
+
57
+ it "with `{}`" do
58
+ expect(rendering.to_bool({})).to be_falsey
59
+ end
60
+
61
+ it "with `0`" do
62
+ expect(rendering.to_bool(0)).to be_falsey
63
+ end
64
+
65
+ it "with `nil`" do
66
+ expect(rendering.to_bool(nil)).to be_falsey
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#path" do
72
+ it "returns the method in the current context" do
73
+ allow_all_methods(presenter)
74
+ allow(presenter).to receive(:method) { :method }
75
+
76
+ expect(rendering.path('method', rendering.position(0, 1))).to eq :method
77
+ end
78
+
79
+ it "returns the method in the current context" do
80
+ sub = double(:sub_presenter)
81
+ allow_all_methods(sub)
82
+ allow(sub).to receive(:method) { :method }
83
+
84
+ allow_all_methods(presenter)
85
+ allow(presenter).to receive(:sub) { sub }
86
+
87
+ expect(rendering.path('sub.method', rendering.position(0, 1))).to eq :method
88
+ end
89
+
90
+ it "returns the length of a collection, when `lenght` is the last step" do
91
+ allow_all_methods(presenter)
92
+ single_element_presenter = double(:single_element_presenter)
93
+ allow_all_methods(single_element_presenter)
94
+ collection = [single_element_presenter]
95
+ allow(presenter).to receive(:collection) { collection }
96
+
97
+ returned_method = rendering.path('collection.length', rendering.position(0, 1))
98
+ expect(returned_method.call).to eq collection.length
99
+ end
100
+
101
+ it "raises an exception when the method is not allowed" do
102
+ disallow_all_methods(presenter)
103
+ allow(presenter).to receive(:forbidden_method) { :forbidden_method }
104
+
105
+ expect do
106
+ rendering.path('forbidden_method', rendering.position(0, 1))
107
+ end.to raise_error(Curlybars::Error::Render)
108
+ end
109
+
110
+ it "exposes the unallowed method in the exception payload" do
111
+ disallow_all_methods(presenter)
112
+ allow(presenter).to receive(:forbidden_method) { :forbidden_method }
113
+
114
+ begin
115
+ rendering.path('forbidden_method', rendering.position(0, 1))
116
+ rescue Curlybars::Error::Render => e
117
+ expect(e.metadata).to eq(meth: :forbidden_method)
118
+ end
119
+ end
120
+
121
+ it "raises an exception when the context is not a presenter" do
122
+ sub = double(:not_presenter)
123
+ allow(presenter).to receive(:sub) { sub }
124
+
125
+ expect do
126
+ rendering.path('sub.method', rendering.position(0, 1))
127
+ end.to raise_error(Curlybars::Error::Render)
128
+ end
129
+
130
+ it "refers to the second to last presenter in the stack when using `../`" do
131
+ sub = double(:sub_presenter)
132
+ allow_all_methods(sub)
133
+ allow(sub).to receive(:method) { :sub_method }
134
+
135
+ allow_all_methods(presenter)
136
+ allow(presenter).to receive(:method) { :root_method }
137
+
138
+ contexts.push(sub)
139
+
140
+ expect(rendering.path('../method', rendering.position(0, 1))).to eq :root_method
141
+ end
142
+
143
+ it "refers to the third to last presenter in the stack when using `../../`" do
144
+ sub_sub = double(:sub_presenter)
145
+ allow_all_methods(sub_sub)
146
+ allow(sub_sub).to receive(:method) { :sub_sub_method }
147
+
148
+ sub = double(:sub_presenter)
149
+ allow_all_methods(sub)
150
+ allow(sub).to receive(:method) { :sub_method }
151
+
152
+ allow_all_methods(presenter)
153
+ allow(presenter).to receive(:method) { :root_method }
154
+
155
+ contexts.push(sub)
156
+ contexts.push(sub_sub)
157
+
158
+ expect(rendering.path('../../method', rendering.position(0, 1))).to eq :root_method
159
+ end
160
+
161
+ it "returns a method that returns nil, if nil is returned from any method in the chain (except the latter)" do
162
+ allow_all_methods(presenter)
163
+ allow(presenter).to receive(:returns_nil) { nil }
164
+
165
+ outcome = rendering.path('returns_nil.another_method', rendering.position(0, 1)).call
166
+ expect(outcome).to be_nil
167
+ end
168
+
169
+ it "returns a method that returns nil, if `../`` goes too deep in the stack" do
170
+ outcome = rendering.path('../too_deep', rendering.position(0, 1)).call
171
+ expect(outcome).to be_nil
172
+ end
173
+
174
+ it "raises an exception if tha path is too deep (> 10)" do
175
+ expect do
176
+ rendering.path('a.b.c.d.e.f.g.h.i.l', rendering.position(0, 1))
177
+ end.to raise_error(Curlybars::Error::Render)
178
+ end
179
+ end
180
+
181
+ describe "#cached_call" do
182
+ before do
183
+ class APresenter
184
+ def meth
185
+ :value
186
+ end
187
+ end
188
+ end
189
+
190
+ it "(cache miss) calls the method if not cached already" do
191
+ meth = presenter.method(:meth)
192
+ allow(meth).to receive(:call)
193
+
194
+ rendering.cached_call(meth)
195
+
196
+ expect(meth).to have_received(:call).once
197
+ end
198
+
199
+ it "(cache hit) avoids to call a method for more than one time" do
200
+ meth = presenter.method(:meth)
201
+ allow(meth).to receive(:call)
202
+
203
+ rendering.cached_call(meth)
204
+ rendering.cached_call(meth)
205
+
206
+ expect(meth).to have_received(:call).once
207
+ end
208
+
209
+ it "the returned cached value is the same as the uncached one" do
210
+ meth = presenter.method(:meth)
211
+
212
+ first_outcome = rendering.cached_call(meth)
213
+ second_outcome = rendering.cached_call(meth)
214
+
215
+ expect(second_outcome).to eq first_outcome
216
+ end
217
+ end
218
+
219
+ describe "#call" do
220
+ it "calls with no arguments a method with no parameters" do
221
+ method = ->() { :return }
222
+ arguments = []
223
+
224
+ output = rendering.call(method, "method", position, arguments, :options, &block)
225
+ expect(output).to eq :return
226
+ end
227
+
228
+ it "calls with one argument a method with no parameters, discarding the parameter" do
229
+ method = ->() { :return }
230
+ arguments = [:argument]
231
+
232
+ output = rendering.call(method, "method", position, arguments, :options, &block)
233
+ expect(output).to eq :return
234
+ end
235
+
236
+ it "calls a method with only one parameter can only receive the options" do
237
+ method = ->(parameter) { parameter }
238
+ arguments = []
239
+
240
+ output = rendering.call(method, "method", position, arguments, :options, &block)
241
+ expect(output).to eq :options
242
+ end
243
+
244
+ it "calls a method with only one parameter can only receive the options, even with some arguments" do
245
+ method = ->(parameter) { parameter }
246
+ arguments = [:argument]
247
+
248
+ output = rendering.call(method, "method", position, arguments, :options, &block)
249
+ expect(output).to eq :options
250
+ end
251
+
252
+ it "calls a method with two parameter can receive nil as first argument and the options" do
253
+ method = ->(parameter, options) { [parameter, options] }
254
+ arguments = []
255
+
256
+ output = rendering.call(method, "method", position, arguments, :options, &block)
257
+ expect(output).to eq [nil, :options]
258
+ end
259
+
260
+ it "calls a method with two parameter can receive an argument and the options" do
261
+ method = ->(parameter, options) { [parameter, options] }
262
+ arguments = [:argument]
263
+
264
+ output = rendering.call(method, "method", position, arguments, :options, &block)
265
+ expect(output).to eq [:argument, :options]
266
+ end
267
+
268
+ it "calls a method with three parameter can receive two arguments and the options" do
269
+ method = ->(first, second, options) { [first, second, options] }
270
+ arguments = [:first, :second]
271
+
272
+ output = rendering.call(method, "method", position, arguments, :options, &block)
273
+ expect(output).to eq [:first, :second, :options]
274
+ end
275
+
276
+ it "calls a method with three parameter can receive one argument, nil and the options" do
277
+ method = ->(first, second, options) { [first, second, options] }
278
+ arguments = [:first]
279
+
280
+ output = rendering.call(method, "method", position, arguments, :options, &block)
281
+ expect(output).to eq [:first, nil, :options]
282
+ end
283
+
284
+ it "calls a method with three parameter can receive nil, nil and the options" do
285
+ method = ->(first, second, options) { [first, second, options] }
286
+ arguments = []
287
+
288
+ output = rendering.call(method, "method", position, arguments, :options, &block)
289
+ expect(output).to eq [nil, nil, :options]
290
+ end
291
+
292
+ it "calls a method passing an array as argument" do
293
+ method = ->(parameter, _) { parameter }
294
+ array = [1, 2, 3]
295
+ arguments = [array]
296
+
297
+ output = rendering.call(method, "method", position, arguments, :options, &block)
298
+ expect(output).to eq arguments.first
299
+ end
300
+
301
+ it "raises Curlybars::Error::Render if the helper has at least an optional parameter" do
302
+ method = ->(one, two = :optional) {}
303
+ arguments = [:arg1]
304
+ options = { key: :value }
305
+
306
+ expect do
307
+ rendering.call(method, "method", position, arguments, options, &block)
308
+ end.to raise_error(Curlybars::Error::Render)
309
+ end
310
+
311
+ it "raises Curlybars::Error::Render if the helper has at least a keyword parameter" do
312
+ method = ->(keyword:) {}
313
+ arguments = [:arg1]
314
+ options = { key: :value }
315
+
316
+ expect do
317
+ rendering.call(method, "method", position, arguments, options, &block)
318
+ end.to raise_error(Curlybars::Error::Render)
319
+ end
320
+
321
+ it "raises Curlybars::Error::Render if the helper has at least an optional keyword parameter" do
322
+ method = ->(keyword: :optional) {}
323
+ arguments = [:arg1]
324
+ options = { key: :value }
325
+
326
+ expect do
327
+ rendering.call(method, "meth", position, arguments, options, &block)
328
+ end.to raise_error(Curlybars::Error::Render)
329
+ end
330
+ end
331
+
332
+ describe "#position" do
333
+ it "returns a position with file_name" do
334
+ position = rendering.position(0, 0)
335
+ expect(position.file_name).to eq file_name
336
+ end
337
+
338
+ it "returns a position with line_number" do
339
+ position = rendering.position(1, 0)
340
+ expect(position.line_number).to eq 1
341
+ end
342
+
343
+ it "returns a position with line_offset" do
344
+ position = rendering.position(0, 1)
345
+ expect(position.line_offset).to eq 1
346
+ end
347
+ end
348
+
349
+ describe "#check_context_is_hash_or_enum_of_presenters" do
350
+ it "doesn't raise an exception when argument is an empty enumerable" do
351
+ collection = []
352
+ rendering.check_context_is_hash_or_enum_of_presenters(collection, 'path', position)
353
+ end
354
+
355
+ it "doesn't raise an exception when argument is an empty hash" do
356
+ collection = {}
357
+ rendering.check_context_is_hash_or_enum_of_presenters(collection, nil, position)
358
+ end
359
+
360
+ it "doesn't raise an exception when argument is an enumerable of presenters" do
361
+ collection = [presenter]
362
+ rendering.check_context_is_hash_or_enum_of_presenters(collection, 'path', position)
363
+ end
364
+
365
+ it "doesn't raise an exception when argument is a hash of presenters" do
366
+ collection = { presenter: presenter }
367
+ rendering.check_context_is_hash_or_enum_of_presenters(collection, 'path', position)
368
+ end
369
+
370
+ it "raises when it is not an hash or an enumerable" do
371
+ expect do
372
+ rendering.check_context_is_hash_or_enum_of_presenters(:not_a_presenter, 'path', position)
373
+ end.to raise_error(Curlybars::Error::Render)
374
+ end
375
+
376
+ it "raises when it is not an hash or an enumerable of presenters" do
377
+ expect do
378
+ rendering.check_context_is_hash_or_enum_of_presenters([:not_a_presenter], 'path', position)
379
+ end.to raise_error(Curlybars::Error::Render)
380
+ end
381
+ end
382
+
383
+ describe "#check_context_is_presenter" do
384
+ it "doesn't raise an exception when argument is a presenter" do
385
+ rendering.check_context_is_presenter(presenter, 'path', position)
386
+ end
387
+
388
+ it "raises when it is not a presenter" do
389
+ expect do
390
+ rendering.check_context_is_presenter(:not_a_presenter, 'path', position)
391
+ end.to raise_error(Curlybars::Error::Render)
392
+ end
393
+ end
394
+
395
+ describe "#coerce_to_hash!" do
396
+ let(:a_presenter) { double(:a_presenter, allows_method?: true, meth: :value) }
397
+ let(:another_presenter) { double(:another_presenter, allows_method?: true, meth: :value) }
398
+
399
+ it "leaves hashes intacted" do
400
+ hash = { first: a_presenter }
401
+ expect(rendering.coerce_to_hash!(hash, 'path', position)).to be hash
402
+ end
403
+
404
+ it "transform an Array to a Hash" do
405
+ array = [a_presenter, another_presenter]
406
+ expected_hash = { 0 => a_presenter, 1 => another_presenter }
407
+ expect(rendering.coerce_to_hash!(array, 'path', position)).to eq expected_hash
408
+ end
409
+
410
+ it "raises when it is not a hash or an enumerable" do
411
+ expect do
412
+ rendering.coerce_to_hash!(:not_a_presenter, 'path', position)
413
+ end.to raise_error(Curlybars::Error::Render)
414
+ end
415
+ end
416
+
417
+ private
418
+
419
+ def allow_all_methods(presenter)
420
+ allow(presenter).to receive(:allows_method?) { true }
421
+ end
422
+
423
+ def disallow_all_methods(presenter)
424
+ allow(presenter).to receive(:allows_method?) { false }
425
+ end
426
+ end