hash_mapper 0.0.8

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,266 @@
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
+ ==== Nested mappers
127
+
128
+ You want to map nested structures delegating to different mappers:
129
+
130
+ From this:
131
+ input = {
132
+ :project => 'HashMapper',
133
+ :url => 'http://github.com/ismasan/hash_mapper',
134
+ :author_names => {:first => 'Ismael', :last => 'Celis'}
135
+ }
136
+ To this:
137
+ output = {
138
+ :project_name => 'HashMapper',
139
+ :url => 'http://github.com/ismasan/hash_mapper',
140
+ :author => {:first_name => 'Ismael', :last_name => 'Celis'}
141
+ }
142
+
143
+ Define an UserMapper separate from your ProjectMapper, so you reuse them combined or standalone
144
+
145
+ class UserMapper
146
+ extend HashMapper
147
+ map from('/first'), to('/first_name')
148
+ map from('/last'), to('/lastt_name')
149
+ end
150
+
151
+ class ProjectMapper
152
+ extend HashMapper
153
+ map from('/project'), to('/project_name')
154
+ map from('/url'), to('/url')
155
+ map from('/author_names'), to('/author'), using(UserMapper)
156
+ end
157
+
158
+ Now ProjectMapper will delegate parsing of :author_names to UserMapper
159
+
160
+ ProjectMapper.normalize( input ) # => output
161
+
162
+ * Note the ampersand in &UserMapper. This is important if you are passing custom classes instead of procs.
163
+ * If you want to implement your own filter class just define to_proc in it.
164
+
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:
166
+
167
+ class CompanyMapper
168
+ map from('/info/name'), to('/company_name')
169
+ map form('/info/address'), to('/company_address')
170
+ map from('/info/year_founded'), to('year_founded', :to_i)
171
+
172
+ map from('/employees'), to('employees') do |employees_array|
173
+ employees_array.collect {|emp_hash| UserMapper.normalize(emp_hash)}
174
+ end
175
+ end
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
+
183
+ ==== Before and after filters
184
+
185
+ Sometimes you will need some slightly more complex processing on the whole hash, either before or after normalizing/denormalizing.
186
+
187
+ For this you can use the class methods before_normalize, before_denormalize, after_normalize and after_denormalize.
188
+
189
+ They all yield a block with 2 arguments - the hash you are mapping from and the hash you are mapping to, e.g.
190
+
191
+ class EggMapper
192
+ map from('/raw'), to('/fried')
193
+
194
+ before_normalize do |input, output|
195
+ input['raw'] ||= 'please' # this will give 'raw' a default value
196
+ input
197
+ end
198
+
199
+ after_denormalize do |input, output|
200
+ output.to_a # the denormalized object will now be an array, not a hash!!
201
+ end
202
+
203
+ end
204
+
205
+ Important: note that for before filters, you need to return the (modified) input, and for after filters, you need to return the output.
206
+ 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.
207
+
208
+
209
+ == REQUIREMENTS:
210
+
211
+ == TODO:
212
+ ==== Array access
213
+ You want:
214
+
215
+ {:names => ['Ismael', 'Celis']} converted to {:first_name => 'Ismael', :last_name => 'Celis'}
216
+
217
+ Do this:
218
+
219
+ map from('/names[0]'), to('/first_name')
220
+ map from('/names[1]'), to('/last_name')
221
+
222
+ ==== Optimizations
223
+
224
+ * Get rid of ActiveSupport (used for inherited class variables and HashWithIndifferentAccess)
225
+
226
+ == INSTALL:
227
+
228
+ # If you haven't done this already:
229
+
230
+ gem sources -a http://gems.github.com
231
+
232
+ # Now install
233
+
234
+ sudo gem install ismasan-hash_mapper
235
+
236
+ == Credits:
237
+
238
+ * Ismael Celis (Author - http://www.estadobeta.com)
239
+ * Mark Evans (Contributor - http://github.com/markevans)
240
+ * Jdeveloper (Contributor - http://github.com/jdeveloper)
241
+ * nightscape (Contributor - http://github.com/nightscape)
242
+
243
+ == LICENSE:
244
+
245
+ (The MIT License)
246
+
247
+ Copyright (c) 2009 Ismael Celis
248
+
249
+ Permission is hereby granted, free of charge, to any person obtaining
250
+ a copy of this software and associated documentation files (the
251
+ 'Software'), to deal in the Software without restriction, including
252
+ without limitation the rights to use, copy, modify, merge, publish,
253
+ distribute, sublicense, and/or sell copies of the Software, and to
254
+ permit persons to whom the Software is furnished to do so, subject to
255
+ the following conditions:
256
+
257
+ The above copyright notice and this permission notice shall be
258
+ included in all copies or substantial portions of the Software.
259
+
260
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
261
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
262
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
263
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
264
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
265
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
266
+ 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]
@@ -0,0 +1,211 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ def require_active_support
5
+ require 'active_support/core_ext/array/extract_options'
6
+ require 'active_support/core_ext/hash/indifferent_access'
7
+ require 'active_support/core_ext/duplicable'
8
+ Array.send(:include, ActiveSupport::CoreExtensions::Array::ExtractOptions)
9
+ Hash.send(:include, ActiveSupport::CoreExtensions::Hash::IndifferentAccess)
10
+ require 'active_support/core_ext/class/inheritable_attributes'
11
+
12
+ end
13
+
14
+ begin
15
+ require_active_support
16
+ rescue LoadError
17
+ require 'rubygems'
18
+ require_active_support
19
+ end
20
+
21
+
22
+
23
+ # This allows us to call blah(&:some_method) instead of blah{|i| i.some_method }
24
+ unless Symbol.instance_methods.include?('to_proc')
25
+ class Symbol
26
+ def to_proc
27
+ Proc.new {|obj| obj.send(self) }
28
+ end
29
+ end
30
+ end
31
+
32
+ # http://rpheath.com/posts/341-ruby-inject-with-index
33
+ unless Array.instance_methods.include?("inject_with_index")
34
+ module Enumerable
35
+ def inject_with_index(injected)
36
+ each_with_index{ |obj, index| injected = yield(injected, obj, index) }
37
+ injected
38
+ end
39
+ end
40
+ end
41
+
42
+ module HashMapper
43
+ VERSION = '0.0.8'
44
+
45
+ # we need this for inheritable mappers, which is annoying because it needs ActiveSupport, kinda overkill.
46
+ #
47
+ def self.extended(base)
48
+ base.class_eval do
49
+ write_inheritable_attribute :maps, []
50
+ class_inheritable_accessor :maps
51
+ end
52
+ end
53
+
54
+ def map(from, to, using=nil, &filter)
55
+ self.maps << Map.new(from, to, using)
56
+ to.filter = filter if block_given? # Useful if just one block given
57
+ end
58
+
59
+ def from(path, &filter)
60
+ path_map = PathMap.new(path)
61
+ path_map.filter = filter if block_given? # Useful if two blocks given
62
+ path_map
63
+ end
64
+
65
+ alias :to :from
66
+
67
+ def using(mapper_class)
68
+ mapper_class
69
+ end
70
+
71
+ def normalize(a_hash)
72
+ perform_hash_mapping a_hash, :normalize
73
+ end
74
+
75
+ def denormalize(a_hash)
76
+ perform_hash_mapping a_hash, :denormalize
77
+ end
78
+
79
+ def before_normalize(&blk)
80
+ @before_normalize = blk
81
+ end
82
+
83
+ def before_denormalize(&blk)
84
+ @before_denormalize = blk
85
+ end
86
+
87
+ def after_normalize(&blk)
88
+ @after_normalize = blk
89
+ end
90
+
91
+ def after_denormalize(&blk)
92
+ @after_denormalize = blk
93
+ end
94
+
95
+ protected
96
+
97
+
98
+ def perform_hash_mapping(a_hash, meth)
99
+ output = {}
100
+ # Before filter
101
+ before_filter = instance_eval "@before_#{meth}"
102
+ a_hash = before_filter.call(a_hash, output) if before_filter
103
+ # Do the mapping
104
+ maps.each do |m|
105
+ m.process_into(output, a_hash, meth)
106
+ end
107
+ # After filter
108
+ after_filter = instance_eval "@after_#{meth}"
109
+ output = after_filter.call(a_hash, output) if after_filter
110
+ # Return
111
+ output
112
+ end
113
+
114
+ # Contains PathMaps
115
+ # Makes them interact
116
+ #
117
+ class Map
118
+
119
+ attr_reader :path_from, :path_to, :delegated_mapper
120
+
121
+ def initialize(path_from, path_to, delegated_mapper = nil)
122
+ @path_from, @path_to, @delegated_mapper = path_from, path_to, delegated_mapper
123
+ end
124
+
125
+ def process_into(output, input, meth = :normalize)
126
+ path_1, path_2 = (meth == :normalize ? [path_from, path_to] : [path_to, path_from])
127
+ catch :no_value do
128
+ value = get_value_from_input(output, input, path_1, meth)
129
+ add_value_to_hash!(output, path_2, value)
130
+ end
131
+ end
132
+
133
+ protected
134
+
135
+ def get_value_from_input(output, input, path, meth)
136
+ value = path.inject(input) do |h,e|
137
+ v = h.with_indifferent_access[e] # this does it, but uses ActiveSupport
138
+ throw :no_value if v.nil?#.has_key?(e)
139
+ v
140
+ end
141
+ delegated_mapper ? delegate_to_nested_mapper(value, meth) : value
142
+ end
143
+
144
+
145
+ def delegate_to_nested_mapper(value, meth)
146
+ case value
147
+ when Array
148
+ value.map {|h| delegated_mapper.send(meth, h)}
149
+ when nil
150
+ throw :no_value
151
+ else
152
+ delegated_mapper.send(meth, value)
153
+ end
154
+ end
155
+
156
+ def add_value_to_hash!(hash, path, value)
157
+ path.inject_with_index(hash) do |h,e,i|
158
+ if !h[e].nil? # it can be FALSE
159
+ h[e]
160
+ else
161
+ h[e] = (i == path.size-1 ? path.apply_filter(value) : {})
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ end
168
+
169
+ # contains array of path segments
170
+ #
171
+ class PathMap
172
+ include Enumerable
173
+
174
+ attr_reader :segments
175
+ attr_writer :filter
176
+
177
+ def initialize(path)
178
+ @path = path.dup
179
+ @segments = parse(path)
180
+ @filter = lambda{|value| value}# default filter does nothing
181
+ end
182
+
183
+ def apply_filter(value)
184
+ @filter.call(value)
185
+ end
186
+
187
+ def each(&blk)
188
+ @segments.each(&blk)
189
+ end
190
+
191
+ def first
192
+ @segments.first
193
+ end
194
+
195
+ def last
196
+ @segments.last
197
+ end
198
+
199
+ def size
200
+ @segments.size
201
+ end
202
+
203
+ private
204
+
205
+ def parse(path)
206
+ path.sub(/^\//,'').split('/').map(&:to_sym)
207
+ end
208
+
209
+ end
210
+
211
+ 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,430 @@
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
+ pending
150
+ WithArrays.normalize(@from).should == @to
151
+ end
152
+
153
+ it "should map the other way restoring arrays" do
154
+ pending
155
+ WithArrays.denormalize(@to).should == @from
156
+ end
157
+ end
158
+
159
+ class PersonWithBlock
160
+ extend HashMapper
161
+ def self.normalize(h)
162
+ super
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
169
+ end
170
+
171
+ describe "with blocks filters" do
172
+ before :each do
173
+ @from = {
174
+ :names => {:first => 'Ismael'}
175
+ }
176
+ @to = {
177
+ :first_name => '+++Ismael+++'
178
+ }
179
+ end
180
+
181
+ it "should pass final value through given block" do
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
187
+ end
188
+
189
+ it "should accept a block for just one direction" do
190
+ PersonWithBlockOneWay.normalize(@from).should == @to
191
+ end
192
+
193
+ end
194
+
195
+ class ProjectMapper
196
+ extend HashMapper
197
+
198
+ map from('/name'), to('/project_name')
199
+ map from('/author_hash'), to('/author'), using(PersonWithBlock)
200
+ end
201
+
202
+ describe "with nested mapper" do
203
+ before :each do
204
+ @from ={
205
+ :name => 'HashMapper',
206
+ :author_hash => {
207
+ :names => {:first => 'Ismael'}
208
+ }
209
+ }
210
+ @to = {
211
+ :project_name => 'HashMapper',
212
+ :author => {:first_name => '+++Ismael+++'}
213
+ }
214
+ end
215
+
216
+ it "should delegate nested hashes to another mapper" do
217
+ ProjectMapper.normalize(@from).should == @to
218
+ end
219
+
220
+ it "should translate the other way using nested hashes" do
221
+ ProjectMapper.denormalize(@to).should == @from
222
+ end
223
+
224
+ end
225
+
226
+ class CompanyMapper
227
+ extend HashMapper
228
+
229
+ map from('/name'), to('/company_name')
230
+ map from('/employees'), to('/employees') do |employees_array|
231
+ employees_array.collect{|emp_hash| PersonWithBlock.normalize(emp_hash)}
232
+ end
233
+ end
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
+
242
+ describe "with arrays of nested hashes" do
243
+ before :each do
244
+ @from = {
245
+ :name => 'New Bamboo',
246
+ :employees => [
247
+ {:names => {:first => 'Ismael'}},
248
+ {:names => {:first => 'Sachiyo'}},
249
+ {:names => {:first => 'Pedro'}}
250
+ ]
251
+ }
252
+ @to = {
253
+ :company_name => 'New Bamboo',
254
+ :employees => [
255
+ {:first_name => '+++Ismael+++'},
256
+ {:first_name => '+++Sachiyo+++'},
257
+ {:first_name => '+++Pedro+++'}
258
+ ]
259
+ }
260
+ end
261
+
262
+ it "should pass array value though given block mapper" do
263
+ CompanyMapper.normalize(@from).should == @to
264
+ end
265
+
266
+ it "should map array elements automatically" do
267
+ CompanyEmployeesMapper.normalize(@from).should == @to
268
+ end
269
+ end
270
+
271
+ class NoKeys
272
+ extend HashMapper
273
+
274
+ map from('/exists'), to('/exists_yahoo') #in
275
+ map from('/exists_as_nil'), to('/exists_nil') #in
276
+ map from('/foo'), to('/bar') # not in
277
+
278
+ end
279
+
280
+ describe "with non-matching maps" do
281
+ before :all do
282
+ @input = {
283
+ :exists => 1,
284
+ :exists_as_nil => nil,
285
+ :doesnt_exist => 2
286
+ }
287
+ @output = {
288
+ :exists_yahoo => 1
289
+ }
290
+ end
291
+
292
+ it "should ignore maps that don't exist" do
293
+ NoKeys.normalize(@input).should == @output
294
+ end
295
+ end
296
+
297
+ describe "with false values" do
298
+
299
+ it "should include values in output" do
300
+ NoKeys.normalize({'exists' => false}).should == {:exists_yahoo => false}
301
+ NoKeys.normalize({:exists => false}).should == {:exists_yahoo => false}
302
+ end
303
+
304
+ end
305
+
306
+ describe "with nil values" do
307
+
308
+ it "should not include values in output" do
309
+ NoKeys.normalize({:exists => nil}).should == {}
310
+ NoKeys.normalize({'exists' => nil}).should == {}
311
+ end
312
+
313
+ end
314
+
315
+ class WithBeforeFilters
316
+ extend HashMapper
317
+ map from('/hello'), to('/goodbye')
318
+ map from('/extra'), to('/extra')
319
+
320
+ before_normalize do |input, output|
321
+ input[:extra] = "extra #{input[:hello]} innit"
322
+ input
323
+ end
324
+ before_denormalize do |input, output|
325
+ input[:goodbye] = 'changed'
326
+ input
327
+ end
328
+ end
329
+
330
+ class WithAfterFilters
331
+ extend HashMapper
332
+ map from('/hello'), to('/goodbye')
333
+
334
+ after_normalize do |input, output|
335
+ output = output.to_a
336
+ output
337
+ end
338
+ after_denormalize do |input, output|
339
+ output.delete(:hello)
340
+ output
341
+ end
342
+ end
343
+
344
+ describe "before and after filters" do
345
+ before(:all) do
346
+ @denorm = {:hello => 'wassup?!'}
347
+ @norm = {:goodbye => 'seeya later!'}
348
+ end
349
+ it "should allow filtering before normalize" do
350
+ WithBeforeFilters.normalize(@denorm).should == {:goodbye => 'wassup?!', :extra => 'extra wassup?! innit'}
351
+ end
352
+ it "should allow filtering before denormalize" do
353
+ WithBeforeFilters.denormalize(@norm).should == {:hello => 'changed'}
354
+ end
355
+ it "should allow filtering after normalize" do
356
+ WithAfterFilters.normalize(@denorm).should == [[:goodbye, 'wassup?!']]
357
+ end
358
+ it "should allow filtering after denormalize" do
359
+ WithAfterFilters.denormalize(@norm).should == {}
360
+ end
361
+
362
+ end
363
+
364
+ class NotRelated
365
+ extend HashMapper
366
+ map from('/n'), to('/n/n')
367
+ end
368
+
369
+ class A
370
+ extend HashMapper
371
+ map from('/a'), to('/a/a')
372
+ end
373
+
374
+ class B < A
375
+ map from('/b'), to('/b/b')
376
+ end
377
+
378
+ class C < B
379
+ map from('/c'), to('/c/c')
380
+ end
381
+
382
+ describe "inherited mappers" do
383
+ before :all do
384
+ @from = {
385
+ :a => 'a',
386
+ :b => 'b',
387
+ :c => 'c'
388
+ }
389
+ @to_b ={
390
+ :a => {:a => 'a'},
391
+ :b => {:b => 'b'}
392
+ }
393
+
394
+ end
395
+
396
+ it "should inherit mappings" do
397
+ B.normalize(@from).should == @to_b
398
+ end
399
+
400
+ it "should not affect other mappers" do
401
+ NotRelated.normalize('n' => 'nn').should == {:n => {:n => 'nn'}}
402
+ end
403
+ end
404
+
405
+ class MixedMappings
406
+ extend HashMapper
407
+ map from('/big/jobs'), to('dodo')
408
+ map from('/timble'), to('bingo/biscuit')
409
+ end
410
+
411
+ describe "dealing with strings and symbols" do
412
+
413
+ it "should be able to normalize from a nested hash with string keys" do
414
+ MixedMappings.normalize(
415
+ 'big' => {'jobs' => 5},
416
+ 'timble' => 3.2
417
+ ).should == {:dodo => 5,
418
+ :bingo => {:biscuit => 3.2}}
419
+ end
420
+
421
+ it "should not symbolized keys in value hashes" do
422
+ MixedMappings.normalize(
423
+ 'big' => {'jobs' => 5},
424
+ 'timble' => {'string key' => 'value'}
425
+ ).should == {:dodo => 5,
426
+ :bingo => {:biscuit => {'string key' => 'value'}}}
427
+ end
428
+
429
+ end
430
+
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,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hash_mapper
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
+ platform: ruby
6
+ authors:
7
+ - Ismael Celis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-05 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: newgem
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.3
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: hoe
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 1.8.0
34
+ version:
35
+ description: |-
36
+ 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).
37
+
38
+ Tiny module that allows you to easily adapt from one hash structure to another with a simple declarative DSL.
39
+ email:
40
+ - ismaelct@gmail.com
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - PostInstall.txt
49
+ - README.rdoc
50
+ files:
51
+ - History.txt
52
+ - Manifest.txt
53
+ - PostInstall.txt
54
+ - README.rdoc
55
+ - Rakefile
56
+ - lib/hash_mapper.rb
57
+ - script/console
58
+ - script/destroy
59
+ - script/generate
60
+ - spec/hash_mapper_spec.rb
61
+ - spec/spec.opts
62
+ - spec/spec_helper.rb
63
+ - tasks/rspec.rake
64
+ has_rdoc: true
65
+ homepage: http://github.com/ismasan/hash_mapper
66
+ licenses: []
67
+
68
+ post_install_message: PostInstall.txt
69
+ rdoc_options:
70
+ - --main
71
+ - README.rdoc
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: "0"
85
+ version:
86
+ requirements: []
87
+
88
+ rubyforge_project: hash_mapper
89
+ rubygems_version: 1.3.5
90
+ signing_key:
91
+ specification_version: 3
92
+ summary: Maps values from hashes with different structures and/or key names
93
+ test_files: []
94
+