hash_mapper 0.2.0 → 0.2.5
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 +5 -5
- data/README.md +37 -12
- data/hash_mapper.gemspec +6 -7
- data/lib/hash_mapper.rb +45 -33
- data/lib/hash_mapper/version.rb +1 -1
- data/spec/hash_mapper_spec.rb +329 -148
- data/spec/spec_helper.rb +1 -1
- metadata +18 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3bddd2af1ce1793dad8c629df875b3896616603d26ca85d1373acf19bc366c8a
|
4
|
+
data.tar.gz: c3c7d3ea740effd2faa8e870025e1bdff510e99fd6d03981be91facb23f014b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae9aa35b6b73b898c1db69c1fe154578035286f1ed4bc77d8d413aa44b8225b8425c0b40f897236a07e9ba2b96520bc7c09b80d63d286cd4e2a0f8f1ecc05822
|
7
|
+
data.tar.gz: 5c708bb349d7aa178a5461e3e6076cb838ee3aef005eebc7437f85b135dc79c268f7dc08436211959f637ceb94a34ce0e291cc3eb7990a76757f15b2b665e797
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
[ ](https://www.codeship.io/projects/45296)
|
2
|
+
|
1
3
|
# hash_mapper
|
2
4
|
|
3
5
|
* http://ismasan.github.com/hash_mapper/
|
@@ -5,7 +7,7 @@
|
|
5
7
|
## DESCRIPTION:
|
6
8
|
|
7
9
|
Maps values from hashes with different structures and/or key names. Ideal for normalizing arbitrary data to be consumed by your applications, or to prepare your data for different display formats (ie. json).
|
8
|
-
|
10
|
+
|
9
11
|
Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.
|
10
12
|
|
11
13
|
## FEATURES/PROBLEMS:
|
@@ -59,7 +61,7 @@ You can use HashMapper in your own little hash-like objects:
|
|
59
61
|
class NiceHash
|
60
62
|
include Enumerable
|
61
63
|
extend HashMapper
|
62
|
-
|
64
|
+
|
63
65
|
map from('/names/first'), to('/first_name')
|
64
66
|
map from('/names/last'), to('/last_name')
|
65
67
|
|
@@ -87,7 +89,7 @@ end
|
|
87
89
|
|
88
90
|
#### Coercing values
|
89
91
|
|
90
|
-
You want to make sure an incoming value gets converted to a certain type, so
|
92
|
+
You want to make sure an incoming value gets converted to a certain type, so
|
91
93
|
|
92
94
|
```ruby
|
93
95
|
{'one' => '1', 'two' => '2'}
|
@@ -95,7 +97,7 @@ You want to make sure an incoming value gets converted to a certain type, so
|
|
95
97
|
|
96
98
|
gets translated to
|
97
99
|
|
98
|
-
```ruby
|
100
|
+
```ruby
|
99
101
|
{:one => 1, :two => 2}
|
100
102
|
```
|
101
103
|
|
@@ -146,7 +148,7 @@ Do this:
|
|
146
148
|
|
147
149
|
```ruby
|
148
150
|
map from('/names'), to('/user') do |names|
|
149
|
-
"Mr. #{names[
|
151
|
+
"Mr. #{names[:last]}, #{names[:first]}"
|
150
152
|
end
|
151
153
|
```
|
152
154
|
|
@@ -163,9 +165,9 @@ output = NameMapper.normalize(input) # => {:first_name => 'Mark', :last_name =>
|
|
163
165
|
|
164
166
|
NameMapper.denormalize(output) # => input
|
165
167
|
```
|
166
|
-
|
168
|
+
|
167
169
|
This will work with your block filters and even nested mappers (see below).
|
168
|
-
|
170
|
+
|
169
171
|
### Advanced usage
|
170
172
|
#### Array access
|
171
173
|
You want:
|
@@ -250,7 +252,7 @@ end
|
|
250
252
|
|
251
253
|
But HashMapper's nested mappers will actually do that for you if a value is an array, so:
|
252
254
|
|
253
|
-
```ruby
|
255
|
+
```ruby
|
254
256
|
map from('/employees'), to('employees'), using: UserMapper
|
255
257
|
```
|
256
258
|
... Will map each employee using UserMapper.
|
@@ -266,12 +268,12 @@ They all yield a block with 2 arguments - the hash you are mapping from and the
|
|
266
268
|
```ruby
|
267
269
|
class EggMapper
|
268
270
|
map from('/raw'), to('/fried')
|
269
|
-
|
271
|
+
|
270
272
|
before_normalize do |input, output|
|
271
|
-
input['raw'] ||= 'please' # this will give 'raw' a default value
|
273
|
+
input['raw'] ||= 'please' # this will give 'raw' a default value
|
272
274
|
input
|
273
275
|
end
|
274
|
-
|
276
|
+
|
275
277
|
after_denormalize do |input, output|
|
276
278
|
output.to_a # the denormalized object will now be an array, not a hash!!
|
277
279
|
end
|
@@ -282,7 +284,29 @@ end
|
|
282
284
|
Important: note that for before filters, you need to return the (modified) input, and for after filters, you need to return the output.
|
283
285
|
Note also that 'output' is correct at the time of the filter, i.e. before_normalize yields 'output' as an empty hash, while after_normalize yields it as an already normalized hash.
|
284
286
|
|
285
|
-
|
287
|
+
It is possible to define multiple filters of a given type. These are run in the order in which they are defined. A common use case might be to define a `before_normalize` filter in a parent class and a child class. The output from the previous invocation of the filter is passed as the input of the next invocation.
|
288
|
+
|
289
|
+
You can pass one extra argument to before and after filters if you need to:
|
290
|
+
```ruby
|
291
|
+
class EggMapper
|
292
|
+
map from('/raw'), to('/fried')
|
293
|
+
|
294
|
+
before_normalize do |input, output, opts|
|
295
|
+
input['raw'] ||= 'please' unless opts[:no_default] # this will give 'raw' a default value
|
296
|
+
input
|
297
|
+
end
|
298
|
+
|
299
|
+
after_denormalize do |input, output, opts|
|
300
|
+
output.to_a # the denormalized object will now be an array, not a hash!!
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
|
305
|
+
EggMapper.normalize({}, options: { no_default: true })
|
306
|
+
EggMapper.denormalize({fried: 4})
|
307
|
+
```
|
308
|
+
|
309
|
+
|
286
310
|
## REQUIREMENTS:
|
287
311
|
|
288
312
|
## TODO:
|
@@ -304,6 +328,7 @@ Note also that 'output' is correct at the time of the filter, i.e. before_normal
|
|
304
328
|
* Jdeveloper (Contributor - http://github.com/jdeveloper)
|
305
329
|
* nightscape (Contributor - http://github.com/nightscape)
|
306
330
|
* radamanthus (Contributor - http://github.com/radamanthus)
|
331
|
+
* Tom Wey (Contributor - (https://github.com/tjmw)
|
307
332
|
|
308
333
|
## LICENSE:
|
309
334
|
|
data/hash_mapper.gemspec
CHANGED
@@ -7,9 +7,8 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = HashMapper::VERSION
|
8
8
|
s.authors = ['Ismael Celis']
|
9
9
|
s.description = %q{Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.}
|
10
|
-
s.date = %q{2010-09-21}
|
11
10
|
s.email = %q{ismaelct@gmail.com}
|
12
|
-
|
11
|
+
|
13
12
|
s.files = `git ls-files`.split("\n")
|
14
13
|
s.homepage = %q{http://github.com/ismasan/hash_mapper}
|
15
14
|
s.rdoc_options = ['--charset=UTF-8']
|
@@ -18,14 +17,14 @@ Gem::Specification.new do |s|
|
|
18
17
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
18
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
19
|
s.require_paths = ['lib']
|
21
|
-
|
20
|
+
|
22
21
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
23
|
-
s.add_runtime_dependency("activesupport", "
|
22
|
+
s.add_runtime_dependency("activesupport", ">= 4")
|
24
23
|
else
|
25
|
-
s.add_dependency("activesupport", "
|
24
|
+
s.add_dependency("activesupport", ">= 4")
|
26
25
|
end
|
27
|
-
|
26
|
+
|
28
27
|
# specify any dependencies here; for example:
|
29
|
-
s.add_development_dependency 'rspec'
|
28
|
+
s.add_development_dependency 'rspec', '>= 3.9'
|
30
29
|
s.add_development_dependency 'rake'
|
31
30
|
end
|
data/lib/hash_mapper.rb
CHANGED
@@ -39,11 +39,21 @@ unless [].respond_to?(:inject_with_index)
|
|
39
39
|
end
|
40
40
|
|
41
41
|
module HashMapper
|
42
|
+
DEFAULT_OPTIONS = {}.freeze
|
43
|
+
NO_VALUE = :hash_mapper_no_value
|
44
|
+
NO_DEFAULT = :hash_mapper_no_default
|
42
45
|
|
43
46
|
def self.extended(base)
|
44
47
|
base.class_eval do
|
45
|
-
class_attribute :maps
|
48
|
+
class_attribute :maps, :before_normalize_filters,
|
49
|
+
:before_denormalize_filters, :after_normalize_filters,
|
50
|
+
:after_denormalize_filters
|
51
|
+
|
46
52
|
self.maps = []
|
53
|
+
self.before_normalize_filters = []
|
54
|
+
self.before_denormalize_filters = []
|
55
|
+
self.after_normalize_filters = []
|
56
|
+
self.after_denormalize_filters = []
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
@@ -66,47 +76,49 @@ module HashMapper
|
|
66
76
|
{ using: mapper_class }
|
67
77
|
end
|
68
78
|
|
69
|
-
def normalize(a_hash)
|
70
|
-
perform_hash_mapping a_hash, :normalize
|
79
|
+
def normalize(a_hash, options: DEFAULT_OPTIONS, context: nil)
|
80
|
+
perform_hash_mapping a_hash, :normalize, options: options, context: context
|
71
81
|
end
|
72
82
|
|
73
|
-
def denormalize(a_hash)
|
74
|
-
perform_hash_mapping a_hash, :denormalize
|
83
|
+
def denormalize(a_hash, options: DEFAULT_OPTIONS, context: nil)
|
84
|
+
perform_hash_mapping a_hash, :denormalize, options: options, context: context
|
75
85
|
end
|
76
86
|
|
77
87
|
def before_normalize(&blk)
|
78
|
-
|
88
|
+
self.before_normalize_filters = self.before_normalize_filters + [blk]
|
79
89
|
end
|
80
90
|
|
81
91
|
def before_denormalize(&blk)
|
82
|
-
|
92
|
+
self.before_denormalize_filters = self.before_denormalize_filters + [blk]
|
83
93
|
end
|
84
94
|
|
85
95
|
def after_normalize(&blk)
|
86
|
-
|
96
|
+
self.after_normalize_filters = self.after_normalize_filters + [blk]
|
87
97
|
end
|
88
98
|
|
89
99
|
def after_denormalize(&blk)
|
90
|
-
|
100
|
+
self.after_denormalize_filters = self.after_denormalize_filters + [blk]
|
91
101
|
end
|
92
102
|
|
93
103
|
protected
|
94
104
|
|
95
|
-
|
96
|
-
def perform_hash_mapping(a_hash, meth)
|
105
|
+
def perform_hash_mapping(a_hash, meth, options:, context:)
|
97
106
|
output = {}
|
98
|
-
|
99
|
-
|
100
|
-
a_hash =
|
107
|
+
|
108
|
+
# Before filters
|
109
|
+
a_hash = self.send(:"before_#{meth}_filters").inject(a_hash) do |memo, filter|
|
110
|
+
filter.call(memo, output, options)
|
111
|
+
end
|
112
|
+
|
101
113
|
# Do the mapping
|
102
114
|
self.maps.each do |m|
|
103
|
-
m.process_into(output, a_hash, meth)
|
115
|
+
m.process_into(output, a_hash, method_name: meth, context: context)
|
116
|
+
end
|
117
|
+
|
118
|
+
# After filters
|
119
|
+
self.send(:"after_#{meth}_filters").inject(output) do |memo, filter|
|
120
|
+
filter.call(a_hash, memo, options)
|
104
121
|
end
|
105
|
-
# After filter
|
106
|
-
after_filter = instance_eval "@after_#{meth}"
|
107
|
-
output = after_filter.call(a_hash, output) if after_filter
|
108
|
-
# Return
|
109
|
-
output
|
110
122
|
end
|
111
123
|
|
112
124
|
# Contains PathMaps
|
@@ -120,32 +132,32 @@ module HashMapper
|
|
120
132
|
@path_from = path_from
|
121
133
|
@path_to = path_to
|
122
134
|
@delegated_mapper = options.fetch(:using, nil)
|
123
|
-
@default_value = options.fetch(:default,
|
135
|
+
@default_value = options.fetch(:default, NO_DEFAULT)
|
124
136
|
end
|
125
137
|
|
126
|
-
def process_into(output, input,
|
127
|
-
path_1, path_2 = (
|
128
|
-
value = get_value_from_input(output, input, path_1,
|
138
|
+
def process_into(output, input, method_name: :normalize, context: nil)
|
139
|
+
path_1, path_2 = (method_name == :normalize ? [path_from, path_to] : [path_to, path_from])
|
140
|
+
value = get_value_from_input(output, input, path_1, method_name: method_name, context: context)
|
129
141
|
set_value_in_output(output, path_2, value)
|
130
142
|
end
|
131
143
|
protected
|
132
144
|
|
133
|
-
def get_value_from_input(output, input, path,
|
145
|
+
def get_value_from_input(output, input, path, method_name:, context:)
|
134
146
|
value = path.inject(input) do |h,e|
|
135
147
|
if h.is_a?(Hash)
|
136
148
|
v = [h[e.to_sym], h[e.to_s]].compact.first
|
137
149
|
else
|
138
150
|
v = h[e]
|
139
151
|
end
|
140
|
-
return
|
152
|
+
return NO_VALUE if v.nil?
|
141
153
|
v
|
142
154
|
end
|
143
|
-
delegated_mapper ? delegate_to_nested_mapper(value,
|
155
|
+
delegated_mapper ? delegate_to_nested_mapper(value, method_name, context: context) : value
|
144
156
|
end
|
145
157
|
|
146
158
|
def set_value_in_output(output, path, value)
|
147
|
-
if value ==
|
148
|
-
if default_value ==
|
159
|
+
if value == NO_VALUE
|
160
|
+
if default_value == NO_DEFAULT
|
149
161
|
return
|
150
162
|
else
|
151
163
|
value = default_value
|
@@ -154,14 +166,14 @@ module HashMapper
|
|
154
166
|
add_value_to_hash!(output, path, value)
|
155
167
|
end
|
156
168
|
|
157
|
-
def delegate_to_nested_mapper(value,
|
169
|
+
def delegate_to_nested_mapper(value, method_name, context:)
|
158
170
|
case value
|
159
171
|
when Array
|
160
|
-
value.map {|
|
172
|
+
value.map {|v| delegated_mapper.public_send(method_name, v, context: context)}
|
161
173
|
when nil
|
162
|
-
return
|
174
|
+
return NO_VALUE
|
163
175
|
else
|
164
|
-
delegated_mapper.
|
176
|
+
delegated_mapper.public_send(method_name, value, context: context)
|
165
177
|
end
|
166
178
|
end
|
167
179
|
|
data/lib/hash_mapper/version.rb
CHANGED
data/spec/hash_mapper_spec.rb
CHANGED
@@ -6,24 +6,24 @@ class OneLevel
|
|
6
6
|
end
|
7
7
|
|
8
8
|
describe 'mapping a hash with one level' do
|
9
|
-
|
9
|
+
|
10
10
|
before :each do
|
11
|
-
@from = {:
|
12
|
-
@to = {:
|
11
|
+
@from = {name: 'ismael'}
|
12
|
+
@to = {nombre: 'ismael'}
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
it "should map to" do
|
16
|
-
OneLevel.normalize(@from).
|
16
|
+
expect(OneLevel.normalize(@from)).to eq(@to)
|
17
17
|
end
|
18
|
-
|
18
|
+
|
19
19
|
it "should have indifferent access" do
|
20
|
-
OneLevel.normalize({'name' => 'ismael'}).
|
20
|
+
expect(OneLevel.normalize({'name' => 'ismael'})).to eq(@to)
|
21
21
|
end
|
22
|
-
|
22
|
+
|
23
23
|
it "should map back the other way" do
|
24
|
-
OneLevel.denormalize(@to).
|
24
|
+
expect(OneLevel.denormalize(@to)).to eq(@from)
|
25
25
|
end
|
26
|
-
|
26
|
+
|
27
27
|
end
|
28
28
|
|
29
29
|
class ManyLevels
|
@@ -35,35 +35,35 @@ class ManyLevels
|
|
35
35
|
end
|
36
36
|
|
37
37
|
describe 'mapping from one nested hash to another' do
|
38
|
-
|
38
|
+
|
39
39
|
before :each do
|
40
40
|
@from = {
|
41
|
-
:
|
42
|
-
:
|
43
|
-
:
|
44
|
-
:
|
45
|
-
:
|
41
|
+
name: 'ismael',
|
42
|
+
tagid: 1,
|
43
|
+
properties: {
|
44
|
+
type: 'BLAH',
|
45
|
+
egg: 33
|
46
46
|
}
|
47
47
|
}
|
48
|
-
|
48
|
+
|
49
49
|
@to = {
|
50
|
-
:
|
51
|
-
:
|
52
|
-
:
|
53
|
-
:
|
54
|
-
:
|
50
|
+
tag_id: 1,
|
51
|
+
chicken: 33,
|
52
|
+
tag_attributes: {
|
53
|
+
name: 'ismael',
|
54
|
+
type: 'BLAH'
|
55
55
|
}
|
56
56
|
}
|
57
57
|
end
|
58
|
-
|
58
|
+
|
59
59
|
it "should map from and to different depths" do
|
60
|
-
ManyLevels.normalize(@from).
|
60
|
+
expect(ManyLevels.normalize(@from)).to eq(@to)
|
61
61
|
end
|
62
|
-
|
62
|
+
|
63
63
|
it "should map back the other way" do
|
64
|
-
ManyLevels.denormalize(@to).
|
64
|
+
expect(ManyLevels.denormalize(@to)).to eq(@from)
|
65
65
|
end
|
66
|
-
|
66
|
+
|
67
67
|
end
|
68
68
|
|
69
69
|
class DifferentTypes
|
@@ -73,53 +73,53 @@ class DifferentTypes
|
|
73
73
|
end
|
74
74
|
|
75
75
|
describe 'coercing types' do
|
76
|
-
|
76
|
+
|
77
77
|
before :each do
|
78
78
|
@from = {
|
79
|
-
:
|
80
|
-
:
|
79
|
+
strings: {a: '10'},
|
80
|
+
integers: {b: 20}
|
81
81
|
}
|
82
|
-
|
82
|
+
|
83
83
|
@to = {
|
84
|
-
:
|
85
|
-
:
|
84
|
+
integers: {a: 10},
|
85
|
+
strings: {b: '20'}
|
86
86
|
}
|
87
87
|
end
|
88
|
-
|
88
|
+
|
89
89
|
it "should coerce values to specified types" do
|
90
|
-
DifferentTypes.normalize(@from).
|
90
|
+
expect(DifferentTypes.normalize(@from)).to eq(@to)
|
91
91
|
end
|
92
|
-
|
92
|
+
|
93
93
|
it "should coerce the other way if specified" do
|
94
|
-
DifferentTypes.denormalize(@to).
|
94
|
+
expect(DifferentTypes.denormalize(@to)).to eq(@from)
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
end
|
98
98
|
|
99
99
|
|
100
100
|
describe 'arrays in hashes' do
|
101
101
|
before :each do
|
102
102
|
@from = {
|
103
|
-
:
|
104
|
-
:
|
105
|
-
:
|
106
|
-
:
|
107
|
-
:
|
103
|
+
name: ['ismael','sachiyo'],
|
104
|
+
tagid: 1,
|
105
|
+
properties: {
|
106
|
+
type: 'BLAH',
|
107
|
+
egg: 33
|
108
108
|
}
|
109
109
|
}
|
110
|
-
|
110
|
+
|
111
111
|
@to = {
|
112
|
-
:
|
113
|
-
:
|
114
|
-
:
|
115
|
-
:
|
116
|
-
:
|
112
|
+
tag_id: 1,
|
113
|
+
chicken: 33,
|
114
|
+
tag_attributes: {
|
115
|
+
name: ['ismael','sachiyo'],
|
116
|
+
type: 'BLAH'
|
117
117
|
}
|
118
118
|
}
|
119
119
|
end
|
120
|
-
|
120
|
+
|
121
121
|
it "should map array values as normal" do
|
122
|
-
ManyLevels.normalize(@from).
|
122
|
+
expect(ManyLevels.normalize(@from)).to eq(@to)
|
123
123
|
end
|
124
124
|
end
|
125
125
|
|
@@ -129,34 +129,34 @@ class WithArrays
|
|
129
129
|
map from('/arrays/names[1]'), to('/last_name')
|
130
130
|
map from('/arrays/company'), to('/work/company')
|
131
131
|
end
|
132
|
-
|
132
|
+
|
133
133
|
describe "array indexes" do
|
134
134
|
before :each do
|
135
135
|
@from = {
|
136
|
-
:
|
137
|
-
:
|
138
|
-
:
|
136
|
+
arrays: {
|
137
|
+
names: ['ismael','celis'],
|
138
|
+
company: 'New Bamboo'
|
139
139
|
}
|
140
140
|
}
|
141
|
-
@to ={
|
142
|
-
:
|
143
|
-
:
|
144
|
-
:
|
141
|
+
@to = {
|
142
|
+
first_name: 'ismael',
|
143
|
+
last_name: 'celis',
|
144
|
+
work: {company: 'New Bamboo'}
|
145
145
|
}
|
146
146
|
end
|
147
|
-
|
147
|
+
|
148
148
|
it "should extract defined array values" do
|
149
|
-
WithArrays.normalize(@from).
|
149
|
+
expect(WithArrays.normalize(@from)).to eq(@to)
|
150
150
|
end
|
151
|
-
|
151
|
+
|
152
152
|
it "should map the other way restoring arrays" do
|
153
|
-
WithArrays.denormalize(@to).
|
153
|
+
expect(WithArrays.denormalize(@to)).to eq(@from)
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
157
157
|
class PersonWithBlock
|
158
158
|
extend HashMapper
|
159
|
-
def self.normalize(
|
159
|
+
def self.normalize(*_)
|
160
160
|
super
|
161
161
|
end
|
162
162
|
map from('/names/first'){|n| n.gsub('+','')}, to('/first_name'){|n| "+++#{n}+++"}
|
@@ -166,64 +166,64 @@ class PersonWithBlockOneWay
|
|
166
166
|
map from('/names/first'), to('/first_name') do |n| "+++#{n}+++" end
|
167
167
|
end
|
168
168
|
|
169
|
-
describe "with
|
169
|
+
describe "with block filters" do
|
170
170
|
before :each do
|
171
171
|
@from = {
|
172
|
-
:
|
172
|
+
names: {first: 'Ismael'}
|
173
173
|
}
|
174
174
|
@to = {
|
175
|
-
:
|
175
|
+
first_name: '+++Ismael+++'
|
176
176
|
}
|
177
177
|
end
|
178
|
-
|
178
|
+
|
179
179
|
it "should pass final value through given block" do
|
180
|
-
PersonWithBlock.normalize(@from).
|
180
|
+
expect(PersonWithBlock.normalize(@from)).to eq(@to)
|
181
181
|
end
|
182
|
-
|
182
|
+
|
183
183
|
it "should be able to map the other way using a block" do
|
184
|
-
PersonWithBlock.denormalize(@to).
|
184
|
+
expect(PersonWithBlock.denormalize(@to)).to eq(@from)
|
185
185
|
end
|
186
|
-
|
186
|
+
|
187
187
|
it "should accept a block for just one direction" do
|
188
|
-
PersonWithBlockOneWay.normalize(@from).
|
188
|
+
expect(PersonWithBlockOneWay.normalize(@from)).to eq(@to)
|
189
189
|
end
|
190
|
-
|
190
|
+
|
191
191
|
end
|
192
192
|
|
193
193
|
class ProjectMapper
|
194
194
|
extend HashMapper
|
195
|
-
|
195
|
+
|
196
196
|
map from('/name'), to('/project_name')
|
197
|
-
map from('/author_hash'), to('/author'), using
|
197
|
+
map from('/author_hash'), to('/author'), using: PersonWithBlock
|
198
198
|
end
|
199
199
|
|
200
200
|
describe "with nested mapper" do
|
201
201
|
before :each do
|
202
202
|
@from ={
|
203
|
-
:
|
204
|
-
:
|
205
|
-
:
|
203
|
+
name: 'HashMapper',
|
204
|
+
author_hash: {
|
205
|
+
names: {first: 'Ismael'}
|
206
206
|
}
|
207
207
|
}
|
208
208
|
@to = {
|
209
|
-
:
|
210
|
-
:
|
209
|
+
project_name: 'HashMapper',
|
210
|
+
author: {first_name: '+++Ismael+++'}
|
211
211
|
}
|
212
212
|
end
|
213
|
-
|
213
|
+
|
214
214
|
it "should delegate nested hashes to another mapper" do
|
215
|
-
ProjectMapper.normalize(@from).
|
215
|
+
expect(ProjectMapper.normalize(@from)).to eq(@to)
|
216
216
|
end
|
217
|
-
|
217
|
+
|
218
218
|
it "should translate the other way using nested hashes" do
|
219
|
-
ProjectMapper.denormalize(@to).
|
219
|
+
expect(ProjectMapper.denormalize(@to)).to eq(@from)
|
220
220
|
end
|
221
|
-
|
221
|
+
|
222
222
|
end
|
223
223
|
|
224
224
|
class CompanyMapper
|
225
225
|
extend HashMapper
|
226
|
-
|
226
|
+
|
227
227
|
map from('/name'), to('/company_name')
|
228
228
|
map from('/employees'), to('/employees') do |employees_array|
|
229
229
|
employees_array.collect{|emp_hash| PersonWithBlock.normalize(emp_hash)}
|
@@ -232,82 +232,82 @@ end
|
|
232
232
|
|
233
233
|
class CompanyEmployeesMapper
|
234
234
|
extend HashMapper
|
235
|
-
|
235
|
+
|
236
236
|
map from('/name'), to('/company_name')
|
237
|
-
map from('/employees'), to('/employees'), using
|
237
|
+
map from('/employees'), to('/employees'), using: PersonWithBlock
|
238
238
|
end
|
239
239
|
|
240
240
|
describe "with arrays of nested hashes" do
|
241
241
|
before :each do
|
242
242
|
@from = {
|
243
|
-
:
|
244
|
-
:
|
245
|
-
{:
|
246
|
-
{:
|
247
|
-
{:
|
243
|
+
name: 'New Bamboo',
|
244
|
+
employees: [
|
245
|
+
{names: {first: 'Ismael'}},
|
246
|
+
{names: {first: 'Sachiyo'}},
|
247
|
+
{names: {first: 'Pedro'}}
|
248
248
|
]
|
249
249
|
}
|
250
250
|
@to = {
|
251
|
-
:
|
252
|
-
:
|
253
|
-
{:
|
254
|
-
{:
|
255
|
-
{:
|
251
|
+
company_name: 'New Bamboo',
|
252
|
+
employees: [
|
253
|
+
{first_name: '+++Ismael+++'},
|
254
|
+
{first_name: '+++Sachiyo+++'},
|
255
|
+
{first_name: '+++Pedro+++'}
|
256
256
|
]
|
257
257
|
}
|
258
258
|
end
|
259
|
-
|
259
|
+
|
260
260
|
it "should pass array value though given block mapper" do
|
261
|
-
CompanyMapper.normalize(@from).
|
261
|
+
expect(CompanyMapper.normalize(@from)).to eq(@to)
|
262
262
|
end
|
263
|
-
|
263
|
+
|
264
264
|
it "should map array elements automatically" do
|
265
|
-
CompanyEmployeesMapper.normalize(@from).
|
265
|
+
expect(CompanyEmployeesMapper.normalize(@from)).to eq(@to)
|
266
266
|
end
|
267
267
|
end
|
268
268
|
|
269
269
|
class NoKeys
|
270
270
|
extend HashMapper
|
271
|
-
|
271
|
+
|
272
272
|
map from('/exists'), to('/exists_yahoo') #in
|
273
273
|
map from('/exists_as_nil'), to('/exists_nil') #in
|
274
274
|
map from('/foo'), to('/bar') # not in
|
275
|
-
|
275
|
+
|
276
276
|
end
|
277
277
|
|
278
278
|
describe "with non-matching maps" do
|
279
279
|
before :all do
|
280
280
|
@input = {
|
281
|
-
:
|
282
|
-
:
|
283
|
-
:
|
281
|
+
exists: 1,
|
282
|
+
exists_as_nil: nil,
|
283
|
+
doesnt_exist: 2
|
284
284
|
}
|
285
285
|
@output = {
|
286
|
-
:
|
286
|
+
exists_yahoo: 1
|
287
287
|
}
|
288
288
|
end
|
289
|
-
|
289
|
+
|
290
290
|
it "should ignore maps that don't exist" do
|
291
|
-
NoKeys.normalize(@input).
|
291
|
+
expect(NoKeys.normalize(@input)).to eq(@output)
|
292
292
|
end
|
293
293
|
end
|
294
294
|
|
295
295
|
describe "with false values" do
|
296
|
-
|
296
|
+
|
297
297
|
it "should include values in output" do
|
298
|
-
NoKeys.normalize({'exists' => false}).
|
299
|
-
NoKeys.normalize({:
|
298
|
+
expect(NoKeys.normalize({'exists' => false})).to eq({exists_yahoo: false})
|
299
|
+
expect(NoKeys.normalize({exists: false})).to eq({exists_yahoo: false})
|
300
300
|
end
|
301
|
-
|
301
|
+
|
302
302
|
end
|
303
303
|
|
304
304
|
describe "with nil values" do
|
305
|
-
|
305
|
+
|
306
306
|
it "should not include values in output" do
|
307
|
-
NoKeys.normalize({:
|
308
|
-
NoKeys.normalize({'exists' => nil}).
|
307
|
+
expect(NoKeys.normalize({exists: nil})).to eq({})
|
308
|
+
expect(NoKeys.normalize({'exists' => nil})).to eq({})
|
309
309
|
end
|
310
|
-
|
310
|
+
|
311
311
|
end
|
312
312
|
|
313
313
|
class WithBeforeFilters
|
@@ -341,22 +341,22 @@ end
|
|
341
341
|
|
342
342
|
describe "before and after filters" do
|
343
343
|
before(:all) do
|
344
|
-
@denorm = {:
|
345
|
-
@norm = {:
|
344
|
+
@denorm = {hello: 'wassup?!'}
|
345
|
+
@norm = {goodbye: 'seeya later!'}
|
346
346
|
end
|
347
|
+
|
347
348
|
it "should allow filtering before normalize" do
|
348
|
-
WithBeforeFilters.normalize(@denorm).
|
349
|
+
expect(WithBeforeFilters.normalize(@denorm)).to eq({goodbye: 'wassup?!', extra: 'extra wassup?! innit'})
|
349
350
|
end
|
350
351
|
it "should allow filtering before denormalize" do
|
351
|
-
WithBeforeFilters.denormalize(@norm).
|
352
|
+
expect(WithBeforeFilters.denormalize(@norm)).to eq({hello: 'changed'})
|
352
353
|
end
|
353
354
|
it "should allow filtering after normalize" do
|
354
|
-
WithAfterFilters.normalize(@denorm).
|
355
|
+
expect(WithAfterFilters.normalize(@denorm)).to eq([[:goodbye, 'wassup?!']])
|
355
356
|
end
|
356
357
|
it "should allow filtering after denormalize" do
|
357
|
-
WithAfterFilters.denormalize(@norm).
|
358
|
+
expect(WithAfterFilters.denormalize(@norm)).to eq({})
|
358
359
|
end
|
359
|
-
|
360
360
|
end
|
361
361
|
|
362
362
|
class NotRelated
|
@@ -380,23 +380,23 @@ end
|
|
380
380
|
describe "inherited mappers" do
|
381
381
|
before :all do
|
382
382
|
@from = {
|
383
|
-
:
|
384
|
-
:
|
385
|
-
:
|
383
|
+
a: 'a',
|
384
|
+
b: 'b',
|
385
|
+
c: 'c'
|
386
386
|
}
|
387
387
|
@to_b ={
|
388
|
-
:
|
389
|
-
:
|
388
|
+
a: {a: 'a'},
|
389
|
+
b: {b: 'b'}
|
390
390
|
}
|
391
391
|
|
392
392
|
end
|
393
|
-
|
393
|
+
|
394
394
|
it "should inherit mappings" do
|
395
|
-
B.normalize(@from).
|
395
|
+
expect(B.normalize(@from)).to eq(@to_b)
|
396
396
|
end
|
397
|
-
|
397
|
+
|
398
398
|
it "should not affect other mappers" do
|
399
|
-
NotRelated.normalize('n' => 'nn').
|
399
|
+
expect(NotRelated.normalize('n' => 'nn')).to eq({n: {n: 'nn'}})
|
400
400
|
end
|
401
401
|
end
|
402
402
|
|
@@ -407,23 +407,21 @@ class MixedMappings
|
|
407
407
|
end
|
408
408
|
|
409
409
|
describe "dealing with strings and symbols" do
|
410
|
-
|
410
|
+
|
411
411
|
it "should be able to normalize from a nested hash with string keys" do
|
412
|
-
MixedMappings.normalize(
|
412
|
+
expect(MixedMappings.normalize(
|
413
413
|
'big' => {'jobs' => 5},
|
414
414
|
'timble' => 3.2
|
415
|
-
).
|
416
|
-
:bingo => {:biscuit => 3.2}}
|
415
|
+
)).to eq({dodo: 5, bingo: {biscuit: 3.2}})
|
417
416
|
end
|
418
|
-
|
417
|
+
|
419
418
|
it "should not symbolized keys in value hashes" do
|
420
|
-
MixedMappings.normalize(
|
419
|
+
expect(MixedMappings.normalize(
|
421
420
|
'big' => {'jobs' => 5},
|
422
421
|
'timble' => {'string key' => 'value'}
|
423
|
-
).
|
424
|
-
:bingo => {:biscuit => {'string key' => 'value'}}}
|
422
|
+
)).to eq({dodo: 5, bingo: {biscuit: {'string key' => 'value'}}})
|
425
423
|
end
|
426
|
-
|
424
|
+
|
427
425
|
end
|
428
426
|
|
429
427
|
class DefaultValues
|
@@ -435,15 +433,198 @@ end
|
|
435
433
|
|
436
434
|
describe "default values" do
|
437
435
|
it "should use a default value whenever a key is not set" do
|
438
|
-
DefaultValues.normalize(
|
436
|
+
expect(DefaultValues.normalize(
|
439
437
|
'without_default' => 'some_value'
|
440
|
-
).
|
438
|
+
)).to eq({ not_defaulted: 'some_value', defaulted: 'the_default_value' })
|
441
439
|
end
|
442
440
|
|
443
441
|
it "should not use a default if a key is set (even if the value is falsy)" do
|
444
|
-
DefaultValues.normalize({
|
442
|
+
expect(DefaultValues.normalize({
|
445
443
|
'without_default' => 'some_value',
|
446
444
|
'with_default' => false
|
447
|
-
}).
|
445
|
+
})).to eq({ not_defaulted: 'some_value', defaulted: false })
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
class MultiBeforeFilter
|
450
|
+
extend HashMapper
|
451
|
+
|
452
|
+
before_normalize do |input, output|
|
453
|
+
input[:foo] << 'Y'
|
454
|
+
input
|
455
|
+
end
|
456
|
+
|
457
|
+
before_normalize do |input, output|
|
458
|
+
input[:foo] << 'Z'
|
459
|
+
input
|
460
|
+
end
|
461
|
+
|
462
|
+
before_denormalize do |input, output|
|
463
|
+
input[:bar].prepend('A')
|
464
|
+
input
|
465
|
+
end
|
466
|
+
|
467
|
+
before_denormalize do |input, output|
|
468
|
+
input[:bar].prepend('B')
|
469
|
+
input
|
470
|
+
end
|
471
|
+
|
472
|
+
map from('/foo'), to('bar')
|
473
|
+
end
|
474
|
+
|
475
|
+
class MultiBeforeFilterSubclass < MultiBeforeFilter
|
476
|
+
before_normalize do |input, output|
|
477
|
+
input[:foo] << '!'
|
478
|
+
input
|
479
|
+
end
|
480
|
+
|
481
|
+
before_denormalize do |input, output|
|
482
|
+
input[:bar].prepend('C')
|
483
|
+
input
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
describe 'multiple before filters' do
|
488
|
+
it 'runs before_normalize filters in the order they are defined' do
|
489
|
+
expect(MultiBeforeFilter.normalize({ foo: 'X' })).to eq({ bar: 'XYZ' })
|
490
|
+
end
|
491
|
+
|
492
|
+
it 'runs before_denormalize filters in the order they are defined' do
|
493
|
+
expect(MultiBeforeFilter.denormalize({ bar: 'X' })).to eq({ foo: 'BAX' })
|
494
|
+
end
|
495
|
+
|
496
|
+
context 'when the filters are spread across classes' do
|
497
|
+
it 'runs before_normalize filters in the order they are defined' do
|
498
|
+
expect(MultiBeforeFilterSubclass.normalize({ foo: 'X' })).to eq({ bar: 'XYZ!' })
|
499
|
+
end
|
500
|
+
|
501
|
+
it 'runs before_denormalize filters in the order they are defined' do
|
502
|
+
expect(MultiBeforeFilterSubclass.denormalize({ bar: 'X' })).to eq({ foo: 'CBAX' })
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
class MultiAfterFilter
|
508
|
+
extend HashMapper
|
509
|
+
|
510
|
+
map from('/baz'), to('bat')
|
511
|
+
|
512
|
+
after_normalize do |input, output|
|
513
|
+
output[:bat] << '1'
|
514
|
+
output
|
515
|
+
end
|
516
|
+
|
517
|
+
after_normalize do |input, output|
|
518
|
+
output[:bat] << '2'
|
519
|
+
output
|
520
|
+
end
|
521
|
+
|
522
|
+
after_denormalize do |input, output|
|
523
|
+
output[:baz].prepend('9')
|
524
|
+
output
|
525
|
+
end
|
526
|
+
|
527
|
+
after_denormalize do |input, output|
|
528
|
+
output[:baz].prepend('8')
|
529
|
+
output
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
class MultiAfterFilterSubclass < MultiAfterFilter
|
534
|
+
after_normalize do |input, output|
|
535
|
+
output[:bat] << '3'
|
536
|
+
output
|
537
|
+
end
|
538
|
+
|
539
|
+
after_denormalize do |input, output|
|
540
|
+
output[:baz].prepend('7')
|
541
|
+
output
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
describe 'multiple after filters' do
|
546
|
+
it 'runs after_normalize filters in the order they are defined' do
|
547
|
+
expect(MultiAfterFilter.normalize({ baz: '0' })).to eq({ bat: '012' })
|
548
|
+
end
|
549
|
+
|
550
|
+
it 'runs after_denormalize filters in the order they are defined' do
|
551
|
+
expect(MultiAfterFilter.denormalize({ bat: '0' })).to eq({ baz: '890' })
|
552
|
+
end
|
553
|
+
|
554
|
+
context 'when the filters are spread across classes' do
|
555
|
+
it 'runs after_normalize filters in the order they are defined' do
|
556
|
+
expect(MultiAfterFilterSubclass.normalize({ baz: '0' })).to eq({ bat: '0123' })
|
557
|
+
end
|
558
|
+
|
559
|
+
it 'runs after_denormalize filters in the order they are defined' do
|
560
|
+
expect(MultiAfterFilterSubclass.denormalize({ bat: '0' })).to eq({ baz: '7890' })
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
class WithOptions
|
566
|
+
extend HashMapper
|
567
|
+
|
568
|
+
before_normalize do |input, output, opts|
|
569
|
+
output[:bn] = opts[:bn] if opts[:bn]
|
570
|
+
input
|
571
|
+
end
|
572
|
+
|
573
|
+
after_normalize do |input, output, opts|
|
574
|
+
output[:an] = opts[:an] if opts[:an]
|
575
|
+
output
|
576
|
+
end
|
577
|
+
|
578
|
+
before_denormalize do |input, output, opts|
|
579
|
+
output[:bdn] = opts[:bdn] if opts[:bdn]
|
580
|
+
input
|
581
|
+
end
|
582
|
+
|
583
|
+
after_denormalize do |input, output, opts|
|
584
|
+
output[:adn] = opts[:adn] if opts[:adn]
|
585
|
+
output
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
describe 'with options' do
|
590
|
+
context 'when called with options' do
|
591
|
+
it 'passes the options to all the filters' do
|
592
|
+
expect(WithOptions.normalize({}, options: { bn: 1, an: 2 })).to eq({bn: 1, an: 2})
|
593
|
+
expect(WithOptions.denormalize({}, options: { bdn: 1, adn: 2 })).to eq({bdn: 1, adn: 2})
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
context 'when called without options' do
|
598
|
+
it 'stills work' do
|
599
|
+
expect(WithOptions.normalize({})).to eq({})
|
600
|
+
expect(WithOptions.denormalize({})).to eq({})
|
601
|
+
end
|
602
|
+
end
|
603
|
+
end
|
604
|
+
|
605
|
+
describe 'passing custom context object' do
|
606
|
+
it 'passes context object down to sub-mappers' do
|
607
|
+
friend_mapper = Class.new do
|
608
|
+
extend HashMapper
|
609
|
+
|
610
|
+
map from('/name'), to('/name')
|
611
|
+
|
612
|
+
def normalize(input, context: , **kargs)
|
613
|
+
context[:names] ||= []
|
614
|
+
context[:names] << input[:name]
|
615
|
+
self.class.normalize(input, context: context, **kargs)
|
616
|
+
end
|
617
|
+
end
|
618
|
+
|
619
|
+
mapper = Class.new do
|
620
|
+
extend HashMapper
|
621
|
+
|
622
|
+
map from('/friends'), to('/friends'), using: friend_mapper.new
|
623
|
+
end
|
624
|
+
|
625
|
+
input = {friends: [{name: 'Ismael', last_name: 'Celis'}, {name: 'Joe'}]}
|
626
|
+
ctx = {}
|
627
|
+
mapper.normalize(input, context: ctx)
|
628
|
+
expect(ctx[:names]).to eq(%w(Ismael Joe))
|
448
629
|
end
|
449
630
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,55 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hash_mapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-09-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- -
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- -
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '4'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rspec
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- -
|
31
|
+
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '3.9'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- -
|
38
|
+
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '3.9'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- -
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
47
|
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- -
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
description: Tiny module that allows you to easily adapt from one hash structure to
|
@@ -59,7 +59,7 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
-
- .gitignore
|
62
|
+
- ".gitignore"
|
63
63
|
- Gemfile
|
64
64
|
- History.txt
|
65
65
|
- README.md
|
@@ -72,25 +72,24 @@ files:
|
|
72
72
|
homepage: http://github.com/ismasan/hash_mapper
|
73
73
|
licenses: []
|
74
74
|
metadata: {}
|
75
|
-
post_install_message:
|
75
|
+
post_install_message:
|
76
76
|
rdoc_options:
|
77
|
-
- --charset=UTF-8
|
77
|
+
- "--charset=UTF-8"
|
78
78
|
require_paths:
|
79
79
|
- lib
|
80
80
|
required_ruby_version: !ruby/object:Gem::Requirement
|
81
81
|
requirements:
|
82
|
-
- -
|
82
|
+
- - ">="
|
83
83
|
- !ruby/object:Gem::Version
|
84
84
|
version: '0'
|
85
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- -
|
87
|
+
- - ">="
|
88
88
|
- !ruby/object:Gem::Version
|
89
89
|
version: '0'
|
90
90
|
requirements: []
|
91
|
-
|
92
|
-
|
93
|
-
signing_key:
|
91
|
+
rubygems_version: 3.0.3
|
92
|
+
signing_key:
|
94
93
|
specification_version: 4
|
95
94
|
summary: Maps input hashes to a normalized format
|
96
95
|
test_files:
|