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