hash_mapper 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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
+