namo 0.2.0 → 0.4.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: 6841b3921358ce7eefdbfbdba3a62eafd74857223d9df86038f9cc8087bbb947
4
- data.tar.gz: bf05398ce78ea49683fbb7adb0b9989c6d6df86864a9bdf2373b156b14d8d5e7
3
+ metadata.gz: 0dbfe699c8bbb334e0f72bbfd1859b0eee910be2c72eaab988da633b58b438e6
4
+ data.tar.gz: 40238ef4593d7820bddfb6cb708823fa9c269a51def947e89cdb43e4b69cbae0
5
5
  SHA512:
6
- metadata.gz: 2887c9dee76341b5e4ee75d50a1416475bd8c579d52117f67dcab81c9fe7c8cb1c50c08be7295db74aaf1f2d8727aa678b95d231d4c2e1d82a25b1ca050de61e
7
- data.tar.gz: d5c5a35ac078d60dcd1baf43e73b5be1aa1bf02ed27a65071fd146223402a812a8b9ff9b88e6be5e4e62b5b47966460df613994d79215405439cde96c1a63278
6
+ metadata.gz: 9e6dfd0bcf4b8d370373a43e507a4d208e4ccb4478e044eb138bf5f2bfa61612cbb4f10735be691c852d2f8bf672eca70720cc6a5c35cd5fb02fae224388e010
7
+ data.tar.gz: 2b147c6e7faf7b9f92e8e2457fcd657734a35946c326857e3b7c09177ceaa7f3ce4f0892bdcbb68ec251d673e1b298c1ed06416fe43918656734a58bd0c639ea
data/CHANGELOG CHANGED
@@ -1,6 +1,26 @@
1
1
  CHANGELOG
2
2
  _________
3
3
 
4
+ 20260415
5
+ 0.4.0: + concatenation (+) and row removal (-)
6
+
7
+ 1. + Namo#+: Concatenate two Namo instances with matching dimensions. Appends rows from the second to the first. Raises ArgumentError when dimensions differ.
8
+ 2. + Namo#-: Remove rows from the first Namo that appear exactly in the second. Whole-row set difference. Raises ArgumentError when dimensions differ.
9
+ 3. ~ Namo_test.rb: Add tests for #+ and #-.
10
+ 4. ~ README.md: + Concatenation section, + Row removal section.
11
+ 5. ~ Namo::VERSION: /0.3.0/0.4.0/
12
+
13
+ 20260415
14
+ 0.3.0: + contraction
15
+
16
+ 1. + Namo::NegatedDimension: Wrapper for negated dimension names used in contraction.
17
+ 2. + Symbol#-@: Produce a NegatedDimension, enabling -:date syntax.
18
+ 3. ~ Namo#[]: Extend to recognise NegatedDimension for contraction (remove named dimensions, keep everything else). Raise ArgumentError when mixing projection and contraction.
19
+ 4. > Namo::Row: Extract to Namo/Row.rb.
20
+ 5. ~ namo_test.rb: Add tests for contraction, mixed-mode error, and contraction with selection and formulae. Split tests into one file per class: Row_test.rb, NegatedDimension_test.rb, Symbol_test.rb.
21
+ 6. ~ README.md: + Contraction section
22
+ 7. ~ Namo::VERSION: /0.2.0/0.3.0/
23
+
4
24
  20260414
5
25
  0.2.0: Include Enumerable.
6
26
 
data/README.md CHANGED
@@ -111,7 +111,99 @@ sales[:quarter, :price, product: 'Widget']
111
111
  # ]>
