namo 0.25.1 → 0.26.1
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 +4 -4
- data/CHANGELOG +24 -0
- data/README.md +20 -2
- data/lib/Namo/Formulae.rb +28 -7
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +5 -0
- data/test/Namo/Collection_test.rb +9 -0
- data/test/Namo/Formulae_test.rb +93 -3
- data/test/namo_test.rb +88 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40ae1191c5889356a1f8442673b3f44588521aa07f3f379fe58c9afa85b574c8
|
|
4
|
+
data.tar.gz: c38c7996e0a31e6050adcd9162dbbd6bde3b1c8b12cbaf0b1ab6a7848eeffb78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bf0fb1cf44efc3096388e5adda7503802003e3b49a6539404cefed47ea03af1ca2414089e2599d0d7e0e1676f1a674f3bfadc80131ca4c88401d8c87d38b4c7
|
|
7
|
+
data.tar.gz: f8ea345eee0bca6cf22ea50f470874becce72795649a0e28bba8df3027c70511634accf961f61aa0647177214e18ad139bb24a5870a534ec9e0de096023b67bc
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## 20260710
|
|
4
|
+
|
|
5
|
+
0.26.1: Refactor Namo::Formulae#attach through a private #bind, and document mutability and the corrected collision-resolution idioms.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
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!).
|
|
9
|
+
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.
|
|
10
|
+
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).
|
|
11
|
+
5. ~ docs/: regenerate the print-ready PDFs.
|
|
12
|
+
6. ~ Namo::VERSION: /0.26.0/0.26.1/
|
|
13
|
+
|
|
14
|
+
## 20260704
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
detach becomes the sanctioned formula-removal idiom, displacing the namo.formulae.delete(:name) reach-through. It is by-name with no provenance (the store is a snapshot): the Module arm removes whatever currently holds each of the module's method names. There is no detach! (removing a formula can't collide with data, so no guard to force through) and no removal operator (<< appends; per the 0.14.0 principle, no >>).
|
|
19
|
+
|
|
20
|
+
1. + Namo::Formulae#detach(constituent) — dispatches a Symbol to @store.delete(constituent); a Module to deleting each of its public_instance_methods(false) names, after the same Namo::Formulary marker guard attach uses (an untagged module raises ArgumentError); anything else raises TypeError naming the class, in the << else-arm's style. Returns self. Placed directly after attach/<<. Formulae#delete is unchanged — it stays the store-level primitive detach sits on.
|
|
21
|
+
2. + Namo#detach(constituent) — forwards to @formulae.detach and returns self, placed with the attach family (after attach!, before <<). No guard at the Namo level: there is no data-side interaction, so the Formulae worker owns the dispatch and the raises. Collection inherits detach harmlessly.
|
|
22
|
+
3. ~ test/Namo/Formulae_test.rb: + a "#detach" describe in the formulary tier — both arms return self; the Symbol arm removes the named formula and no-ops on an absent name; the Module arm removes exactly the formulary's method names, leaves a []= name it doesn't contribute, and deletes a []= overwrite occupying a formulary-method name (the by-name/no-provenance pin); ArgumentError on an untagged module; TypeError on a String and a Hash naming the class; chaining removes both. ~ test/namo_test.rb: + a "#detach" describe under formularies — returns the Namo; removes a []= formula from the queryable namespace (derived_dimensions drops it, values returns one nil per row); leaves other formulae resolving; the Module arm empties derived_dimensions; a data dimension is a no-op (the column survives); an absent name no-ops returning the Namo; TypeError on a non-Symbol non-Module and ArgumentError on an untagged module; the #dimensions "reflects mutation" test migrates sales.formulae.delete(:revenue) to sales.detach(:revenue). ~ test/Namo/Collection_test.rb: + a "#detach" describe pinning that a Collection inherits detach — a formulary attached to the collection detaches, so the detail rows no longer resolve its name.
|
|
23
|
+
4. ~ README.md: + a "Removing a formula" note under Formulae (detach removes a formula, returns the Namo, chains, no-ops on an absent name; complements contraction across the data/derived and algebra/mutation splits; never touches @data); + a detach(Module) note in the Formularies section (the reverse of attach, by-name with no provenance, raises on an untagged module, the attach/attach!/detach family, no << removal counterpart, no detach!).
|
|
24
|
+
5. ~ ROADMAP.md: + the 0.26.0 release entry recording the settled decisions (by-name/no-provenance, marker required, loose-on-absent, derived-side-only, no-bang, no-operator, single-argument/chaining, untouched-host-ancestry); current-state bumped to 0.26.0; detach folded into the Summary's formulary vocabulary and the 1.0.0 feature list.
|
|
25
|
+
6. ~ Namo::VERSION: /0.25.1/0.26.0/
|
|
26
|
+
|
|
3
27
|
## 20260703
|
|
4
28
|
|
|
5
29
|
0.25.1: Fix documentation and reformat CHANGELOG to use Markdown headers.
|
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 `[]=`:
|
|
@@ -716,6 +724,12 @@ prices[:sma_close_20] = proc{|row| row[:sma, :close, 20]}
|
|
|
716
724
|
prices[:date, :sma_close_20] # materialises per the usual projection rule
|
|
717
725
|
```
|
|
718
726
|
|
|
727
|
+
#### Removing a formula
|
|
728
|
+
|
|
729
|
+
`detach(:name)` removes a formula, returns the Namo, and chains — `sales.detach(:revenue).detach(:cost)`. Detaching a name that isn't there is a quiet no-op, so a cleanup step composes over any Namo without first checking whether the formula is present. It is the sanctioned way to drop a derived dimension, sitting over the store-level primitive `formulae.delete`.
|
|
730
|
+
|
|
731
|
+
`detach` and contraction are complements across two axes. Contraction (`sales[-:price]`) is non-mutating — it returns a *new* Namo with a *data* dimension removed and the formulae carried. `detach` mutates the receiver and removes a *derived* dimension. So the pair spans both the data/derived split and the algebra/mutation split: contraction is algebra over data, `detach` is mutation of the derived side. `detach` never touches `@data` — naming a data dimension is a no-op and the column survives; removing a data dimension is contraction's job.
|
|
732
|
+
|
|
719
733
|
#### Formularies
|
|
720
734
|
|
|
721
735
|
A *formulary* is a reusable body of derived dimensions — a module whose public instance methods are formulae. A Namo attaches a formulary, and from then on those methods resolve as derived dimensions through the same interface as `[]=` formulae. The methods take `row` (and `namo`, and any further parameters) exactly as a `[]=` formula does:
|
|
@@ -751,10 +765,12 @@ flows.values(:cum_delta) # => [20, -20, -20]
|
|
|
751
765
|
flows.derived_dimensions # => [:signed_volume, :cum_delta]
|
|
752
766
|
```
|
|
753
767
|
|
|
754
|
-
`<<` 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.
|
|
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.
|
|
755
769
|
|
|
756
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.
|
|
757
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
|
+
|
|
758
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.
|
|
759
775
|
|
|
760
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.)
|
|
@@ -764,7 +780,7 @@ The copy is a *snapshot* taken at the moment of attachment, and this is where a
|
|
|
764
780
|
Because each method is copied into the store, the rules already governing `[]=` apply:
|
|
765
781
|
|
|
766
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.
|
|
767
|
-
- **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
|
|
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.
|
|
768
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.
|
|
769
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.
|
|
770
786
|
|
|
@@ -772,6 +788,8 @@ Because each method is copied into the store, the rules already governing `[]=`
|
|
|
772
788
|
|
|
773
789
|
Note that there is no counterpart in `[]=`'s scalar-over-formula eviction and is worth naming: a formulary method that reads a column its own name evicts will **recurse** on access. The eviction removes the datum, and exclusive storage leaves no shadowed value behind the formula, so `Row#[]` re-enters the formula rather than falling back to data — a `def close(row); row[:close] * ...; end` attached with `attach!` over a `:close` data column has no `:close` data left to read. `[]=` rarely bites this way because you write the proc at the call site, beside the name it replaces; a formulary is authored elsewhere, so a method reading its own evicted column is easy to attach without noticing. Self-reference is unguarded here as everywhere in the formula mechanism — keep a formulary's inputs and outputs under distinct names, or `attach` (not `attach!`) and contract deliberately.
|
|
774
790
|
|
|
791
|
+
`detach(OrderFlow)` is the reverse of `attach`: it removes the formulary's public method names from the store, so `flows.attach(OrderFlow)` then `flows.detach(OrderFlow)` leaves the formulae as they started. It works by name and keeps no provenance — the store is a snapshot, so `detach` removes *whatever currently holds each name*, including a `[]=` formula that later overwrote a formulary method of the same name. Like the `attach` side, the marker is required: `detach` raises `ArgumentError` for an untagged module, so you can only detach what could have been attached. The mutating family is `attach` / `attach!` / `detach`; `<<` has no removal counterpart (it *appends* constituents — a removal operator would be cuteness over clarity), and there is no `detach!` because removing a formula can't collide with data, so there is no guard for a bang to force through.
|
|
792
|
+
|
|
775
793
|
Two Namos that expose the same names — one via `[]=`, one via a formulary — are `===`; a Namo with a formulary attached is not `===` to a vanilla Namo of the same data dimensions.
|
|
776
794
|
|
|
777
795
|
Formulary methods take `row` explicitly and index it (`row[:buys]`), the same as `[]=` formulae. Resolving bare names inside a method body (`buys` for `row[:buys]`) and memoising derived values on a frozen Namo arrive in a later release; until then a formulary method is a plain function of its `row` (and `namo`).
|
data/lib/Namo/Formulae.rb
CHANGED
|
@@ -14,17 +14,25 @@ class Namo
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def attach(modul)
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
27
22
|
|
|
23
|
+
def detach(constituent)
|
|
24
|
+
case constituent
|
|
25
|
+
when Symbol
|
|
26
|
+
@store.delete(constituent)
|
|
27
|
+
when Module
|
|
28
|
+
raise_unless_formulary(constituent)
|
|
29
|
+
constituent.public_instance_methods(false).each{|name| @store.delete(name)}
|
|
30
|
+
else
|
|
31
|
+
raise TypeError, "can't detach #{constituent.class} from a Formulae; expected a Symbol or a Module (formulary)"
|
|
32
|
+
end
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
28
36
|
def derive(name, row, namo, *arguments)
|
|
29
37
|
formula = self[name]
|
|
30
38
|
if collection_scoped?(name)
|
|
@@ -99,6 +107,19 @@ class Namo
|
|
|
99
107
|
@host ||= Object.new
|
|
100
108
|
end
|
|
101
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
|
+
|
|
102
123
|
def collection_scoped?(name)
|
|
103
124
|
required_parameter_count(name) >= 2
|
|
104
125
|
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -145,6 +145,15 @@ describe Namo::Collection do
|
|
|
145
145
|
end
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
describe "#detach" do
|
|
149
|
+
it "detaches a formulary attached to the collection, so the detail rows no longer resolve its name" do
|
|
150
|
+
collection << component_pricing
|
|
151
|
+
_(collection.values(:cost_per_kg)).must_equal [250, 250, 200, 250]
|
|
152
|
+
collection.detach(component_pricing)
|
|
153
|
+
_(collection.derived_dimensions).wont_include :cost_per_kg
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
148
157
|
describe "#find" do
|
|
149
158
|
it "returns the member with the given name" do
|
|
150
159
|
_(collection.find(:chassis)).must_be_same_as chassis
|
data/test/Namo/Formulae_test.rb
CHANGED
|
@@ -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
|
|
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]).
|
|
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
|
-
|
|
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,73 @@ 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
|
|
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
|
|
331
421
|
end
|
|
332
422
|
|
|
333
423
|
describe "carry-through" 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
|
[
|
|
@@ -165,7 +172,7 @@ describe Namo do
|
|
|
165
172
|
it "reflects mutation on the next call" do
|
|
166
173
|
sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
|
|
167
174
|
_(sales.dimensions).must_include :revenue
|
|
168
|
-
sales.
|
|
175
|
+
sales.detach(:revenue)
|
|
169
176
|
_(sales.dimensions).wont_include :revenue
|
|
170
177
|
end
|
|
171
178
|
end
|
|
@@ -978,6 +985,86 @@ describe Namo do
|
|
|
978
985
|
_(flows === other).must_equal true
|
|
979
986
|
end
|
|
980
987
|
end
|
|
988
|
+
|
|
989
|
+
describe "#detach" do
|
|
990
|
+
it "returns the Namo" do
|
|
991
|
+
_(flows.detach(:signed_volume)).must_be_same_as flows
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
it "removes a formula from the queryable namespace" do
|
|
995
|
+
namo = Namo.new(flow_data)
|
|
996
|
+
namo[:revenue] = proc{|r| r[:buys]}
|
|
997
|
+
namo.detach(:revenue)
|
|
998
|
+
_(namo.derived_dimensions).wont_include :revenue
|
|
999
|
+
_(namo.values(:revenue)).must_equal [nil, nil, nil]
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
it "leaves other formulae resolving" do
|
|
1003
|
+
namo = Namo.new(flow_data)
|
|
1004
|
+
namo[:revenue] = proc{|r| r[:buys]}
|
|
1005
|
+
namo[:tally] = proc{|r| r[:sells]}
|
|
1006
|
+
namo.detach(:revenue)
|
|
1007
|
+
_(namo.values(:tally)).must_equal [40, 120, 75]
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
it "removes an attached formulary's methods via the Module arm" do
|
|
1011
|
+
flows.detach(delta)
|
|
1012
|
+
_(flows.derived_dimensions).must_equal []
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
it "never exposed the formulary's private helper" do
|
|
1016
|
+
_(flows.derived_dimensions).wont_include :helper
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
it "never touches data — a data dimension is a no-op" do
|
|
1020
|
+
flows.detach(:date)
|
|
1021
|
+
_(flows.data_dimensions).must_include :date
|
|
1022
|
+
_(flows.values(:date)).must_equal [1, 2, 3]
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
it "is a no-op on an absent name, returning the Namo" do
|
|
1026
|
+
_(flows.detach(:missing)).must_be_same_as flows
|
|
1027
|
+
_(flows.values(:signed_volume)).must_equal [20, -40, 0]
|
|
1028
|
+
end
|
|
1029
|
+
|
|
1030
|
+
it "raises TypeError on a non-Symbol non-Module" do
|
|
1031
|
+
_(proc{flows.detach("signed_volume")}).must_raise TypeError
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
it "raises ArgumentError on an untagged module" do
|
|
1035
|
+
_(proc{flows.detach(untagged)}).must_raise ArgumentError
|
|
1036
|
+
end
|
|
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
|
|
981
1068
|
end
|
|
982
1069
|
|
|
983
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.
|
|
4
|
+
version: 0.26.1
|
|
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.
|
|
101
|
+
rubygems_version: 4.0.15
|
|
102
102
|
specification_version: 4
|
|
103
103
|
summary: Named dimensional data for Ruby.
|
|
104
104
|
test_files: []
|