adept_dynamoid 0.5.0.6

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 (119) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Dynamoid.gemspec +193 -0
  4. data/Gemfile +23 -0
  5. data/Gemfile.lock +86 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.markdown +265 -0
  8. data/Rakefile +62 -0
  9. data/VERSION +1 -0
  10. data/doc/.nojekyll +0 -0
  11. data/doc/Dynamoid.html +312 -0
  12. data/doc/Dynamoid/Adapter.html +1385 -0
  13. data/doc/Dynamoid/Adapter/AwsSdk.html +1585 -0
  14. data/doc/Dynamoid/Adapter/Local.html +1574 -0
  15. data/doc/Dynamoid/Associations.html +131 -0
  16. data/doc/Dynamoid/Associations/Association.html +794 -0
  17. data/doc/Dynamoid/Associations/BelongsTo.html +158 -0
  18. data/doc/Dynamoid/Associations/ClassMethods.html +723 -0
  19. data/doc/Dynamoid/Associations/HasAndBelongsToMany.html +164 -0
  20. data/doc/Dynamoid/Associations/HasMany.html +164 -0
  21. data/doc/Dynamoid/Associations/HasOne.html +158 -0
  22. data/doc/Dynamoid/Associations/ManyAssociation.html +1640 -0
  23. data/doc/Dynamoid/Associations/SingleAssociation.html +598 -0
  24. data/doc/Dynamoid/Components.html +204 -0
  25. data/doc/Dynamoid/Config.html +395 -0
  26. data/doc/Dynamoid/Config/Options.html +609 -0
  27. data/doc/Dynamoid/Criteria.html +131 -0
  28. data/doc/Dynamoid/Criteria/Chain.html +1063 -0
  29. data/doc/Dynamoid/Criteria/ClassMethods.html +98 -0
  30. data/doc/Dynamoid/Document.html +666 -0
  31. data/doc/Dynamoid/Document/ClassMethods.html +937 -0
  32. data/doc/Dynamoid/Errors.html +118 -0
  33. data/doc/Dynamoid/Errors/DocumentNotValid.html +210 -0
  34. data/doc/Dynamoid/Errors/Error.html +130 -0
  35. data/doc/Dynamoid/Errors/InvalidField.html +133 -0
  36. data/doc/Dynamoid/Errors/MissingRangeKey.html +133 -0
  37. data/doc/Dynamoid/Fields.html +669 -0
  38. data/doc/Dynamoid/Fields/ClassMethods.html +309 -0
  39. data/doc/Dynamoid/Finders.html +128 -0
  40. data/doc/Dynamoid/Finders/ClassMethods.html +516 -0
  41. data/doc/Dynamoid/Indexes.html +308 -0
  42. data/doc/Dynamoid/Indexes/ClassMethods.html +353 -0
  43. data/doc/Dynamoid/Indexes/Index.html +1104 -0
  44. data/doc/Dynamoid/Persistence.html +651 -0
  45. data/doc/Dynamoid/Persistence/ClassMethods.html +670 -0
  46. data/doc/Dynamoid/Validations.html +399 -0
  47. data/doc/_index.html +461 -0
  48. data/doc/class_list.html +47 -0
  49. data/doc/css/common.css +1 -0
  50. data/doc/css/full_list.css +55 -0
  51. data/doc/css/style.css +322 -0
  52. data/doc/file.LICENSE.html +66 -0
  53. data/doc/file.README.html +312 -0
  54. data/doc/file_list.html +52 -0
  55. data/doc/frames.html +13 -0
  56. data/doc/index.html +312 -0
  57. data/doc/js/app.js +205 -0
  58. data/doc/js/full_list.js +173 -0
  59. data/doc/js/jquery.js +16 -0
  60. data/doc/method_list.html +1238 -0
  61. data/doc/top-level-namespace.html +105 -0
  62. data/lib/dynamoid.rb +47 -0
  63. data/lib/dynamoid/adapter.rb +177 -0
  64. data/lib/dynamoid/adapter/aws_sdk.rb +223 -0
  65. data/lib/dynamoid/associations.rb +106 -0
  66. data/lib/dynamoid/associations/association.rb +105 -0
  67. data/lib/dynamoid/associations/belongs_to.rb +44 -0
  68. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +40 -0
  69. data/lib/dynamoid/associations/has_many.rb +39 -0
  70. data/lib/dynamoid/associations/has_one.rb +39 -0
  71. data/lib/dynamoid/associations/many_association.rb +191 -0
  72. data/lib/dynamoid/associations/single_association.rb +69 -0
  73. data/lib/dynamoid/components.rb +36 -0
  74. data/lib/dynamoid/config.rb +57 -0
  75. data/lib/dynamoid/config/options.rb +78 -0
  76. data/lib/dynamoid/criteria.rb +29 -0
  77. data/lib/dynamoid/criteria/chain.rb +243 -0
  78. data/lib/dynamoid/dirty.rb +41 -0
  79. data/lib/dynamoid/document.rb +184 -0
  80. data/lib/dynamoid/errors.rb +28 -0
  81. data/lib/dynamoid/fields.rb +130 -0
  82. data/lib/dynamoid/finders.rb +131 -0
  83. data/lib/dynamoid/identity_map.rb +96 -0
  84. data/lib/dynamoid/indexes.rb +69 -0
  85. data/lib/dynamoid/indexes/index.rb +103 -0
  86. data/lib/dynamoid/middleware/identity_map.rb +16 -0
  87. data/lib/dynamoid/persistence.rb +247 -0
  88. data/lib/dynamoid/validations.rb +36 -0
  89. data/spec/app/models/address.rb +10 -0
  90. data/spec/app/models/camel_case.rb +24 -0
  91. data/spec/app/models/magazine.rb +11 -0
  92. data/spec/app/models/message.rb +9 -0
  93. data/spec/app/models/sponsor.rb +8 -0
  94. data/spec/app/models/subscription.rb +12 -0
  95. data/spec/app/models/tweet.rb +12 -0
  96. data/spec/app/models/user.rb +26 -0
  97. data/spec/dynamoid/adapter/aws_sdk_spec.rb +186 -0
  98. data/spec/dynamoid/adapter_spec.rb +117 -0
  99. data/spec/dynamoid/associations/association_spec.rb +194 -0
  100. data/spec/dynamoid/associations/belongs_to_spec.rb +71 -0
  101. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +47 -0
  102. data/spec/dynamoid/associations/has_many_spec.rb +42 -0
  103. data/spec/dynamoid/associations/has_one_spec.rb +45 -0
  104. data/spec/dynamoid/associations_spec.rb +16 -0
  105. data/spec/dynamoid/config_spec.rb +27 -0
  106. data/spec/dynamoid/criteria/chain_spec.rb +140 -0
  107. data/spec/dynamoid/criteria_spec.rb +72 -0
  108. data/spec/dynamoid/dirty_spec.rb +49 -0
  109. data/spec/dynamoid/document_spec.rb +118 -0
  110. data/spec/dynamoid/fields_spec.rb +127 -0
  111. data/spec/dynamoid/finders_spec.rb +135 -0
  112. data/spec/dynamoid/identity_map_spec.rb +45 -0
  113. data/spec/dynamoid/indexes/index_spec.rb +104 -0
  114. data/spec/dynamoid/indexes_spec.rb +25 -0
  115. data/spec/dynamoid/persistence_spec.rb +176 -0
  116. data/spec/dynamoid/validations_spec.rb +36 -0
  117. data/spec/dynamoid_spec.rb +9 -0
  118. data/spec/spec_helper.rb +50 -0
  119. metadata +376 -0
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ module Associations
5
+ module SingleAssociation
6
+ include Association
7
+
8
+ delegate :class, :to => :target
9
+
10
+ def setter(object)
11
+ delete
12
+ source.update_attribute(source_attribute, Set[object.id])
13
+ self.send(:associate_target, object) if target_association
14
+ object
15
+ end
16
+
17
+ def delete
18
+ source.update_attribute(source_attribute, nil)
19
+ self.send(:disassociate_target, target) if target && target_association
20
+ target
21
+ end
22
+
23
+ def create!(attributes = {})
24
+ setter(target_class.create!(attributes))
25
+ end
26
+
27
+ def create(attributes = {})
28
+ setter(target_class.create!(attributes))
29
+ end
30
+
31
+
32
+ # Is this object equal to the association's target?
33
+ #
34
+ # @return [Boolean] true/false
35
+ #
36
+ # @since 0.2.0
37
+ def ==(other)
38
+ target == other
39
+ end
40
+
41
+ # Delegate methods we don't find directly to the target.
42
+ #
43
+ # @since 0.2.0
44
+ def method_missing(method, *args)
45
+ if target.respond_to?(method)
46
+ target.send(method, *args)
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ def nil?
53
+ target.nil?
54
+ end
55
+
56
+ private
57
+
58
+ # Find the target of the has_one association.
59
+ #
60
+ # @return [Dynamoid::Document] the found target (or nil if nothing)
61
+ #
62
+ # @since 0.2.0
63
+ def find_target
64
+ return if source_ids.empty?
65
+ target_class.find(source_ids.first)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+
4
+ # All modules that a Document is composed of are defined in this
5
+ # module, to keep the document class from getting too cluttered.
6
+ module Components
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ extend ActiveModel::Translation
11
+ extend ActiveModel::Callbacks
12
+
13
+ define_model_callbacks :create, :save, :destroy, :initialize
14
+
15
+ before_create :set_created_at
16
+ before_save :set_updated_at
17
+ end
18
+
19
+ include ActiveModel::AttributeMethods
20
+ include ActiveModel::Conversion
21
+ include ActiveModel::MassAssignmentSecurity
22
+ include ActiveModel::Naming
23
+ include ActiveModel::Observing
24
+ include ActiveModel::Serializers::JSON
25
+ include ActiveModel::Serializers::Xml
26
+ include Dynamoid::Fields
27
+ include Dynamoid::Indexes
28
+ include Dynamoid::Persistence
29
+ include Dynamoid::Finders
30
+ include Dynamoid::Associations
31
+ include Dynamoid::Criteria
32
+ include Dynamoid::Validations
33
+ include Dynamoid::IdentityMap
34
+ include Dynamoid::Dirty
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ # encoding: utf-8
2
+ require "uri"
3
+ require "dynamoid/config/options"
4
+
5
+ module Dynamoid
6
+
7
+ # Contains all the basic configuration information required for Dynamoid: both sensible defaults and required fields.
8
+ module Config
9
+ extend self
10
+ extend Options
11
+ include ActiveModel::Observing
12
+
13
+ # All the default options.
14
+ option :adapter, :default => 'aws-sdk'
15
+ option :namespace, :default => defined?(Rails) ? "dynamoid_#{Rails.application.class.parent_name}_#{Rails.env}" : "dynamoid"
16
+ option :logger, :default => defined?(Rails)
17
+ option :access_key
18
+ option :secret_key
19
+ option :read_capacity, :default => 100
20
+ option :write_capacity, :default => 20
21
+ option :warn_on_scan, :default => true
22
+ option :partitioning, :default => false
23
+ option :partition_size, :default => 200
24
+ option :endpoint, :default => 'dynamodb.us-east-1.amazonaws.com'
25
+ option :use_ssl, :default => true
26
+ option :port, :default => '443'
27
+ option :included_models, :default => []
28
+ option :identity_map, :default => false
29
+
30
+ # The default logger for Dynamoid: either the Rails logger or just stdout.
31
+ #
32
+ # @since 0.2.0
33
+ def default_logger
34
+ defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
35
+ end
36
+
37
+ # Returns the assigned logger instance.
38
+ #
39
+ # @since 0.2.0
40
+ def logger
41
+ @logger ||= default_logger
42
+ end
43
+
44
+ # If you want to, set the logger manually to any output you'd like. Or pass false or nil to disable logging entirely.
45
+ #
46
+ # @since 0.2.0
47
+ def logger=(logger)
48
+ case logger
49
+ when false, nil then @logger = nil
50
+ when true then @logger = default_logger
51
+ else
52
+ @logger = logger if logger.respond_to?(:info)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,78 @@
1
+ # Shamelessly stolen from Mongoid!
2
+ module Dynamoid #:nodoc
3
+ module Config
4
+
5
+ # Encapsulates logic for setting options.
6
+ module Options
7
+
8
+ # Get the defaults or initialize a new empty hash.
9
+ #
10
+ # @example Get the defaults.
11
+ # options.defaults
12
+ #
13
+ # @return [ Hash ] The default options.
14
+ #
15
+ # @since 0.2.0
16
+ def defaults
17
+ @defaults ||= {}
18
+ end
19
+
20
+ # Define a configuration option with a default.
21
+ #
22
+ # @example Define the option.
23
+ # Options.option(:persist_in_safe_mode, :default => false)
24
+ #
25
+ # @param [ Symbol ] name The name of the configuration option.
26
+ # @param [ Hash ] options Extras for the option.
27
+ #
28
+ # @option options [ Object ] :default The default value.
29
+ #
30
+ # @since 0.2.0
31
+ def option(name, options = {})
32
+ defaults[name] = settings[name] = options[:default]
33
+
34
+ class_eval <<-RUBY
35
+ def #{name}
36
+ settings[#{name.inspect}]
37
+ end
38
+
39
+ def #{name}=(value)
40
+ settings[#{name.inspect}] = value
41
+ end
42
+
43
+ def #{name}?
44
+ #{name}
45
+ end
46
+
47
+ def reset_#{name}
48
+ settings[#{name.inspect}] = defaults[#{name.inspect}]
49
+ end
50
+ RUBY
51
+ end
52
+
53
+ # Reset the configuration options to the defaults.
54
+ #
55
+ # @example Reset the configuration options.
56
+ # config.reset
57
+ #
58
+ # @return [ Hash ] The defaults.
59
+ #
60
+ # @since 0.2.0
61
+ def reset
62
+ settings.replace(defaults)
63
+ end
64
+
65
+ # Get the settings or initialize a new empty hash.
66
+ #
67
+ # @example Get the settings.
68
+ # options.settings
69
+ #
70
+ # @return [ Hash ] The setting options.
71
+ #
72
+ # @since 0.2.0
73
+ def settings
74
+ @settings ||= {}
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ require 'dynamoid/criteria/chain'
3
+
4
+ module Dynamoid
5
+
6
+ # Allows classes to be queried by where, all, first, and each and return criteria chains.
7
+ module Criteria
8
+ extend ActiveSupport::Concern
9
+
10
+ module ClassMethods
11
+
12
+ [:where, :all, :first, :each, :limit, :start, :scan_index_forward].each do |meth|
13
+ # Return a criteria chain in response to a method that will begin or end a chain. For more information,
14
+ # see Dynamoid::Criteria::Chain.
15
+ #
16
+ # @since 0.2.0
17
+ define_method(meth) do |*args|
18
+ chain = Dynamoid::Criteria::Chain.new(self)
19
+ if args
20
+ chain.send(meth, *args)
21
+ else
22
+ chain.send(meth)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,243 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+ module Criteria
4
+
5
+ # The criteria chain is equivalent to an ActiveRecord relation (and realistically I should change the name from
6
+ # chain to relation). It is a chainable object that builds up a query and eventually executes it either on an index
7
+ # or by a full table scan.
8
+ class Chain
9
+ attr_accessor :query, :source, :index, :values, :limit, :start, :consistent_read
10
+ include Enumerable
11
+
12
+ # Create a new criteria chain.
13
+ #
14
+ # @param [Class] source the class upon which the ultimate query will be performed.
15
+ def initialize(source)
16
+ @query = {}
17
+ @source = source
18
+ @consistent_read = false
19
+ @scan_index_forward = true
20
+ end
21
+
22
+ # The workhorse method of the criteria chain. Each key in the passed in hash will become another criteria that the
23
+ # ultimate query must match. A key can either be a symbol or a string, and should be an attribute name or
24
+ # an attribute name with a range operator.
25
+ #
26
+ # @example A simple criteria
27
+ # where(:name => 'Josh')
28
+ #
29
+ # @example A more complicated criteria
30
+ # where(:name => 'Josh', 'created_at.gt' => DateTime.now - 1.day)
31
+ #
32
+ # @since 0.2.0
33
+ def where(args)
34
+ args.each {|k, v| query[k] = v}
35
+ self
36
+ end
37
+
38
+ def consistent
39
+ @consistent_read = true
40
+ self
41
+ end
42
+
43
+ # Returns all the records matching the criteria.
44
+ #
45
+ # @since 0.2.0
46
+ def all
47
+ records
48
+ end
49
+
50
+ # Returns the first record matching the criteria.
51
+ #
52
+ # @since 0.2.0
53
+ def first
54
+ limit(1).first
55
+ end
56
+
57
+ def limit(limit)
58
+ @limit = limit
59
+ records
60
+ end
61
+
62
+ def start(start)
63
+ @start = start
64
+ self
65
+ end
66
+
67
+ def scan_index_forward(scan_index_forward)
68
+ @scan_index_forward = scan_index_forward
69
+ self
70
+ end
71
+
72
+ # Allows you to use the results of a search as an enumerable over the results found.
73
+ #
74
+ # @since 0.2.0
75
+ def each(&block)
76
+ records.each(&block)
77
+ end
78
+
79
+ def consistent_opts
80
+ { :consistent_read => consistent_read }
81
+ end
82
+
83
+ private
84
+
85
+ # The actual records referenced by the association.
86
+ #
87
+ # @return [Array] an array of the found records.
88
+ #
89
+ # @since 0.2.0
90
+ def records
91
+ if range?
92
+ records_with_range
93
+ elsif index
94
+ records_with_index
95
+ else
96
+ records_without_index
97
+ end
98
+ end
99
+
100
+ # If the query matches an index on the associated class, then this method will retrieve results from the index table.
101
+ #
102
+ # @return [Array] an array of the found records.
103
+ #
104
+ # @since 0.2.0
105
+ def records_with_index
106
+ ids = ids_from_index
107
+ if ids.nil? || ids.empty?
108
+ []
109
+ else
110
+ ids = ids.to_a
111
+
112
+ if @start
113
+ ids = ids.drop_while { |id| id != @start.hash_key }.drop(1)
114
+ end
115
+
116
+ ids = ids.take(@limit) if @limit
117
+ Array(source.find(ids, consistent_opts))
118
+ end
119
+ end
120
+
121
+ # Returns the Set of IDs from the index table.
122
+ #
123
+ # @return [Set] a Set containing the IDs from the index.
124
+ def ids_from_index
125
+ if index.range_key?
126
+ Dynamoid::Adapter.query(index.table_name, index_query.merge(consistent_opts)).inject(Set.new) do |all, record|
127
+ all + Set.new(record[:ids])
128
+ end
129
+ else
130
+ results = Dynamoid::Adapter.read(index.table_name, index_query[:hash_value], consistent_opts)
131
+ results ? results[:ids] : []
132
+ end
133
+ end
134
+
135
+ def records_with_range
136
+ Dynamoid::Adapter.query(source.table_name, range_query).collect {|hash| source.from_database(hash) }
137
+ end
138
+
139
+ # If the query does not match an index, we'll manually scan the associated table to find results.
140
+ #
141
+ # @return [Array] an array of the found records.
142
+ #
143
+ # @since 0.2.0
144
+ def records_without_index
145
+ if Dynamoid::Config.warn_on_scan
146
+ Dynamoid.logger.warn 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
147
+ Dynamoid.logger.warn "You can index this query by adding this to #{source.to_s.downcase}.rb: index [#{source.attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
148
+ end
149
+
150
+ if @consistent_read
151
+ raise Dynamoid::Errors::InvalidQuery, 'Consistent read is not supported by SCAN operation'
152
+ end
153
+
154
+ Dynamoid::Adapter.scan(source.table_name, query, query_opts).collect {|hash| source.from_database(hash) }
155
+ end
156
+
157
+ # Format the provided query so that it can be used to query results from DynamoDB.
158
+ #
159
+ # @return [Hash] a hash with keys of :hash_value and :range_value
160
+ #
161
+ # @since 0.2.0
162
+ def index_query
163
+ values = index.values(query)
164
+ {}.tap do |hash|
165
+ hash[:hash_value] = values[:hash_value]
166
+ if index.range_key?
167
+ key = query.keys.find{|k| k.to_s.include?('.')}
168
+ if key
169
+ hash.merge!(range_hash(key))
170
+ else
171
+ raise Dynamoid::Errors::MissingRangeKey, 'This index requires a range key'
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ def range_hash(key)
178
+ val = query[key]
179
+
180
+ return { :range_value => query[key] } if query[key].is_a?(Range)
181
+
182
+ case key.split('.').last
183
+ when 'gt'
184
+ { :range_greater_than => val.to_f }
185
+ when 'lt'
186
+ { :range_less_than => val.to_f }
187
+ when 'gte'
188
+ { :range_gte => val.to_f }
189
+ when 'lte'
190
+ { :range_lte => val.to_f }
191
+ when 'begins_with'
192
+ { :range_begins_with => val }
193
+ end
194
+ end
195
+
196
+ def range_query
197
+ opts = { :hash_value => query[source.hash_key] }
198
+ if key = query.keys.find { |k| k.to_s.include?('.') }
199
+ opts.merge!(range_hash(key))
200
+ end
201
+ opts.merge(query_opts).merge(consistent_opts)
202
+ end
203
+
204
+ # Return an index that fulfills all the attributes the criteria is querying, or nil if none is found.
205
+ #
206
+ # @since 0.2.0
207
+ def index
208
+ index = source.find_index(query_keys)
209
+ return nil if index.blank?
210
+ index
211
+ end
212
+
213
+ def query_keys
214
+ query.keys.collect{|k| k.to_s.split('.').first}
215
+ end
216
+
217
+ def range?
218
+ return false unless source.range_key
219
+ query_keys == [source.hash_key.to_s] || (query_keys.to_set == [source.hash_key.to_s, source.range_key.to_s].to_set)
220
+ end
221
+
222
+ def start_key
223
+ hash_key_type = @start.class.attributes[@start.class.hash_key][:type] == :string ? 'S' : 'N'
224
+ key = { :hash_key_element => { hash_key_type => @start.hash_key.to_s } }
225
+ if range_key = @start.class.range_key
226
+ range_key_type = @start.class.attributes[range_key][:type] == :string ? 'S' : 'N'
227
+ key.merge!({:range_key_element => { range_key_type => @start.send(range_key).to_s } })
228
+ end
229
+ key
230
+ end
231
+
232
+ def query_opts
233
+ opts = {}
234
+ opts[:limit] = @limit if @limit
235
+ opts[:next_token] = start_key if @start
236
+ opts[:scan_index_forward] = @scan_index_forward
237
+ opts
238
+ end
239
+ end
240
+
241
+ end
242
+
243
+ end