namo 0.23.0 → 0.24.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 +4 -4
- data/CHANGELOG +13 -0
- data/README.md +20 -1
- data/lib/Namo/Collection.rb +14 -4
- data/lib/Namo/VERSION.rb +1 -1
- data/lib/namo.rb +13 -1
- data/test/Namo/Collection_test.rb +42 -0
- data/test/namo_test.rb +36 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9e1ecaf29846a7876a59dae0d82c6c9d4e1fddd84b0c6a101941214ed6544517
|
|
4
|
+
data.tar.gz: 4245b13c3485a504ab5c365803de282cb508579b2ca9aec0799df0f1ce653d68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41156dc96b98f59f8ff0dcf0d78f3b80ed895f402e11af9a53ba2e6cdb13d8bf39f1a38f48f2457bfe8bcf9082d146b73dafa4663e068816b2339d6888fd25af
|
|
7
|
+
data.tar.gz: e9c1f033e17fd2b10ffab984ea6349b7641de8abf0f04f23f85c893793242abd9e795847244855ad18b8c3f74b5876cce16dd212b2d1d2a6d327998657c9a4dc
|
data/CHANGELOG
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
CHANGELOG
|
|
2
2
|
_________
|
|
3
3
|
|
|
4
|
+
20260629
|
|
5
|
+
0.24.0: ~ Namo#<<, Namo::Collection#<< — widen from a formulary-only alias into a polymorphic "append a constituent" operator.
|
|
6
|
+
|
|
7
|
+
A base Namo's constituents are formularies and rows: << dispatches a Module to attach (0.23.0 behaviour, unchanged), a Hash to a data-row append, and a Row to a row append of its underlying hash. It does not take a bare callable (formula registration stays with []=) or a whole Namo (combining is +'s job) — both raise. Collection#<< is reconciled to the same verb over its own constituents: a Namo is a member, a Module attaches, and a loose Hash/Row raises (a Collection's rows are derived from its members, so a loose row has no durable home). This Hash/Row divergence — a base Namo appends the row, a Collection refuses it — is deliberate, not an oversight.
|
|
8
|
+
|
|
9
|
+
1. ~ lib/namo.rb: ~ Namo#<< — replace the alias_method :<<, :attach with a dispatching << that routes Module→attach, Row→add_row(row.to_h), Hash→add_row(row), else→TypeError. attach stays Module-only and unchanged, so the 0.23.0 collision-guard and formulary semantics carry over verbatim and a Module's << still reaches it. + private add_row(row), appending to @data and returning self (so << chains). A whole Namo falls to the else and raises (it is not a Module/Row/Hash) — combining is +. Row-append is a data mutation that does not pass through []=, so it does not trigger formula-eviction; a row carrying a formula-named key surfaces live through the formula on access, per existing precedence (not guarded — the exclusivity invariant is []='s).
|
|
10
|
+
2. ~ lib/Namo/Collection.rb: ~ Collection#<< — keep the splat + flatten (a member or an array of them), but dispatch each constituent: Namo→add_member, Module→attach (inherited from Namo — a Collection carries formulae over its detail view), Hash/Row→ArgumentError redirecting to member-add, else→TypeError. + private add_member(member), the prior reject-by-name + append body extracted verbatim. The @data = detail.data rebuild after the loop is preserved, so member-adds re-materialise detail as before and a Module-only << rebuilds harmlessly. Module routes to the inherited Namo#attach; member-add behaviour is byte-identical to 0.23.0 for the Namo/array cases.
|
|
11
|
+
3. ~ test/namo_test.rb: ~ the formularies "#<<" describe gains the row arms — a Hash appends as a data row (and returns the Namo), a Row appends its underlying hash, a whole Namo raises TypeError (+'s job), a bare callable raises TypeError ([]='s job), and a mixed chain interleaves rows and a formulary. The two 0.23.0 Module-arm tests are unchanged.
|
|
12
|
+
4. ~ test/Namo/Collection_test.rb: ~ the "#<<" describe gains a formulary attach whose row-scoped method resolves over the detail rows, a collection-scoped method (the namo_collection convention) reaching members, a loose Hash and a loose Row each raising ArgumentError with the member-redirect message, and a chain mixing member-adds with a formulary attach. The 0.23.0 member-add tests are unchanged.
|
|
13
|
+
5. ~ README.md: ~ the Formularies << note — << now appends a constituent (Module, Hash, or Row) to a base Namo, chains, and raises on a bare callable or a whole Namo; the bare-Hash-is-always-a-row and row-append-does-not-evict notes added. ~ the Collection "<< and unnamed members" section — Collection#<< dispatches member/Module/raise, with the deliberate Hash/Row divergence spelled out. + a "Collection formulae" subsection — the namo_collection convention (an ordinary two-arity formulary method reaching a collection view in its body), no marker and no second store.
|
|
14
|
+
6. ~ ROADMAP.md: + the 0.24.0 release entry; current-state bumped to 0.24.0; pivot/reshaping recorded as a decision (long-form aggregation is in-grain future work, widening-to-a-grid is presentational and belongs at an output edge, not the algebra), and collection formulae noted as needing no new mechanism.
|
|
15
|
+
7. ~ Namo::VERSION: /0.23.0/0.24.0/
|
|
16
|
+
|
|
4
17
|
20260627
|
|
5
18
|
0.23.0: + Namo::Formulary — A formulary is a reusable body of derived dimensions a Namo instance may draw upon.
|
|
6
19
|
|
data/README.md
CHANGED
|
@@ -751,7 +751,7 @@ flows.values(:cum_delta) # => [20, -20, -20]
|
|
|
751
751
|
flows.derived_dimensions # => [:signed_volume, :cum_delta]
|
|
752
752
|
```
|
|
753
753
|
|
|
754
|
-
`<<`
|
|
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 is a data mutation that does not pass through `[]=`, so it does not trigger formula-eviction; a row carrying a formula-named key is your data, surfacing live through the formula on access per the usual precedence.
|
|
755
755
|
|
|
756
756
|
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
757
|
|
|
@@ -1073,6 +1073,8 @@ gt << SubAssembly.new(name: :powertrain, data: [...]) # replaces the existing
|
|
|
1073
1073
|
gt << [front_suspension, rear_suspension] # adds each
|
|
1074
1074
|
```
|
|
1075
1075
|
|
|
1076
|
+
On a Collection `<<` dispatches to the *constituent* appropriate to a collection: a **member** (a Namo, or an array of them) is added as above; a **module** attaches a formulary (a Collection is a Namo, so it carries formulae over its detail view — see below); a loose **Hash or Row raises** an `ArgumentError` that redirects to member-add. This is the one place `Namo#<<` and `Collection#<<` deliberately diverge: a base Namo appends a loose row, a Collection refuses it — a Collection's rows are *derived* from its members, so a loose row has no durable home (the next member-add rebuilds the data view and would erase it). The honest response is to refuse and point at member-add. A whole Namo arriving at a Collection is unambiguously a member, never a row-merge, because `<<` has no whole-Namo arm (that is `+`).
|
|
1077
|
+
|
|
1076
1078
|
There is no insertion-time guard against unnamed members. An unnamed member is simply appended (no name to collide on) and is unfindable by `find` — the honest consequence of having no name, not an error. `find(name)` returns the member with that name, or `nil`:
|
|
1077
1079
|
|
|
1078
1080
|
```ruby
|
|
@@ -1082,6 +1084,23 @@ gt.find(:missing) # => nil
|
|
|
1082
1084
|
|
|
1083
1085
|
(`find(name)` is member lookup; it shadows `Enumerable#find` on Collections. Predicate search over rows remains available as `detect`.)
|
|
1084
1086
|
|
|
1087
|
+
#### Collection formulae
|
|
1088
|
+
|
|
1089
|
+
A formulary attached to a Collection resolves over its detail rows like any other — a Collection is a Namo, and its data view is the materialised detail. A *collection formula* needs no new mechanism: it is an ordinary two-arity formulary method whose `namo` argument is the Collection, reaching for a collection view (`summary`, `detail`, `members`) in its body. It resolves through the same `derive` path as every other formula; member-awareness lives in the method body, not in any marker or second store. By convention, name that argument `namo_collection` to signal it expects a Collection — a plain Namo lacking `members`/`summary` will `NoMethodError`, which is honest self-enforcement, no guard needed.
|
|
1090
|
+
|
|
1091
|
+
```ruby
|
|
1092
|
+
module FleetMetrics
|
|
1093
|
+
include Namo::Formulary
|
|
1094
|
+
|
|
1095
|
+
def member_count(row, namo_collection)
|
|
1096
|
+
namo_collection.members.size
|
|
1097
|
+
end
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
gt << FleetMetrics
|
|
1101
|
+
gt.values(:member_count) # => one entry per detail row, each the member count
|
|
1102
|
+
```
|
|
1103
|
+
|
|
1085
1104
|
#### View lifetime and liveness
|
|
1086
1105
|
|
|
1087
1106
|
Materialisation is pure-live: the Collection rebuilds its data view from the current members on every `<<`, with no memoisation. So a mutation is reflected immediately — add a member, then summarise or detail, and the new member is included.
|
data/lib/Namo/Collection.rb
CHANGED
|
@@ -5,10 +5,15 @@ class Namo
|
|
|
5
5
|
class Collection < Namo
|
|
6
6
|
attr_reader :members
|
|
7
7
|
|
|
8
|
-
def <<(*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
def <<(*constituents)
|
|
9
|
+
constituents.flatten.each do |constituent|
|
|
10
|
+
case constituent
|
|
11
|
+
when Namo then add_member(constituent)
|
|
12
|
+
when Module then attach(constituent)
|
|
13
|
+
when Hash, Row
|
|
14
|
+
raise ArgumentError, "a Collection's rows come from its members; add a member (a named Namo), not a loose row"
|
|
15
|
+
else raise TypeError, "can't append #{constituent.class} to a Collection"
|
|
16
|
+
end
|
|
12
17
|
end
|
|
13
18
|
@data = detail.data
|
|
14
19
|
self
|
|
@@ -48,5 +53,10 @@ class Namo
|
|
|
48
53
|
@members = []
|
|
49
54
|
super
|
|
50
55
|
end
|
|
56
|
+
|
|
57
|
+
def add_member(member)
|
|
58
|
+
@members.reject!{|existing| existing.name == member.name} unless member.name.nil?
|
|
59
|
+
@members << member
|
|
60
|
+
end
|
|
51
61
|
end
|
|
52
62
|
end
|
data/lib/Namo/VERSION.rb
CHANGED
data/lib/namo.rb
CHANGED
|
@@ -97,7 +97,14 @@ class Namo
|
|
|
97
97
|
self
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
def <<(constituent)
|
|
101
|
+
case constituent
|
|
102
|
+
when Module then attach(constituent)
|
|
103
|
+
when Row then add_row(constituent.to_h)
|
|
104
|
+
when Hash then add_row(constituent)
|
|
105
|
+
else raise TypeError, "can't append #{constituent.class} to a Namo; expected a Module (formulary), a Hash, or a Row"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
101
108
|
|
|
102
109
|
def +(other)
|
|
103
110
|
raise_unless_namo(other)
|
|
@@ -252,6 +259,11 @@ class Namo
|
|
|
252
259
|
attach_included_formularies
|
|
253
260
|
end
|
|
254
261
|
|
|
262
|
+
def add_row(row)
|
|
263
|
+
@data << row
|
|
264
|
+
self
|
|
265
|
+
end
|
|
266
|
+
|
|
255
267
|
def attach_included_formularies
|
|
256
268
|
self.class.ancestors.reverse.each do |modul|
|
|
257
269
|
next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
|
|
@@ -45,6 +45,20 @@ describe Namo::Collection do
|
|
|
45
45
|
Namo::Collection.new.tap{|c| c << [powertrain, chassis, body]}
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
let(:component_pricing) do
|
|
49
|
+
Module.new do
|
|
50
|
+
include Namo::Formulary
|
|
51
|
+
def cost_per_kg(row); row[:cost] / row[:weight]; end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
let(:fleet_metrics) do
|
|
56
|
+
Module.new do
|
|
57
|
+
include Namo::Formulary
|
|
58
|
+
def member_count(row, namo_collection); namo_collection.members.size; end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
48
62
|
describe "construction" do
|
|
49
63
|
it "starts with empty members" do
|
|
50
64
|
_(Namo::Collection.new.members).must_equal []
|
|
@@ -101,6 +115,34 @@ describe Namo::Collection do
|
|
|
101
115
|
collection = Namo::Collection.new
|
|
102
116
|
_(collection << powertrain).must_be_same_as collection
|
|
103
117
|
end
|
|
118
|
+
|
|
119
|
+
it "attaches a formulary whose row-scoped method resolves over the detail rows" do
|
|
120
|
+
collection << component_pricing
|
|
121
|
+
_(collection.values(:cost_per_kg)).must_equal [250, 250, 200, 250]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it "attaches a formulary whose collection-scoped method reaches members" do
|
|
125
|
+
collection << fleet_metrics
|
|
126
|
+
_(collection.values(:member_count)).must_equal [3, 3, 3, 3]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "raises an ArgumentError on a loose Hash, redirecting to member-add" do
|
|
130
|
+
error = _(proc{collection << {component: 'bolt', weight: 1, cost: 5}}).must_raise ArgumentError
|
|
131
|
+
_(error.message).must_match(/member/)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises an ArgumentError on a loose Row, redirecting to member-add" do
|
|
135
|
+
row = Namo.new([{component: 'bolt', weight: 1, cost: 5}]).entries.first
|
|
136
|
+
error = _(proc{collection << row}).must_raise ArgumentError
|
|
137
|
+
_(error.message).must_match(/member/)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
it "chains member-adds and a formulary attach" do
|
|
141
|
+
collection = Namo::Collection.new
|
|
142
|
+
collection << powertrain << chassis << component_pricing
|
|
143
|
+
_(collection.members.size).must_equal 2
|
|
144
|
+
_(collection.values(:cost_per_kg)).must_equal [250, 250, 200]
|
|
145
|
+
end
|
|
104
146
|
end
|
|
105
147
|
|
|
106
148
|
describe "#find" do
|
data/test/namo_test.rb
CHANGED
|
@@ -758,6 +758,42 @@ describe Namo do
|
|
|
758
758
|
namo = Namo.new(flow_data)
|
|
759
759
|
_(namo << delta).must_be_same_as namo
|
|
760
760
|
end
|
|
761
|
+
|
|
762
|
+
it "appends a Hash as a data row" do
|
|
763
|
+
namo = Namo.new(flow_data)
|
|
764
|
+
namo << {date: 4, buys: 10, sells: 5}
|
|
765
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4]
|
|
766
|
+
_(namo.values(:buys)).must_equal [60, 80, 75, 10]
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
it "returns the Namo when appending a row" do
|
|
770
|
+
namo = Namo.new(flow_data)
|
|
771
|
+
_(namo << {date: 4, buys: 10, sells: 5}).must_be_same_as namo
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it "appends a Row's underlying hash as a data row" do
|
|
775
|
+
namo = Namo.new(flow_data)
|
|
776
|
+
row = Namo.new([{date: 4, buys: 10, sells: 5}]).entries.first
|
|
777
|
+
namo << row
|
|
778
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4]
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
it "raises a TypeError on a whole Namo, which is +'s job" do
|
|
782
|
+
namo = Namo.new(flow_data)
|
|
783
|
+
_(proc{namo << Namo.new(flow_data)}).must_raise TypeError
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
it "raises a TypeError on a bare callable, which is []='s job" do
|
|
787
|
+
namo = Namo.new(flow_data)
|
|
788
|
+
_(proc{namo << ->(row){row[:buys]}}).must_raise TypeError
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
it "chains mixed arms: rows and a formulary" do
|
|
792
|
+
namo = Namo.new(flow_data)
|
|
793
|
+
namo << {date: 4, buys: 10, sells: 5} << delta << {date: 5, buys: 0, sells: 0}
|
|
794
|
+
_(namo.values(:date)).must_equal [1, 2, 3, 4, 5]
|
|
795
|
+
_(namo.values(:signed_volume)).must_equal [20, -40, 0, 5, 0]
|
|
796
|
+
end
|
|
761
797
|
end
|
|
762
798
|
|
|
763
799
|
describe "#derived_dimensions" do
|