Dynamoid 0.0.1

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.
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc:
3
+
4
+ module Attributes
5
+ extend ActiveSupport::Concern
6
+
7
+ attr_accessor :attributes
8
+ alias :raw_attributes :attributes
9
+
10
+ def write_attribute(name, value)
11
+ attributes[name.to_sym] = value
12
+ end
13
+ alias :[]= :write_attribute
14
+
15
+ def read_attribute(name)
16
+ attributes[name.to_sym]
17
+ end
18
+ alias :[] :read_attribute
19
+
20
+ module ClassMethods
21
+ def attributes
22
+ [self.fields + [:id]].flatten.uniq
23
+ end
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ module Dynamoid #:nodoc
3
+ module Components #:nodoc
4
+ extend ActiveSupport::Concern
5
+
6
+ # All modules that a +Document+ is composed of are defined in this
7
+ # module, to keep the document class from getting too cluttered.
8
+ included do
9
+ extend ActiveModel::Translation
10
+ end
11
+
12
+ include ActiveModel::Conversion
13
+ include ActiveModel::MassAssignmentSecurity
14
+ include ActiveModel::Naming
15
+ include ActiveModel::Observing
16
+ include ActiveModel::Serializers::JSON
17
+ include ActiveModel::Serializers::Xml
18
+ include Dynamoid::Attributes
19
+ include Dynamoid::Fields
20
+ include Dynamoid::Indexes
21
+ include Dynamoid::Persistence
22
+ include Dynamoid::Finders
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ require "uri"
3
+ require "dynamoid/config/options"
4
+
5
+ module Dynamoid #:nodoc
6
+
7
+ module Config
8
+ extend self
9
+ extend Options
10
+ include ActiveModel::Observing
11
+
12
+ option :adapter, :default => 'local'
13
+ option :namespace, :default => 'dynamoid'
14
+ option :access_key
15
+ option :secret_key
16
+ option :warn_on_scan, :default => true
17
+
18
+ end
19
+ end
@@ -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,58 @@
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
+ obj = self.new(Dynamoid::Adapter.get_item(self.table_name, id))
22
+ obj.new_record = false
23
+ obj
24
+ end
25
+
26
+ def method_missing(method, *args)
27
+ if method =~ /find/
28
+ finder = method.to_s.split('_by_').first
29
+ attributes = method.to_s.split('_by_').last.split('_and_')
30
+
31
+ results = if index = self.indexes.find {|i| i == attributes.sort.collect(&:to_sym)}
32
+ ids = Dynamoid::Adapter.get_item(self.index_table_name(index), self.key_for_index(index, args))
33
+ if ids.nil? || ids.empty?
34
+ []
35
+ else
36
+ self.find(ids[:ids].to_a)
37
+ end
38
+ else
39
+ if Dynamoid::Config.warn_on_scan
40
+ puts 'Queries without an index are forced to use scan and are generally much slower than indexed queries!'
41
+ puts "You can index this query by adding this to #{self.to_s.downcase}.rb: index [#{attributes.sort.collect{|attr| ":#{attr}"}.join(', ')}]"
42
+ end
43
+ scan_hash = {}
44
+ attributes.each_with_index {|attr, index| scan_hash[attr.to_sym] = args[index]}
45
+ Dynamoid::Adapter.scan(self.table_name, scan_hash).collect {|hash| self.new(hash)}
46
+ end
47
+
48
+ if finder =~ /all/
49
+ return Array(results)
50
+ else
51
+ return results.first
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ 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,11 @@
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
+ 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