kvom 6.8.0.110.6570b45 → 6.8.0.210.ed204b0
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/lib/kvom.rb +7 -0
- data/lib/kvom/adapter.rb +3 -0
- data/lib/kvom/adapter/attributes.rb +73 -0
- data/lib/kvom/adapter/base.rb +21 -15
- data/lib/kvom/adapter/document.rb +25 -5
- data/lib/kvom/adapter/dynamodb_adapter.rb +5 -6
- data/lib/kvom/adapter/dynamodb_document.rb +21 -93
- data/lib/kvom/adapter/filesystem_adapter.rb +14 -4
- data/lib/kvom/adapter/filesystem_document.rb +2 -12
- data/lib/kvom/conflict.rb +3 -0
- data/lib/kvom/model/base.rb +4 -1
- data/lib/kvom/write_conflict.rb +17 -0
- data/script/cleanup +78 -0
- data/spec/model/all_ids_spec.rb +5 -1
- data/spec/model/base_spec.rb +177 -40
- data/spec/model/conflict_spec.rb +110 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/support/adapter.rb +23 -0
- data/spec/support/model.rb +6 -14
- data/spec/support/test_ids.rb +1 -1
- metadata +16 -9
- data/lib/kvom/adapter/key_attributes_document.rb +0 -36
- data/lib/kvom/not_found.rb +0 -7
data/lib/kvom.rb
CHANGED
data/lib/kvom/adapter.rb
CHANGED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'kvom/adapter'
|
2
|
+
|
3
|
+
module Kvom; module Adapter
|
4
|
+
|
5
|
+
class Attributes
|
6
|
+
def initialize(attributes, from_db = false)
|
7
|
+
if from_db
|
8
|
+
meta = attributes && attributes.delete("@meta.json")
|
9
|
+
@coding = meta && Lib::Json.load(meta)["coding"] || {}
|
10
|
+
@coded_attributes = attributes
|
11
|
+
@attributes = {}
|
12
|
+
else
|
13
|
+
@coding = {}
|
14
|
+
@coded_attributes = {}
|
15
|
+
@attributes = attributes ? attributes.dup : {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](name)
|
20
|
+
return @attributes[name] if @attributes.key?(name)
|
21
|
+
coded_value = @coded_attributes[name]
|
22
|
+
@attributes[name] =
|
23
|
+
case @coding[name]
|
24
|
+
when nil
|
25
|
+
coded_value
|
26
|
+
when "json"
|
27
|
+
::Kvom::Lib::Json.load(coded_value)
|
28
|
+
when "jsonvalue"
|
29
|
+
::Kvom::Lib::JsonValue.load(coded_value)
|
30
|
+
else
|
31
|
+
raise "Unexpected coding #{@coding[name]} for attribute #{name}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def []=(name, value)
|
36
|
+
@coding.delete(name)
|
37
|
+
@coded_attributes.delete(name)
|
38
|
+
@attributes[name] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_h
|
42
|
+
@attributes.each do |(name, value)|
|
43
|
+
next if @coded_attributes.key?(name)
|
44
|
+
if value == nil
|
45
|
+
# can happen for (the writer bypassing) new(:property => nil)
|
46
|
+
@coded_attributes.delete(name)
|
47
|
+
else
|
48
|
+
@coded_attributes[name] =
|
49
|
+
case value
|
50
|
+
when "", true, false
|
51
|
+
@coding[name] = "jsonvalue"
|
52
|
+
::Kvom::Lib::JsonValue.dump(value)
|
53
|
+
when Array, Hash
|
54
|
+
@coding[name] = "json"
|
55
|
+
::Kvom::Lib::Json.dump(value)
|
56
|
+
when String, Numeric
|
57
|
+
value
|
58
|
+
else
|
59
|
+
raise UnsupportedValue, "Unsupported value type #{value.class}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
if @coding.empty?
|
65
|
+
@coded_attributes.delete("@meta.json")
|
66
|
+
else
|
67
|
+
@coded_attributes["@meta.json"] = Lib::Json.dump("coding" => @coding)
|
68
|
+
end
|
69
|
+
@coded_attributes
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end; end
|
data/lib/kvom/adapter/base.rb
CHANGED
@@ -24,8 +24,10 @@ class Base
|
|
24
24
|
end
|
25
25
|
|
26
26
|
def save(doc)
|
27
|
-
|
28
|
-
|
27
|
+
doc.update_revision do
|
28
|
+
count_request do
|
29
|
+
write_doc(doc)
|
30
|
+
end
|
29
31
|
end
|
30
32
|
end
|
31
33
|
|
@@ -37,6 +39,7 @@ class Base
|
|
37
39
|
|
38
40
|
def destroy(doc)
|
39
41
|
count_request do
|
42
|
+
key = doc.key
|
40
43
|
destroy_doc(doc)
|
41
44
|
end
|
42
45
|
end
|
@@ -61,6 +64,22 @@ class Base
|
|
61
64
|
|
62
65
|
private
|
63
66
|
|
67
|
+
%w[
|
68
|
+
read_doc
|
69
|
+
write_doc
|
70
|
+
write_index_doc
|
71
|
+
destroy_doc
|
72
|
+
destroy_index_doc
|
73
|
+
query_docs
|
74
|
+
doc_range_values_of_hash_value
|
75
|
+
].each do |name|
|
76
|
+
define_method(name) do |*args|
|
77
|
+
raise "Method #{name} not yet implemented for #{self.class.name}!"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
64
83
|
def count_document_request
|
65
84
|
result = count_request {yield}
|
66
85
|
@documents_loaded_counter +=
|
@@ -80,19 +99,6 @@ class Base
|
|
80
99
|
yield
|
81
100
|
end
|
82
101
|
|
83
|
-
%w[
|
84
|
-
read_doc
|
85
|
-
write_doc
|
86
|
-
write_index_doc
|
87
|
-
destroy_doc
|
88
|
-
destroy_index_doc
|
89
|
-
query_docs
|
90
|
-
doc_range_values_of_hash_value
|
91
|
-
].each do |name|
|
92
|
-
define_method(name) do |*args|
|
93
|
-
raise "Method #{name} not yet implemented for #{self.class.name}!"
|
94
|
-
end
|
95
|
-
end
|
96
102
|
end
|
97
103
|
|
98
104
|
end; end # module Kvom::Adapter
|
@@ -1,19 +1,39 @@
|
|
1
|
+
require 'securerandom'
|
1
2
|
require 'kvom/adapter'
|
2
3
|
|
3
4
|
module Kvom; module Adapter
|
4
5
|
|
5
6
|
class Document
|
6
|
-
|
7
|
-
|
8
|
-
end
|
7
|
+
|
8
|
+
attr_reader :key, :persisted_revision
|
9
9
|
|
10
10
|
def [](name)
|
11
|
-
|
11
|
+
attributes[name]
|
12
12
|
end
|
13
13
|
|
14
14
|
def []=(name, value)
|
15
|
-
|
15
|
+
attributes[name] = value
|
16
16
|
end
|
17
|
+
|
18
|
+
def update_revision
|
19
|
+
revision_count = @persisted_revision ? @persisted_revision[17..-1].to_i : 0
|
20
|
+
attributes["@rev"] = "%s-%d" % [SecureRandom.hex(8), revision_count + 1]
|
21
|
+
yield
|
22
|
+
@persisted_revision = attributes["@rev"]
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
attr_reader :attributes
|
28
|
+
|
29
|
+
# TODO: from_db is obsolete as soon as all persisted models have got "@rev"
|
30
|
+
# (then, from_db <=> attributes.key?(@"rev")
|
31
|
+
def initialize(key, attributes, from_db = false)
|
32
|
+
@key = key
|
33
|
+
@attributes = Attributes.new(attributes, from_db)
|
34
|
+
@persisted_revision = from_db ? attributes["@rev"] : nil
|
35
|
+
end
|
36
|
+
|
17
37
|
end
|
18
38
|
|
19
39
|
end; end # module Kvom::Adapter
|
@@ -6,7 +6,7 @@ module Kvom; module Adapter
|
|
6
6
|
class DynamodbAdapter < Base
|
7
7
|
|
8
8
|
def new_document(key, attributes = nil)
|
9
|
-
DynamodbDocument.
|
9
|
+
DynamodbDocument.new(key, attributes, item_for_hash_and_range(key.first, key[1]))
|
10
10
|
end
|
11
11
|
|
12
12
|
private
|
@@ -15,12 +15,11 @@ class DynamodbAdapter < Base
|
|
15
15
|
item = item_for_hash_and_range(key.first, key[1])
|
16
16
|
attributes = item.attributes.to_h
|
17
17
|
attributes.empty? and raise NotFound.for_key(key)
|
18
|
-
DynamodbDocument.document_from_query(
|
18
|
+
DynamodbDocument.document_from_query(key, attributes, item)
|
19
19
|
end
|
20
20
|
|
21
21
|
def write_doc(doc)
|
22
|
-
|
23
|
-
doc.save
|
22
|
+
doc.write
|
24
23
|
end
|
25
24
|
|
26
25
|
def write_index_doc(key)
|
@@ -47,7 +46,7 @@ class DynamodbAdapter < Base
|
|
47
46
|
[]
|
48
47
|
else
|
49
48
|
key = key_from_item(item)
|
50
|
-
[DynamodbDocument.document_from_query(
|
49
|
+
[DynamodbDocument.document_from_query(key, attributes, item)]
|
51
50
|
end
|
52
51
|
when Range
|
53
52
|
dynamo_range_start = dynamo_range_value(range_value.begin)
|
@@ -61,7 +60,7 @@ class DynamodbAdapter < Base
|
|
61
60
|
table.items.query(query_options).map do |item_data|
|
62
61
|
item = item_data.item
|
63
62
|
key = key_from_item(item)
|
64
|
-
DynamodbDocument.document_from_query(item_data.attributes,
|
63
|
+
DynamodbDocument.document_from_query(key, item_data.attributes, item)
|
65
64
|
end
|
66
65
|
end
|
67
66
|
end
|
@@ -2,77 +2,44 @@ require 'kvom/adapter'
|
|
2
2
|
|
3
3
|
module Kvom; module Adapter
|
4
4
|
|
5
|
-
class DynamodbDocument <
|
5
|
+
class DynamodbDocument < Document
|
6
6
|
class << self
|
7
|
-
def
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
def document_from_query(item_attributes, key, item)
|
12
|
-
attributes = AttributesFromItemAttributes.from(item_attributes)
|
13
|
-
new(key, item, attributes, attributes["@rev"])
|
7
|
+
def document_from_query(key, item_attributes, item)
|
8
|
+
item_attributes.delete("hash_key")
|
9
|
+
item_attributes.delete("range_key")
|
10
|
+
new(key, item_attributes, item, true)
|
14
11
|
end
|
15
12
|
|
16
13
|
end
|
17
14
|
|
18
|
-
def initialize(key,
|
19
|
-
super(key, attributes)
|
15
|
+
def initialize(key, attributes, item, from_db = false)
|
20
16
|
@dynamo_item = item
|
21
|
-
|
22
|
-
@modified = @revision == nil
|
23
|
-
end
|
24
|
-
|
25
|
-
def []=(name, value)
|
26
|
-
case value
|
27
|
-
when String, Fixnum, nil
|
28
|
-
# okay ...
|
29
|
-
else
|
30
|
-
raise "Unsupported value type: #{value.class.name}"
|
31
|
-
end
|
32
|
-
unless self[name] == value
|
33
|
-
@modified = true
|
34
|
-
super
|
35
|
-
end
|
17
|
+
super(key, attributes, from_db)
|
36
18
|
end
|
37
19
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
when String, Fixnum, BigDecimal
|
49
|
-
memo[name] = value
|
50
|
-
else
|
51
|
-
raise unsupported_value(value)
|
52
|
-
end
|
53
|
-
memo
|
20
|
+
def write
|
21
|
+
dynamo_attributes = attributes.to_h.merge({
|
22
|
+
"hash_key" => @dynamo_item.hash_value,
|
23
|
+
"range_key" => @dynamo_item.range_value,
|
24
|
+
})
|
25
|
+
begin
|
26
|
+
# might be improved to saving only changed attributes
|
27
|
+
@dynamo_item.table.items.put(dynamo_attributes, revision_condition)
|
28
|
+
rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
|
29
|
+
raise Kvom::WriteConflict.wrap(e)
|
54
30
|
end
|
55
|
-
dynamo_attributes["hash_key"] = @dynamo_item.hash_value
|
56
|
-
dynamo_attributes["range_key"] = @dynamo_item.range_value
|
57
|
-
dynamo_attributes["@rev"] = new_revision = (@rev || 0) + 1
|
58
|
-
# might be improved to saving only changed attributes
|
59
|
-
@dynamo_item.table.items.put(dynamo_attributes, revision_condition)
|
60
|
-
@revision = new_revision
|
61
|
-
@modified = false
|
62
31
|
end
|
63
32
|
|
64
33
|
def destroy
|
65
34
|
@dynamo_item.delete(revision_condition)
|
35
|
+
rescue AWS::DynamoDB::Errors::ConditionalCheckFailedException => e
|
36
|
+
raise Kvom::WriteConflict.wrap(e)
|
66
37
|
end
|
67
38
|
|
68
39
|
private
|
69
40
|
|
70
|
-
def modified?
|
71
|
-
@modified
|
72
|
-
end
|
73
|
-
|
74
41
|
def revision_condition
|
75
|
-
dynamo_condition("@rev" =>
|
42
|
+
dynamo_condition("@rev" => persisted_revision)
|
76
43
|
end
|
77
44
|
|
78
45
|
def dynamo_condition(condition)
|
@@ -87,49 +54,10 @@ class DynamodbDocument < KeyAttributesDocument
|
|
87
54
|
end
|
88
55
|
options = {}
|
89
56
|
options[:if] = present_condition unless present_condition.empty?
|
90
|
-
options[:
|
57
|
+
options[:unless_exists] = missing_condition unless missing_condition.empty?
|
91
58
|
options
|
92
59
|
end
|
93
60
|
|
94
|
-
class AttributesFromItemAttributes
|
95
|
-
def self.from(attributes)
|
96
|
-
attributes.delete("hash_key")
|
97
|
-
attributes.delete("range_key")
|
98
|
-
meta = attributes.delete("@meta.json")
|
99
|
-
return attributes unless meta
|
100
|
-
meta = Lib::Json.load(meta)
|
101
|
-
coding = meta["coding"]
|
102
|
-
return attributes if !coding || coding.empty?
|
103
|
-
new(attributes, coding)
|
104
|
-
end
|
105
|
-
|
106
|
-
def initialize(attributes, coding)
|
107
|
-
@attributes = attributes
|
108
|
-
@coding = coding
|
109
|
-
end
|
110
|
-
|
111
|
-
def [](name)
|
112
|
-
value = @attributes[name]
|
113
|
-
coding = @coding[name]
|
114
|
-
return value unless coding
|
115
|
-
self[name] =
|
116
|
-
case coding
|
117
|
-
when "json"
|
118
|
-
Lib::Json.load(value)
|
119
|
-
when "jsonvalue"
|
120
|
-
Lib::JsonValue.load(value)
|
121
|
-
else
|
122
|
-
raise "Unexpected coding #{coding} for attribute #{name}"
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
def []=(name, value)
|
127
|
-
@coding.delete(name)
|
128
|
-
@attributes[name] = value
|
129
|
-
end
|
130
|
-
|
131
|
-
end
|
132
|
-
|
133
61
|
end
|
134
62
|
|
135
63
|
end; end # module Kvom::Adapter
|
@@ -11,7 +11,6 @@ class FilesystemAdapter < Base
|
|
11
11
|
EXT_OFFSET = -(EXT_SIZE + 1)
|
12
12
|
|
13
13
|
def new_document(key, attributes)
|
14
|
-
# there is no "was loaded from db" yet (e.g. for conditional put)
|
15
14
|
FilesystemDocument.new(key, attributes)
|
16
15
|
end
|
17
16
|
|
@@ -22,9 +21,9 @@ class FilesystemAdapter < Base
|
|
22
21
|
end
|
23
22
|
|
24
23
|
def write_doc(doc)
|
24
|
+
check_unmodified(doc)
|
25
25
|
path = filepath_for_document(doc)
|
26
26
|
FileUtils.mkdir_p(path.dirname)
|
27
|
-
# TODO: conditional put
|
28
27
|
File.open(path, "w") {|f| f.write(doc.to_json)}
|
29
28
|
end
|
30
29
|
|
@@ -35,7 +34,10 @@ class FilesystemAdapter < Base
|
|
35
34
|
end
|
36
35
|
|
37
36
|
def destroy_doc(doc)
|
38
|
-
|
37
|
+
check_unmodified(doc)
|
38
|
+
FileUtils.rm(filepath_for_document(doc))
|
39
|
+
rescue ::Errno::ENOENT => e
|
40
|
+
raise Kvom::WriteConflict.wrap(e)
|
39
41
|
end
|
40
42
|
|
41
43
|
def destroy_index_doc(key)
|
@@ -84,7 +86,7 @@ class FilesystemAdapter < Base
|
|
84
86
|
rescue ::Errno::ENOENT
|
85
87
|
nil
|
86
88
|
else
|
87
|
-
FilesystemDocument.new(key, Lib::Json.load(serialized_doc))
|
89
|
+
FilesystemDocument.new(key, Lib::Json.load(serialized_doc), true)
|
88
90
|
end
|
89
91
|
|
90
92
|
def filepath_for_document(doc)
|
@@ -153,6 +155,14 @@ class FilesystemAdapter < Base
|
|
153
155
|
entry[-EXT_SIZE..-1] == EXT
|
154
156
|
end
|
155
157
|
end
|
158
|
+
|
159
|
+
def check_unmodified(doc)
|
160
|
+
current = document_for_key(doc.key)
|
161
|
+
current_revision = current && current.persisted_revision
|
162
|
+
unless current_revision == doc.persisted_revision
|
163
|
+
raise Kvom::WriteConflict
|
164
|
+
end
|
165
|
+
end
|
156
166
|
end
|
157
167
|
|
158
168
|
end; end # module Kvom::Adapter
|
@@ -2,19 +2,9 @@ require 'kvom/adapter'
|
|
2
2
|
|
3
3
|
module Kvom; module Adapter
|
4
4
|
|
5
|
-
class FilesystemDocument <
|
5
|
+
class FilesystemDocument < Document
|
6
6
|
def to_json
|
7
|
-
|
8
|
-
attributes.each do |key, value|
|
9
|
-
case value
|
10
|
-
when ""
|
11
|
-
raise unsupported_value(value)
|
12
|
-
when nil, String, Fixnum # BigDecimal is not standard ruby
|
13
|
-
else
|
14
|
-
raise unsupported_value(value)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
Lib::Json.dump(attributes)
|
7
|
+
Lib::Json.dump(attributes.to_h)
|
18
8
|
end
|
19
9
|
end
|
20
10
|
|
data/lib/kvom/model/base.rb
CHANGED
@@ -13,7 +13,6 @@ class Base
|
|
13
13
|
extend ActiveModel::Naming
|
14
14
|
|
15
15
|
|
16
|
-
|
17
16
|
class_attribute :key_prefix
|
18
17
|
|
19
18
|
class << self
|
@@ -73,6 +72,10 @@ class Base
|
|
73
72
|
|
74
73
|
end
|
75
74
|
|
75
|
+
# Currently, the caller is responsible to not leak non-property
|
76
|
+
# attributes into the (persisted!) model by using the mass assignment
|
77
|
+
# available by new(). A filter would require to remember the properties
|
78
|
+
# (through a class inheritable attribute)
|
76
79
|
def initialize(doc_or_attrs = {})
|
77
80
|
@document =
|
78
81
|
case doc_or_attrs
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'kvom'
|
2
|
+
|
3
|
+
class Kvom::WriteConflict < Kvom::Conflict
|
4
|
+
|
5
|
+
attr_reader :cause
|
6
|
+
|
7
|
+
def self.wrap(cause)
|
8
|
+
new.tap do
|
9
|
+
new.cause = cause
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def cause=(ex)
|
14
|
+
@cause and raise "Cannot set cause twice"
|
15
|
+
@cause = ex
|
16
|
+
end
|
17
|
+
end
|
data/script/cleanup
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
Bundler.setup
|
7
|
+
|
8
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
9
|
+
require 'kvom'
|
10
|
+
require File.expand_path("../../spec/support/adapter", __FILE__)
|
11
|
+
|
12
|
+
module KvomGarbageCollector
|
13
|
+
|
14
|
+
def self.cleanup(type)
|
15
|
+
adapter = AdapterForSpec.adapter_for_type(type)
|
16
|
+
case adapter.class.name
|
17
|
+
when /FilesystemAdapter/
|
18
|
+
Filesystem
|
19
|
+
when /Dynamo/
|
20
|
+
Dynamo
|
21
|
+
else
|
22
|
+
raise "Cleanup aborted (unexpected adapter class #{adapter.class})"
|
23
|
+
end.cleanup(adapter)
|
24
|
+
end
|
25
|
+
|
26
|
+
module Filesystem
|
27
|
+
|
28
|
+
def self.cleanup(adapter)
|
29
|
+
require 'fileutils'
|
30
|
+
dir = adapter.__send__(:base_dir)
|
31
|
+
dir.to_s.include?("tmp") or raise "Unexpected filesystem adapter directory: #{dir}"
|
32
|
+
FileUtils.rm_rf(dir)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
module Dynamo
|
38
|
+
|
39
|
+
def self.cleanup(adapter)
|
40
|
+
now = Time.now.strftime("%Y%m%d%H%M%S")
|
41
|
+
statistic = Hash.new(0)
|
42
|
+
item_collection = adapter.__send__(:table).items
|
43
|
+
items = item_collection.to_a
|
44
|
+
sleep 5
|
45
|
+
item_collection.select(:hash_key, :range_key, :tfc) do |item_data|
|
46
|
+
item = item_data.item
|
47
|
+
if items.include?(item)
|
48
|
+
type = "skipped"
|
49
|
+
time_for_cleanup = item_data.attributes["tfc"]
|
50
|
+
case time_for_cleanup
|
51
|
+
when nil
|
52
|
+
# legacy
|
53
|
+
item_collection.put({
|
54
|
+
:hash_key => item.hash_value,
|
55
|
+
:range_key => item.range_value,
|
56
|
+
:tfc => (Time.now + 2 * 60 * 60).strftime("%Y%m%d%H%M%S"),
|
57
|
+
})
|
58
|
+
type = "marked"
|
59
|
+
else
|
60
|
+
if time_for_cleanup < now
|
61
|
+
item.delete
|
62
|
+
type = "removed"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
statistic[type] += 1
|
66
|
+
else
|
67
|
+
type = "vanished"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
actions = statistic.keys.sort.reverse.map {|type| "#{type}: #{statistic[type]}"}.join(", ")
|
71
|
+
puts "Dynamo cleanup: #{statistic.values.inject(0) {|sum, i| sum + i}} items (#{actions})"
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
KvomGarbageCollector.cleanup(ARGV.first || ENV['KVOM_ADAPTER_TYPE'] || "file")
|
data/spec/model/all_ids_spec.rb
CHANGED
@@ -21,8 +21,12 @@ describe Kvom::Model::Base, "with all-index enabled" do
|
|
21
21
|
|
22
22
|
it "returns ids for which a model can be fetched" do
|
23
23
|
all_ids = TestModelWithIndex.all_ids
|
24
|
+
erroneous_ids = {}
|
24
25
|
all_ids.each do |model_id|
|
25
|
-
|
26
|
+
TestModelWithIndex.find(model_id).tap do |model|
|
27
|
+
model.should be
|
28
|
+
model.should be_kind_of(TestModelWithIndex)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
end
|
28
32
|
end
|
data/spec/model/base_spec.rb
CHANGED
@@ -1,89 +1,226 @@
|
|
1
|
+
# encoding: utf-8
|
1
2
|
require 'spec_helper'
|
2
3
|
|
3
4
|
describe Kvom::Model::Base do
|
5
|
+
|
4
6
|
it "should complain when no adapter is specified" do
|
5
7
|
expect { ModelWithoutAdapter.find("spam") }.to raise_error(/overwritten/)
|
6
8
|
end
|
7
9
|
|
8
10
|
describe "getters and setters" do
|
9
11
|
it "work for freshly created models" do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
12
|
+
model = ExampleModel.create(:spam => "foo")
|
13
|
+
model.spam.should == "foo"
|
14
|
+
model.spam = "bar"
|
15
|
+
model.spam.should == "bar"
|
14
16
|
end
|
15
17
|
|
16
|
-
it "
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
it "handles access via string as well as symbols" do
|
19
|
+
model = ExampleModel.create(:spam => "foo")
|
20
|
+
model.spam.should == "foo"
|
21
|
+
model = ExampleModel.create("spam" => "foo")
|
22
|
+
model.spam.should == "foo"
|
21
23
|
end
|
22
24
|
|
23
25
|
it "works for models loaded from database" do
|
24
|
-
|
25
|
-
|
26
|
+
model = ExampleModel.create(:spam => "foo")
|
27
|
+
model.save
|
26
28
|
|
27
|
-
loaded = ExampleModel.find(
|
29
|
+
loaded = ExampleModel.find(model.id)
|
28
30
|
loaded.spam.should == "foo"
|
29
31
|
end
|
30
32
|
|
31
|
-
context "when setting
|
33
|
+
context "when setting" do
|
34
|
+
def raise_unsupported_value(*message)
|
35
|
+
raise_error(Kvom::Adapter::UnsupportedValue, *message)
|
36
|
+
end
|
32
37
|
|
33
|
-
context "
|
34
|
-
|
35
|
-
|
38
|
+
context "an unsupported property value" do
|
39
|
+
context "when created" do
|
40
|
+
it "raises a Kvom::Adapter::UnsupportedValue error" do
|
41
|
+
expect {ExampleModel.create(:spam => Object.new)}.to raise_unsupported_value
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "when saved" do
|
46
|
+
let(:model) {ExampleModel.create(:spam => "not empty")}
|
47
|
+
|
48
|
+
it "raises a Kvom::Adapter::UnsupportedValue error" do
|
49
|
+
model.spam = Object.new
|
50
|
+
expect {
|
51
|
+
model.save
|
52
|
+
}.to raise_unsupported_value
|
53
|
+
end
|
36
54
|
end
|
37
55
|
end
|
38
56
|
|
39
|
-
|
40
|
-
|
57
|
+
shared_examples_for "setting a supported property value" do |value|
|
58
|
+
it "can persist and return this value" do
|
59
|
+
model = ExampleModel.create(:spam => value)
|
60
|
+
model.spam.should == value
|
61
|
+
ExampleModel.find(model.id).spam.should == value
|
62
|
+
end
|
63
|
+
end
|
41
64
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
65
|
+
# shared_examples_for "setting an unsupported property value" do |value, message|
|
66
|
+
# it "raises a Kvom::Adapter::UnsupportedValue" do
|
67
|
+
# expect {
|
68
|
+
# ExampleModel.create(:spam => value)
|
69
|
+
# }.to raise_unsupported_value(message + message)
|
70
|
+
|
71
|
+
# model = ExampleModel.new
|
72
|
+
# expect {
|
73
|
+
# model.spam = value
|
74
|
+
# model.save
|
75
|
+
# }.to raise_unsupported_value(message)
|
76
|
+
# end
|
77
|
+
# end
|
78
|
+
|
79
|
+
context "the supported value type Fixnum" do
|
80
|
+
it_should_behave_like "setting a supported property value", 2
|
81
|
+
end
|
82
|
+
|
83
|
+
context "the supported value type String" do
|
84
|
+
context 'and the value is ""' do
|
85
|
+
it_should_behave_like "setting a supported property value", ""
|
47
86
|
end
|
48
87
|
|
88
|
+
context 'and the value is not ""' do
|
89
|
+
it_should_behave_like "setting a supported property value", " "
|
90
|
+
it_should_behave_like "setting a supported property value", "not empty"
|
91
|
+
end
|
49
92
|
end
|
50
93
|
|
94
|
+
context "the supported value type Hash" do
|
95
|
+
context "containing jsonable values" do
|
96
|
+
it_should_behave_like "setting a supported property value", {
|
97
|
+
"hash" => "is",
|
98
|
+
"also" => "supported",
|
99
|
+
"true" => true,
|
100
|
+
"false" => false,
|
101
|
+
"nil" => nil,
|
102
|
+
"array" => %w[a r r a y],
|
103
|
+
"number" => 3,
|
104
|
+
"nested hash" => {
|
105
|
+
"deep" => "nested",
|
106
|
+
},
|
107
|
+
}
|
108
|
+
end
|
109
|
+
|
110
|
+
# No spec: explicitely undefined behaviour
|
111
|
+
# context "containing a non jsonable value" do
|
112
|
+
# it_should_behave_like "setting an unsupported property value", {"object" => Object.new},
|
113
|
+
# "Unsupported value type: Object"
|
114
|
+
# end
|
115
|
+
end
|
116
|
+
|
117
|
+
context "the supported value type Array" do
|
118
|
+
context "containing jsonable values" do
|
119
|
+
it_should_behave_like "setting a supported property value", [
|
120
|
+
"hash" => "is",
|
121
|
+
"also" => "supported",
|
122
|
+
"true" => true,
|
123
|
+
"false" => false,
|
124
|
+
"nil" => nil,
|
125
|
+
"array" => %w[a r r a y],
|
126
|
+
"number" => 3,
|
127
|
+
"nested hash" => {
|
128
|
+
"deep" => "nested",
|
129
|
+
},
|
130
|
+
]
|
131
|
+
end
|
132
|
+
|
133
|
+
# No spec: explicitely undefined behaviour
|
134
|
+
# context "containing a non jsonable value" do
|
135
|
+
# it_should_behave_like "setting an unsupported property value", [Object.new],
|
136
|
+
# "Unsupported value type: Object"
|
137
|
+
# end
|
138
|
+
end
|
51
139
|
end
|
52
140
|
|
53
|
-
context "
|
54
|
-
|
55
|
-
|
141
|
+
context "one property with changing types of value" do
|
142
|
+
|
143
|
+
KVOM_PROPERTY_VALUES = [
|
144
|
+
"string",
|
145
|
+
{"ha" => "sh"},
|
146
|
+
[],
|
147
|
+
true,
|
148
|
+
%w[a r r a y],
|
149
|
+
"",
|
150
|
+
{"ha" => "sh"},
|
151
|
+
nil,
|
152
|
+
false,
|
153
|
+
]
|
154
|
+
|
155
|
+
KVOM_PROPERTY_VALUE_TRANSITIONS = (
|
156
|
+
current = nil
|
157
|
+
transitions = []
|
158
|
+
KVOM_PROPERTY_VALUES.each do |v|
|
159
|
+
transitions << [current, v]
|
160
|
+
current = v
|
161
|
+
end
|
162
|
+
transitions
|
163
|
+
)
|
164
|
+
|
165
|
+
context "for a new model (never persisted)" do
|
56
166
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
167
|
+
let(:new_model) {ExampleModel.new}
|
168
|
+
|
169
|
+
it "always returns the current value" do
|
170
|
+
KVOM_PROPERTY_VALUES.each do |value|
|
171
|
+
expect {
|
172
|
+
expect {
|
173
|
+
new_model.spam = value
|
174
|
+
}.to change(new_model, :spam).to(value)
|
175
|
+
}.to_not change(new_model, :new?)
|
176
|
+
end
|
62
177
|
end
|
63
178
|
|
64
179
|
end
|
65
180
|
|
181
|
+
KVOM_PROPERTY_VALUE_TRANSITIONS.each do |(before, after)|
|
182
|
+
context "for a persisted model with value #{before.inspect}" do
|
183
|
+
context "for the persisted model itself" do
|
184
|
+
let(:model_just_persisted) {ExampleModel.create(:spam => before)}
|
185
|
+
|
186
|
+
it "it returns the current value" do
|
187
|
+
expect {
|
188
|
+
model_just_persisted.spam = after
|
189
|
+
}.to change(model_just_persisted, :spam).to(after)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context "when the model is fresh loaded" do
|
194
|
+
let(:loaded_model) {ExampleModel.find(ExampleModel.create(:spam => before).id)}
|
195
|
+
|
196
|
+
it "it returns the current value" do
|
197
|
+
expect {
|
198
|
+
loaded_model.spam = after
|
199
|
+
}.to change(loaded_model, :spam).to(after)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
66
204
|
end
|
67
|
-
|
68
205
|
end
|
69
206
|
|
70
207
|
describe "#save" do
|
71
208
|
|
72
209
|
context "when loaded from database" do
|
73
210
|
|
74
|
-
let(:
|
75
|
-
let(:
|
211
|
+
let(:model_id) {ExampleModel.create(:spam => "ham").id}
|
212
|
+
let(:model_from_db) {ExampleModel.find(model_id)}
|
76
213
|
|
77
214
|
context "when modified" do
|
78
215
|
before do
|
79
|
-
|
216
|
+
model_from_db.spam = "and eggs"
|
80
217
|
end
|
81
218
|
|
82
219
|
it "persists the new value to the database" do
|
83
220
|
expect {
|
84
|
-
|
221
|
+
model_from_db.save
|
85
222
|
}.to change {
|
86
|
-
ExampleModel.find(
|
223
|
+
ExampleModel.find(model_id).spam
|
87
224
|
}.to("and eggs")
|
88
225
|
end
|
89
226
|
|
@@ -114,9 +251,9 @@ describe Kvom::Model::Base do
|
|
114
251
|
end
|
115
252
|
|
116
253
|
it "should find model instances" do
|
117
|
-
|
118
|
-
ExampleModel.find(
|
119
|
-
ExampleModel.find(
|
254
|
+
model = ExampleModel.create(:spam => "foo")
|
255
|
+
ExampleModel.find(model.id).id.should == model.id
|
256
|
+
ExampleModel.find(model.id).spam.should == "foo"
|
120
257
|
end
|
121
258
|
|
122
259
|
it "should raise exception when instances cannot be found" do
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Kvom::Model::Base do
|
4
|
+
|
5
|
+
describe "#save" do
|
6
|
+
|
7
|
+
let(:model) {ExampleModel.create(:spam => "foo")}
|
8
|
+
|
9
|
+
before do
|
10
|
+
model
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when the model has been changed concurrently" do
|
14
|
+
before do
|
15
|
+
Process.waitpid(fork {
|
16
|
+
model.spam = "changed"
|
17
|
+
model.save
|
18
|
+
})
|
19
|
+
end
|
20
|
+
|
21
|
+
it "raises a Kvom::WriteConflict error" do
|
22
|
+
expect {
|
23
|
+
model.save
|
24
|
+
}.to raise_error(Kvom::WriteConflict)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when the model has been deleted concurrently" do
|
30
|
+
before do
|
31
|
+
Process.waitpid(fork {
|
32
|
+
model.destroy
|
33
|
+
})
|
34
|
+
end
|
35
|
+
|
36
|
+
it "raises a Kvom::WriteConflict error" do
|
37
|
+
expect {
|
38
|
+
model.save
|
39
|
+
}.to raise_error(Kvom::WriteConflict)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#destroy" do
|
47
|
+
|
48
|
+
let(:model) {ExampleModel.create(:spam => "foo")}
|
49
|
+
|
50
|
+
before do
|
51
|
+
model
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when the model has been changed concurrently" do
|
55
|
+
before do
|
56
|
+
Process.waitpid(fork {
|
57
|
+
model.spam = "changed"
|
58
|
+
model.save
|
59
|
+
})
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raises a Kvom::WriteConflict error" do
|
63
|
+
expect {
|
64
|
+
model.destroy
|
65
|
+
}.to raise_error(Kvom::WriteConflict)
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
context "when the model has been deleted concurrently" do
|
71
|
+
before do
|
72
|
+
Process.waitpid(fork {
|
73
|
+
model.destroy
|
74
|
+
})
|
75
|
+
end
|
76
|
+
|
77
|
+
it "raises a Kvom::WriteConflict error" do
|
78
|
+
expect {
|
79
|
+
model.destroy
|
80
|
+
}.to raise_error(Kvom::WriteConflict)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
describe ".create" do
|
88
|
+
|
89
|
+
before do
|
90
|
+
@model_id = "published-" + SecureRandom.hex(8)
|
91
|
+
end
|
92
|
+
|
93
|
+
context "when the model has been created concurrently" do
|
94
|
+
before do
|
95
|
+
Process.waitpid(fork {
|
96
|
+
ExampleModel.create(:id => @model_id)
|
97
|
+
})
|
98
|
+
end
|
99
|
+
|
100
|
+
it "raises a Kvom::WriteConflict error" do
|
101
|
+
expect {
|
102
|
+
ExampleModel.create(:id => @model_id)
|
103
|
+
}.to raise_error(Kvom::WriteConflict)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
module AdapterForSpec
|
2
|
+
def self.adapter
|
3
|
+
@adapter ||= adapter_for_type(ENV['KVOM_ADAPTER_TYPE'] || "file")
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.adapter_for_type(type)
|
7
|
+
case type
|
8
|
+
when /dynamo/
|
9
|
+
require File.expand_path("../../../../tasks/support/local_config", __FILE__)
|
10
|
+
options = {
|
11
|
+
:table => "test-kvom",
|
12
|
+
:access_key_id => local_config["aws_access_key_id"],
|
13
|
+
:secret_access_key => local_config["aws_secret_access_key"],
|
14
|
+
}
|
15
|
+
options[:partition] = "korb" if type == "partitioned_dynamo"
|
16
|
+
Kvom::Adapter::DynamodbAdapter.new(options)
|
17
|
+
when /file/
|
18
|
+
Kvom::Adapter::FilesystemAdapter.new(:path => File.expand_path("../../../tmp/fsa", __FILE__))
|
19
|
+
else
|
20
|
+
raise "Unexpected kvom adapter type specification #{type}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/support/model.rb
CHANGED
@@ -1,19 +1,6 @@
|
|
1
1
|
class TestModel < Kvom::Model::Base
|
2
2
|
def self.adapter
|
3
|
-
@adapter ||=
|
4
|
-
case (type = ENV['KVOM_ADAPTER_TYPE'] || "file")
|
5
|
-
when /dynamo/
|
6
|
-
require File.expand_path("../../../../tasks/support/local_config", __FILE__)
|
7
|
-
options = {
|
8
|
-
:table => "test-kvom",
|
9
|
-
:access_key_id => local_config["aws_access_key_id"],
|
10
|
-
:secret_access_key => local_config["aws_secret_access_key"],
|
11
|
-
}
|
12
|
-
options[:partition] = "korb" if type == "partitioned_dynamo"
|
13
|
-
Kvom::Adapter::DynamodbAdapter.new(options)
|
14
|
-
when /file/
|
15
|
-
Kvom::Adapter::FilesystemAdapter.new(:path => File.expand_path("../../../tmp/fsa", __FILE__))
|
16
|
-
end
|
3
|
+
@adapter ||= AdapterForSpec.adapter
|
17
4
|
end
|
18
5
|
end
|
19
6
|
|
@@ -28,10 +15,15 @@ end
|
|
28
15
|
class ModelWithoutAdapter < Kvom::Model::Base
|
29
16
|
end
|
30
17
|
|
18
|
+
require 'securerandom'
|
31
19
|
class TestModelWithIndex < TestModel
|
32
20
|
has_all_ids
|
33
21
|
|
34
22
|
property :something
|
23
|
+
|
24
|
+
# not required to be set, but keeps cleanup and test separated
|
25
|
+
$test_model_with_index_key_prefix ||= "#{key_prefix}-#{SecureRandom.hex(8)}"
|
26
|
+
self.key_prefix = $test_model_with_index_key_prefix
|
35
27
|
end
|
36
28
|
|
37
29
|
class OtherModelWithIndex < TestModel
|
data/spec/support/test_ids.rb
CHANGED
@@ -3,7 +3,7 @@ module TestIds
|
|
3
3
|
# - returns a different id when running the specs again
|
4
4
|
# => no id collision when running the tests on a reused and not emptied database
|
5
5
|
def self.volatile_id(id)
|
6
|
-
"#{@discriminator ||= SecureRandom.hex(8)}#{id}"
|
6
|
+
"#{@discriminator ||= Time.now.strftime("%Y%m%d%H%M%S-") + SecureRandom.hex(8)}#{id}"
|
7
7
|
end
|
8
8
|
|
9
9
|
def volatile_id(id)
|
metadata
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kvom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash: -
|
5
|
-
prerelease:
|
4
|
+
hash: -268426508
|
5
|
+
prerelease: 10
|
6
6
|
segments:
|
7
7
|
- 6
|
8
8
|
- 8
|
9
9
|
- 0
|
10
|
-
-
|
11
|
-
-
|
10
|
+
- 210
|
11
|
+
- ed
|
12
|
+
- 204
|
12
13
|
- b
|
13
|
-
-
|
14
|
-
version: 6.8.0.
|
14
|
+
- 0
|
15
|
+
version: 6.8.0.210.ed204b0
|
15
16
|
platform: ruby
|
16
17
|
authors:
|
17
18
|
- Kristian Hanekamp, Infopark AG
|
@@ -19,7 +20,7 @@ autorequire:
|
|
19
20
|
bindir: bin
|
20
21
|
cert_chain: []
|
21
22
|
|
22
|
-
date: 2012-
|
23
|
+
date: 2012-11-06 00:00:00 +01:00
|
23
24
|
default_executable:
|
24
25
|
dependencies:
|
25
26
|
- !ruby/object:Gem::Dependency
|
@@ -137,13 +138,14 @@ files:
|
|
137
138
|
- kvom.gemspec
|
138
139
|
- lib/kvom.rb
|
139
140
|
- lib/kvom/adapter.rb
|
141
|
+
- lib/kvom/adapter/attributes.rb
|
140
142
|
- lib/kvom/adapter/base.rb
|
141
143
|
- lib/kvom/adapter/document.rb
|
142
144
|
- lib/kvom/adapter/dynamodb_adapter.rb
|
143
145
|
- lib/kvom/adapter/dynamodb_document.rb
|
144
146
|
- lib/kvom/adapter/filesystem_adapter.rb
|
145
147
|
- lib/kvom/adapter/filesystem_document.rb
|
146
|
-
- lib/kvom/
|
148
|
+
- lib/kvom/conflict.rb
|
147
149
|
- lib/kvom/lib.rb
|
148
150
|
- lib/kvom/lib/json.rb
|
149
151
|
- lib/kvom/lib/json_value.rb
|
@@ -152,13 +154,14 @@ files:
|
|
152
154
|
- lib/kvom/model/feature.rb
|
153
155
|
- lib/kvom/model/feature/all_ids.rb
|
154
156
|
- lib/kvom/model_identity.rb
|
155
|
-
- lib/kvom/not_found.rb
|
156
157
|
- lib/kvom/storage.rb
|
157
158
|
- lib/kvom/storage/base.rb
|
158
159
|
- lib/kvom/storage/cache_with_prefix.rb
|
159
160
|
- lib/kvom/storage/file_system_storage.rb
|
160
161
|
- lib/kvom/storage/not_found.rb
|
161
162
|
- lib/kvom/storage/s3_storage.rb
|
163
|
+
- lib/kvom/write_conflict.rb
|
164
|
+
- script/cleanup
|
162
165
|
- spec/adapter/counter_spec.rb
|
163
166
|
- spec/adapter/dynamodb_adapter_spec.rb
|
164
167
|
- spec/adapter/model_identity_spec.rb
|
@@ -166,9 +169,11 @@ files:
|
|
166
169
|
- spec/lib/json_spec.rb
|
167
170
|
- spec/model/all_ids_spec.rb
|
168
171
|
- spec/model/base_spec.rb
|
172
|
+
- spec/model/conflict_spec.rb
|
169
173
|
- spec/spec_helper.rb
|
170
174
|
- spec/storage/file_system_spec.rb
|
171
175
|
- spec/storage/s3_spec.rb
|
176
|
+
- spec/support/adapter.rb
|
172
177
|
- spec/support/model.rb
|
173
178
|
- spec/support/test_ids.rb
|
174
179
|
- tmp/.gitignore
|
@@ -216,8 +221,10 @@ test_files:
|
|
216
221
|
- spec/lib/json_spec.rb
|
217
222
|
- spec/model/all_ids_spec.rb
|
218
223
|
- spec/model/base_spec.rb
|
224
|
+
- spec/model/conflict_spec.rb
|
219
225
|
- spec/spec_helper.rb
|
220
226
|
- spec/storage/file_system_spec.rb
|
221
227
|
- spec/storage/s3_spec.rb
|
228
|
+
- spec/support/adapter.rb
|
222
229
|
- spec/support/model.rb
|
223
230
|
- spec/support/test_ids.rb
|
@@ -1,36 +0,0 @@
|
|
1
|
-
require 'kvom/adapter'
|
2
|
-
|
3
|
-
# :enddoc:
|
4
|
-
module Kvom; module Adapter;
|
5
|
-
|
6
|
-
class KeyAttributesDocument < Document
|
7
|
-
|
8
|
-
attr_reader :key, :attributes
|
9
|
-
|
10
|
-
def initialize(key, attributes)
|
11
|
-
@key = key
|
12
|
-
@attributes = attributes || {}
|
13
|
-
end
|
14
|
-
|
15
|
-
def [](name)
|
16
|
-
@attributes[name]
|
17
|
-
end
|
18
|
-
|
19
|
-
def []=(name, value)
|
20
|
-
@attributes[name] = value
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def unsupported_value(value)
|
26
|
-
case value
|
27
|
-
when ""
|
28
|
-
"Unsupported value: empty string"
|
29
|
-
else
|
30
|
-
"Unsupported value type: #{value.class.to_s}"
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
end
|
35
|
-
|
36
|
-
end; end # module Kvom::Adapter
|