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.
@@ -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