namo 0.26.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 326ba6ba610149b0998452136812f313617014d5e68fc066d5c81ace134ad8eb
4
- data.tar.gz: ddd93f8e3866169c6bc6835c5720375fbfd53860b2e036f1540dcb567414b61c
3
+ metadata.gz: 5649b6afb8e1272273a87286f053376ad42caddec81eed0610a672f26bc7e66a
4
+ data.tar.gz: 77a5e0c6499c3d600b76753e28c5e600b0dcd6dc0968d93069a56e3b10fc7652
5
5
  SHA512:
6
- metadata.gz: 9402c8b7193dd068d624b883f9e2b7492942cf67dcf0c3d697e85bb77c2615deb205538cf86c5b341e567e25990241235b88bd5bfa0582b0cace025c65a7fa10
7
- data.tar.gz: 40af6d9dd32c57b4a60e038887fbc4c73c7a6412e25854688b9ff8ddc5ee35c6419d30fcb76ce6e0ee5cdcf491c757a3ebcd1f81ec584ad41243413300b8c40f
6
+ metadata.gz: 33c03e6c39cee1242a576a8355928b041464c9049a5bbb54900b7654dc36a9dec44741edaf28a8408eb293f4fcf4b702736841e8cf3b3bae7ce43e6684b098f1
7
+ data.tar.gz: d0ad82d6b415a09ba086a9d6a7b0f06c24d1dfad8bb3eaad72e1a40c878f9a431ccfb093f781836a94d0d4f4ab6bdf8b7917ddb61123012bf1fd0455168e3e05
data/CHANGELOG CHANGED
@@ -1,5 +1,25 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260710
4
+
5
+ 0.27.0: ~ Namo::Collection#summary/#as_summary: + an optional block for a per-member reduction beyond a single named reducer.
6
+
7
+ 1. ~ Namo::Collection#summary(dimension = nil, by:, reducer:, &block): dimension goes optional; with a block, maps each member through block.call(member).merge(by => member.name), the member label merged last so it wins; given neither a dimension nor a block, raises ArgumentError; the no-block path is byte-identical. as_summary forwards &block to summary and persists the result, so the block form lives in one place.
8
+ 2. ~ test/Namo/Collection_test.rb: + block-form summary returns one row per member merged with the member label; the member label wins over a by key the block returns; the neither-arg ArgumentError; the map/flat_map cardinality pin (summary one row per member, detail all rows per member); as_summary with a block sets the data and delegates. ~ the non-block custom-by test also pins the reduced value carried alongside the relabelled column.
9
+ 3. ~ README.md: + an "A block for per-member reductions" note under the Collection view methods (the one-row-per-member contract, the member-label-wins merge, an argmax example, the neither-arg raise); + a bare-collection call-site by: example beside the class-default prose; the summary/as_summary signatures gain (dimension = nil, ..., &block).
10
+ 4. ~ ROADMAP.md: + the 0.27.0 release entry (the block warrant from 0.14.0, summarise considered-and-refused, the pinned decisions, the transient-block serialisation-independence, the deferred group-scoped formula); current-state bumped to 0.27.0; the block form folded into the Summary vocabulary and the 1.0.0 feature list.
11
+ 5. ~ docs/: regenerate the print-ready PDFs.
12
+ 6. ~ Namo::VERSION: /0.26.1/0.27.0/
13
+
14
+ 0.26.1: Refactor Namo::Formulae#attach through a private #bind, and document mutability and the corrected collision-resolution idioms.
15
+
16
+ 1. ~ lib/Namo/Formulae.rb — attach binds each of a formulary's public method names through a new private bind(name, modul): marker guard, host.extend(modul), then self[name] = modul.instance_method(name).bind(host). attach and detach share one private raise_unless_formulary marker guard. bind stays private — attach is its only caller.
17
+ 2. ~ README.md: + a "Mutability" section (Namos are mutable; an unfrozen Namo is not a reliable Hash key or Set member since hash tracks data and formula names; the deliberate attr_accessor :data back door and the sanctioned namo << row append idiom); + a private-helper support note under Formularies (a public formulary method resolves its private helpers through the shared host; a same-named helper in a later-attached formulary shadows the earlier one); ~ the collision-resolution passages — correct a prescribed rename operation Namo does not have to the shipped idioms (detach, formula-plus-projection, re-key before ingestion, attach!).
18
+ 3. ~ ROADMAP.md: + a note in the deep-freeze open question — the attr_accessor :data back door means a generation counter cannot observe all mutation, deciding the 2.x caching mechanism in deep freeze's favour.
19
+ 4. ~ test/Namo/Formulae_test.rb, test/namo_test.rb: identity assertions on stored callables migrate to behavioural ones; + a "private support methods" describe (a public formulary method resolving through its private helper; shared-host same-named-helper shadowing).
20
+ 5. ~ docs/: regenerate the print-ready PDFs.
21
+ 6. ~ Namo::VERSION: /0.26.0/0.26.1/
22
+
3
23
  ## 20260704
