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