hashformer 0.2.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.
@@ -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