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