newbamboo-hash_mapper 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-01-29
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/Manifest.txt ADDED
@@ -0,0 +1,13 @@
1
+ History.txt
2
+ Manifest.txt
3
+ PostInstall.txt
4
+ README.rdoc
5
+ Rakefile
6
+ lib/hash_mapper.rb
7
+ script/console
8
+ script/destroy
9
+ script/generate
10
+ spec/hash_mapper_spec.rb
11
+ spec/spec.opts
12
+ spec/spec_helper.rb
13
+ tasks/rspec.rake
data/PostInstall.txt ADDED
@@ -0,0 +1,7 @@
1
+
2
+ For more information on hash_mapper, see http://hash_mapper.rubyforge.org
3
+
4
+ NOTE: Change this information in PostInstall.txt
5
+ You can also delete it if you don't want it.
6
+
7
+
data/README.rdoc ADDED
@@ -0,0 +1,261 @@
1
+ = hash_mapper
2
+
3
+ * http://github.com/ismasan/hash_mapper
4
+
5
+ == DESCRIPTION:
6
+
7
+ 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
+
9
+ Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.
10
+
11
+ == FEATURES/PROBLEMS:
12
+
13
+ It is a module so it doesn't get in the way of your inheritance tree.
14
+
15
+ == SYNOPSIS:
16
+
17
+ class ManyLevels
18
+ extend HashMapper
19
+ map from('/name'), to('/tag_attributes/name')
20
+ map from('/properties/type'), to('/tag_attributes/type')
21
+ map from('/tagid'), to('/tag_id')
22
+ map from('/properties/egg'), to('/chicken')
23
+ end
24
+
25
+ input = {
26
+ :name => 'ismael',
27
+ :tagid => 1,
28
+ :properties => {
29
+ :type => 'BLAH',
30
+ :egg => 33
31
+ }
32
+ }
33
+
34
+ ManyLevels.normalize(input)
35
+
36
+ # outputs:
37
+ {
38
+ :tag_id => 1,
39
+ :chicken => 33,
40
+ :tag_attributes => {
41
+ :name => 'ismael',
42
+ :type => 'BLAH'
43
+ }
44
+ }
45
+
46
+ === Uses:
47
+
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
+
50
+ @article = Article.create( ArticleParams.normalize(params[:weird_article_data]) )
51
+
52
+ You can use HashMapper in your own little hash-like objects:
53
+
54
+ class NiceHash
55
+ include Enumerable
56
+ extend HashMap
57
+
58
+ map from('/names/first'), to('/first_name')
59
+ map from('/names/last'), to('/last_name')
60
+
61
+ def initialize(input_hash)
62
+ @hash = self.class.normalize(input_hash)
63
+ end
64
+
65
+ def [](k)
66
+ @hash[k]
67
+ end
68
+
69
+ def []=(k,v)
70
+ @hash[k] = v
71
+ end
72
+
73
+ def each(&block)
74
+ @hash.each(&block)
75
+ end
76
+ end
77
+
78
+ @user = User.new(NiceHash.new(params))
79
+
80
+ === Options:
81
+
82
+ ==== Coercing values
83
+
84
+ You want to make sure an incoming value get converted to a certain type, so
85
+
86
+ {'one' => '1', 'two' => '2'} gets translated to {:one => 1, :two => 2}
87
+
88
+ Do this:
89
+
90
+ map from('/one'), to('/one', &:to_i)
91
+ map from('/two'), to('/two', &:to_i)
92
+
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}
98
+
99
+ ==== Custom value filtering
100
+
101
+ You want to pass the final value of a key through a custom filter:
102
+
103
+ {:names => {:first => 'Ismael', :last => 'Celis'}} gets translated to {:user => 'Mr. Celis, Ismael'}
104
+
105
+ Do this:
106
+
107
+ map from('/names'), to('/user') do |names|
108
+ "Mr. #{names[1]}, #{names[0]}"
109
+ end
110
+
111
+ === Mapping in reverse
112
+
113
+ Cool, you can map one hash into another, but what if I want the opposite operation?
114
+
115
+ Just use the denormalize() method instead:
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
+
125
+ === Advanced usage
126
+ ==== Array access
127
+ You want:
128
+
129
+ {:names => ['Ismael', 'Celis']} converted to {:first_name => 'Ismael', :last_name => 'Celis'}
130
+
131
+ Do this:
132
+
133
+ map from('/names[0]'), to('/first_name')
134
+ map from('/names[1]'), to('/last_name')
135
+ ==== Nested mappers
136
+
137
+ You want to map nested structures delegating to different mappers:
138
+
139
+ From this:
140
+ input = {
141
+ :project => 'HashMapper',
142
+ :url => 'http://github.com/ismasan/hash_mapper',
143
+ :author_names => {:first => 'Ismael', :last => 'Celis'}
144
+ }
145
+ To this:
146
+ output = {
147
+ :project_name => 'HashMapper',
148
+ :url => 'http://github.com/ismasan/hash_mapper',
149
+ :author => {:first_name => 'Ismael', :last_name => 'Celis'}
150
+ }
151
+
152
+ Define an UserMapper separate from your ProjectMapper, so you reuse them combined or standalone
153
+
154
+ class UserMapper
155
+ extend HashMapper
156
+ map from('/first'), to('/first_name')
157
+ map from('/last'), to('/lastt_name')
158
+ end
159
+
160
+ class ProjectMapper
161
+ extend HashMapper
162
+ map from('/project'), to('/project_name')
163
+ map from('/url'), to('/url')
164
+ map from('/author_names'), to('/author'), using(UserMapper)
165
+ end
166
+
167
+ Now ProjectMapper will delegate parsing of :author_names to UserMapper
168
+
169
+ ProjectMapper.normalize( input ) # => output
170
+
171
+ * Note the ampersand in &UserMapper. This is important if you are passing custom classes instead of procs.
172
+ * If you want to implement your own filter class just define to_proc in it.
173
+
174
+ 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:
175
+
176
+ class CompanyMapper
177
+ map from('/info/name'), to('/company_name')
178
+ map form('/info/address'), to('/company_address')
179
+ map from('/info/year_founded'), to('year_founded', :to_i)
180
+
181
+ map from('/employees'), to('employees') do |employees_array|
182
+ employees_array.collect {|emp_hash| UserMapper.normalize(emp_hash)}
183
+ end
184
+ end
185
+
186
+ But HashMapper's nested mappers will actually do that for you if a value is an array, so:
187
+
188
+ map from('/employees'), to('employees'), using(UserMapper)
189
+
190
+ ... Will map each employee using UserMapper.
191
+
192
+ ==== Before and after filters
193
+
194
+ Sometimes you will need some slightly more complex processing on the whole hash, either before or after normalizing/denormalizing.
195
+
196
+ For this you can use the class methods before_normalize, before_denormalize, after_normalize and after_denormalize.
197
+
198
+ They all yield a block with 2 arguments - the hash you are mapping from and the hash you are mapping to, e.g.
199
+
200
+ class EggMapper
201
+ map from('/raw'), to('/fried')
202
+
203
+ before_normalize do |input, output|
204
+ output[:time] = Time.now # the normalized hash will now be {:fried => 'blah', :time => <time object> }
205
+ output
206
+ end
207
+
208
+ after_denormalize do |input, output|
209
+ output.to_a # the denormalized object will now be an array, not a hash!!
210
+ end
211
+
212
+ end
213
+
214
+ Important: note that you need to return the output each time.
215
+ 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.
216
+
217
+
218
+ == REQUIREMENTS:
219
+
220
+ == TODO:
221
+
222
+ == INSTALL:
223
+
224
+ # If you haven't done this already:
225
+
226
+ gem sources -a http://gems.github.com
227
+
228
+ # Now install
229
+
230
+ sudo gem install ismasan-hash_mapper
231
+
232
+ == Credits:
233
+
234
+ * Ismael Celis (Author - http://www.estadobeta.com)
235
+ * Mark Evans (Contributor - http://github.com/markevans)
236
+ * Jdeveloper (Contributor - http://github.com/jdeveloper)
237
+
238
+ == LICENSE:
239
+
240
+ (The MIT License)
241
+
242
+ Copyright (c) 2009 Ismael Celis
243
+
244
+ Permission is hereby granted, free of charge, to any person obtaining
245
+ a copy of this software and associated documentation files (the
246
+ 'Software'), to deal in the Software without restriction, including
247
+ without limitation the rights to use, copy, modify, merge, publish,
248
+ distribute, sublicense, and/or sell copies of the Software, and to
249
+ permit persons to whom the Software is furnished to do so, subject to
250
+ the following conditions:
251
+
252
+ The above copyright notice and this permission notice shall be
253
+ included in all copies or substantial portions of the Software.
254
+
255
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
256
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
257
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
258
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
259
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
260
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
261
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/hash_mapper'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('hash_mapper', HashMapper::VERSION) do |p|
7
+ p.developer('Ismael Celis', 'ismaelct@gmail.com')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.post_install_message = 'PostInstall.txt' # TODO remove if post-install message not required
10
+ p.rubyforge_name = p.name # TODO this is default value
11
+ # p.extra_deps = [
12
+ # ['activesupport','>= 2.0.2'],
13
+ # ]
14
+ p.extra_dev_deps = [
15
+ ['newgem', ">= #{::Newgem::VERSION}"]
16
+ ]
17
+
18
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
19
+ path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
20
+ p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
21
+ p.rsync_args = '-av --delete --ignore-errors'
22
+ end
23
+
24
+ require 'newgem/tasks' # load /tasks/*.rake
25
+ Dir['tasks/**/*.rake'].each { |t| load t }
26
+
27
+ # TODO - want other tests/tasks run by default? Add them to the list
28
+ # task :default => [:spec, :features]
@@ -0,0 +1,197 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
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
+
13
+ module HashMapper
14
+ VERSION = '0.0.5'
15
+
16
+ def maps
17
+ @maps ||= []
18
+ end
19
+
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
23
+ end
24
+
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
29
+ end
30
+
31
+ alias :to :from
32
+
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
+ def before_normalize(&blk)
46
+ @before_normalize = blk
47
+ end
48
+
49
+ def before_denormalize(&blk)
50
+ @before_denormalize = blk
51
+ end
52
+
53
+ def after_normalize(&blk)
54
+ @after_normalize = blk
55
+ end
56
+
57
+ def after_denormalize(&blk)
58
+ @after_denormalize = blk
59
+ end
60
+
61
+ protected
62
+
63
+ def perform_hash_mapping(a_hash, meth)
64
+ output = {}
65
+ # Before filter
66
+ before_filter = instance_eval "@before_#{meth}"
67
+ output = before_filter.call(a_hash, output) if before_filter
68
+ # Do the mapping
69
+ a_hash = symbolize_keys(a_hash)
70
+ maps.each do |m|
71
+ m.process_into(output, a_hash, meth)
72
+ end
73
+ # After filter
74
+ after_filter = instance_eval "@after_#{meth}"
75
+ output = after_filter.call(a_hash, output) if after_filter
76
+ # Return
77
+ output
78
+ end
79
+
80
+ # from http://www.geekmade.co.uk/2008/09/ruby-tip-normalizing-hash-keys-as-symbols/
81
+ #
82
+ def symbolize_keys(hash)
83
+ hash.inject({}) do |options, (key, value)|
84
+ options[(key.to_sym rescue key) || key] = value
85
+ options
86
+ end
87
+ end
88
+
89
+ # Contains PathMaps
90
+ # Makes them interact
91
+ #
92
+ class Map
93
+
94
+ attr_reader :path_from, :path_to, :delegated_mapper
95
+
96
+ def initialize(path_from, path_to, delegated_mapper = nil)
97
+ @path_from, @path_to, @delegated_mapper = path_from, path_to, delegated_mapper
98
+ end
99
+
100
+ def process_into(output, input, meth = :normalize)
101
+ path_1, path_2 = (meth == :normalize ? [path_from, path_to] : [path_to, path_from])
102
+ catch :no_value do
103
+ value = get_value_from_input(output, input, path_1, meth)
104
+ add_value_to_hash!(output, path_2, value)
105
+ end
106
+ end
107
+
108
+ protected
109
+
110
+ def get_value_from_input(output, input, path, meth)
111
+ value = path.inject(input) do |h,e|
112
+ throw :no_value unless h.has_key?(e[0].to_sym)
113
+ e[1].nil? ? h[e[0].to_sym] : h[e[0].to_sym][e[1].to_i]
114
+ #h[e[0].to_sym]
115
+ end
116
+ value = delegate_to_nested_mapper(value, meth) if delegated_mapper
117
+ value
118
+ end
119
+
120
+
121
+ def delegate_to_nested_mapper(value, meth)
122
+ v = if value.kind_of?(Array)
123
+ value.map {|h| delegated_mapper.send(meth, h)}
124
+ else
125
+ delegated_mapper.send(meth, value)
126
+ end
127
+ end
128
+
129
+ def add_value_to_hash!(hash, path, value)
130
+ path.inject(hash) do |h,e|
131
+ if contained?(h,e)
132
+ if e[1].nil?
133
+ h[e[0].to_sym]
134
+ else
135
+ if e == path.last
136
+ h[e[0].to_sym][e[1].to_i] = value
137
+ end
138
+ h[e[0].to_sym][e[1].to_i]
139
+ end
140
+ else
141
+ if e[1].nil?
142
+ h[e[0].to_sym] = (e == path.last ? path.apply_filter(value) : {})
143
+ else
144
+ h[e[0].to_sym] = []
145
+ h[e[0].to_sym][e[1].to_i] = (e == path.last ? path.apply_filter(value) : {})
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def contained?(h,e)
152
+ e[1].nil? ? h[e[0].to_sym] : h[e[0].to_sym][e[1].to_i].nil?
153
+ rescue
154
+ false
155
+ end
156
+ end
157
+
158
+ # contains array of path segments
159
+ #
160
+ class PathMap
161
+ include Enumerable
162
+
163
+ attr_reader :segments
164
+ attr_writer :filter
165
+
166
+ def initialize(path)
167
+ @path = path.dup
168
+ @segments = parse(path)
169
+ @filter = lambda{|value| value}# default filter does nothing
170
+ end
171
+
172
+ def apply_filter(value)
173
+ @filter.call(value)
174
+ end
175
+
176
+ def each(&blk)
177
+ @segments.each(&blk)
178
+ end
179
+
180
+ def last
181
+ @segments.last
182
+ end
183
+
184
+ private
185
+
186
+ def parse(path)
187
+ #path.sub(/^\//,'').split('/').map(&:to_sym)
188
+ path.sub(/^\//,'').split('/').map{ |p| key_index p }
189
+ end
190
+
191
+ def key_index(p)
192
+ p =~ /\[[0-9]+\]$/ ? p.sub(/\[([0-9]+)\]$/,' \1').split(' ') : [p,nil]
193
+ end
194
+
195
+ end
196
+
197
+ end
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/hash_mapper.rb'}"
9
+ puts "Loading hash_mapper gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,342 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ class OneLevel
4
+ extend HashMapper
5
+ map from('/name'), to('/nombre')
6
+ end
7
+
8
+ describe 'mapping a hash wit one level' do
9
+
10
+ before :each do
11
+ @from = {:name => 'ismael'}
12
+ @to = {:nombre => 'ismael'}
13
+ end
14
+
15
+ it "should map to" do
16
+ OneLevel.normalize(@from).should == @to
17
+ end
18
+
19
+ it "should have indifferent access" do
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
25
+ end
26
+
27
+ end
28
+
29
+ class ManyLevels
30
+ extend HashMapper
31
+ map from('/name'), to('/tag_attributes/name')
32
+ map from('/properties/type'), to('/tag_attributes/type')
33
+ map from('/tagid'), to('/tag_id')
34
+ map from('/properties/egg'), to('/chicken')
35
+ end
36
+
37
+ describe 'mapping from one nested hash to another' do
38
+
39
+ before :each do
40
+ @from = {
41
+ :name => 'ismael',
42
+ :tagid => 1,
43
+ :properties => {
44
+ :type => 'BLAH',
45
+ :egg => 33
46
+ }
47
+ }
48
+
49
+ @to = {
50
+ :tag_id => 1,
51
+ :chicken => 33,
52
+ :tag_attributes => {
53
+ :name => 'ismael',
54
+ :type => 'BLAH'
55
+ }
56
+ }
57
+ end
58
+
59
+ it "should map from and to different depths" do
60
+ ManyLevels.normalize(@from).should == @to
61
+ end
62
+
63
+ it "should map back the other way" do
64
+ ManyLevels.denormalize(@to).should == @from
65
+ end
66
+
67
+ end
68
+
69
+ class DifferentTypes
70
+ extend HashMapper
71
+ map from('/strings/a', &:to_s), to('/integers/a', &:to_i)
72
+ map from('/integers/b', &:to_i), to('/strings/b', &:to_s)
73
+ end
74
+
75
+ describe 'coercing types' do
76
+
77
+ before :each do
78
+ @from = {
79
+ :strings => {:a => '10'},
80
+ :integers =>{:b => 20}
81
+ }
82
+
83
+ @to = {
84
+ :integers => {:a => 10},
85
+ :strings => {:b => '20'}
86
+ }
87
+ end
88
+
89
+ it "should coerce values to specified types" do
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
95
+ end
96
+
97
+ end
98
+
99
+
100
+ describe 'arrays in hashes' do
101
+ before :each do
102
+ @from = {
103
+ :name => ['ismael','sachiyo'],
104
+ :tagid => 1,
105
+ :properties => {
106
+ :type => 'BLAH',
107
+ :egg => 33
108
+ }
109
+ }
110
+
111
+ @to = {
112
+ :tag_id => 1,
113
+ :chicken => 33,
114
+ :tag_attributes => {
115
+ :name => ['ismael','sachiyo'],
116
+ :type => 'BLAH'
117
+ }
118
+ }
119
+ end
120
+
121
+ it "should map array values as normal" do
122
+ ManyLevels.normalize(@from).should == @to
123
+ end
124
+ end
125
+
126
+ class WithArrays
127
+ extend HashMapper
128
+ map from('/arrays/names[0]'), to('/first_name')
129
+ map from('/arrays/names[1]'), to('/last_name')
130
+ map from('/arrays/company'), to('/work/company')
131
+ end
132
+
133
+ describe "array indexes" do
134
+ before :each do
135
+ @from = {
136
+ :arrays => {
137
+ :names => ['ismael','celis'],
138
+ :company => 'New Bamboo'
139
+ }
140
+ }
141
+ @to ={
142
+ :first_name => 'ismael',
143
+ :last_name => 'celis',
144
+ :work => {:company => 'New Bamboo'}
145
+ }
146
+ end
147
+
148
+ it "should extract defined array values" do
149
+ WithArrays.normalize(@from).should == @to
150
+ end
151
+
152
+ it "should map the other way restoring arrays" do
153
+ WithArrays.denormalize(@to).should == @from
154
+ end
155
+ end
156
+
157
+ class PersonWithBlock
158
+ extend HashMapper
159
+ def self.normalize(h)
160
+ super
161
+ end
162
+ map from('/names/first'){|n| n.gsub('+','')}, to('/first_name'){|n| "+++#{n}+++"}
163
+ end
164
+ class PersonWithBlockOneWay
165
+ extend HashMapper
166
+ map from('/names/first'), to('/first_name') do |n| "+++#{n}+++" end
167
+ end
168
+
169
+ describe "with blocks filters" do
170
+ before :each do
171
+ @from = {
172
+ :names => {:first => 'Ismael'}
173
+ }
174
+ @to = {
175
+ :first_name => '+++Ismael+++'
176
+ }
177
+ end
178
+
179
+ it "should pass final value through given block" do
180
+ PersonWithBlock.normalize(@from).should == @to
181
+ end
182
+
183
+ it "should be able to map the other way using a block" do
184
+ PersonWithBlock.denormalize(@to).should == @from
185
+ end
186
+
187
+ it "should accept a block for just one direction" do
188
+ PersonWithBlockOneWay.normalize(@from).should == @to
189
+ end
190
+
191
+ end
192
+
193
+ class ProjectMapper
194
+ extend HashMapper
195
+
196
+ map from('/name'), to('/project_name')
197
+ map from('/author_hash'), to('/author'), using(PersonWithBlock)
198
+ end
199
+
200
+ describe "with nested mapper" do
201
+ before :each do
202
+ @from ={
203
+ :name => 'HashMapper',
204
+ :author_hash => {
205
+ :names => {:first => 'Ismael'}
206
+ }
207
+ }
208
+ @to = {
209
+ :project_name => 'HashMapper',
210
+ :author => {:first_name => '+++Ismael+++'}
211
+ }
212
+ end
213
+
214
+ it "should delegate nested hashes to another mapper" do
215
+ ProjectMapper.normalize(@from).should == @to
216
+ end
217
+
218
+ it "should translate the other way using nested hashes" do
219
+ ProjectMapper.denormalize(@to).should == @from
220
+ end
221
+
222
+ end
223
+
224
+ class CompanyMapper
225
+ extend HashMapper
226
+
227
+ map from('/name'), to('/company_name')
228
+ map from('/employees'), to('/employees') do |employees_array|
229
+ employees_array.collect{|emp_hash| PersonWithBlock.normalize(emp_hash)}
230
+ end
231
+ end
232
+
233
+ class CompanyEmployeesMapper
234
+ extend HashMapper
235
+
236
+ map from('/name'), to('/company_name')
237
+ map from('/employees'), to('/employees'), using(PersonWithBlock)
238
+ end
239
+
240
+ describe "with arrays of nested hashes" do
241
+ before :each do
242
+ @from = {
243
+ :name => 'New Bamboo',
244
+ :employees => [
245
+ {:names => {:first => 'Ismael'}},
246
+ {:names => {:first => 'Sachiyo'}},
247
+ {:names => {:first => 'Pedro'}}
248
+ ]
249
+ }
250
+ @to = {
251
+ :company_name => 'New Bamboo',
252
+ :employees => [
253
+ {:first_name => '+++Ismael+++'},
254
+ {:first_name => '+++Sachiyo+++'},
255
+ {:first_name => '+++Pedro+++'}
256
+ ]
257
+ }
258
+ end
259
+
260
+ it "should pass array value though given block mapper" do
261
+ CompanyMapper.normalize(@from).should == @to
262
+ end
263
+
264
+ it "should map array elements automatically" do
265
+ CompanyEmployeesMapper.normalize(@from).should == @to
266
+ end
267
+ end
268
+
269
+ class NoKeys
270
+ extend HashMapper
271
+
272
+ map from('/exists'), to('/exists_yahoo') #in
273
+ map from('/exists_as_nil'), to('/exists_nil') #in
274
+ map from('/foo'), to('/bar') # not in
275
+
276
+ end
277
+
278
+ describe "with non-matching maps" do
279
+ before :all do
280
+ @input = {
281
+ :exists => 1,
282
+ :exists_as_nil => nil,
283
+ :doesnt_exist => 2
284
+ }
285
+ @output = {
286
+ :exists_yahoo => 1,
287
+ :exists_nil => nil
288
+ }
289
+ end
290
+
291
+ it "should ignore maps that don't exist" do
292
+ NoKeys.normalize(@input).should == @output
293
+ end
294
+ end
295
+
296
+ class WithBeforeFilters
297
+ extend HashMapper
298
+ map from('/hello'), to('/goodbye')
299
+
300
+ before_normalize do |input, output|
301
+ output[:this_is] = "extra #{input[:hello]} innit"
302
+ output
303
+ end
304
+ before_denormalize do |input, output|
305
+ input[:goodbye] = 'changed'
306
+ output
307
+ end
308
+ end
309
+
310
+ class WithAfterFilters
311
+ extend HashMapper
312
+ map from('/hello'), to('/goodbye')
313
+
314
+ after_normalize do |input, output|
315
+ output = output.to_a
316
+ output
317
+ end
318
+ after_denormalize do |input, output|
319
+ output.delete(:hello)
320
+ output
321
+ end
322
+ end
323
+
324
+ describe "before and after filters" do
325
+ before(:all) do
326
+ @denorm = {:hello => 'wassup?!'}
327
+ @norm = {:goodbye => 'seeya later!'}
328
+ end
329
+ it "should allow filtering before normalize" do
330
+ WithBeforeFilters.normalize(@denorm).should == {:goodbye => 'wassup?!', :this_is => 'extra wassup?! innit'}
331
+ end
332
+ it "should allow filtering before denormalize" do
333
+ WithBeforeFilters.denormalize(@norm).should == {:hello => 'changed'}
334
+ end
335
+ it "should allow filtering after normalize" do
336
+ WithAfterFilters.normalize(@denorm).should == [[:goodbye, 'wassup?!']]
337
+ end
338
+ it "should allow filtering after denormalize" do
339
+ WithAfterFilters.denormalize(@norm).should == {}
340
+ end
341
+
342
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,10 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
10
+ require 'hash_mapper'
data/tasks/rspec.rake ADDED
@@ -0,0 +1,21 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'spec'
6
+ end
7
+ begin
8
+ require 'spec/rake/spectask'
9
+ rescue LoadError
10
+ puts <<-EOS
11
+ To use rspec for testing you must install rspec gem:
12
+ gem install rspec
13
+ EOS
14
+ exit(0)
15
+ end
16
+
17
+ desc "Run the specs under spec/models"
18
+ Spec::Rake::SpecTask.new do |t|
19
+ t.spec_opts = ['--options', "spec/spec.opts"]
20
+ t.spec_files = FileList['spec/**/*_spec.rb']
21
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: newbamboo-hash_mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Ismael Celis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-08 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: newgem
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.3
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: hoe
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.8.0
32
+ version:
33
+ description: 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). Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.
34
+ email:
35
+ - ismaelct@gmail.com
36
+ executables: []
37
+
38
+ extensions: []
39
+
40
+ extra_rdoc_files:
41
+ - History.txt
42
+ - Manifest.txt
43
+ - PostInstall.txt
44
+ - README.rdoc
45
+ files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - PostInstall.txt
49
+ - README.rdoc
50
+ - Rakefile
51
+ - lib/hash_mapper.rb
52
+ - script/console
53
+ - script/destroy
54
+ - script/generate
55
+ - spec/hash_mapper_spec.rb
56
+ - spec/spec.opts
57
+ - spec/spec_helper.rb
58
+ - tasks/rspec.rake
59
+ has_rdoc: true
60
+ homepage: http://github.com/ismasan/hash_mapper
61
+ post_install_message: PostInstall.txt
62
+ rdoc_options:
63
+ - --main
64
+ - README.rdoc
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: "0"
78
+ version:
79
+ requirements: []
80
+
81
+ rubyforge_project: hash_mapper
82
+ rubygems_version: 1.2.0
83
+ signing_key:
84
+ specification_version: 2
85
+ summary: Maps values from hashes with different structures and/or key names
86
+ test_files: []
87
+