dm-types 1.1.0 → 1.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -5,32 +5,33 @@ source 'http://rubygems.org'
5
5
  SOURCE = ENV.fetch('SOURCE', :git).to_sym
6
6
  REPO_POSTFIX = SOURCE == :path ? '' : '.git'
7
7
  DATAMAPPER = SOURCE == :path ? Pathname(__FILE__).dirname.parent : 'http://github.com/datamapper'
8
- DM_VERSION = '~> 1.1.0'
9
- DO_VERSION = '~> 0.10.2'
8
+ DM_VERSION = '~> 1.2.0.rc1'
9
+ DO_VERSION = '~> 0.10.6'
10
10
  DM_DO_ADAPTERS = %w[ sqlite postgres mysql oracle sqlserver ]
11
11
 
12
- gem 'bcrypt-ruby', '~> 2.1.4'
12
+ gem 'bcrypt-ruby', '~> 3.0.0'
13
13
  gem 'dm-core', DM_VERSION, SOURCE => "#{DATAMAPPER}/dm-core#{REPO_POSTFIX}"
14
14
  gem 'fastercsv', '~> 1.5.4'
15
- gem 'json', '~> 1.4.6'
16
- gem 'stringex', '~> 1.2.0'
15
+ gem 'multi_json', '~> 1.0.3'
16
+ gem 'json', '~> 1.5.4', :platforms => [ :ruby_18 ]
17
+ gem 'stringex', '~> 1.3.0'
17
18
  gem 'uuidtools', '~> 2.1.2'
18
19
 
19
20
  group :development do
20
21
 
21
22
  gem 'dm-validations', DM_VERSION, SOURCE => "#{DATAMAPPER}/dm-validations#{REPO_POSTFIX}"
22
- gem 'jeweler', '~> 1.5.2'
23
- gem 'rake', '~> 0.8.7'
24
- gem 'rspec', '~> 1.3.1'
23
+ gem 'jeweler', '~> 1.6.4'
24
+ gem 'rake', '~> 0.9.2'
25
+ gem 'rspec', '~> 1.3.2'
25
26
 
26
27
  end
27
28
 
28
29
  platforms :mri_18 do
29
30
  group :quality do
30
31
 
31
- gem 'rcov', '~> 0.9.9'
32
- gem 'yard', '~> 0.6'
33
- gem 'yardstick', '~> 0.2'
32
+ gem 'rcov', '~> 0.9.10'
33
+ gem 'yard', '~> 0.7.2'
34
+ gem 'yardstick', '~> 0.4'
34
35
 
35
36
  end
36
37
  end
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'rubygems'
2
2
  require 'rake'
3
3
 
4
4
  begin
5
- gem 'jeweler', '~> 1.5.2'
5
+ gem 'jeweler', '~> 1.6.4'
6
6
  require 'jeweler'
7
7
 
8
8
  Jeweler::Tasks.new do |gem|
@@ -21,5 +21,5 @@ begin
21
21
 
22
22
  FileList['tasks/**/*.rake'].each { |task| import task }
23
23
  rescue LoadError
24
- puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler -v 1.5.2'
24
+ puts 'Jeweler (or a dependency) not available. Install it with: gem install jeweler -v 1.6.4'
25
25
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 1.2.0.rc1
data/dm-types.gemspec CHANGED
@@ -4,14 +4,14 @@
4
4
  # -*- encoding: utf-8 -*-
5
5
 
6
6
  Gem::Specification.new do |s|
