hsume2-mapped-record 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,3 +2,12 @@
2
2
 
3
3
  * 1 major enhancement:
4
4
  * Initial release
5
+
6
+ == 0.0.1 2009-06-13
7
+
8
+ * 5 minor enhancements:
9
+ * using define_method instead of instance_eval
10
+ * using indifferent access
11
+ * added coverage (99.6%)
12
+ * disallow invalid attr_mapped_named names
13
+ * added better exceptions
@@ -1,18 +1,18 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
1
4
  # See +attr_mapped+ or the README for details.
2
5
  #
3
6
  #
4
7
  # Created by Henry Hsu on 2009-06-07.
5
8
  # 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__)))
9
+ #
10
10
 
11
11
  require 'mapped-record/hash/mappable'
12
12
  require 'mapped-record/mapping'
13
13
 
14
14
  module MappedRecord
15
- VERSION = '0.0.1' # :nodoc:
15
+ VERSION = '0.0.2' # :nodoc:
16
16
 
17
17
  IMPLICIT = 0 # :nodoc:
18
18
  NAMESPACE = 1 # :nodoc:
@@ -21,14 +21,36 @@ module MappedRecord
21
21
  class << self
22
22
  def included base #:nodoc:
23
23
  base.extend ClassMethods
24
+ base.send :include, InstanceMethods
24
25
  end
25
26
  end
26
-
27
- #--
28
- # TODO subclass this to signify each kind of error
29
- #++
27
+
30
28
  class MappingError < StandardError; end # :nodoc:
31
29
 
30
+ class TargetError < MappingError
31
+ attr_reader :message
32
+ def initialize(mapping_name, key, target = nil, expected = [])
33
+ @message = "Got #{target.class} for target. #{expected.join(' or ')} expected." if target
34
+ @message = "Got #{key.class} for key. #{expected.join(' or ')} expected." unless target
35
+ end
36
+ end
37
+
38
+ class NamespaceError < MappingError; end
39
+
40
+ class CollisionError < MappingError
41
+ attr_reader :message
42
+ def initialize(mapping_name, key, value, collisions)
43
+ targets = collisions.map { |c| c.first } << key
44
+ @message = "Collision: #{targets.join(', ')} map to '#{value}'"
45
+ end
46
+ end
47
+
48
+ class NameError < MappingError
49
+ def initialize(message)
50
+ @message = "#{message} is an invalid mapping name."
51
+ end
52
+ end
53
+
32
54
  module ClassMethods
33
55
  # Assigns a mapping for the current ActiveRecord class.
34
56
  #
@@ -57,7 +79,7 @@ module MappedRecord
57
79
  def attr_mapped(*map_attrs)
58
80
  attr_mapped_named(class_name, *map_attrs)
59
81
  end
60
-
82
+
61
83
  # Assigns mappings to a name.
62
84
  #
63
85
  # class Person < ActiveRecord::Base
@@ -69,11 +91,9 @@ module MappedRecord
69
91
  # p = Person.create_with_public({ 'PBName' => 'Mr. Name' })
70
92
  # p.update_with_public({ 'PBName' => 'Full Name' })
71
93
  def attr_mapped_named(named_mapping = nil, *map_attrs)
72
- include InstanceMethods
73
-
74
94
  unless self.respond_to?(:attr_mapped_serialized)
75
95
  class_inheritable_accessor :attr_mapped_serialized
76
- write_inheritable_attribute :attr_mapped_serialized, Hash.new
96
+ write_inheritable_attribute :attr_mapped_serialized, HashWithIndifferentAccess.new
77
97
  end
78
98
 
79
99
  unless self.respond_to?(:attr_hashed_id)
@@ -81,7 +101,8 @@ module MappedRecord
81
101
  write_inheritable_attribute :attr_hashed_id, ''
82
102
  end
83
103
 
84
- raise ArgumentError, "Mapping name not given." if named_mapping.nil?
104
+ raise NameError.new(named_mapping) if named_mapping.nil?
105
+ raise NameError.new(named_mapping) unless named_mapping.to_s[/[a-zA-Z_]?[a-zA-Z_0-9]+/] == named_mapping.to_s
85
106
  raise MappingError, "No options given." if map_attrs.blank?
