dynamoid 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/Dynamoid.gemspec +65 -3
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +6 -0
  4. data/README.markdown +117 -22
  5. data/Rakefile +22 -9
  6. data/VERSION +1 -1
  7. data/doc/.nojekyll +0 -0
  8. data/doc/Dynamoid.html +300 -0
  9. data/doc/Dynamoid/Adapter.html +1387 -0
  10. data/doc/Dynamoid/Adapter/AwsSdk.html +1561 -0
  11. data/doc/Dynamoid/Adapter/Local.html +1487 -0
  12. data/doc/Dynamoid/Associations.html +131 -0
  13. data/doc/Dynamoid/Associations/Association.html +1706 -0
  14. data/doc/Dynamoid/Associations/BelongsTo.html +339 -0
  15. data/doc/Dynamoid/Associations/ClassMethods.html +723 -0
  16. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +339 -0
  17. data/doc/Dynamoid/Associations/HasMany.html +339 -0
  18. data/doc/Dynamoid/Associations/HasOne.html +339 -0
  19. data/doc/Dynamoid/Components.html +202 -0
  20. data/doc/Dynamoid/Config.html +395 -0
  21. data/doc/Dynamoid/Config/Options.html +609 -0
  22. data/doc/Dynamoid/Criteria.html +131 -0
  23. data/doc/Dynamoid/Criteria/Chain.html +759 -0
  24. data/doc/Dynamoid/Criteria/ClassMethods.html +98 -0
  25. data/doc/Dynamoid/Document.html +512 -0
  26. data/doc/Dynamoid/Document/ClassMethods.html +581 -0
  27. data/doc/Dynamoid/Errors.html +118 -0
  28. data/doc/Dynamoid/Errors/DocumentNotValid.html +210 -0
  29. data/doc/Dynamoid/Errors/Error.html +130 -0
  30. data/doc/Dynamoid/Errors/InvalidField.html +133 -0
  31. data/doc/Dynamoid/Errors/MissingRangeKey.html +133 -0
  32. data/doc/Dynamoid/Fields.html +649 -0
  33. data/doc/Dynamoid/Fields/ClassMethods.html +264 -0
  34. data/doc/Dynamoid/Finders.html +128 -0
  35. data/doc/Dynamoid/Finders/ClassMethods.html +502 -0
  36. data/doc/Dynamoid/Indexes.html +308 -0
  37. data/doc/Dynamoid/Indexes/ClassMethods.html +351 -0
  38. data/doc/Dynamoid/Indexes/Index.html +1089 -0
  39. data/doc/Dynamoid/Persistence.html +653 -0
  40. data/doc/Dynamoid/Persistence/ClassMethods.html +568 -0
  41. data/doc/Dynamoid/Validations.html +399 -0
  42. data/doc/_index.html +439 -0
  43. data/doc/class_list.html +47 -0
  44. data/doc/css/common.css +1 -0
  45. data/doc/css/full_list.css +55 -0
  46. data/doc/css/style.css +322 -0
  47. data/doc/file.LICENSE.html +66 -0
  48. data/doc/file.README.html +279 -0
  49. data/doc/file_list.html +52 -0
  50. data/doc/frames.html +13 -0
  51. data/doc/index.html +279 -0
  52. data/doc/js/app.js +205 -0
  53. data/doc/js/full_list.js +173 -0
  54. data/doc/js/jquery.js +16 -0
  55. data/doc/method_list.html +1054 -0
  56. data/doc/top-level-namespace.html +105 -0
  57. data/lib/dynamoid.rb +2 -1
  58. data/lib/dynamoid/adapter.rb +77 -6
  59. data/lib/dynamoid/adapter/aws_sdk.rb +96 -16
  60. data/lib/dynamoid/adapter/local.rb +84 -15
  61. data/lib/dynamoid/associations.rb +53 -4
  62. data/lib/dynamoid/associations/association.rb +154 -26
  63. data/lib/dynamoid/associations/belongs_to.rb +32 -6
  64. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +29 -3
  65. data/lib/dynamoid/associations/has_many.rb +30 -4
  66. data/lib/dynamoid/associations/has_one.rb +26 -3
  67. data/lib/dynamoid/components.rb +7 -5
  68. data/lib/dynamoid/config.rb +15 -2
  69. data/lib/dynamoid/config/options.rb +8 -0
  70. data/lib/dynamoid/criteria.rb +7 -2
  71. data/lib/dynamoid/criteria/chain.rb +55 -8
  72. data/lib/dynamoid/document.rb +68 -7
  73. data/lib/dynamoid/errors.rb +17 -2
  74. data/lib/dynamoid/fields.rb +44 -1
  75. data/lib/dynamoid/finders.rb +32 -6
  76. data/lib/dynamoid/indexes.rb +22 -2
  77. data/lib/dynamoid/indexes/index.rb +48 -7
  78. data/lib/dynamoid/persistence.rb +111 -51
  79. data/lib/dynamoid/validations.rb +36 -0
  80. data/spec/app/models/address.rb +2 -1
  81. data/spec/app/models/camel_case.rb +11 -0
  82. data/spec/app/models/magazine.rb +4 -1
  83. data/spec/app/models/sponsor.rb +3 -1
  84. data/spec/app/models/subscription.rb +5 -1
  85. data/spec/app/models/user.rb +10 -1
  86. data/spec/dynamoid/associations/association_spec.rb +67 -1
  87. data/spec/dynamoid/associations/belongs_to_spec.rb +16 -1
  88. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +7 -0
  89. data/spec/dynamoid/associations/has_many_spec.rb +14 -0
  90. data/spec/dynamoid/associations/has_one_spec.rb +10 -1
  91. data/spec/dynamoid/criteria_spec.rb +5 -1
  92. data/spec/dynamoid/document_spec.rb +23 -3
  93. data/spec/dynamoid/fields_spec.rb +10 -1
  94. data/spec/dynamoid/indexes/index_spec.rb +19 -0
  95. data/spec/dynamoid/persistence_spec.rb +24 -0
  96. data/spec/dynamoid/validations_spec.rb +36 -0
  97. metadata +105 -4
