hashformer 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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).
@@ -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
@@ -0,0 +1,3 @@
1
+ module Hashformer
2
+ VERSION = "0.2.0"
3
+ end