86
107
 
87
108
  options = map_attrs.extract_options!
@@ -93,8 +114,14 @@ module MappedRecord
93
114
  when :id
94
115
  self.attr_hashed_id = value.to_s
95
116
  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)
117
+ case value
118
+ when String, Symbol, Array
119
+ else
120
+ raise TargetError.new(named_mapping, key, value, [String, Symbol, Array])
121
+ end
122
+
123
+ value = [value] unless value.kind_of?(Array)
124
+ keys = value.collect { |v| v.to_s }
98
125
  serialize_mappings |= keys
99
126
  end
100
127
  end
@@ -105,9 +132,15 @@ module MappedRecord
105
132
  Mapping.create named_mapping, *map_attrs
106
133
 
107
134
  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
135
+ # create dynamic methods
136
+ self.class.send(:define_method, :"create_with_#{named_mapping}") do |hash|
137
+ create_with(hash, :"#{named_mapping}")
138
+ end
139
+ self.send(:define_method, :"update_with_#{named_mapping}") do |hash|
140
+ update_with(hash, :"#{named_mapping}")
141
+ end
142
+
143
+ self.attr_mapped_serialized[named_mapping] ||= HashWithIndifferentAccess.new
111
144
  self.attr_mapped_serialized[named_mapping] = update_serialized(named_mapping)
112
145
  end
113
146
 
@@ -132,11 +165,6 @@ module MappedRecord
132
165
  end
133
166
  end
134
167
 
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
168
  # Maps the values in +hash+ with +named_mapping+ for use in Active Record.
141
169
  def with_attributes(named_mapping, hash)
142
170
  attrs = hash.map_with(named_mapping)
@@ -166,7 +194,7 @@ module MappedRecord
166
194
  # Accepts a hash to map and update the object with.
167
195
  def update_with(hash = {}, named_mapping = nil)
168
196
  named_mapping = self.class.class_name unless named_mapping
169
-
197
+
170
198
  self.attributes = self.class.with_attributes(named_mapping, hash)
171
199
 
172
200
  if !self.changes.blank?
@@ -20,13 +20,11 @@ module MappedRecord
20
20
  # [<tt>'key' => :target, 'key2' => :target2, ...</tt>]
21
21
  # As many manual mappings as needed.
22
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
23
+ raise NameError, "Not creating mapping with nil name" if mapping_name.nil?
24
+ named_mappings[mapping_name] ||= HashWithIndifferentAccess.new
25
25
 
26
26
  options = map_attrs.extract_options!
27
- verbose = parse_verbose(options)
28
27
 
29
- serialize_mappings = []
30
28
  namespace = nil
31
29
  type = IMPLICIT
32
30
 
@@ -36,25 +34,24 @@ module MappedRecord
36
34
  namespace = value.to_s unless value.to_s.blank?
37
35
  when :filter
38
36
  value.each_pair do |attr, proc|
39
- named_mappings[mapping_name][attr.to_s] ||= Hash.new
37
+ named_mappings[mapping_name][attr.to_s] ||= HashWithIndifferentAccess.new
40
38
  named_mappings[mapping_name][attr.to_s][:filter] = proc
41
39
  end
42
40
  when String, Symbol # deals with explicit mappings
43
- raise MappingError, "Must be symbol" unless value.kind_of?(Symbol)
41
+ raise TargetError.new(mapping_name, key, value, [String, Symbol]) unless value.kind_of?(Symbol)
44
42
  update_mapping(mapping_name, key, value.to_sym, EXPLICIT)
45
43
  end
46
44
  end
47
45
 
48
- mapping_temp = Hash.new
46
+ mapping_temp = HashWithIndifferentAccess.new
49
47
 
50
48
  map_attrs.each do |attr|
51
- raise MappingError, "Must be string or symbol." unless attr.kind_of?(String) or attr.kind_of?(Symbol)
49
+ raise TargetError.new(mapping_name, attr, nil, [Symbol]) unless attr.kind_of?(String) or attr.kind_of?(Symbol)
52
50
 
53
51
  mapped_attr = attr.to_s
54
52
  if namespace
