dynamoid 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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