aws-sdk 1.2.6 → 1.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.
- data/lib/aws.rb +2 -0
- data/lib/aws/api_config/DynamoDB-2011-12-05.yml +721 -0
- data/lib/aws/core.rb +10 -1
- data/lib/aws/core/client.rb +17 -12
- data/lib/aws/core/configuration.rb +13 -3
- data/lib/aws/core/configured_json_client_methods.rb +71 -0
- data/lib/aws/core/lazy_error_classes.rb +7 -2
- data/lib/aws/core/option_grammar.rb +67 -13
- data/lib/aws/core/resource.rb +9 -1
- data/lib/aws/core/session_signer.rb +95 -0
- data/lib/aws/dynamo_db.rb +169 -0
- data/lib/aws/dynamo_db/attribute_collection.rb +460 -0
- data/lib/aws/dynamo_db/batch_get.rb +206 -0
- data/lib/aws/dynamo_db/client.rb +119 -0
- data/lib/aws/dynamo_db/config.rb +20 -0
- data/lib/aws/dynamo_db/errors.rb +57 -0
- data/lib/aws/dynamo_db/expectations.rb +40 -0
- data/lib/aws/dynamo_db/item.rb +130 -0
- data/lib/aws/dynamo_db/item_collection.rb +837 -0
- data/lib/aws/{record/optimistic_locking.rb → dynamo_db/item_data.rb} +9 -12
- data/lib/aws/{record/attributes/boolean.rb → dynamo_db/keys.rb} +15 -23
- data/lib/aws/dynamo_db/primary_key_element.rb +47 -0
- data/lib/aws/dynamo_db/request.rb +78 -0
- data/lib/aws/{record/attributes/float.rb → dynamo_db/resource.rb} +10 -25
- data/lib/aws/dynamo_db/table.rb +418 -0
- data/lib/aws/dynamo_db/table_collection.rb +165 -0
- data/lib/aws/dynamo_db/types.rb +86 -0
- data/lib/aws/ec2/resource_tag_collection.rb +3 -1
- data/lib/aws/record.rb +36 -8
- data/lib/aws/record/abstract_base.rb +642 -0
- data/lib/aws/record/attributes.rb +384 -0
- data/lib/aws/record/dirty_tracking.rb +0 -1
- data/lib/aws/record/errors.rb +0 -8
- data/lib/aws/record/hash_model.rb +163 -0
- data/lib/aws/record/hash_model/attributes.rb +182 -0
- data/lib/aws/record/hash_model/finder_methods.rb +178 -0
- data/lib/aws/record/hash_model/scope.rb +108 -0
- data/lib/aws/record/model.rb +429 -0
- data/lib/aws/record/model/attributes.rb +377 -0
- data/lib/aws/record/model/finder_methods.rb +232 -0
- data/lib/aws/record/model/scope.rb +213 -0
- data/lib/aws/record/scope.rb +43 -169
- data/lib/aws/record/validations.rb +11 -11
- data/lib/aws/s3/client.rb +9 -6
- data/lib/aws/s3/object_collection.rb +1 -1
- data/lib/aws/simple_db/expect_condition_option.rb +1 -1
- data/lib/aws/simple_db/item_collection.rb +5 -3
- data/lib/aws/sts/client.rb +9 -0
- metadata +73 -30
- data/lib/aws/record/attribute.rb +0 -94
- data/lib/aws/record/attribute_macros.rb +0 -312
- data/lib/aws/record/attributes/date.rb +0 -89
- data/lib/aws/record/attributes/datetime.rb +0 -86
- data/lib/aws/record/attributes/integer.rb +0 -68
- data/lib/aws/record/attributes/sortable_float.rb +0 -60
- data/lib/aws/record/attributes/sortable_integer.rb +0 -95
- data/lib/aws/record/attributes/string.rb +0 -69
- data/lib/aws/record/base.rb +0 -828
- data/lib/aws/record/finder_methods.rb +0 -230
- data/lib/aws/record/scopes.rb +0 -55
@@ -0,0 +1,165 @@
|
|
1
|
+
# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
module AWS
|
15
|
+
class DynamoDB
|
16
|
+
|
17
|
+
# Represents the tables in your account. Each table is
|
18
|
+
# represented by an instance of the {Table} class.
|
19
|
+
#
|
20
|
+
# == Schemas
|
21
|
+
#
|
22
|
+
# Before you can operate on items in a table you must specify the schema.
|
23
|
+
# You do this by calling #hash_key= (and optionally #range_key=) on
|
24
|
+
# a table.
|
25
|
+
#
|
26
|
+
# table = dynamo_db.tables['mytable']
|
27
|
+
# table.hash_key = [:id, :string]
|
28
|
+
#
|
29
|
+
# @example Creating a Table
|
30
|
+
# table = dynamo_db.tables.create('mytable', 10, 10, :hash_key => { :id => :string })
|
31
|
+
#
|
32
|
+
# @example Enumerating Tables
|
33
|
+
# dynamo_db.tables.each {|table| puts table.name }
|
34
|
+
#
|
35
|
+
# @example Getting a Table by Name
|
36
|
+
# table = dynamo_db.tables['mytable']
|
37
|
+
#
|
38
|
+
class TableCollection
|
39
|
+
|
40
|
+
include Core::Collection::Limitable
|
41
|
+
|
42
|
+
# Creates a new table.
|
43
|
+
#
|
44
|
+
# table = dynamo_db.tables.create('mytable', 25, 25,
|
45
|
+
# :hash_key => { :id => :string })
|
46
|
+
#
|
47
|
+
# @note Creating a table is an eventualy consistent operation. You
|
48
|
+
# can not interact with the table until its status
|
49
|
+
# ({Table#status}) is +:active+.
|
50
|
+
#
|
51
|
+
# @param [String] name The name of the table.
|
52
|
+
#
|
53
|
+
# @param [Integer] read_capacity_units Sets the minimum
|
54
|
+
# number of reads supported before read requests are throttled.
|
55
|
+
#
|
56
|
+
# @param [Integer] write_capacity_units Sets the minimum
|
57
|
+
# number of writes supported before writes requests are throttled.
|
58
|
+
#
|
59
|
+
# @param [Hash] options
|
60
|
+
#
|
61
|
+
# @option options [Hash] :hash_key A hash key is a combination
|
62
|
+
# of an attribute name and type. If you want to have the
|
63
|
+
# hash key on the string attribute username you would call #create
|
64
|
+
# with:
|
65
|
+
#
|
66
|
+
# :hash_key => { :username => :string }
|
67
|
+
#
|
68
|
+
# The other supported type is +:number+. If you wanted to
|
69
|
+
# set the hash key on a numeric (integer) attribute then you
|
70
|
+
# could call #create with:
|
71
|
+
#
|
72
|
+
# :hash_key => { :id => :number }
|
73
|
+
#
|
74
|
+
# All tables require a hash key. If +:hash_key+ is not provided
|
75
|
+
# then a default hash key will be provided. The default hash
|
76
|
+
# key is:
|
77
|
+
#
|
78
|
+
# :hash_key => { :id => :string }
|
79
|
+
#
|
80
|
+
# @option options [String] :range_key You can setup a table to use
|
81
|
+
# composite keys by providing a +:range_key+. Range keys are
|
82
|
+
# configured the same way as hash keys. They are useful
|
83
|
+
# for ordering items that share the same hash key.
|
84
|
+
#
|
85
|
+
# @return [Table] The newly created table.
|
86
|
+
#
|
87
|
+
def create name, read_capacity_units, write_capacity_units, options = {}
|
88
|
+
|
89
|
+
client_opts = {
|
90
|
+
:table_name => name.to_s,
|
91
|
+
:key_schema => key_schema(options),
|
92
|
+
:provisioned_throughput => {
|
93
|
+
:read_capacity_units => read_capacity_units,
|
94
|
+
:write_capacity_units => write_capacity_units,
|
95
|
+
},
|
96
|
+
}
|
97
|
+
|
98
|
+
response = client.create_table(client_opts)
|
99
|
+
|
100
|
+
Table.new(name, :config => config)
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
# References a table by name.
|
105
|
+
#
|
106
|
+
# dynamo_db.tables["MyTable"]
|
107
|
+
#
|
108
|
+
# @param [String] name
|
109
|
+
# @return [Table] Returns the table with the given name.
|
110
|
+
def [] name
|
111
|
+
Table.new(name, :config => config)
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
protected
|
116
|
+
def _each_item next_token, limit, options = {}, &block
|
117
|
+
|
118
|
+
options[:limit] = limit if limit
|
119
|
+
options[:exclusive_start_table_name] = next_token if next_token
|
120
|
+
|
121
|
+
response = client.list_tables(options)
|
122
|
+
response.data['TableNames'].each do |name|
|
123
|
+
yield Table.new(name, :config => config)
|
124
|
+
end
|
125
|
+
|
126
|
+
response.data['LastEvaluatedTableName']
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
def key_schema options
|
132
|
+
|
133
|
+
hash_key, range_key = options.values_at(:hash_key, :range_key)
|
134
|
+
|
135
|
+
# default options for :hash_key
|
136
|
+
hash_key ||= { :id => :string }
|
137
|
+
|
138
|
+
schema = {}
|
139
|
+
schema[:hash_key_element] = schema_element(hash_key, "hash")
|
140
|
+
if range_key
|
141
|
+
schema[:range_key_element] = schema_element(range_key, "range")
|
142
|
+
end
|
143
|
+
schema
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def schema_element desc, key_type
|
149
|
+
|
150
|
+
(name, type) = desc.to_a.first
|
151
|
+
|
152
|
+
unless type == :string or type == :number
|
153
|
+
msg = "invalid #{key_type} key type, expected :string or :number"
|
154
|
+
raise ArgumentError, msg
|
155
|
+
end
|
156
|
+
|
157
|
+
{ :attribute_name => name.to_s,
|
158
|
+
:attribute_type => type.to_s[0,1].upcase }
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
require 'bigdecimal'
|
15
|
+
require 'set'
|
16
|
+
|
17
|
+
module AWS
|
18
|
+
class DynamoDB
|
19
|
+
|
20
|
+
# @private
|
21
|
+
module Types
|
22
|
+
|
23
|
+
def value_from_response(hash)
|
24
|
+
(type, value) = hash.to_a.first
|
25
|
+
case type
|
26
|
+
when "S", :s
|
27
|
+
value
|
28
|
+
when "SS", :ss
|
29
|
+
Set[*value]
|
30
|
+
when "N", :n
|
31
|
+
BigDecimal(value.to_s)
|
32
|
+
when "NS", :ns
|
33
|
+
Set[*value.map { |v| BigDecimal(v.to_s) }]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def values_from_response_hash(hash)
|
38
|
+
hash.inject({}) do |h, (key, value_hash)|
|
39
|
+
h.update(key => value_from_response(value_hash))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def format_attribute_value(value, context = nil)
|
44
|
+
indicator = type_indicator(value, context)
|
45
|
+
|
46
|
+
value = [] if value == :empty_number_set
|
47
|
+
value = value.to_s if indicator == :n
|
48
|
+
value = value.map(&:to_s) if indicator == :ns
|
49
|
+
|
50
|
+
Hash[[[indicator, value]]]
|
51
|
+
end
|
52
|
+
|
53
|
+
protected
|
54
|
+
def type_indicator(value, context)
|
55
|
+
case
|
56
|
+
when value.respond_to?(:to_str) then :s
|
57
|
+
when value.kind_of?(Numeric) then :n
|
58
|
+
when value.respond_to?(:each)
|
59
|
+
indicator = nil
|
60
|
+
value.each do |v|
|
61
|
+
member_indicator = type_indicator(v, context)
|
62
|
+
raise_error("nested collections", context) if
|
63
|
+
member_indicator.to_s.size > 1
|
64
|
+
raise_error("mixed types", context) if
|
65
|
+
indicator and member_indicator != indicator
|
66
|
+
indicator = member_indicator
|
67
|
+
end
|
68
|
+
indicator ||= :s
|
69
|
+
:"#{indicator}s"
|
70
|
+
when value == :empty_number_set
|
71
|
+
:ns
|
72
|
+
else
|
73
|
+
raise_error("unsupported attribute type #{value.class}", context)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
protected
|
78
|
+
def raise_error(msg, context)
|
79
|
+
msg = "#{msg} in #{context}" if context
|
80
|
+
raise ArgumentError, msg
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
data/lib/aws/record.rb
CHANGED
@@ -14,12 +14,16 @@
|
|
14
14
|
require 'set'
|
15
15
|
|
16
16
|
module AWS
|
17
|
+
|
18
|
+
# AWS::Record is an ORM built on top of AWS services.
|
17
19
|
module Record
|
18
20
|
|
19
21
|
AWS.register_autoloads(self) do
|
20
|
-
autoload :Base,
|
22
|
+
autoload :Base, 'model'
|
23
|
+
autoload :Model, 'model'
|
24
|
+
autoload :HashModel, 'hash_model'
|
21
25
|
end
|
22
|
-
|
26
|
+
|
23
27
|
# @private
|
24
28
|
class RecordNotFound < StandardError; end
|
25
29
|
|
@@ -37,14 +41,38 @@ module AWS
|
|
37
41
|
# @param [String] A prefix to append to all domains. This is useful for
|
38
42
|
# grouping domains used by one application with a single prefix.
|
39
43
|
def self.domain_prefix= prefix
|
40
|
-
@
|
44
|
+
@domain_prefix = prefix
|
41
45
|
end
|
42
|
-
|
46
|
+
|
43
47
|
# @return [String,nil] The string that is prepended to all domain names.
|
44
48
|
def self.domain_prefix
|
45
|
-
@
|
49
|
+
@domain_prefix
|
46
50
|
end
|
47
|
-
|
51
|
+
|
52
|
+
# Sets a prefix to be applied to all DynamoDB tables associated
|
53
|
+
# with {AWS::Record::HashModel} and {AWS::Record::ListModel}
|
54
|
+
# classes.
|
55
|
+
#
|
56
|
+
# AWS::Record.table_prefix = 'production_'
|
57
|
+
#
|
58
|
+
# class Product < AWS::Record::HashModel
|
59
|
+
# set_table_name 'products'
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# Product.table_name #=> 'production_products'
|
63
|
+
#
|
64
|
+
# @param [String] A prefix to append to all tables. This is
|
65
|
+
# useful for grouping tables used by one application with a
|
66
|
+
# single prefix.
|
67
|
+
def self.table_prefix= prefix
|
68
|
+
@table_prefix = prefix
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [String,nil] The string that is prepended to all table names.
|
72
|
+
def self.table_prefix
|
73
|
+
@table_prefix
|
74
|
+
end
|
75
|
+
|
48
76
|
# A utility method for casting values into an array.
|
49
77
|
#
|
50
78
|
# * nil is returned as an empty array, []
|
@@ -62,7 +90,7 @@ module AWS
|
|
62
90
|
else [value]
|
63
91
|
end
|
64
92
|
end
|
65
|
-
|
93
|
+
|
66
94
|
# A utility method for casting values into
|
67
95
|
#
|
68
96
|
# * Sets are returned unmodified
|
@@ -77,6 +105,6 @@ module AWS
|
|
77
105
|
else Set.new(as_array(value))
|
78
106
|
end
|
79
107
|
end
|
80
|
-
|
108
|
+
|
81
109
|
end
|
82
110
|
end
|
@@ -0,0 +1,642 @@
|
|
1
|
+
# Copyright 2011-2012 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
# http://aws.amazon.com/apache2.0/
|
8
|
+
#
|
9
|
+
# or in the "license" file accompanying this file. This file is
|
10
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
11
|
+
# ANY KIND, either express or implied. See the License for the specific
|
12
|
+
# language governing permissions and limitations under the License.
|
13
|
+
|
14
|
+
require 'uuidtools'
|
15
|
+
require 'set'
|
16
|
+
|
17
|
+
require 'aws/record/scope'
|
18
|
+
require 'aws/record/naming'
|
19
|
+
require 'aws/record/validations'
|
20
|
+
require 'aws/record/dirty_tracking'
|
21
|
+
require 'aws/record/conversion'
|
22
|
+
require 'aws/record/errors'
|
23
|
+
require 'aws/record/exceptions'
|
24
|
+
|
25
|
+
module AWS
|
26
|
+
module Record
|
27
|
+
module AbstractBase
|
28
|
+
|
29
|
+
def self.extended base
|
30
|
+
|
31
|
+
base.send(:extend, ClassMethods)
|
32
|
+
base.send(:include, InstanceMethods)
|
33
|
+
base.send(:include, DirtyTracking)
|
34
|
+
base.send(:extend, Validations)
|
35
|
+
|
36
|
+
# these 3 modules are for rails 3+ active model compatability
|
37
|
+
base.send(:extend, Naming)
|
38
|
+
base.send(:include, Naming)
|
39
|
+
base.send(:include, Conversion)
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
module InstanceMethods
|
44
|
+
|
45
|
+
# Constructs a new record.
|
46
|
+
#
|
47
|
+
# @param [Hash] attributes A set of attribute values to seed this record
|
48
|
+
# with. The attributes are bulk assigned.
|
49
|
+
#
|
50
|
+
# @param [Hash] attributes Attributes that should be bulk assigned
|
51
|
+
# to this record. You can also specify the shard (i.e. domain
|
52
|
+
# or table) this record should persist to via +:shard+).
|
53
|
+
#
|
54
|
+
# @option attributes [String] :shard The domain/table this record
|
55
|
+
# should persist to. If this is omitted, it will persist to the
|
56
|
+
# class default shard (which defaults to the class name).
|
57
|
+
#
|
58
|
+
# @return [Model,HashModel] Returns a new (non-persisted) record.
|
59
|
+
# Call {#save} to persist changes to AWS.
|
60
|
+
#
|
61
|
+
def initialize attributes = {}
|
62
|
+
|
63
|
+
attributes = attributes.dup
|
64
|
+
|
65
|
+
# supporting :domain for backwards compatability, :shard is prefered
|
66
|
+
@_shard = attributes.delete(:domain)
|
67
|
+
@_shard ||= attributes.delete('domain')
|
68
|
+
@_shard ||= attributes.delete(:shard)
|
69
|
+
@_shard ||= attributes.delete('shard')
|
70
|
+
@_shard = self.class.shard_name(@_shard)
|
71
|
+
|
72
|
+
@_data = {}
|
73
|
+
assign_default_values
|
74
|
+
bulk_assign(attributes)
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [String] Returns the name of the shard this record
|
79
|
+
# is persisted to or will be persisted to. Defaults to the
|
80
|
+
# domain/table named after this record class.
|
81
|
+
def shard
|
82
|
+
@_shard
|
83
|
+
end
|
84
|
+
alias_method :domain, :shard # for backwards compatability
|
85
|
+
|
86
|
+
# The id for each record is auto-generated. The default strategy
|
87
|
+
# generates uuid strings.
|
88
|
+
# @return [String] Returns the id string (uuid) for this record. Retuns
|
89
|
+
# nil if this is a new record that has not been persisted yet.
|
90
|
+
def id
|
91
|
+
@_id
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [Hash] A hash with attribute names as hash keys (strings) and
|
95
|
+
# attribute values (of mixed types) as hash values.
|
96
|
+
def attributes
|
97
|
+
attributes = Core::IndifferentHash.new
|
98
|
+
attributes['id'] = id if persisted?
|
99
|
+
self.class.attributes.keys.inject(attributes) do |hash,attr_name|
|
100
|
+
hash.merge(attr_name => __send__(attr_name))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Acts like {#update} but does not call {#save}.
|
105
|
+
#
|
106
|
+
# record.attributes = { :name => 'abc', :age => 20 }
|
107
|
+
#
|
108
|
+
# @param [Hash] attributes A hash of attributes to set on this record
|
109
|
+
# without calling save.
|
110
|
+
#
|
111
|
+
# @return [Hash] Returns the attribute hash that was passed in.
|
112
|
+
#
|
113
|
+
def attributes= attributes
|
114
|
+
bulk_assign(attributes)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Persistence indicates if the record has been saved previously or not.
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# @recipe = Recipe.new(:name => 'Buttermilk Pancackes')
|
121
|
+
# @recipe.persisted? #=> false
|
122
|
+
# @recipe.save!
|
123
|
+
# @recipe.persisted? #=> true
|
124
|
+
#
|
125
|
+
# @return [Boolean] Returns true if this record has been persisted.
|
126
|
+
def persisted?
|
127
|
+
!!@_persisted
|
128
|
+
end
|
129
|
+
|
130
|
+
# @return [Boolean] Returns true if this record has not been persisted
|
131
|
+
# to SimpleDB.
|
132
|
+
def new_record?
|
133
|
+
!persisted?
|
134
|
+
end
|
135
|
+
|
136
|
+
# @return [Boolean] Returns true if this record has no validation errors.
|
137
|
+
def valid?
|
138
|
+
run_validations
|
139
|
+
errors.empty?
|
140
|
+
end
|
141
|
+
|
142
|
+
def errors
|
143
|
+
@errors ||= Errors.new
|
144
|
+
end
|
145
|
+
|
146
|
+
# Creates new records, updates existing records.
|
147
|
+
# @return [Boolean] Returns true if the record saved without errors,
|
148
|
+
# false otherwise.
|
149
|
+
def save
|
150
|
+
if valid?
|
151
|
+
persisted? ? update : create
|
152
|
+
clear_changes!
|
153
|
+
true
|
154
|
+
else
|
155
|
+
false
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Creates new records, updates exsting records. If there is a validation
|
160
|
+
# error then an exception is raised.
|
161
|
+
# @raise [InvalidRecordError] Raised when the record has validation
|
162
|
+
# errors and can not be saved.
|
163
|
+
# @return [true] Returns true after a successful save.
|
164
|
+
def save!
|
165
|
+
raise InvalidRecordError.new(self) unless save
|
166
|
+
true
|
167
|
+
end
|
168
|
+
|
169
|
+
# Bulk assigns the attributes and then saves the record.
|
170
|
+
# @param [Hash] attribute_hash A hash of attribute names (keys) and
|
171
|
+
# attribute values to assign to this record.
|
172
|
+
# @return (see #save)
|
173
|
+
def update_attributes attribute_hash
|
174
|
+
bulk_assign(attribute_hash)
|
175
|
+
save
|
176
|
+
end
|
177
|
+
|
178
|
+
# Bulk assigns the attributes and then saves the record. Raises
|
179
|
+
# an exception (AWS::Record::InvalidRecordError) if the record is not
|
180
|
+
# valid.
|
181
|
+
# @param (see #update_attributes)
|
182
|
+
# @return [true]
|
183
|
+
def update_attributes! attribute_hash
|
184
|
+
if update_attributes(attribute_hash)
|
185
|
+
true
|
186
|
+
else
|
187
|
+
raise InvalidRecordError.new(self)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Deletes the record.
|
192
|
+
# @return [true]
|
193
|
+
def delete
|
194
|
+
if persisted?
|
195
|
+
if deleted?
|
196
|
+
raise 'unable to delete, this object has already been deleted'
|
197
|
+
else
|
198
|
+
delete_storage
|
199
|
+
@_deleted = true
|
200
|
+
end
|
201
|
+
else
|
202
|
+
raise 'unable to delete, this object has not been saved yet'
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# @return [Boolean] Returns true if this instance object has been deleted.
|
207
|
+
def deleted?
|
208
|
+
persisted? ? !!@_deleted : false
|
209
|
+
end
|
210
|
+
|
211
|
+
# If you define a custom setter, you use #[]= to set the value
|
212
|
+
# on the record.
|
213
|
+
#
|
214
|
+
# class Book < AWS::Record::Model
|
215
|
+
#
|
216
|
+
# string_attr :name
|
217
|
+
#
|
218
|
+
# # replace the default #author= method
|
219
|
+
# def author= name
|
220
|
+
# self['author'] = name.blank? ? 'Anonymous' : name
|
221
|
+
# end
|
222
|
+
#
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
# @param [String,Symbol] The attribute name to set a value for
|
226
|
+
# @param attribute_value The value to assign.
|
227
|
+
protected
|
228
|
+
def []= attribute_name, new_value
|
229
|
+
self.class.attribute_for(attribute_name) do |attribute|
|
230
|
+
|
231
|
+
if_tracking_changes do
|
232
|
+
original_value = type_cast(attribute, attribute_was(attribute.name))
|
233
|
+
incoming_value = type_cast(attribute, new_value)
|
234
|
+
if original_value == incoming_value
|
235
|
+
clear_change!(attribute.name)
|
236
|
+
else
|
237
|
+
attribute_will_change!(attribute.name)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
@_data[attribute.name] = new_value
|
242
|
+
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Returns the typecasted value for the named attribute.
|
247
|
+
#
|
248
|
+
# book = Book.new(:title => 'My Book')
|
249
|
+
# book['title'] #=> 'My Book'
|
250
|
+
# book.title #=> 'My Book'
|
251
|
+
#
|
252
|
+
# === Intended Use
|
253
|
+
#
|
254
|
+
# This method's primary use is for getting/setting the value for
|
255
|
+
# an attribute inside a custom method:
|
256
|
+
#
|
257
|
+
# class Book < AWS::Record::Model
|
258
|
+
#
|
259
|
+
# string_attr :title
|
260
|
+
#
|
261
|
+
# def title
|
262
|
+
# self['title'] ? self['title'].upcase : nil
|
263
|
+
# end
|
264
|
+
#
|
265
|
+
# end
|
266
|
+
#
|
267
|
+
# book = Book.new(:title => 'My Book')
|
268
|
+
# book.title #=> 'MY BOOK'
|
269
|
+
#
|
270
|
+
# @param [String,Symbol] attribute_name The name of the attribute to fetch
|
271
|
+
# a value for.
|
272
|
+
# @return The current type-casted value for the named attribute.
|
273
|
+
protected
|
274
|
+
def [] attribute_name
|
275
|
+
self.class.attribute_for(attribute_name) do |attribute|
|
276
|
+
type_cast(attribute, @_data[attribute.name])
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
protected
|
281
|
+
def create
|
282
|
+
populate_id
|
283
|
+
touch_timestamps('created_at', 'updated_at')
|
284
|
+
increment_optimistic_lock_value
|
285
|
+
create_storage
|
286
|
+
@_persisted = true
|
287
|
+
end
|
288
|
+
|
289
|
+
private
|
290
|
+
def update
|
291
|
+
return unless changed?
|
292
|
+
touch_timestamps('updated_at')
|
293
|
+
increment_optimistic_lock_value
|
294
|
+
update_storage
|
295
|
+
end
|
296
|
+
|
297
|
+
protected
|
298
|
+
def populate_id
|
299
|
+
@_id = UUIDTools::UUID.random_create.to_s
|
300
|
+
end
|
301
|
+
|
302
|
+
protected
|
303
|
+
def touch_timestamps *attributes
|
304
|
+
now = Time.now
|
305
|
+
attributes.each do |attr_name|
|
306
|
+
if
|
307
|
+
self.class.attributes[attr_name] and
|
308
|
+
!attribute_changed?(attr_name)
|
309
|
+
# don't touch timestamps the user modified
|
310
|
+
then
|
311
|
+
__send__("#{attr_name}=", now)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
protected
|
317
|
+
def increment_optimistic_lock_value
|
318
|
+
if_locks_optimistically do |lock_attr|
|
319
|
+
if value = self[lock_attr.name]
|
320
|
+
self[lock_attr.name] = value + 1
|
321
|
+
else
|
322
|
+
self[lock_attr.name] = 1
|
323
|
+
end
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
protected
|
328
|
+
def if_locks_optimistically &block
|
329
|
+
if opt_lock_attr = self.class.optimistic_locking_attr
|
330
|
+
yield(opt_lock_attr)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
protected
|
335
|
+
def opt_lock_conditions
|
336
|
+
conditions = {}
|
337
|
+
if_locks_optimistically do |lock_attr|
|
338
|
+
if was = attribute_was(lock_attr.name)
|
339
|
+
conditions[:if] = { lock_attr.name => lock_attr.serialize(was) }
|
340
|
+
else
|
341
|
+
conditions[:unless_exists] = lock_attr.name
|
342
|
+
end
|
343
|
+
end
|
344
|
+
conditions
|
345
|
+
end
|
346
|
+
|
347
|
+
private
|
348
|
+
def assign_default_values
|
349
|
+
# populate default attribute values
|
350
|
+
ignore_changes do
|
351
|
+
self.class.attributes.values.each do |attribute|
|
352
|
+
begin
|
353
|
+
# copy default values down so methods like #gsub! don't
|
354
|
+
# modify the default values for other objects
|
355
|
+
@_data[attribute.name] = attribute.default_value.clone
|
356
|
+
rescue TypeError
|
357
|
+
@_data[attribute.name] = attribute.default_value
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
private
|
364
|
+
def bulk_assign hash
|
365
|
+
flatten_date_parts(hash).each_pair do |attr_name, attr_value|
|
366
|
+
__send__("#{attr_name}=", attr_value)
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
# Rails date and time select helpers split date and time
|
372
|
+
# attributes into multiple values for form submission.
|
373
|
+
# These attributes get named things like 'created_at(1i)'
|
374
|
+
# and represent year/month/day/hour/min/sec parts of
|
375
|
+
# the date/time.
|
376
|
+
#
|
377
|
+
# This method converts these attributes back into a single
|
378
|
+
# value and converts them to Date and DateTime objects.
|
379
|
+
def flatten_date_parts attributes
|
380
|
+
|
381
|
+
multi_attributes = Set.new
|
382
|
+
|
383
|
+
hash = attributes.inject({}) do |hash,(key,value)|
|
384
|
+
# collects attribuets like "created_at(1i)" into an array of parts
|
385
|
+
if key =~ /\(/
|
386
|
+
key, index = key.to_s.split(/\(|i\)/)
|
387
|
+
hash[key] ||= []
|
388
|
+
hash[key][index.to_i - 1] = value.to_i
|
389
|
+
multi_attributes << key
|
390
|
+
else
|
391
|
+
hash[key] = value
|
392
|
+
end
|
393
|
+
hash
|
394
|
+
end
|
395
|
+
|
396
|
+
# convert multiattribute values into date/time objects
|
397
|
+
multi_attributes.each do |key|
|
398
|
+
|
399
|
+
values = hash[key]
|
400
|
+
|
401
|
+
hash[key] = case values.size
|
402
|
+
when 0 then nil
|
403
|
+
when 2
|
404
|
+
now = Time.now
|
405
|
+
Time.local(now.year, now.month, now.day, values[0], values[1], 0, 0)
|
406
|
+
when 3 then Date.new(*values)
|
407
|
+
else DateTime.new(*values)
|
408
|
+
end
|
409
|
+
|
410
|
+
end
|
411
|
+
|
412
|
+
hash
|
413
|
+
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
def type_cast attribute, raw
|
418
|
+
if attribute.set?
|
419
|
+
values = Record.as_array(raw).inject([]) do |values,value|
|
420
|
+
values << attribute.type_cast(value)
|
421
|
+
values
|
422
|
+
end
|
423
|
+
Set.new(values.compact)
|
424
|
+
else
|
425
|
+
attribute.type_cast(raw)
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
private
|
430
|
+
def serialize_attributes
|
431
|
+
|
432
|
+
hash = {}
|
433
|
+
self.class.attributes.each_pair do |attribute_name,attribute|
|
434
|
+
value = serialize_attribute(attribute, @_data[attribute_name])
|
435
|
+
unless [nil, []].include?(value)
|
436
|
+
hash[attribute_name] = value
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# simple db does not support persisting items without attribute values
|
441
|
+
raise EmptyRecordError.new(self) if hash.empty?
|
442
|
+
|
443
|
+
hash
|
444
|
+
|
445
|
+
end
|
446
|
+
|
447
|
+
private
|
448
|
+
def serialize_attribute attribute, raw_value
|
449
|
+
type_casted_value = type_cast(attribute, raw_value)
|
450
|
+
case type_casted_value
|
451
|
+
when nil then nil
|
452
|
+
when Set then type_casted_value.map{|v| attribute.serialize(v) }
|
453
|
+
else attribute.serialize(type_casted_value)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# @private
|
458
|
+
protected
|
459
|
+
def hydrate id, data
|
460
|
+
|
461
|
+
# @todo need to do something about partial hyrdation of attributes
|
462
|
+
|
463
|
+
@_id = id
|
464
|
+
|
465
|
+
# New objects are populated with default values, but we don't
|
466
|
+
# want these values to hang around when hydrating persisted values
|
467
|
+
# (those values may have been blanked out before save).
|
468
|
+
self.class.attributes.values.each do |attribute|
|
469
|
+
@_data[attribute.name] = nil
|
470
|
+
end
|
471
|
+
|
472
|
+
ignore_changes do
|
473
|
+
bulk_assign(deserialize_item_data(data))
|
474
|
+
end
|
475
|
+
|
476
|
+
@_persisted = true
|
477
|
+
|
478
|
+
end
|
479
|
+
|
480
|
+
protected
|
481
|
+
def create_storage
|
482
|
+
raise NotImplementedError
|
483
|
+
end
|
484
|
+
|
485
|
+
protected
|
486
|
+
def update_storage
|
487
|
+
raise NotImplementedError
|
488
|
+
end
|
489
|
+
|
490
|
+
protected
|
491
|
+
def delete_storage
|
492
|
+
raise NotImplementedError
|
493
|
+
end
|
494
|
+
|
495
|
+
end
|
496
|
+
|
497
|
+
module ClassMethods
|
498
|
+
|
499
|
+
# Allows you to override the default shard name for this class.
|
500
|
+
# The shard name defaults to the class name.
|
501
|
+
# @param [String] name
|
502
|
+
def set_shard_name name
|
503
|
+
@_shard_name = name
|
504
|
+
end
|
505
|
+
alias_method :set_domain_name, :set_shard_name
|
506
|
+
alias_method :shard_name=, :set_shard_name
|
507
|
+
|
508
|
+
# Returns the name of the shard this class will persist records
|
509
|
+
# into by default.
|
510
|
+
#
|
511
|
+
# @param [String] name Defaults to the name of this class.
|
512
|
+
# @return [String] Returns the full prefixed domain name for this class.
|
513
|
+
def shard_name name = nil
|
514
|
+
name = @_shard_name if name.nil?
|
515
|
+
name = self.name if name.nil?
|
516
|
+
name = name.name if name.is_a?(Core::Model) # sdb domain or ddb table
|
517
|
+
name
|
518
|
+
end
|
519
|
+
alias_method :domain_name, :shard_name
|
520
|
+
|
521
|
+
# Adds a scoped finder to this class.
|
522
|
+
#
|
523
|
+
# class Book < AWS::Record::Model
|
524
|
+
# scope :top_10, order(:popularity, :desc).limit(10)
|
525
|
+
# end
|
526
|
+
#
|
527
|
+
# Book.top_10.to_a
|
528
|
+
# #=> [#<Book...>, #<Book...>]
|
529
|
+
#
|
530
|
+
# Book.top_10.first
|
531
|
+
# #=> #<Book...>
|
532
|
+
#
|
533
|
+
# You can also provide a block that accepts params for the scoped
|
534
|
+
# finder. This block should return a scope.
|
535
|
+
#
|
536
|
+
# class Book < AWS::Record::Model
|
537
|
+
# scope :by_author, lambda {|name| where(:author => name) }
|
538
|
+
# end
|
539
|
+
#
|
540
|
+
# # top 10 books by the author 'John Doe'
|
541
|
+
# Book.by_author('John Doe').top_10
|
542
|
+
#
|
543
|
+
# @param [Symbol] name The name of the scope. Scope names should be
|
544
|
+
# method-safe and should not conflict with any other class methods.
|
545
|
+
#
|
546
|
+
# @param [Scope] scope
|
547
|
+
#
|
548
|
+
def scope name, scope = nil, &block
|
549
|
+
|
550
|
+
method_definition = scope ? lambda { scope } : block
|
551
|
+
|
552
|
+
extend(Module.new { define_method(name, &method_definition) })
|
553
|
+
|
554
|
+
end
|
555
|
+
|
556
|
+
# @private
|
557
|
+
def new_scope
|
558
|
+
self::Scope.new(self)
|
559
|
+
end
|
560
|
+
|
561
|
+
def optimistic_locking attribute_name = :version_id
|
562
|
+
attribute = integer_attr(attribute_name)
|
563
|
+
@optimistic_locking_attr = attribute
|
564
|
+
end
|
565
|
+
|
566
|
+
# @return [Boolean] Returns true if this class is configured to
|
567
|
+
# perform optimistic locking.
|
568
|
+
def optimistic_locking?
|
569
|
+
!!@optimistic_locking_attr
|
570
|
+
end
|
571
|
+
|
572
|
+
@private
|
573
|
+
def optimistic_locking_attr
|
574
|
+
@optimistic_locking_attr
|
575
|
+
end
|
576
|
+
|
577
|
+
# @return [Hash<String,Attribute>] Returns a hash of all of the
|
578
|
+
# configured attributes for this class.
|
579
|
+
def attributes
|
580
|
+
@attributes ||= {}
|
581
|
+
end
|
582
|
+
|
583
|
+
# @private
|
584
|
+
def attribute_for attribute_name, &block
|
585
|
+
unless attribute = attributes[attribute_name.to_s]
|
586
|
+
raise UndefinedAttributeError.new(attribute_name.to_s)
|
587
|
+
end
|
588
|
+
block_given? ? yield(attribute) : attribute
|
589
|
+
end
|
590
|
+
|
591
|
+
# @private
|
592
|
+
def add_attribute attribute
|
593
|
+
|
594
|
+
attr_name = attribute.name
|
595
|
+
|
596
|
+
attributes[attr_name] = attribute
|
597
|
+
|
598
|
+
# setter
|
599
|
+
define_method("#{attr_name}=") do |value|
|
600
|
+
self[attr_name] = value
|
601
|
+
end
|
602
|
+
|
603
|
+
# getter
|
604
|
+
define_method(attr_name) do
|
605
|
+
self[attr_name]
|
606
|
+
end
|
607
|
+
|
608
|
+
# before type-cast getter
|
609
|
+
define_method("#{attr_name}_before_type_cast") do
|
610
|
+
@_data[attr_name]
|
611
|
+
end
|
612
|
+
|
613
|
+
## dirty tracking methods
|
614
|
+
|
615
|
+
define_method("#{attr_name}_changed?") do
|
616
|
+
attribute_changed?(attr_name)
|
617
|
+
end
|
618
|
+
|
619
|
+
define_method("#{attr_name}_change") do
|
620
|
+
attribute_change(attr_name)
|
621
|
+
end
|
622
|
+
|
623
|
+
define_method("#{attr_name}_was") do
|
624
|
+
attribute_was(attr_name)
|
625
|
+
end
|
626
|
+
|
627
|
+
define_method("#{attr_name}_will_change!") do
|
628
|
+
attribute_will_change!(attr_name)
|
629
|
+
end
|
630
|
+
|
631
|
+
define_method("reset_#{attr_name}!") do
|
632
|
+
reset_attribute!(attr_name)
|
633
|
+
end
|
634
|
+
|
635
|
+
attribute
|
636
|
+
|
637
|
+
end
|
638
|
+
|
639
|
+
end
|
640
|
+
end
|
641
|
+
end
|
642
|
+
end
|