55
- match = mapped_attr[/^#{namespace}/]
56
- raise MappingError, "Causes mapping to be ''" if mapped_attr == match
57
- if match
53
+ raise NamespaceError, "Causes mapping to be ''" if mapped_attr == namespace
54
+ if mapped_attr.match(/^#{namespace}/)
58
55
  mapped_attr = mapped_attr.sub(/^#{namespace}/, '')
59
56
  type = NAMESPACE
60
57
  end
@@ -66,13 +63,13 @@ module MappedRecord
66
63
  named_mappings[mapping_name]
67
64
  end
68
65
 
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
66
+ # def parse_verbose(options) # :nodoc:
67
+ # if !options[:verbose].nil? && (options[:verbose].kind_of?(FalseClass) || options[:verbose].kind_of?(TrueClass))
68
+ # verbose = options[:verbose]
69
+ # options.delete(:verbose)
70
+ # verbose
71
+ # end
72
+ # end
76
73
 
77
74
  # Returns true if mapping of +mapping_name+ is assigned.
78
75
  def has?(mapping_name)
@@ -104,20 +101,27 @@ module MappedRecord
104
101
  create(key, *values)
105
102
  end
106
103
 
107
- attr_accessor :named_mappings # :nodoc:
104
+ attr_reader :named_mappings # :nodoc:
108
105
 
109
106
  def named_mappings # :nodoc:
110
- @named_mappings ||= Hash.new
107
+ @named_mappings ||= HashWithIndifferentAccess.new
111
108
  end
112
109
 
113
- private :named_mappings
110
+ # Returns all defined mappings.
111
+ def all
112
+ named_mappings
113
+ end
114
114
 
115
115
  def update_mapping(mapping_name, key, value, type) # :nodoc:
116
116
  named_mapping = named_mappings[mapping_name]
117
- named_mapping[key] ||= Hash.new
117
+ named_mapping[key] ||= HashWithIndifferentAccess.new
118
118
 
119
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?
120
+ # check collision
121
+ collisions = named_mapping.select { |key_name, mapping| key_name.to_s != key.to_s && mapping[:to] == value }
122
+ raise CollisionError.new(mapping_name, key, value, collisions) unless collisions.blank?
123
+
124
+ # assign mapping
121
125
  named_mapping[key][:to] = value and named_mapping[key][:type] = type
122
126
  end
123
127
  end
@@ -5,17 +5,22 @@ class TestMapping < Test::Unit::TestCase
5
5
  setup do
6
6
  @sample_hash = {"PhotoCount"=>1, "KeyList"=>["2"], "KeyPhotoKey"=>"2", "RollID"=>3, "RollDateAsTimerInterval"=>263609145.0, "RollName"=>"May 9, 2009"}
7
7
  end
8
+
9
+ should "use use hash with indifferent access" do
10
+ assert(Mapping.send(:named_mappings).kind_of?(HashWithIndifferentAccess), "Should be hash with indifferent access.")
11
+ end
8
12
 
9
13
  context "creating named mapping" do
10
14
  setup do
11
15
  @mapping_name = :test_mapping
12
16
  @@proc = Proc.new { |p| 'PROC-ED' }
13
- Mapping.create @mapping_name, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', 'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :filter => { 'ImplicitMapping' => @@proc }
17
+ Mapping.create @mapping_name, 'ImplicitMapping', :AnotherMapping, 'ForNamespaceMapping', 'ExplicitMapping' => :explicit, :namespace => 'ForNamespace', :filter => { 'ImplicitMapping' => @@proc }
14
18
  end
15
19
 
16
20
  should "have mapping named #{@mapping_name}" do
17
21
  assert Mapping.has?(@mapping_name)
18
22
  assert Mapping[@mapping_name].is_a?(Hash)
23
+ assert Mapping.all.include?(@mapping_name)
19
24
  end
20
25
 
21
26
  should "have #[] method" do
@@ -43,27 +48,29 @@ class TestMapping < Test::Unit::TestCase
43
48
  setup { Mapping.reset }
44
49
 
45
50
  should "allow symbol mapping" do
46
- assert_nothing_raised(MappedRecord::MappingError) { Mapping.create :mixed, :key => :symbol }
51
+ assert_nothing_raised(MappedRecord::TargetError) { Mapping.create :mixed, :key => :symbol }
47
52
  Mapping.reset
48
- assert_nothing_raised(MappedRecord::MappingError) { Mapping.create :mixed, :symbol }
53
+ assert_nothing_raised(MappedRecord::TargetError) { Mapping.create :mixed, :symbol }
49
54
  end
50
55
 
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 }
56
+ should "raise TargetError with non-Symbol targets" do
57
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, :key => 1 }
58
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, :key => Fixnum }
59
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, :key => 'String' }
60
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, 'key2' => 'String' }
61
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, 1 }
62
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, Fixnum }
63
+ assert_raise(MappedRecord::TargetError) { Mapping.create :mixed, Mapping.class }
57
64
  end
