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
@@ -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)