hsume2-mapped-record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/LICENSE +25 -0
- data/Manifest.txt +16 -0
- data/README.rdoc +131 -0
- data/Rakefile +29 -0
- data/lib/mapped-record/hash/mappable.rb +22 -0
- data/lib/mapped-record/mapping.rb +130 -0
- data/lib/mapped-record.rb +183 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/database.yml +3 -0
- data/test/hashed-record/hash/test_mappable.rb +30 -0
- data/test/hashed-record/test_mapping.rb +166 -0
- data/test/test_helper.rb +93 -0
- data/test/test_mapped_record.rb +260 -0
- metadata +124 -0
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
LICENSE
|
3
|
+
|
4
|
+
(The MIT License)
|
5
|
+
|
6
|
+
Copyright (c) 2009 Henry Hsu
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
9
|
+
a copy of this software and associated documentation files (the
|
10
|
+
'Software'), to deal in the Software without restriction, including
|
11
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
12
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
13
|
+
permit persons to whom the Software is furnished to do so, subject to
|
14
|
+
the following conditions:
|
15
|
+
|
16
|
+
The above copyright notice and this permission notice shall be
|
17
|
+
included in all copies or substantial portions of the Software.
|
18
|
+
|
19
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
20
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
21
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
22
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
23
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
24
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
25
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
LICENSE
|
3
|
+
Manifest.txt
|
4
|
+
README.rdoc
|
5
|
+
Rakefile
|
6
|
+
lib/mapped-record.rb
|
7
|
+
lib/mapped-record/hash/mappable.rb
|
8
|
+
lib/mapped-record/mapping.rb
|
9
|
+
script/console
|
10
|
+
script/destroy
|
11
|
+
script/generate
|
12
|
+
test/database.yml
|
13
|
+
test/hashed-record/hash/test_mappable.rb
|
14
|
+
test/hashed-record/test_mapping.rb
|
15
|
+
test/test_helper.rb
|
16
|
+
test/test_mapped_record.rb
|
data/README.rdoc
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
= mapped-record
|
2
|
+
|
3
|
+
http://github.com/hsume2/mapped-record
|
4
|
+
|
5
|
+
Auto-magically map Hash[keys] to ActiveRecord.attributes.
|
6
|
+
|
7
|
+
Suppose you have a hash <tt>a_hash = { 'FullName' => 'Your Name' }</tt>, and an Active Record object
|
8
|
+
with a +:full_name+ attribute to create with 'Your Name'. It's easy to deal with this on a one-time basis:
|
9
|
+
|
10
|
+
model.create( :full_name => a_hash['FullName'] )
|
11
|
+
|
12
|
+
However, add in 20 other keys and it gets tiresome.
|
13
|
+
|
14
|
+
Suppose you could define hash +keys+ and their target ActiveRecord +attribute+ in one place; then, initialize
|
15
|
+
Active Record objects with the corresponding data. Continue reading.
|
16
|
+
|
17
|
+
== Getting Started
|
18
|
+
|
19
|
+
All you have to do is add +attr_mapped+ to your model.
|
20
|
+
|
21
|
+
class Person < ActiveRecord::Base
|
22
|
+
attr_mapped 'FullName' => :full_name
|
23
|
+
end
|
24
|
+
|
25
|
+
Then you can create and update like so:
|
26
|
+
|
27
|
+
p = Person.create_with(h)
|
28
|
+
h['FullName'] = 'Mr. Name'
|
29
|
+
p.update_with(h)
|
30
|
+
|
31
|
+
+mapped-record+ is more powerful than that. See Mapping Types for efficient ways to assign mappings.
|
32
|
+
See Mapping Helpers for extra-added features (e.g. post-processing data).
|
33
|
+
|
34
|
+
== Mapping Types
|
35
|
+
|
36
|
+
Mappings can be created in the following ways:
|
37
|
+
|
38
|
+
=== Automatic mappings (implicit)
|
39
|
+
|
40
|
+
If you use,
|
41
|
+
attr_mapped 'FullName'
|
42
|
+
+attr_mapped+ will automatically downcase_and_underscore the key to +:full_name+, which is often useful.
|
43
|
+
Whenever used, specify these first.
|
44
|
+
|
45
|
+
=== Manual mappings (explicit)
|
46
|
+
|
47
|
+
To manually set which Active Record attribute you want a key to map to, add manual mappings in the options hash.
|
48
|
+
attr_mapped 'FullName' => :full_name
|
49
|
+
attr_mapped { 'FullName' => :full_name } # same as above
|
50
|
+
attr_mapped 'FullName' => :full_name, 'Email' # will fail, because the options hash is considered the last argument
|
51
|
+
|
52
|
+
=== Namespace mappings
|
53
|
+
|
54
|
+
Suppose you have a lot of keys starting with +PBType+ which you want removed. Then add <tt>:namespace => 'PBType'</tt> to remove the prefix and then map automatically (also in the options hash)
|
55
|
+
|
56
|
+
attr_mapped 'PBTypeName', 'PBTypeAddress', 'PBTypeCode', { :namespace => 'PBType' }
|
57
|
+
|
58
|
+
will map +PBTypeName+ to +:name+, +PBTypeAddress+ to +:address+, etc.
|
59
|
+
|
60
|
+
Namespaces only apply to the keys for each +attr_mapped+ call. So
|
61
|
+
|
62
|
+
class PBPerson < ActiveRecord::Base
|
63
|
+
attr_mapped 'PBTypeName', { :namespace => 'PBType' }
|
64
|
+
attr_mapped 'PBTypeAddr'
|
65
|
+
end
|
66
|
+
|
67
|
+
will map +PBTypeName+ to +:name+, but +PBTypeAddr+ to +:pb_type_addr+.
|
68
|
+
|
69
|
+
=== Mapping priority
|
70
|
+
|
71
|
+
Regardless of how many times you call +attr_mapped+, mappings are overridden in increasing order of
|
72
|
+
priority:
|
73
|
+
|
74
|
+
* implicit
|
75
|
+
* namespace
|
76
|
+
* explicit
|
77
|
+
|
78
|
+
That means explicit will always override namespace and implicit, regardless of the order in which #attr_mapped is called. To illustrate this behavior:
|
79
|
+
|
80
|
+
class PBPerson < ActiveRecord::Base
|
81
|
+
attr_mapped 'PBTypeName', { :namespace => 'PBType' }
|
82
|
+
attr_mapped 'PBTypeName'
|
83
|
+
attr_mapped { 'PBTypeName' => :actually_this }
|
84
|
+
|
85
|
+
attr_mapped 'PBTypeName', { :namespace => 'PBType', 'PBTypeName' => :actually_this } # even in this confusing example
|
86
|
+
end
|
87
|
+
|
88
|
+
will map to +:actually_this+.
|
89
|
+
|
90
|
+
== Mapping Helpers
|
91
|
+
|
92
|
+
=== :id
|
93
|
+
|
94
|
+
If one of the hash keys should map to the Active Record id, setting it like <tt>attr_mapped :key => :id</tt>
|
95
|
+
won't work. Active Record won't let you mass assign +:id+ anyway. Instead
|
96
|
+
attr_mapped 'PBTypeName', { :namespace => 'PBType', :id => 'PBKey' }
|
97
|
+
to force creation with +PBKey+'s value.
|
98
|
+
|
99
|
+
=== :serialize
|
100
|
+
|
101
|
+
You can also specify which keys to serialize after they've been mapped. Using,
|
102
|
+
attr_mapped 'PBArray', { :namespace => 'PB', :serialize => 'PBArray' }
|
103
|
+
will map +PBArray+ to +:array+ and call <tt>serialize :array</tt> in the Active Record.
|
104
|
+
|
105
|
+
=== :filter
|
106
|
+
|
107
|
+
You can add proc filters to process data from hashes before it's used by Active Record.
|
108
|
+
|
109
|
+
Suppose all the dates are in the wrong format, then,
|
110
|
+
MYDATE = Proc.new { |p| Time.at(p + 978307200) }
|
111
|
+
attr_mapped 'PBDate', { :filter => { 'PBDate' => PBPerson::MYDATE } }
|
112
|
+
|
113
|
+
== Named Mappings
|
114
|
+
|
115
|
+
If for some reason, you want to use multiple mappings on the same model, you can create named mappings
|
116
|
+
with +attr_mapped_named+, where the first argument is the mapping name, followed by the same as +attr_mapped+.
|
117
|
+
|
118
|
+
class Person < ActiveRecord::Base
|
119
|
+
attr_mapped_named :phone_record, 'FullName'
|
120
|
+
end
|
121
|
+
|
122
|
+
=== Dynamic methods
|
123
|
+
|
124
|
+
+mapped-record+ will then dynamically create methods so you can:
|
125
|
+
|
126
|
+
p = Person.create_with_phone_record(h)
|
127
|
+
p.update_with_phone_record(h)
|
128
|
+
|
129
|
+
== Credit
|
130
|
+
|
131
|
+
... where deserved. A lot of cues from thoughtbot/paperclip[http://github.com/thoughtbot/paperclip] on how to set up the gem and testing, so thanks.
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems' unless ENV['NO_RUBYGEMS']
|
2
|
+
%w[rake rake/clean fileutils newgem rubigen].each { |f| require f }
|
3
|
+
require File.dirname(__FILE__) + '/lib/mapped-record'
|
4
|
+
|
5
|
+
# Generate all the Rake tasks
|
6
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
7
|
+
$hoe = Hoe.new('mapped-record', MappedRecord::VERSION) do |p|
|
8
|
+
p.developer('Henry Hsu', 'henry@qlane.com')
|
9
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
10
|
+
p.rubyforge_name = p.name
|
11
|
+
p.extra_deps = [
|
12
|
+
['activesupport','>= 2.0.2'],
|
13
|
+
]
|
14
|
+
p.extra_dev_deps = [
|
15
|
+
['newgem', ">= #{::Newgem::VERSION}"],
|
16
|
+
['thoughtbot-shoulda', '>= 0'],
|
17
|
+
['sqlite3-ruby', '>= 0']
|
18
|
+
]
|
19
|
+
p.summary = 'Auto-magically map Hash[keys] to ActiveRecord.attributes'
|
20
|
+
p.description = 'Auto-magically map Hash[keys] to ActiveRecord.attributes'
|
21
|
+
|
22
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
23
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
24
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
25
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
29
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a new Hash where the keys have been swapped with those defined in <tt>Mapping[named_mapping]</tt>.
|
3
|
+
def map_with(named_mapping)
|
4
|
+
if Mapping.has?(named_mapping)
|
5
|
+
m = Mapping[named_mapping]
|
6
|
+
|
7
|
+
result = self.inject({}) do |result, element|
|
8
|
+
mapping = m[element.first]
|
9
|
+
|
10
|
+
to = mapping[:to] if mapping
|
11
|
+
proc = mapping[:filter] if mapping
|
12
|
+
|
13
|
+
if to
|
14
|
+
result[to] = element.last unless proc
|
15
|
+
result[to] = proc.call(element.last) if proc
|
16
|
+
end
|
17
|
+
|
18
|
+
result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
module MappedRecord
|
2
|
+
class Mapping
|
3
|
+
class << self
|
4
|
+
# Assigns a named mapping accessible throughout the runtime environment.
|
5
|
+
#
|
6
|
+
# Mapping.create :my_mapping, 'Ready', 'Set', 'Go'
|
7
|
+
#
|
8
|
+
# The mapping is then accessible from <tt>Mapping[:my_mapping]</tt>
|
9
|
+
#
|
10
|
+
# Given a list of strings, those keys will be mapped automatically to downcase and
|
11
|
+
# underscored attributes. (Specify these first).
|
12
|
+
#
|
13
|
+
# Configuration options:
|
14
|
+
# [<tt>:filter</tt>]
|
15
|
+
# Specify a hash of keys and procs to call before assigning to attributes.
|
16
|
+
# attr_mapped 'Date', { :after => { 'Date' => Proc.new { ... } } }
|
17
|
+
# [<tt>:namespace</tt>]
|
18
|
+
# A prefix string to remove before automatically mapping.
|
19
|
+
# attr_mapped 'PBOne', 'PBTwo', { :namespace => 'PB' }
|
20
|
+
# [<tt>'key' => :target, 'key2' => :target2, ...</tt>]
|
21
|
+
# As many manual mappings as needed.
|
22
|
+
def create(mapping_name, *map_attrs)
|
23
|
+
raise MappingError, "Not creating mapping with nil name" if mapping_name.nil?
|
24
|
+
named_mappings[mapping_name] ||= Hash.new
|
25
|
+
|
26
|
+
options = map_attrs.extract_options!
|
27
|
+
verbose = parse_verbose(options)
|
28
|
+
|
29
|
+
serialize_mappings = []
|
30
|
+
namespace = nil
|
31
|
+
type = IMPLICIT
|
32
|
+
|
33
|
+
options.each_pair do |key, value|
|
34
|
+
case key
|
35
|
+
when :namespace
|
36
|
+
namespace = value.to_s unless value.to_s.blank?
|
37
|
+
when :filter
|
38
|
+
value.each_pair do |attr, proc|
|
39
|
+
named_mappings[mapping_name][attr.to_s] ||= Hash.new
|
40
|
+
named_mappings[mapping_name][attr.to_s][:filter] = proc
|
41
|
+
end
|
42
|
+
when String, Symbol # deals with explicit mappings
|
43
|
+
raise MappingError, "Must be symbol" unless value.kind_of?(Symbol)
|
44
|
+
update_mapping(mapping_name, key, value.to_sym, EXPLICIT)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
mapping_temp = Hash.new
|
49
|
+
|
50
|
+
map_attrs.each do |attr|
|
51
|
+
raise MappingError, "Must be string or symbol." unless attr.kind_of?(String) or attr.kind_of?(Symbol)
|
52
|
+
|
53
|
+
mapped_attr = attr.to_s
|
54
|
+
if namespace
|
55
|
+
match = mapped_attr[/^#{namespace}/]
|
56
|
+
raise MappingError, "Causes mapping to be ''" if mapped_attr == match
|
57
|
+
if match
|
58
|
+
mapped_attr = mapped_attr.sub(/^#{namespace}/, '')
|
59
|
+
type = NAMESPACE
|
60
|
+
end
|
61
|
+
end
|
62
|
+
mapped_attr = mapped_attr.underscore.gsub(' ', '_')
|
63
|
+
update_mapping(mapping_name, attr, mapped_attr.to_sym, type)
|
64
|
+
end
|
65
|
+
|
66
|
+
named_mappings[mapping_name]
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_verbose(options) # :nodoc:
|
70
|
+
if !options[:verbose].nil? && (options[:verbose].kind_of?(FalseClass) || options[:verbose].kind_of?(TrueClass))
|
71
|
+
verbose = options[:verbose]
|
72
|
+
options.delete(:verbose)
|
73
|
+
verbose
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns true if mapping of +mapping_name+ is assigned.
|
78
|
+
def has?(mapping_name)
|
79
|
+
named_mappings.include?(mapping_name)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Returns true of no mappings are assigned.
|
83
|
+
def empty?
|
84
|
+
named_mappings.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns true of no mappings are assigned.
|
88
|
+
def blank?
|
89
|
+
named_mappings.blank?
|
90
|
+
end
|
91
|
+
|
92
|
+
# Clear all mappings.
|
93
|
+
def reset
|
94
|
+
named_mappings.clear
|
95
|
+
end
|
96
|
+
|
97
|
+
# Access named mappings straight from the class.
|
98
|
+
def [](key)
|
99
|
+
named_mappings[key]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Assign mappings straight from the class.
|
103
|
+
def []=(key, values)
|
104
|
+
create(key, *values)
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_accessor :named_mappings # :nodoc:
|
108
|
+
|
109
|
+
def named_mappings # :nodoc:
|
110
|
+
@named_mappings ||= Hash.new
|
111
|
+
end
|
112
|
+
|
113
|
+
private :named_mappings
|
114
|
+
|
115
|
+
def update_mapping(mapping_name, key, value, type) # :nodoc:
|
116
|
+
named_mapping = named_mappings[mapping_name]
|
117
|
+
named_mapping[key] ||= Hash.new
|
118
|
+
|
119
|
+
if named_mapping[key][:to].nil? or type >= named_mapping[key][:type]
|
120
|
+
raise MappingError, "Multiple keys pointing to the same symbol" unless named_mapping.select { |key_name, mapping| key_name != key && mapping[:to] == value }.blank?
|
121
|
+
named_mapping[key][:to] = value and named_mapping[key][:type] = type
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
private :update_mapping
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Mapping = MappedRecord::Mapping
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# See +attr_mapped+ or the README for details.
|
2
|
+
#
|
3
|
+
#
|
4
|
+
# Created by Henry Hsu on 2009-06-07.
|
5
|
+
# Copyright 2009 Qlane. All rights reserved.
|
6
|
+
#
|
7
|
+
|
8
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
9
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
10
|
+
|
11
|
+
require 'mapped-record/hash/mappable'
|
12
|
+
require 'mapped-record/mapping'
|
13
|
+
|
14
|
+
module MappedRecord
|
15
|
+
VERSION = '0.0.1' # :nodoc:
|
16
|
+
|
17
|
+
IMPLICIT = 0 # :nodoc:
|
18
|
+
NAMESPACE = 1 # :nodoc:
|
19
|
+
EXPLICIT = 2 # :nodoc:
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def included base #:nodoc:
|
23
|
+
base.extend ClassMethods
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#--
|
28
|
+
# TODO subclass this to signify each kind of error
|
29
|
+
#++
|
30
|
+
class MappingError < StandardError; end # :nodoc:
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
# Assigns a mapping for the current ActiveRecord class.
|
34
|
+
#
|
35
|
+
# class Person < ActiveRecord::Base
|
36
|
+
# attr_mapped 'PBName', 'PBAddress', 'PBEmail', { :namespace => 'Email' }
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# Given a list of strings, those keys will be mapped automatically to downcase and
|
40
|
+
# underscored attributes. (Specify these first).
|
41
|
+
#
|
42
|
+
# Configuration options:
|
43
|
+
# [<tt>:id</tt>]
|
44
|
+
# The key to map to the primary key.
|
45
|
+
# attr_mapped { :id => 'Key' }
|
46
|
+
# [<tt>:serialize</tt>]
|
47
|
+
# Any keys to serialize after mapping.
|
48
|
+
# attr_mapped 'Array', 'Blob', { :serialize => ['Array', 'Blob'] }
|
49
|
+
# [<tt>:filter</tt>]
|
50
|
+
# Specify a hash of keys and procs to call before assigning to attributes.
|
51
|
+
# attr_mapped 'Date', { :after => { 'Date' => Proc.new { ... } } }
|
52
|
+
# [<tt>:namespace</tt>]
|
53
|
+
# A prefix string to remove before automatically mapping.
|
54
|
+
# attr_mapped 'PBOne', 'PBTwo', { :namespace => 'PB' }
|
55
|
+
# [<tt>'key' => :attribute, 'key2' => :attribute2, ...</tt>]
|
56
|
+
# As many manual mappings as needed.
|
57
|
+
def attr_mapped(*map_attrs)
|
58
|
+
attr_mapped_named(class_name, *map_attrs)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Assigns mappings to a name.
|
62
|
+
#
|
63
|
+
# class Person < ActiveRecord::Base
|
64
|
+
# attr_mapped_named :public, 'PBName', 'PBAddress', 'PBEmail', { :namespace => 'Email' }
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# The mapping can then be used with dynamic create and update methods.
|
68
|
+
# From the example above:
|
69
|
+
# p = Person.create_with_public({ 'PBName' => 'Mr. Name' })
|
70
|
+
# p.update_with_public({ 'PBName' => 'Full Name' })
|
71
|
+
def attr_mapped_named(named_mapping = nil, *map_attrs)
|
72
|
+
include InstanceMethods
|
73
|
+
|
74
|
+
unless self.respond_to?(:attr_mapped_serialized)
|
75
|
+
class_inheritable_accessor :attr_mapped_serialized
|
76
|
+
write_inheritable_attribute :attr_mapped_serialized, Hash.new
|
77
|
+
end
|
78
|
+
|
79
|
+
unless self.respond_to?(:attr_hashed_id)
|
80
|
+
class_inheritable_accessor :attr_hashed_id
|
81
|
+
write_inheritable_attribute :attr_hashed_id, ''
|
82
|
+
end
|
83
|
+
|
84
|
+
raise ArgumentError, "Mapping name not given." if named_mapping.nil?
|
85
|
+
raise MappingError, "No options given." if map_attrs.blank?
|
86
|
+
|
87
|
+
options = map_attrs.extract_options!
|
88
|
+
|
89
|
+
serialize_mappings = []
|
90
|
+
|
91
|
+
options.each_pair do |key, value|
|
92
|
+
case key
|
93
|
+
when :id
|
94
|
+
self.attr_hashed_id = value.to_s
|
95
|
+
when :serialize
|
96
|
+
keys = [value.to_s] unless value.kind_of?(Array) # TODO if-else blocks probably more efficient
|
97
|
+
keys = value.collect { |v| v.to_s } if value.kind_of?(Array)
|
98
|
+
serialize_mappings |= keys
|
99
|
+
end
|
100
|
+
end
|
101
|
+
options.delete(:id)
|
102
|
+
options.delete(:serialize)
|
103
|
+
|
104
|
+
map_attrs << options
|
105
|
+
Mapping.create named_mapping, *map_attrs
|
106
|
+
|
107
|
+
if Mapping.has?(named_mapping)
|
108
|
+
self.instance_eval %Q{ def create_with_#{named_mapping}(hash); create_with(hash, :#{named_mapping}); end; }
|
109
|
+
self.class_eval %Q{ def update_with_#{named_mapping}(hash); update_with(hash, :#{named_mapping}); end }
|
110
|
+
self.attr_mapped_serialized[named_mapping] ||= Hash.new
|
111
|
+
self.attr_mapped_serialized[named_mapping] = update_serialized(named_mapping)
|
112
|
+
end
|
113
|
+
|
114
|
+
serialize_mappings.each do |attr|
|
115
|
+
raise MappingError, "Serializing :id not allowed." if !self.attr_hashed_id.blank? && attr == self.attr_hashed_id
|
116
|
+
to_serialize = Mapping[named_mapping][attr][:to].to_sym
|
117
|
+
|
118
|
+
# need to know serialized attributes to 'watch'
|
119
|
+
self.attr_mapped_serialized[named_mapping][attr] = to_serialize
|
120
|
+
|
121
|
+
self.instance_eval { serialize to_serialize }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Accepts a hash to map and creates the Active Record object with its values.
|
126
|
+
def create_with(hash = {}, named_mapping = nil)
|
127
|
+
named_mapping = self.class_name unless named_mapping
|
128
|
+
|
129
|
+
self.create(with_attributes(named_mapping, hash)) do |r|
|
130
|
+
id = hash[self.attr_hashed_id]
|
131
|
+
r.id = id if id
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# A helper to check if the Active Record object responds to mapped-record methods.
|
136
|
+
def acts_like_mapped?
|
137
|
+
true
|
138
|
+
end
|
139
|
+
|
140
|
+
# Maps the values in +hash+ with +named_mapping+ for use in Active Record.
|
141
|
+
def with_attributes(named_mapping, hash)
|
142
|
+
attrs = hash.map_with(named_mapping)
|
143
|
+
attrs.delete(:id) if attrs[:id]
|
144
|
+
attrs
|
145
|
+
end
|
146
|
+
|
147
|
+
# Maintains that +serialize+ is set for correct attribute.
|
148
|
+
def update_serialized(named_mapping)
|
149
|
+
self.attr_mapped_serialized[named_mapping].inject({}) do |result, element|
|
150
|
+
key = element.first
|
151
|
+
serialized_as = element.last
|
152
|
+
|
153
|
+
to_serialize = Mapping[named_mapping][key][:to].to_sym
|
154
|
+
if to_serialize != serialized_as
|
155
|
+
warn "[MappedRecord] overriding :#{serialized_as} with :#{to_serialize}, will not remove 'serialize :#{serialized_as}'"
|
156
|
+
self.instance_eval { serialize to_serialize }
|
157
|
+
result[key] = to_serialize
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
private :update_serialized
|
163
|
+
end
|
164
|
+
|
165
|
+
module InstanceMethods
|
166
|
+
# Accepts a hash to map and update the object with.
|
167
|
+
def update_with(hash = {}, named_mapping = nil)
|
168
|
+
named_mapping = self.class.class_name unless named_mapping
|
169
|
+
|
170
|
+
self.attributes = self.class.with_attributes(named_mapping, hash)
|
171
|
+
|
172
|
+
if !self.changes.blank?
|
173
|
+
self.save
|
174
|
+
else
|
175
|
+
false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
if Object.const_defined?("ActiveRecord")
|
182
|
+
ActiveRecord::Base.send(:include, MappedRecord)
|
183
|
+
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/mapped-record.rb'}"
|
9
|
+
puts "Loading mapped-record 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)
|
data/test/database.yml
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../test_helper.rb'
|
2
|
+
|
3
|
+
class TestMappable < Test::Unit::TestCase
|
4
|
+
context "Hash" do
|
5
|
+
setup do
|
6
|
+
@hash = {"PhotoCount"=>1, "KeyList"=>["2"], "KeyPhotoKey"=>"2", "RollID"=>3, "RollDateAsTimerInterval"=>263609145.0, "RollName"=>"May 9, 2009"}
|
7
|
+
end
|
8
|
+
|
9
|
+
should "have #map_with method" do
|
10
|
+
assert_respond_to @hash, :map_with
|
11
|
+
end
|
12
|
+
|
13
|
+
context "with mapping" do
|
14
|
+
setup do
|
15
|
+
Mapping.create :iphoto_roll2, 'PhotoCount', 'KeyList', 'RollName', 'RollID', :namespace => 'Roll', 'KeyPhotoKey' => :key_photo_id, 'RollDateAsTimerInterval' => :date, :filter => { 'RollDateAsTimerInterval' => Proc.new { |p| Time.at(p.to_f + 978307200) } }
|
16
|
+
end
|
17
|
+
|
18
|
+
should "map properly" do
|
19
|
+
mapped_hash = @hash.map_with(:iphoto_roll2)
|
20
|
+
assert_not_nil mapped_hash
|
21
|
+
assert_equal 1, mapped_hash[:photo_count]
|
22
|
+
assert_equal "2", mapped_hash[:key_photo_id]
|
23
|
+
assert_equal ["2"], mapped_hash[:key_list]
|
24
|
+
assert_equal Time.at(263609145.0 + 978307200), mapped_hash[:date]
|
25
|
+
assert_equal "May 9, 2009", mapped_hash[:name]
|
26
|
+
assert_equal 3, mapped_hash[:id]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper.rb'
|
2
|
+
|
3
|
+
class TestMapping < Test::Unit::TestCase
|
4
|
+
context "Mapping" do
|
5
|
+
setup do
|
6
|
+
@sample_hash = {"PhotoCount"=>1, "KeyList"=>["2"], "KeyPhotoKey"=>"2", "RollID"=>3, "RollDateAsTimerInterval"=>263609145.0, "RollName"=>"May 9, 2009"}
|
7
|
+
end
|
8
|
+
|
9
|
+
context "creating named mapping" do
|
10
|
+
setup do
|
11
|
+
@mapping_name = :test_mapping
|
12
|
+
@@proc = Proc.new { |p| 'PROC-ED' }
|
13
|
+
Mapping.create @mapping_name, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', 'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :filter => { 'ImplicitMapping' => @@proc }
|
14
|
+
end
|
15
|
+
|
16
|
+
should "have mapping named #{@mapping_name}" do
|
17
|
+
assert Mapping.has?(@mapping_name)
|
18
|
+
assert Mapping[@mapping_name].is_a?(Hash)
|
19
|
+
end
|
20
|
+
|
21
|
+
should "have #[] method" do
|
22
|
+
assert_respond_to Mapping, :[]
|
23
|
+
end
|
24
|
+
|
25
|
+
should "have #[]= method do the same as #create" do
|
26
|
+
assert_respond_to Mapping, :[]=
|
27
|
+
Mapping[:alt_test_mapping] = 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', {'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :filter => { 'ImplicitMapping' => @@proc }}
|
28
|
+
assert_equal Mapping[:alt_test_mapping], Mapping[@mapping_name]
|
29
|
+
end
|
30
|
+
|
31
|
+
should_map_implicit :test_mapping, 'ImplicitMapping', :implicit_mapping
|
32
|
+
should_map_implicit :test_mapping, 'AnotherMapping', :another_mapping
|
33
|
+
should_map_namespace :test_mapping, 'ForNamespaceMapping', :mapping
|
34
|
+
should_map_explicit :test_mapping, 'ExplicitMapping', :explicit
|
35
|
+
|
36
|
+
should "map proc" do
|
37
|
+
assert_same @@proc, Mapping[@mapping_name]['ImplicitMapping'][:filter]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "with options" do
|
42
|
+
|
43
|
+
setup { Mapping.reset }
|
44
|
+
|
45
|
+
should "allow symbol mapping" do
|
46
|
+
assert_nothing_raised(MappedRecord::MappingError) { Mapping.create :mixed, :key => :symbol }
|
47
|
+
Mapping.reset
|
48
|
+
assert_nothing_raised(MappedRecord::MappingError) { Mapping.create :mixed, :symbol }
|
49
|
+
end
|
50
|
+
|
51
|
+
should "not allow non-symbol mappings" do
|
52
|
+
assert_raise(MappedRecord::MappingError) { Mapping.create :mixed, :key => 1 }
|
53
|
+
assert_raise(MappedRecord::MappingError) { Mapping.create :mixed, :key => Fixnum }
|
54
|
+
assert_raise(MappedRecord::MappingError) { Mapping.create :mixed, :key => 'String' }
|
55
|
+
assert_raise(MappedRecord::MappingError) { Mapping.create :mixed, 1 }
|
56
|
+
assert_raise(MappedRecord::MappingError) { Mapping.create :mixed, Fixnum }
|
57
|
+
end
|
58
|
+
|
59
|
+
context "with mixed options" do
|
60
|
+
# 4 combinations of symbol:string
|
61
|
+
# validate :to is string or symbol
|
62
|
+
setup do
|
63
|
+
Mapping.create :mixed, :SymbolMapping, 'StringMapping', :symbol_key => :key, "symbol_key" => :other_key
|
64
|
+
end
|
65
|
+
|
66
|
+
should_map_explicit :mixed, :symbol_key, :key
|
67
|
+
should_map_explicit :mixed, "symbol_key", :other_key
|
68
|
+
should_map_implicit :mixed, 'StringMapping', :string_mapping
|
69
|
+
should_map_implicit :mixed, :SymbolMapping, :symbol_mapping
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "resetting mappings" do
|
74
|
+
should "have #reset method" do
|
75
|
+
assert_respond_to(Mapping, :reset)
|
76
|
+
end
|
77
|
+
|
78
|
+
should "clear mappings" do
|
79
|
+
assert(!Mapping.blank?, "Mapping is blank.")
|
80
|
+
Mapping.reset
|
81
|
+
assert(Mapping.blank?, "Mappings should be blank.")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "called multiple times" do
|
86
|
+
|
87
|
+
# Namespaces work on a per-#attr_mapped basis
|
88
|
+
context "with different namespaces" do
|
89
|
+
setup do
|
90
|
+
Mapping.create :diff_ns, 'ForNamespaceMapping', :namespace => 'ForNamespace'
|
91
|
+
Mapping.create :diff_ns, 'ForNamespaceMapping', :namespace => 'For'
|
92
|
+
end
|
93
|
+
|
94
|
+
should_map_namespace :diff_ns, 'ForNamespaceMapping', :namespace_mapping
|
95
|
+
end
|
96
|
+
|
97
|
+
context "with all (naturally ordered) mappings" do
|
98
|
+
setup do
|
99
|
+
Mapping.create :natural, 'OrderedMapping'
|
100
|
+
Mapping.create :natural, 'OrderedMapping', :namespace => 'Ordered'
|
101
|
+
Mapping.create :natural, 'OrderedMapping' => :ordered
|
102
|
+
end
|
103
|
+
|
104
|
+
should_map_explicit :natural, 'OrderedMapping', :ordered
|
105
|
+
end
|
106
|
+
|
107
|
+
context "with all (inverse ordered) mappings" do
|
108
|
+
setup do
|
109
|
+
Mapping.create :reversed, 'OrderedMapping' => :ordered
|
110
|
+
Mapping.create :reversed, 'OrderedMapping', :namespace => 'Ordered'
|
111
|
+
Mapping.create :reversed, 'OrderedMapping'
|
112
|
+
end
|
113
|
+
|
114
|
+
should_map_explicit :reversed, 'OrderedMapping', :ordered
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# ==================
|
119
|
+
# = Tests overlaps =
|
120
|
+
# ==================
|
121
|
+
context "with namespace and explicit overlap" do
|
122
|
+
setup { Mapping.create :ns_e_overlap, 'ImplicitMapping', 'AnotherMapping', 'ExplicitMapping' => :explicit_mapping, :namespace => 'Explicit' }
|
123
|
+
should_map_explicit :ns_e_overlap, 'ExplicitMapping', :explicit_mapping
|
124
|
+
end
|
125
|
+
|
126
|
+
context "with namespace and implicit overlap" do
|
127
|
+
setup { Mapping.create :ns_i_overlap, 'ImplicitMapping', 'ImplicitMappingSecond', 'ExplicitMapping' => :explicit_mapping, :namespace => 'Implicit' }
|
128
|
+
should_map_namespace :ns_i_overlap, 'ImplicitMapping', :mapping
|
129
|
+
end
|
130
|
+
|
131
|
+
context "with explicit and implicit overlap" do
|
132
|
+
setup { Mapping.create :e_i_overlap, 'ImplicitMapping', 'ImplicitMapping' => :explicit_mapping, :namespace => 'NoMatch' }
|
133
|
+
should_map_explicit :e_i_overlap, 'ImplicitMapping', :explicit_mapping
|
134
|
+
end
|
135
|
+
|
136
|
+
# ========================
|
137
|
+
# = Raising MappingError =
|
138
|
+
# ========================
|
139
|
+
should "raise MappingError when #namespace causes mapping to be ''" do
|
140
|
+
assert_raises MappedRecord::MappingError do
|
141
|
+
Mapping.create :ns_to_blank, 'ImplicitMapping', 'ImplicitMappingSecond', :namespace => 'ImplicitMapping'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
should "raise MappingError with many-to-one mappings" do # because it will have unexpected results
|
146
|
+
assert_raises MappedRecord::MappingError do
|
147
|
+
Mapping.create :many_to_one, 'Root', 'MappingWithRoot', :namespace => 'MappingWith'
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
should "raise MappingError with un-named mapping" do
|
152
|
+
Mapping.reset
|
153
|
+
assert_raises MappedRecord::MappingError do
|
154
|
+
Mapping.create nil, 'Root', 'MappingWithRoot', :namespace => 'MappingWith'
|
155
|
+
end
|
156
|
+
assert(Mapping.blank?, "Mapping shouldn't be set.")
|
157
|
+
end
|
158
|
+
|
159
|
+
# should "raise MappingError with invalid mapping names" do
|
160
|
+
# Mapping.reset
|
161
|
+
# assert_raise(MappedRecord::MappingError) do
|
162
|
+
# Mapping.create 1, 'Root'
|
163
|
+
# end
|
164
|
+
# end
|
165
|
+
end
|
166
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'test/unit'
|
4
|
+
gem 'thoughtbot-shoulda', ">= 2.9.0"
|
5
|
+
require 'shoulda'
|
6
|
+
|
7
|
+
gem 'sqlite3-ruby'
|
8
|
+
|
9
|
+
require 'active_record'
|
10
|
+
require 'active_support'
|
11
|
+
|
12
|
+
require File.dirname(__FILE__) + '/../lib/mapped-record'
|
13
|
+
|
14
|
+
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
|
15
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
16
|
+
ActiveRecord::Base.establish_connection(config['test'])
|
17
|
+
|
18
|
+
def reset_class class_name
|
19
|
+
ActiveRecord::Base.send(:include, MappedRecord)
|
20
|
+
Object.send(:remove_const, class_name) rescue nil
|
21
|
+
klass = Object.const_set(class_name, Class.new(ActiveRecord::Base))
|
22
|
+
klass.class_eval{ include MappedRecord }
|
23
|
+
klass
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset_table table_name, &block
|
27
|
+
block ||= lambda{ true }
|
28
|
+
ActiveRecord::Base.connection.create_table :dummies, {:force => true}, &block
|
29
|
+
end
|
30
|
+
|
31
|
+
def modify_table table_name, &block
|
32
|
+
ActiveRecord::Base.connection.change_table :dummies, &block
|
33
|
+
end
|
34
|
+
|
35
|
+
def rebuild_model(*args)
|
36
|
+
ActiveRecord::Base.connection.create_table :dummies, :force => true do |t|
|
37
|
+
t.string :name
|
38
|
+
t.datetime :date
|
39
|
+
t.references :key_photo
|
40
|
+
t.integer :photo_count
|
41
|
+
t.text :key_list
|
42
|
+
t.references :library
|
43
|
+
end
|
44
|
+
rebuild_class(*args)
|
45
|
+
end
|
46
|
+
|
47
|
+
def rebuild_class(*args)
|
48
|
+
ActiveRecord::Base.send(:include, MappedRecord)
|
49
|
+
Object.send(:remove_const, "Dummy") rescue nil
|
50
|
+
Object.const_set("Dummy", Class.new(ActiveRecord::Base))
|
51
|
+
Dummy.class_eval do
|
52
|
+
include MappedRecord
|
53
|
+
end
|
54
|
+
if args.size > 0
|
55
|
+
Dummy.class_eval do
|
56
|
+
attr_mapped_named(*args)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def should_map name, field, mapping, type, klass=Mapping
|
62
|
+
type_s = ''
|
63
|
+
case type
|
64
|
+
when MappedRecord::EXPLICIT
|
65
|
+
type_s = 'explicit'
|
66
|
+
when MappedRecord::IMPLICIT
|
67
|
+
type_s = 'implicit'
|
68
|
+
when MappedRecord::NAMESPACE
|
69
|
+
type_s = 'namespace'
|
70
|
+
else
|
71
|
+
raise "Unknown mapping type"
|
72
|
+
end
|
73
|
+
|
74
|
+
should "map #{type_s} from #{field} => #{mapping} for mapping :#{name}" do
|
75
|
+
assert_not_nil(klass.blank?, "Mappings not set up correctly.")
|
76
|
+
assert_not_nil(klass[name], "Mapping #{name} not set up correctly.")
|
77
|
+
assert_not_nil(klass[name][field], "Mapping #{name}'s #{field} not set up correctly.")
|
78
|
+
assert_equal(mapping, klass[name][field][:to], "Mapping doesn't match.")
|
79
|
+
assert_equal(type, klass[name][field][:type], "Mapping type doesn't match.")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def should_map_explicit name, field, mapping, klass=Mapping
|
84
|
+
should_map name, field, mapping, MappedRecord::EXPLICIT, klass
|
85
|
+
end
|
86
|
+
|
87
|
+
def should_map_implicit name, field, mapping, klass=Mapping
|
88
|
+
should_map name, field, mapping, MappedRecord::IMPLICIT, klass
|
89
|
+
end
|
90
|
+
|
91
|
+
def should_map_namespace name, field, mapping, klass=Mapping
|
92
|
+
should_map name, field, mapping, MappedRecord::NAMESPACE, klass
|
93
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
|
+
|
3
|
+
class TestMappedRecord < Test::Unit::TestCase
|
4
|
+
context "Dummy" do
|
5
|
+
setup do
|
6
|
+
rebuild_class
|
7
|
+
end
|
8
|
+
|
9
|
+
should "have #attr_mapped method" do
|
10
|
+
assert_respond_to Dummy, :attr_mapped
|
11
|
+
end
|
12
|
+
|
13
|
+
should "have #attr_mapped_named method" do
|
14
|
+
assert_respond_to Dummy, :attr_mapped_named
|
15
|
+
end
|
16
|
+
|
17
|
+
should "act like mapped" do
|
18
|
+
assert Dummy.acts_like?(:mapped)
|
19
|
+
end
|
20
|
+
|
21
|
+
should "raise ArgumentError when no mapping name and no options not given" do
|
22
|
+
assert_raises ArgumentError do
|
23
|
+
Dummy.class_eval do
|
24
|
+
attr_mapped_named
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
should "raise MappingError when name given but options not given" do
|
30
|
+
assert_raises MappedRecord::MappingError do
|
31
|
+
rebuild_class 'some_name'
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# =======================
|
36
|
+
# = Setting up mappings =
|
37
|
+
# =======================
|
38
|
+
context "when #attr_mapped" do
|
39
|
+
context "untitled" do
|
40
|
+
setup do
|
41
|
+
Mapping.reset
|
42
|
+
Dummy.class_eval do
|
43
|
+
attr_mapped 'FullName', 'Email'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
should "map with class_name as default mapping name" do
|
48
|
+
assert(Mapping.has?(Dummy.class_name), "Mapping name not #{Dummy.class_name}.")
|
49
|
+
end
|
50
|
+
|
51
|
+
should_map_implicit 'Dummy', 'FullName', :full_name
|
52
|
+
should_map_implicit 'Dummy', 'Email', :email
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "when #attr_mapped_named" do # TODO should not allow mapping names with spaces
|
57
|
+
context "with dummy mapping and options" do
|
58
|
+
|
59
|
+
setup do
|
60
|
+
Mapping.reset
|
61
|
+
@@proc = Proc.new { |p| 'PROC-ED' }
|
62
|
+
rebuild_class :dummy, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', 'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :id => 'ForID', :filter => { 'ImplicitMapping' => @@proc }, :serialize => 'ForNamespaceMapping'
|
63
|
+
end
|
64
|
+
|
65
|
+
should_map_implicit :dummy, 'ImplicitMapping', :implicit_mapping
|
66
|
+
should_map_implicit :dummy, 'AnotherMapping', :another_mapping
|
67
|
+
should_map_namespace :dummy, 'ForNamespaceMapping', :mapping
|
68
|
+
should_map_explicit :dummy, 'ExplicitMapping', :explicit
|
69
|
+
|
70
|
+
should "map id" do
|
71
|
+
assert_equal 'ForID', Dummy.attr_hashed_id
|
72
|
+
end
|
73
|
+
|
74
|
+
should "map proc" do
|
75
|
+
assert_same @@proc, Mapping[:dummy]['ImplicitMapping'][:filter]
|
76
|
+
end
|
77
|
+
|
78
|
+
should "serialize" do
|
79
|
+
assert Dummy.serialized_attributes.include?("mapping")
|
80
|
+
end
|
81
|
+
|
82
|
+
context "a subclass" do
|
83
|
+
setup do
|
84
|
+
class ::SubDummy < Dummy; end
|
85
|
+
end
|
86
|
+
|
87
|
+
should "map id" do
|
88
|
+
assert_equal 'ForID', SubDummy.attr_hashed_id
|
89
|
+
end
|
90
|
+
|
91
|
+
should "serialize" do
|
92
|
+
assert SubDummy.serialized_attributes.include?("mapping")
|
93
|
+
end
|
94
|
+
|
95
|
+
teardown do
|
96
|
+
Object.send(:remove_const, "SubDummy") rescue nil
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "with multiple serializes" do
|
102
|
+
setup do
|
103
|
+
Mapping.reset
|
104
|
+
rebuild_class :multiple_s, 'KeyOne', 'KeyTwo', :serialize => ['KeyOne', 'KeyTwo']
|
105
|
+
end
|
106
|
+
|
107
|
+
should "serialize :key_one and :key_two" do
|
108
|
+
assert Dummy.serialized_attributes.include?("key_one")
|
109
|
+
assert Dummy.serialized_attributes.include?("key_two")
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# ========================
|
114
|
+
# = Raising MappingError =
|
115
|
+
# ========================
|
116
|
+
|
117
|
+
should "raise MappingError when #serialize and #id overlap" do
|
118
|
+
assert_raises MappedRecord::MappingError do
|
119
|
+
rebuild_class :ser, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', 'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :id => 'AnotherMapping', :serialize => 'AnotherMapping'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# ============================================
|
124
|
+
# = #attr_mapped_named called multiple times =
|
125
|
+
# ============================================
|
126
|
+
context "called multiple times" do
|
127
|
+
|
128
|
+
context "with serialized mapping being overridden" do
|
129
|
+
setup do
|
130
|
+
rebuild_class
|
131
|
+
Dummy.class_eval do
|
132
|
+
attr_mapped_named :overriding, 'AMapping', :serialize => 'AMapping'
|
133
|
+
attr_mapped_named :overriding, 'AMapping' => :mapping
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
should "update serialize" do
|
138
|
+
assert Dummy.serialized_attributes.include?("mapping")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# ======================
|
145
|
+
# = Creating with hash =
|
146
|
+
# ======================
|
147
|
+
context "creating" do
|
148
|
+
context "with named mapping" do
|
149
|
+
setup do
|
150
|
+
Mapping.reset
|
151
|
+
rebuild_model
|
152
|
+
Dummy.class_eval do
|
153
|
+
attr_mapped_named :iphoto_roll, 'PhotoCount', 'KeyList', 'RollName', :namespace => 'Roll', 'KeyPhotoKey' => :key_photo_id, 'RollDateAsTimerInterval' => :date, :id => 'RollID', :serialize => 'KeyList', :filter => { 'RollDateAsTimerInterval' => Proc.new { |p| Time.at(p.to_f + 978307200) } }
|
154
|
+
end
|
155
|
+
@rand = rand(10000)
|
156
|
+
@sample_hash = {"PhotoCount"=>1, "KeyList"=>["2"], "KeyPhotoKey"=>"2", "RollID"=>@rand, "RollDateAsTimerInterval"=>263609145.0, "RollName"=>"May 9, 2009"}
|
157
|
+
@dummy = Dummy.create_with_iphoto_roll(@sample_hash)
|
158
|
+
assert_not_nil @dummy
|
159
|
+
end
|
160
|
+
|
161
|
+
should "have dynamic #create_with_iphoto_roll method" do
|
162
|
+
assert_respond_to Dummy, :create_with_iphoto_roll
|
163
|
+
end
|
164
|
+
|
165
|
+
should "not mass-assign #id" do
|
166
|
+
assert !Dummy.with_attributes(:iphoto_roll, @sample_hash).include?(:id)
|
167
|
+
end
|
168
|
+
|
169
|
+
should "create properly" do
|
170
|
+
assert_equal 1, @dummy.photo_count
|
171
|
+
assert_equal 2, @dummy.key_photo_id
|
172
|
+
assert_equal ["2"], @dummy.key_list
|
173
|
+
assert_equal Time.at(263609145.0 + 978307200), @dummy.date
|
174
|
+
assert_equal "May 9, 2009", @dummy.name
|
175
|
+
assert_equal @rand, @dummy.id
|
176
|
+
end
|
177
|
+
|
178
|
+
should "have #update_with method" do
|
179
|
+
assert_respond_to @dummy, :update_with
|
180
|
+
end
|
181
|
+
|
182
|
+
should "have dynamic #update_with_iphoto_roll method" do
|
183
|
+
assert_respond_to @dummy, :update_with_iphoto_roll
|
184
|
+
end
|
185
|
+
|
186
|
+
should "fail with empty update" do
|
187
|
+
assert !@dummy.update_with_iphoto_roll({})
|
188
|
+
end
|
189
|
+
|
190
|
+
should "fail updating with no changes" do
|
191
|
+
assert !@dummy.update_with_iphoto_roll(@sample_hash)
|
192
|
+
end
|
193
|
+
|
194
|
+
context "then updating" do
|
195
|
+
|
196
|
+
setup do
|
197
|
+
@update_hash = {"PhotoCount"=>2, "KeyList"=>["2", "3"], "KeyPhotoKey"=>"3", "RollID"=>rand(10000), "RollDateAsTimerInterval"=>263609245.0, "RollName"=>"NewName"}
|
198
|
+
assert_not_nil @dummy
|
199
|
+
end
|
200
|
+
|
201
|
+
should "update properly" do
|
202
|
+
assert @dummy.update_with_iphoto_roll(@update_hash)
|
203
|
+
assert_equal 2, @dummy.photo_count
|
204
|
+
assert_equal 3, @dummy.key_photo_id
|
205
|
+
assert_equal ["2", "3"], @dummy.key_list
|
206
|
+
assert_equal Time.at(263609245.0 + 978307200), @dummy.date
|
207
|
+
assert_equal "NewName", @dummy.name
|
208
|
+
assert_equal @rand, @dummy.id
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# the rest relies on named mapping (above)
|
215
|
+
context "with unnamed mapping" do
|
216
|
+
setup do
|
217
|
+
Mapping.reset
|
218
|
+
rebuild_model
|
219
|
+
Dummy.class_eval do
|
220
|
+
attr_mapped 'PhotoCount', 'KeyList', 'RollName', :namespace => 'Roll', 'KeyPhotoKey' => :key_photo_id, 'RollDateAsTimerInterval' => :date, :id => 'RollID', :serialize => 'KeyList', :filter => { 'RollDateAsTimerInterval' => Proc.new { |p| Time.at(p.to_f + 978307200) } }
|
221
|
+
end
|
222
|
+
@rand = rand(10000)
|
223
|
+
@sample_hash = {"PhotoCount"=>1, "KeyList"=>["2"], "KeyPhotoKey"=>"2", "RollID"=>@rand, "RollDateAsTimerInterval"=>263609145.0, "RollName"=>"May 9, 2009"}
|
224
|
+
@dummy = Dummy.create_with(@sample_hash)
|
225
|
+
end
|
226
|
+
|
227
|
+
should "have #create_with method" do
|
228
|
+
assert_respond_to Dummy, :create_with
|
229
|
+
end
|
230
|
+
|
231
|
+
should "create properly" do
|
232
|
+
assert_not_nil @dummy
|
233
|
+
assert_equal 1, @dummy.photo_count
|
234
|
+
assert_equal 2, @dummy.key_photo_id
|
235
|
+
assert_equal ["2"], @dummy.key_list
|
236
|
+
assert_equal Time.at(263609145.0 + 978307200), @dummy.date
|
237
|
+
assert_equal "May 9, 2009", @dummy.name
|
238
|
+
assert_equal @rand, @dummy.id
|
239
|
+
end
|
240
|
+
|
241
|
+
context "then updating" do
|
242
|
+
|
243
|
+
setup do
|
244
|
+
@update_hash = {"PhotoCount"=>2, "KeyList"=>["2", "3"], "KeyPhotoKey"=>"3", "RollID"=>rand(10000), "RollDateAsTimerInterval"=>263609245.0, "RollName"=>"NewName"}
|
245
|
+
assert_not_nil @dummy
|
246
|
+
end
|
247
|
+
|
248
|
+
should "update properly" do
|
249
|
+
assert @dummy.update_with(@update_hash)
|
250
|
+
assert_equal 2, @dummy.photo_count
|
251
|
+
assert_equal 3, @dummy.key_photo_id
|
252
|
+
assert_equal ["2", "3"], @dummy.key_list
|
253
|
+
assert_equal Time.at(263609245.0 + 978307200), @dummy.date
|
254
|
+
assert_equal "NewName", @dummy.name
|
255
|
+
assert_equal @rand, @dummy.id
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hsume2-mapped-record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henry Hsu
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-07 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: activesupport
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.0.2
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: newgem
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.4.1
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: thoughtbot-shoulda
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: sqlite3-ruby
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: hoe
|
57
|
+
type: :development
|
58
|
+
version_requirement:
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 1.8.0
|
64
|
+
version:
|
65
|
+
description: Auto-magically map Hash[keys] to ActiveRecord.attributes
|
66
|
+
email:
|
67
|
+
- henry@qlane.com
|
68
|
+
executables: []
|
69
|
+
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files:
|
73
|
+
- History.txt
|
74
|
+
- Manifest.txt
|
75
|
+
- README.rdoc
|
76
|
+
files:
|
77
|
+
- History.txt
|
78
|
+
- LICENSE
|
79
|
+
- Manifest.txt
|
80
|
+
- README.rdoc
|
81
|
+
- Rakefile
|
82
|
+
- lib/mapped-record.rb
|
83
|
+
- lib/mapped-record/hash/mappable.rb
|
84
|
+
- lib/mapped-record/mapping.rb
|
85
|
+
- script/console
|
86
|
+
- script/destroy
|
87
|
+
- script/generate
|
88
|
+
- test/database.yml
|
89
|
+
- test/hashed-record/hash/test_mappable.rb
|
90
|
+
- test/hashed-record/test_mapping.rb
|
91
|
+
- test/test_helper.rb
|
92
|
+
- test/test_mapped_record.rb
|
93
|
+
has_rdoc: false
|
94
|
+
homepage: http://github.com/hsume2/mapped-record
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options:
|
97
|
+
- --main
|
98
|
+
- README.rdoc
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: "0"
|
106
|
+
version:
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: "0"
|
112
|
+
version:
|
113
|
+
requirements: []
|
114
|
+
|
115
|
+
rubyforge_project: mapped-record
|
116
|
+
rubygems_version: 1.2.0
|
117
|
+
signing_key:
|
118
|
+
specification_version: 3
|
119
|
+
summary: Auto-magically map Hash[keys] to ActiveRecord.attributes
|
120
|
+
test_files:
|
121
|
+
- test/hashed-record/hash/test_mappable.rb
|
122
|
+
- test/hashed-record/test_mapping.rb
|
123
|
+
- test/test_helper.rb
|
124
|
+
- test/test_mapped_record.rb
|