58
65
 
59
66
  context "with mixed options" do
60
67
  # 4 combinations of symbol:string
61
68
  # validate :to is string or symbol
62
69
  setup do
63
- Mapping.create :mixed, :SymbolMapping, 'StringMapping', :symbol_key => :key, "symbol_key" => :other_key
70
+ Mapping.create :mixed, :SymbolMapping, 'StringMapping', :symbol_key, "symbol_key" => :other_key
64
71
  end
65
72
 
66
- should_map_explicit :mixed, :symbol_key, :key
73
+ should_map_explicit :mixed, :symbol_key, :other_key
67
74
  should_map_explicit :mixed, "symbol_key", :other_key
68
75
  should_map_implicit :mixed, 'StringMapping', :string_mapping
69
76
  should_map_implicit :mixed, :SymbolMapping, :symbol_mapping
@@ -77,8 +84,10 @@ class TestMapping < Test::Unit::TestCase
77
84
 
78
85
  should "clear mappings" do
79
86
  assert(!Mapping.blank?, "Mapping is blank.")
87
+ assert(!Mapping.empty?, "Mapping is blank.")
80
88
  Mapping.reset
81
89
  assert(Mapping.blank?, "Mappings should be blank.")
90
+ assert(Mapping.empty?, "Mappings should be blank.")
82
91
  end
83
92
  end
84
93
 
@@ -136,30 +145,32 @@ class TestMapping < Test::Unit::TestCase
136
145
  # ========================
137
146
  # = Raising MappingError =
138
147
  # ========================
139
- should "raise MappingError when #namespace causes mapping to be ''" do
140
- assert_raises MappedRecord::MappingError do
148
+ should "raise NamespaceError when #namespace causes mapping to be ''" do
149
+ assert_raises MappedRecord::NamespaceError do
141
150
  Mapping.create :ns_to_blank, 'ImplicitMapping', 'ImplicitMappingSecond', :namespace => 'ImplicitMapping'
142
151
  end
143
152
  end
144
153
 
145
- should "raise MappingError with many-to-one mappings" do # because it will have unexpected results
146
- assert_raises MappedRecord::MappingError do
154
+ should "raise CollisionError with many-to-one mappings" do # because it will have unexpected results
155
+ assert_raises MappedRecord::CollisionError do
147
156
  Mapping.create :many_to_one, 'Root', 'MappingWithRoot', :namespace => 'MappingWith'
148
157
  end
149
158
  end
150
159
 
151
- should "raise MappingError with un-named mapping" do
160
+ should "raise NameError with un-named mapping" do
152
161
  Mapping.reset
153
- assert_raises MappedRecord::MappingError do
162
+ assert_raises MappedRecord::NameError do
154
163
  Mapping.create nil, 'Root', 'MappingWithRoot', :namespace => 'MappingWith'
155
164
  end
156
165
  assert(Mapping.blank?, "Mapping shouldn't be set.")
157
166
  end
158
167
 
159
- # should "raise MappingError with invalid mapping names" do
168
+ # should "raise NameError with invalid mapping names" do
160
169
  # Mapping.reset
161
- # assert_raise(MappedRecord::MappingError) do
170
+ # assert_raise(MappedRecord::NameError) do
162
171
  # Mapping.create 1, 'Root'
172
+ # Mapping.create 'name with spaces', 'Root'
173
+ # Mapping.create 'name with |n\/al|# ch*r%ct!rs', 'Root'
163
174
  # end
