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.
- 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 [](https://codeclimate.com/github/deseretbook/hashformer) [](https://codeclimate.com/github/deseretbook/hashformer) [](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
|