7
- s.name = %q{dm-types}
8
- s.version = "1.1.0"
7
+ s.name = "dm-types"
8
+ s.version = "1.2.0.rc1"
9
9
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Dan Kubb"]
12
- s.date = %q{2011-03-16}
13
- s.description = %q{DataMapper plugin providing extra data types}
14
- s.email = %q{dan.kubb [a] gmail [d] com}
12
+ s.date = "2011-09-09"
13
+ s.description = "DataMapper plugin providing extra data types"
14
+ s.email = "dan.kubb [a] gmail [d] com"
15
15
  s.extra_rdoc_files = [
16
16
  "LICENSE",
17
17
  "README.rdoc"
@@ -24,6 +24,7 @@ Gem::Specification.new do |s|
24
24
  "VERSION",
25
25
  "dm-types.gemspec",
26
26
  "lib/dm-types.rb",
27
+ "lib/dm-types/api_key.rb",
27
28
  "lib/dm-types/bcrypt_hash.rb",
28
29
  "lib/dm-types/comma_separated_list.rb",
29
30
  "lib/dm-types/csv.rb",
@@ -38,10 +39,12 @@ Gem::Specification.new do |s|
38
39
  "lib/dm-types/paranoid_datetime.rb",
39
40
  "lib/dm-types/regexp.rb",
40
41
  "lib/dm-types/slug.rb",
42
+ "lib/dm-types/support/dirty_minder.rb",
41
43
  "lib/dm-types/support/flags.rb",
42
44
  "lib/dm-types/uri.rb",
43
45
  "lib/dm-types/uuid.rb",
44
46
  "lib/dm-types/yaml.rb",
47
+ "spec/fixtures/api_user.rb",
45
48
  "spec/fixtures/article.rb",
46
49
  "spec/fixtures/bookmark.rb",
47
50
  "spec/fixtures/invention.rb",
@@ -50,9 +53,12 @@ Gem::Specification.new do |s|
50
53
  "spec/fixtures/software_package.rb",
51
54
  "spec/fixtures/ticket.rb",
52
55
  "spec/fixtures/tshirt.rb",
56
+ "spec/integration/api_key_spec.rb",
53
57
  "spec/integration/bcrypt_hash_spec.rb",
54
58
  "spec/integration/comma_separated_list_spec.rb",
59
+ "spec/integration/dirty_minder_spec.rb",
55
60
  "spec/integration/enum_spec.rb",
61
+ "spec/integration/epoch_time_spec.rb",
56
62
  "spec/integration/file_path_spec.rb",
57
63
  "spec/integration/flag_spec.rb",
58
64
  "spec/integration/ip_address_spec.rb",
@@ -84,87 +90,52 @@ Gem::Specification.new do |s|
84
90
  "tasks/yard.rake",
85
91
  "tasks/yardstick.rake"
86
92
  ]
