hsume2-mapped-record 0.0.1
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 +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
|