4
24
 
5
25
  0.26.0: + Namo#detach, Namo::Formulae#detach — remove a formula by name or a formulary's methods by module, completing the attach/attach!/detach family.
data/README.md CHANGED
@@ -599,6 +599,14 @@ one < two
599
599
 
600
600
  The dimensions must match; different dimensions raise an `ArgumentError`. Comparing against a non-Namo raises a `TypeError`.
601
601
 
602
+ ### Mutability
603
+
604
+ Namos are mutable. Rows append (`<<`), formulae register and remove (`[]=`, `attach`, `detach`), and every view recomputes from current state on the next access — mutability is what the live computation model runs on.
605
+
606
+ The consequence for the equality machinery above: because `hash` is computed from data and formula names, mutation shifts it, so an *unfrozen* Namo is not a reliable Hash key or Set member — a lookup can miss the entry the Namo was stored under. Freeze marks the transition: mutate during exploration, `freeze` before using a Namo for hash-keyed lookup or sharing it across threads. Enforcement — a deep freeze that propagates to `@data` and `@formulae` — arrives with the 2.x performance phase, where freeze also gates caching; until then `freeze` is Ruby's shallow default and the lifecycle discipline is the user's.
607
+
608
+ One back door is deliberate: `data` is a public accessor, so `namo.data << row` and whole-array replacement are possible. This is the same trust the constructor extends — Namo takes your array of hashes without copying or validating it — kept open for the surrounding system that owns the data feed. The sanctioned append idiom remains `namo << row`, which guards against data/formula name collisions where the accessor bypasses every guard.
609
+
602
610
  ### Formulae
603
611
 
604
612
  Define computed dimensions using `[]=`:
@@ -757,10 +765,12 @@ flows.values(:cum_delta) # => [20, -20, -20]
757
765
  flows.derived_dimensions # => [:signed_volume, :cum_delta]
