ismasan-hash_mapper 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.translate(input)
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.translate(params[:weird_article_data]) )
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.translate(input_hash)
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', :to_i)
91
- map from('/two'), to('/two', :to_i)
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
- ==== Array access
108
-
109
- You want:
110
-
111
- {:names => ['Ismael', 'Celis']} converted to {:first_name => 'Ismael', :last_name => 'Celis'}
111
+ === Mapping in reverse
112
112
 
113
- Do this:
113
+ Cool, you can map one hash into another, but what if I want the opposite operation?
114
114
 
115
- map from('/names[0]'), to('/first_name')
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'), &UserMapper
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.translate( input ) # => output
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.translate(emp_hash)}
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
 
@@ -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.2'
14
+ VERSION = '0.0.3'
6
15
 
7
16
  def maps
8
17
  @maps ||= []
9
18
  end
10
19
 
11
- def map(from, to, &blk)
12
- to.filter = blk if block_given?
13
- self.maps << [from, to]
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, coerce_method = nil)
17
- PathMap.new(path, coerce_method)
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 translate(incoming_hash)
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
- incoming_hash = simbolize_keys(incoming_hash)
25
- maps.each do |path_from, path_to|
26
- path_to.inject(output){|h,e|
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 simbolize_keys(hash)
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
- # This allows us to pass mapper classes as block arguments
65
+ # Contains PathMaps
66
+ # Makes them interact
47
67
  #
48
- def to_proc
49
- Proc.new{|*args| self.translate(*args)}
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, coerce_method = nil)
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 resolve_value(another_path, incoming_hash)
69
- coerce another_path.extract_from(incoming_hash)
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
- p = path.split('/')
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
@@ -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.translate(@from).should == @to
16
+ OneLevel.normalize(@from).should == @to
17
17
  end
18
18
 
19
19
  it "should have indifferent access" do
20
- OneLevel.translate({'name' => 'ismael'}).should == @to
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.translate(@from).should == @to
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',:to_i)
64
- map from('/integers/b'), to('/strings/b',:to_s)
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.translate(@from).should == @to
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.translate(@from).should == @to
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
- WithArrays.translate(@from).should == @to
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
- map from('/names/first'), to('/first_name') do |name|
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 => '+++ Ismael +++'
177
+ :first_name => '+++Ismael+++'
156
178
  }
157
179
  end
158
180
 
159
181
  it "should pass final value through given block" do
160
- PersonWithBlock.translate(@from).should == @to
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'), &PersonWithBlock
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 => '+++ Ismael +++'}
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.translate(@from).should == @to
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.translate(emp_hash)}
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 => '+++ Ismael +++'},
213
- {:first_name => '+++ Sachiyo +++'},
214
- {:first_name => '+++ Pedro +++'}
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.translate(@from).should == @to
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.2
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-01-31 00:00:00 -08:00
12
+ date: 2009-02-03 00:00:00 -08:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency