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
@@ -12,34 +12,95 @@ module Dynamoid #:nodoc:
12
12
  end
13
13
 
14
14
  module ClassMethods
15
+
16
+ # Initialize a new object and immediately save it to the database.
17
+ #
18
+ # @param [Hash] attrs Attributes with which to create the object.
19
+ #
20
+ # @return [Dynamoid::Document] the saved document
21
+ #
22
+ # @since 0.2.0
15
23
  def create(attrs = {})
16
24
  obj = self.new(attrs)
17
25
  obj.run_callbacks(:create) do
18
- obj.save && obj.new_record = false
26
+ obj.save
27
+ end
28
+ obj
29
+ end
30
+
31
+ # Initialize a new object and immediately save it to the database. Raise an exception if persistence failed.
32
+ #
33
+ # @param [Hash] attrs Attributes with which to create the object.
34
+ #
35
+ # @return [Dynamoid::Document] the saved document
36
+ #
37
+ # @since 0.2.0
38
+ def create!(attrs = {})
39
+ obj = self.new(attrs)
40
+ obj.run_callbacks(:create) do
41
+ obj.save!
19
42
  end
20
43
  obj
21
44
  end
22
45
 
46
+ # Initialize a new object.
47
+ #
48
+ # @param [Hash] attrs Attributes with which to create the object.
49
+ #
50
+ # @return [Dynamoid::Document] the new document
51
+ #
52
+ # @since 0.2.0
23
53
  def build(attrs = {})
24
54
  self.new(attrs)
25
55
  end
56
+
57
+ # Does this object exist?
58
+ #
59
+ # @param [String] id the id of the object
60
+ #
61
+ # @return [Boolean] true/false
62
+ #
63
+ # @since 0.2.0
64
+ def exists?(id)
65
+ !! find(id)
66
+ end
26
67
  end
27
-
68
+
69
+ # Initialize a new object.
70
+ #
71
+ # @param [Hash] attrs Attributes with which to create the object.
72
+ #
73
+ # @return [Dynamoid::Document] the new document
74
+ #
75
+ # @since 0.2.0
28
76
  def initialize(attrs = {})
29
77
  @new_record = true
30
78
  @attributes ||= {}
31
- attrs = self.class.undump(attrs)
32
- self.class.attributes.keys.each {|att| write_attribute(att, attrs[att])}
79
+ incoming_attributes = self.class.undump(attrs)
80
+
81
+ self.class.attributes.keys.each do |attribute|
82
+ send "#{attribute}=", incoming_attributes[attribute]
83
+ end
33
84
  end
34
-
85
+
86
+ # An object is equal to another object if their ids are equal.
87
+ #
88
+ # @since 0.2.0
35
89
  def ==(other)
90
+ return false if other.nil?
36
91
  other.respond_to?(:id) && other.id == self.id
37
92
  end
38
-
93
+
94
+ # Reload an object from the database -- if you suspect the object has changed in the datastore and you need those
95
+ # changes to be reflected immediately, you would call this method.
96
+ #
97
+ # @return [Dynamoid::Document] the document this method was called on
98
+ #
99
+ # @since 0.2.0
39
100
  def reload
40
101
  self.attributes = self.class.find(self.id).attributes
41
102
  self
42
103
  end
43
104
  end
44
105
 
45
- end
106
+ end
@@ -1,8 +1,23 @@
1
1
  # encoding: utf-8
2
2
  module Dynamoid
3
+
4
+ # All the error specific to Dynamoid.
3
5
  module Errors
6
+
7
+ # Generic error class.
8
+ class Error < StandardError; end
9
+
10
+ # InvalidField is raised when an attribute is specified for an index, but the attribute does not exist.
11
+ class InvalidField < Error; end
12
+
13
+ # MissingRangeKey is raised when a table that requires a range key is quieried without one.
14
+ class MissingRangeKey < Error; end
4
15
 
5
- class InvalidField < Exception; end
6
- class MissingRangeKey < Exception; end
16
+ # DocumentNotValid is raised when the document fails validation.
17
+ class DocumentNotValid < Error
18
+ def initialize(document)
19
+ super("Validation failed: #{document.errors.full_messages.join(", ")}")
20
+ end
21
+ end
7
22
  end
8
23
  end
@@ -1,9 +1,12 @@
1
1
  # encoding: utf-8
2
2
  module Dynamoid #:nodoc:
3
3
 