758
766
  ```
759
767
 
760
- `<<` appends a *constituent*. For a base Namo the constituents are formularies and rows: a **module** attaches (the operator form of `attach`, so `flows << OrderFlow` reads the same as `flows.attach(OrderFlow)`); a **Hash** appends as a data row; a **Row** (one drawn from another Namo) appends its underlying hash as a row. It returns the Namo, so the arms chain freely: `flows << OrderFlow << {date: 4, buys: 10, sells: 5} << Scoring`. Two things `<<` deliberately does not take: a bare callable (formula *registration* stays with `[]=`, which dispatches callable-vs-scalar and enforces data/formula exclusivity) and a whole Namo (combining collections is `+`'s job) — both raise. A bare Hash is therefore always a row, never a body of formulae. Appending a row whose keys collide with an existing formula name **raises** an `ArgumentError` naming the collision — a name is data or derived, never both. The guard is symmetric with `attach`'s: `[]=` names one column at the call site, so its scalar form evicts a same-named formula as your explicit intent, but a row is a bulk append you didn't name column-by-column, so the safe response is to refuse rather than let data and a formula of the same name disagree on access. Contract or rename first, then append.
768
+ `<<` appends a *constituent*. For a base Namo the constituents are formularies and rows: a **module** attaches (the operator form of `attach`, so `flows << OrderFlow` reads the same as `flows.attach(OrderFlow)`); a **Hash** appends as a data row; a **Row** (one drawn from another Namo) appends its underlying hash as a row. It returns the Namo, so the arms chain freely: `flows << OrderFlow << {date: 4, buys: 10, sells: 5} << Scoring`. Two things `<<` deliberately does not take: a bare callable (formula *registration* stays with `[]=`, which dispatches callable-vs-scalar and enforces data/formula exclusivity) and a whole Namo (combining collections is `+`'s job) — both raise. A bare Hash is therefore always a row, never a body of formulae. Appending a row whose keys collide with an existing formula name **raises** an `ArgumentError` naming the collision — a name is data or derived, never both. The guard is symmetric with `attach`'s: `[]=` names one column at the call site, so its scalar form evicts a same-named formula as your explicit intent, but a row is a bulk append you didn't name column-by-column, so the safe response is to refuse rather than let data and a formula of the same name disagree on access. Detach the formula (`namo.detach(:signed_volume)`) or re-key the incoming row first, then append.
761
769
 
762
770
  A module brands itself a formulary by including `Namo::Formulary`. The marker is mandatory — `attach` raises `ArgumentError` for an untagged module, so an ordinary module of helpers can't be mistaken for a body of derivations. Within a formulary, the public methods are the derivations and `private` methods are helpers: `net` above never appears in `derived_dimensions` and never resolves as a dimension. The separation needs no per-method declaration — public or private says it.
763
771
 
772
+ Private helpers remain fully *usable* in their support role: `attach` extends the whole module onto the internal host, so a public formulary method calling a helper resolves it through ordinary method lookup — only the store, and therefore the dimension namespace, is restricted to the public tier. One caveat of the single shared host: attached formularies share one method-resolution scope, so if two formularies define a *same-named* private helper, the most recently attached one wins — even for calls made from the earlier formulary's own methods. Give helpers distinctive names, or re-attach the formulary whose helper should prevail.
773
+
764
774
  Attaching **copies** the formulary's public methods into the Namo's formula collection — each becomes a stored formula, indistinguishable from one assigned through `[]=`. From there they are `[]=` formulae in everything but how they were defined: they resolve through the same path, carry through the same operators, and obey the same exclusivity rule.
765
775
 
766
776
  The copy is a *snapshot* taken at the moment of attachment, and this is where a formulary departs from ordinary Ruby. A real `include` (or `extend`) is a live link: its methods resolve through the ancestor chain, so a method added to the module later, a redefinition, or a further module mixed in afterwards are all seen by objects that already include it. `attach` forgoes that — it copies the method set once, so later changes to the module are not reflected on an already-attached Namo. Re-attach to pick them up. (A live-link mechanism may come in a later release; it is not in 0.23.0.)
@@ -770,7 +780,7 @@ The copy is a *snapshot* taken at the moment of attachment, and this is where a
770
780
  Because each method is copied into the store, the rules already governing `[]=` apply:
771
781
 
772
782
  - **Most-recent wins.** Attaching a second formulary that defines a colliding name overwrites the first, exactly as a second `[]=` would. Equally, `[]=` and `attach` interleave as plain last-write-wins on a shared name — whichever was applied last governs.
773
- - **Data collisions raise.** A name is data or derived, never both. If a formulary method's name collides with an existing data column, attaching raises an `ArgumentError` naming the collision rather than silently destroying the data. The reason it raises where `[]=` silently clears: `[]=` names one column at the call site, so replacing it is your explicit intent; a formulary names a whole *module*, so a data collision is a name you never typed — and possibly several columns at once — that you most likely didn't mean to destroy. The same raise guards the class-`include` channel, where it fires at construction. Resolve it explicitly first contract the column away (`namo[-:signed_volume]`) or renamethen attach.
783
+ - **Data collisions raise.** A name is data or derived, never both. If a formulary method's name collides with an existing data column, attaching raises an `ArgumentError` naming the collision rather than silently destroying the data. The reason it raises where `[]=` silently clears: `[]=` names one column at the call site, so replacing it is your explicit intent; a formulary names a whole *module*, so a data collision is a name you never typed — and possibly several columns at once — that you most likely didn't mean to destroy. The same raise guards the class-`include` channel, where it fires at construction. Resolve it explicitly first, then attach: contract the column away (`namo[-:signed_volume]`); or move it to a new name by formula-plus-projection register the new name as a formula reading the old (`namo[:raw_signed_volume] = proc{|r| r[:signed_volume]}`) and project the kept dimensions plus the new name, which materialises it as a data column and drops the formula; or re-key the rows before ingestion. `attach!` is the fourth resolution — consenting to the eviction instead.
774
784
  - **Materialise-and-drop on projection.** Naming a formulary method in a projection snapshots its values into a data column *and drops the formula*, exactly as for a `[]=` formula: `flows[:date, :signed_volume]` returns a Namo reporting `:signed_volume` as data, not derived, with all access paths agreeing on the stored snapshot.
775
785
  - **Carry-through.** Selection, the set and composition operators, and a projection that *omits* the name all keep the formula live, computing against the result's own rows — the carry-through is free, because the formulae are ordinary store entries.
776
786
 
@@ -1036,7 +1046,15 @@ gt.detail.values(:weight).sum # total weight by summing every line
1036
1046
  # => 490
1037
1047
  ```