@@ -1,9 +1,10 @@
1
1
  require 'securerandom'
2
2
 
3
3
  # encoding: utf-8
4
- module Dynamoid #:nodoc:
4
+ module Dynamoid
5
5
 
6
- # This module saves things!
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
- def undump(incoming = {})
27
- incoming.symbolize_keys!
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
- value = incoming[attribute]
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
- def save
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
- value = self.read_attribute(attribute)
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
@@ -2,4 +2,5 @@ class Address
2
2
  include Dynamoid::Document
3
3
 
4
4
  field :city
5
- end
5
+ field :options, :serialized
6
+ end
@@ -0,0 +1,11 @@
1
+ class CamelCase
2
+ include Dynamoid::Document
3
+
4
+ field :color
5
+
6
+ belongs_to :magazine
7
+ has_many :users
8
+ has_one :sponsor
9
+ has_and_belongs_to_many :subscriptions
10
+
11
+ end
@@ -4,5 +4,8 @@ class Magazine
4
4
  field :title
5
5
 
6
6
  has_many :subscriptions
7
+ has_many :camel_cases
7
8
  has_one :sponsor
8
- end
9
+
10
+ belongs_to :owner, :class_name => 'User', :inverse_of => :books
11
+ end
@@ -3,4 +3,6 @@ class Sponsor
3
3
 
4
4
  belongs_to :magazine
5
5
  has_many :subscriptions
6
- end
6
+
7
+ belongs_to :camel_case
8
+ end
@@ -5,4 +5,8 @@ class Subscription
5
5
 
6
6
  belongs_to :magazine
7
7
  has_and_belongs_to_many :users
8
- end
8
+
9
+ belongs_to :customer, :class_name => 'User', :inverse_of => :monthly
10
+
11
+ has_and_belongs_to_many :camel_cases
12
+ end
@@ -14,4 +14,13 @@ class User
14
14
  index :created_at, :range => true
15
15
 
16
16
  has_and_belongs_to_many :subscriptions
17
- end
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 'determins nil if it has no associated record' do
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