dynamoid 0.2.0 → 0.3.0
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/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
|