1038
1048
 
1039
- `Car` overrides `summary`/`detail` only to set `by: :assembly` as the per-class default and then calls `super`. A bare `Namo::Collection.new` works equally well, defaulting `by:` to `:member` and taking it at the call site.
1049
+ `Car` overrides `summary`/`detail` only to set `by: :assembly` as the per-class default and then calls `super`. A bare `Namo::Collection.new` works equally well, defaulting `by:` to `:member` and taking it at the call site:
1050
+
1051
+ ```ruby
1052
+ parts = Namo::Collection.new
1053
+ parts << [powertrain, chassis, body]
1054
+
1055
+ parts.summary(:weight).values(:member) # default label => [:powertrain, :chassis, :body]
1056
+ parts.summary(:weight, by: :assembly).values(:assembly) # call-site by: => [:powertrain, :chassis, :body]
1057
+ ```
1040
1058
 
1041
1059
  #### Lazy detail, behaving as its line items
1042
1060
 
@@ -1056,8 +1074,8 @@ Detail is the lazy view because a Collection's rows simply *are* its members' ro
1056
1074
 
1057
1075
  The views come in a non-mutating pair and a mutating pair:
1058
1076
 
1059
- - `summary(dimension, by:, reducer:)` and `detail(by:)` are **non-mutating** — each returns a fresh `Namo` derived from the members, leaving the Collection untouched. Use these when you want a view to keep: assign the result to a variable and operate on it independently.
1060
- - `as_summary(dimension, by:, reducer:)` and `as_detail(by)` are **mutating** — each sets the Collection's data to the chosen view and returns `self`, for a fluent step. (`as_detail` carries no `dimension`, so its label argument is positional: `as_detail(:assembly)`.)
1077
+ - `summary(dimension = nil, by:, reducer:, &block)` and `detail(by:)` are **non-mutating** — each returns a fresh `Namo` derived from the members, leaving the Collection untouched. Use these when you want a view to keep: assign the result to a variable and operate on it independently.
1078
+ - `as_summary(dimension = nil, by:, reducer:, &block)` and `as_detail(by)` are **mutating** — each sets the Collection's data to the chosen view and returns `self`, for a fluent step. (`as_detail` carries no `dimension`, so its label argument is positional: `as_detail(:assembly)`.)
1061
1079
 
1062
1080
  ```ruby
1063
1081
  gt.summary(:cost, reducer: :mean) # a fresh Namo; gt is unchanged
@@ -1067,6 +1085,23 @@ gt.as_detail(:assembly) # gt's data becomes the detail; retur
1067
1085
 
1068
1086
  `reducer:` is any method the member's column responds to — `:sum` (the default) and `:mean` are typical (`:mean` via a statistics gem that adds `Array#mean`).
1069
1087
 
