dynamoid 0.0.2

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 (54) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Dynamoid.gemspec +116 -0
  4. data/Gemfile +19 -0
  5. data/Gemfile.lock +58 -0
  6. data/LICENSE.txt +20 -0
  7. data/README.markdown +86 -0
  8. data/Rakefile +49 -0
  9. data/VERSION +1 -0
  10. data/lib/dynamoid.rb +32 -0
  11. data/lib/dynamoid/adapter.rb +24 -0
  12. data/lib/dynamoid/adapter/aws_sdk.rb +100 -0
  13. data/lib/dynamoid/adapter/local.rb +77 -0
  14. data/lib/dynamoid/associations.rb +54 -0
  15. data/lib/dynamoid/associations/association.rb +80 -0
  16. data/lib/dynamoid/associations/belongs_to.rb +39 -0
  17. data/lib/dynamoid/associations/has_and_belongs_to_many.rb +34 -0
  18. data/lib/dynamoid/associations/has_many.rb +33 -0
  19. data/lib/dynamoid/associations/has_one.rb +36 -0
  20. data/lib/dynamoid/attributes.rb +37 -0
  21. data/lib/dynamoid/components.rb +25 -0
  22. data/lib/dynamoid/config.rb +19 -0
  23. data/lib/dynamoid/config/options.rb +74 -0
  24. data/lib/dynamoid/document.rb +35 -0
  25. data/lib/dynamoid/errors.rb +8 -0
  26. data/lib/dynamoid/fields.rb +32 -0
  27. data/lib/dynamoid/finders.rb +62 -0
  28. data/lib/dynamoid/indexes.rb +59 -0
  29. data/lib/dynamoid/persistence.rb +35 -0
  30. data/lib/dynamoid/relations.rb +21 -0
  31. data/spec/app/models/address.rb +5 -0
  32. data/spec/app/models/magazine.rb +6 -0
  33. data/spec/app/models/sponsor.rb +6 -0
  34. data/spec/app/models/subscription.rb +6 -0
  35. data/spec/app/models/user.rb +13 -0
  36. data/spec/dynamoid/adapter/aws_sdk_spec.rb +123 -0
  37. data/spec/dynamoid/adapter/local_spec.rb +150 -0
  38. data/spec/dynamoid/adapter_spec.rb +13 -0
  39. data/spec/dynamoid/associations/association_spec.rb +71 -0
  40. data/spec/dynamoid/associations/belongs_to_spec.rb +50 -0
  41. data/spec/dynamoid/associations/has_and_belongs_to_many_spec.rb +30 -0
  42. data/spec/dynamoid/associations/has_many_spec.rb +28 -0
  43. data/spec/dynamoid/associations/has_one_spec.rb +37 -0
  44. data/spec/dynamoid/associations_spec.rb +16 -0
  45. data/spec/dynamoid/attributes_spec.rb +43 -0
  46. data/spec/dynamoid/document_spec.rb +38 -0
  47. data/spec/dynamoid/fields_spec.rb +26 -0
  48. data/spec/dynamoid/finders_spec.rb +114 -0
  49. data/spec/dynamoid/indexes_spec.rb +54 -0
  50. data/spec/dynamoid/persistence_spec.rb +55 -0
  51. data/spec/dynamoid/relations_spec.rb +6 -0
  52. data/spec/dynamoid_spec.rb +5 -0
  53. data/spec/spec_helper.rb +52 -0
  54. metadata +204 -0
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
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 2.3.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 2.0.0.rc.1
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
+ RUBY
47
+ end
48
+
49
+ # Reset the configuration options to the defaults.
50
+ #
51
+ # @example Reset the configuration options.
52
+ # config.reset
53
+ #
54
+ # @return [ Hash ] The defaults.
55
+ #
56
+ # @since 2.3.0
57
+ def reset
58
+ settings.replace(defaults)
59
+ end
60
+
61
+ # Get the settings or initialize a new empty hash.
62
+ #
63
+ # @example Get the settings.
64
+ # options.settings
65
+ #
66
+ # @return [ Hash ] The setting options.
67
+ #
68
+ # @since 2.3.0
69
+ def settings
70
+ @settings ||= {}
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ # This is the base module for all domain objects that need to be persisted to
5
+ # the database as documents.
6
+ module Document
7
+ extend ActiveSupport::Concern
8
+ include Dynamoid::Components
9
+
10
+ attr_accessor :new_record
11
+
12
+ def initialize(attrs = {})
13
+ @new_record = true
14
+ @attributes ||= {}
15
+ self.class.attributes.each {|att| write_attribute(att, attrs[att])}
16
+ end
17
+
18
+ def ==(other)
19
+ other.respond_to?(:id) && other.id == self.id
20
+ end
21
+
22
+ module ClassMethods
23
+ def create(attrs = {})
24
+ obj = self.new(attrs)
25
+ obj.save && obj.new_record = false
26
+ obj
27
+ end
28
+
29
+ def build(attrs = {})
30
+ self.new(attrs)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,8 @@
1
+ # encoding: utf-8
2
+ module Dynamoid
3
+ module Errors
4
+
5
+ class InvalidField < Exception; end
6
+
7
+ end
8
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ module Fields
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :fields
9
+
10
+ self.fields = []
11
+ field :id
12
+ end
13
+
14
+ module ClassMethods
15
+ def field(name, options = {})
16
+ named = name.to_s
17
+ self.fields << name
18
+ define_method(named) do
19
+ read_attribute(named)
20
+ end
21
+ define_method("#{named}=") do |value|
22
+ write_attribute(named, value)
23
+ end
24
+ define_method("#{named}?") do
25
+ !read_attribute(named).nil?
26
+ end
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ # This module defines the finder methods that hang off the document at the
5
+ # class level.
6
+ module Finders
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ def find(*id)
11
+ id = Array(id.flatten.uniq)
12
+ if id.count == 1
13
+ self.find_by_id(id.first)
14
+ else
15
+ items = Dynamoid::Adapter.batch_get_item(self.table_name => id)
16
+ items[self.table_name].collect{|i| o = self.build(i); o.new_record = false; o}
17
+ end
18
+ end
19
+
20
+ def find_by_id(id)
21
+ if item = Dynamoid::Adapter.get_item(self.table_name, id)
22
+ obj = self.new(Dynamoid::Adapter.get_item(self.table_name, id))
23
+ obj.new_record = false
24
+ return obj
25
+ else
26
+ return nil
27
+ end
28
+ end
29
+
30
+ def method_missing(method, *args)
31
+ if method =~ /find/
32
+ finder = method.to_s.split('_by_').first
33
+ attributes = method.to_s.split('_by_').last.split('_and_')
34
+
35
+ results = if index = self.indexes.find {|i| i == attributes.sort.collect(&:to_sym)}
36
+ ids = Dynamoid::Adapter.get_item(self.index_table_name(index), self.key_for_index(index, args))
37
+ if ids.nil? || ids.empty?
38
+ []
39
+ else
40
+ self.find(ids[:ids].to_a)
41
+ end
42
+ else
43
+ if Dynamoid::Config.warn_on_scan
44
+ puts 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
45
+ puts "You can index this query by adding this to #{self.to_s.downcase}.rb: index [#{attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
46
+ end
47
+ scan_hash = {}
48
+ attributes.each_with_index {|attr, index| scan_hash[attr.to_sym] = args[index]}
49
+ Dynamoid::Adapter.scan(self.table_name, scan_hash).collect {|hash| self.new(hash)}
50
+ end
51
+
52
+ if finder =~ /all/
53
+ return Array(results)
54
+ else
55
+ return results.first
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ end
@@ -0,0 +1,59 @@
1
+ require 'digest/sha2'
2
+
3
+ # encoding: utf-8
4
+ module Dynamoid #:nodoc:
5
+
6
+ # Builds all indexes present on the model.
7
+ module Indexes
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :indexes
12
+
13
+ self.indexes = []
14
+ end
15
+
16
+ module ClassMethods
17
+ def index(name, options = {})
18
+ name = Array(name).collect(&:to_s).sort.collect(&:to_sym)
19
+ raise Dynamoid::Errors::InvalidField, 'A key specified for an index is not a field' unless name.all?{|n| self.fields.include?(n)}
20
+ self.indexes << name
21
+ create_indexes
22
+ end
23
+
24
+ def create_indexes
25
+ self.indexes.each do |index|
26
+ self.create_table(index_table_name(index), index_key_name(index)) unless self.table_exists?(index_table_name(index))
27
+ end
28
+ end
29
+
30
+ def index_table_name(index)
31
+ "#{Dynamoid::Config.namespace}_index_#{index_key_name(index)}"
32
+ end
33
+
34
+ def index_key_name(index)
35
+ "#{self.to_s.downcase}_#{index.collect(&:to_s).collect(&:pluralize).join('_and_')}"
36
+ end
37
+
38
+ def key_for_index(index, values = [])
39
+ values = values.collect(&:to_s).sort
40
+ Digest::SHA2.new.tap do |sha|
41
+ index.each_with_index {|i, index| sha << values[index] if values[index]}
42
+ end.to_s
43
+ end
44
+ end
45
+
46
+ def key_for_index(index)
47
+ self.class.key_for_index(index, index.collect{|i| self.send(i)})
48
+ end
49
+
50
+ def save_indexes
51
+ self.class.indexes.each do |index|
52
+ existing = Dynamoid::Adapter.get_item(self.class.index_table_name(index), self.key_for_index(index))
53
+ ids = existing ? existing[:ids] : Set.new
54
+ Dynamoid::Adapter.put_item(self.class.index_table_name(index), {self.class.index_key_name(index).to_sym => self.key_for_index(index), :ids => ids.merge([self.id])})
55
+ end
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,35 @@
1
+ require 'securerandom'
2
+
3
+ # encoding: utf-8
4
+ module Dynamoid #:nodoc:
5
+
6
+ # This module saves things!
7
+ module Persistence
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ self.create_table(self.table_name) unless self.table_exists?(self.table_name)
12
+ end
13
+
14
+ def save
15
+ self.id = SecureRandom.uuid if self.id.nil? || self.id.blank?
16
+ Dynamoid::Adapter.put_item(self.class.table_name, self.attributes)
17
+ save_indexes
18
+ end
19
+
20
+ module ClassMethods
21
+ def table_name
22
+ "#{Dynamoid::Config.namespace}_#{self.to_s.downcase.pluralize}"
23
+ end
24
+
25
+ def create_table(table_name, id = :id)
26
+ Dynamoid::Adapter.create_table(table_name, id.to_sym)
27
+ end
28
+
29
+ def table_exists?(table_name)
30
+ Dynamoid::Adapter.list_tables.include?(table_name)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,21 @@
1
+ require 'digest/sha2'
2
+
3
+ # encoding: utf-8
4
+ module Dynamoid #:nodoc:
5
+
6
+ # Associate a document with another object: belongs_to, has_many, and has_and_belongs_to_many
7
+ module Relations
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class_attribute :indexes
12
+
13
+ self.indexes = []
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,5 @@
1
+ class Address
2
+ include Dynamoid::Document
3
+
4
+ field :city
5
+ end
@@ -0,0 +1,6 @@
1
+ class Magazine
2
+ include Dynamoid::Document
3
+
4
+ has_many :subscriptions
5
+ has_one :sponsor
6
+ end
@@ -0,0 +1,6 @@
1
+ class Sponsor
2
+ include Dynamoid::Document
3
+
4
+ belongs_to :magazine
5
+ has_many :subscriptions
6
+ end
@@ -0,0 +1,6 @@
1
+ class Subscription
2
+ include Dynamoid::Document
3
+
4
+ belongs_to :magazine
5
+ has_and_belongs_to_many :users
6
+ end
@@ -0,0 +1,13 @@
1
+ class User
2
+ include Dynamoid::Document
3
+
4
+ field :name
5
+ field :email
6
+ field :password
7
+
8
+ index :name
9
+ index :email
10
+ index [:name, :email]
11
+
12
+ has_and_belongs_to_many :subscriptions
13
+ end
@@ -0,0 +1,123 @@
1
+ require 'dynamoid/adapter/aws_sdk'
2
+ require File.expand_path(File.dirname(__FILE__) + '../../../spec_helper')
3
+
4
+ describe Dynamoid::Adapter::AwsSdk do
5
+
6
+ if ENV['ACCESS_KEY'] && ENV['SECRET_KEY']
7
+
8
+ context 'without a preexisting table' do
9
+ # CreateTable and DeleteTable
10
+ it 'performs CreateTable and DeleteTable' do
11
+ table = Dynamoid::Adapter.create_table('CreateTable', :id)
12
+
13
+ Dynamoid::Adapter.connection.tables.collect{|t| t.name}.should include 'CreateTable'
14
+
15
+ Dynamoid::Adapter.delete_table('CreateTable')
16
+ end
17
+ end
18
+
19
+ context 'with a preexisting table' do
20
+ before(:all) do
21
+ Dynamoid::Adapter.create_table('dynamoid_tests_TestTable1', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable1')
22
+ Dynamoid::Adapter.create_table('dynamoid_tests_TestTable2', :id) unless Dynamoid::Adapter.list_tables.include?('dynamoid_tests_TestTable2')
23
+ end
24
+
25
+ # GetItem, PutItem and DeleteItem
26
+ it "performs GetItem for an item that does not exist" do
27
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
28
+ end
29
+
30
+ it "performs GetItem for an item that does exist" do
31
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
32
+
33
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:name => 'Josh', :id => '1'}
34
+
35
+ Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
36
+
37
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
38
+ end
39
+
40
+ it 'performs DeleteItem for an item that does not exist' do
41
+ Dynamoid::Adapter.delete_item('dynamoid_tests_TestTable1', '1')
42
+
43
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should be_nil
44
+ end
45
+
46
+ it 'performs PutItem for an item that does not exist' do
47
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
48
+
49
+ Dynamoid::Adapter.get_item('dynamoid_tests_TestTable1', '1').should == {:id => '1', :name => 'Josh'}
50
+ end
51
+
52
+ # BatchGetItem
53
+ it "performs BatchGetItem with singular keys" do
54
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
55
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable2', {:id => '1', :name => 'Justin'})
56
+
57
+ results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => '1', 'dynamoid_tests_TestTable2' => '1')
58
+ results.size.should == 2
59
+ results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
60
+ results['dynamoid_tests_TestTable2'].should include({:name => 'Justin', :id => '1'})
61
+ end
62
+
63
+ it "performs BatchGetItem with multiple keys" do
64
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
65
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
66
+
67
+ results = Dynamoid::Adapter.batch_get_item('dynamoid_tests_TestTable1' => ['1', '2'])
68
+ results.size.should == 1
69
+ results['dynamoid_tests_TestTable1'].should include({:name => 'Josh', :id => '1'})
70
+ results['dynamoid_tests_TestTable1'].should include({:name => 'Justin', :id => '2'})
71
+ end
72
+
73
+ # ListTables
74
+ it 'performs ListTables' do
75
+ Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable1'
76
+ Dynamoid::Adapter.list_tables.should include 'dynamoid_tests_TestTable2'
77
+ end
78
+
79
+ # Query
80
+ it 'performs query on a table and returns items' do
81
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
82
+
83
+ Dynamoid::Adapter.query('dynamoid_tests_TestTable1', '1').should == { :id=> '1', :name=>"Josh" }
84
+ end
85
+
86
+ it 'performs query on a table and returns items if there are multiple items' do
87
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
88
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
89
+
90
+ Dynamoid::Adapter.query('dynamoid_tests_TestTable1', '1').should == { :id=> '1', :name=>"Josh" }
91
+ end
92
+
93
+ # Scan
94
+ it 'performs scan on a table and returns items' do
95
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
96
+
97
+ Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
98
+ end
99
+
100
+ it 'performs scan on a table and returns items if there are multiple items but only one match' do
101
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
102
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Justin'})
103
+
104
+ Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{ :id=> '1', :name=>"Josh" }]
105
+ end
106
+
107
+ it 'performs scan on a table and returns multiple items if there are multiple matches' do
108
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '1', :name => 'Josh'})
109
+ Dynamoid::Adapter.put_item('dynamoid_tests_TestTable1', {:id => '2', :name => 'Josh'})
110
+
111
+ Dynamoid::Adapter.scan('dynamoid_tests_TestTable1', :name => 'Josh').should == [{:name=>"Josh", :id=>"2"}, {:name=>"Josh", :id=>"1"}]
112
+ end
113
+
114
+ end
115
+
116
+ # DescribeTable
117
+
118
+ # UpdateItem
119
+
120
+ # UpdateTable
121
+
122
+ end
123
+ end