fake_dynamo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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