87
- s.homepage = %q{http://github.com/datamapper/dm-types}
93
+ s.homepage = "http://github.com/datamapper/dm-types"
88
94
  s.require_paths = ["lib"]
89
- s.rubyforge_project = %q{datamapper}
90
- s.rubygems_version = %q{1.6.2}
91
- s.summary = %q{DataMapper plugin providing extra data types}
92
- s.test_files = [
93
- "spec/fixtures/article.rb",
94
- "spec/fixtures/bookmark.rb",
95
- "spec/fixtures/invention.rb",
96
- "spec/fixtures/network_node.rb",
97
- "spec/fixtures/person.rb",
98
- "spec/fixtures/software_package.rb",
99
- "spec/fixtures/ticket.rb",
100
- "spec/fixtures/tshirt.rb",
101
- "spec/integration/bcrypt_hash_spec.rb",
102
- "spec/integration/comma_separated_list_spec.rb",
103
- "spec/integration/enum_spec.rb",
104
- "spec/integration/file_path_spec.rb",
105
- "spec/integration/flag_spec.rb",
106
- "spec/integration/ip_address_spec.rb",
107
- "spec/integration/json_spec.rb",
108
- "spec/integration/slug_spec.rb",
109
- "spec/integration/uri_spec.rb",
110
- "spec/integration/uuid_spec.rb",
111
- "spec/integration/yaml_spec.rb",
112
- "spec/shared/flags_shared_spec.rb",
113
- "spec/shared/identity_function_group.rb",
114
- "spec/spec_helper.rb",
115
- "spec/unit/bcrypt_hash_spec.rb",
116
- "spec/unit/csv_spec.rb",
117
- "spec/unit/enum_spec.rb",
118
- "spec/unit/epoch_time_spec.rb",
119
- "spec/unit/file_path_spec.rb",
120
- "spec/unit/flag_spec.rb",
121
- "spec/unit/ip_address_spec.rb",
122
- "spec/unit/json_spec.rb",
123
- "spec/unit/paranoid_boolean_spec.rb",
124
- "spec/unit/paranoid_datetime_spec.rb",
125
- "spec/unit/regexp_spec.rb",
126
- "spec/unit/uri_spec.rb",
127
- "spec/unit/uuid_spec.rb",
128
- "spec/unit/yaml_spec.rb"
129
- ]
95
+ s.rubyforge_project = "datamapper"
96
+ s.rubygems_version = "1.8.10"
97
+ s.summary = "DataMapper plugin providing extra data types"
130
98
 
131
99
  if s.respond_to? :specification_version then
132
100
  s.specification_version = 3
133
101
 
134
102
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
135
- s.add_runtime_dependency(%q<bcrypt-ruby>, ["~> 2.1.4"])
136
- s.add_runtime_dependency(%q<dm-core>, ["~> 1.1.0"])
103
+ s.add_runtime_dependency(%q<bcrypt-ruby>, ["~> 3.0.0"])
104
+ s.add_runtime_dependency(%q<dm-core>, ["~> 1.2.0.rc1"])
137
105
  s.add_runtime_dependency(%q<fastercsv>, ["~> 1.5.4"])
138
- s.add_runtime_dependency(%q<json>, ["~> 1.4.6"])
139
- s.add_runtime_dependency(%q<stringex>, ["~> 1.2.0"])
106
+ s.add_runtime_dependency(%q<multi_json>, ["~> 1.0.3"])
107
+ s.add_runtime_dependency(%q<json>, ["~> 1.5.4"])
108
+ s.add_runtime_dependency(%q<stringex>, ["~> 1.3.0"])
140
109
  s.add_runtime_dependency(%q<uuidtools>, ["~> 2.1.2"])
141
- s.add_development_dependency(%q<dm-validations>, ["~> 1.1.0"])
142
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
143
- s.add_development_dependency(%q<rake>, ["~> 0.8.7"])
144
- s.add_development_dependency(%q<rspec>, ["~> 1.3.1"])
110
+ s.add_development_dependency(%q<dm-validations>, ["~> 1.2.0.rc1"])
111
+ s.add_development_dependency(%q<jeweler>, ["~> 1.6.4"])
112
+ s.add_development_dependency(%q<rake>, ["~> 0.9.2"])
113
+ s.add_development_dependency(%q<rspec>, ["~> 1.3.2"])
145
114
  else
146
- s.add_dependency(%q<bcrypt-ruby>, ["~> 2.1.4"])
147
- s.add_dependency(%q<dm-core>, ["~> 1.1.0"])
115
+ s.add_dependency(%q<bcrypt-ruby>, ["~> 3.0.0"])
116
+ s.add_dependency(%q<dm-core>, ["~> 1.2.0.rc1"])
148
117
  s.add_dependency(%q<fastercsv>, ["~> 1.5.4"])
149
- s.add_dependency(%q<json>, ["~> 1.4.6"])
150
- s.add_dependency(%q<stringex>, ["~> 1.2.0"])
118
+ s.add_dependency(%q<multi_json>, ["~> 1.0.3"])
119
+ s.add_dependency(%q<json>, ["~> 1.5.4"])
120
+ s.add_dependency(%q<stringex>, ["~> 1.3.0"])
151
121
  s.add_dependency(%q<uuidtools>, ["~> 2.1.2"])
152
- s.add_dependency(%q<dm-validations>, ["~> 1.1.0"])
153
- s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
154
- s.add_dependency(%q<rake>, ["~> 0.8.7"])
155
- s.add_dependency(%q<rspec>, ["~> 1.3.1"])
122
+ s.add_dependency(%q<dm-validations>, ["~> 1.2.0.rc1"])
123
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
124
+ s.add_dependency(%q<rake>, ["~> 0.9.2"])
125
+ s.add_dependency(%q<rspec>, ["~> 1.3.2"])
156
126
  end
157
127
  else
158
- s.add_dependency(%q<bcrypt-ruby>, ["~> 2.1.4"])
159
- s.add_dependency(%q<dm-core>, ["~> 1.1.0"])
128
+ s.add_dependency(%q<bcrypt-ruby>, ["~> 3.0.0"])
129
+ s.add_dependency(%q<dm-core>, ["~> 1.2.0.rc1"])
160
130
  s.add_dependency(%q<fastercsv>, ["~> 1.5.4"])
161
- s.add_dependency(%q<json>, ["~> 1.4.6"])
162
- s.add_dependency(%q<stringex>, ["~> 1.2.0"])
131
+ s.add_dependency(%q<multi_json>, ["~> 1.0.3"])
132
+ s.add_dependency(%q<json>, ["~> 1.5.4"])
133
+ s.add_dependency(%q<stringex>, ["~> 1.3.0"])
163
134
  s.add_dependency(%q<uuidtools>, ["~> 2.1.2"])
164
- s.add_dependency(%q<dm-validations>, ["~> 1.1.0"])
165
- s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
166
- s.add_dependency(%q<rake>, ["~> 0.8.7"])
167
- s.add_dependency(%q<rspec>, ["~> 1.3.1"])
135
+ s.add_dependency(%q<dm-validations>, ["~> 1.2.0.rc1"])
136
+ s.add_dependency(%q<jeweler>, ["~> 1.6.4"])
137
+ s.add_dependency(%q<rake>, ["~> 0.9.2"])
138
+ s.add_dependency(%q<rspec>, ["~> 1.3.2"])
168
139
  end
169
140
  end
170
141
 
data/lib/dm-types.rb CHANGED
@@ -18,5 +18,6 @@ module DataMapper
18
18
  autoload :UUID, 'dm-types/uuid'
19
19
  autoload :URI, 'dm-types/uri'
20
20
  autoload :Yaml, 'dm-types/yaml'
21
+ autoload :APIKey, 'dm-types/api_key'
21
22
  end
22
23
  end
@@ -0,0 +1,30 @@
1
+ require 'dm-core'
2
+
3
+ require 'digest/sha1'
4
+
5
+ module DataMapper
6
+ class Property
7
+ class APIKey < String
8
+
9
+ # The amount of random seed data to use to generate tha API Key
10
+ PADDING = 256
11
+
12
+ length 40
13
+ unique true
14
+ default proc { APIKey.generate }
15
+
16
+ #
17
+ # Generates a new API Key.
18
+ #
19
+ # @return [String]
20
+ # The new API Key.
21
+ #
22
+ def self.generate
23
+ sha1 = Digest::SHA1.new
24
+
25
+ PADDING.times { sha1 << rand(256).chr }
26
+ return sha1.hexdigest
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/dm-types/csv.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'dm-core'
2
+ require 'dm-types/support/dirty_minder'
2
3
 
3
4
  if RUBY_VERSION >= '1.9.0'
4
5
  require 'csv'
@@ -30,6 +31,8 @@ module DataMapper
30
31
  end
31
32
  end
32
33
 
34
+ include ::DataMapper::Property::DirtyMinder
35
+
33
36
  end # class Csv
34
37
  end # class Property
35
38
  end # module DataMapper
data/lib/dm-types/enum.rb CHANGED
@@ -8,8 +8,6 @@ module DataMapper
8
8
  include Flags
9
9
 
10
10
  def initialize(model, name, options = {})
11
- super
12
-
13
11
  @flag_map = {}
14
12
 
15
13
  flags = options.fetch(:flags, self.class.flags)
@@ -17,14 +15,11 @@ module DataMapper
17
15
  @flag_map[i + 1] = flag
18
16
  end
19
17
 
20
- if defined?(::DataMapper::Validations)
21
- unless model.skip_auto_validation_for?(self)
22
- if self.class.ancestors.include?(Property::Enum)
23
- allowed = flag_map.values_at(*flag_map.keys.sort)
24
- model.validates_within name, model.options_with_message({ :set => allowed }, self, :within)
25
- end
26
- end
18
+ if self.class.accepted_options.include?(:set) && !options.include?(:set)
19
+ options[:set] = @flag_map.values_at(*@flag_map.keys.sort)
27
20
  end
21
+
22
+ super
28
23
  end
29
24
 
30
25
  def load(value)
@@ -13,9 +13,19 @@ module DataMapper
13
13
  end
14
14
 
15
15
  def dump(value)
16
+ value.to_i if value
17
+ end
18
+
19
+ def custom?
20
+ true
21
+ end
22
+
23
+ def typecast(value)
16
24
  case value
17
- when ::Numeric, ::Time then value.to_i
18
- when ::DateTime then datetime_to_time(value).to_i
25
+ when ::Time then value
26
+ when ::Numeric, /\A\d+\z/ then ::Time.at(value.to_i)
27
+ when ::DateTime then datetime_to_time(value)
28
+ when ::String then ::Time.parse(value)
19
29
  end
20
30
  end
21
31
 
data/lib/dm-types/json.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'dm-core'
2
- require 'json' unless defined? JSON
2
+ require 'dm-types/support/dirty_minder'
3
+ require 'multi_json'
3
4
 
4
5
  module DataMapper
5
6
  class Property
@@ -21,7 +22,7 @@ module DataMapper
21
22
  if value.nil?
22
23
  nil
23
24
  elsif value.is_a?(::String)
24
- ::JSON.load(value)
25
+ typecast_to_primitive(value)
25
26
  else
26
27
  raise ArgumentError.new("+value+ of a property of JSON type must be nil or a String")
27
28
  end
@@ -31,14 +32,16 @@ module DataMapper
31
32
  if value.nil? || value.is_a?(::String)
32
33
  value
33
34
  else
34
- ::JSON.dump(value)
35
+ MultiJson.encode(value)
35
36
  end
36
37
  end
37
38
 
38
39
  def typecast_to_primitive(value)
39
- ::JSON.load(value.to_s)
40
+ MultiJson.decode(value.to_s)
40
41
  end
41
42
 
43
+ include ::DataMapper::Property::DirtyMinder
44
+
42
45
  end # class Json
43
46
 
44
47
  JSON = Json
@@ -2,30 +2,21 @@ module DataMapper
2
2
  module Types
3
3
  module Paranoid
4
4
  module Base
5
- extend Chainable
6
-
7
5
  def self.included(model)
8
6
  model.extend ClassMethods
9
7
  model.instance_variable_set(:@paranoid_properties, {})
10
8
  end
11
9
 
12
- chainable do
13
- def inherited(model)
14
- model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup)
15
- super
16
- end
17
- end
18
-
19
10
  def paranoid_destroy
