dm-types 1.1.0 → 1.2.0.rc1

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/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