hashformer 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,5 @@
1
+ # Start code coverage monitoring
2
+ require 'simplecov'
3
+ require 'codeclimate-test-reporter'
4
+ SimpleCov.start
5
+ CodeClimate::TestReporter.start
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