164
175
  # end
165
176
  end
@@ -71,7 +71,7 @@ def should_map name, field, mapping, type, klass=Mapping
71
71
  raise "Unknown mapping type"
72
72
  end
73
73
 
74
- should "map #{type_s} from #{field} => #{mapping} for mapping :#{name}" do
74
+ should "map #{type_s} from #{field.kind_of?(Symbol) ? ':' : ''}#{field} => #{mapping} for mapping :#{name}" do
75
75
  assert_not_nil(klass.blank?, "Mappings not set up correctly.")
76
76
  assert_not_nil(klass[name], "Mapping #{name} not set up correctly.")
77
77
  assert_not_nil(klass[name][field], "Mapping #{name}'s #{field} not set up correctly.")
@@ -14,12 +14,8 @@ class TestMappedRecord < Test::Unit::TestCase
14
14
  assert_respond_to Dummy, :attr_mapped_named
15
15
  end
16
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
17
+ should "raise NameError when no mapping name and no options not given" do
18
+ assert_raises MappedRecord::NameError do
23
19
  Dummy.class_eval do
24
20
  attr_mapped_named
25
21
  end
@@ -53,7 +49,7 @@ class TestMappedRecord < Test::Unit::TestCase
53
49
  end
54
50
  end
55
51
 
56
- context "when #attr_mapped_named" do # TODO should not allow mapping names with spaces
52
+ context "when #attr_mapped_named" do
57
53
  context "with dummy mapping and options" do
58
54
 
59
55
  setup do
@@ -79,6 +75,26 @@ class TestMappedRecord < Test::Unit::TestCase
79
75
  assert Dummy.serialized_attributes.include?("mapping")
80
76
  end
81
77
 
78
+ context "with indifferent access" do
79
+ setup do
80
+ rebuild_class :dummy2, :ImplicitMapping, :AnotherMapping, :ForNamespaceMapping, :ExplicitMapping => :explicit, :namespace => 'ForNamespace', :id => 'ForID', :filter => { :ImplicitMapping => @@proc }, :serialize => :ImplicitMapping
81
+ end
82
+
83
+ # using same tests as above
84
+ should_map_implicit :dummy2, 'ImplicitMapping', :implicit_mapping
85
+ should_map_implicit :dummy2, 'AnotherMapping', :another_mapping
86
+ should_map_namespace :dummy2, 'ForNamespaceMapping', :mapping
87
+ should_map_explicit :dummy2, 'ExplicitMapping', :explicit
88
+
89
+ should "map proc" do
90
+ assert_same @@proc, Mapping[:dummy2]['ImplicitMapping'][:filter]
91
+ end
92
+
93
+ should "serialize" do
94
+ assert Dummy.serialized_attributes.include?("implicit_mapping")
95
+ end
96
+ end
97
+
82
98
  context "a subclass" do
83
99
  setup do
84
100
  class ::SubDummy < Dummy; end
@@ -120,6 +136,20 @@ class TestMappedRecord < Test::Unit::TestCase
120
136
  end
121
137
  end
122
138
 
139
+ should "raise NameError when mapping name has invalid characters" do
140
+ assert_raises MappedRecord::NameError do
141
+ rebuild_class 'name with spaces', 'ImplicitMapping'
142
+ rebuild_class 'namewith|n\/al|#ch*r%ct!rs', 'ImplicitMapping'
143
+ end
144
+ end
145
+
146
+ should "raise TargetError when serialize target is not String, Symbol or Array" do
147
+ assert_raises MappedRecord::TargetError do
148
+ rebuild_class :serialize_fail, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', :serialize => Fixnum
149
+ rebuild_class :serialize_fail, 'ImplicitMapping', 'AnotherMapping', 'ForNamespaceMapping', :serialize => Proc.new{ |p| puts p }
150
+ end
151
+ end
152
+
123
153
  # ============================================
124
154
  # = #attr_mapped_named called multiple times =
125
155
  # ============================================
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hsume2-mapped-record
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henry Hsu
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-06-07 00:00:00 -07:00
12
+ date: 2009-06-13 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency