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 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