dynamoid 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Dynamoid.gemspec +65 -3
- data/Gemfile +3 -0
- data/Gemfile.lock +6 -0
- data/README.markdown +117 -22
- data/Rakefile +22 -9
- data/VERSION +1 -1
- data/doc/.nojekyll +0 -0
- data/doc/Dynamoid.html +300 -0
- data/doc/Dynamoid/Adapter.html +1387 -0
- data/doc/Dynamoid/Adapter/AwsSdk.html +1561 -0
- data/doc/Dynamoid/Adapter/Local.html +1487 -0
- data/doc/Dynamoid/Associations.html +131 -0
- data/doc/Dynamoid/Associations/Association.html +1706 -0
- data/doc/Dynamoid/Associations/BelongsTo.html +339 -0
- data/doc/Dynamoid/Associations/ClassMethods.html +723 -0
- data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +339 -0
- data/doc/Dynamoid/Associations/HasMany.html +339 -0
- data/doc/Dynamoid/Associations/HasOne.html +339 -0
- data/doc/Dynamoid/Components.html +202 -0
- data/doc/Dynamoid/Config.html +395 -0
- data/doc/Dynamoid/Config/Options.html +609 -0
- data/doc/Dynamoid/Criteria.html +131 -0
- data/doc/Dynamoid/Criteria/Chain.html +759 -0
- data/doc/Dynamoid/Criteria/ClassMethods.html +98 -0
- data/doc/Dynamoid/Document.html +512 -0
- data/doc/Dynamoid/Document/ClassMethods.html +581 -0
- data/doc/Dynamoid/Errors.html +118 -0
- data/doc/Dynamoid/Errors/DocumentNotValid.html +210 -0
- data/doc/Dynamoid/Errors/Error.html +130 -0
- data/doc/Dynamoid/Errors/InvalidField.html +133 -0
- data/doc/Dynamoid/Errors/MissingRangeKey.html +133 -0
- data/doc/Dynamoid/Fields.html +649 -0
- data/doc/Dynamoid/Fields/ClassMethods.html +264 -0
- data/doc/Dynamoid/Finders.html +128 -0
- data/doc/Dynamoid/Finders/ClassMethods.html +502 -0
- data/doc/Dynamoid/Indexes.html +308 -0
- data/doc/Dynamoid/Indexes/ClassMethods.html +351 -0
- data/doc/Dynamoid/Indexes/Index.html +1089 -0
- data/doc/Dynamoid/Persistence.html +653 -0
- data/doc/Dynamoid/Persistence/ClassMethods.html +568 -0
- data/doc/Dynamoid/Validations.html +399 -0
- data/doc/_index.html +439 -0
- data/doc/class_list.html +47 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +55 -0
- data/doc/css/style.css +322 -0
- data/doc/file.LICENSE.html +66 -0
- data/doc/file.README.html +279 -0
- data/doc/file_list.html +52 -0
- data/doc/frames.html +13 -0
- data/doc/index.html +279 -0
- data/doc/js/app.js +205 -0
- data/doc/js/full_list.js +173 -0
- data/doc/js/jquery.js +16 -0
- data/doc/method_list.html +1054 -0
- data/doc/top-level-namespace.html +105 -0
- data/lib/dynamoid.rb +2 -1
- data/lib/dynamoid/adapter.rb +77 -6
- data/lib/dynamoid/adapter/aws_sdk.rb +96 -16
- data/lib/dynamoid/adapter/local.rb +84 -15
- data/lib/dynamoid/associations.rb +53 -4
- data/lib/dynamoid/associations/association.rb +154 -26
- data/lib/dynamoid/associations/belongs_to.rb +32 -6
- data/lib/dynamoid/associations/has_and_belongs_to_many.rb +29 -3
- data/lib/dynamoid/associations/has_many.rb +30 -4
- data/lib/dynamoid/associations/has_one.rb +26 -3
- data/lib/dynamoid/components.rb +7 -5
- data/lib/dynamoid/config.rb +15 -2
- data/lib/dynamoid/config/options.rb +8 -0
- data/lib/dynamoid/criteria.rb +7 -2
- data/lib/dynamoid/criteria/chain.rb +55 -8
- data/lib/dynamoid/document.rb +68 -7
- data/lib/dynamoid/errors.rb +17 -2
- data/lib/dynamoid/fields.rb +44 -1
- data/lib/dynamoid/finders.rb +32 -6
- data/lib/dynamoid/indexes.rb +22 -2
- data/lib/dynamoid/indexes/index.rb +48 -7
- data/lib/dynamoid/persistence.rb +111 -51
- data/lib/dynamoid/validations.rb +36 -0
- data/spec/app/models/address.rb +2 -1
- data/spec/app/models/camel_case.rb +11 -0
- data/spec/app/models/magazine.rb +4 -1
- data/spec/app/models/sponsor.rb +3 -1
- data/spec/app/models/subscription.rb +5 -1
- data/spec/app/models/user.rb +10 -1
- data/spec/dynamoid/associations/association_spec.rb +67 -1
- data/spec/dynamoid/associations/belongs_to_spec.rb +16 -1
- data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +7 -0
- data/spec/dynamoid/associations/has_many_spec.rb +14 -0
- data/spec/dynamoid/associations/has_one_spec.rb +10 -1
- data/spec/dynamoid/criteria_spec.rb +5 -1
- data/spec/dynamoid/document_spec.rb +23 -3
- data/spec/dynamoid/fields_spec.rb +10 -1
- data/spec/dynamoid/indexes/index_spec.rb +19 -0
- data/spec/dynamoid/persistence_spec.rb +24 -0
- data/spec/dynamoid/validations_spec.rb +36 -0
- metadata +105 -4
data/lib/dynamoid/persistence.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
require 'securerandom'
|
2
2
|
|
3
3
|
# encoding: utf-8
|
4
|
-
module Dynamoid
|
4
|
+
module Dynamoid
|
5
5
|
|
6
|
-
#
|
6
|
+
# Persistence is responsible for dumping objects to and marshalling objects from the datastore. It tries to reserialize
|
7
|
+
# values to be of the same type as when they were passed in, based on the fields in the class.
|
7
8
|
module Persistence
|
8
9
|
extend ActiveSupport::Concern
|
9
10
|
|
@@ -11,59 +12,94 @@ module Dynamoid #:nodoc:
|
|
11
12
|
alias :new_record? :new_record
|
12
13
|
|
13
14
|
module ClassMethods
|
15
|
+
|
16
|
+
# Returns the name of the table the class is for.
|
17
|
+
#
|
18
|
+
# @since 0.2.0
|
14
19
|
def table_name
|
15
20
|
"#{Dynamoid::Config.namespace}_#{self.to_s.downcase.pluralize}"
|
16
21
|
end
|
17
22
|
|
23
|
+
# Creates a table for a given table name, hash key, and range key.
|
24
|
+
#
|
25
|
+
# @since 0.2.0
|
18
26
|
def create_table(table_name, id = :id, options = {})
|
19
27
|
Dynamoid::Adapter.tables << table_name if Dynamoid::Adapter.create_table(table_name, id.to_sym, options)
|
20
28
|
end
|
21
|
-
|
29
|
+
|
30
|
+
# Does a table with this name exist?
|
31
|
+
#
|
32
|
+
# @since 0.2.0
|
22
33
|
def table_exists?(table_name)
|
23
34
|
Dynamoid::Adapter.tables.include?(table_name)
|
24
35
|
end
|
25
36
|
|
26
|
-
|
27
|
-
|
37
|
+
# Undump an object into a hash, converting each type from a string representation of itself into the type specified by the field.
|
38
|
+
#
|
39
|
+
# @since 0.2.0
|
40
|
+
def undump(incoming = nil)
|
41
|
+
(incoming ||= {}).symbolize_keys!
|
28
42
|
Hash.new.tap do |hash|
|
29
43
|
self.attributes.each do |attribute, options|
|
30
|
-
|
31
|
-
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
32
|
-
case options[:type]
|
33
|
-
when :string
|
34
|
-
hash[attribute] = value.to_s
|
35
|
-
when :integer
|
36
|
-
hash[attribute] = value.to_i
|
37
|
-
when :float
|
38
|
-
hash[attribute] = value.to_f
|
39
|
-
when :set, :array
|
40
|
-
if value.is_a?(Set) || value.is_a?(Array)
|
41
|
-
hash[attribute] = value
|
42
|
-
else
|
43
|
-
hash[attribute] = Set[value]
|
44
|
-
end
|
45
|
-
when :datetime
|
46
|
-
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
47
|
-
hash[attribute] = value
|
48
|
-
else
|
49
|
-
hash[attribute] = Time.at(value).to_datetime
|
50
|
-
end
|
51
|
-
end
|
44
|
+
hash[attribute] = undump_field(incoming[attribute], options[:type])
|
52
45
|
end
|
53
46
|
end
|
54
47
|
end
|
55
|
-
|
48
|
+
|
49
|
+
# Undump a value for a given type. Given a string, it'll determine (based on the type provided) whether to turn it into a
|
50
|
+
# string, integer, float, set, array, datetime, or serialized return value.
|
51
|
+
#
|
52
|
+
# @since 0.2.0
|
53
|
+
def undump_field(value, type)
|
54
|
+
return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
55
|
+
|
56
|
+
case type
|
57
|
+
when :string
|
58
|
+
value.to_s
|
59
|
+
when :integer
|
60
|
+
value.to_i
|
61
|
+
when :float
|
62
|
+
value.to_f
|
63
|
+
when :set, :array
|
64
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
65
|
+
value
|
66
|
+
else
|
67
|
+
Set[value]
|
68
|
+
end
|
69
|
+
when :datetime
|
70
|
+
if value.is_a?(Date) || value.is_a?(DateTime) || value.is_a?(Time)
|
71
|
+
value
|
72
|
+
else
|
73
|
+
Time.at(value).to_datetime
|
74
|
+
end
|
75
|
+
when :serialized
|
76
|
+
if value.is_a?(String)
|
77
|
+
YAML.load(value)
|
78
|
+
else
|
79
|
+
value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
56
84
|
end
|
57
85
|
|
86
|
+
# Create the table if it doesn't exist already upon loading the class.
|
58
87
|
included do
|
59
88
|
self.create_table(self.table_name) unless self.table_exists?(self.table_name)
|
60
89
|
end
|
61
90
|
|
91
|
+
# Is this object persisted in the datastore? Required for some ActiveModel integration stuff.
|
92
|
+
#
|
93
|
+
# @since 0.2.0
|
62
94
|
def persisted?
|
63
95
|
!new_record?
|
64
96
|
end
|
65
97
|
|
66
|
-
|
98
|
+
# Run the callbacks and then persist this object in the datastore.
|
99
|
+
#
|
100
|
+
# @since 0.2.0
|
101
|
+
def save(options = {})
|
102
|
+
@previously_changed = changes
|
67
103
|
if self.new_record?
|
68
104
|
run_callbacks(:create) do
|
69
105
|
run_callbacks(:save) do
|
@@ -77,52 +113,76 @@ module Dynamoid #:nodoc:
|
|
77
113
|
end
|
78
114
|
self
|
79
115
|
end
|
80
|
-
|
116
|
+
|
117
|
+
# Delete this object, but only after running callbacks for it.
|
118
|
+
#
|
119
|
+
# @since 0.2.0
|
81
120
|
def destroy
|
82
121
|
run_callbacks(:destroy) do
|
83
122
|
self.delete
|
84
123
|
end
|
85
124
|
self
|
86
125
|
end
|
87
|
-
|
126
|
+
|
127
|
+
# Delete this object from the datastore and all indexes.
|
128
|
+
#
|
129
|
+
# @since 0.2.0
|
88
130
|
def delete
|
89
131
|
delete_indexes
|
90
132
|
Dynamoid::Adapter.delete(self.class.table_name, self.id)
|
91
133
|
end
|
92
134
|
|
135
|
+
# Dump this object's attributes into hash form, fit to be persisted into the datastore.
|
136
|
+
#
|
137
|
+
# @since 0.2.0
|
93
138
|
def dump
|
94
139
|
Hash.new.tap do |hash|
|
95
140
|
self.class.attributes.each do |attribute, options|
|
96
|
-
|
97
|
-
next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
98
|
-
case options[:type]
|
99
|
-
when :string
|
100
|
-
hash[attribute] = value.to_s
|
101
|
-
when :integer
|
102
|
-
hash[attribute] = value.to_i
|
103
|
-
when :float
|
104
|
-
hash[attribute] = value.to_f
|
105
|
-
when :set, :array
|
106
|
-
if value.is_a?(Set) || value.is_a?(Array)
|
107
|
-
hash[attribute] = value
|
108
|
-
else
|
109
|
-
hash[attribute] = Set[value]
|
110
|
-
end
|
111
|
-
when :datetime
|
112
|
-
hash[attribute] = value.to_time.to_f
|
113
|
-
end
|
141
|
+
hash[attribute] = dump_field(self.read_attribute(attribute), options[:type])
|
114
142
|
end
|
115
143
|
end
|
116
144
|
end
|
117
145
|
|
118
146
|
private
|
119
|
-
|
147
|
+
|
148
|
+
# Determine how to dump this field. Given a value, it'll determine how to turn it into a value that can be
|
149
|
+
# persisted into the datastore.
|
150
|
+
#
|
151
|
+
# @since 0.2.0
|
152
|
+
def dump_field(value, type)
|
153
|
+
return if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
154
|
+
|
155
|
+
case type
|
156
|
+
when :string
|
157
|
+
value.to_s
|
158
|
+
when :integer
|
159
|
+
value.to_i
|
160
|
+
when :float
|
161
|
+
value.to_f
|
162
|
+
when :set, :array
|
163
|
+
if value.is_a?(Set) || value.is_a?(Array)
|
164
|
+
value
|
165
|
+
else
|
166
|
+
Set[value]
|
167
|
+
end
|
168
|
+
when :datetime
|
169
|
+
value.to_time.to_f
|
170
|
+
when :serialized
|
171
|
+
value.to_yaml
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Persist the object into the datastore. Assign it an id first if it doesn't have one; then afterwards,
|
176
|
+
# save its indexes.
|
177
|
+
#
|
178
|
+
# @since 0.2.0
|
120
179
|
def persist
|
121
180
|
self.id = SecureRandom.uuid if self.id.nil? || self.id.blank?
|
122
181
|
Dynamoid::Adapter.write(self.class.table_name, self.dump)
|
123
182
|
save_indexes
|
183
|
+
@new_record = false
|
124
184
|
end
|
125
185
|
|
126
186
|
end
|
127
187
|
|
128
|
-
end
|
188
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Dynamoid
|
3
|
+
|
4
|
+
# Provide ActiveModel validations to Dynamoid documents.
|
5
|
+
module Validations
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
include ActiveModel::Validations
|
9
|
+
include ActiveModel::Validations::Callbacks
|
10
|
+
|
11
|
+
# Override save to provide validation support.
|
12
|
+
#
|
13
|
+
# @since 0.2.0
|
14
|
+
def save(options = {})
|
15
|
+
options.reverse_merge!(:validate => true)
|
16
|
+
return false if options[:validate] and (not valid?)
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
# Is this object valid?
|
21
|
+
#
|
22
|
+
# @since 0.2.0
|
23
|
+
def valid?(context = nil)
|
24
|
+
context ||= (new_record? ? :create : :update)
|
25
|
+
super(context)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Raise an error unless this object is valid.
|
29
|
+
#
|
30
|
+
# @since 0.2.0
|
31
|
+
def save!
|
32
|
+
raise Dynamoid::Errors::DocumentNotValid.new(self) unless valid?
|
33
|
+
save(:validate => false)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/spec/app/models/address.rb
CHANGED
data/spec/app/models/magazine.rb
CHANGED
data/spec/app/models/sponsor.rb
CHANGED
data/spec/app/models/user.rb
CHANGED
@@ -14,4 +14,13 @@ class User
|
|
14
14
|
index :created_at, :range => true
|
15
15
|
|
16
16
|
has_and_belongs_to_many :subscriptions
|
17
|
-
|
17
|
+
|
18
|
+
has_many :books, :class_name => 'Magazine', :inverse_of => :owner
|
19
|
+
has_one :monthly, :class_name => 'Subscription', :inverse_of => :customer
|
20
|
+
|
21
|
+
has_and_belongs_to_many :followers, :class_name => 'User', :inverse_of => :following
|
22
|
+
has_and_belongs_to_many :following, :class_name => 'User', :inverse_of => :followers
|
23
|
+
|
24
|
+
belongs_to :camel_case
|
25
|
+
|
26
|
+
end
|
@@ -33,7 +33,7 @@ describe "Dynamoid::Associations::Association" do
|
|
33
33
|
@magazine.subscriptions.size.should == 1
|
34
34
|
@magazine.subscriptions.should include @subscription
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
37
|
it 'returns the number of items in the association' do
|
38
38
|
@magazine.subscriptions.create
|
39
39
|
|
@@ -101,4 +101,70 @@ describe "Dynamoid::Associations::Association" do
|
|
101
101
|
@magazine.subscriptions.collect(&:id).should =~ [@subscription1.id, @subscription2.id, @subscription3.id]
|
102
102
|
end
|
103
103
|
|
104
|
+
it 'works for camel-cased associations' do
|
105
|
+
@magazine.camel_cases.create.class.should == CamelCase
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'destroys associations' do
|
109
|
+
@subscription = Subscription.new
|
110
|
+
@magazine.subscriptions.expects(:records).returns([@subscription])
|
111
|
+
@subscription.expects(:destroy)
|
112
|
+
|
113
|
+
@magazine.subscriptions.destroy_all
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'deletes associations' do
|
117
|
+
@subscription = Subscription.new
|
118
|
+
@magazine.subscriptions.expects(:records).returns([@subscription])
|
119
|
+
@subscription.expects(:delete)
|
120
|
+
|
121
|
+
@magazine.subscriptions.delete_all
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns the first and last record when they exist' do
|
125
|
+
@subscription1 = @magazine.subscriptions.create
|
126
|
+
@subscription2 = @magazine.subscriptions.create
|
127
|
+
@subscription3 = @magazine.subscriptions.create
|
128
|
+
|
129
|
+
@magazine.subscriptions.instance_eval { [first, last] }.should == [@subscription1, @subscription3]
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'replaces existing associations when using the setter' do
|
133
|
+
@subscription1 = @magazine.subscriptions.create
|
134
|
+
@subscription2 = @magazine.subscriptions.create
|
135
|
+
@subscription3 = Subscription.create
|
136
|
+
|
137
|
+
@subscription1.reload.magazine_ids.should_not be_nil
|
138
|
+
@subscription2.reload.magazine_ids.should_not be_nil
|
139
|
+
|
140
|
+
@magazine.subscriptions = @subscription3
|
141
|
+
@magazine.subscriptions_ids.should == Set[@subscription3.id]
|
142
|
+
|
143
|
+
@subscription1.reload.magazine_ids.should be_nil
|
144
|
+
@subscription2.reload.magazine_ids.should be_nil
|
145
|
+
@subscription3.reload.magazine_ids.should == Set[@magazine.id]
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'destroys all objects and removes them from the association' do
|
149
|
+
@subscription1 = @magazine.subscriptions.create
|
150
|
+
@subscription2 = @magazine.subscriptions.create
|
151
|
+
@subscription3 = @magazine.subscriptions.create
|
152
|
+
|
153
|
+
@magazine.subscriptions.destroy_all
|
154
|
+
|
155
|
+
@magazine.subscriptions.should be_empty
|
156
|
+
Subscription.all.should be_empty
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'deletes all objects and removes them from the association' do
|
160
|
+
@subscription1 = @magazine.subscriptions.create
|
161
|
+
@subscription2 = @magazine.subscriptions.create
|
162
|
+
@subscription3 = @magazine.subscriptions.create
|
163
|
+
|
164
|
+
@magazine.subscriptions.delete_all
|
165
|
+
|
166
|
+
@magazine.subscriptions.should be_empty
|
167
|
+
Subscription.all.should be_empty
|
168
|
+
end
|
169
|
+
|
104
170
|
end
|
@@ -5,11 +5,17 @@ describe "Dynamoid::Associations::BelongsTo" do
|
|
5
5
|
context 'has many' do
|
6
6
|
before do
|
7
7
|
@subscription = Subscription.create
|
8
|
+
@camel_case = CamelCase.create
|
8
9
|
end
|
9
10
|
|
10
|
-
it '
|
11
|
+
it 'determines nil if it has no associated record' do
|
11
12
|
@subscription.magazine.should be_nil
|
12
13
|
end
|
14
|
+
|
15
|
+
it 'determines target association correctly' do
|
16
|
+
@camel_case.magazine.send(:target_association).should == :camel_cases
|
17
|
+
end
|
18
|
+
|
13
19
|
|
14
20
|
it 'delegates equality to its source record' do
|
15
21
|
@magazine = @subscription.magazine.create
|
@@ -22,6 +28,11 @@ describe "Dynamoid::Associations::BelongsTo" do
|
|
22
28
|
|
23
29
|
@magazine.subscriptions.size.should == 1
|
24
30
|
@magazine.subscriptions.should include @subscription
|
31
|
+
|
32
|
+
@magazine = Magazine.create
|
33
|
+
@user = @magazine.owner.create
|
34
|
+
@user.books.size.should == 1
|
35
|
+
@user.books.should include @magazine
|
25
36
|
end
|
26
37
|
|
27
38
|
it 'behaves like the object it is trying to be' do
|
@@ -36,6 +47,7 @@ describe "Dynamoid::Associations::BelongsTo" do
|
|
36
47
|
context 'has one' do
|
37
48
|
before do
|
38
49
|
@sponsor = Sponsor.create
|
50
|
+
@subscription = Subscription.create
|
39
51
|
end
|
40
52
|
|
41
53
|
it 'determins nil if it has no associated record' do
|
@@ -53,6 +65,9 @@ describe "Dynamoid::Associations::BelongsTo" do
|
|
53
65
|
|
54
66
|
@magazine.sponsor.size.should == 1
|
55
67
|
@magazine.sponsor.should == @sponsor
|
68
|
+
|
69
|
+
@user = @subscription.customer.create
|
70
|
+
@user.monthly.should == @subscription
|
56
71
|
end
|
57
72
|
end
|
58
73
|
end
|