namo.rb 0.31.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +566 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +1268 -0
- data/Rakefile +42 -0
- data/lib/Namo/Collection.rb +130 -0
- data/lib/Namo/Enumerable.rb +83 -0
- data/lib/Namo/Formulae.rb +137 -0
- data/lib/Namo/Formulary.rb +6 -0
- data/lib/Namo/NegatedDimension.rb +14 -0
- data/lib/Namo/Row.rb +93 -0
- data/lib/Namo/VERSION.rb +6 -0
- data/lib/Symbol.rb +8 -0
- data/lib/namo.rb +433 -0
- data/namo.rb.gemspec +42 -0
- data/test/Namo/Collection_test.rb +548 -0
- data/test/Namo/Formulae_test.rb +458 -0
- data/test/Namo/NegatedDimension_test.rb +13 -0
- data/test/Namo/Row_test.rb +487 -0
- data/test/Symbol_test.rb +16 -0
- data/test/console_test.rb +44 -0
- data/test/demo_test.rb +86 -0
- data/test/namo_test.rb +3328 -0
- data/test/sizing_test.rb +40 -0
- metadata +107 -0
data/lib/namo.rb
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
# namo.rb
|
|
2
|
+
# Namo
|
|
3
|
+
|
|
4
|
+
require_relative './Namo/NegatedDimension'
|
|
5
|
+
require_relative './Namo/Formulary'
|
|
6
|
+
require_relative './Namo/Formulae'
|
|
7
|
+
require_relative './Namo/Row'
|
|
8
|
+
require_relative './Namo/Collection'
|
|
9
|
+
require_relative './Namo/Enumerable'
|
|
10
|
+
require_relative './Namo/VERSION'
|
|
11
|
+
require_relative './Symbol'
|
|
12
|
+
|
|
13
|
+
class Namo
|
|
14
|
+
include Namo::Enumerable
|
|
15
|
+
|
|
16
|
+
attr_accessor :data
|
|
17
|
+
attr_accessor :formulae
|
|
18
|
+
attr_accessor :name
|
|
19
|
+
|
|
20
|
+
INSPECTED_ROWS = 10
|
|
21
|
+
|
|
22
|
+
def dimensions
|
|
23
|
+
data_dimensions + derived_dimensions
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def data_dimensions
|
|
27
|
+
@data.first&.keys || []
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def derived_dimensions
|
|
31
|
+
@formulae.keys
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def values(*dims)
|
|
35
|
+
materialising do
|
|
36
|
+
if dims.empty?
|
|
37
|
+
materialisable_dimensions.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
|
|
38
|
+
elsif dims.length == 1
|
|
39
|
+
values_for(dims.first)
|
|
40
|
+
else
|
|
41
|
+
dims.each_with_object({}){|dim, hash| hash[dim] = values_for(dim)}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def coordinates(*dims)
|
|
47
|
+
if dims.empty?
|
|
48
|
+
values.transform_values(&:uniq)
|
|
49
|
+
elsif dims.length == 1
|
|
50
|
+
values(dims.first).uniq
|
|
51
|
+
else
|
|
52
|
+
dims.each_with_object({}){|dim, hash| hash[dim] = values(dim).uniq}
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
values
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def [](*names, **selections)
|
|
61
|
+
rows = selections.any? ? select{|row| row.match?(selections)} : entries
|
|
62
|
+
negated, positive = names.partition{|n| n.is_a?(NegatedDimension)}
|
|
63
|
+
if negated.any? && positive.any?
|
|
64
|
+
raise ArgumentError, "cannot mix projection and contraction in a single call"
|
|
65
|
+
end
|
|
66
|
+
projected = (
|
|
67
|
+
if negated.any?
|
|
68
|
+
excluded = negated.map(&:name)
|
|
69
|
+
kept = data_dimensions - excluded
|
|
70
|
+
rows.map do |row|
|
|
71
|
+
kept.each_with_object({}){|name, hash| hash[name] = row[name]}
|
|
72
|
+
end
|
|
73
|
+
elsif positive.any?
|
|
74
|
+
rows.map do |row|
|
|
75
|
+
positive.each_with_object({}){|name, hash| hash[name] = row[name]}
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
rows.map(&:to_h)
|
|
79
|
+
end
|
|
80
|
+
)
|
|
81
|
+
carried = positive.any? ? @formulae.reject{|name, _| positive.include?(name)} : @formulae.dup
|
|
82
|
+
return_class.new(projected, formulae: carried)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def []=(name, value)
|
|
86
|
+
if value.respond_to?(:call)
|
|
87
|
+
@data.each{|row| row.delete(name)} if @data.first&.key?(name)
|
|
88
|
+
@formulae[name] = value
|
|
89
|
+
else
|
|
90
|
+
@formulae.delete(name)
|
|
91
|
+
@data.each{|row| row[name] = value}
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def attach(modul)
|
|
96
|
+
collisions = attach_collisions(modul)
|
|
97
|
+
unless collisions.empty?
|
|
98
|
+
raise ArgumentError, "formulary methods collide with data dimensions: #{collisions.inspect}"
|
|
99
|
+
end
|
|
100
|
+
@formulae.attach(modul)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def attach!(modul)
|
|
105
|
+
attach_collisions(modul).each{|name| @data.each{|row| row.delete(name)}}
|
|
106
|
+
@formulae.attach(modul)
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def detach(constituent)
|
|
111
|
+
@formulae.detach(constituent)
|
|
112
|
+
self
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def <<(constituent)
|
|
116
|
+
case constituent
|
|
117
|
+
when Module then attach(constituent)
|
|
118
|
+
when Row then add_row(constituent.to_h)
|
|
119
|
+
when Hash then add_row(constituent)
|
|
120
|
+
else raise TypeError, "can't append #{constituent.class} to a Namo; expected a Module (formulary), a Hash, or a Row"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def +(other)
|
|
125
|
+
raise_unless_namo(other)
|
|
126
|
+
raise_unless_matching_data_dimensions(other)
|
|
127
|
+
return_class.new(@data + other.data, formulae: other.formulae.merge(@formulae))
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def -(other)
|
|
131
|
+
raise_unless_namo(other)
|
|
132
|
+
raise_unless_matching_data_dimensions(other)
|
|
133
|
+
return_class.new(@data - other.data, formulae: @formulae.dup)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def &(other)
|
|
137
|
+
raise_unless_namo(other)
|
|
138
|
+
raise_unless_matching_data_dimensions(other)
|
|
139
|
+
return_class.new(@data & other.data, formulae: @formulae.dup)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def |(other)
|
|
143
|
+
raise_unless_namo(other)
|
|
144
|
+
raise_unless_matching_data_dimensions(other)
|
|
145
|
+
return_class.new((@data | other.data), formulae: other.formulae.merge(@formulae))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ^(other)
|
|
149
|
+
raise_unless_namo(other)
|
|
150
|
+
raise_unless_matching_data_dimensions(other)
|
|
151
|
+
return_class.new((@data - other.data) + (other.data - @data), formulae: other.formulae.merge(@formulae))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def *(other, &block)
|
|
155
|
+
raise_unless_namo(other)
|
|
156
|
+
raise_unless_shared_data_dimensions(other)
|
|
157
|
+
raise_unless_data_formula_exclusivity(other)
|
|
158
|
+
shared = data_dimensions & other.data_dimensions
|
|
159
|
+
combined_data = []
|
|
160
|
+
@data.each do |left_row|
|
|
161
|
+
matched = other.data.select{|right_row| shared.all?{|dim| left_row[dim] == right_row[dim]}}
|
|
162
|
+
if block
|
|
163
|
+
candidates = other.class.new(matched, formulae: other.formulae.dup)
|
|
164
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
165
|
+
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
166
|
+
else
|
|
167
|
+
matched.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
return_class.new(combined_data, formulae: other.formulae.merge(@formulae))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def **(other, &block)
|
|
174
|
+
raise_unless_namo(other)
|
|
175
|
+
raise_unless_disjoint_data_dimensions(other)
|
|
176
|
+
raise_unless_data_formula_exclusivity(other)
|
|
177
|
+
combined_data = []
|
|
178
|
+
@data.each do |left_row|
|
|
179
|
+
if block
|
|
180
|
+
candidates = other.class.new(other.data, formulae: other.formulae.dup)
|
|
181
|
+
chosen = block.call(Row.new(left_row, @formulae, self), candidates)
|
|
182
|
+
chosen.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
183
|
+
else
|
|
184
|
+
other.data.each{|right_row| combined_data << left_row.merge(right_row)}
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
return_class.new(combined_data, formulae: other.formulae.merge(@formulae))
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def /(other)
|
|
191
|
+
raise_unless_namo(other)
|
|
192
|
+
kept = data_dimensions - other.data_dimensions
|
|
193
|
+
projected = @data.map do |row|
|
|
194
|
+
kept.each_with_object({}){|dim, hash| hash[dim] = row[dim]}
|
|
195
|
+
end
|
|
196
|
+
return_class.new(projected.uniq, formulae: @formulae.dup)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def ==(other)
|
|
200
|
+
return false unless other.is_a?(Namo)
|
|
201
|
+
row_multiset == other.row_multiset
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def ===(other)
|
|
205
|
+
return false unless other.is_a?(Namo)
|
|
206
|
+
dimensions.sort == other.dimensions.sort &&
|
|
207
|
+
@formulae.keys.sort == other.formulae.keys.sort
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def eql?(other)
|
|
211
|
+
self.class == other.class &&
|
|
212
|
+
row_multiset == other.row_multiset &&
|
|
213
|
+
@formulae.keys.sort == other.formulae.keys.sort
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def hash
|
|
217
|
+
[self.class, row_multiset, @formulae.keys.sort].hash
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def <(other)
|
|
221
|
+
raise_unless_namo(other)
|
|
222
|
+
raise_unless_matching_data_dimensions(other)
|
|
223
|
+
proper_subset_of_rows?(other)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def <=(other)
|
|
227
|
+
raise_unless_namo(other)
|
|
228
|
+
raise_unless_matching_data_dimensions(other)
|
|
229
|
+
subset_of_rows?(other)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def >(other)
|
|
233
|
+
raise_unless_namo(other)
|
|
234
|
+
raise_unless_matching_data_dimensions(other)
|
|
235
|
+
other.proper_subset_of_rows?(self)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def >=(other)
|
|
239
|
+
raise_unless_namo(other)
|
|
240
|
+
raise_unless_matching_data_dimensions(other)
|
|
241
|
+
other.subset_of_rows?(self)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Each row's own keys, in their own order, and then the derived dimensions which
|
|
245
|
+
# can be materialised without arguments — the rule values, coordinates and to_h
|
|
246
|
+
# already follow. data is the stored rows, and stays so.
|
|
247
|
+
def to_a
|
|
248
|
+
materialising do
|
|
249
|
+
derived = materialisable_dimensions - data_dimensions
|
|
250
|
+
@data.map do |row|
|
|
251
|
+
subject = Row.new(row, @formulae, self)
|
|
252
|
+
stored = row.keys.each_with_object({}){|key, hash| hash[key] = row[key]}
|
|
253
|
+
derived.each_with_object(stored){|dimension, hash| hash[dimension] = subject[dimension]}
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def inspect
|
|
259
|
+
rendered = @data.first(INSPECTED_ROWS).map{|row| [row, inspected_derivations(row)]}
|
|
260
|
+
"#<#{self.class}#{inspected_name}#{inspected_rows(rendered)}#{inspected_derived(rendered)}>"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
protected
|
|
264
|
+
|
|
265
|
+
# The class an operation returns. A subclass is its own kind of thing, so a
|
|
266
|
+
# projection of a PriceData is a PriceData; Namo::Collection is not, its kind
|
|
267
|
+
# being its members, which a row operation carries none of.
|
|
268
|
+
def return_class
|
|
269
|
+
self.class
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# The rendered rows, the count of those not shown, and the derived dimensions
|
|
273
|
+
# none of them carries. Shared with Collection, which renders its members' rows
|
|
274
|
+
# rather than its own and must report what a member would report of itself.
|
|
275
|
+
def inspected_data(limit)
|
|
276
|
+
rendered = @data.first(limit).map{|row| [row, inspected_derivations(row)]}
|
|
277
|
+
[rendered.map{|row, derived| row.merge(derived).inspect},
|
|
278
|
+
@data.length - limit,
|
|
279
|
+
unshown_derived(rendered)]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def row_multiset
|
|
283
|
+
@data.tally
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def subset_of_rows?(other)
|
|
287
|
+
self_counts = row_multiset
|
|
288
|
+
other_counts = other.row_multiset
|
|
289
|
+
self_counts.all?{|row, count| (other_counts[row] || 0) >= count}
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def proper_subset_of_rows?(other)
|
|
293
|
+
subset_of_rows?(other) && self != other
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
private
|
|
297
|
+
|
|
298
|
+
def initialize(positional_data = nil, data: [], formulae: {}, name: nil)
|
|
299
|
+
@data = positional_data || data
|
|
300
|
+
@formulae = formulae.is_a?(Formulae) ? formulae : Formulae.new(formulae)
|
|
301
|
+
@name = name
|
|
302
|
+
attach_included_formularies
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def inspected_name
|
|
306
|
+
@name.nil? ? '' : " #{@name.inspect}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# The rows as they are stored, never the derived values: inspect is called
|
|
310
|
+
# for every result in a console, and evaluating a formula there would cost a
|
|
311
|
+
# pass over the data per access and raise whatever the formula raises.
|
|
312
|
+
def inspected_rows(rendered)
|
|
313
|
+
return ' []' if @data.empty?
|
|
314
|
+
shown = rendered.map{|row, derived| " #{row.merge(derived).inspect}"}.join(",\n")
|
|
315
|
+
shown += "\n ... #{@data.length - INSPECTED_ROWS} more rows" if @data.length > INSPECTED_ROWS
|
|
316
|
+
" [\n#{shown}\n]"
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# A derived dimension is shown with its value, in among the stored ones. A
|
|
320
|
+
# formula wanting arguments has no value to show without them, and one which
|
|
321
|
+
# raises has none to show either; the derived list still names both.
|
|
322
|
+
def inspected_derivations(row)
|
|
323
|
+
subject = Row.new(row, @formulae, self)
|
|
324
|
+
derived_dimensions.each_with_object({}) do |dimension, derived|
|
|
325
|
+
next if @formulae.required_parameter_count(dimension) > 2
|
|
326
|
+
derived[dimension] = subject[dimension]
|
|
327
|
+
rescue StandardError
|
|
328
|
+
next
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Only those the rows above do not already carry. A formula which raises, and a
|
|
333
|
+
# parameterised one which cannot be materialised without its arguments, render
|
|
334
|
+
# no value and would otherwise be nowhere in the output at all. A Collection
|
|
335
|
+
# renders no rows of its own and so passes none, naming every one of them.
|
|
336
|
+
def inspected_derived(rendered = [])
|
|
337
|
+
inspected_derived_names(unshown_derived(rendered))
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def inspected_derived_names(unshown)
|
|
341
|
+
unshown.empty? ? '' : " derived: #{unshown.inspect}"
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def unshown_derived(rendered)
|
|
345
|
+
derived_dimensions.reject do |dimension|
|
|
346
|
+
rendered.any? && rendered.all?{|_, derived| derived.key?(dimension)}
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def add_row(row)
|
|
351
|
+
collisions = row.keys & derived_dimensions
|
|
352
|
+
unless collisions.empty?
|
|
353
|
+
raise ArgumentError, "row keys collide with formulae: #{collisions.inspect}"
|
|
354
|
+
end
|
|
355
|
+
@data << row
|
|
356
|
+
self
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def attach_collisions(modul)
|
|
360
|
+
modul.public_instance_methods(false) & data_dimensions
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def attach_included_formularies
|
|
364
|
+
self.class.ancestors.reverse.each do |modul|
|
|
365
|
+
next if modul.is_a?(Class) || !modul.include?(Namo::Formulary)
|
|
366
|
+
attach(modul)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def materialising
|
|
371
|
+
return yield if @materialised
|
|
372
|
+
@materialised = {}
|
|
373
|
+
begin
|
|
374
|
+
yield
|
|
375
|
+
ensure
|
|
376
|
+
@materialised = nil
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def values_for(dim)
|
|
381
|
+
@materialised[dim] ||= (
|
|
382
|
+
if data_dimensions.include?(dim)
|
|
383
|
+
@data.map{|row_data| row_data[dim]}
|
|
384
|
+
else
|
|
385
|
+
@data.map{|row_data| Row.new(row_data, @formulae, self)[dim]}
|
|
386
|
+
end
|
|
387
|
+
)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def materialisable_dimensions
|
|
391
|
+
dimensions.reject{|dim| requires_arguments?(dim)}
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def requires_arguments?(name)
|
|
395
|
+
formula = @formulae[name]
|
|
396
|
+
!!formula && required_parameter_count(formula) > 2
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def required_parameter_count(formula)
|
|
400
|
+
formula.arity >= 0 ? formula.arity : -formula.arity - 1
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def raise_unless_namo(other)
|
|
404
|
+
unless other.is_a?(Namo)
|
|
405
|
+
raise TypeError, "can't compare Namo with #{other.class}"
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def raise_unless_matching_data_dimensions(other)
|
|
410
|
+
unless data_dimensions == other.data_dimensions
|
|
411
|
+
raise ArgumentError, "dimensions don't match: #{data_dimensions} vs #{other.data_dimensions}"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def raise_unless_shared_data_dimensions(other)
|
|
416
|
+
if (data_dimensions & other.data_dimensions).empty?
|
|
417
|
+
raise ArgumentError, "no shared dimensions, need to have shared dimensions: #{data_dimensions} vs #{other.data_dimensions}"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def raise_unless_disjoint_data_dimensions(other)
|
|
422
|
+
if (data_dimensions & other.data_dimensions).any?
|
|
423
|
+
raise ArgumentError, "dimensions in common, need no common dimensions: #{data_dimensions} vs #{other.data_dimensions}"
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def raise_unless_data_formula_exclusivity(other)
|
|
428
|
+
collisions = (data_dimensions & other.derived_dimensions) | (derived_dimensions & other.data_dimensions)
|
|
429
|
+
if collisions.any?
|
|
430
|
+
raise ArgumentError, "name collision between data and formulae: #{collisions.inspect}"
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
end
|
data/namo.rb.gemspec
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# namo.rb.gemspec
|
|
2
|
+
|
|
3
|
+
require_relative './lib/Namo/VERSION'
|
|
4
|
+
|
|
5
|
+
class Gem::Specification
|
|
6
|
+
def development_dependencies=(gems)
|
|
7
|
+
gems.each{|gem| add_development_dependency(*gem)}
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
Gem::Specification.new do |spec|
|
|
12
|
+
spec.name = 'namo.rb'
|
|
13
|
+
spec.version = Namo::VERSION
|
|
14
|
+
|
|
15
|
+
spec.summary = "Named dimensional data for Ruby."
|
|
16
|
+
spec.description = "A Ruby library for working with multi-dimensional data using named dimensions. Initialise from an array of hashes making it trivial to use with databases, CSV, JSON, and YAML. Dimensions and coordinates are inferred automatically."
|
|
17
|
+
|
|
18
|
+
spec.author = 'thoran'
|
|
19
|
+
spec.email = 'code@thoran.com'
|
|
20
|
+
spec.homepage = 'https://github.com/thoran/namo'
|
|
21
|
+
spec.license = 'MIT'
|
|
22
|
+
|
|
23
|
+
spec.required_ruby_version = '>= 2.7'
|
|
24
|
+
spec.require_paths = ['lib']
|
|
25
|
+
|
|
26
|
+
spec.files = [
|
|
27
|
+
Dir['lib/**/*.rb'],
|
|
28
|
+
Dir['test/**/*.rb'],
|
|
29
|
+
'CHANGELOG',
|
|
30
|
+
'Gemfile',
|
|
31
|
+
'LICENSE',
|
|
32
|
+
'namo.rb.gemspec',
|
|
33
|
+
'Rakefile',
|
|
34
|
+
'README.md',
|
|
35
|
+
].flatten
|
|
36
|
+
|
|
37
|
+
spec.development_dependencies = %w{
|
|
38
|
+
minitest
|
|
39
|
+
minitest-spec-context
|
|
40
|
+
rake
|
|
41
|
+
}
|
|
42
|
+
end
|