aws-record 1.0.0.pre.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.
- checksums.yaml +7 -0
- data/lib/aws-record.rb +24 -0
- data/lib/aws-record/record.rb +65 -0
- data/lib/aws-record/record/attribute.rb +46 -0
- data/lib/aws-record/record/attributes.rb +242 -0
- data/lib/aws-record/record/attributes/boolean_marshaler.rb +41 -0
- data/lib/aws-record/record/attributes/date_marshaler.rb +49 -0
- data/lib/aws-record/record/attributes/date_time_marshaler.rb +50 -0
- data/lib/aws-record/record/attributes/float_marshaler.rb +40 -0
- data/lib/aws-record/record/attributes/integer_marshaler.rb +40 -0
- data/lib/aws-record/record/attributes/string_marshaler.rb +47 -0
- data/lib/aws-record/record/errors.rb +13 -0
- data/lib/aws-record/record/item_operations.rb +107 -0
- data/lib/aws-record/record/table_migration.rb +89 -0
- data/lib/aws-record/record/version.rb +5 -0
- metadata +73 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 84fe186e9a5995f6ca277aa4776a37dfaef73928
|
4
|
+
data.tar.gz: 267cf8b8556f23108e5cc3e660177af43ebe0c26
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e69390465c8fe8f2eb45eaffeeef9d90a8ded9159a440170e3d5a3258e0679a230f4c6cc50930e2b6cd377f4a132a84389607569c1811238cd858f6a5fb9e830
|
7
|
+
data.tar.gz: b2ff7b50b0c2a652f21901d592ed6476fe97bd146cc95c89bdb0fccd34a2ffec3fe4233ea1e764fe72ff53bbf9af5d53e58a7cd4ac10e4717f2c45d9fe778778
|
data/lib/aws-record.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'aws-sdk-resources'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
autoload :Record, 'aws-record/record'
|
5
|
+
|
6
|
+
module Record
|
7
|
+
autoload :Attribute, 'aws-record/record/attribute'
|
8
|
+
autoload :Attributes, 'aws-record/record/attributes'
|
9
|
+
autoload :Errors, 'aws-record/record/errors'
|
10
|
+
autoload :ItemOperations, 'aws-record/record/item_operations'
|
11
|
+
autoload :TableMigration, 'aws-record/record/table_migration'
|
12
|
+
autoload :VERSION, 'aws-record/record/version'
|
13
|
+
|
14
|
+
module Attributes
|
15
|
+
autoload :StringMarshaler, 'aws-record/record/attributes/string_marshaler'
|
16
|
+
autoload :BooleanMarshaler, 'aws-record/record/attributes/boolean_marshaler'
|
17
|
+
autoload :IntegerMarshaler, 'aws-record/record/attributes/integer_marshaler'
|
18
|
+
autoload :FloatMarshaler, 'aws-record/record/attributes/float_marshaler'
|
19
|
+
autoload :DateMarshaler, 'aws-record/record/attributes/date_marshaler'
|
20
|
+
autoload :DateTimeMarshaler, 'aws-record/record/attributes/date_time_marshaler'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
def self.included(sub_class)
|
4
|
+
sub_class.extend(RecordClassMethods)
|
5
|
+
sub_class.include(Attributes)
|
6
|
+
sub_class.include(ItemOperations)
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def dynamodb_client
|
11
|
+
self.class.dynamodb_client
|
12
|
+
end
|
13
|
+
|
14
|
+
module RecordClassMethods
|
15
|
+
def table_name
|
16
|
+
if @table_name
|
17
|
+
@table_name
|
18
|
+
else
|
19
|
+
@table_name = self.name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_table_name(name)
|
24
|
+
@table_name = name
|
25
|
+
end
|
26
|
+
|
27
|
+
def provisioned_throughput
|
28
|
+
begin
|
29
|
+
resp = dynamodb_client.describe_table(table_name: @table_name)
|
30
|
+
throughput = resp.table.provisioned_throughput
|
31
|
+
return {
|
32
|
+
read_capacity_units: throughput.read_capacity_units,
|
33
|
+
write_capacity_units: throughput.write_capacity_units
|
34
|
+
}
|
35
|
+
rescue DynamoDB::Errors::ResourceNotFoundException
|
36
|
+
raise Record::Errors::TableDoesNotExist
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def table_exists?
|
41
|
+
begin
|
42
|
+
resp = dynamodb_client.describe_table(table_name: @table_name)
|
43
|
+
if resp.table.table_status == "ACTIVE"
|
44
|
+
true
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
rescue DynamoDB::Errors::ResourceNotFoundException
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def configure_client(opts = {})
|
54
|
+
provided_client = opts.delete(:client)
|
55
|
+
opts[:user_agent_suffix] = user_agent(opts.delete(:user_agent_suffix))
|
56
|
+
client = provided_client || Aws::DynamoDB::Client.new(opts)
|
57
|
+
@dynamodb_client = client
|
58
|
+
end
|
59
|
+
|
60
|
+
def dynamodb_client
|
61
|
+
@dynamodb_client ||= configure_client
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
class Attribute
|
4
|
+
|
5
|
+
attr_reader :name, :database_name, :dynamodb_type
|
6
|
+
|
7
|
+
def initialize(name, options = {})
|
8
|
+
@name = name
|
9
|
+
@database_name = options[:database_attribute_name] || name.to_s
|
10
|
+
@dynamodb_type = options[:dynamodb_type]
|
11
|
+
@marshaler = options[:marshaler] || DefaultMarshaler
|
12
|
+
@validators = options[:validators] || []
|
13
|
+
end
|
14
|
+
|
15
|
+
def type_cast(raw_value)
|
16
|
+
@marshaler.type_cast(raw_value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def serialize(raw_value)
|
20
|
+
@marshaler.serialize(raw_value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def valid?(raw_value)
|
24
|
+
value = type_cast(raw_value)
|
25
|
+
@validators.all? do |validator|
|
26
|
+
validator.validate(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract(dynamodb_item)
|
31
|
+
dynamodb_item[database_name]
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
module DefaultMarshaler
|
37
|
+
def self.type_cast(raw_value, options = {})
|
38
|
+
raw_value
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.serialize(raw_value, options = {})
|
42
|
+
raw_value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,242 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Attributes
|
4
|
+
|
5
|
+
def self.included(sub_class)
|
6
|
+
sub_class.extend(ClassMethods)
|
7
|
+
sub_class.instance_variable_set("@keys", {})
|
8
|
+
sub_class.instance_variable_set("@attributes", {})
|
9
|
+
sub_class.instance_variable_set("@storage_attributes", {})
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@data = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a hash representation of the attribute data.
|
17
|
+
#
|
18
|
+
# @return [Hash] Map of attribute names to raw values.
|
19
|
+
def to_h
|
20
|
+
@data.dup
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
|
25
|
+
# Define an attribute for your model, providing your own attribute type.
|
26
|
+
#
|
27
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
28
|
+
# is safe to use as a method.
|
29
|
+
# @param [Marshaler] marshaler The marshaler for this attribute. So long
|
30
|
+
# as you provide a marshaler which implements `#type_cast` and
|
31
|
+
# `#serialize` that consume raw values as expected, you can bring your
|
32
|
+
# own marshaler type. Convenience methods will provide this for you.
|
33
|
+
# @param [Hash] options
|
34
|
+
# @option options [Array] :validators An array of validator classes that
|
35
|
+
# will be run when an attribute is checked for validity.
|
36
|
+
# @option options [String] :database_attribute_name Optional attribute
|
37
|
+
# used to specify a different name for database persistence than the
|
38
|
+
# `name` parameter. Must be unique (you can't have overlap between
|
39
|
+
# database attribute names and the names of other attributes).
|
40
|
+
# @option options [String] :dynamodb_type Generally used for keys and
|
41
|
+
# index attributes, one of "S", "N", "B", "BOOL", "SS", "NS", "BS",
|
42
|
+
# "M", "L". Optional if this attribute will never be used for a key or
|
43
|
+
# secondary index, but most convenience methods for setting attributes
|
44
|
+
# will provide this.
|
45
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
46
|
+
# the hash key for the table.
|
47
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
48
|
+
# the range key for the table.
|
49
|
+
def attr(name, marshaler, opts = {})
|
50
|
+
validate_attr_name(name)
|
51
|
+
|
52
|
+
opts = opts.merge(marshaler: marshaler)
|
53
|
+
attribute = Attribute.new(name, opts)
|
54
|
+
|
55
|
+
storage_name = attribute.database_name
|
56
|
+
|
57
|
+
check_for_naming_collisions(name, storage_name)
|
58
|
+
check_if_reserved(name)
|
59
|
+
|
60
|
+
@attributes[name] = attribute
|
61
|
+
@storage_attributes[storage_name] = name
|
62
|
+
|
63
|
+
define_attr_methods(name, attribute)
|
64
|
+
key_attributes(name, opts)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Define a string-type attribute for your model.
|
68
|
+
#
|
69
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
70
|
+
# is safe to use as a method.
|
71
|
+
# @param [Hash] options
|
72
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
73
|
+
# the hash key for the table.
|
74
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
75
|
+
# the range key for the table.
|
76
|
+
def string_attr(id, opts = {})
|
77
|
+
opts[:dynamodb_type] = "S"
|
78
|
+
attr(id, Attributes::StringMarshaler, opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Define a boolean-type attribute for your model.
|
82
|
+
#
|
83
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
84
|
+
# is safe to use as a method.
|
85
|
+
# @param [Hash] options
|
86
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
87
|
+
# the hash key for the table.
|
88
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
89
|
+
# the range key for the table.
|
90
|
+
def boolean_attr(id, opts = {})
|
91
|
+
opts[:dynamodb_type] = "BOOL"
|
92
|
+
attr(id, Attributes::BooleanMarshaler, opts)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Define a integer-type attribute for your model.
|
96
|
+
#
|
97
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
98
|
+
# is safe to use as a method.
|
99
|
+
# @param [Hash] options
|
100
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
101
|
+
# the hash key for the table.
|
102
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
103
|
+
# the range key for the table.
|
104
|
+
def integer_attr(id, opts = {})
|
105
|
+
opts[:dynamodb_type] = "N"
|
106
|
+
attr(id, Attributes::IntegerMarshaler, opts)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Define a float-type attribute for your model.
|
110
|
+
#
|
111
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
112
|
+
# is safe to use as a method.
|
113
|
+
# @param [Hash] options
|
114
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
115
|
+
# the hash key for the table.
|
116
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
117
|
+
# the range key for the table.
|
118
|
+
def float_attr(id, opts = {})
|
119
|
+
opts[:dynamodb_type] = "N"
|
120
|
+
attr(id, Attributes::FloatMarshaler, opts)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Define a date-type attribute for your model.
|
124
|
+
#
|
125
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
126
|
+
# is safe to use as a method.
|
127
|
+
# @param [Hash] options
|
128
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
129
|
+
# the hash key for the table.
|
130
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
131
|
+
# the range key for the table.
|
132
|
+
def date_attr(id, opts = {})
|
133
|
+
opts[:dynamodb_type] = "S"
|
134
|
+
attr(id, Attributes::DateMarshaler, opts)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Define a datetime-type attribute for your model.
|
138
|
+
#
|
139
|
+
# @param [Symbol] name Name of this attribute. It should be a name that
|
140
|
+
# is safe to use as a method.
|
141
|
+
# @param [Hash] options
|
142
|
+
# @option options [Boolean] :hash_key Set to true if this attribute is
|
143
|
+
# the hash key for the table.
|
144
|
+
# @option options [Boolean] :range_key Set to true if this attribute is
|
145
|
+
# the range key for the table.
|
146
|
+
def datetime_attr(id, opts = {})
|
147
|
+
opts[:dynamodb_type] = "S"
|
148
|
+
attr(id, Attributes::DateTimeMarshaler, opts)
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Hash] hash of symbolized attribute names to attribute objects
|
152
|
+
def attributes
|
153
|
+
@attributes
|
154
|
+
end
|
155
|
+
|
156
|
+
# @return [Aws::Record::Attribute,nil]
|
157
|
+
def hash_key
|
158
|
+
@attributes[@keys[:hash]]
|
159
|
+
end
|
160
|
+
|
161
|
+
# @return [Aws::Record::Attribute,nil]
|
162
|
+
def range_key
|
163
|
+
@attributes[@keys[:range]]
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return [Hash] A mapping of the :hash and :range keys to the attribute
|
167
|
+
# name symbols associated with them.
|
168
|
+
def keys
|
169
|
+
@keys
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
def define_attr_methods(name, attribute)
|
174
|
+
define_method(name) do
|
175
|
+
raw = @data[name]
|
176
|
+
attribute.type_cast(raw)
|
177
|
+
end
|
178
|
+
|
179
|
+
define_method("#{name}=") do |value|
|
180
|
+
@data[name] = value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def key_attributes(id, opts)
|
185
|
+
if opts[:hash_key] == true && opts[:range_key] == true
|
186
|
+
raise ArgumentError.new(
|
187
|
+
"Cannot have the same attribute be a hash and range key."
|
188
|
+
)
|
189
|
+
elsif opts[:hash_key] == true
|
190
|
+
define_key(id, :hash)
|
191
|
+
elsif opts[:range_key] == true
|
192
|
+
define_key(id, :range)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def define_key(id, type)
|
197
|
+
@keys[type] = id
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_attr_name(name)
|
201
|
+
unless name.is_a?(Symbol)
|
202
|
+
raise ArgumentError.new("Must use symbolized :name attribute.")
|
203
|
+
end
|
204
|
+
if @attributes[name]
|
205
|
+
raise Errors::NameCollision.new(
|
206
|
+
"Cannot overwrite existing attribute #{name}"
|
207
|
+
)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def check_if_reserved(name)
|
212
|
+
if instance_methods.include?(name)
|
213
|
+
raise Errors::ReservedName.new(
|
214
|
+
"Cannot name an attribute #{name}, that would collide with an"\
|
215
|
+
" existing instance method."
|
216
|
+
)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def check_for_naming_collisions(name, storage_name)
|
221
|
+
if @attributes[storage_name]
|
222
|
+
raise Errors::NameCollision.new(
|
223
|
+
"Custom storage name #{storage_name} already exists as an"\
|
224
|
+
" attribute name in #{@attributes}"
|
225
|
+
)
|
226
|
+
elsif @storage_attributes[name]
|
227
|
+
raise Errors::NameCollision.new(
|
228
|
+
"Attribute name #{name} already exists as a custom storage"\
|
229
|
+
" name in #{@storage_attributes}"
|
230
|
+
)
|
231
|
+
elsif @storage_attributes[storage_name]
|
232
|
+
raise Errors::NameCollision.new(
|
233
|
+
"Custom storage name #{storage_name} already in use in"\
|
234
|
+
" #{@storage_attributes}"
|
235
|
+
)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Attributes
|
4
|
+
module BooleanMarshaler
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def type_cast(raw_value, options = {})
|
9
|
+
case raw_value
|
10
|
+
when nil
|
11
|
+
nil
|
12
|
+
when ''
|
13
|
+
nil
|
14
|
+
when false, 'false', '0', 0
|
15
|
+
false
|
16
|
+
else
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def serialize(raw_value, options = {})
|
22
|
+
boolean = type_cast(raw_value, options)
|
23
|
+
case boolean
|
24
|
+
when nil
|
25
|
+
nil
|
26
|
+
when false
|
27
|
+
false
|
28
|
+
when true
|
29
|
+
true
|
30
|
+
else
|
31
|
+
msg = "expected a boolean value or nil, got #{boolean.class}"
|
32
|
+
raise ArgumentError, msg
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module Record
|
5
|
+
module Attributes
|
6
|
+
module DateMarshaler
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def type_cast(raw_value, options = {})
|
11
|
+
case raw_value
|
12
|
+
when nil
|
13
|
+
nil
|
14
|
+
when ''
|
15
|
+
nil
|
16
|
+
when Date
|
17
|
+
raw_value
|
18
|
+
when Integer
|
19
|
+
begin
|
20
|
+
Date.parse(Time.at(raw_value).to_s) # assumed timestamp
|
21
|
+
rescue
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
else
|
25
|
+
begin
|
26
|
+
Date.parse(raw_value.to_s) # Time, DateTime or String
|
27
|
+
rescue
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def serialize(raw_value, options = {})
|
34
|
+
date = type_cast(raw_value)
|
35
|
+
if date.nil?
|
36
|
+
nil
|
37
|
+
elsif date.is_a?(Date)
|
38
|
+
date.strftime('%Y-%m-%d')
|
39
|
+
else
|
40
|
+
raise ArgumentError, "expected a Date value or nil, got #{date.class}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module Record
|
5
|
+
module Attributes
|
6
|
+
module DateTimeMarshaler
|
7
|
+
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def type_cast(raw_value, options = {})
|
11
|
+
case raw_value
|
12
|
+
when nil
|
13
|
+
nil
|
14
|
+
when ''
|
15
|
+
nil
|
16
|
+
when DateTime
|
17
|
+
raw_value
|
18
|
+
when Integer
|
19
|
+
begin
|
20
|
+
DateTime.parse(Time.at(raw_value).to_s) # timestamp
|
21
|
+
rescue
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
else
|
25
|
+
begin
|
26
|
+
DateTime.parse(raw_value.to_s) # Time, Date or String
|
27
|
+
rescue
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def serialize(raw_value, options = {})
|
34
|
+
datetime = type_cast(raw_value)
|
35
|
+
if datetime.nil?
|
36
|
+
nil
|
37
|
+
elsif datetime.is_a?(DateTime)
|
38
|
+
datetime.strftime('%Y-%m-%dT%H:%M:%S%Z')
|
39
|
+
else
|
40
|
+
msg = "expected a DateTime value or nil, got #{datetime.class}"
|
41
|
+
raise ArgumentError, msg
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Attributes
|
4
|
+
module FloatMarshaler
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def type_cast(raw_value, options = {})
|
9
|
+
case raw_value
|
10
|
+
when nil
|
11
|
+
nil
|
12
|
+
when ''
|
13
|
+
nil
|
14
|
+
when Float
|
15
|
+
raw_value
|
16
|
+
else
|
17
|
+
raw_value.respond_to?(:to_f) ?
|
18
|
+
raw_value.to_f :
|
19
|
+
raw_value.to_s.to_f
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize(raw_value, options = {})
|
24
|
+
float = type_cast(raw_value, options = {})
|
25
|
+
if float.nil?
|
26
|
+
nil
|
27
|
+
elsif float.is_a?(Float)
|
28
|
+
float
|
29
|
+
else
|
30
|
+
msg = "expected a Float value or nil, got #{value.class}"
|
31
|
+
raise ArgumentError, msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Attributes
|
4
|
+
module IntegerMarshaler
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def type_cast(raw_value, options = {})
|
9
|
+
case raw_value
|
10
|
+
when nil
|
11
|
+
nil
|
12
|
+
when ''
|
13
|
+
nil
|
14
|
+
when Integer
|
15
|
+
raw_value
|
16
|
+
else
|
17
|
+
raw_value.respond_to?(:to_i) ?
|
18
|
+
raw_value.to_i :
|
19
|
+
raw_value.to_s.to_i
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def serialize(raw_value, options = {})
|
24
|
+
integer = type_cast(raw_value, options = {})
|
25
|
+
if integer.nil?
|
26
|
+
nil
|
27
|
+
elsif integer.is_a?(Integer)
|
28
|
+
integer
|
29
|
+
else
|
30
|
+
msg = "expected an Integer value or nil, got #{value.class}"
|
31
|
+
raise ArgumentError, msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Attributes
|
4
|
+
module StringMarshaler
|
5
|
+
|
6
|
+
class << self
|
7
|
+
|
8
|
+
def type_cast(raw_value, options = {})
|
9
|
+
case raw_value
|
10
|
+
when nil
|
11
|
+
if options[:nil_as_empty_string]
|
12
|
+
''
|
13
|
+
else
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
when String
|
17
|
+
if raw_value.empty? && !options[:nil_as_empty_string]
|
18
|
+
nil
|
19
|
+
else
|
20
|
+
raw_value
|
21
|
+
end
|
22
|
+
else
|
23
|
+
raw_value.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def serialize(raw_value, options = {})
|
28
|
+
value = type_cast(raw_value)
|
29
|
+
if value.is_a?(String)
|
30
|
+
if value.empty?
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
elsif value.nil?
|
36
|
+
nil
|
37
|
+
else
|
38
|
+
msg = "expected a String value or nil, got #{value.class}"
|
39
|
+
raise ArgumentError, msg
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module Errors
|
4
|
+
|
5
|
+
class KeyMissing < RuntimeError; end
|
6
|
+
class NameCollision < RuntimeError; end
|
7
|
+
class ReservedName < RuntimeError; end
|
8
|
+
class InvalidModel < RuntimeError; end
|
9
|
+
class TableDoesNotExist < RuntimeError; end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
module ItemOperations
|
4
|
+
|
5
|
+
def self.included(sub_class)
|
6
|
+
sub_class.extend(ItemOperationsClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
def save
|
10
|
+
dynamodb_client.put_item(
|
11
|
+
table_name: self.class.table_name,
|
12
|
+
item: build_item_for_save
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete!
|
17
|
+
dynamodb_client.delete_item(
|
18
|
+
table_name: self.class.table_name,
|
19
|
+
key: key_values
|
20
|
+
)
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def build_item_for_save
|
26
|
+
validate_key_values
|
27
|
+
attributes = self.class.attributes
|
28
|
+
@data.inject({}) do |acc, name_value_pair|
|
29
|
+
attr_name, raw_value = name_value_pair
|
30
|
+
db_name = attributes[attr_name].database_name
|
31
|
+
acc[db_name] = attributes[attr_name].serialize(raw_value)
|
32
|
+
acc
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_values
|
37
|
+
validate_key_values
|
38
|
+
attributes = self.class.attributes
|
39
|
+
self.class.keys.inject({}) do |acc, (_, attr_name)|
|
40
|
+
db_name = attributes[attr_name].database_name
|
41
|
+
acc[db_name] = attributes[attr_name].serialize(@data[attr_name])
|
42
|
+
acc
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_key_values
|
47
|
+
missing = missing_key_values
|
48
|
+
unless missing.empty?
|
49
|
+
raise Errors::KeyMissing.new(
|
50
|
+
"Missing required keys: #{missing.join(', ')}"
|
51
|
+
)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def missing_key_values
|
56
|
+
self.class.keys.inject([]) do |acc, key|
|
57
|
+
acc << key.last if @data[key.last].nil?
|
58
|
+
acc
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module ItemOperationsClassMethods
|
63
|
+
def find(opts)
|
64
|
+
key = {}
|
65
|
+
@keys.each_value do |attr_sym|
|
66
|
+
unless opts[attr_sym]
|
67
|
+
raise Errors::KeyMissing.new(
|
68
|
+
"Missing required key #{attr_sym} in #{opts}"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
attr_name = attr_sym.to_s
|
72
|
+
key[attr_name] = attributes[attr_sym].serialize(opts[attr_sym])
|
73
|
+
end
|
74
|
+
request_opts = {
|
75
|
+
table_name: table_name,
|
76
|
+
key: key
|
77
|
+
}
|
78
|
+
resp = dynamodb_client.get_item(request_opts)
|
79
|
+
if resp.item.nil?
|
80
|
+
nil
|
81
|
+
else
|
82
|
+
build_item_from_resp(resp)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def build_item_from_resp(resp)
|
88
|
+
record = new
|
89
|
+
data = record.instance_variable_get("@data")
|
90
|
+
attributes.each do |name, attr|
|
91
|
+
data[name] = attr.extract(resp.item)
|
92
|
+
end
|
93
|
+
record
|
94
|
+
end
|
95
|
+
|
96
|
+
def user_agent(custom)
|
97
|
+
if custom
|
98
|
+
custom
|
99
|
+
else
|
100
|
+
" aws-record/#{VERSION}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Aws
|
2
|
+
module Record
|
3
|
+
class TableMigration
|
4
|
+
|
5
|
+
attr_accessor :client
|
6
|
+
|
7
|
+
def initialize(model, client: nil)
|
8
|
+
assert_model_valid(model)
|
9
|
+
@model = model
|
10
|
+
@client = client || Aws::DynamoDB::Client.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def create!(opts)
|
14
|
+
create_opts = opts.merge({
|
15
|
+
table_name: @model.table_name,
|
16
|
+
attribute_definitions: attribute_definitions,
|
17
|
+
key_schema: key_schema
|
18
|
+
})
|
19
|
+
@client.create_table(create_opts)
|
20
|
+
end
|
21
|
+
|
22
|
+
def update!(opts)
|
23
|
+
begin
|
24
|
+
update_opts = opts.merge({
|
25
|
+
table_name: @model.table_name
|
26
|
+
})
|
27
|
+
@client.update_table(update_opts)
|
28
|
+
rescue DynamoDB::Errors::ResourceNotFoundException => e
|
29
|
+
raise Errors::TableDoesNotExist.new(e)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete!
|
34
|
+
begin
|
35
|
+
@client.delete_table(table_name: @model.table_name)
|
36
|
+
rescue DynamoDB::Errors::ResourceNotFoundException => e
|
37
|
+
raise Errors::TableDoesNotExist.new(e)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def wait_until_available
|
42
|
+
@client.wait_until(:table_exists, table_name: @model.table_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def assert_model_valid(model)
|
47
|
+
assert_required_include(model)
|
48
|
+
assert_keys(model)
|
49
|
+
end
|
50
|
+
|
51
|
+
def assert_required_include(model)
|
52
|
+
unless model.include?(::Aws::Record)
|
53
|
+
raise Errors::InvalidModel.new("Table models must include Aws::Record")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def assert_keys(model)
|
58
|
+
if model.hash_key.nil?
|
59
|
+
raise Errors::InvalidModel.new("Table models must include a hash key")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def attribute_definitions
|
64
|
+
keys.map do |type, attr|
|
65
|
+
{
|
66
|
+
attribute_name: attr.database_name,
|
67
|
+
attribute_type: attr.dynamodb_type
|
68
|
+
}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def key_schema
|
73
|
+
keys.map do |type, attr|
|
74
|
+
{
|
75
|
+
attribute_name: attr.database_name,
|
76
|
+
key_type: type == :hash ? "HASH" : "RANGE"
|
77
|
+
}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def keys
|
82
|
+
@model.keys.inject({}) do |acc, (type, name)|
|
83
|
+
acc[type] = @model.attributes[name]
|
84
|
+
acc
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aws-record
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Amazon Web Services
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk-resources
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
description: Provides an object mapping abstraction for Amazon DynamoDB.
|
28
|
+
email:
|
29
|
+
- alexwood@amazon.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/aws-record.rb
|
35
|
+
- lib/aws-record/record.rb
|
36
|
+
- lib/aws-record/record/attribute.rb
|
37
|
+
- lib/aws-record/record/attributes.rb
|
38
|
+
- lib/aws-record/record/attributes/boolean_marshaler.rb
|
39
|
+
- lib/aws-record/record/attributes/date_marshaler.rb
|
40
|
+
- lib/aws-record/record/attributes/date_time_marshaler.rb
|
41
|
+
- lib/aws-record/record/attributes/float_marshaler.rb
|
42
|
+
- lib/aws-record/record/attributes/integer_marshaler.rb
|
43
|
+
- lib/aws-record/record/attributes/string_marshaler.rb
|
44
|
+
- lib/aws-record/record/errors.rb
|
45
|
+
- lib/aws-record/record/item_operations.rb
|
46
|
+
- lib/aws-record/record/table_migration.rb
|
47
|
+
- lib/aws-record/record/version.rb
|
48
|
+
homepage: http://github.com/aws/aws-sdk-ruby-record
|
49
|
+
licenses:
|
50
|
+
- Apache 2.0
|
51
|
+
metadata: {}
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - ">"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.3.1
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 2.2.2
|
69
|
+
signing_key:
|
70
|
+
specification_version: 4
|
71
|
+
summary: AWS Record library for Amazon DynamoDB
|
72
|
+
test_files: []
|
73
|
+
has_rdoc:
|