1088
+ #### A block for per-member reductions
1089
+
1090
+ The single-`dimension`, single-`reducer` form reduces one column with one named method. Per-member work that wants several columns at once, or a reduction that isn't a single-column `send` — notably a *pick-by-extremum* ("the component of the heaviest part", an argmax that reduces no one column) — is supplied by an optional block. The block receives each member and returns the hash for that member's summary row; `summary` labels it with the member name and collects one row per member:
1091
+
1092
+ ```ruby
1093
+ gt.summary(by: :assembly) do |assembly|
1094
+ heaviest = assembly.max_by{|part| part[:weight]}
1095
+ {
1096
+ part_count: assembly.values(:weight).size,
1097
+ heaviest_part: heaviest[:component],
1098
+ total_cost: assembly.values(:cost).sum,
1099
+ }
1100
+ end
1101
+ ```
1102
+
1103
+ The contract is one row per member: `summary` maps members to rows, where `detail` flat-maps them to line items — the two views have opposite cardinality. The member label merges last, so a block returning a hash that carries the `by` key cannot override the group's identity — `by` is `summary`'s to set, not the block's. `as_summary` takes the same block and persists its result, delegating to `summary`. A `summary` given neither a `dimension` nor a block raises an `ArgumentError`.
1104
+
1070
1105
  #### Inject-iff-absent
1071
1106
 
1072
1107
  `detail(by:)` unions the members' rows and labels each with its origin, but only when that label isn't already present:
@@ -23,10 +23,14 @@ class Namo
23
23
  @members.find{|member| member.name == name} unless name.nil?
24
24
  end
25
25
 
26
- def summary(dimension, by: :member, reducer: :sum)
27
- rows = @members.map do |member|
28
- {by => member.name, dimension => member.values(dimension).send(reducer)}
29
- end
26
+ def summary(dimension = nil, by: :member, reducer: :sum, &block)
27
+ raise ArgumentError, "summary needs a dimension or a block" unless dimension || block
28
+ rows =
29
+ if block
30
+ @members.map{|member| block.call(member).merge(by => member.name)}
31
+ else
32
+ @members.map{|member| {by => member.name, dimension => member.values(dimension).send(reducer)}}
33
+ end
30
34
  Namo.new(rows)
31
35
  end
32
36
 
@@ -37,8 +41,8 @@ class Namo
37
41
  Namo.new(rows)
38
42
  end
39
43
 
40
- def as_summary(dimension, by: :member, reducer: :sum)
41
- @data = summary(dimension, by: by, reducer: reducer).data
44
+ def as_summary(dimension = nil, by: :member, reducer: :sum, &block)
45
+ @data = summary(dimension, by: by, reducer: reducer, &block).data
42
46
  self
43
47
  end
44
48
 
data/lib/Namo/Formulae.rb CHANGED
@@ -14,13 +14,8 @@ class Namo
14
14
  end
15
15
 
16
16
  def attach(modul)
17
- unless modul.include?(Namo::Formulary)
18
- raise ArgumentError, "not a Namo::Formulary: #{modul}"
19
- end
20
- host.extend(modul)
21
- modul.public_instance_methods(false).each do |name|
22
- @store[name] = host.method(name)
23
- end
17
+ raise_unless_formulary(modul)
18
+ modul.public_instance_methods(false).each{|name| bind(name, modul)}
24
19
  self
25
20
  end
26
21
  alias_method :<<, :attach
@@ -30,9 +25,7 @@ class Namo
30
25
  when Symbol
31
26
  @store.delete(constituent)
32
27
  when Module
33
- unless constituent.include?(Namo::Formulary)
34
- raise ArgumentError, "not a Namo::Formulary: #{constituent}"
35
- end
28
+ raise_unless_formulary(constituent)
36
29
  constituent.public_instance_methods(false).each{|name| @store.delete(name)}
37
30
  else
38
31
  raise TypeError, "can't detach #{constituent.class} from a Formulae; expected a Symbol or a Module (formulary)"
@@ -114,6 +107,19 @@ class Namo
114
107
  @host ||= Object.new
115
108
  end
116
109
 