4
+ # All fields on a Dynamoid::Document must be explicitly defined -- if you have fields in the database that are not
5
+ # specified with field, then they will be ignored.
4
6
  module Fields
5
7
  extend ActiveSupport::Concern
6
8
 
9
+ # Initialize the attributes we know the class has, in addition to our magic attributes: id, created_at, and updated_at.
7
10
  included do
8
11
  class_attribute :attributes
9
12
 
@@ -14,6 +17,15 @@ module Dynamoid #:nodoc:
14
17
  end
15
18
 
16
19
  module ClassMethods
20
+
21
+ # Specify a field for a document. Its type determines how it is coerced when read in and out of the datastore:
22
+ # default is string, but you can also specify :integer, :float, :set, :array, :datetime, and :serialized.
23
+ #
24
+ # @param [Symbol] name the name of the field
25
+ # @param [Symbol] type the type of the field (one of :integer, :float, :set, :array, :datetime, or :serialized)
26
+ # @param [Hash] options any additional options for the field
27
+ #
28
+ # @since 0.2.0
17
29
  def field(name, type = :string, options = {})
18
30
  named = name.to_s
19
31
  self.attributes[name] = {:type => type}.merge(options)
@@ -26,27 +38,52 @@ module Dynamoid #:nodoc:
26
38
  define_method("#{named}?") do
27
39
  !read_attribute(named).nil?
28
40
  end
41
+ define_attribute_methods(self.attributes.keys)
29
42
  end
30
43
  end
31
44
 
45
+ # You can access the attributes of an object directly on its attributes method, which is by default an empty hash.
32
46
  attr_accessor :attributes
33
47
  alias :raw_attributes :attributes
34
48
 
49
+ # Write an attribute on the object. Also marks the previous value as dirty.
50
+ #
51
+ # @param [Symbol] name the name of the field
52
+ # @param [Object] value the value to assign to that field
53
+ #
54
+ # @since 0.2.0
35
55
  def write_attribute(name, value)
56
+ self.send("#{name}_will_change!".to_sym) unless self.read_attribute(name) == value
36
57
  attributes[name.to_sym] = value
37
58
  end
38
59
  alias :[]= :write_attribute
39
60
 
61
+ # Read an attribute from an object.
62
+ #
63
+ # @param [Symbol] name the name of the field
64
+ #
65
+ # @since 0.2.0
40
66
  def read_attribute(name)
41
67
  attributes[name.to_sym]
42
68
  end
43
69
  alias :[] :read_attribute
44
70
 
71
+ # Updates multiple attibutes at once, saving the object once the updates are complete.
72
+ #
73
+ # @param [Hash] attributes a hash of attributes to update
74
+ #
75
+ # @since 0.2.0
45
76
  def update_attributes(attributes)
46
77
  attributes.each {|attribute, value| self.write_attribute(attribute, value)}
47
78
  save
48
79
  end
49
80
 
81
+ # Update a single attribute, saving the object afterwards.
82
+ #
83
+ # @param [Symbol] attribute the attribute to update
84
+ # @param [Object] value the value to assign it
85
+ #
86
+ # @since 0.2.0
50
87
  def update_attribute(attribute, value)
51
88
  write_attribute(attribute, value)
52
89
  save
@@ -54,10 +91,16 @@ module Dynamoid #:nodoc:
54
91
 
55
92
  private
56
93
 
94
+ # Automatically called during the created callback to set the created_at time.
95
+ #
96
+ # @since 0.2.0
57
97
  def set_created_at
58
98
  self.created_at = DateTime.now
59
99
  end
60
-
100
+
101
+ # Automatically called during the save callback to set the updated_at time.
102
+ #
103
+ # @since 0.2.0
61
104
  def set_updated_at
62
105
  self.updated_at = DateTime.now
63
106
  end
@@ -1,22 +1,37 @@
1
1
  # encoding: utf-8
2
- module Dynamoid #:nodoc:
2
+ module Dynamoid
3
3
 
4
4
  # This module defines the finder methods that hang off the document at the
5
- # class level.
5
+ # class level, like find, find_by_id, and the method_missing style finders.
6
6
  module Finders
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  module ClassMethods
10
+
11
+ # Find one or many objects, specified by one id or an array of ids.
12
+ #
13
+ # @param [Array/String] *id an array of ids or one single id
14
+ #
15
+ # @return [Dynamoid::Document] one object or an array of objects, depending on whether the input was an array or not
16
+ #
17
+ # @since 0.2.0
10
18
  def find(*id)
