hashformer 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +11 -0
- data/Gemfile +0 -16
- data/README.md +86 -3
- data/hashformer.gemspec +8 -2
- data/lib/hashformer/date.rb +93 -0
- data/lib/hashformer/generate.rb +112 -13
- data/lib/hashformer/version.rb +1 -1
- data/lib/hashformer.rb +7 -6
- data/spec/lib/hashformer/date_spec.rb +96 -0
- data/spec/lib/hashformer/generate_spec.rb +61 -10
- data/spec/lib/hashformer_spec.rb +1 -5
- data/spec/spec_helper.rb +2 -0
- metadata +87 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 05a8926c5ecb538a6ddf72a14f118d655a18852a
|
4
|
+
data.tar.gz: 967d6cd3decd71173baafa6dd21745a0ebfa1542
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e4f0f3d3b1161500ca05fafd4c3949ee3e5660be691a3051c0f3823b74d2c3d713c76e212db9116dc73ee68e192cb9caec61a1a52cfc50a1b7f92eb1f3a99844
|
7
|
+
data.tar.gz: 15d32b0a42af0a007028793131e7fc69493c5228eedc4b468ac5e0831a678859d9e0c780f44aa0bc56bb9d5b26715d906a4d360b29fa82241501f7f60b58e562
|
data/.rspec
CHANGED
data/.travis.yml
ADDED
data/Gemfile
CHANGED
@@ -1,19 +1,3 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
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
3
|
gemspec
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Hashformer
|
2
2
|
=========
|
3
|
-
[![Gem Version](https://badge.fury.io/rb/hashformer.svg)](http://badge.fury.io/rb/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) [![
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/hashformer.svg)](http://badge.fury.io/rb/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) [![Build Status](https://travis-ci.org/deseretbook/hashformer.svg)](https://travis-ci.org/deseretbook/hashformer)
|
4
4
|
|
5
5
|
### Transform any Ruby Hash with a declarative DSL
|
6
6
|
|
@@ -14,9 +14,17 @@ keys, input keys, and transformations, and Hashformer will convert your data
|
|
14
14
|
into the format you specify. It can also help verify your transformations by
|
15
15
|
validating input and output data using [Classy Hash](https://github.com/deseretbook/classy_hash).
|
16
16
|
|
17
|
+
Note that Hashformer is not for everyone. If your data transformation needs
|
18
|
+
don't involve massive changes to the data structure or values, and/or you don't
|
19
|
+
need multiple people to be able to work on the transformations separately from
|
20
|
+
other code, you might be better off doing your transformations in plain Ruby.
|
21
|
+
|
17
22
|
|
18
23
|
### Examples
|
19
24
|
|
25
|
+
Examples of each feature are provided here, but complete documentation for each
|
26
|
+
method lives in the code.
|
27
|
+
|
20
28
|
#### Basic renaming
|
21
29
|
|
22
30
|
If you just need to move/copy/rename keys, you specify the source key as the
|
@@ -147,7 +155,8 @@ This is the most useful and powerful aspect of Hashformer. You can use
|
|
147
155
|
lookups:
|
148
156
|
|
149
157
|
_**Note:** Method chaining may not work as expected if entered in `irb`, because
|
150
|
-
`irb` might try to call `#to_s` or `#inspect` on the method chain!
|
158
|
+
`irb` might try to call `#to_s` or `#inspect` on the method chain! See `.__end`
|
159
|
+
and `.enable_debugging` for possible solutions_
|
151
160
|
|
152
161
|
```ruby
|
153
162
|
data = {
|
@@ -210,6 +219,56 @@ Hashformer.transform({x: -12}, xform)
|
|
210
219
|
# => {x: 29}
|
211
220
|
```
|
212
221
|
|
222
|
+
##### `__as`
|
223
|
+
|
224
|
+
The special `__as` method on a method chain, added in version 0.3.0, allows you
|
225
|
+
to work with the chain's current value in a block like `Object#tap`, but the
|
226
|
+
return value of the block is passed to the next step of the chain. This is
|
227
|
+
useful if you need to pass the chain value to an outside function.
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
def func(x)
|
231
|
+
"something to do with #{x}"
|
232
|
+
end
|
233
|
+
|
234
|
+
xform = {
|
235
|
+
out: HF[:in].__as{|v| 'test ' + func(v) }
|
236
|
+
}
|
237
|
+
|
238
|
+
Hashformer.transform({ in: 'code' }, xform)
|
239
|
+
# => { out: 'something to do with test code' }
|
240
|
+
```
|
241
|
+
|
242
|
+
##### `__end`
|
243
|
+
|
244
|
+
The `__end` method on a method chain will disable further modification of the
|
245
|
+
chain. This is not normally needed unless your transformation Hashes might be
|
246
|
+
`#inspect`ed by other code (e.g. IRB or Pry). Using `__end` might prevent you
|
247
|
+
from needing to enable chain debugging.
|
248
|
+
|
249
|
+
```ruby
|
250
|
+
xform = {
|
251
|
+
# Everything after __end will be ignored, including __as
|
252
|
+
out: HF[:in].to_s.__end.to_i.no.more.methods
|
253
|
+
}
|
254
|
+
|
255
|
+
Hashformer.transform({ in: 100 })
|
256
|
+
# => { out: '100' }
|
257
|
+
```
|
258
|
+
|
259
|
+
##### Debugging chains
|
260
|
+
|
261
|
+
If `__end` isn't enough to make your method chains work with whatever debugging
|
262
|
+
or instrumentation you have, you can enable chain debugging. *When chain
|
263
|
+
debugging is enabled, any standard `Object` methods cannot be added to chains
|
264
|
+
(this includes commonly chained methods like `#to_s`).* Each method added to a
|
265
|
+
chain will also be printed to `$stdout`.
|
266
|
+
|
267
|
+
```ruby
|
268
|
+
HF::G::Chain.enable_debugging
|
269
|
+
HF::G::Chain.disable_debugging
|
270
|
+
```
|
271
|
+
|
213
272
|
|
214
273
|
#### Mapping one or more values
|
215
274
|
|
@@ -232,7 +291,8 @@ Hashformer.transform(data, xform)
|
|
232
291
|
```
|
233
292
|
|
234
293
|
You can also mix and match paths and method chains in the `HF::G.map`
|
235
|
-
parameters
|
294
|
+
parameters. The result of the method chain transformation or path retrieval
|
295
|
+
will be used in the map, instead of looking up a key in the original hash:
|
236
296
|
|
237
297
|
```ruby
|
238
298
|
data = {
|
@@ -347,6 +407,29 @@ Hashformer.transform(data, xform)
|
|
347
407
|
# => {b: { n: 1, o: 2, p: 5 }}
|
348
408
|
```
|
349
409
|
|
410
|
+
#### Dates and times
|
411
|
+
|
412
|
+
We found ourselves writing a lot of identical date transformation `Proc`s in our
|
413
|
+
transformations, so version 0.3.0 adds some helpers for transforming dates to
|
414
|
+
and from numeric values. If you use Hashformer in a project that also uses
|
415
|
+
ActiveSupport, you can transform time zones as well.
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
xform = {
|
419
|
+
int: HF::Date.to_i(:time),
|
420
|
+
float: HF::Date.to_f(:time),
|
421
|
+
date: HF::Date.to_date(:numeric),
|
422
|
+
}
|
423
|
+
|
424
|
+
data = {
|
425
|
+
time: Time.at(10.75),
|
426
|
+
numeric: 10.75
|
427
|
+
}
|
428
|
+
|
429
|
+
Hashformer.transform(data, xform)
|
430
|
+
# => { int: 10, float: 10.75, date: #<DateTime 1970-01-01...}
|
431
|
+
```
|
432
|
+
|
350
433
|
|
351
434
|
#### Practical example with validation
|
352
435
|
|
data/hashformer.gemspec
CHANGED
@@ -21,8 +21,14 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.required_ruby_version = '>=
|
24
|
+
spec.required_ruby_version = '>= 2.0'
|
25
25
|
|
26
|
-
spec.add_development_dependency "bundler", "~> 1.5"
|
27
26
|
spec.add_runtime_dependency "classy_hash", "~> 0.1", ">= 0.1.1"
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
30
|
+
spec.add_development_dependency "simplecov", "~> 0.12.0"
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
32
|
+
spec.add_development_dependency "byebug", "~> 9.0" unless RUBY_PLATFORM == 'java'
|
33
|
+
spec.add_development_dependency "activesupport", "~> 4.2"
|
28
34
|
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# Hashformer date and time transformation generators.
|
2
|
+
# Created July 2016 by Mike Bourgeous, DeseretBook.com
|
3
|
+
# Copyright (C)2016 Deseret Book
|
4
|
+
# See LICENSE and README.md for details.
|
5
|
+
|
6
|
+
require 'date'
|
7
|
+
require 'time'
|
8
|
+
|
9
|
+
module Hashformer
|
10
|
+
module Date
|
11
|
+
# Generates a Proc to convert a Date, Time, or DateTime to an Integer UNIX
|
12
|
+
# timestamp. Passes nil through unmodified. Useful for transforming to
|
13
|
+
# serialization formats that don't support dates directly.
|
14
|
+
#
|
15
|
+
# Returns a method chain that can be further modified.
|
16
|
+
#
|
17
|
+
# Raises an error during transformation if the input value is not nil and
|
18
|
+
# is a different class.
|
19
|
+
#
|
20
|
+
# Example:
|
21
|
+
# data = { time: Time.at(0.75), no_time: nil }
|
22
|
+
# xform = { ts: HF::Date.to_i(:time), ts2: HF::Date.to_i(:no_time) }
|
23
|
+
# HF.transform(data, xform)
|
24
|
+
# # => { ts: 0, ts2: nil }
|
25
|
+
def self.to_i(key)
|
26
|
+
HF[key].__as{|d|
|
27
|
+
d = d.to_time if d.is_a?(::Date)
|
28
|
+
raise "Invalid date/time class #{d.class}" unless d.nil? || d.is_a?(Time) || d.is_a?(DateTime)
|
29
|
+
|
30
|
+
d && d.to_i
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Generates a Proc to convert a Date, Time, or DateTime to a floating point
|
35
|
+
# UNIX timestamp. Passes nil through unmodified. Useful for transforming
|
36
|
+
# to serialization formats that don't support dates directly.
|
37
|
+
#
|
38
|
+
# Returns a method chain that can be further modified.
|
39
|
+
#
|
40
|
+
# Raises an error during transformation if the input value is not nil and
|
41
|
+
# is a different class.
|
42
|
+
#
|
43
|
+
# Example:
|
44
|
+
# data = { time: Time.at(0.75), no_time: nil }
|
45
|
+
# xform = { ts: HF::Date.to_f(:time), ts2: HF::Date.to_f(:no_time) }
|
46
|
+
# HF.transform(data, xform)
|
47
|
+
# # => { ts: 0.75, ts2: nil }
|
48
|
+
def self.to_f(key)
|
49
|
+
HF[key].__as{|d|
|
50
|
+
d = d.to_time if d.is_a?(::Date)
|
51
|
+
raise "Invalid date/time class #{d.class}" unless d.nil? || d.is_a?(Time) || d.is_a?(DateTime)
|
52
|
+
|
53
|
+
d && d.to_f
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Generates a Proc to convert an Integer or Numeric UNIX timestamp to a
|
58
|
+
# DateTime. Passes nil through unmodified. Useful for transforming from
|
59
|
+
# serialization formats that don't support dates directly to database
|
60
|
+
# interfaces that might expect DateTime rather than Time.
|
61
|
+
#
|
62
|
+
# If +tz_name+ is the default special value of :utc, then the resulting
|
63
|
+
# DateTime will be in UTC time. If +tz_name+ is nil, the default zone will
|
64
|
+
# be used. Otherwise, if +tz_name+ is given and the Time class responds to
|
65
|
+
# #in_time_zone (requires ActiveSupport::TimeWithZone, which is not loaded
|
66
|
+
# by Hashformer), then the date will be converted to the given named
|
67
|
+
# timezone.
|
68
|
+
#
|
69
|
+
# Returns a method chain that can be further modified.
|
70
|
+
#
|
71
|
+
# Raises an error during transformation if the input value is not a type
|
72
|
+
# supported by Time.at().
|
73
|
+
#
|
74
|
+
# Example:
|
75
|
+
def self.to_date(key, tz_name = :utc)
|
76
|
+
if tz_name == :utc
|
77
|
+
HF[key].__as{|d|
|
78
|
+
d && Time.at(d).utc.to_datetime
|
79
|
+
}
|
80
|
+
elsif tz_name
|
81
|
+
raise 'ActiveSupport time helpers are required for tz_name' unless Time.instance_methods.include?(:in_time_zone)
|
82
|
+
|
83
|
+
HF[key].__as{|d|
|
84
|
+
d && Time.at(d).in_time_zone(tz_name).to_datetime
|
85
|
+
}
|
86
|
+
else
|
87
|
+
HF[key].__as{|d|
|
88
|
+
d && Time.at(d).to_datetime
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
data/lib/hashformer/generate.rb
CHANGED
@@ -62,22 +62,53 @@ module Hashformer
|
|
62
62
|
# Internal representation of a method call and array lookup chainer. Do
|
63
63
|
# not use this directly; instead use HF::G.chain().
|
64
64
|
class Chain
|
65
|
-
#
|
66
|
-
#
|
67
|
-
|
68
|
-
# IMPORTANT: No methods other than .__chain can be called on this object,
|
69
|
-
# because they will be chained! Instead, use === to detect the object's
|
70
|
-
# type, for example.
|
71
|
-
class Receiver < BasicObject
|
65
|
+
# Base module that defines methods included by BasicReceiver and
|
66
|
+
# DebuggableReceiver.
|
67
|
+
module ReceiverMethods
|
72
68
|
# An oddly named accessor is used instead of #initialize to avoid
|
73
69
|
# conflicts with any methods that might be chained.
|
74
70
|
attr_accessor :__chain
|
75
71
|
|
76
|
-
# Adds a method call or array dereference to the list of calls to
|
72
|
+
# Adds a method call or array dereference to the list of calls to
|
73
|
+
# apply. Does nothing if #__end has been called. Returns self for
|
74
|
+
# more chaining.
|
77
75
|
def method_missing(name, *args, &block)
|
78
|
-
@
|
76
|
+
@__ended ||= false
|
77
|
+
@__chain << {name: name, args: args, block: block} unless @__ended
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Adds a call to the given +block+ to the chain like Object#tap, but
|
82
|
+
# returns the result of the block instead of the original object. Any
|
83
|
+
# arguments given will be passed to the +block+ after the current
|
84
|
+
# value. Does nothing if #__end has been called. Returns self for
|
85
|
+
# more chaining.
|
86
|
+
#
|
87
|
+
# This is similar in spirit (but not implementation) to
|
88
|
+
# http://stackoverflow.com/a/12849214
|
89
|
+
def __as(*args, &block)
|
90
|
+
::Kernel.raise 'No block given to #__as' unless ::Kernel.block_given?
|
91
|
+
@__ended ||= false
|
92
|
+
@__chain << {args: args, block: block} unless @__ended
|
93
|
+
self
|
94
|
+
end
|
95
|
+
|
96
|
+
# Disables further chaining. Any future method calls will just return
|
97
|
+
# the existing chain without modifying it.
|
98
|
+
def __end
|
99
|
+
@__ended = true
|
79
100
|
self
|
80
101
|
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Receiver for chaining calls that has no methods of its own except
|
105
|
+
# initialize. This allows methods like :call to be chained.
|
106
|
+
#
|
107
|
+
# IMPORTANT: No methods other than #__chain, #__as, or #__end should be
|
108
|
+
# called on this object, because they will be chained! Instead, use ===
|
109
|
+
# to detect the object's type, for example.
|
110
|
+
class BasicReceiver < BasicObject
|
111
|
+
include ReceiverMethods
|
81
112
|
|
82
113
|
undef !=
|
83
114
|
undef ==
|
@@ -90,12 +121,73 @@ module Hashformer
|
|
90
121
|
undef singleton_method_undefined
|
91
122
|
end
|
92
123
|
|
93
|
-
#
|
124
|
+
# Debuggable chain receiver that inherits from Object. This will break a
|
125
|
+
# lot of chains (e.g. any chain using #to_s or #inspect), but will allow
|
126
|
+
# some debugging tools to operate without crashing. See
|
127
|
+
# Hashformer::Generate::Chain.enable_debugging.
|
128
|
+
class DebuggableReceiver
|
129
|
+
include ReceiverMethods
|
130
|
+
|
131
|
+
# Overrides ReceiverMethods#method_missing to print out methods as they
|
132
|
+
# are added to the chain.
|
133
|
+
def method_missing(name, *args, &block)
|
134
|
+
__dbg_msg(name, args, block)
|
135
|
+
super
|
136
|
+
end
|
137
|
+
|
138
|
+
# Overrides ReceiverMethods#__as to print out blocks as they are added
|
139
|
+
# to the chain.
|
140
|
+
def __as(*args, &block)
|
141
|
+
__dbg_msg('__as', args, block)
|
142
|
+
super
|
143
|
+
end
|
144
|
+
|
145
|
+
# Overrides ReceiverMethods#__end to print a message when a chain is
|
146
|
+
# ended.
|
147
|
+
def __end
|
148
|
+
$stdout.puts "Ending chain #{__id__}"
|
149
|
+
super
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
|
154
|
+
# Prints a debugging message for the addition of the given method
|
155
|
+
# +name+, +args+, and +block+. Prints "Adding..." for active chains,
|
156
|
+
# "Ignoring..." for ended chains.
|
157
|
+
def __dbg_msg(name, args, block)
|
158
|
+
$stdout.puts "#{@__ended ? 'Ignoring' : 'Adding'} " \
|
159
|
+
"#{name.inspect}(#{args.map(&:inspect).join(', ')}){#{block}} " \
|
160
|
+
"to chain #{__id__}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
class << self
|
165
|
+
# The chaining receiver class that will be used by newly created chains
|
166
|
+
# (must include ReceiverMethods).
|
167
|
+
def receiver_class
|
168
|
+
@receiver_class ||= BasicReceiver
|
169
|
+
end
|
170
|
+
|
171
|
+
# Switches Receiver to an Object (DebuggableReceiver) for debugging.
|
172
|
+
# debugging tools to introspect Receiver without crashing.
|
173
|
+
def enable_debugging
|
174
|
+
@receiver_class = DebuggableReceiver
|
175
|
+
end
|
176
|
+
|
177
|
+
# Switches Receiver back to a BasicObject (BasicReceiver).
|
178
|
+
def disable_debugging
|
179
|
+
@receiver_class = BasicReceiver
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
# Returns the call chaining receiver for this chain.
|
94
185
|
attr_reader :receiver
|
95
186
|
|
187
|
+
# Initializes an empty chain.
|
96
188
|
def initialize
|
97
189
|
@calls = []
|
98
|
-
@receiver =
|
190
|
+
@receiver = self.class.receiver_class.new
|
99
191
|
@receiver.__chain = self
|
100
192
|
end
|
101
193
|
|
@@ -103,12 +195,17 @@ module Hashformer
|
|
103
195
|
def call(input_hash)
|
104
196
|
value = input_hash
|
105
197
|
@calls.each do |c|
|
106
|
-
|
198
|
+
if c[:name]
|
199
|
+
value = value.send(c[:name], *c[:args], &c[:block])
|
200
|
+
else
|
201
|
+
# Support #__as
|
202
|
+
value = c[:block].call(value, *c[:args])
|
203
|
+
end
|
107
204
|
end
|
108
205
|
value
|
109
206
|
end
|
110
207
|
|
111
|
-
# Adds the given call info (used by
|
208
|
+
# Adds the given call info (used by ReceiverMethods).
|
112
209
|
def <<(info)
|
113
210
|
@calls << info
|
114
211
|
self
|
@@ -179,6 +276,8 @@ module Hashformer
|
|
179
276
|
# transformation. This allows path references (as with HF::G.path) and
|
180
277
|
# method calls to be stored and applied later.
|
181
278
|
#
|
279
|
+
# See Hashformer::Generate::Chain.enable_debugging if you run into issues.
|
280
|
+
#
|
182
281
|
# Example:
|
183
282
|
# data = { in1: { in2: [1, 2, 3, [4, 5, 6, 7]] } }
|
184
283
|
# xform = { out1: HF::G.chain[:in1][:in2][3].reduce(&:+) }
|
data/lib/hashformer/version.rb
CHANGED
data/lib/hashformer.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
# Hashformer: A declarative data transformation DSL for Ruby
|
2
2
|
# Created June 2014 by Mike Bourgeous, DeseretBook.com
|
3
|
-
# Copyright (C)
|
3
|
+
# Copyright (C)2016 Deseret Book
|
4
4
|
# See LICENSE and README.md for details.
|
5
5
|
|
6
6
|
require 'classy_hash'
|
7
7
|
|
8
8
|
require 'hashformer/version'
|
9
9
|
require 'hashformer/generate'
|
10
|
+
require 'hashformer/date'
|
10
11
|
|
11
12
|
|
12
13
|
# This module contains the Hashformer methods for transforming Ruby Hash
|
@@ -47,7 +48,7 @@ module Hashformer
|
|
47
48
|
next if key == :__in_schema || key == :__out_schema
|
48
49
|
|
49
50
|
key = key.call(value, data) if key.respond_to?(:call)
|
50
|
-
out[key] = self.get_value(data, value
|
51
|
+
out[key] = self.get_value(data, value)
|
51
52
|
end
|
52
53
|
|
53
54
|
validate(out, xform[:__out_schema], 'output') if validate
|
@@ -56,10 +57,10 @@ module Hashformer
|
|
56
57
|
end
|
57
58
|
|
58
59
|
# Returns a value for the given +key+, method chain, or callable on the given
|
59
|
-
# +input_hash+.
|
60
|
-
#
|
61
|
-
def self.get_value(input_hash, key
|
62
|
-
if Hashformer::Generate::Chain::
|
60
|
+
# +input_hash+. Hash keys will be processed with Hashformer.transform for
|
61
|
+
# supporting nested transformations.
|
62
|
+
def self.get_value(input_hash, key)
|
63
|
+
if Hashformer::Generate::Chain::ReceiverMethods === key
|
63
64
|
# Had to special case chains to allow chaining .call
|
64
65
|
key.__chain.call(input_hash)
|
65
66
|
elsif Hashformer::Generate::Constant === key
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# Hashformer date helper tests
|
2
|
+
# Created July 2016 by Mike Bourgeous, DeseretBook.com
|
3
|
+
# Copyright (C)2016 Deseret Book
|
4
|
+
# See LICENSE and README.md for details.
|
5
|
+
|
6
|
+
require 'rational'
|
7
|
+
require 'active_support/time'
|
8
|
+
|
9
|
+
describe Hashformer::Date do
|
10
|
+
let(:data) {
|
11
|
+
{
|
12
|
+
one_time: Time.at(1),
|
13
|
+
little_time: Time.at(0.25),
|
14
|
+
no_time: nil,
|
15
|
+
|
16
|
+
one_stamp: 1,
|
17
|
+
little_stamp: 0.25,
|
18
|
+
rational_stamp: Rational(3, 4),
|
19
|
+
no_stamp: nil,
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
describe '.to_i' do
|
24
|
+
let(:xform) {
|
25
|
+
{
|
26
|
+
one: HF::Date.to_i(:one_time),
|
27
|
+
zero: HF::Date.to_i(:little_time),
|
28
|
+
none: HF::Date.to_i(:no_time),
|
29
|
+
}
|
30
|
+
}
|
31
|
+
|
32
|
+
it 'converts dates to integer timestamps' do
|
33
|
+
expect(HF.transform(data, xform)).to eq({ one: 1, zero: 0, none: nil })
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'raises an error for invalid types' do
|
37
|
+
expect{HF.transform({ one_time: 'Bogus' }, xform)}.to raise_error(/Invalid/)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '.to_f' do
|
42
|
+
let(:xform) {
|
43
|
+
{
|
44
|
+
one: HF::Date.to_f(:one_time),
|
45
|
+
some: HF::Date.to_f(:little_time),
|
46
|
+
none: HF::Date.to_f(:no_time),
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
it 'converts dates to float timestamps' do
|
51
|
+
expect(HF.transform(data, xform)).to eq({ one: 1.0, some: 0.25, none: nil })
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'raises an error for invalid types' do
|
55
|
+
expect{HF.transform({ one_time: 'Bogus' }, xform)}.to raise_error(/Invalid/)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe '.to_date' do
|
60
|
+
let(:xform) {
|
61
|
+
{
|
62
|
+
one_utc: HF::Date.to_date(:one_stamp),
|
63
|
+
little_utc: HF::Date.to_date(:little_stamp),
|
64
|
+
rational_utc: HF::Date.to_date(:rational_stamp),
|
65
|
+
|
66
|
+
one_local: HF::Date.to_date(:one_stamp, nil),
|
67
|
+
little_local: HF::Date.to_date(:little_stamp, nil),
|
68
|
+
rational_local: HF::Date.to_date(:rational_stamp, nil),
|
69
|
+
|
70
|
+
one_zone: HF::Date.to_date(:one_stamp, 'MST'),
|
71
|
+
little_zone: HF::Date.to_date(:little_stamp, 'MST'),
|
72
|
+
rational_zone: HF::Date.to_date(:rational_stamp, 'MST'),
|
73
|
+
}
|
74
|
+
}
|
75
|
+
|
76
|
+
let(:expected) {
|
77
|
+
{
|
78
|
+
one_utc: Time.at(1).utc.to_datetime,
|
79
|
+
little_utc: Time.at(0.25).utc.to_datetime,
|
80
|
+
rational_utc: Time.at(Rational(3, 4)).utc.to_datetime,
|
81
|
+
|
82
|
+
one_local: Time.at(1).to_datetime,
|
83
|
+
little_local: Time.at(0.25).to_datetime,
|
84
|
+
rational_local: Time.at(Rational(3, 4)).to_datetime,
|
85
|
+
|
86
|
+
one_zone: Time.at(1).in_time_zone('MST').to_datetime,
|
87
|
+
little_zone: Time.at(0.25).in_time_zone('MST').to_datetime,
|
88
|
+
rational_zone: Time.at(Rational(3, 4)).in_time_zone('MST').to_datetime,
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
it 'converts timestamps to expected dates' do
|
93
|
+
expect(HF.transform(data, xform)).to eq(expected)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -1,18 +1,10 @@
|
|
1
1
|
# Hashformer transformation generator tests
|
2
2
|
# Created July 2014 by Mike Bourgeous, DeseretBook.com
|
3
|
-
# Copyright (C)
|
3
|
+
# Copyright (C)2016 Deseret Book
|
4
4
|
# See LICENSE and README.md for details.
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
require 'hashformer'
|
9
|
-
|
10
|
-
RSpec.describe Hashformer::Generate do
|
6
|
+
describe Hashformer::Generate do
|
11
7
|
describe '.const' do
|
12
|
-
let(:data) {
|
13
|
-
{}
|
14
|
-
}
|
15
|
-
|
16
8
|
it 'returns the original integer when given an integer' do
|
17
9
|
expect(Hashformer.transform({}, { a: HF::G.const(5) })).to eq({a: 5})
|
18
10
|
end
|
@@ -228,6 +220,65 @@ RSpec.describe Hashformer::Generate do
|
|
228
220
|
expect(Hashformer.transform(data, xform)).to eq({out1: 22, out2: 'd', out3: 1})
|
229
221
|
end
|
230
222
|
|
223
|
+
it 'does not add more chained methods when Chain#inspect is called' do
|
224
|
+
chain = HF[:test].one(1).two(2).three(3).four(4).five(5).__chain
|
225
|
+
inspect = chain.inspect
|
226
|
+
expect(inspect).to match(/one.*two.*three.*four.*five/)
|
227
|
+
expect(chain.inspect).to eq(inspect)
|
228
|
+
expect(chain.inspect).to eq(inspect)
|
229
|
+
end
|
230
|
+
|
231
|
+
describe '.__as' do
|
232
|
+
it 'returns the value from the block' do
|
233
|
+
xf = { out1: HF[:in1][:in1][0].__as{|v| "H#{v}shformer" } }
|
234
|
+
expect(Hashformer.transform(data, xf)).to eq({ out1: 'Hashformer' })
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'raises an error if no block is given' do
|
238
|
+
expect{ HF[].__as() }.to raise_error(/No block given/)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe '.__end' do
|
243
|
+
it 'prevents further method calls or __as blocks from being added' do
|
244
|
+
xf = { out1: HF[:in1][:in1].count.__end.odd?.__as{nil}.__end.no.more.calls.added }
|
245
|
+
expect(Hashformer.transform(data, xf)).to eq({ out1: 4 })
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
context 'debugging methods' do
|
250
|
+
it 'can enable and disable debugging' do
|
251
|
+
begin
|
252
|
+
HF::G::Chain.enable_debugging
|
253
|
+
|
254
|
+
chain = HF[]
|
255
|
+
|
256
|
+
expect($stdout).to receive(:puts).with(/Adding.*__as/)
|
257
|
+
chain.__as{}
|
258
|
+
|
259
|
+
expect($stdout).to receive(:puts).with(/Adding.*info/)
|
260
|
+
chain.info
|
261
|
+
|
262
|
+
expect($stdout).to receive(:puts).with(/Ending/)
|
263
|
+
chain.__end
|
264
|
+
|
265
|
+
expect($stdout).to receive(:puts).with(/Ignoring.*__as/)
|
266
|
+
chain.__as{}
|
267
|
+
|
268
|
+
expect($stdout).to receive(:puts).with(/Ignoring.*info/)
|
269
|
+
chain.info
|
270
|
+
|
271
|
+
|
272
|
+
HF::G::Chain.disable_debugging
|
273
|
+
|
274
|
+
expect($stdout).not_to receive(:puts)
|
275
|
+
HF[].add.__as{}.and.some.methods.then.__end.the.chain
|
276
|
+
ensure
|
277
|
+
HF::G::Chain.disable_debugging
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
231
282
|
context 'using normally reserved methods' do
|
232
283
|
it 'calls a proc with .call' do
|
233
284
|
calldata = {
|
data/spec/lib/hashformer_spec.rb
CHANGED
@@ -3,11 +3,7 @@
|
|
3
3
|
# Copyright (C)2014 Deseret Book
|
4
4
|
# See LICENSE and README.md for details.
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
require 'hashformer'
|
9
|
-
|
10
|
-
RSpec.describe Hashformer do
|
6
|
+
describe Hashformer do
|
11
7
|
describe '.validate' do
|
12
8
|
let(:in_schema) {
|
13
9
|
# ClassyHash schema - https://github.com/deseretbook/classy_hash
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hashformer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Deseret Book
|
@@ -9,42 +9,112 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2016-07-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: classy_hash
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0.1'
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.1.1
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0.1'
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.1.1
|
14
34
|
- !ruby/object:Gem::Dependency
|
15
35
|
name: bundler
|
16
36
|
requirement: !ruby/object:Gem::Requirement
|
17
37
|
requirements:
|
18
38
|
- - "~>"
|
19
39
|
- !ruby/object:Gem::Version
|
20
|
-
version: '1.
|
40
|
+
version: '1.11'
|
21
41
|
type: :development
|
22
42
|
prerelease: false
|
23
43
|
version_requirements: !ruby/object:Gem::Requirement
|
24
44
|
requirements:
|
25
45
|
- - "~>"
|
26
46
|
- !ruby/object:Gem::Version
|
27
|
-
version: '1.
|
47
|
+
version: '1.11'
|
28
48
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
49
|
+
name: rspec
|
30
50
|
requirement: !ruby/object:Gem::Requirement
|
31
51
|
requirements:
|
32
52
|
- - "~>"
|
33
53
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
35
|
-
|
54
|
+
version: '3.5'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
36
60
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
38
|
-
|
61
|
+
version: '3.5'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: simplecov
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.12.0
|
69
|
+
type: :development
|
39
70
|
prerelease: false
|
40
71
|
version_requirements: !ruby/object:Gem::Requirement
|
41
72
|
requirements:
|
42
73
|
- - "~>"
|
43
74
|
- !ruby/object:Gem::Version
|
44
|
-
version:
|
75
|
+
version: 0.12.0
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: codeclimate-test-reporter
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
45
80
|
- - ">="
|
46
81
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0
|
82
|
+
version: '0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: byebug
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '9.0'
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '9.0'
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: activesupport
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '4.2'
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '4.2'
|
48
118
|
description: |2
|
49
119
|
Hashformer provides a simple, Ruby Hash-based way of transforming data from
|
50
120
|
one format to another. It's vaguely like XSLT, but way less complicated
|
@@ -57,14 +127,17 @@ extra_rdoc_files: []
|
|
57
127
|
files:
|
58
128
|
- ".gitignore"
|
59
129
|
- ".rspec"
|
130
|
+
- ".travis.yml"
|
60
131
|
- Gemfile
|
61
132
|
- LICENSE
|
62
133
|
- README.md
|
63
134
|
- Rakefile
|
64
135
|
- hashformer.gemspec
|
65
136
|
- lib/hashformer.rb
|
137
|
+
- lib/hashformer/date.rb
|
66
138
|
- lib/hashformer/generate.rb
|
67
139
|
- lib/hashformer/version.rb
|
140
|
+
- spec/lib/hashformer/date_spec.rb
|
68
141
|
- spec/lib/hashformer/generate_spec.rb
|
69
142
|
- spec/lib/hashformer_spec.rb
|
70
143
|
- spec/spec_helper.rb
|
@@ -79,7 +152,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
152
|
requirements:
|
80
153
|
- - ">="
|
81
154
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
155
|
+
version: '2.0'
|
83
156
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
157
|
requirements:
|
85
158
|
- - ">="
|
@@ -87,11 +160,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
160
|
version: '0'
|
88
161
|
requirements: []
|
89
162
|
rubyforge_project:
|
90
|
-
rubygems_version: 2.
|
163
|
+
rubygems_version: 2.5.1
|
91
164
|
signing_key:
|
92
165
|
specification_version: 4
|
93
166
|
summary: Transform any Hash with a declarative data transformation DSL for Ruby
|
94
167
|
test_files:
|
168
|
+
- spec/lib/hashformer/date_spec.rb
|
95
169
|
- spec/lib/hashformer/generate_spec.rb
|
96
170
|
- spec/lib/hashformer_spec.rb
|
97
171
|
- spec/spec_helper.rb
|