20
11
  model.paranoid_properties.each do |name, block|
21
12
  attribute_set(name, block.call(self))
22
13
  end
23
14
  save_self
24
- self.persisted_state = Resource::State::Immutable.new(self)
15
+ self.persistence_state = Resource::PersistenceState::Immutable.new(self)
25
16
  true
26
17
  end
27
18
 
28
- private
19
+ private
29
20
 
30
21
  # @api private
31
22
  def _destroy(execute_hooks = true)
@@ -39,6 +30,12 @@ module DataMapper
39
30
  end # module Base
40
31
 
41
32
  module ClassMethods
33
+ def inherited(model)
34
+ model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup)
35
+ super
36
+ end
37
+
38
+ # @api public
42
39
  def with_deleted
43
40
  with_exclusive_scope({}) { block_given? ? yield : all }
44
41
  end
@@ -0,0 +1,166 @@
1
+ # Approach
2
+ #
3
+ # We need to detect whether or not the underlying Hash or Array changed and
4
+ # update the dirty-ness of the encapsulating Resource accordingly (so that it
5
+ # will actually save).
6
+ #
7
+ # DM's state-tracking code only triggers dirty-ness by comparing the new value
8
+ # against the instance's Property's current value. WRT mutation, we have to
9
+ # choose one of the following approaches:
10
+ #
11
+ # (1) mutate a copy ("after"), then invoke the Resource assignment and State
12
+ # tracking
13
+ #
14
+ # (2) create a copy ("before"), mutate self ("after"), then invoke the
15
+ # Resource assignment and State tracking
16
+ #
17
+ # (1) seemed simpler at first, but it required additional steps to alias the
18
+ # original (pre-hooked) methods before overriding them (so they could be invoked
19
+ # externally, ala self.clone.send("orig_...")), and more importantly it resulted
20
+ # in any external references keeping their old value (instead of getting the
21
+ # new), like so:
22
+ #
23
+ # copy = instance.json
24
+ # copy[:some] = :value
25
+ # instance.json[:some] == :value
26
+ # => true
27
+ # copy[:some] == :value
28
+ # => false # fk!
29
+ #
30
+ # In order to do (2) and still have State tracking trigger normally, we need to
31
+ # ensure the Property has a different value other than self when the State
32
+ # tracking does the comparison. This equates to setting the Property directly
33
+ # to the "before" value (a clone and thus a different object/value) before
34
+ # invoking the Resource Property/attribute assignment.
35
+ #
36
+ # The cloning of any value might sound expensive, but it's identical in cost to
37
+ # what you already had to do: assign a cloned copy in order to trigger
38
+ # dirty-ness (e.g. ::DataMapper::Property::Json):
39
+ #
40
+ # model.json = model.json.merge({:some=>:value})
41
+ #
42
+ # Hooking Core Classes
43
+ #
44
+ # We want to hook certain methods on Hash and Array to trigger dirty-ness in the
45
+ # resource. However, because these are core classes, they are individually
46
+ # mapped to C primitives and thus cannot be hooked through #send/#__send__. We
47
+ # have to override each method, but we don't want to write a lot of code.
48
+ #
49
+ # Minimally Invasive
50
+ #
51
+ # We also want to extend behaviour of existing class instances instead of
52
+ # impersonating/delegating from a proxy class of our own, or overriding a global
53
+ # class behaviour. This is the most flexible approach and least prone to error,
54
+ # since it leaves open the option for consumers to proxy or override global
55
+ # classes, and is less likely to interfere with method_missing/etc shenanigans.
56
+ #
57
+ # Nested Object Mutations
58
+ #
59
+ # Since we use {Array,Hash}#hash to compare before & after, and #hash accounts
60
+ # for/traverses nested structures, no "deep" inspection logic is technically
61
+ # necessary. However, Resource#dirty? only queries a cache of dirtied
62
+ # attributes, whose own population strategy is to hook assignment (instead of
63
+ # interrogating properties on demand). So the approach is still limited to
64
+ # top-level mutators.
65
+ #
66
+ # Maybe consider optional "advisory" Property#dirty? method for Resource#dirty?
67
+ # that custom properties could use for this purpose.
68
+ #
69
+ # TODO: add support for detecting mutations in nested objects, but we can't
70
+ # catch the assignment from here (yet?).
71
+ # TODO: ensure we covered all indirectly-mutable classes that DM uses underneath
72
+ # a property type
73
+ # TODO: figure out how to hook core class methods on RBX (which do use #send)
74
+
75
+ module DataMapper
76
+ class Property
77
+ module DirtyMinder
78
+
79
+ module Hooker
80
+ MUTATION_METHODS = {
81
+ ::Array => %w{
82
+ []= push << shift pop insert unshift delete
83
+ delete_at replace fill clear
84
+ slice! reverse! rotate! compact! flatten! uniq!
85
+ collect! map! sort! sort_by! reject! delete_if!
86
+ select! shuffle!
87
+ }.select { |meth| ::Array.instance_methods.any? { |m| m.to_s == meth } },
88
+
89
+ ::Hash => %w{
90
+ []= store delete delete_if replace update
91
+ delete rehash shift clear
92
+ merge! reject! select!
93
+ }.select { |meth| ::Hash.instance_methods.any? { |m| m.to_s == meth } },
94
+ }
95
+
96
+ def self.extended(instance)
97
+ # FIXME: DirtyMinder is currently unsupported on RBX, because unlike
98
+ # the other supported Rubies, RBX core class (e.g. Array, Hash)
99
+ # methods use #send(). In other words, the other Rubies don't use
100
+ # #send() (they map directly to their C functions).
101
+ #
102
+ # The current methodology takes advantage of this by using #send() to
103
+ # forward method invocations we've hooked. Supporting RBX will
104
+ # require finding another way, possibly for all Rubies. In the
105
+ # meantime, something is better than nothing.
106
+ return if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'rbx'
107
+
108
+ return unless type = MUTATION_METHODS.keys.find { |k| instance.kind_of?(k) }
109
+ instance.extend const_get("#{type}Hooks")
110
+ end
111
+
112
+ MUTATION_METHODS.each do |klass, methods|
113
+ methods.each do |meth|
114
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
115
+ module #{klass}Hooks
116
+ def #{meth}(*)
117
+ before = self.clone
118
+ ret = super
119
+ after = self
120
+
121
+ # If the hashes aren't equivalent then we know the Resource
122
+ # should be dirty. However because we mutated self, normal
123
+ # State tracking will never trigger, because it will compare the
124
+ # new value - self - to the Resource's existing property value -
125
+ # which is also self.
126
+ #
127
+ # The solution is to drop 1 level beneath Resource State
128
+ # tracking and set the value of the property directly to the
129
+ # previous value (a different object now, because it's a clone).
130
+ # Then trigger the State tracking like normal.
131
+ if before.hash != after.hash
132
+ @property.set(@resource, before)
133
+ @resource.attribute_set(@property.name, after)
134
+ end
135
+
136
+ ret
137
+ end
138
+ end
139
+ RUBY
140
+ end
141
+ end
142
+
143
+ def track(resource, property)
144
+ @resource, @property = resource, property
145
+ end
146
+
147
+ end # Hooker
148
+
149
+ # Catch any direct assignment (#set), and any Resource#reload (set!).
150
+ def set!(resource, value)
151
+ hook_value(resource, value) unless value.kind_of? Hooker
152
+ super
153
+ end
154
+
155
+ private
156
+
157
+ def hook_value(resource, value)
158
+ return if value.kind_of? Hooker
159
+
160
+ value.extend Hooker
161
+ value.track(resource, self)
162
+ end
163
+
164
+ end # DirtyMinder
165
+ end # Property
166
+ end # DataMapper