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 +4 -0
- data/Manifest.txt +13 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +261 -0
- data/Rakefile +28 -0
- data/lib/hash_mapper.rb +197 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/spec/hash_mapper_spec.rb +342 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/tasks/rspec.rake +21 -0
- metadata +87 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/PostInstall.txt
ADDED
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]
|
data/lib/hash_mapper.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
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
|
+
|