hashformer 0.2.2 → 0.3.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 +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
|
-
[](http://badge.fury.io/rb/hashformer) [](https://codeclimate.com/github/deseretbook/hashformer) [](https://codeclimate.com/github/deseretbook/hashformer) [](http://badge.fury.io/rb/hashformer) [](https://codeclimate.com/github/deseretbook/hashformer) [](https://codeclimate.com/github/deseretbook/hashformer) [](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
|