fake_dynamo 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.
- 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
|