ismasan-hash_mapper 0.0.2 → 0.0.3
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.
- data/README.rdoc +44 -18
- data/lib/hash_mapper.rb +80 -51
- data/spec/hash_mapper_spec.rb +69 -22
- metadata +2 -2
data/README.rdoc
CHANGED
@@ -31,7 +31,7 @@ It is a module so it doesn't get in the way of your inheritance tree.
|
|
31
31
|
}
|
32
32
|
}
|
33
33
|
|
34
|
-
ManyLevels.
|
34
|
+
ManyLevels.normalize(input)
|
35
35
|
|
36
36
|
# outputs:
|
37
37
|
{
|
@@ -47,7 +47,7 @@ It is a module so it doesn't get in the way of your inheritance tree.
|
|
47
47
|
|
48
48
|
HashMapper was primarily written as a way of mapping data structure in json requests to hashes with structures friendlier to our ActiveRecord models:
|
49
49
|
|
50
|
-
@article = Article.create( ArticleParams.
|
50
|
+
@article = Article.create( ArticleParams.normalize(params[:weird_article_data]) )
|
51
51
|
|
52
52
|
You can use HashMapper in your own little hash-like objects:
|
53
53
|
|
@@ -59,7 +59,7 @@ You can use HashMapper in your own little hash-like objects:
|
|
59
59
|
map from('/names/last'), to('/last_name')
|
60
60
|
|
61
61
|
def initialize(input_hash)
|
62
|
-
@hash = self.class.
|
62
|
+
@hash = self.class.normalize(input_hash)
|
63
63
|
end
|
64
64
|
|
65
65
|
def [](k)
|
@@ -87,10 +87,14 @@ You want to make sure an incoming value get converted to a certain type, so
|
|
87
87
|
|
88
88
|
Do this:
|
89
89
|
|
90
|
-
map from('/one'), to('/one',
|
91
|
-
map from('/two'), to('/two',
|
90
|
+
map from('/one'), to('/one', &:to_i)
|
91
|
+
map from('/two'), to('/two', &:to_i)
|
92
92
|
|
93
|
-
You can pass :to_i, :to_s or anything available method that makes sense.
|
93
|
+
You can pass :to_i, :to_s or anything available method that makes sense. Don't forget the block notation (&).
|
94
|
+
|
95
|
+
You guessed it. That means that you can actually pass custom blocks to each to() definition as well. The following is similar to the previous example:
|
96
|
+
|
97
|
+
map from('/one), to('/one'){|value| value.to_i}
|
94
98
|
|
95
99
|
==== Custom value filtering
|
96
100
|
|
@@ -104,17 +108,20 @@ Do this:
|
|
104
108
|
"Mr. #{names[1]}, #{names[0]}"
|
105
109
|
end
|
106
110
|
|
107
|
-
|
108
|
-
|
109
|
-
You want:
|
110
|
-
|
111
|
-
{:names => ['Ismael', 'Celis']} converted to {:first_name => 'Ismael', :last_name => 'Celis'}
|
111
|
+
=== Mapping in reverse
|
112
112
|
|
113
|
-
|
113
|
+
Cool, you can map one hash into another, but what if I want the opposite operation?
|
114
114
|
|
115
|
-
|
116
|
-
map from('/names[1]'), to('/last_name')
|
115
|
+
Just use the denormalize() method instead:
|
117
116
|
|
117
|
+
input = {:first => 'Mark', :last => 'Evans'}
|
118
|
+
|
119
|
+
output = NameMapper.normalize(input) # => {:first_name => 'Mark', :last_name => 'Evans'}
|
120
|
+
|
121
|
+
NameMapper.denormalize(output) # => input
|
122
|
+
|
123
|
+
This will work with your block filters and even nested mappers (see below).
|
124
|
+
|
118
125
|
=== Advanced usage
|
119
126
|
==== Nested mappers
|
120
127
|
|
@@ -145,17 +152,17 @@ Define an UserMapper separate from your ProjectMapper, so you reuse them combine
|
|
145
152
|
extend HashMapper
|
146
153
|
map from('/project'), to('/project_name')
|
147
154
|
map from('/url'), to('/url')
|
148
|
-
map from('/author_names'), to('/author'),
|
155
|
+
map from('/author_names'), to('/author'), using(UserMapper)
|
149
156
|
end
|
150
157
|
|
151
158
|
Now ProjectMapper will delegate parsing of :author_names to UserMapper
|
152
159
|
|
153
|
-
ProjectMapper.
|
160
|
+
ProjectMapper.normalize( input ) # => output
|
154
161
|
|
155
162
|
* Note the ampersand in &UserMapper. This is important if you are passing custom classes instead of procs.
|
156
163
|
* If you want to implement your own filter class just define to_proc in it.
|
157
164
|
|
158
|
-
Let's say you have a CompanyMapper which maps a hash with an array of employees, and you want to reuse UserMapper to map each employee:
|
165
|
+
Let's say you have a CompanyMapper which maps a hash with an array of employees, and you want to reuse UserMapper to map each employee. You could:
|
159
166
|
|
160
167
|
class CompanyMapper
|
161
168
|
map from('/info/name'), to('/company_name')
|
@@ -163,12 +170,31 @@ Let's say you have a CompanyMapper which maps a hash with an array of employees,
|
|
163
170
|
map from('/info/year_founded'), to('year_founded', :to_i)
|
164
171
|
|
165
172
|
map from('/employees'), to('employees') do |employees_array|
|
166
|
-
employees_array.collect {|emp_hash| UserMapper.
|
173
|
+
employees_array.collect {|emp_hash| UserMapper.normalize(emp_hash)}
|
167
174
|
end
|
168
175
|
end
|
169
176
|
|
177
|
+
But HashMapper's nested mappers will actually do that for you if a value is an array, so:
|
178
|
+
|
179
|
+
map from('/employees'), to('employees'), using(UserMapper)
|
180
|
+
|
181
|
+
... Will map each employee using UserMapper.
|
182
|
+
|
170
183
|
== REQUIREMENTS:
|
171
184
|
|
185
|
+
== TODO:
|
186
|
+
=== Array access
|
187
|
+
|
188
|
+
* See pending specs
|
189
|
+
|
190
|
+
You want:
|
191
|
+
|
192
|
+
{:names => ['Ismael', 'Celis']} converted to {:first_name => 'Ismael', :last_name => 'Celis'}
|
193
|
+
|
194
|
+
Do this:
|
195
|
+
|
196
|
+
map from('/names[0]'), to('/first_name')
|
197
|
+
map from('/names[1]'), to('/last_name')
|
172
198
|
|
173
199
|
== INSTALL:
|
174
200
|
|
data/lib/hash_mapper.rb
CHANGED
@@ -1,84 +1,124 @@
|
|
1
1
|
$:.unshift(File.dirname(__FILE__)) unless
|
2
2
|
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
3
|
|
4
|
+
# This allows us to call blah(&:some_method) instead of blah{|i| i.some_method }
|
5
|
+
unless Symbol.instance_methods.include?('to_proc')
|
6
|
+
class Symbol
|
7
|
+
def to_proc
|
8
|
+
Proc.new {|obj| obj.send(self) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
4
13
|
module HashMapper
|
5
|
-
VERSION = '0.0.
|
14
|
+
VERSION = '0.0.3'
|
6
15
|
|
7
16
|
def maps
|
8
17
|
@maps ||= []
|
9
18
|
end
|
10
19
|
|
11
|
-
def map(from, to, &
|
12
|
-
|
13
|
-
|
20
|
+
def map(from, to, using=nil, &filter)
|
21
|
+
self.maps << Map.new(from, to, using)
|
22
|
+
to.filter = filter if block_given? # Useful if just one block given
|
14
23
|
end
|
15
24
|
|
16
|
-
def from(path,
|
17
|
-
PathMap.new(path
|
25
|
+
def from(path, &filter)
|
26
|
+
path_map = PathMap.new(path)
|
27
|
+
path_map.filter = filter if block_given? # Useful if two blocks given
|
28
|
+
path_map
|
18
29
|
end
|
19
30
|
|
20
31
|
alias :to :from
|
21
32
|
|
22
|
-
def
|
33
|
+
def using(mapper_class)
|
34
|
+
mapper_class
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize(a_hash)
|
38
|
+
perform_hash_mapping a_hash, :normalize
|
39
|
+
end
|
40
|
+
|
41
|
+
def denormalize(a_hash)
|
42
|
+
perform_hash_mapping a_hash, :denormalize
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def perform_hash_mapping(a_hash, meth)
|
23
48
|
output = {}
|
24
|
-
|
25
|
-
maps.each do |
|
26
|
-
|
27
|
-
if h[e]
|
28
|
-
h[e]
|
29
|
-
else
|
30
|
-
h[e] = (e == path_to.last ? path_to.resolve_value(path_from, incoming_hash) : {})
|
31
|
-
end
|
32
|
-
}
|
49
|
+
a_hash = symbolize_keys(a_hash)
|
50
|
+
maps.each do |m|
|
51
|
+
m.process_into(output, a_hash, meth)
|
33
52
|
end
|
34
53
|
output
|
35
54
|
end
|
36
55
|
|
37
56
|
# from http://www.geekmade.co.uk/2008/09/ruby-tip-normalizing-hash-keys-as-symbols/
|
38
57
|
#
|
39
|
-
def
|
58
|
+
def symbolize_keys(hash)
|
40
59
|
hash.inject({}) do |options, (key, value)|
|
41
60
|
options[(key.to_sym rescue key) || key] = value
|
42
61
|
options
|
43
62
|
end
|
44
63
|
end
|
45
64
|
|
46
|
-
#
|
65
|
+
# Contains PathMaps
|
66
|
+
# Makes them interact
|
47
67
|
#
|
48
|
-
|
49
|
-
|
68
|
+
class Map
|
69
|
+
|
70
|
+
attr_reader :path_from, :path_to, :delegated_mapper
|
71
|
+
|
72
|
+
def initialize(path_from, path_to, delegated_mapper = nil)
|
73
|
+
@path_from, @path_to, @delegated_mapper = path_from, path_to, delegated_mapper
|
74
|
+
end
|
75
|
+
|
76
|
+
def process_into(output, incoming_hash, meth = :normalize)
|
77
|
+
paths = [path_from, path_to]
|
78
|
+
paths.reverse! unless meth == :normalize
|
79
|
+
value = paths.first.inject(incoming_hash){|h,e| h[e]}
|
80
|
+
value = delegate_to_nested_mapper(value, meth) if delegated_mapper
|
81
|
+
add_value_to_hash!(output, paths.last, value)
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
|
86
|
+
def delegate_to_nested_mapper(value, meth)
|
87
|
+
v = if value.kind_of?(Array)
|
88
|
+
value.map {|h| delegated_mapper.send(meth, h)}
|
89
|
+
else
|
90
|
+
delegated_mapper.send(meth, value)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_value_to_hash!(hash, path, value)
|
95
|
+
path.inject(hash) do |h,e|
|
96
|
+
if h[e]
|
97
|
+
h[e]
|
98
|
+
else
|
99
|
+
h[e] = (e == path.last ? path.apply_filter(value) : {})
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
50
104
|
end
|
51
105
|
|
106
|
+
# contains array of path segments
|
107
|
+
#
|
52
108
|
class PathMap
|
53
|
-
|
54
109
|
include Enumerable
|
55
110
|
|
56
111
|
attr_reader :segments
|
57
|
-
|
58
112
|
attr_writer :filter
|
59
113
|
|
60
|
-
def initialize(path
|
114
|
+
def initialize(path)
|
61
115
|
@path = path.dup
|
62
|
-
@coerce_method = coerce_method
|
63
|
-
@index = extract_array_index!(path)
|
64
116
|
@segments = parse(path)
|
65
117
|
@filter = lambda{|value| value}# default filter does nothing
|
66
118
|
end
|
67
119
|
|
68
|
-
def
|
69
|
-
|
70
|
-
end
|
71
|
-
|
72
|
-
def coerce(value)
|
73
|
-
value = @filter.call(value)
|
74
|
-
return value unless @coerce_method
|
75
|
-
value.send(@coerce_method) rescue value
|
76
|
-
end
|
77
|
-
|
78
|
-
def extract_from(incoming_hash)
|
79
|
-
value = inject(incoming_hash){|hh,ee| hh[ee]}
|
80
|
-
return value unless @index
|
81
|
-
value.to_a[@index]
|
120
|
+
def apply_filter(value)
|
121
|
+
@filter.call(value)
|
82
122
|
end
|
83
123
|
|
84
124
|
def each(&blk)
|
@@ -91,19 +131,8 @@ module HashMapper
|
|
91
131
|
|
92
132
|
private
|
93
133
|
|
94
|
-
def extract_array_index!(path)
|
95
|
-
path.gsub! /(\[[0-9]+\])/, ''
|
96
|
-
if idx = $1
|
97
|
-
idx.gsub(/(\[|\])/, '').to_i
|
98
|
-
else
|
99
|
-
nil
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
134
|
def parse(path)
|
104
|
-
|
105
|
-
p.shift
|
106
|
-
p.collect{|e| e.to_sym}
|
135
|
+
path.sub(/^\//,'').split('/').map(&:to_sym)
|
107
136
|
end
|
108
137
|
|
109
138
|
end
|
data/spec/hash_mapper_spec.rb
CHANGED
@@ -13,11 +13,15 @@ describe 'mapping a hash wit one level' do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it "should map to" do
|
16
|
-
OneLevel.
|
16
|
+
OneLevel.normalize(@from).should == @to
|
17
17
|
end
|
18
18
|
|
19
19
|
it "should have indifferent access" do
|
20
|
-
OneLevel.
|
20
|
+
OneLevel.normalize({'name' => 'ismael'}).should == @to
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should map back the other way" do
|
24
|
+
OneLevel.denormalize(@to).should == @from
|
21
25
|
end
|
22
26
|
|
23
27
|
end
|
@@ -53,15 +57,19 @@ describe 'mapping from one nested hash to another' do
|
|
53
57
|
end
|
54
58
|
|
55
59
|
it "should map from and to different depths" do
|
56
|
-
ManyLevels.
|
60
|
+
ManyLevels.normalize(@from).should == @to
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should map back the other way" do
|
64
|
+
ManyLevels.denormalize(@to).should == @from
|
57
65
|
end
|
58
66
|
|
59
67
|
end
|
60
68
|
|
61
69
|
class DifferentTypes
|
62
70
|
extend HashMapper
|
63
|
-
map from('/strings/a'), to('/integers/a'
|
64
|
-
map from('/integers/b'), to('/strings/b'
|
71
|
+
map from('/strings/a', &:to_s), to('/integers/a', &:to_i)
|
72
|
+
map from('/integers/b', &:to_i), to('/strings/b', &:to_s)
|
65
73
|
end
|
66
74
|
|
67
75
|
describe 'coercing types' do
|
@@ -79,7 +87,11 @@ describe 'coercing types' do
|
|
79
87
|
end
|
80
88
|
|
81
89
|
it "should coerce values to specified types" do
|
82
|
-
DifferentTypes.
|
90
|
+
DifferentTypes.normalize(@from).should == @to
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should coerce the other way if specified" do
|
94
|
+
DifferentTypes.denormalize(@to).should == @from
|
83
95
|
end
|
84
96
|
|
85
97
|
end
|
@@ -107,7 +119,7 @@ describe 'arrays in hashes' do
|
|
107
119
|
end
|
108
120
|
|
109
121
|
it "should map array values as normal" do
|
110
|
-
ManyLevels.
|
122
|
+
ManyLevels.normalize(@from).should == @to
|
111
123
|
end
|
112
124
|
end
|
113
125
|
|
@@ -117,7 +129,7 @@ class WithArrays
|
|
117
129
|
map from('/arrays/names[1]'), to('/last_name')
|
118
130
|
map from('/arrays/company'), to('/work/company')
|
119
131
|
end
|
120
|
-
|
132
|
+
|
121
133
|
describe "array indexes" do
|
122
134
|
before :each do
|
123
135
|
@from = {
|
@@ -134,16 +146,26 @@ describe "array indexes" do
|
|
134
146
|
end
|
135
147
|
|
136
148
|
it "should extract defined array values" do
|
137
|
-
|
149
|
+
pending "must reimplement for normalize and denormalize"
|
150
|
+
WithArrays.normalize(@from).should == @to
|
151
|
+
end
|
152
|
+
|
153
|
+
it "should map the other way restoring arrays" do
|
154
|
+
pending "must reimplement for normalize and denormalize"
|
155
|
+
WithArrays.denormalize(@from).should == @to
|
138
156
|
end
|
139
157
|
end
|
140
158
|
|
141
159
|
class PersonWithBlock
|
142
160
|
extend HashMapper
|
143
|
-
|
144
|
-
|
145
|
-
"+++ #{name} +++"
|
161
|
+
def self.normalize(h)
|
162
|
+
super
|
146
163
|
end
|
164
|
+
map from('/names/first'){|n| n.gsub('+','')}, to('/first_name'){|n| "+++#{n}+++"}
|
165
|
+
end
|
166
|
+
class PersonWithBlockOneWay
|
167
|
+
extend HashMapper
|
168
|
+
map from('/names/first'), to('/first_name') do |n| "+++#{n}+++" end
|
147
169
|
end
|
148
170
|
|
149
171
|
describe "with blocks filters" do
|
@@ -152,20 +174,29 @@ describe "with blocks filters" do
|
|
152
174
|
:names => {:first => 'Ismael'}
|
153
175
|
}
|
154
176
|
@to = {
|
155
|
-
:first_name => '+++
|
177
|
+
:first_name => '+++Ismael+++'
|
156
178
|
}
|
157
179
|
end
|
158
180
|
|
159
181
|
it "should pass final value through given block" do
|
160
|
-
PersonWithBlock.
|
182
|
+
PersonWithBlock.normalize(@from).should == @to
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should be able to map the other way using a block" do
|
186
|
+
PersonWithBlock.denormalize(@to).should == @from
|
161
187
|
end
|
188
|
+
|
189
|
+
it "should accept a block for just one direction" do
|
190
|
+
PersonWithBlockOneWay.normalize(@from).should == @to
|
191
|
+
end
|
192
|
+
|
162
193
|
end
|
163
194
|
|
164
195
|
class ProjectMapper
|
165
196
|
extend HashMapper
|
166
197
|
|
167
198
|
map from('/name'), to('/project_name')
|
168
|
-
map from('/author_hash'), to('/author'),
|
199
|
+
map from('/author_hash'), to('/author'), using(PersonWithBlock)
|
169
200
|
end
|
170
201
|
|
171
202
|
describe "with nested mapper" do
|
@@ -178,13 +209,18 @@ describe "with nested mapper" do
|
|
178
209
|
}
|
179
210
|
@to = {
|
180
211
|
:project_name => 'HashMapper',
|
181
|
-
:author => {:first_name => '+++
|
212
|
+
:author => {:first_name => '+++Ismael+++'}
|
182
213
|
}
|
183
214
|
end
|
184
215
|
|
185
216
|
it "should delegate nested hashes to another mapper" do
|
186
|
-
ProjectMapper.
|
217
|
+
ProjectMapper.normalize(@from).should == @to
|
187
218
|
end
|
219
|
+
|
220
|
+
it "should translate the other way using nested hashes" do
|
221
|
+
ProjectMapper.denormalize(@to).should == @from
|
222
|
+
end
|
223
|
+
|
188
224
|
end
|
189
225
|
|
190
226
|
class CompanyMapper
|
@@ -192,10 +228,17 @@ class CompanyMapper
|
|
192
228
|
|
193
229
|
map from('/name'), to('/company_name')
|
194
230
|
map from('/employees'), to('/employees') do |employees_array|
|
195
|
-
employees_array.collect{|emp_hash| PersonWithBlock.
|
231
|
+
employees_array.collect{|emp_hash| PersonWithBlock.normalize(emp_hash)}
|
196
232
|
end
|
197
233
|
end
|
198
234
|
|
235
|
+
class CompanyEmployeesMapper
|
236
|
+
extend HashMapper
|
237
|
+
|
238
|
+
map from('/name'), to('/company_name')
|
239
|
+
map from('/employees'), to('/employees'), using(PersonWithBlock)
|
240
|
+
end
|
241
|
+
|
199
242
|
describe "with arrays of nested hashes" do
|
200
243
|
before :each do
|
201
244
|
@from = {
|
@@ -209,15 +252,19 @@ describe "with arrays of nested hashes" do
|
|
209
252
|
@to = {
|
210
253
|
:company_name => 'New Bamboo',
|
211
254
|
:employees => [
|
212
|
-
{:first_name => '+++
|
213
|
-
{:first_name => '+++
|
214
|
-
{:first_name => '+++
|
255
|
+
{:first_name => '+++Ismael+++'},
|
256
|
+
{:first_name => '+++Sachiyo+++'},
|
257
|
+
{:first_name => '+++Pedro+++'}
|
215
258
|
]
|
216
259
|
}
|
217
260
|
end
|
218
261
|
|
219
262
|
it "should pass array value though given block mapper" do
|
220
|
-
CompanyMapper.
|
263
|
+
CompanyMapper.normalize(@from).should == @to
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should map array elements automatically" do
|
267
|
+
CompanyEmployeesMapper.normalize(@from).should == @to
|
221
268
|
end
|
222
269
|
end
|
223
270
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ismasan-hash_mapper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ismael Celis
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-02-03 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|