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
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b7f71daa79a2af9d68aaa378dca0748ab4e19157
|
4
|
+
data.tar.gz: 7f17e9b9260f1ad6cbb362a7eebf435995fb2449
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 265374fe6b61f42539c24e3007ce6da1b36da296c92e6c5889325a58da6f4ac4a24f1361586cf42df6afc871be96bd0f740692b65b0d76b4c830e05b038343a0
|
7
|
+
data.tar.gz: daa336e0a096baebb1bab6535adb24723ac56a9c8f64ddc4513975a3920fcf4b671a91d7dabf5ebfa3ef37cfe73a877c0cd4b90e57513b366f582768b1974fca
|
data/.gitignore
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/lib/bundler/man/
|
26
|
+
|
27
|
+
# for a library or gem, you might want to ignore these files since the code is
|
28
|
+
# intended to run in multiple environments; otherwise, check them in:
|
29
|
+
Gemfile.lock
|
30
|
+
.ruby-version
|
31
|
+
.ruby-gemset
|
32
|
+
|
33
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
34
|
+
.rvmrc
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
group :test do
|
4
|
+
# RSpec for tests
|
5
|
+
gem 'rspec'
|
6
|
+
|
7
|
+
# SimpleCov for test coverage
|
8
|
+
gem 'simplecov', '~> 0.7.1', require: false
|
9
|
+
|
10
|
+
# Code Climate test coverage
|
11
|
+
gem "codeclimate-test-reporter", require: nil
|
12
|
+
end
|
13
|
+
|
14
|
+
group :development do
|
15
|
+
gem 'debugger', platforms: [:ruby_19]
|
16
|
+
gem 'byebug', platforms: [:ruby_20, :ruby_21]
|
17
|
+
end
|
18
|
+
|
19
|
+
gemspec
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Deseret Book and Contributors
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,344 @@
|
|
1
|
+
Hashformer [![Code Climate](https://codeclimate.com/github/deseretbook/hashformer.png)](https://codeclimate.com/github/deseretbook/hashformer) [![Test Coverage](https://codeclimate.com/github/deseretbook/hashformer/coverage.png)](https://codeclimate.com/github/deseretbook/hashformer) [![Codeship Status for deseretbook/hashformer](https://www.codeship.io/projects/dd988da0-dee7-0131-9e92-7e1ff0bec112/status?branch=master)](https://www.codeship.io/projects/24888)
|
2
|
+
=========
|
3
|
+
|
4
|
+
### Transform any Ruby Hash with a declarative DSL
|
5
|
+
|
6
|
+
Hashformer is the ultimate Ruby Hash transformation tool, made from 100% pure
|
7
|
+
Hashformium (may contain trace amounts of caffeine). It provides a simple,
|
8
|
+
Ruby Hash-based DSL for transforming data from one format to another. It's
|
9
|
+
vaguely like XSLT, but way less complicated and way more Ruby.
|
10
|
+
|
11
|
+
You specify Hash to Hash transformations using a Hash with a list of output
|
12
|
+
keys, input keys, and transformations, and Hashformer will convert your data
|
13
|
+
into the format you specify. It can also help verify your transformations by
|
14
|
+
validating input and output data using [Classy Hash](https://github.com/deseretbook/classy_hash).
|
15
|
+
|
16
|
+
|
17
|
+
### Examples
|
18
|
+
|
19
|
+
#### Basic renaming
|
20
|
+
|
21
|
+
If you just need to move/copy/rename keys, you specify the source key as the
|
22
|
+
value for the destination key in your transformation:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
data = {
|
26
|
+
'first_name' => 'Hash',
|
27
|
+
'last_name' => 'Former'
|
28
|
+
}
|
29
|
+
xform = {
|
30
|
+
first: 'first_name',
|
31
|
+
last: 'last_name'
|
32
|
+
}
|
33
|
+
|
34
|
+
Hashformer.transform(data, xform)
|
35
|
+
# => {first: 'Hash', last: 'Former'}
|
36
|
+
```
|
37
|
+
|
38
|
+
Just about any source key type will work:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
data = {
|
42
|
+
0 => 'Nothing',
|
43
|
+
1 => 'Only One'
|
44
|
+
}
|
45
|
+
xform = {
|
46
|
+
zero: 0,
|
47
|
+
one: 1
|
48
|
+
}
|
49
|
+
|
50
|
+
Hashformer.transform(data, xform)
|
51
|
+
# => {zero: 'Nothing', one: 'Only One'}
|
52
|
+
```
|
53
|
+
|
54
|
+
|
55
|
+
#### Nested values
|
56
|
+
|
57
|
+
If you need to grab values from a Hash or Array within a Hash, you can use
|
58
|
+
`Hashformer::Generate.path` (or, the convenient shortcut, `HF::G.path`):
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
data = {
|
62
|
+
name: 'Hashformer',
|
63
|
+
addresses: [
|
64
|
+
{
|
65
|
+
line1: 'Hash',
|
66
|
+
line2: 'Former'
|
67
|
+
}
|
68
|
+
]
|
69
|
+
}
|
70
|
+
xform = {
|
71
|
+
name: :name,
|
72
|
+
line1: HF::G.path[:addresses][0][:line1],
|
73
|
+
line2: HF::G.path[:addresses][0][:line2]
|
74
|
+
}
|
75
|
+
|
76
|
+
Hashformer.transform(data, xform)
|
77
|
+
# => {name: 'Hashformer', line1: 'Hash', line2: 'Former'}
|
78
|
+
```
|
79
|
+
|
80
|
+
If you try to access beyond a path that doesn't exist, nil will be returned
|
81
|
+
instead:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
data = {
|
85
|
+
a: { b: 'c' }
|
86
|
+
}
|
87
|
+
xform = {
|
88
|
+
a: HF::G.path[:a][0][:c]
|
89
|
+
}
|
90
|
+
|
91
|
+
Hashformer.transform(data, xform)
|
92
|
+
# => {a: nil}
|
93
|
+
```
|
94
|
+
|
95
|
+
If no path is specified, the entire Hash will be returned:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
data = {
|
99
|
+
a: 1,
|
100
|
+
b: 2
|
101
|
+
}
|
102
|
+
xform = {
|
103
|
+
h: HF::G.path
|
104
|
+
}
|
105
|
+
|
106
|
+
Hashformer.transform(data, xform)
|
107
|
+
# => {h: {a: 1, b: 2}}
|
108
|
+
```
|
109
|
+
|
110
|
+
|
111
|
+
#### Method chaining
|
112
|
+
|
113
|
+
This is the most useful and powerful aspect of Hashformer. You can use
|
114
|
+
`HF::G.chain`, or the shortcut `HF[]`, to chain method calls and Array or Hash
|
115
|
+
lookups:
|
116
|
+
|
117
|
+
**Note:** *Method chaining may not work as expected if entered in `irb`, because
|
118
|
+
`irb` might try to call `#to_s` or `#inspect` on the method chain!*
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
data = {
|
122
|
+
s: 'Hashformer',
|
123
|
+
v: [1, 2, 3, 4, 5]
|
124
|
+
}
|
125
|
+
xform = {
|
126
|
+
s: HF[:s].reverse.capitalize,
|
127
|
+
# It's important to call clone before calling methods that modify the array
|
128
|
+
v: HF[:v].clone.concat([6]).map{|x| x * x}.reduce(0, &:+)
|
129
|
+
}
|
130
|
+
|
131
|
+
Hashformer.transform(data, xform)
|
132
|
+
# => {s: 'Remrofhsah', v: 91}
|
133
|
+
```
|
134
|
+
|
135
|
+
Unlike `HF::g.path`, `HF[]`/`HF::G.chain` will raise an exception if you try to
|
136
|
+
access beyond a path that doesn't exist:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
data = {
|
140
|
+
a: [1, 2, 3]
|
141
|
+
}
|
142
|
+
xform = {
|
143
|
+
a: HF[:b][0]
|
144
|
+
}
|
145
|
+
|
146
|
+
Hashformer.transform(data, xform)
|
147
|
+
# Raises "undefined method `[]' for nil:NilClass"
|
148
|
+
```
|
149
|
+
|
150
|
+
`HF[]` or `HF::G.chain` without any methods or references will return the input
|
151
|
+
Hash:
|
152
|
+
|
153
|
+
```ruby
|
154
|
+
data = {
|
155
|
+
a: 1
|
156
|
+
}
|
157
|
+
xform = {
|
158
|
+
a: HF[].count,
|
159
|
+
b: HF::G.chain
|
160
|
+
}
|
161
|
+
|
162
|
+
Hashformer.transform(data, xform)
|
163
|
+
# => {a: 1, b: {a: 1}}
|
164
|
+
```
|
165
|
+
|
166
|
+
Although it's not recommended, you can also chain operators as long as `HF[]`
|
167
|
+
is the first element evaluated by Ruby:
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
xform = {
|
171
|
+
x: -(HF[:x] * 2) + 5
|
172
|
+
}
|
173
|
+
|
174
|
+
Hashformer.transform({x: 3}, xform)
|
175
|
+
# => {x: -1}
|
176
|
+
|
177
|
+
Hashformer.transform({x: -12}, xform)
|
178
|
+
# => {x: 29}
|
179
|
+
```
|
180
|
+
|
181
|
+
|
182
|
+
#### Mapping one or more values
|
183
|
+
|
184
|
+
If you want Hashformer to gather one or more values for you and either place
|
185
|
+
them in an Array or pass them to a lambda, you can use `HF::G.map`. Pass the
|
186
|
+
names of the keys to map as parameters, followed by the optional Proc or
|
187
|
+
lambda:
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
data = {
|
191
|
+
a: 'Hashformer'
|
192
|
+
}
|
193
|
+
xform = {
|
194
|
+
a: HF::G.map(:a, &:upcase),
|
195
|
+
b: HF::G.map(:a)
|
196
|
+
}
|
197
|
+
|
198
|
+
Hashformer.transform(data, xform)
|
199
|
+
# => {a: 'HASHFORMER', b: ['Hashformer']}
|
200
|
+
```
|
201
|
+
|
202
|
+
You can also mix and match paths and method chains in the `HF::G.map`
|
203
|
+
parameters:
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
data = {
|
207
|
+
items: [
|
208
|
+
{name: 'Item 1', price: 1.50},
|
209
|
+
{name: 'Item 2', price: 2.50},
|
210
|
+
{name: 'Item 3', price: 3.50},
|
211
|
+
{name: 'Item 4', price: 4.50},
|
212
|
+
],
|
213
|
+
shipping: 5.50
|
214
|
+
}
|
215
|
+
xform = {
|
216
|
+
item_total: HF[:items].map{|i| i[:price]}.reduce(0.0, &:+),
|
217
|
+
total: HF::G.map(HF[:items].map{|i| i[:price]}.reduce(0.0, &:+), HF::G.path[:shipping], &:+)
|
218
|
+
}
|
219
|
+
|
220
|
+
Hashformer.transform(data, xform)
|
221
|
+
# => {item_total: 12.0, total: 17.5}
|
222
|
+
```
|
223
|
+
|
224
|
+
|
225
|
+
#### Lambda processing
|
226
|
+
|
227
|
+
If you need to apply a completely custom transformation to your data, you can
|
228
|
+
use a raw lambda. The lambda will be called with the entire input Hash.
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
data = {
|
232
|
+
x: 3.0,
|
233
|
+
y: 4.0
|
234
|
+
}
|
235
|
+
xform = {
|
236
|
+
radius: ->(h){ Math.sqrt(h[:x] * h[:x] + h[:y] * h[:y]) }
|
237
|
+
}
|
238
|
+
|
239
|
+
Hashformer.transform(data, xform)
|
240
|
+
# => {radius: 5.0}
|
241
|
+
```
|
242
|
+
|
243
|
+
|
244
|
+
#### Dynamic key names
|
245
|
+
|
246
|
+
There might not be much use for it, but you can use a lambda as a key as well.
|
247
|
+
It will be called with its associated unprocessed value and the input Hash:
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
data = {
|
251
|
+
key: :x,
|
252
|
+
value: 0
|
253
|
+
}
|
254
|
+
xform = {
|
255
|
+
->(value, h){h[:key]} => :value
|
256
|
+
}
|
257
|
+
|
258
|
+
Hashformer.transform(data, xform)
|
259
|
+
# => {x: 0}
|
260
|
+
```
|
261
|
+
|
262
|
+
|
263
|
+
#### Practical example with validation
|
264
|
+
|
265
|
+
Suppose your application receives addresses in one format, but you need to pass
|
266
|
+
them along in another format. You might need to rename some keys, convert some
|
267
|
+
keys to different types, merge keys, etc. We'll define the input and output
|
268
|
+
data formats using [Classy Hash schemas](https://github.com/deseretbook/classy_hash#simple-example).
|
269
|
+
|
270
|
+
```ruby
|
271
|
+
# Classy Hash schema - https://github.com/deseretbook/classy_hash
|
272
|
+
in_schema = {
|
273
|
+
# Totally violates http://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/
|
274
|
+
first: String,
|
275
|
+
last: String,
|
276
|
+
city: String,
|
277
|
+
phone: String,
|
278
|
+
}
|
279
|
+
|
280
|
+
out_schema = {
|
281
|
+
name: String,
|
282
|
+
location: String,
|
283
|
+
phone: Integer, # Just for example; probably shouldn't make phone numbers integers
|
284
|
+
}
|
285
|
+
```
|
286
|
+
|
287
|
+
You can write a Hashformer transformation to turn any Hash with the `in_schema`
|
288
|
+
format into a Hash with the `out_schema` format, and verify the results:
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
# Hashformer transformation - https://github.com/deseretbook/hashformer
|
292
|
+
xform = {
|
293
|
+
# Validate input and output data according to the Classy Hash schemas
|
294
|
+
__in_schema: in_schema,
|
295
|
+
__out_schema: out_schema,
|
296
|
+
|
297
|
+
# Combine first and last name into a single String
|
298
|
+
name: HF::G.map(:first, :last) {|f, l| "#{f} #{l}".strip},
|
299
|
+
|
300
|
+
# Copy the :city field directly into :location
|
301
|
+
location: :city,
|
302
|
+
|
303
|
+
# Remove non-digits from :phone
|
304
|
+
phone: HF[:phone].gsub(/[^\d]/, '').to_i
|
305
|
+
}
|
306
|
+
|
307
|
+
data = {
|
308
|
+
first: 'Hash',
|
309
|
+
last: 'Transformed',
|
310
|
+
city: 'Here',
|
311
|
+
phone: '555-555-5555',
|
312
|
+
}
|
313
|
+
|
314
|
+
Hashformer.transform(data, xform)
|
315
|
+
# => {name: 'Hash Transformed', location: 'Here', phone: 5555555555}
|
316
|
+
```
|
317
|
+
|
318
|
+
|
319
|
+
### Testing
|
320
|
+
|
321
|
+
Hashformer includes a thorough [RSpec](http://rspec.info) test suite:
|
322
|
+
|
323
|
+
```bash
|
324
|
+
# Execute within a clone of the Git repository:
|
325
|
+
bundle install --without=development
|
326
|
+
rspec
|
327
|
+
```
|
328
|
+
|
329
|
+
|
330
|
+
### Alternatives
|
331
|
+
|
332
|
+
Hashformer just might be the coolest Ruby Hash data transformer out there. But
|
333
|
+
if you disagree, here are some other options:
|
334
|
+
|
335
|
+
- [hash_transformer](https://github.com/trampoline/hash_transformer) provides
|
336
|
+
an *imperative* DSL for Hash modification.
|
337
|
+
- [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)
|
338
|
+
- [XSLT](https://en.wikipedia.org/wiki/Xslt)
|
339
|
+
|
340
|
+
|
341
|
+
### License
|
342
|
+
|
343
|
+
Hashformer is released under the MIT license (see the `LICENSE` file for the
|
344
|
+
license text and copyright notice).
|
data/hashformer.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'hashformer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "hashformer"
|
8
|
+
spec.version = Hashformer::VERSION
|
9
|
+
spec.authors = ['Deseret Book', 'Mike Bourgeous']
|
10
|
+
spec.email = ["mike@mikebourgeous.com"]
|
11
|
+
spec.summary = 'Transform any Hash with a declarative data transformation DSL for Ruby'
|
12
|
+
spec.description = <<-DESC
|
13
|
+
Hashformer provides a simple, Ruby Hash-based way of transforming data from
|
14
|
+
one format to another. It's vaguely like XSLT, but way less complicated
|
15
|
+
and way more Ruby.
|
16
|
+
DESC
|
17
|
+
spec.homepage = "https://github.com/deseretbook/hashformer"
|
18
|
+
|
19
|
+
spec.files = `git ls-files -z`.split("\x0")
|
20
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
21
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.required_ruby_version = '>= 1.9.3'
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
27
|
+
spec.add_runtime_dependency "classy_hash", "~> 0.1", ">= 0.1.1"
|
28
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# Hashformer transformation generators
|
2
|
+
# Created July 2014 by Mike Bourgeous, DeseretBook.com
|
3
|
+
# Copyright (C)2014 Deseret Book
|
4
|
+
# See LICENSE and README.md for details.
|
5
|
+
|
6
|
+
require 'classy_hash'
|
7
|
+
|
8
|
+
module Hashformer
|
9
|
+
# This module contains simple methods for generating complex transformations
|
10
|
+
# for Hashformer.
|
11
|
+
module Generate
|
12
|
+
# Internal representation of a mapping transformation. Do not instantiate
|
13
|
+
# directly; call Hashformer::Generate.map (or HF::G.map) instead.
|
14
|
+
class Map
|
15
|
+
def initialize(*keys_or_callables, &block)
|
16
|
+
@keys = keys_or_callables
|
17
|
+
@block = block
|
18
|
+
end
|
19
|
+
|
20
|
+
# Called to process the map on the given +input_hash+
|
21
|
+
def call(input_hash)
|
22
|
+
values = @keys.map{|k| Hashformer.get_value(input_hash, k)}
|
23
|
+
values = @block.call(*values) if @block
|
24
|
+
values
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal representation of a path to a nested key/value. Do not
|
29
|
+
# instantiate directly; call Hashformer::Generate.path (or HF::G.path)
|
30
|
+
# instead.
|
31
|
+
class Path
|
32
|
+
def initialize
|
33
|
+
@pathlist = []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Called to dereference the path on the given +input_hash+.
|
37
|
+
def call(input_hash)
|
38
|
+
begin
|
39
|
+
value = input_hash
|
40
|
+
@pathlist.each do |path_item|
|
41
|
+
value = value && value[path_item]
|
42
|
+
end
|
43
|
+
value
|
44
|
+
rescue => e
|
45
|
+
raise "Error dereferencing path #{self}: #{e}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds a path item to the end of the saved path.
|
50
|
+
def [](path_item)
|
51
|
+
@pathlist << path_item
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
@pathlist.map{|p| "[#{p.inspect}]"}.join
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Internal representation of a method call and array lookup chainer. Do
|
61
|
+
# not use this directly; instead use HF::G.chain().
|
62
|
+
class Chain
|
63
|
+
# Receiver for chaining calls that has no methods of its own except
|
64
|
+
# initialize. This allows methods like :call to be chained.
|
65
|
+
#
|
66
|
+
# IMPORTANT: No methods other than .__chain can be called on this object,
|
67
|
+
# because they will be chained! Instead, use === to detect the object's
|
68
|
+
# type, for example.
|
69
|
+
class Receiver < BasicObject
|
70
|
+
# An oddly named accessor is used instead of #initialize to avoid
|
71
|
+
# conflicts with any methods that might be chained.
|
72
|
+
attr_accessor :__chain
|
73
|
+
|
74
|
+
# Adds a method call or array dereference to the list of calls to apply.
|
75
|
+
def method_missing(name, *args, &block)
|
76
|
+
@__chain << {name: name, args: args, block: block}
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
undef !=
|
81
|
+
undef ==
|
82
|
+
undef !
|
83
|
+
undef instance_exec
|
84
|
+
undef instance_eval
|
85
|
+
undef equal?
|
86
|
+
undef singleton_method_added
|
87
|
+
undef singleton_method_removed
|
88
|
+
undef singleton_method_undefined
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the call chaining receiver.
|
92
|
+
attr_reader :receiver
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@calls = []
|
96
|
+
@receiver = Receiver.new
|
97
|
+
@receiver.__chain = self
|
98
|
+
end
|
99
|
+
|
100
|
+
# Applies the methods stored by #method_missing
|
101
|
+
def call(input_hash)
|
102
|
+
value = input_hash
|
103
|
+
@calls.each do |c|
|
104
|
+
value = value.send(c[:name], *c[:args], &c[:block])
|
105
|
+
end
|
106
|
+
value
|
107
|
+
end
|
108
|
+
|
109
|
+
# Adds the given call info (used by Receiver).
|
110
|
+
def <<(info)
|
111
|
+
@calls << info
|
112
|
+
self
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Generates a transformation that passes one or more values from the input
|
117
|
+
# Hash (denoted by key names or paths (see Hashformer::Generate.path) to
|
118
|
+
# the block. If the block is not given, then the values are placed in an
|
119
|
+
# array in the order in which their keys were given as parameters.
|
120
|
+
#
|
121
|
+
# Examples:
|
122
|
+
# HF::G.map(:first, :last) do |f, l| "#{f} #{l}".strip end
|
123
|
+
# HF::G.map(:a1, :a2) # Turns {a1: 1, a2: 2} into [1, 2]
|
124
|
+
# HF::G.map(HF::G.path[:address][:line1], HF::G.path[:address][:line2])
|
125
|
+
def self.map(*keys_or_paths, &block)
|
126
|
+
Map.new(*keys_or_paths, &block)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Generates a path reference (via Path#[]) that grabs a nested value for
|
130
|
+
# use directly or in other transformations. If no path is specified, the
|
131
|
+
# transformation will use the input hash.
|
132
|
+
#
|
133
|
+
# When the path is dereferenced, if any of the parent elements referred to
|
134
|
+
# by the path are nil, then nil will be returned. If any path elements do
|
135
|
+
# not respond to [], or otherwise raise an exception, then an exception
|
136
|
+
# will be raised by the transformation.
|
137
|
+
#
|
138
|
+
# The major difference between .path and .chain is that .path will return
|
139
|
+
# nil if a nonexistent key is referenced (even multiple times), while
|
140
|
+
# .chain will raise an exception.
|
141
|
+
#
|
142
|
+
# Examples:
|
143
|
+
# HF::G.path[:user][:address][:line1]
|
144
|
+
# HF::G.path[:lines][5]
|
145
|
+
def self.path
|
146
|
+
Path.new
|
147
|
+
end
|
148
|
+
|
149
|
+
# Generates a method call chain to apply to the input hash given to a
|
150
|
+
# transformation. This allows path references (as with HF::G.path) and
|
151
|
+
# method calls to be stored and applied later.
|
152
|
+
#
|
153
|
+
# Example:
|
154
|
+
# data = { in1: { in2: [1, 2, 3, [4, 5, 6, 7]] } }
|
155
|
+
# xform = { out1: HF::G.chain[:in1][:in2][3].reduce(&:+) }
|
156
|
+
# Hashformer.transform(data, xform) # Returns { out1: 22 }
|
157
|
+
def self.chain
|
158
|
+
Chain.new.receiver
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Shortcut to Hashformer::Generate
|
163
|
+
G = Generate
|
164
|
+
|
165
|
+
# Convenience method for calling HF::G.chain() to generate a path reference
|
166
|
+
# and/or method call chain. If the initial +path_item+ is not given, then
|
167
|
+
# the method chain will start with the input hash. Chaining methods that
|
168
|
+
# have side effects or modify the underlying data is not recommended.
|
169
|
+
#
|
170
|
+
# Example:
|
171
|
+
# data = { in1: { in2: ['a', 'b', 'c', 'd'] } }
|
172
|
+
# xform = { out1: HF[:in1][:in2][3], out2: HF[].count }
|
173
|
+
# Hashformer.transform(data, xform) # Returns { out1: 'd', out2: 1 }
|
174
|
+
def self.[](path_item = :__hashformer_not_given)
|
175
|
+
path_item == :__hashformer_not_given ? HF::G.chain : HF::G.chain[path_item]
|
176
|
+
end
|
177
|
+
end
|