110
+ def bind(name, modul)
111
+ raise_unless_formulary(modul)
112
+ host.extend(modul)
113
+ self[name] = modul.instance_method(name).bind(host)
114
+ self
115
+ end
116
+
117
+ def raise_unless_formulary(modul)
118
+ unless modul.include?(Namo::Formulary)
119
+ raise ArgumentError, "not a Namo::Formulary: #{modul}"
120
+ end
121
+ end
122
+
117
123
  def collection_scoped?(name)
118
124
  required_parameter_count(name) >= 2
119
125
  end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.26.0'
5
+ VERSION = '0.27.0'
6
6
  end
@@ -181,9 +181,10 @@ describe Namo::Collection do
181
181
  _(summary.values(:weight)).must_equal [280, 150, 60]
182
182
  end
183
183
 
184
- it "labels with a custom by dimension" do
184
+ it "labels with a custom by dimension, carrying the reduced value alongside" do
185
185
  summary = collection.summary(:weight, by: :assembly)
186
186
  _(summary.values(:assembly)).must_equal [:powertrain, :chassis, :body]
187
+ _(summary.values(:weight)).must_equal [280, 150, 60]
187
188
  end
188
189
 
189
190
  it "reduces with a custom reducer" do
@@ -195,6 +196,32 @@ describe Namo::Collection do
195
196
  collection.summary(:weight)
196
197
  _(collection.values(:weight)).must_equal [200, 80, 150, 60]
197
198
  end
199
+
200
+ it "with a block returns one row per member, the block's hash merged with the member label" do
201
+ result = collection.summary do |member|
202
+ {heaviest: member.max_by{|row| row[:weight]}[:component]}
203
+ end
204
+ _(result.values(:member)).must_equal [:powertrain, :chassis, :body]
205
+ _(result.values(:heaviest)).must_equal ['engine', 'frame', 'panels']
206
+ end
207
+
208
+ it "with a block, the member label wins over a by key the block returns" do
209
+ result = collection.summary(by: :assembly) do |member|
210
+ {assembly: :overridden, total: member.values(:weight).sum}
211
+ end
212
+ _(result.values(:assembly)).must_equal [:powertrain, :chassis, :body]
213
+ _(result.values(:total)).must_equal [280, 150, 60]
214
+ end
215
+
216
+ it "raises an ArgumentError when given neither a dimension nor a block" do
217
+ error = _(proc{collection.summary}).must_raise ArgumentError
218
+ _(error.message).must_match(/dimension or a block/)
219
+ end
220
+
221
+ it "yields one row per member (map), where detail yields all rows per member (flat_map)" do
222
+ _(collection.summary(:weight).data.size).must_equal collection.members.size
223
+ _(collection.detail.data.size).must_equal collection.members.sum{|member| member.data.size}
224
+ end
198
225
  end
199
226
 
200
227
  describe "#detail" do
@@ -251,6 +278,13 @@ describe Namo::Collection do
251
278
  collection.as_summary(:weight)
252
279
  _(collection.dimensions.sort).must_equal [:member, :weight].sort
253
280
  end
281
+
282
+ it "as_summary with a block sets the data to the block summary and returns self" do
283
+ result = collection.as_summary(by: :assembly){|member| {count: member.values(:weight).size}}
284
+ _(result).must_be_same_as collection
285
+ _(collection.values(:assembly)).must_equal [:powertrain, :chassis, :body]
286
+ _(collection.values(:count)).must_equal [2, 1, 1]
287
+ end
254
288
  end
255
289
 
256
290
  describe "as_* view lifetime (rebuild-on-<<: persists until the next <<)" do
@@ -111,7 +111,8 @@ describe Namo::Formulae do
111
111
  it "yields name/callable pairs" do
112
112
  yielded = {}
113
113
  formulae.each{|name, callable| yielded[name] = callable}
114
- _(yielded).must_equal({revenue: revenue})
114
+ _(yielded.keys).must_equal [:revenue]
115
+ _(yielded[:revenue].call({price: 10.0, quantity: 100})).must_equal 1000.0
115
116
  end
116
117
 
117
118
  it "returns an enumerator with no block" do
@@ -136,7 +137,7 @@ describe Namo::Formulae do
136
137
  it "lets the argument win on a name conflict" do
137
138
  other = proc{|r| 0}
