fake_dynamo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +11 -0
- data/Guardfile +19 -0
- data/LICENSE +22 -0
- data/README.md +4 -0
- data/Rakefile +2 -0
- data/bin/fake_dynamo +17 -0
- data/fake_dynamo.gemspec +18 -0
- data/lib/fake_dynamo.rb +20 -0
- data/lib/fake_dynamo/api.yml +734 -0
- data/lib/fake_dynamo/attribute.rb +54 -0
- data/lib/fake_dynamo/db.rb +113 -0
- data/lib/fake_dynamo/exceptions.rb +57 -0
- data/lib/fake_dynamo/filter.rb +110 -0
- data/lib/fake_dynamo/item.rb +102 -0
- data/lib/fake_dynamo/key.rb +71 -0
- data/lib/fake_dynamo/key_schema.rb +26 -0
- data/lib/fake_dynamo/server.rb +43 -0
- data/lib/fake_dynamo/storage.rb +71 -0
- data/lib/fake_dynamo/table.rb +362 -0
- data/lib/fake_dynamo/validation.rb +155 -0
- data/lib/fake_dynamo/version.rb +3 -0
- data/spec/fake_dynamo/db_spec.rb +257 -0
- data/spec/fake_dynamo/filter_spec.rb +122 -0
- data/spec/fake_dynamo/item_spec.rb +97 -0
- data/spec/fake_dynamo/server_spec.rb +47 -0
- data/spec/fake_dynamo/table_spec.rb +435 -0
- data/spec/fake_dynamo/validation_spec.rb +63 -0
- data/spec/spec_helper.rb +28 -0
- metadata +105 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class Attribute
|
3
|
+
attr_accessor :name, :value, :type
|
4
|
+
|
5
|
+
def initialize(name, value, type)
|
6
|
+
@name, @value, @type = name, value, type
|
7
|
+
end
|
8
|
+
|
9
|
+
def description
|
10
|
+
{
|
11
|
+
'AttributeName' => name,
|
12
|
+
'AttributeType' => type
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def as_hash
|
17
|
+
{ @name => { @type => @value } }
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(attribute)
|
21
|
+
@name == attribute.name &&
|
22
|
+
@value == attribute.value &&
|
23
|
+
@type == attribute.type
|
24
|
+
end
|
25
|
+
|
26
|
+
def <=>(other)
|
27
|
+
if @type == 'N'
|
28
|
+
@value.to_i <=> other.value.to_i
|
29
|
+
else
|
30
|
+
@value <=> other.value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def eql?(attribute)
|
35
|
+
return false unless attribute.kind_of? Attribute
|
36
|
+
|
37
|
+
self == attribute
|
38
|
+
end
|
39
|
+
|
40
|
+
def hash
|
41
|
+
name.hash ^ value.hash ^ type.hash
|
42
|
+
end
|
43
|
+
|
44
|
+
class << self
|
45
|
+
def from_data(data)
|
46
|
+
Attribute.new(data['AttributeName'], nil, data['AttributeType'])
|
47
|
+
end
|
48
|
+
|
49
|
+
def from_hash(name, hash)
|
50
|
+
Attribute.new(name, hash.values.first, hash.keys.first)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class DB
|
3
|
+
|
4
|
+
include Validation
|
5
|
+
|
6
|
+
attr_reader :tables
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def instance
|
10
|
+
@db ||= DB.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@tables = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def process(operation, data)
|
19
|
+
validate_payload(operation, data)
|
20
|
+
operation = operation.underscore
|
21
|
+
self.send operation, data
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_table(data)
|
25
|
+
table_name = data['TableName']
|
26
|
+
raise ResourceInUseException, "Duplicate table name: #{table_name}" if tables[table_name]
|
27
|
+
|
28
|
+
table = Table.new(data)
|
29
|
+
tables[table_name] = table
|
30
|
+
response = table.description
|
31
|
+
table.activate
|
32
|
+
response
|
33
|
+
end
|
34
|
+
|
35
|
+
def describe_table(data)
|
36
|
+
table = find_table(data['TableName'])
|
37
|
+
table.describe_table
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete_table(data)
|
41
|
+
table_name = data['TableName']
|
42
|
+
table = find_table(table_name)
|
43
|
+
tables.delete(table_name)
|
44
|
+
table.delete
|
45
|
+
end
|
46
|
+
|
47
|
+
def list_tables(data)
|
48
|
+
start_table = data['ExclusiveStartTableName']
|
49
|
+
limit = data['Limit']
|
50
|
+
|
51
|
+
all_tables = tables.keys
|
52
|
+
start = 0
|
53
|
+
|
54
|
+
if start_table
|
55
|
+
if i = all_tables.index(start_table)
|
56
|
+
start = i + 1
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
limit ||= all_tables.size
|
61
|
+
result_tables = all_tables[start, limit]
|
62
|
+
response = { 'TableNames' => result_tables }
|
63
|
+
|
64
|
+
if (start + limit ) < all_tables.size
|
65
|
+
last_table = all_tables[start + limit -1]
|
66
|
+
response.merge!({ 'LastEvaluatedTableName' => last_table })
|
67
|
+
end
|
68
|
+
response
|
69
|
+
end
|
70
|
+
|
71
|
+
def update_table(data)
|
72
|
+
table = find_table(data['TableName'])
|
73
|
+
table.update(data['ProvisionedThroughput']['ReadCapacityUnits'], data['ProvisionedThroughput']['WriteCapacityUnits'])
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.delegate_to_table(*methods)
|
77
|
+
methods.each do |method|
|
78
|
+
define_method(method) do |data|
|
79
|
+
find_table(data['TableName']).send(method, data)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
delegate_to_table :put_item, :get_item, :delete_item, :update_item, :query, :scan
|
85
|
+
|
86
|
+
|
87
|
+
def batch_get_item(data)
|
88
|
+
response = {}
|
89
|
+
|
90
|
+
data['RequestItems'].each do |table_name, table_data|
|
91
|
+
table = find_table(table_name)
|
92
|
+
|
93
|
+
unless response[table_name]
|
94
|
+
response[table_name] = { 'ConsumedCapacityUnits' => 1, 'Items' => [] }
|
95
|
+
end
|
96
|
+
|
97
|
+
table_data['Keys'].each do |key|
|
98
|
+
if item_hash = table.get_raw_item(key, table_data['AttributesToGet'])
|
99
|
+
response[table_name]['Items'] << item_hash
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
{ 'Responses' => response, 'UnprocessedKeys' => {}}
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def find_table(table_name)
|
109
|
+
tables[table_name] or raise ResourceNotFoundException, "Table : #{table_name} not found"
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class Error < ::StandardError
|
3
|
+
|
4
|
+
class_attribute :description, :type, :status
|
5
|
+
|
6
|
+
self.type = 'com.amazon.dynamodb.v20111205'
|
7
|
+
self.status = 400
|
8
|
+
|
9
|
+
attr_reader :detail
|
10
|
+
|
11
|
+
def initialize(detail='')
|
12
|
+
@detail = detail
|
13
|
+
super(detail)
|
14
|
+
end
|
15
|
+
|
16
|
+
def response
|
17
|
+
{
|
18
|
+
'__type' => "#{self.class.type}##{class_name}",
|
19
|
+
'message' => "#{self.class.description}: #{@detail}"
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def class_name
|
24
|
+
self.class.name.split('::').last
|
25
|
+
end
|
26
|
+
|
27
|
+
def status
|
28
|
+
self.class.status
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class UnknownOperationException < Error
|
33
|
+
self.type = 'com.amazon.coral.service'
|
34
|
+
self.description = ''
|
35
|
+
end
|
36
|
+
|
37
|
+
class InvalidParameterValueException < Error
|
38
|
+
self.description = 'invalid parameter'
|
39
|
+
end
|
40
|
+
|
41
|
+
class ResourceNotFoundException < Error
|
42
|
+
self.description = 'Requested resource not found'
|
43
|
+
end
|
44
|
+
|
45
|
+
class ResourceInUseException < Error
|
46
|
+
self.description = 'Attempt to change a resource which is still in use'
|
47
|
+
end
|
48
|
+
|
49
|
+
class ValidationException < Error
|
50
|
+
self.description = 'Validation error detected'
|
51
|
+
self.type = 'com.amazon.coral.validate'
|
52
|
+
end
|
53
|
+
|
54
|
+
class ConditionalCheckFailedException < Error
|
55
|
+
self.description = 'The conditional request failed'
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
module Filter
|
3
|
+
include Validation
|
4
|
+
|
5
|
+
def comparison_filter(value_list, size, target_attribute, fail_on_type_mismatch, supported_types, comparator)
|
6
|
+
|
7
|
+
validate_size(value_list, size)
|
8
|
+
|
9
|
+
if fail_on_type_mismatch
|
10
|
+
value_list.each do |value|
|
11
|
+
validate_type(value, target_attribute)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
value_attribute_list = value_list.map do |value|
|
16
|
+
value_attribute = Attribute.from_hash(target_attribute.name, value)
|
17
|
+
validate_supported_types(value_attribute, supported_types)
|
18
|
+
value_attribute
|
19
|
+
end
|
20
|
+
|
21
|
+
value_attribute_list.each do |value_attribute|
|
22
|
+
return false if target_attribute.type != value_attribute.type
|
23
|
+
end
|
24
|
+
|
25
|
+
if target_attribute.type == 'N'
|
26
|
+
comparator.call(target_attribute.value.to_i, *value_attribute_list.map(&:value).map(&:to_i))
|
27
|
+
else
|
28
|
+
comparator.call(target_attribute.value, *value_attribute_list.map(&:value))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_supported_types(value_attribute, supported_types)
|
33
|
+
unless supported_types.include? value_attribute.type
|
34
|
+
raise ValidationException, "The attempted filter operation is not supported for the provided type"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_size(value_list, size)
|
39
|
+
if (size.kind_of? Range and (not (size.include? value_list.size))) or
|
40
|
+
(size.kind_of? Fixnum and value_list.size != size)
|
41
|
+
raise ValidationException, "The attempted filter operation is not supported for the provided filter argument count"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.def_filter(name, size, supported_types, &comparator)
|
46
|
+
define_method "#{name}_filter" do |value_list, target_attribute, fail_on_type_mismatch|
|
47
|
+
comparison_filter(value_list, size, target_attribute, fail_on_type_mismatch, supported_types, comparator)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def_filter(:eq, 1, ['N', 'S'], &:==)
|
52
|
+
def_filter(:le, 1, ['N', 'S'], &:<=)
|
53
|
+
def_filter(:lt, 1, ['N', 'S'], &:<)
|
54
|
+
def_filter(:ge, 1, ['N', 'S'], &:>=)
|
55
|
+
def_filter(:gt, 1, ['N', 'S'], &:>)
|
56
|
+
def_filter(:begins_with, 1, ['S'], &:start_with?)
|
57
|
+
def_filter(:between, 2, ['N', 'S'], &:between?)
|
58
|
+
def_filter(:ne, 1, ['N', 'S'], &:!=)
|
59
|
+
|
60
|
+
def not_null_filter(value_list, target_attribute, fail_on_type_mismatch)
|
61
|
+
not target_attribute.nil?
|
62
|
+
end
|
63
|
+
|
64
|
+
def null_filter(value_list, target_attribute, fail_on_type_mismatch)
|
65
|
+
target_attribute.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
def contains_filter(value_list, target_attribute, fail_on_type_mismatch)
|
69
|
+
validate_size(value_list, 1)
|
70
|
+
value_attribute = Attribute.from_hash(target_attribute.name, value_list.first)
|
71
|
+
validate_supported_types(value_attribute, ['N', 'S'])
|
72
|
+
|
73
|
+
if ((value_attribute.type == 'S' and
|
74
|
+
(target_attribute.type == 'S' or target_attribute.type == 'SS')) or
|
75
|
+
(value_attribute.type == 'N' and target_attribute.type == 'NS'))
|
76
|
+
target_attribute.value.include?(value_attribute.value)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def not_contains_filter(value_list, target_attribute, fail_on_type_mismatch)
|
81
|
+
validate_size(value_list, 1)
|
82
|
+
value_attribute = Attribute.from_hash(target_attribute.name, value_list.first)
|
83
|
+
validate_supported_types(value_attribute, ['N', 'S'])
|
84
|
+
|
85
|
+
if ((value_attribute.type == 'S' and
|
86
|
+
(target_attribute.type == 'S' or target_attribute.type == 'SS')) or
|
87
|
+
(value_attribute.type == 'N' and target_attribute.type == 'NS'))
|
88
|
+
!target_attribute.value.include?(value_attribute.value)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
INF = 1.0/0.0
|
93
|
+
|
94
|
+
def in_filter(value_list, target_attribute, fail_on_type_mismatch)
|
95
|
+
validate_size(value_list, (1..INF))
|
96
|
+
|
97
|
+
value_attribute_list = value_list.map do |value|
|
98
|
+
value_attribute = Attribute.from_hash(target_attribute.name, value)
|
99
|
+
validate_supported_types(value_attribute, ['N', 'S'])
|
100
|
+
value_attribute
|
101
|
+
end
|
102
|
+
|
103
|
+
value_attribute_list.each do |value_attribute|
|
104
|
+
return true if value_attribute == target_attribute
|
105
|
+
end
|
106
|
+
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class Item
|
3
|
+
include Validation
|
4
|
+
attr_accessor :key, :attributes
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def from_data(data, key_schema)
|
8
|
+
item = Item.new
|
9
|
+
item.key = Key.from_schema(data, key_schema)
|
10
|
+
|
11
|
+
item.attributes = {}
|
12
|
+
data.each do |name, value|
|
13
|
+
unless item.key[name]
|
14
|
+
item.attributes[name] = Attribute.from_hash(name, value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
item
|
18
|
+
end
|
19
|
+
|
20
|
+
def from_key(key)
|
21
|
+
item = Item.new
|
22
|
+
item.key = key
|
23
|
+
item.attributes = {}
|
24
|
+
item
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
def [](name)
|
30
|
+
attributes[name] or key[name]
|
31
|
+
end
|
32
|
+
|
33
|
+
def as_hash
|
34
|
+
result = {}
|
35
|
+
result.merge!(key.as_hash)
|
36
|
+
@attributes.each do |name, attribute|
|
37
|
+
result.merge!(attribute.as_hash)
|
38
|
+
end
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def update(name, data)
|
43
|
+
if key[name]
|
44
|
+
raise ValidationException, "Cannot update attribute #{name}. This attribute is part of the key"
|
45
|
+
end
|
46
|
+
|
47
|
+
new_value = data['Value']
|
48
|
+
action = data['Action'] || 'PUT'
|
49
|
+
|
50
|
+
unless available_actions.include? action
|
51
|
+
raise ValidationException, "Unknown action '#{action}' in AttributeUpdates.#{name}"
|
52
|
+
end
|
53
|
+
|
54
|
+
if (not new_value) and action != 'DELETE'
|
55
|
+
raise ValidationException, "Only DELETE action is allowed when no attribute value is specified"
|
56
|
+
end
|
57
|
+
|
58
|
+
self.send(action.downcase, name, new_value)
|
59
|
+
end
|
60
|
+
|
61
|
+
def available_actions
|
62
|
+
%w[ PUT ADD DELETE ]
|
63
|
+
end
|
64
|
+
|
65
|
+
def put(name, value)
|
66
|
+
attributes[name] = Attribute.from_hash(name, value)
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete(name, value)
|
70
|
+
if not value
|
71
|
+
attributes.delete(name)
|
72
|
+
elsif old_attribute = attributes[name]
|
73
|
+
validate_type(value, old_attribute)
|
74
|
+
unless ["SS", "NS"].include? old_attribute.type
|
75
|
+
raise ValidationException, "Action DELETE is not supported for type #{old_attribute.type}"
|
76
|
+
end
|
77
|
+
attribute = Attribute.from_hash(name, value)
|
78
|
+
old_attribute.value -= attribute.value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def add(name, value)
|
83
|
+
attribute = Attribute.from_hash(name, value)
|
84
|
+
|
85
|
+
unless ["N", "SS", "NS"].include? attribute.type
|
86
|
+
raise ValidationException, "Action ADD is not supported for type #{attribute.type}"
|
87
|
+
end
|
88
|
+
|
89
|
+
if old_attribute = attributes[name]
|
90
|
+
validate_type(value, old_attribute)
|
91
|
+
case attribute.type
|
92
|
+
when "N"
|
93
|
+
old_attribute.value = (old_attribute.value.to_i + attribute.value.to_i).to_s
|
94
|
+
else
|
95
|
+
old_attribute.value += attribute.value
|
96
|
+
end
|
97
|
+
else
|
98
|
+
attributes[name] = attribute
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module FakeDynamo
|
2
|
+
class Key
|
3
|
+
extend Validation
|
4
|
+
|
5
|
+
attr_accessor :primary, :range
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def from_data(key_data, key_schema)
|
9
|
+
key = Key.new
|
10
|
+
validate_key_data(key_data, key_schema)
|
11
|
+
key.primary = Attribute.from_hash(key_schema.hash_key.name, key_data['HashKeyElement'])
|
12
|
+
|
13
|
+
if key_schema.range_key
|
14
|
+
key.range = Attribute.from_hash(key_schema.range_key.name, key_data['RangeKeyElement'])
|
15
|
+
end
|
16
|
+
key
|
17
|
+
end
|
18
|
+
|
19
|
+
def from_schema(data, key_schema)
|
20
|
+
key = Key.new
|
21
|
+
validate_key_schema(data, key_schema)
|
22
|
+
key.primary = create_attribute(key_schema.hash_key, data)
|
23
|
+
|
24
|
+
if key_schema.range_key
|
25
|
+
key.range = create_attribute(key_schema.range_key, data)
|
26
|
+
end
|
27
|
+
key
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_attribute(key, data)
|
31
|
+
name = key.name
|
32
|
+
attr = Attribute.from_hash(name, data[name])
|
33
|
+
attr
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def [](name)
|
38
|
+
return @primary if @primary.name == name
|
39
|
+
return @range if @range and @range.name == name
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def eql?(key)
|
44
|
+
return false unless key.kind_of? Key
|
45
|
+
|
46
|
+
@primary == key.primary &&
|
47
|
+
@range == key.range
|
48
|
+
end
|
49
|
+
|
50
|
+
def hash
|
51
|
+
primary.hash ^ range.hash
|
52
|
+
end
|
53
|
+
|
54
|
+
def as_hash
|
55
|
+
result = @primary.as_hash
|
56
|
+
if @range
|
57
|
+
result.merge!(@range.as_hash)
|
58
|
+
end
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def as_key_hash
|
63
|
+
result = { 'HashKeyElement' => { @primary.type => @primary.value }}
|
64
|
+
if @range
|
65
|
+
result.merge!({'RangeKeyElement' => { @range.type => @range.value }})
|
66
|
+
end
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|