11
19
  id = Array(id.flatten.uniq)
12
20
  if id.count == 1
13
21
  self.find_by_id(id.first)
14
22
  else
15
23
  items = Dynamoid::Adapter.read(self.table_name, id)
16
- items[self.table_name].collect{|i| o = self.build(i); o.new_record = false; o}
24
+ items[self.table_name].collect{|i| self.build(i).tap { |o| o.new_record = false } }
17
25
  end
18
26
  end
19
-
27
+
28
+ # Find one object directly by id.
29
+ #
30
+ # @param [String] id the id of the object to find
31
+ #
32
+ # @return [Dynamoid::Document] the found object, or nil if nothing was found
33
+ #
34
+ # @since 0.2.0
20
35
  def find_by_id(id)
21
36
  if item = Dynamoid::Adapter.read(self.table_name, id)
22
37
  obj = self.new(item)
@@ -26,7 +41,18 @@ module Dynamoid #:nodoc:
26
41
  return nil
27
42
  end
28
43
  end
29
-
44
+
45
+ # Find using exciting method_missing finders attributes. Uses criteria chains under the hood to accomplish this neatness.
46
+ #
47
+ # @example find a user by a first name
48
+ # User.find_by_first_name('Josh')
49
+ #
50
+ # @example find all users by first and last name
51
+ # User.find_all_by_first_name_and_last_name('Josh', 'Symonds')
52
+ #
53
+ # @return [Dynamoid::Document/Array] the found object, or an array of found objects if all was somewhere in the method
54
+ #
55
+ # @since 0.2.0
30
56
  def method_missing(method, *args)
31
57
  if method =~ /find/
32
58
  finder = method.to_s.split('_by_').first
@@ -47,4 +73,4 @@ module Dynamoid #:nodoc:
47
73
  end
48
74
  end
49
75
 
50
- end
76
+ end
@@ -3,10 +3,13 @@ require 'dynamoid/indexes/index'
3
3
 
4
4
  module Dynamoid #:nodoc:
5
5
 
6
- # Builds all indexes present on the model.
6
+ # Indexes are quick ways of performing queries by anything other than id in DynamoDB. They are denormalized tables;
7
+ # that is, data is duplicated in the initial table (where the object is saved) and the index table (where
8
+ # we perform indexing).
7
9
  module Indexes
8
10
  extend ActiveSupport::Concern
9
11
 
12
+ # Make some helpful attributes to persist indexes.
10
13
  included do
11
14
  class_attribute :indexes
12
15
 
@@ -14,16 +17,27 @@ module Dynamoid #:nodoc:
14
17
  end
15
18
 
16
19
  module ClassMethods
20
+
21
+ # The call to create an index. Generates a new index with the specified options -- for more information, see Dynamoid::Indexes::Index.
22
+ # This function also attempts to immediately create the indexing table if it does not exist already.
23
+ #
24
+ # @since 0.2.0
17
25
  def index(name, options = {})
18
26
  index = Dynamoid::Indexes::Index.new(self, name, options)
19
27
  self.indexes[index.name] = index
20
28
  create_indexes
21
29
  end
22
30
 
31
+ # Helper function to find indexes.
32
+ #
33
+ # @since 0.2.0
23
34
  def find_index(index)
24
35
  self.indexes[Array(index).collect(&:to_s).sort.collect(&:to_sym)]
25
36
  end
26
37
 
38
+ # Helper function to create indexes (if they don't exist already).
39
+ #
40
+ # @since 0.2.0
27
41
  def create_indexes
28
42
  self.indexes.each do |name, index|
29
43
  opts = index.range_key? ? {:range_key => :range} : {}
@@ -32,12 +46,18 @@ module Dynamoid #:nodoc:
32
46
  end
33
47
  end
34
48
 
49
+ # Callback for an object to save itself to each of a class' indexes.
50
+ #
51
+ # @since 0.2.0
35
52
  def save_indexes
36
53
  self.class.indexes.each do |name, index|
37
54
  index.save(self)
38
55
  end
39
56
  end
40
-
57
+
58
+ # Callback for an object to delete itself from each of a class' indexes.
59
+ #
60
+ # @since 0.2.0
41
61
  def delete_indexes
42
62
  self.class.indexes.each do |name, index|
43
63
  index.delete(self)
@@ -2,11 +2,17 @@
2
2
  module Dynamoid #:nodoc:
3
3
  module Indexes
4
4
 
5
- # The class abstracts information about an index
5
+ # The class contains all the information an index contains, including its keys and which attributes it covers.
6
6
  class Index
7
7
  attr_accessor :source, :name, :hash_keys, :range_keys
8
8
  alias_method :range_key?, :range_keys
9
9
 
10
+ # Create a new index. Pass either :range => true or :range => :column_name to create a ranged index on that column.
11
+ #
12
+ # @param [Class] source the source class for the index
13
+ # @param [Symbol] name the name of the index
14
+ #
15
+ # @since 0.2.0
10
16
  def initialize(source, name, options = {})
11
17
  @source = source
12
18
 
@@ -21,19 +27,45 @@ module Dynamoid #:nodoc:
21
27
  raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless keys.all?{|n| source.attributes.include?(n)}
22
28
  end
23
29
 
30
+ # Sort objects into alphabetical strings, used for composing index names correctly (since we always assume they're alphabetical).
31
+ #
32
+ # @example find all users by first and last name
33
+ # sort([:gamma, :alpha, :beta, :omega]) # => [:alpha, :beta, :gamma, :omega]
34
+ #
35
+ # @since 0.2.0
24
36
  def sort(objs)
25
37
  Array(objs).flatten.compact.uniq.collect(&:to_s).sort.collect(&:to_sym)
26
38
  end
27
-
39
+
40
+ # Return the array of keys this index uses for its table.
41
+ #
42
+ # @since 0.2.0
28
43
  def keys
29
44
  [Array(hash_keys) + Array(range_keys)].flatten.uniq
30
45
  end
31
46
 
47
+ # Return the table name for this index.
48
+ #
49
+ # @since 0.2.0
32
50
  def table_name
33
51
  "#{Dynamoid::Config.namespace}_index_#{source.to_s.downcase}_#{name.collect(&:to_s).collect(&:pluralize).join('_and_')}"
34
52
  end
35
-
36
- def values(attrs)
53
+
54
+ # Given either an object or a list of attributes, generate a hash key and a range key for the index. Optionally pass in
55
+ # true to changed_attributes for a list of all the object's dirty attributes in convenient index form (for deleting stale
56
+ # information from the indexes).
57
+ #
58
+ # @param [Object] attrs either an object that responds to :attributes, or a hash of attributes
59
+ #
60
+ # @return [Hash] a hash with the keys :hash_value and :range_value
61
+ #
62
+ # @since 0.2.0
63
+ def values(attrs, changed_attributes = false)
64
+ if changed_attributes
65
+ hash = {}
66
+ attrs.changes.each {|k, v| hash[k.to_sym] = (v.first || v.last)}
67
+ attrs = hash
68
+ end
37
69
  attrs = attrs.send(:attributes) if attrs.respond_to?(:attributes)
38
70
  {}.tap do |hash|
39
71
  hash[:hash_value] = hash_keys.collect{|key| attrs[key]}.join('.')
@@ -41,16 +73,25 @@ module Dynamoid #:nodoc:
41
73
  end
42
74
  end
43
75
 
76
+ # Save an object to this index, merging it with existing ids if there's already something present at this index location.
77
+ # First, though, delete this object from its old indexes (so the object isn't listed in an erroneous index).
78
+ #
79
+ # @since 0.2.0
44
80
  def save(obj)
81
+ self.delete(obj, true)
45
82
  values = values(obj)
46
83
  return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
47
84
  existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], values[:range_value])
48
85
  ids = ((existing and existing[:ids]) or Set.new)
49
86
  Dynamoid::Adapter.write(self.table_name, {:id => values[:hash_value], :ids => ids.merge([obj.id]), :range => values[:range_value]})
50
87
  end
51
-
52
- def delete(obj)
53
- values = values(obj)
88
+
89
+ # Delete an object from this index, preserving existing ids if there are any, and failing gracefully if for some reason the
90
+ # index doesn't already have this object in it.
91
+ #
92
+ # @since 0.2.0
93
+ def delete(obj, changed_attributes = false)
94
+ values = values(obj, changed_attributes)
54
95
  return true if values[:hash_value].blank? || (!values[:range_value].nil? && values[:range_value].blank?)
55
96
  existing = Dynamoid::Adapter.read(self.table_name, values[:hash_value], values[:range_value])
56
97
  return true unless existing && existing[:ids] && existing[:ids].include?(obj.id)