138
139
  merged = formulae.merge(Namo::Formulae.new({revenue: other}))
139
- _(merged[:revenue]).must_be_same_as other
140
+ _(merged[:revenue].call({price: 10.0, quantity: 100})).must_equal 0
140
141
  end
141
142
 
142
143
  it "does not mutate the receiver" do
@@ -177,7 +178,9 @@ describe Namo::Formulae do
177
178
 
178
179
  it "supports select returning name/callable pairs" do
179
180
  formulae[:cost] = cost
180
- _(formulae.select{|name, _| name == :cost}).must_equal [[: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
181
184
  end
182
185
  end
183
186
 
@@ -207,6 +210,12 @@ describe Namo::Formulae do
207
210
  it "defaults to an empty store" do
208
211
  _(Namo::Formulae.new.keys).must_equal []
209
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
210
219
  end
211
220
 
212
221
  describe "formulary tier" do
@@ -258,6 +267,20 @@ describe Namo::Formulae do
258
267
  formulae = Namo::Formulae.new.attach(delta).attach(other)
259
268
  _(formulae[:signed_volume].receiver).must_be_same_as formulae[:momentum].receiver
260
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
261
284
  end
262
285
 
263
286
  describe "#<<" do
@@ -328,6 +351,15 @@ describe Namo::Formulae do
328
351
  formulae = Namo::Formulae.new.attach(delta).attach(alt)
329
352
  _(formulae.derive(:signed_volume, {buys: 60, sells: 40}, nil)).must_equal 999
330
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
331
363
  end
332
364
 
333
365
  describe "#detach" do
data/test/namo_test.rb CHANGED
@@ -3,6 +3,13 @@ require 'minitest-spec-context'
3
3
 
4
4
  require_relative '../lib/namo'
5
5
 
6
+ module SupportedFlow
7
+ include Namo::Formulary
8
+ def net_volume(row); gross(row) - row[:sells] * 2; end
9
+ private
10
+ def gross(row); row[:buys] + row[:sells]; end
11
+ end
12
+
6
13
  describe Namo do
7
14
  let(:sample_data) do
8
15
  [
@@ -1028,6 +1035,36 @@ describe Namo do
1028
1035
  _(proc{flows.detach(untagged)}).must_raise ArgumentError
1029
1036
  end
1030
1037
  end
1038
+
1039
+ describe "private support methods" do
1040
+ it "resolves a public formulary method through its private helper" do
1041
+ namo = Namo.new(flow_data).attach(SupportedFlow)
1042
+ _(namo.values(:net_volume)).must_equal [20, -40, 0]
1043
+ _(namo.derived_dimensions).must_equal [:net_volume]
1044
+ end
1045
+
1046
+ it "resolves an anonymous formulary's public method through its private helper (carrier-collected, host-resolved)" do
1047
+ anonymous = Module.new do
1048
+ include Namo::Formulary
1049
+ def net_volume(row); gross(row) - row[:sells] * 2; end
1050
+ private
1051
+ def gross(row); row[:buys] + row[:sells]; end
1052
+ end
1053
+ namo = Namo.new(flow_data).attach(anonymous)
1054
+ _(namo.values(:net_volume)).must_equal [20, -40, 0]
1055
+ end
1056
+
1057
+ it "lets a later-attached formulary's same-named private helper shadow the earlier one (single shared host)" do
1058
+ shadower = Module.new do
1059
+ include Namo::Formulary
1060
+ def momentum(row); 0; end
1061
+ private
1062
+ def gross(row); 0; end
1063
+ end
1064
+ namo = Namo.new(flow_data).attach(SupportedFlow).attach(shadower)
1065
+ _(namo.values(:net_volume)).must_equal [-80, -240, -150]
1066
+ end
1067
+ end
1031
1068
  end
1032
1069
 
1033
1070
  describe "formularies via class include" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: namo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -98,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
98
  - !ruby/object:Gem::Version
99
99
  version: '0'
100
100
  requirements: []
101
- rubygems_version: 4.0.10
101
+ rubygems_version: 4.0.15
102
102
  specification_version: 4
103
103
  summary: Named dimensional data for Ruby.
104
104
  test_files: []