112
112
  ```
113
113
 
114
- Selection and projection always return a new Namo instance, so everything chains.
114
+ ### Contraction
115
+
116
+ Contraction is the complement of projection. Projection says "keep these dimensions"; contraction says "remove these dimensions, keep everything else":
117
+
118
+ ```ruby
119
+ sales[-:price, -:quantity]
120
+ # => #<Namo [
121
+ # {product: 'Widget', quarter: 'Q1'},
122
+ # {product: 'Widget', quarter: 'Q2'},
123
+ # {product: 'Gadget', quarter: 'Q1'},
124
+ # {product: 'Gadget', quarter: 'Q2'}
125
+ # ]>
126
+ ```
127
+
128
+ The `-:price` syntax uses unary minus on Symbol to produce a negated dimension. Mixing projection and contraction in the same call is an error — the two modes are mutually exclusive:
129
+
130
+ ```ruby
131
+ sales[:product, -:price] # => ArgumentError
132
+ ```
133
+
134
+ Selection and contraction can be chained:
135
+
136
+ ```ruby
137
+ sales[product: 'Widget'][-:price, -:quantity]
138
+ # => #<Namo [
139
+ # {product: 'Widget', quarter: 'Q1'},
140
+ # {product: 'Widget', quarter: 'Q2'}
141
+ # ]>
142
+ ```
143
+
144
+ Or combined in a single call (names before selectors):
145
+
146
+ ```ruby
147
+ sales[-:price, -:quantity, product: 'Widget']
148
+ # => #<Namo [
149
+ # {product: 'Widget', quarter: 'Q1'},
150
+ # {product: 'Widget', quarter: 'Q2'}
151
+ # ]>
152
+ ```
153
+
154
+ Selection, projection, and contraction always return a new Namo instance, so everything chains.
155
+
156
+ ### Concatenation
157
+
158
+ `+` combines two Namo objects that share the same dimensions by appending the rows of the second to the first:
159
+
160
+ ```ruby
161
+ q1_sales = Namo.new([
162
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
163
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
164
+ ])
165
+
166
+ q2_sales = Namo.new([
167
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
168
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
169
+ ])
170
+
171
+ all_sales = q1_sales + q2_sales
172
+ # => #<Namo [
173
+ # {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
174
+ # {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
175
+ # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
176
+ # {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
177
+ # ]>
178
+ ```
179
+
180
+ The dimensions must match — concatenating Namo objects with different dimensions raises an `ArgumentError`. Formulae carry through from the left-hand side.
181
+
182
+ ### Row Removal
183
+
184
+ `-` removes from the first Namo any row that appears exactly in the second:
185
+
186
+ ```ruby
187
+ sales = Namo.new([
188
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
189
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
190
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
191
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
192
+ ])
193
+
194
+ discontinued = Namo.new([
195
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40},
196
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
197
+ ])
198
+
199
+ sales - discontinued
200
+ # => #<Namo [
201
+ # {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
202
+ # {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150}
203
+ # ]>
204
+ ```
205
+
206
+ Removal is exact — every dimension, every value must match. The dimensions must match; different dimensions raise an `ArgumentError`. Formulae carry through from the left-hand side.
115
207
 
116
208
  ### Formulae
117
209
 
@@ -0,0 +1,14 @@
1
+ # Namo/NegatedDimension.rb
2
+ # Namo::NegatedDimension
3
+
4
+ class Namo
5
+ class NegatedDimension
6
+ attr_reader :name
7
+
8
+ private
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+ end
14
+ end
data/lib/Namo/Row.rb ADDED
@@ -0,0 +1,36 @@
1
+ # Namo/Row.rb
2
+ # Namo::Row
3
+
4
+ class Namo
5
+ class Row
6
+ def [](name)
7
+ if @formulae.key?(name)
8
+ @formulae[name].call(self)
9
+ else
10
+ @row[name]
11
+ end
12
+ end
13
+
14
+ def match?(selections)
15
+ selections.all? do |dimension, coordinate|
16
+ case coordinate
17
+ when Array, Range
18
+ coordinate.include?(self[dimension])
19
+ else
20
+ self[dimension] == coordinate
21
+ end
22
+ end
23
+ end
24
+
25
+ def to_h
26
+ @row
27
+ end
28
+
29
+ private
30
+
31
+ def initialize(row, formulae)
32
+ @row = row
33
+ @formulae = formulae
34
+ end
35
+ end
36
+ end
data/lib/Namo/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Namo::VERSION
3
3
 
4
4
  class Namo
5
- VERSION = '0.2.0'
5
+ VERSION = '0.4.0'
6
6
  end
data/lib/Symbol.rb ADDED
@@ -0,0 +1,8 @@
1
+ # Symbol.rb
2
+ # Symbol#-@
3
+
4
+ class Symbol
5
+ def -@
6
+ Namo::NegatedDimension.new(self)
7
+ end
8
+ end
data/lib/namo.rb CHANGED
@@ -1,41 +1,13 @@
1
1
  # namo.rb
2
2
  # Namo
3
3
 
4
+ require_relative 'Namo/NegatedDimension'
5
+ require_relative 'Namo/Row'
6
+ require_relative 'Symbol'
7
+
4
8
  class Namo
5
9
  include Enumerable
6
10
 
7
- class Row
8
- def [](name)
9
- if @formulae.key?(name)
10
- @formulae[name].call(self)
11
- else
12
- @row[name]
13
- end
14
- end
15
-
16
- def match?(selections)
17
- selections.all? do |dimension, coordinate|
18
- case coordinate
19
- when Array, Range
20
- coordinate.include?(self[dimension])
21
- else
22
- self[dimension] == coordinate
23
- end
24
- end
25
- end
26
-
27
- def to_h
28
- @row
29
- end
30
-
31
- private
32
-
33
- def initialize(row, formulae)
34
- @row = row
35
- @formulae = formulae
36
- end
37
- end
38
-
39
11
  attr_accessor :data
40
12
  attr_accessor :formulae
41
13
 
@@ -53,16 +25,26 @@ class Namo
53
25
 
54
26
  def [](*names, **selections)
55
27
  rows = selections.any? ? select{|row| row.match?(selections)} : entries
56
- data = (
57
- if names.any?
28
+ negated, positive = names.partition{|n| n.is_a?(NegatedDimension)}
29
+ if negated.any? && positive.any?
30
+ raise ArgumentError, "cannot mix projection and contraction in a single call"
31
+ end
32
+ projected = (
33
+ if negated.any?
34
+ excluded = negated.map(&:name)
35
+ kept = dimensions - excluded
36
+ rows.map do |row|
37
+ kept.each_with_object({}){|name, hash| hash[name] = row[name]}
38
+ end
39
+ elsif positive.any?
58
40
  rows.map do |row|
59
- names.each_with_object({}){|name, hash| hash[name] = row[name]}
41
+ positive.each_with_object({}){|name, hash| hash[name] = row[name]}
60
42
  end
61
43
  else
62
44
  rows.map(&:to_h)
63
45
  end
64
46
  )
65
- self.class.new(data, formulae: @formulae.dup)
47
+ self.class.new(projected, formulae: @formulae.dup)
66
48
  end
67
49
 
68
50
  def []=(name, proc)
@@ -74,6 +56,20 @@ class Namo
74
56
  @data.each{|row_data| block.call(Row.new(row_data, @formulae))}
75
57
  end
76
58
 
59
+ def +(other)
60
+ unless dimensions == other.dimensions
61
+ raise ArgumentError, "dimensions do not match"
62
+ end
63
+ self.class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
64
+ end
65
+
66
+ def -(other)
67
+ unless dimensions == other.dimensions
68
+ raise ArgumentError, "dimensions do not match"
69
+ end
70
+ self.class.new(@data - other.data, formulae: @formulae.dup)
71
+ end
72
+
77
73
  def to_a
78
74
  @data.map do |row|
79
75
  row.keys.each_with_object({}) do |key, hash|
data/namo.gemspec CHANGED
@@ -1,5 +1,11 @@
1
1
  require_relative './lib/Namo/VERSION'
2
2
 
3
+ class Gem::Specification
4
+ def development_dependencies=(gems)
5
+ gems.each{|gem| add_development_dependency(*gem)}
6
+ end
7
+ end
8
+
3
9
  Gem::Specification.new do |spec|
4
10
  spec.name = 'namo'
5
11
  spec.version = Namo::VERSION
@@ -13,20 +19,23 @@ Gem::Specification.new do |spec|
13
19
  spec.license = 'MIT'
14
20
 
15
21
  spec.required_ruby_version = '>= 2.7'
22
+
16
23
  spec.require_paths = ['lib']
17
24
 
18
25
  spec.files = [
26
+ 'namo.gemspec',
19
27
  'CHANGELOG',
20
28
  'Gemfile',
21
- Dir['lib/**/*.rb'],
22
29
  'LICENSE',
23
- 'namo.gemspec',
24
30
  'Rakefile',
25
31
  'README.md',
32
+ Dir['lib/**/*.rb'],
26
33
  Dir['test/**/*.rb'],
27
34
  ].flatten
28
35
 
29
- spec.add_development_dependency 'minitest'
30
- spec.add_development_dependency 'minitest-spec-context'
31
- spec.add_development_dependency 'rake'
36
+ spec.development_dependencies = %w{
37
+ minitest
38
+ minitest-spec-context
39
+ rake
40
+ }
32
41
  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
@@ -0,0 +1,69 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest-spec-context'
3
+
4
+ require_relative '../../lib/namo'
5
+
6
+ describe Namo::Row do
7
+ let(:row_data) do
8
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100}
9
+ end
10
+
11
+ let(:formulae) do
12
+ {}
13
+ end
14
+
15
+ let(:row) do
16
+ Namo::Row.new(row_data, formulae)
17
+ end
18
+
19
+ describe "#[]" do
20
+ it "returns raw data by dimension name" do
21
+ _(row[:product]).must_equal 'Widget'
22
+ _(row[:price]).must_equal 10.0
23
+ end
24
+
25
+ it "returns nil for missing dimensions" do
26
+ _(row[:missing]).must_be_nil
27
+ end
28
+
29
+ it "resolves formulae over raw data" do
30
+ formulae[:revenue] = proc{|r| r[:price] * r[:quantity]}
31
+ _(row[:revenue]).must_equal 1000.0
32
+ end
33
+
34
+ it "composes formulae" do
35
+ formulae[:revenue] = proc{|r| r[:price] * r[:quantity]}
36
+ formulae[:cost] = proc{|r| r[:quantity] * 4.0}
37
+ formulae[:profit] = proc{|r| r[:revenue] - r[:cost]}
38
+ _(row[:profit]).must_equal 600.0
39
+ end
40
+ end
41
+
42
+ describe "#match?" do
43
+ it "matches a single value" do
44
+ _(row.match?(product: 'Widget')).must_equal true
45
+ _(row.match?(product: 'Gadget')).must_equal false
46
+ end
47
+
48
+ it "matches an array of values" do
49
+ _(row.match?(product: ['Widget', 'Gadget'])).must_equal true
50
+ _(row.match?(product: ['Gadget'])).must_equal false
51
+ end
52
+
53
+ it "matches a range" do
54
+ _(row.match?(price: 5.0..15.0)).must_equal true
55
+ _(row.match?(price: 20.0..30.0)).must_equal false
56
+ end
57
+
58
+ it "matches multiple dimensions" do
59
+ _(row.match?(product: 'Widget', quarter: 'Q1')).must_equal true
60
+ _(row.match?(product: 'Widget', quarter: 'Q2')).must_equal false
61
+ end
62
+ end
63
+
64
+ describe "#to_h" do
65
+ it "returns the underlying row hash" do
66
+ _(row.to_h).must_equal row_data
67
+ end
68
+ end
69
+ end
@@ -85,6 +85,53 @@ describe Namo do
85
85
  _(result.to_a).must_equal [{price: 10.0}, {price: 10.0}]
86
86
  end
87
87
  end
88
+
89
+ context "contraction" do
90
+ it "removes named dimensions" do
91
+ result = sales[-:price, -:quantity]
92
+ _(result.dimensions).must_equal [:product, :quarter]
93
+ _(result.to_a.count).must_equal 4
94
+ _(result.to_a.first).must_equal({product: 'Widget', quarter: 'Q1'})
95
+ end
96
+
97
+ it "removes a single dimension" do
98
+ result = sales[-:price]
99
+ _(result.dimensions).must_equal [:product, :quarter, :quantity]
100
+ _(result.to_a.count).must_equal 4
101
+ end
102
+
103
+ it "raises when mixing projection and contraction" do
104
+ _ { sales[:product, -:price] }.must_raise ArgumentError
105
+ end
106
+
107
+ it "carries formulae through contraction" do
108
+ sales[:label] = proc{|r| "#{r[:product]}-#{r[:quarter]}"}
109
+ result = sales[-:price, -:quantity]
110
+ _(result.map{|row| row[:label]}).must_equal [
111
+ 'Widget-Q1', 'Widget-Q2', 'Gadget-Q1', 'Gadget-Q2'
112
+ ]
113
+ end
114
+ end
115
+
116
+ context "selection and contraction" do
117
+ it "can use them together" do
118
+ result = sales[-:price, -:quantity, product: 'Widget']
119
+ _(result.to_a.count).must_equal 2
120
+ _(result.to_a).must_equal [
121
+ {product: 'Widget', quarter: 'Q1'},
122
+ {product: 'Widget', quarter: 'Q2'}
123
+ ]
124
+ end
125
+
126
+ it "can chain them" do
127
+ result = sales[product: 'Widget'][-:price, -:quantity]
128
+ _(result.to_a.count).must_equal 2
129
+ _(result.to_a).must_equal [
130
+ {product: 'Widget', quarter: 'Q1'},
131
+ {product: 'Widget', quarter: 'Q2'}
132
+ ]
133
+ end
134
+ end
88
135
  end
89
136
 
90
137
  describe "#[]= formulae" do
@@ -204,6 +251,92 @@ describe Namo do
204
251
  end
205
252
  end
206
253
 
254
+ describe "#+" do
255
+ let(:more_data) do
256
+ [
257
+ {product: 'Widget', quarter: 'Q3', price: 10.0, quantity: 200},
258
+ {product: 'Gadget', quarter: 'Q3', price: 25.0, quantity: 80}
259
+ ]
260
+ end
261
+
262
+ let(:more_sales) do
263
+ Namo.new(more_data)
264
+ end
265
+
266
+ it "concatenates rows" do
267
+ result = sales + more_sales
268
+ _(result.to_a.count).must_equal 6
269
+ _(result.to_a).must_equal(sample_data + more_data)
270
+ end
271
+
272
+ it "preserves dimensions" do
273
+ result = sales + more_sales
274
+ _(result.dimensions).must_equal [:product, :quarter, :price, :quantity]
275
+ end
276
+
277
+ it "carries formulae through from self" do
278
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
279
+ result = sales + more_sales
280
+ _(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
281
+ end
282
+
283
+ it "merges formulae from other" do
284
+ more_sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
285
+ result = sales + more_sales
286
+ _(result.map{|row| row[:revenue]}).must_equal [1000.0, 1500.0, 1000.0, 1500.0, 2000.0, 2000.0]
287
+ end
288
+
289
+ it "prefers self's formulae on conflict" do
290
+ sales[:label] = proc{|r| "self: #{r[:product]}"}
291
+ more_sales[:label] = proc{|r| "other: #{r[:product]}"}
292
+ result = sales + more_sales
293
+ _(result.map{|row| row[:label]}).must_equal [
294
+ 'self: Widget', 'self: Widget', 'self: Gadget', 'self: Gadget',
295
+ 'self: Widget', 'self: Gadget'
296
+ ]
297
+ end
298
+
299
+ it "raises when dimensions differ" do
300
+ other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
301
+ _ { sales + other }.must_raise ArgumentError
302
+ end
303
+ end
304
+
305
+ describe "#-" do
306
+ let(:to_remove) do
307
+ Namo.new([
308
+ {product: 'Widget', quarter: 'Q1', price: 10.0, quantity: 100},
309
+ {product: 'Gadget', quarter: 'Q2', price: 25.0, quantity: 60}
310
+ ])
311
+ end
312
+
313
+ it "removes matching rows" do
314
+ result = sales - to_remove
315
+ _(result.to_a.count).must_equal 2
316
+ _(result.to_a).must_equal [
317
+ {product: 'Widget', quarter: 'Q2', price: 10.0, quantity: 150},
318
+ {product: 'Gadget', quarter: 'Q1', price: 25.0, quantity: 40}
319
+ ]
320
+ end
321
+
322
+ it "preserves non-matching rows" do
323
+ other = Namo.new([{product: 'Thingo', quarter: 'Q4', price: 99.0, quantity: 1}])
324
+ result = sales - other
325
+ _(result.to_a).must_equal sample_data
326
+ end
327
+
328
+ it "carries formulae through from self" do
329
+ sales[:revenue] = proc{|r| r[:price] * r[:quantity]}
330
+ result = sales - to_remove
331
+ _(result.map{|row| row[:revenue]}).must_equal [1500.0, 1000.0]
332
+ end
333
+
334
+ it "raises when dimensions differ" do
335
+ other = Namo.new([{product: 'Widget', quarter: 'Q1'}])
336
+ _ { sales - other }.must_raise ArgumentError
337
+ end
338
+ end
339
+
207
340
  describe "#to_a" do
208
341
  it "returns the data as an array of hashes" do
209
342
  _(sales.to_a).must_equal sample_data
@@ -0,0 +1,16 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest-spec-context'
3
+
4
+ require_relative '../lib/namo'
5
+
6
+ describe Symbol do
7
+ describe "#-@" do
8
+ it "returns a NegatedDimension" do
9
+ _(-:price).must_be_kind_of Namo::NegatedDimension
10
+ end
11
+
12
+ it "preserves the symbol name" do
13
+ _((-:price).name).must_equal :price
14
+ end
15
+ end
16
+ end
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.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -64,10 +64,16 @@ files:
64
64
  - LICENSE
65
65
  - README.md
66
66
  - Rakefile
67
+ - lib/Namo/NegatedDimension.rb
68
+ - lib/Namo/Row.rb
67
69
  - lib/Namo/VERSION.rb
70
+ - lib/Symbol.rb
68
71
  - lib/namo.rb
69
72
  - namo.gemspec
70
- - test/namo_test.rb
73
+ - test/Namo/NegatedDimension_test.rb
74
+ - test/Namo/Row_test.rb
75
+ - test/Namo_test.rb
76
+ - test/Symbol_test.rb
71
77
  homepage: https://github.com/thoran/namo
72
78
  licenses:
73
79
  - MIT