hsume2-mapped-record 0.0.1 → 0.0.2

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.
@@ -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