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 +12 -11
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/dm-types.gemspec +43 -72
- data/lib/dm-types.rb +1 -0
- data/lib/dm-types/api_key.rb +30 -0
- data/lib/dm-types/csv.rb +3 -0
- data/lib/dm-types/enum.rb +4 -9
- data/lib/dm-types/epoch_time.rb +12 -2
- data/lib/dm-types/json.rb +7 -4
- data/lib/dm-types/paranoid/base.rb +8 -11
- data/lib/dm-types/support/dirty_minder.rb +166 -0
- data/lib/dm-types/yaml.rb +4 -1
- data/spec/fixtures/api_user.rb +14 -0
- data/spec/fixtures/person.rb +1 -0
- data/spec/integration/api_key_spec.rb +27 -0
- data/spec/integration/dirty_minder_spec.rb +197 -0
- data/spec/integration/epoch_time_spec.rb +61 -0
- data/spec/integration/ip_address_spec.rb +4 -8
- data/spec/integration/json_spec.rb +1 -0
- data/spec/integration/slug_spec.rb +30 -26
- data/spec/unit/epoch_time_spec.rb +21 -5
- data/spec/unit/json_spec.rb +2 -2
- data/spec/unit/paranoid_boolean_spec.rb +13 -1
- data/spec/unit/paranoid_datetime_spec.rb +12 -1
- data/spec/unit/yaml_spec.rb +4 -4
- metadata +146 -92
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.
|
9
|
-
DO_VERSION = '~> 0.10.
|
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', '~>
|
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 '
|
16
|
-
gem '
|
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.
|
23
|
-
gem 'rake', '~> 0.
|
24
|
-
gem 'rspec', '~> 1.3.
|
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.
|
32
|
-
gem 'yard', '~> 0.
|
33
|
-
gem 'yardstick', '~> 0.
|
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
|
+
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.
|
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
|
+
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 =
|
8
|
-
s.version = "1.
|
7
|
+
s.name = "dm-types"
|
8
|
+
s.version = "1.2.0.rc1"
|
9
9
|
|
10
|
-
s.required_rubygems_version = Gem::Requirement.new("
|
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 =
|
13
|
-
s.description =
|
14
|
-
s.email =
|
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 =
|
93
|
+
s.homepage = "http://github.com/datamapper/dm-types"
|
88
94
|
s.require_paths = ["lib"]
|
89
|
-
s.rubyforge_project =
|
90
|
-
s.rubygems_version =
|
91
|
-
s.summary =
|
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>, ["~>
|
136
|
-
s.add_runtime_dependency(%q<dm-core>, ["~> 1.
|
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<
|
139
|
-
s.add_runtime_dependency(%q<
|
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.
|
142
|
-
s.add_development_dependency(%q<jeweler>, ["~> 1.
|
143
|
-
s.add_development_dependency(%q<rake>, ["~> 0.
|
144
|
-
s.add_development_dependency(%q<rspec>, ["~> 1.3.
|
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>, ["~>
|
147
|
-
s.add_dependency(%q<dm-core>, ["~> 1.
|
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<
|
150
|
-
s.add_dependency(%q<
|
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.
|
153
|
-
s.add_dependency(%q<jeweler>, ["~> 1.
|
154
|
-
s.add_dependency(%q<rake>, ["~> 0.
|
155
|
-
s.add_dependency(%q<rspec>, ["~> 1.3.
|
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>, ["~>
|
159
|
-
s.add_dependency(%q<dm-core>, ["~> 1.
|
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<
|
162
|
-
s.add_dependency(%q<
|
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.
|
165
|
-
s.add_dependency(%q<jeweler>, ["~> 1.
|
166
|
-
s.add_dependency(%q<rake>, ["~> 0.
|
167
|
-
s.add_dependency(%q<rspec>, ["~> 1.3.
|
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
@@ -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
|
21
|
-
|
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)
|
data/lib/dm-types/epoch_time.rb
CHANGED
@@ -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 ::
|
18
|
-
when ::
|
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 '
|
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
|
-
|
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
|
-
|
35
|
+
MultiJson.encode(value)
|
35
36
|
end
|
36
37
|
end
|
37
38
|
|
38
39
|
def typecast_to_primitive(value)
|
39
|
-
|
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.
|
15
|
+
self.persistence_state = Resource::PersistenceState::Immutable.new(self)
|
25
16
|
true
|
26
17
|
end
|
27
18
|
|
28
|
-
|
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
|