hashformer 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/LICENSE +21 -0
- data/README.md +344 -0
- data/hashformer.gemspec +28 -0
- data/lib/hashformer/generate.rb +177 -0
- data/lib/hashformer/version.rb +3 -0
- data/lib/hashformer.rb +87 -0
- data/spec/lib/hashformer/generate_spec.rb +231 -0
- data/spec/lib/hashformer_spec.rb +411 -0
- data/spec/spec_helper.rb +5 -0
- metadata +96 -0
@@ -0,0 +1,411 @@
|
|
1
|
+
# Hashformer: A declarative data transformation DSL for Ruby -- test suite
|
2
|
+
# Created June 2014 by Mike Bourgeous, DeseretBook.com
|
3
|
+
# Copyright (C)2014 Deseret Book
|
4
|
+
# See LICENSE and README.md for details.
|
5
|
+
|
6
|
+
require 'spec_helper'
|
7
|
+
|
8
|
+
require 'hashformer'
|
9
|
+
|
10
|
+
RSpec.describe Hashformer do
|
11
|
+
describe '.validate' do
|
12
|
+
let(:in_schema) {
|
13
|
+
# ClassyHash schema - https://github.com/deseretbook/classy_hash
|
14
|
+
{
|
15
|
+
first: String,
|
16
|
+
last: String,
|
17
|
+
city: [:optional, String],
|
18
|
+
phone: String
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:out_schema) {
|
23
|
+
# ClassyHash schema - https://github.com/deseretbook/classy_hash
|
24
|
+
{
|
25
|
+
name: String,
|
26
|
+
location: String,
|
27
|
+
phone: Integer
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
it 'processes simple transformations' do
|
32
|
+
expect(Hashformer.transform({}, {})).to eq({})
|
33
|
+
expect(Hashformer.transform({a: 1}, {b: :a})).to eq({b: 1})
|
34
|
+
expect(Hashformer.transform({a: 1, b: 2}, {b: :a, a: :b})).to eq({a: 2, b: 1})
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'processes simple lambda transformations' do
|
38
|
+
expect(Hashformer.transform({a: 1}, {a: ->(x){-x[:a]}})).to eq({a: -1})
|
39
|
+
expect(Hashformer.transform({a: 'hello'}, {a: ->(x){x[:a].length}})).to eq({a: 5})
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'processes multi-value lambda transformations' do
|
43
|
+
expect(Hashformer.transform({a: 'hello', b: 'world'}, {c: ->(x){"#{x[:a].capitalize} #{x[:b]}"}})).to eq({c: 'Hello world'})
|
44
|
+
expect(Hashformer.transform({a: 1, b: 2, c: 3}, {sum: ->(x){x.values.reduce(&:+)}})).to eq({sum: 6})
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'processes lambda keys' do
|
48
|
+
keyindex = {
|
49
|
+
->(value, data){ "key#{data.keys.index(value)}".to_sym } => :x
|
50
|
+
}
|
51
|
+
|
52
|
+
expect(Hashformer.transform({x: 0}, keyindex)).to eq({key0: 0})
|
53
|
+
expect(Hashformer.transform({a: 2, b: 1, x: 0}, keyindex)).to eq({key2: 0})
|
54
|
+
|
55
|
+
keyvaluejoin = {
|
56
|
+
->(value, data){ data[:key] } => :value
|
57
|
+
}
|
58
|
+
|
59
|
+
expect(Hashformer.transform({key: :x, value: -3}, keyvaluejoin)).to eq({x: -3})
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'handles missing input keys' do
|
63
|
+
expect(Hashformer.transform({}, {a: :a})).to eq({a: nil})
|
64
|
+
expect(Hashformer.transform({a: 1}, {a: :a, b: :b, c: :c})).to eq({a: 1, b: nil, c: nil})
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'does not pass values not specified in transformation' do
|
68
|
+
expect(Hashformer.transform({a: 1}, {})).to eq({})
|
69
|
+
expect(Hashformer.transform({a: 1, b: 2, c: 3}, {x: :c})).to eq({x: 3})
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'handles strings as key names' do
|
73
|
+
expect(Hashformer.transform({}, {'a' => :a})).to eq({'a' => nil})
|
74
|
+
expect(Hashformer.transform({a: 1}, {'a' => :a})).to eq({'a' => 1})
|
75
|
+
expect(Hashformer.transform({'a' => 1, 'b' => 2}, {'b' => 'a', 'a' => 'b'})).to eq({'a' => 2, 'b' => 1})
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'input schema is given' do
|
79
|
+
let(:xform) {
|
80
|
+
{
|
81
|
+
__in_schema: in_schema
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
it 'accepts valid input hashes' do
|
86
|
+
expect {
|
87
|
+
Hashformer.transform({first: 'Hello', last: 'World', city: 'Here', phone: '1-2-3-4-5'}, xform)
|
88
|
+
}.not_to raise_error
|
89
|
+
end
|
90
|
+
|
91
|
+
context 'validate is true' do
|
92
|
+
it 'rejects invalid input hashes' do
|
93
|
+
expect {
|
94
|
+
Hashformer.transform({}, xform)
|
95
|
+
}.to raise_error(/present/)
|
96
|
+
|
97
|
+
expect {
|
98
|
+
Hashformer.transform({first: :last}, xform)
|
99
|
+
}.to raise_error(/first/)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'validate is false' do
|
104
|
+
it 'accepts invalid input hashes' do
|
105
|
+
expect {
|
106
|
+
Hashformer.transform({}, xform, false)
|
107
|
+
}.not_to raise_error
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'output schema is given' do
|
113
|
+
let(:xform) {
|
114
|
+
{
|
115
|
+
__out_schema: out_schema,
|
116
|
+
|
117
|
+
name: lambda {|data| "#{data[:first]} #{data[:last]}".strip },
|
118
|
+
location: :city,
|
119
|
+
phone: lambda {|data| data[:phone].gsub(/[^\d]/, '').to_i }
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
it 'accepts valid output hashes' do
|
124
|
+
expect {
|
125
|
+
Hashformer.transform({city: '', phone: ''}, xform)
|
126
|
+
}.not_to raise_error
|
127
|
+
end
|
128
|
+
|
129
|
+
context 'validate is true' do
|
130
|
+
it 'rejects invalid output hashes' do
|
131
|
+
expect {
|
132
|
+
Hashformer.transform({city: 17, phone: ''}, xform)
|
133
|
+
}.to raise_error(/location/)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'validate is false' do
|
138
|
+
it 'accepts invalid output hashes' do
|
139
|
+
expect {
|
140
|
+
Hashformer.transform({city: 17, phone: ''}, xform, false)
|
141
|
+
}.not_to raise_error
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context 'both input and output schema are given' do
|
147
|
+
let(:xform) {
|
148
|
+
{
|
149
|
+
__in_schema: in_schema,
|
150
|
+
__out_schema: out_schema,
|
151
|
+
|
152
|
+
name: lambda {|data| "#{data[:first]} #{data[:last]}".strip },
|
153
|
+
location: :city,
|
154
|
+
phone: lambda {|data| data[:phone].gsub(/[^\d]/, '').to_i }
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
it 'transforms valid data correctly' do
|
159
|
+
expect(Hashformer.transform(
|
160
|
+
{
|
161
|
+
first: 'Hash',
|
162
|
+
last: 'Transformed',
|
163
|
+
city: 'Here',
|
164
|
+
phone: '555-555-5555'
|
165
|
+
},
|
166
|
+
xform
|
167
|
+
)).to eq({name: 'Hash Transformed', location: 'Here', phone: 5555555555})
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'rejects invalid input' do
|
171
|
+
expect{
|
172
|
+
Hashformer.transform({}, xform)
|
173
|
+
}.to raise_error(/present/)
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'rejects invalid output' do
|
177
|
+
expect{
|
178
|
+
Hashformer.transform({first: 'Hello', last: 'There', phone: '555-555-5555'}, xform)
|
179
|
+
}.to raise_error(/output data failed/)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'README examples' do
|
184
|
+
context 'Basic renaming' do
|
185
|
+
it 'produces the expected output for string keys' do
|
186
|
+
data = {
|
187
|
+
'first_name' => 'Hash',
|
188
|
+
'last_name' => 'Former'
|
189
|
+
}
|
190
|
+
xform = {
|
191
|
+
first: 'first_name',
|
192
|
+
last: 'last_name'
|
193
|
+
}
|
194
|
+
|
195
|
+
expect(Hashformer.transform(data, xform)).to eq({first: 'Hash', last: 'Former'})
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'produces the expected output for integer keys' do
|
199
|
+
data = {
|
200
|
+
0 => 'Nothing',
|
201
|
+
1 => 'Only One'
|
202
|
+
}
|
203
|
+
xform = {
|
204
|
+
zero: 0,
|
205
|
+
one: 1
|
206
|
+
}
|
207
|
+
|
208
|
+
expect(Hashformer.transform(data, xform)).to eq({zero: 'Nothing', one: 'Only One'})
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'Nested values' do
|
213
|
+
it 'produces the expected output for a present path' do
|
214
|
+
data = {
|
215
|
+
name: 'Hashformer',
|
216
|
+
addresses: [
|
217
|
+
{
|
218
|
+
line1: 'Hash',
|
219
|
+
line2: 'Former'
|
220
|
+
}
|
221
|
+
]
|
222
|
+
}
|
223
|
+
xform = {
|
224
|
+
name: :name,
|
225
|
+
line1: HF::G.path[:addresses][0][:line1],
|
226
|
+
line2: HF::G.path[:addresses][0][:line2]
|
227
|
+
}
|
228
|
+
|
229
|
+
expect(Hashformer.transform(data, xform)).to eq({name: 'Hashformer', line1: 'Hash', line2: 'Former'})
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'produces the expected output for a missing path' do
|
233
|
+
data = {
|
234
|
+
a: { b: 'c' }
|
235
|
+
}
|
236
|
+
xform = {
|
237
|
+
a: HF::G.path[:a][0][:c]
|
238
|
+
}
|
239
|
+
|
240
|
+
expect(Hashformer.transform(data, xform)).to eq({a: nil})
|
241
|
+
end
|
242
|
+
|
243
|
+
it 'returns the entire hash if no path is given' do
|
244
|
+
data = {
|
245
|
+
a: 1,
|
246
|
+
b: 2
|
247
|
+
}
|
248
|
+
xform = {
|
249
|
+
h: HF::G.path
|
250
|
+
}
|
251
|
+
|
252
|
+
expect(Hashformer.transform(data, xform)).to eq({h: {a: 1, b: 2}})
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
context 'Method chaining' do
|
257
|
+
it 'produces the expected output for simple method chaining' do
|
258
|
+
data = {
|
259
|
+
s: 'Hashformer',
|
260
|
+
v: [1, 2, 3, 4, 5]
|
261
|
+
}
|
262
|
+
xform = {
|
263
|
+
s: HF[:s].reverse.capitalize,
|
264
|
+
# It's important to call clone before calling methods that modify the array
|
265
|
+
v: HF[:v].clone.concat([6]).map{|x| x * x}.reduce(0, &:+)
|
266
|
+
}
|
267
|
+
|
268
|
+
expect(Hashformer.transform(data, xform)).to eq({s: 'Remrofhsah', v: 91})
|
269
|
+
end
|
270
|
+
|
271
|
+
it 'raises an exception if accessing beyond a missing path' do
|
272
|
+
data = {
|
273
|
+
a: [1, 2, 3]
|
274
|
+
}
|
275
|
+
xform = {
|
276
|
+
a: HF[:b][0]
|
277
|
+
}
|
278
|
+
|
279
|
+
expect{Hashformer.transform(data, xform)}.to raise_error(/\[\]/)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'returns the input hash with no methods added' do
|
283
|
+
data = {
|
284
|
+
a: 1
|
285
|
+
}
|
286
|
+
xform = {
|
287
|
+
a: HF[].count,
|
288
|
+
b: HF::G.chain
|
289
|
+
}
|
290
|
+
|
291
|
+
expect(Hashformer.transform(data, xform)).to eq({a: 1, b: {a: 1}})
|
292
|
+
end
|
293
|
+
|
294
|
+
it 'produces the expected output for chained operators' do
|
295
|
+
xform = {
|
296
|
+
x: -(HF[:x] * 2) + 5
|
297
|
+
}
|
298
|
+
|
299
|
+
expect(Hashformer.transform({x: 3}, xform)).to eq({x: -1})
|
300
|
+
expect(Hashformer.transform({x: -12}, xform)).to eq({x: 29})
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
context 'Mapping one or more values' do
|
305
|
+
it 'produces the expected output for a single map parameter' do
|
306
|
+
data = {
|
307
|
+
a: 'Hashformer'
|
308
|
+
}
|
309
|
+
xform = {
|
310
|
+
a: HF::G.map(:a, &:upcase),
|
311
|
+
b: HF::G.map(:a)
|
312
|
+
}
|
313
|
+
|
314
|
+
expect(Hashformer.transform(data, xform)).to eq({a: 'HASHFORMER', b: ['Hashformer']})
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'produces the expected output for a map/reduce' do
|
318
|
+
data = {
|
319
|
+
items: [
|
320
|
+
{name: 'Item 1', price: 1.50},
|
321
|
+
{name: 'Item 2', price: 2.50},
|
322
|
+
{name: 'Item 3', price: 3.50},
|
323
|
+
{name: 'Item 4', price: 4.50},
|
324
|
+
],
|
325
|
+
shipping: 5.50
|
326
|
+
}
|
327
|
+
xform = {
|
328
|
+
item_total: HF[:items].map{|i| i[:price]}.reduce(0.0, &:+),
|
329
|
+
total: HF::G.map(HF[:items].map{|i| i[:price]}.reduce(0.0, &:+), HF::G.path[:shipping], &:+)
|
330
|
+
}
|
331
|
+
|
332
|
+
expect(Hashformer.transform(data, xform)).to eq({item_total: 12.0, total: 17.5})
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
context 'Lambda processing' do
|
337
|
+
it 'produces the expected output for the Pythagorean distance equation' do
|
338
|
+
data = {
|
339
|
+
x: 3.0,
|
340
|
+
y: 4.0
|
341
|
+
}
|
342
|
+
xform = {
|
343
|
+
radius: ->(h){ Math.sqrt(h[:x] * h[:x] + h[:y] * h[:y]) }
|
344
|
+
}
|
345
|
+
|
346
|
+
expect(Hashformer.transform(data, xform)).to eq({radius: 5.0})
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context 'Dynamic key names' do
|
351
|
+
it 'produces the expected output for a dynamic key' do
|
352
|
+
data = {
|
353
|
+
key: :x,
|
354
|
+
value: 0
|
355
|
+
}
|
356
|
+
xform = {
|
357
|
+
->(value, h){h[:key]} => :value
|
358
|
+
}
|
359
|
+
|
360
|
+
expect(Hashformer.transform(data, xform)).to eq({x: 0})
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
context 'Practical example with validation' do
|
365
|
+
it 'produces the expected output for a practical example' do
|
366
|
+
# Classy Hash schema - https://github.com/deseretbook/classy_hash
|
367
|
+
in_schema = {
|
368
|
+
# Totally violates http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
|
369
|
+
first: String,
|
370
|
+
last: String,
|
371
|
+
city: String,
|
372
|
+
phone: String,
|
373
|
+
}
|
374
|
+
|
375
|
+
out_schema = {
|
376
|
+
name: String,
|
377
|
+
location: String,
|
378
|
+
phone: Integer, # Just for example; probably shouldn't make phone numbers integers
|
379
|
+
}
|
380
|
+
|
381
|
+
# Hashformer transformation - https://github.com/deseretbook/hashformer
|
382
|
+
xform = {
|
383
|
+
# Validate input and output data according to the Classy Hash schemas
|
384
|
+
__in_schema: in_schema,
|
385
|
+
__out_schema: out_schema,
|
386
|
+
|
387
|
+
# Combine first and last name into a single String
|
388
|
+
name: HF::G.map(:first, :last) {|f, l| "#{f} #{l}".strip},
|
389
|
+
|
390
|
+
# Copy the :city field directly into :location
|
391
|
+
location: :city,
|
392
|
+
|
393
|
+
# Remove non-digits from :phone
|
394
|
+
phone: HF[:phone].gsub(/[^\d]/, '').to_i
|
395
|
+
}
|
396
|
+
|
397
|
+
data = {
|
398
|
+
first: 'Hash',
|
399
|
+
last: 'Transformed',
|
400
|
+
city: 'Here',
|
401
|
+
phone: '555-555-5555',
|
402
|
+
}
|
403
|
+
|
404
|
+
expected = {name: 'Hash Transformed', location: 'Here', phone: 5555555555}
|
405
|
+
|
406
|
+
expect(Hashformer.transform(data, xform)).to eq(expected)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hashformer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Deseret Book
|
8
|
+
- Mike Bourgeous
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.5'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.5'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: classy_hash
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0.1'
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.1.1
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - "~>"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0.1'
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.1.1
|
48
|
+
description: |2
|
49
|
+
Hashformer provides a simple, Ruby Hash-based way of transforming data from
|
50
|
+
one format to another. It's vaguely like XSLT, but way less complicated
|
51
|
+
and way more Ruby.
|
52
|
+
email:
|
53
|
+
- mike@mikebourgeous.com
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- ".gitignore"
|
59
|
+
- ".rspec"
|
60
|
+
- Gemfile
|
61
|
+
- LICENSE
|
62
|
+
- README.md
|
63
|
+
- hashformer.gemspec
|
64
|
+
- lib/hashformer.rb
|
65
|
+
- lib/hashformer/generate.rb
|
66
|
+
- lib/hashformer/version.rb
|
67
|
+
- spec/lib/hashformer/generate_spec.rb
|
68
|
+
- spec/lib/hashformer_spec.rb
|
69
|
+
- spec/spec_helper.rb
|
70
|
+
homepage: https://github.com/deseretbook/hashformer
|
71
|
+
licenses: []
|
72
|
+
metadata: {}
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
require_paths:
|
76
|
+
- lib
|
77
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 1.9.3
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 2.2.2
|
90
|
+
signing_key:
|
91
|
+
specification_version: 4
|
92
|
+
summary: Transform any Hash with a declarative data transformation DSL for Ruby
|
93
|
+
test_files:
|
94
|
+
- spec/lib/hashformer/generate_spec.rb
|
95
|
+
- spec/lib/hashformer_spec.rb
|
96
|
+
- spec/spec_helper.rb
|