namo.rb 0.31.7

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.
@@ -0,0 +1,458 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest-spec-context'
3
+
4
+ require_relative '../../lib/namo'
5
+
6
+ describe Namo::Formulae do
7
+ let(:revenue) do
8
+ proc{|r| r[:price] * r[:quantity]}
9
+ end
10
+
11
+ let(:cost) do
12
+ proc{|r| r[:quantity] * 4.0}
13
+ end
14
+
15
+ let(:formulae) do
16
+ Namo::Formulae.new({revenue: revenue})
17
+ end
18
+
19
+ describe "#[]" do
20
+ it "returns the callable stored under a name" do
21
+ _(formulae[:revenue]).must_be_same_as revenue
22
+ end
23
+
24
+ it "returns nil for an absent name" do
25
+ _(formulae[:missing]).must_be_nil
26
+ end
27
+ end
28
+
29
+ describe "#derive" do
30
+ let(:row) do
31
+ {price: 10.0, quantity: 100}
32
+ end
33
+
34
+ it "resolves a row-only formula to its value" do
35
+ _(formulae.derive(:revenue, row, nil)).must_equal 1000.0
36
+ end
37
+
38
+ it "resolves a collection-scoped formula with the namo context" do
39
+ namo = Object.new
40
+ formulae[:context] = ->(r, n){n}
41
+ _(formulae.derive(:context, row, namo)).must_be_same_as namo
42
+ end
43
+
44
+ it "raises ArgumentError naming the formula when a collection-scoped formula has no namo context" do
45
+ formulae[:context] = ->(r, n){n}
46
+ error = _(proc{formulae.derive(:context, row, nil)}).must_raise ArgumentError
47
+ _(error.message).must_match(/context/)
48
+ end
49
+
50
+ it "resolves a parameterised formula with arguments" do
51
+ formulae[:scaled] = ->(r, n, factor){r[:price] * factor}
52
+ _(formulae.derive(:scaled, row, Object.new, 3)).must_equal 30.0
53
+ end
54
+ end
55
+
56
+ describe "#required_parameter_count" do
57
+ it "counts a row-only formula" do
58
+ _(formulae.required_parameter_count(:revenue)).must_equal 1
59
+ end
60
+
61
+ it "counts a collection-scoped formula" do
62
+ formulae[:context] = ->(r, n){n}
63
+ _(formulae.required_parameter_count(:context)).must_equal 2
64
+ end
65
+
66
+ it "counts a parameterised formula" do
67
+ formulae[:scaled] = ->(r, n, factor){r[:price] * factor}
68
+ _(formulae.required_parameter_count(:scaled)).must_equal 3
69
+ end
70
+
71
+ it "excludes the splat when counting a variadic formula" do
72
+ formulae[:variadic] = ->(r, n, *rest){rest}
73
+ _(formulae.required_parameter_count(:variadic)).must_equal 2
74
+ end
75
+ end
76
+
77
+ describe "#[]=" do
78
+ it "stores a callable under a name" do
79
+ formulae[:cost] = cost
80
+ _(formulae[:cost]).must_be_same_as cost
81
+ end
82
+ end
83
+
84
+ describe "#keys" do
85
+ it "returns the stored names" do
86
+ _(formulae.keys).must_equal [:revenue]
87
+ end
88
+ end
89
+
90
+ describe "#key?" do
91
+ it "is true for a stored name" do
92
+ _(formulae.key?(:revenue)).must_equal true
93
+ end
94
+
95
+ it "is false for an absent name" do
96
+ _(formulae.key?(:missing)).must_equal false
97
+ end
98
+ end
99
+
100
+ describe "#empty?" do
101
+ it "is true with no formulae" do
102
+ _(Namo::Formulae.new).must_be_empty
103
+ end
104
+
105
+ it "is false with formulae" do
106
+ _(formulae.empty?).must_equal false
107
+ end
108
+ end
109
+
110
+ describe "#each" do
111
+ it "yields name/callable pairs" do
112
+ yielded = {}
113
+ formulae.each{|name, callable| yielded[name] = callable}
114
+ _(yielded.keys).must_equal [:revenue]
115
+ _(yielded[:revenue].call({price: 10.0, quantity: 100})).must_equal 1000.0
116
+ end
117
+
118
+ it "returns an enumerator with no block" do
119
+ _(formulae.each).must_be_kind_of Enumerator
120
+ end
121
+ end
122
+
123
+ describe "#delete" do
124
+ it "removes a name" do
125
+ formulae.delete(:revenue)
126
+ _(formulae.key?(:revenue)).must_equal false
127
+ end
128
+ end
129
+
130
+ describe "#merge" do
131
+ it "returns a Formulae combining both sets" do
132
+ merged = formulae.merge(Namo::Formulae.new({cost: cost}))
133
+ _(merged).must_be_kind_of Namo::Formulae
134
+ _(merged.keys.sort).must_equal [:cost, :revenue]
135
+ end
136
+
137
+ it "lets the argument win on a name conflict" do
138
+ other = proc{|r| 0}
139
+ merged = formulae.merge(Namo::Formulae.new({revenue: other}))
140
+ _(merged[:revenue].call({price: 10.0, quantity: 100})).must_equal 0
141
+ end
142
+
143
+ it "does not mutate the receiver" do
144
+ formulae.merge(Namo::Formulae.new({cost: cost}))
145
+ _(formulae.keys).must_equal [:revenue]
146
+ end
147
+ end
148
+
149
+ describe "#reject" do
150
+ it "returns a Formulae without the rejected names" do
151
+ formulae[:cost] = cost
152
+ rejected = formulae.reject{|name, _| name == :cost}
153
+ _(rejected).must_be_kind_of Namo::Formulae
154
+ _(rejected.keys).must_equal [:revenue]
155
+ end
156
+ end
157
+
158
+ describe "#dup" do
159
+ it "returns an independent Formulae" do
160
+ copy = formulae.dup
161
+ copy[:cost] = cost
162
+ _(formulae.key?(:cost)).must_equal false
163
+ end
164
+ end
165
+
166
+ describe "#to_h" do
167
+ it "returns a copy, not the live store" do
168
+ hash = formulae.to_h
169
+ hash[:cost] = cost
170
+ _(formulae.key?(:cost)).must_equal false
171
+ end
172
+ end
173
+
174
+ describe "Enumerable" do
175
+ it "supports map over name/callable pairs" do
176
+ _(formulae.map{|name, _| name}).must_equal [:revenue]
177
+ end
178
+
179
+ it "supports select returning name/callable pairs" do
180
+ formulae[:cost] = cost
181
+ selected = formulae.select{|name, _| name == :cost}
182
+ _(selected.map(&:first)).must_equal [:cost]
183
+ _(selected.first.last.call({quantity: 100})).must_equal 400.0
184
+ end
185
+ end
186
+
187
+ describe "value semantics" do
188
+ it "is == when names match, regardless of proc objects" do
189
+ _(Namo::Formulae.new({revenue: revenue})).must_equal Namo::Formulae.new({revenue: proc{|r| 0}})
190
+ end
191
+
192
+ it "is not == when names differ" do
193
+ _(formulae).wont_equal Namo::Formulae.new({cost: cost})
194
+ end
195
+
196
+ it "is not == to a bare Hash" do
197
+ _(formulae).wont_equal({revenue: revenue})
198
+ end
199
+
200
+ it "is eql? on matching names with differing procs" do
201
+ _(Namo::Formulae.new({revenue: revenue}).eql?(Namo::Formulae.new({revenue: proc{|r| 0}}))).must_equal true
202
+ end
203
+
204
+ it "hashes equal on matching names with differing procs" do
205
+ _(Namo::Formulae.new({revenue: revenue}).hash).must_equal Namo::Formulae.new({revenue: proc{|r| 0}}).hash
206
+ end
207
+ end
208
+
209
+ describe "constructor" do
210
+ it "defaults to an empty store" do
211
+ _(Namo::Formulae.new.keys).must_equal []
212
+ end
213
+
214
+ it "stores a seeded callable as given, resolvable thereafter" do
215
+ formulae = Namo::Formulae.new({revenue: revenue})
216
+ _(formulae[:revenue]).must_be_same_as revenue
217
+ _(formulae.derive(:revenue, {price: 10.0, quantity: 100}, nil)).must_equal 1000.0
218
+ end
219
+ end
220
+
221
+ describe "formulary tier" do
222
+ let(:delta) do
223
+ Module.new do
224
+ include Namo::Formulary
225
+ def signed_volume(row); row[:buys] - row[:sells]; end
226
+ def echo_namo(row, namo); namo; end
227
+ def scaled(row, namo, factor); row[:buys] * factor; end
228
+ def windowed(row, namo, *rest); rest; end
229
+ private
230
+ def helper(row); row[:buys]; end
231
+ end
232
+ end
233
+
234
+ let(:untagged) do
235
+ Module.new do
236
+ def signed_volume(row); 0; end
237
+ end
238
+ end
239
+
240
+ let(:attached) do
241
+ Namo::Formulae.new.attach(delta)
242
+ end
243
+
244
+ describe "#attach" do
245
+ it "returns self" do
246
+ formulae = Namo::Formulae.new
247
+ _(formulae.attach(delta)).must_be_same_as formulae
248
+ end
249
+
250
+ it "raises ArgumentError for a module not tagged Namo::Formulary" do
251
+ _(proc{Namo::Formulae.new.attach(untagged)}).must_raise ArgumentError
252
+ end
253
+
254
+ it "copies the formulary's public methods into the callable store" do
255
+ _(attached.keys.sort).must_equal [:echo_namo, :scaled, :signed_volume, :windowed]
256
+ end
257
+
258
+ it "excludes a private helper" do
259
+ _(attached.keys).wont_include :helper
260
+ end
261
+
262
+ it "binds every attached formulary to a single shared host" do
263
+ other = Module.new do
264
+ include Namo::Formulary
265
+ def momentum(row); row[:buys]; end
266
+ end
267
+ formulae = Namo::Formulae.new.attach(delta).attach(other)
268
+ _(formulae[:signed_volume].receiver).must_be_same_as formulae[:momentum].receiver
269
+ end
270
+
271
+ it "stores a formulary's methods as Methods owned by the module, bound to the shared host" do
272
+ _(attached[:signed_volume]).must_be_kind_of Method
273
+ _(attached[:signed_volume].owner).must_be_same_as delta
274
+ _(attached.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 20
275
+ end
276
+
277
+ it "lets a []= overwrite take effect after an anonymous formulary attached the name" do
278
+ formulae = Namo::Formulae.new
279
+ formulae[:signed_volume] = proc{|r| 1}
280
+ formulae.attach(delta)
281
+ formulae[:signed_volume] = proc{|r| 0}
282
+ _(formulae.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 0
283
+ end
284
+ end
285
+
286
+ describe "#<<" do
287
+ it "attaches a formulary, the same as #attach" do
288
+ formulae = Namo::Formulae.new
289
+ formulae << delta
290
+ _(formulae.keys).must_include :signed_volume
291
+ end
292
+
293
+ it "returns self for chaining" do
294
+ formulae = Namo::Formulae.new
295
+ _(formulae << delta).must_be_same_as formulae
296
+ end
297
+ end
298
+
299
+ describe "#key?" do
300
+ it "is true for a formulary method name" do
301
+ _(attached.key?(:signed_volume)).must_equal true
302
+ end
303
+
304
+ it "is false for a private helper" do
305
+ _(attached.key?(:helper)).must_equal false
306
+ end
307
+
308
+ it "is false for an absent name" do
309
+ _(attached.key?(:missing)).must_equal false
310
+ end
311
+ end
312
+
313
+ describe "#required_parameter_count" do
314
+ it "counts a row-scoped formulary method" do
315
+ _(attached.required_parameter_count(:signed_volume)).must_equal 1
316
+ end
317
+
318
+ it "counts a two-arity formulary method" do
319
+ _(attached.required_parameter_count(:echo_namo)).must_equal 2
320
+ end
321
+
322
+ it "counts a parameterised formulary method" do
323
+ _(attached.required_parameter_count(:scaled)).must_equal 3
324
+ end
325
+
326
+ it "excludes the splat for a variadic formulary method" do
327
+ _(attached.required_parameter_count(:windowed)).must_equal 2
328
+ end
329
+ end
330
+
331
+ describe "#derive" do
332
+ it "resolves a row-scoped formulary method to its value" do
333
+ _(attached.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 20
334
+ end
335
+
336
+ it "resolves a two-arity formulary method with the namo context" do
337
+ namo = Object.new
338
+ _(attached.derive(:echo_namo, {buys: 60, sells: 40}, namo)).must_be_same_as namo
339
+ end
340
+
341
+ it "raises when a two-arity formulary method has no namo context" do
342
+ error = _(proc{attached.derive(:echo_namo, {buys: 60, sells: 40}, nil)}).must_raise ArgumentError
343
+ _(error.message).must_match(/echo_namo/)
344
+ end
345
+
346
+ it "resolves the most-recently attached formulary on a name collision" do
347
+ alt = Module.new do
348
+ include Namo::Formulary
349
+ def signed_volume(row); 999; end
350
+ end
351
+ formulae = Namo::Formulae.new.attach(delta).attach(alt)
352
+ _(formulae.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 999
353
+ end
354
+
355
+ it "brings a shadowed formulary's definition back when it is re-attached (binds the module's own method, not the shared host's ancestry order)" do
356
+ alt = Module.new do
357
+ include Namo::Formulary
358
+ def signed_volume(row); 999; end
359
+ end
360
+ formulae = Namo::Formulae.new.attach(delta).attach(alt).attach(delta)
361
+ _(formulae.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 20
362
+ end
363
+ end
364
+
365
+ describe "#detach" do
366
+ it "returns self for a Symbol" do
367
+ _(attached.detach(:signed_volume)).must_be_same_as attached
368
+ end
369
+
370
+ it "returns self for a Module" do
371
+ _(attached.detach(delta)).must_be_same_as attached
372
+ end
373
+
374
+ it "removes the named formula, leaving the others" do
375
+ attached.detach(:signed_volume)
376
+ _(attached.key?(:signed_volume)).must_equal false
377
+ _(attached.key?(:echo_namo)).must_equal true
378
+ end
379
+
380
+ it "is a no-op on an absent name, still returning self" do
381
+ _(attached.detach(:missing)).must_be_same_as attached
382
+ _(attached.key?(:signed_volume)).must_equal true
383
+ end
384
+
385
+ it "removes exactly the formulary's public method names via the Module arm" do
386
+ attached.detach(delta)
387
+ _(attached.keys).must_equal []
388
+ end
389
+
390
+ it "leaves a []= formula not among the formulary's methods when detaching the module" do
391
+ attached[:extra] = proc{|r| 0}
392
+ attached.detach(delta)
393
+ _(attached.key?(:extra)).must_equal true
394
+ end
395
+
396
+ it "deletes a []= overwrite occupying a formulary-method name" do
397
+ attached[:signed_volume] = proc{|r| 0}
398
+ attached.detach(delta)
399
+ _(attached.key?(:signed_volume)).must_equal false
400
+ end
401
+
402
+ it "raises ArgumentError for an untagged module" do
403
+ _(proc{attached.detach(untagged)}).must_raise ArgumentError
404
+ end
405
+
406
+ it "raises TypeError for a String, naming the class" do
407
+ error = _(proc{attached.detach("signed_volume")}).must_raise TypeError
408
+ _(error.message).must_match(/String/)
409
+ end
410
+
411
+ it "raises TypeError for a Hash, naming the class" do
412
+ error = _(proc{attached.detach({})}).must_raise TypeError
413
+ _(error.message).must_match(/Hash/)
414
+ end
415
+
416
+ it "chains, removing both named formulae" do
417
+ formulae[:cost] = cost
418
+ formulae.detach(:revenue).detach(:cost)
419
+ _(formulae.keys).must_equal []
420
+ end
421
+ end
422
+
423
+ describe "carry-through" do
424
+ it "preserves attached formularies through #dup" do
425
+ _(attached.dup.keys).must_include :signed_volume
426
+ end
427
+
428
+ it "preserves attached formularies through #reject" do
429
+ _(attached.reject{|_name, _| false}.keys).must_include :signed_volume
430
+ end
431
+
432
+ it "carries both sides' formularies through #merge" do
433
+ other_delta = Module.new do
434
+ include Namo::Formulary
435
+ def momentum(row); row[:buys]; end
436
+ end
437
+ merged = attached.merge(Namo::Formulae.new.attach(other_delta))
438
+ _(merged.keys).must_include :momentum
439
+ _(merged.keys).must_include :signed_volume
440
+ end
441
+ end
442
+ end
443
+
444
+ describe "#inspect" do
445
+ it "renders the formula names" do
446
+ formulae = Namo::Formulae.new
447
+ formulae[:revenue] = proc{|row| row[:price]}
448
+ _(formulae.inspect).must_equal "#<Namo::Formulae [:revenue]>"
449
+ end
450
+
451
+ it "does not render the callables" do
452
+ formulae = Namo::Formulae.new
453
+ formulae[:revenue] = proc{|row| row[:price]}
454
+ _(formulae.inspect).wont_match(/Proc/)
455
+ end
456
+ end
457
+
458
+ end
@@ -0,0 +1,13 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest-spec-context'
3
+
4
+ require_relative '../../lib/namo'
5
+
6
+ describe Namo::NegatedDimension do
7
+ describe "#name" do
8
+ it "returns the original symbol" do
9
+ nd = Namo::NegatedDimension.new(:price)
10
+ _(nd.name).must_equal :price
11
+ end
12
+ end
13
+ end