kvom 6.8.0.23.da7f96b → 6.8.0.72.d18d096
Sign up to get free protection for your applications and to get access to all the features.
- data/kvom.gemspec +2 -0
- data/lib/kvom/adapter/base.rb +61 -10
- data/lib/kvom/{document.rb → adapter/document.rb} +3 -3
- data/lib/kvom/adapter/dynamodb_adapter.rb +56 -43
- data/lib/kvom/adapter/filesystem_adapter.rb +68 -40
- data/lib/kvom/model.rb +5 -0
- data/lib/kvom/{base.rb → model/base.rb} +22 -10
- data/lib/kvom/model/feature.rb +5 -0
- data/lib/kvom/model/feature/all_ids.rb +30 -0
- data/lib/kvom/not_found.rb +1 -1
- data/spec/{adaptor → adapter}/counter_spec.rb +5 -1
- data/spec/{adaptor/dynamodb_adaptor_spec.rb → adapter/dynamodb_adapter_spec.rb} +0 -0
- data/spec/{adaptor → adapter}/model_identity_spec.rb +0 -0
- data/spec/model/all_ids_spec.rb +72 -0
- data/spec/{adaptor → model}/base_spec.rb +30 -45
- data/spec/spec_helper.rb +3 -23
- data/spec/support/model.rb +41 -0
- data/spec/support/test_ids.rb +17 -0
- metadata +54 -32
data/kvom.gemspec
CHANGED
@@ -20,10 +20,12 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
21
|
s.require_paths = ["lib"]
|
22
22
|
|
23
|
+
|
23
24
|
s.add_runtime_dependency "multi_json", "~> 1"
|
24
25
|
s.add_runtime_dependency "activesupport", ">= 3" # does not support rational versioning, does it?
|
25
26
|
s.add_runtime_dependency "activemodel", ">= 3" # adapters
|
26
27
|
|
28
|
+
s.add_development_dependency "rake"
|
27
29
|
s.add_development_dependency "rspec", "~> 2.8"
|
28
30
|
s.add_development_dependency "helpful_configuration"
|
29
31
|
s.add_development_dependency "yajl-ruby" # a specific json provider
|
data/lib/kvom/adapter/base.rb
CHANGED
@@ -17,31 +17,82 @@ class Base
|
|
17
17
|
raise "implement me in subclass!"
|
18
18
|
end
|
19
19
|
|
20
|
+
def get(key)
|
21
|
+
count_document_request do
|
22
|
+
read_doc(key)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
20
26
|
def save(doc)
|
21
|
-
|
27
|
+
count_request do
|
28
|
+
write_doc(doc)
|
29
|
+
end
|
22
30
|
end
|
23
31
|
|
24
|
-
def
|
25
|
-
|
32
|
+
def save_index(key)
|
33
|
+
count_request do
|
34
|
+
write_index_doc(key)
|
35
|
+
end
|
26
36
|
end
|
27
37
|
|
28
38
|
def destroy(doc)
|
29
|
-
|
39
|
+
count_request do
|
40
|
+
destroy_doc(doc)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def destroy_index(key)
|
45
|
+
count_request do
|
46
|
+
destroy_index_doc(key)
|
47
|
+
end
|
30
48
|
end
|
31
49
|
|
32
50
|
def query(hash_value, range_value)
|
33
|
-
|
51
|
+
count_document_request do
|
52
|
+
query_docs(hash_value, range_value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def range_values_of_hash_value(hash_value)
|
57
|
+
count_request do
|
58
|
+
doc_range_values_of_hash_value(hash_value)
|
59
|
+
end
|
34
60
|
end
|
35
61
|
|
36
|
-
|
62
|
+
private
|
37
63
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
64
|
+
def count_document_request
|
65
|
+
result = count_request {yield}
|
66
|
+
@documents_loaded_counter +=
|
67
|
+
case result
|
68
|
+
when nil
|
69
|
+
0
|
70
|
+
when Array
|
71
|
+
result.size
|
72
|
+
else
|
73
|
+
1
|
74
|
+
end
|
42
75
|
result
|
43
76
|
end
|
44
77
|
|
78
|
+
def count_request
|
79
|
+
@request_counter += 1
|
80
|
+
yield
|
81
|
+
end
|
82
|
+
|
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
|
45
96
|
end
|
46
97
|
|
47
98
|
end; end # module Kvom::Adapter
|
@@ -6,52 +6,69 @@ module Kvom; module Adapter
|
|
6
6
|
class DynamodbAdapter < Base
|
7
7
|
|
8
8
|
def new_document(key, attributes = nil)
|
9
|
-
DynamodbDocument.new_document(attributes, key,
|
9
|
+
DynamodbDocument.new_document(attributes, key, item_for_hash_and_range(key.first, key[1]))
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
private
|
13
|
+
|
14
|
+
def read_doc(key)
|
15
|
+
item = item_for_hash_and_range(key.first, key[1])
|
16
|
+
attributes = item.attributes.to_h
|
17
|
+
attributes.empty? and raise NotFound.for_key(key)
|
18
|
+
DynamodbDocument.document_from_query(attributes, key, item)
|
18
19
|
end
|
19
20
|
|
20
|
-
def
|
21
|
+
def write_doc(doc)
|
22
|
+
# doc is only saved if modified - might be one request more than sent to Dynamo
|
21
23
|
doc.save
|
22
24
|
end
|
23
25
|
|
24
|
-
def
|
26
|
+
def write_index_doc(key)
|
27
|
+
item = item_for_hash_and_range(key.first, key[1])
|
28
|
+
table.items.put(:hash_key => item.hash_value, :range_key => item.range_value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def destroy_doc(doc)
|
25
32
|
doc.destroy
|
26
33
|
end
|
27
34
|
|
28
|
-
def
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
query_options = {
|
45
|
-
:hash_value => partitioned_hash_value(hash_value),
|
46
|
-
:range_value => Range.new(dynamo_range_start, dynamo_range_end),
|
47
|
-
:select => :all,
|
48
|
-
}
|
49
|
-
table.items.query(query_options).map do |item_data|
|
50
|
-
item = item_data.item
|
51
|
-
key = key_from_item(item)
|
52
|
-
DynamodbDocument.document_from_query(item_data.attributes, key, item)
|
53
|
-
end
|
35
|
+
def destroy_index_doc(key)
|
36
|
+
item = item_for_hash_and_range(key.first, key[1])
|
37
|
+
item.delete
|
38
|
+
end
|
39
|
+
|
40
|
+
def query_docs(hash_value, range_value)
|
41
|
+
raise "Unsupported range value <empty string>" if range_value == ""
|
42
|
+
case range_value
|
43
|
+
when nil, String
|
44
|
+
item = item_for_hash_and_range(hash_value, range_value)
|
45
|
+
attributes = item.attributes.to_h
|
46
|
+
if attributes.empty?
|
47
|
+
[]
|
48
|
+
else
|
49
|
+
key = key_from_item(item)
|
50
|
+
[DynamodbDocument.document_from_query(attributes, key, item)]
|
54
51
|
end
|
52
|
+
when Range
|
53
|
+
dynamo_range_start = dynamo_range_value(range_value.begin)
|
54
|
+
dynamo_range_end = dynamo_range_value(range_value.end)
|
55
|
+
|
56
|
+
query_options = {
|
57
|
+
:hash_value => partitioned_hash_value(hash_value),
|
58
|
+
:range_value => Range.new(dynamo_range_start, dynamo_range_end),
|
59
|
+
:select => :all,
|
60
|
+
}
|
61
|
+
table.items.query(query_options).map do |item_data|
|
62
|
+
item = item_data.item
|
63
|
+
key = key_from_item(item)
|
64
|
+
DynamodbDocument.document_from_query(item_data.attributes, key, item)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def doc_range_values_of_hash_value(hash_value)
|
70
|
+
table.items.query(:hash_value => partitioned_hash_value(hash_value)).map do |item|
|
71
|
+
key_from_item(item).last
|
55
72
|
end
|
56
73
|
end
|
57
74
|
|
@@ -75,13 +92,9 @@ class DynamodbAdapter < Base
|
|
75
92
|
|
76
93
|
def key_from_item(item)
|
77
94
|
hash_value, range_value = item.hash_value, item.range_value
|
78
|
-
range_value =
|
95
|
+
range_value = nil if range_value == " " # range_from_dynamo_range
|
79
96
|
hash_value = hash_value[(partition.length + 1)..-1] if partition # unpartitioned_hash_value
|
80
|
-
[hash_value, range_value]
|
81
|
-
end
|
82
|
-
|
83
|
-
def item_for_key(key)
|
84
|
-
item_for_hash_and_range(*(key.split("|", 2)))
|
97
|
+
[hash_value, range_value]
|
85
98
|
end
|
86
99
|
|
87
100
|
def item_for_hash_and_range(hash, range)
|
@@ -95,8 +108,8 @@ class DynamodbAdapter < Base
|
|
95
108
|
end
|
96
109
|
|
97
110
|
def dynamo_range_value(range_value)
|
98
|
-
case range_value
|
99
|
-
when ""
|
111
|
+
case range_value
|
112
|
+
when nil, ""
|
100
113
|
# range_key is required and must not be empty
|
101
114
|
" "
|
102
115
|
else
|
@@ -15,39 +15,56 @@ class FilesystemAdapter < Base
|
|
15
15
|
FilesystemDocument.new(key, attributes)
|
16
16
|
end
|
17
17
|
|
18
|
-
|
19
|
-
count_request { document_for_key(key) } or raise NotFound.for_key(key)
|
20
|
-
end
|
18
|
+
private
|
21
19
|
|
22
|
-
def
|
23
|
-
|
20
|
+
def read_doc(key)
|
21
|
+
document_for_key(key) or raise NotFound.for_key(key)
|
24
22
|
end
|
25
23
|
|
26
|
-
def
|
24
|
+
def write_doc(doc)
|
27
25
|
path = filepath_for_document(doc)
|
28
26
|
FileUtils.mkdir_p(path.dirname)
|
29
27
|
# TODO: conditional put
|
30
28
|
File.open(path, "w") {|f| f.write(doc.to_json)}
|
31
29
|
end
|
32
30
|
|
33
|
-
def
|
31
|
+
def write_index_doc(key)
|
32
|
+
path = filepath_for_hash_and_range(key.first, key[1])
|
33
|
+
FileUtils.mkdir_p(path.dirname)
|
34
|
+
File.open(path, "w") {} # "touch"
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy_doc(doc)
|
38
|
+
FileUtils.rm_f(filepath_for_document(doc))
|
39
|
+
end
|
40
|
+
|
41
|
+
def destroy_index_doc(key)
|
42
|
+
FileUtils.rm_f(filepath_for_hash_and_range(key.first, key[1]))
|
43
|
+
end
|
44
|
+
|
45
|
+
def query_docs(hash_value, range_value)
|
34
46
|
pathes_and_range_values =
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
single_file_query(hash_value, range_value)
|
41
|
-
end
|
47
|
+
case range_value
|
48
|
+
when Range
|
49
|
+
range_file_query(hash_value, range_value.first, range_value.last)
|
50
|
+
else
|
51
|
+
single_file_query(hash_value, range_value)
|
42
52
|
end
|
43
53
|
pathes_and_range_values.inject([]) do |memo, (path, doc_range_value)|
|
44
|
-
|
54
|
+
doc_range_value = nil if doc_range_value == ""
|
55
|
+
key = [hash_value, doc_range_value]
|
45
56
|
doc = document_for_key_and_path(key, path)
|
46
57
|
memo << doc if doc
|
47
58
|
memo
|
48
59
|
end
|
49
60
|
end
|
50
61
|
|
62
|
+
def doc_range_values_of_hash_value(hash_value)
|
63
|
+
all_files(dir_for_hash_value(hash_value)).map do |entry|
|
64
|
+
range_value_from_path_component(entry)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
51
68
|
private
|
52
69
|
|
53
70
|
def base_dir
|
@@ -59,25 +76,20 @@ class FilesystemAdapter < Base
|
|
59
76
|
end
|
60
77
|
|
61
78
|
def document_for_key(key)
|
62
|
-
document_for_key_and_path(key,
|
79
|
+
document_for_key_and_path(key, filepath_for_hash_and_range(key.first, key[1]))
|
63
80
|
end
|
64
81
|
|
65
82
|
def document_for_key_and_path(key, path)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
71
|
-
# there is no "was loaded from db" yet (e.g. for conditional put)
|
83
|
+
serialized_doc = File.read(path)
|
84
|
+
rescue ::Errno::ENOENT
|
85
|
+
nil
|
86
|
+
else
|
72
87
|
FilesystemDocument.new(key, Lib::Json.load(serialized_doc))
|
73
88
|
end
|
74
89
|
|
75
90
|
def filepath_for_document(doc)
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
def filepath_for_key(key)
|
80
|
-
filepath_for_hash_and_range(*(key.split("|", 2)))
|
91
|
+
key = doc.key
|
92
|
+
filepath_for_hash_and_range(key.first, key[1])
|
81
93
|
end
|
82
94
|
|
83
95
|
def filepath_for_hash_and_range(hash_value, range_value)
|
@@ -89,8 +101,13 @@ class FilesystemAdapter < Base
|
|
89
101
|
end
|
90
102
|
|
91
103
|
def path_component_for_value(value)
|
92
|
-
|
93
|
-
|
104
|
+
case value
|
105
|
+
when nil
|
106
|
+
""
|
107
|
+
else
|
108
|
+
# value should not have a pipe character
|
109
|
+
value.gsub("/", "|")
|
110
|
+
end
|
94
111
|
end
|
95
112
|
|
96
113
|
def path_component_for_range_value(range_value)
|
@@ -98,7 +115,12 @@ class FilesystemAdapter < Base
|
|
98
115
|
end
|
99
116
|
|
100
117
|
def value_from_path_component(path_component)
|
101
|
-
path_component
|
118
|
+
case path_component
|
119
|
+
when ""
|
120
|
+
nil
|
121
|
+
else
|
122
|
+
path_component.gsub("|", "/")
|
123
|
+
end
|
102
124
|
end
|
103
125
|
|
104
126
|
def range_value_from_path_component(path_component)
|
@@ -111,20 +133,26 @@ class FilesystemAdapter < Base
|
|
111
133
|
end
|
112
134
|
|
113
135
|
def range_file_query(hash_value, start_range_value, end_range_value)
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
memo
|
136
|
+
dir = dir_for_hash_value(hash_value)
|
137
|
+
all_files(dir).map do |entry|
|
138
|
+
[range_value_from_path_component(entry), entry]
|
139
|
+
end.select do |range_value, entry|
|
140
|
+
compare_value = range_value || ""
|
141
|
+
start_range_value <= compare_value && compare_value <= end_range_value
|
142
|
+
end.map do |range_value, entry|
|
143
|
+
[dir + entry, range_value]
|
123
144
|
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def all_files(dir)
|
148
|
+
entries = Dir.entries(dir)
|
124
149
|
rescue Errno::ENOENT
|
125
150
|
[]
|
151
|
+
else
|
152
|
+
entries.select do |entry|
|
153
|
+
entry[-EXT_SIZE..-1] == EXT
|
154
|
+
end
|
126
155
|
end
|
127
|
-
|
128
156
|
end
|
129
157
|
|
130
158
|
end; end # module Kvom::Adapter
|
data/lib/kvom/model.rb
ADDED
@@ -1,21 +1,27 @@
|
|
1
|
-
require 'kvom'
|
1
|
+
require 'kvom/model'
|
2
2
|
require 'active_support/core_ext/hash/keys'
|
3
3
|
require 'active_support/core_ext/object/blank'
|
4
4
|
require 'active_support/core_ext/class/attribute'
|
5
5
|
require 'active_model/naming'
|
6
6
|
require 'securerandom'
|
7
7
|
|
8
|
-
module Kvom
|
8
|
+
module Kvom; module Model
|
9
9
|
|
10
10
|
class Base
|
11
11
|
|
12
|
-
include ModelIdentity
|
12
|
+
include Kvom::ModelIdentity
|
13
13
|
extend ActiveModel::Naming
|
14
14
|
|
15
15
|
|
16
|
+
|
16
17
|
class_attribute :key_prefix
|
17
18
|
|
18
19
|
class << self
|
20
|
+
|
21
|
+
def has_all_ids
|
22
|
+
include Feature::AllIds
|
23
|
+
end
|
24
|
+
|
19
25
|
def create(attributes = {})
|
20
26
|
new(attributes).tap { |model| model.save }
|
21
27
|
end
|
@@ -27,7 +33,7 @@ class Base
|
|
27
33
|
|
28
34
|
def find_by_id(id)
|
29
35
|
find(id)
|
30
|
-
rescue NotFound
|
36
|
+
rescue Kvom::NotFound
|
31
37
|
nil
|
32
38
|
end
|
33
39
|
|
@@ -51,14 +57,18 @@ class Base
|
|
51
57
|
raise "must be overwritten in subclasses"
|
52
58
|
end
|
53
59
|
|
54
|
-
def
|
55
|
-
"
|
60
|
+
def id_key_for(id)
|
61
|
+
key_for("id/#{id}", nil)
|
56
62
|
end
|
57
63
|
|
58
64
|
private
|
59
65
|
|
60
66
|
def find_document(id)
|
61
|
-
adapter.get(
|
67
|
+
adapter.get(id_key_for(id))
|
68
|
+
end
|
69
|
+
|
70
|
+
def key_for(hash, range)
|
71
|
+
["#{key_prefix}/#{hash}", range]
|
62
72
|
end
|
63
73
|
|
64
74
|
end
|
@@ -66,12 +76,12 @@ class Base
|
|
66
76
|
def initialize(doc_or_attrs = {})
|
67
77
|
@document =
|
68
78
|
case doc_or_attrs
|
69
|
-
when Document
|
79
|
+
when ::Kvom::Adapter::Document
|
70
80
|
doc_or_attrs
|
71
81
|
else
|
72
82
|
@new = true
|
73
83
|
attrs = doc_or_attrs.stringify_keys
|
74
|
-
key = self.class.
|
84
|
+
key = self.class.id_key_for(attrs["id"] ||= SecureRandom.hex(8))
|
75
85
|
self.class.adapter.new_document(key, attrs)
|
76
86
|
end
|
77
87
|
end
|
@@ -82,6 +92,7 @@ class Base
|
|
82
92
|
|
83
93
|
def save
|
84
94
|
self.class.adapter.save(@document)
|
95
|
+
self.class.adapter.save_index(all_key) if @new && is_a?(Feature::AllIds)
|
85
96
|
@new = false
|
86
97
|
end
|
87
98
|
|
@@ -94,6 +105,7 @@ class Base
|
|
94
105
|
end
|
95
106
|
|
96
107
|
def destroy
|
108
|
+
self.class.adapter.destroy_index(all_key) if is_a?(Feature::AllIds)
|
97
109
|
self.class.adapter.destroy(@document)
|
98
110
|
end
|
99
111
|
|
@@ -109,4 +121,4 @@ class Base
|
|
109
121
|
|
110
122
|
end
|
111
123
|
|
112
|
-
end # module Kvom
|
124
|
+
end; end # module Kvom::Model
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'kvom/model/feature'
|
2
|
+
|
3
|
+
module Kvom; module Model; module Feature
|
4
|
+
|
5
|
+
module AllIds
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
def all_ids
|
13
|
+
adapter.range_values_of_hash_value("#{key_prefix}/all")
|
14
|
+
end
|
15
|
+
|
16
|
+
def all_key_for(id)
|
17
|
+
key_for("all", id)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def all_key
|
25
|
+
self.class.all_key_for(id)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end; end; end
|
data/lib/kvom/not_found.rb
CHANGED
File without changes
|
File without changes
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Kvom::Model::Base, "with all-index enabled" do
|
4
|
+
describe ".all_ids" do
|
5
|
+
before(:all) do
|
6
|
+
TestModelWithIndex.create(:id => volatile_id("0815-list"))
|
7
|
+
TestModelWithIndex.create(:id => volatile_id("007"))
|
8
|
+
OtherModelWithIndex.create(:id => volatile_id("other"))
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns the ids of all persisted models" do
|
12
|
+
all_ids = TestModelWithIndex.all_ids
|
13
|
+
expected_ids = [volatile_id("0815-list"), volatile_id("007")]
|
14
|
+
(expected_ids & all_ids).should eq(expected_ids)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "returns only ids of the model" do
|
18
|
+
all_ids = TestModelWithIndex.all_ids
|
19
|
+
all_ids.should_not include(volatile_id("other"))
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns ids for which a model can be fetched" do
|
23
|
+
all_ids = TestModelWithIndex.all_ids
|
24
|
+
all_ids.each do |model_id|
|
25
|
+
expect {TestModelWithIndex.find(model_id)}.to_not raise_error
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ".create" do
|
31
|
+
it "adds the instance's id to the ids returned by .all_ids" do
|
32
|
+
new_id = volatile_id("neu")
|
33
|
+
expect {
|
34
|
+
TestModelWithIndex.create(:id => new_id)
|
35
|
+
}.to change {
|
36
|
+
TestModelWithIndex.all_ids.include?(new_id)
|
37
|
+
}.to(true)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#destroy" do
|
42
|
+
let(:model) {TestModelWithIndex.create(:id => volatile_id("weg"))}
|
43
|
+
|
44
|
+
before do
|
45
|
+
model
|
46
|
+
end
|
47
|
+
|
48
|
+
it "removes the instance's id from the ids returned by .all_ids" do
|
49
|
+
expect {
|
50
|
+
model.destroy
|
51
|
+
}.to change {
|
52
|
+
TestModelWithIndex.all_ids.include?(volatile_id("weg"))
|
53
|
+
}.to(false)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe ".save" do
|
58
|
+
it "does not write the index doc for an existing model" do
|
59
|
+
new_model = TestModelWithIndex.new(:id => volatile_id("counting"))
|
60
|
+
counter_before = TestModelWithIndex.adapter.request_counter
|
61
|
+
new_model.save
|
62
|
+
counter_new_saved = TestModelWithIndex.adapter.request_counter
|
63
|
+
requests_for_new_model = counter_new_saved - counter_before
|
64
|
+
new_model.something = "changed"
|
65
|
+
new_model.save
|
66
|
+
counter_existing_saved = TestModelWithIndex.adapter.request_counter
|
67
|
+
requests_for_existing_model = counter_existing_saved - counter_new_saved
|
68
|
+
|
69
|
+
requests_for_existing_model.should < requests_for_new_model
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -1,17 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
4
|
-
self.key_prefix = "spam_prefix"
|
5
|
-
end
|
6
|
-
|
7
|
-
class ModelWithoutAdapter < Kvom::Base
|
8
|
-
end
|
9
|
-
|
10
|
-
def random_id
|
11
|
-
rand.to_s
|
12
|
-
end
|
13
|
-
|
14
|
-
describe Kvom::Base do
|
3
|
+
describe Kvom::Model::Base do
|
15
4
|
it "should complain when no adapter is specified" do
|
16
5
|
expect { ModelWithoutAdapter.find("spam") }.to raise_error(/overwritten/)
|
17
6
|
end
|
@@ -111,11 +100,11 @@ describe Kvom::Base do
|
|
111
100
|
|
112
101
|
describe "model's id" do
|
113
102
|
it "should use a custom prefix when given" do
|
114
|
-
CustomKeyPrefixModel.new.__send__(:document).key.should =~ /^spam_prefix\/id\/.+/
|
103
|
+
CustomKeyPrefixModel.new.__send__(:document).key.first.should =~ /^spam_prefix\/id\/.+/
|
115
104
|
end
|
116
105
|
|
117
106
|
it "should use a default prefix" do
|
118
|
-
ExampleModel.new.__send__(:document).key.should =~ /^ExampleModel\/id\/.+/
|
107
|
+
ExampleModel.new.__send__(:document).key.first.should =~ /^ExampleModel\/id\/.+/
|
119
108
|
end
|
120
109
|
end
|
121
110
|
|
@@ -134,7 +123,7 @@ describe Kvom::Base do
|
|
134
123
|
model_id = random_id
|
135
124
|
expect {
|
136
125
|
ExampleModel.find(model_id)
|
137
|
-
}.to raise_error(Kvom::NotFound, %r(document.*"ExampleModel/id/#{model_id}
|
126
|
+
}.to raise_error(Kvom::NotFound, %r(document.* key "ExampleModel/id/#{model_id}\|"))
|
138
127
|
end
|
139
128
|
end
|
140
129
|
|
@@ -167,49 +156,44 @@ describe Kvom::Base do
|
|
167
156
|
|
168
157
|
describe "adapter.query(<hash_value>, <range_spec>)" do
|
169
158
|
|
170
|
-
def random_id(id)
|
171
|
-
$base_spec_random_id ||= SecureRandom.hex(8)
|
172
|
-
"#{$base_spec_random_id}#{id}"
|
173
|
-
end
|
174
|
-
|
175
159
|
def set_of_keys(documents)
|
176
160
|
keys = documents.map{|doc| doc.key}.to_set
|
177
161
|
keys.size.should == documents.size
|
178
162
|
keys
|
179
163
|
end
|
180
164
|
|
181
|
-
def create_document(
|
182
|
-
document = adapter.new_document(
|
165
|
+
def create_document(hash, range)
|
166
|
+
document = adapter.new_document([volatile_id(hash), range], {})
|
183
167
|
adapter.save(document)
|
184
168
|
document
|
185
169
|
end
|
186
170
|
|
187
171
|
let(:adapter) {TestModel.adapter}
|
188
172
|
|
189
|
-
let(:one_001) {create_document("one
|
190
|
-
let(:one_002) {create_document("one
|
191
|
-
let(:one_0021) {create_document("one
|
192
|
-
let(:one_0022) {create_document("one
|
193
|
-
let(:one_3) {create_document("one
|
194
|
-
let(:two_two) {create_document("two
|
195
|
-
let(:three_empty) {create_document("three
|
173
|
+
let(:one_001) {create_document("one", "001")}
|
174
|
+
let(:one_002) {create_document("one", "002")}
|
175
|
+
let(:one_0021) {create_document("one", "0021")}
|
176
|
+
let(:one_0022) {create_document("one", "0022")}
|
177
|
+
let(:one_3) {create_document("one", "3")}
|
178
|
+
let(:two_two) {create_document("two", "two")}
|
179
|
+
let(:three_empty) {create_document("three", nil)}
|
196
180
|
|
197
181
|
before(:all) do
|
198
|
-
[one_001, one_002, one_0021, one_0022, one_3, two_two, three_empty
|
182
|
+
[one_001, one_002, one_0021, one_0022, one_3, two_two, three_empty]
|
199
183
|
end
|
200
184
|
|
201
185
|
context "when range is a single value" do
|
202
186
|
context "when the doc exists" do
|
203
187
|
it "returns a one-element list with the doc's model instance" do
|
204
|
-
set_of_keys(adapter.query(
|
205
|
-
set_of_keys(adapter.query(
|
188
|
+
set_of_keys(adapter.query(volatile_id("one"), "001")).should == set_of_keys([one_001])
|
189
|
+
set_of_keys(adapter.query(volatile_id("three"), nil)).should == set_of_keys([three_empty])
|
206
190
|
end
|
207
191
|
end
|
208
192
|
|
209
193
|
context "when the doc does not exists" do
|
210
194
|
it "returns an empty list" do
|
211
|
-
adapter.query(
|
212
|
-
adapter.query(
|
195
|
+
adapter.query(volatile_id("unknown_hash"), "001").should == []
|
196
|
+
adapter.query(volatile_id("one"), "unknown_range").should == []
|
213
197
|
end
|
214
198
|
end
|
215
199
|
end
|
@@ -217,37 +201,38 @@ describe Kvom::Base do
|
|
217
201
|
context "when range is a Range" do
|
218
202
|
|
219
203
|
it "returns documents exactly matching the range border" do
|
220
|
-
set_of_keys(adapter.query(
|
204
|
+
set_of_keys(adapter.query(volatile_id("one"), Range.new("0021", "0022"))).
|
221
205
|
should == set_of_keys([one_0021, one_0022])
|
222
|
-
set_of_keys(adapter.query(random_id("three"), Range.new("", ""))).
|
223
|
-
should == set_of_keys([three_empty])
|
224
206
|
end
|
225
207
|
|
226
208
|
it "returns all documents with a range value within the range" do
|
227
|
-
set_of_keys(adapter.query(
|
209
|
+
set_of_keys(adapter.query(volatile_id("one"), Range.new("0021", "0022"))).
|
228
210
|
should == set_of_keys([one_0021, one_0022])
|
229
211
|
end
|
230
212
|
|
231
213
|
context "when there is no doc within the range" do
|
232
214
|
it "returns an empty list" do
|
233
|
-
adapter.query(
|
215
|
+
adapter.query(volatile_id("one"), Range.new("5", "8")).
|
234
216
|
should == []
|
235
217
|
end
|
236
218
|
end
|
237
219
|
|
238
220
|
context 'when range is ["", "~"]' do
|
239
221
|
it "does not miss a document for the hash value" do
|
240
|
-
set_of_keys(adapter.query(
|
222
|
+
set_of_keys(adapter.query(volatile_id("one"), Range.new("", "~"))).
|
241
223
|
should == set_of_keys([one_001, one_002, one_0021, one_0022, one_3])
|
242
|
-
|
243
|
-
|
224
|
+
end
|
225
|
+
|
226
|
+
it "returns a document that has the hash value even if it has no range value" do
|
227
|
+
set_of_keys(adapter.query(volatile_id("three"), Range.new("", "~"))).
|
228
|
+
should == set_of_keys([three_empty])
|
244
229
|
end
|
245
230
|
end
|
246
231
|
end
|
247
232
|
|
248
233
|
context "when there is no doc with the hash value" do
|
249
234
|
it "returns an empty list" do
|
250
|
-
adapter.query(
|
235
|
+
adapter.query(volatile_id("none_of_these"), Range.new("", "~")).should == []
|
251
236
|
end
|
252
237
|
end
|
253
238
|
end
|
@@ -271,7 +256,7 @@ describe Kvom::Base do
|
|
271
256
|
|
272
257
|
context "when the model has been created with an id" do
|
273
258
|
|
274
|
-
let(:model_with_id) {ExampleModel.create(:id =>
|
259
|
+
let(:model_with_id) {ExampleModel.create(:id => random_id)}
|
275
260
|
|
276
261
|
it "destroys the model" do
|
277
262
|
id = model_with_id.id
|
@@ -325,7 +310,7 @@ describe Kvom::Base do
|
|
325
310
|
|
326
311
|
context "including id" do
|
327
312
|
|
328
|
-
let(:model_id) {"
|
313
|
+
let(:model_id) {"not_a_default_#{random_id}"}
|
329
314
|
let(:model_from_attributes_including_id) {ExampleModel.new({"id" => model_id})}
|
330
315
|
|
331
316
|
before do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,29 +1,7 @@
|
|
1
1
|
# require 'pry'
|
2
2
|
require File.expand_path("../../lib/kvom", __FILE__)
|
3
3
|
|
4
|
-
|
5
|
-
class TestModel < Kvom::Base
|
6
|
-
def self.adapter
|
7
|
-
@adapter ||=
|
8
|
-
case (type = ENV['KVOM_ADAPTER_TYPE'] || "file")
|
9
|
-
when /dynamo/
|
10
|
-
require File.expand_path("../../../tasks/support/local_config", __FILE__)
|
11
|
-
options = {
|
12
|
-
:table => "test-kvom",
|
13
|
-
:access_key_id => local_config["aws_access_key_id"],
|
14
|
-
:secret_access_key => local_config["aws_secret_access_key"],
|
15
|
-
}
|
16
|
-
options[:partition] = "korb" if type == "partitioned_dynamo"
|
17
|
-
Kvom::Adapter::DynamodbAdapter.new(options)
|
18
|
-
when /file/
|
19
|
-
Kvom::Adapter::FilesystemAdapter.new(:path => File.expand_path("../../tmp/fsa", __FILE__))
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
class ExampleModel < TestModel
|
25
|
-
property :spam
|
26
|
-
end
|
4
|
+
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f[0..-4]}
|
27
5
|
|
28
6
|
RSpec.configure do |config|
|
29
7
|
adapter = TestModel.adapter.class.to_s.demodulize.sub("Adapter", "").sub("db", "").downcase
|
@@ -37,6 +15,8 @@ RSpec.configure do |config|
|
|
37
15
|
satisfied_by?(multi_json_version)
|
38
16
|
}
|
39
17
|
|
18
|
+
config.include(TestIds)
|
19
|
+
|
40
20
|
config.filter_run_excluding :adapter => adapter_specified_and_different
|
41
21
|
config.filter_run_excluding :multi_json_version => multi_json_version_and_insufficient
|
42
22
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class TestModel < Kvom::Model::Base
|
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
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class ExampleModel < TestModel
|
21
|
+
property :spam
|
22
|
+
end
|
23
|
+
|
24
|
+
class CustomKeyPrefixModel < TestModel
|
25
|
+
self.key_prefix = "spam_prefix"
|
26
|
+
end
|
27
|
+
|
28
|
+
class ModelWithoutAdapter < Kvom::Model::Base
|
29
|
+
end
|
30
|
+
|
31
|
+
class TestModelWithIndex < TestModel
|
32
|
+
has_all_ids
|
33
|
+
|
34
|
+
property :something
|
35
|
+
end
|
36
|
+
|
37
|
+
class OtherModelWithIndex < TestModel
|
38
|
+
has_all_ids
|
39
|
+
|
40
|
+
property :something
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TestIds
|
2
|
+
# - returns the same id for the whole test run
|
3
|
+
# - returns a different id when running the specs again
|
4
|
+
# => no id collision when running the tests on a reused and not emptied database
|
5
|
+
def self.volatile_id(id)
|
6
|
+
"#{@discriminator ||= SecureRandom.hex(8)}#{id}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def volatile_id(id)
|
10
|
+
TestIds.volatile_id(id)
|
11
|
+
end
|
12
|
+
|
13
|
+
def random_id
|
14
|
+
rand.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
metadata
CHANGED
@@ -1,19 +1,18 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kvom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 730055358
|
5
5
|
prerelease: 9
|
6
6
|
segments:
|
7
7
|
- 6
|
8
8
|
- 8
|
9
9
|
- 0
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
13
|
-
-
|
10
|
+
- 72
|
11
|
+
- d
|
12
|
+
- 18
|
13
|
+
- d
|
14
14
|
- 96
|
15
|
-
|
16
|
-
version: 6.8.0.23.da7f96b
|
15
|
+
version: 6.8.0.72.d18d096
|
17
16
|
platform: ruby
|
18
17
|
authors:
|
19
18
|
- Kristian Hanekamp, Infopark AG
|
@@ -21,10 +20,11 @@ autorequire:
|
|
21
20
|
bindir: bin
|
22
21
|
cert_chain: []
|
23
22
|
|
24
|
-
date: 2012-
|
23
|
+
date: 2012-10-22 00:00:00 +02:00
|
25
24
|
default_executable:
|
26
25
|
dependencies:
|
27
26
|
- !ruby/object:Gem::Dependency
|
27
|
+
type: :runtime
|
28
28
|
requirement: &id001 !ruby/object:Gem::Requirement
|
29
29
|
none: false
|
30
30
|
requirements:
|
@@ -34,11 +34,11 @@ dependencies:
|
|
34
34
|
segments:
|
35
35
|
- 1
|
36
36
|
version: "1"
|
37
|
-
version_requirements: *id001
|
38
37
|
name: multi_json
|
38
|
+
version_requirements: *id001
|
39
39
|
prerelease: false
|
40
|
-
type: :runtime
|
41
40
|
- !ruby/object:Gem::Dependency
|
41
|
+
type: :runtime
|
42
42
|
requirement: &id002 !ruby/object:Gem::Requirement
|
43
43
|
none: false
|
44
44
|
requirements:
|
@@ -48,11 +48,11 @@ dependencies:
|
|
48
48
|
segments:
|
49
49
|
- 3
|
50
50
|
version: "3"
|
51
|
-
version_requirements: *id002
|
52
51
|
name: activesupport
|
52
|
+
version_requirements: *id002
|
53
53
|
prerelease: false
|
54
|
-
type: :runtime
|
55
54
|
- !ruby/object:Gem::Dependency
|
55
|
+
type: :runtime
|
56
56
|
requirement: &id003 !ruby/object:Gem::Requirement
|
57
57
|
none: false
|
58
58
|
requirements:
|
@@ -62,12 +62,26 @@ dependencies:
|
|
62
62
|
segments:
|
63
63
|
- 3
|
64
64
|
version: "3"
|
65
|
-
version_requirements: *id003
|
66
65
|
name: activemodel
|
66
|
+
version_requirements: *id003
|
67
67
|
prerelease: false
|
68
|
-
type: :runtime
|
69
68
|
- !ruby/object:Gem::Dependency
|
69
|
+
type: :development
|
70
70
|
requirement: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
name: rake
|
80
|
+
version_requirements: *id004
|
81
|
+
prerelease: false
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
type: :development
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
71
85
|
none: false
|
72
86
|
requirements:
|
73
87
|
- - ~>
|
@@ -77,12 +91,12 @@ dependencies:
|
|
77
91
|
- 2
|
78
92
|
- 8
|
79
93
|
version: "2.8"
|
80
|
-
version_requirements: *id004
|
81
94
|
name: rspec
|
95
|
+
version_requirements: *id005
|
82
96
|
prerelease: false
|
83
|
-
type: :development
|
84
97
|
- !ruby/object:Gem::Dependency
|
85
|
-
|
98
|
+
type: :development
|
99
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
86
100
|
none: false
|
87
101
|
requirements:
|
88
102
|
- - ">="
|
@@ -91,12 +105,12 @@ dependencies:
|
|
91
105
|
segments:
|
92
106
|
- 0
|
93
107
|
version: "0"
|
94
|
-
version_requirements: *id005
|
95
108
|
name: helpful_configuration
|
109
|
+
version_requirements: *id006
|
96
110
|
prerelease: false
|
97
|
-
type: :development
|
98
111
|
- !ruby/object:Gem::Dependency
|
99
|
-
|
112
|
+
type: :development
|
113
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
100
114
|
none: false
|
101
115
|
requirements:
|
102
116
|
- - ">="
|
@@ -105,10 +119,9 @@ dependencies:
|
|
105
119
|
segments:
|
106
120
|
- 0
|
107
121
|
version: "0"
|
108
|
-
version_requirements: *id006
|
109
122
|
name: yajl-ruby
|
123
|
+
version_requirements: *id007
|
110
124
|
prerelease: false
|
111
|
-
type: :development
|
112
125
|
description: Use it to build object models in ruby on top of a key value store.
|
113
126
|
email:
|
114
127
|
- kristian.hanekamp@infopark.de
|
@@ -126,16 +139,19 @@ files:
|
|
126
139
|
- lib/kvom.rb
|
127
140
|
- lib/kvom/adapter.rb
|
128
141
|
- lib/kvom/adapter/base.rb
|
142
|
+
- lib/kvom/adapter/document.rb
|
129
143
|
- lib/kvom/adapter/dynamodb_adapter.rb
|
130
144
|
- lib/kvom/adapter/dynamodb_document.rb
|
131
145
|
- lib/kvom/adapter/filesystem_adapter.rb
|
132
146
|
- lib/kvom/adapter/filesystem_document.rb
|
133
147
|
- lib/kvom/adapter/key_attributes_document.rb
|
134
|
-
- lib/kvom/base.rb
|
135
|
-
- lib/kvom/document.rb
|
136
148
|
- lib/kvom/lib.rb
|
137
149
|
- lib/kvom/lib/json.rb
|
138
150
|
- lib/kvom/lib/json_value.rb
|
151
|
+
- lib/kvom/model.rb
|
152
|
+
- lib/kvom/model/base.rb
|
153
|
+
- lib/kvom/model/feature.rb
|
154
|
+
- lib/kvom/model/feature/all_ids.rb
|
139
155
|
- lib/kvom/model_identity.rb
|
140
156
|
- lib/kvom/not_found.rb
|
141
157
|
- lib/kvom/storage.rb
|
@@ -144,15 +160,18 @@ files:
|
|
144
160
|
- lib/kvom/storage/file_system_storage.rb
|
145
161
|
- lib/kvom/storage/not_found.rb
|
146
162
|
- lib/kvom/storage/s3_storage.rb
|
147
|
-
- spec/
|
148
|
-
- spec/
|
149
|
-
- spec/
|
150
|
-
- spec/adaptor/model_identity_spec.rb
|
163
|
+
- spec/adapter/counter_spec.rb
|
164
|
+
- spec/adapter/dynamodb_adapter_spec.rb
|
165
|
+
- spec/adapter/model_identity_spec.rb
|
151
166
|
- spec/cache_with_prefix_spec.rb
|
152
167
|
- spec/lib/json_spec.rb
|
168
|
+
- spec/model/all_ids_spec.rb
|
169
|
+
- spec/model/base_spec.rb
|
153
170
|
- spec/spec_helper.rb
|
154
171
|
- spec/storage/file_system_spec.rb
|
155
172
|
- spec/storage/s3_spec.rb
|
173
|
+
- spec/support/model.rb
|
174
|
+
- spec/support/test_ids.rb
|
156
175
|
- tmp/.gitignore
|
157
176
|
has_rdoc: true
|
158
177
|
homepage: ""
|
@@ -191,12 +210,15 @@ signing_key:
|
|
191
210
|
specification_version: 3
|
192
211
|
summary: Key Value Object Mapper
|
193
212
|
test_files:
|
194
|
-
- spec/
|
195
|
-
- spec/
|
196
|
-
- spec/
|
197
|
-
- spec/adaptor/model_identity_spec.rb
|
213
|
+
- spec/adapter/counter_spec.rb
|
214
|
+
- spec/adapter/dynamodb_adapter_spec.rb
|
215
|
+
- spec/adapter/model_identity_spec.rb
|
198
216
|
- spec/cache_with_prefix_spec.rb
|
199
217
|
- spec/lib/json_spec.rb
|
218
|
+
- spec/model/all_ids_spec.rb
|
219
|
+
- spec/model/base_spec.rb
|
200
220
|
- spec/spec_helper.rb
|
201
221
|
- spec/storage/file_system_spec.rb
|
202
222
|
- spec/storage/s3_spec.rb
|
223
|
+
- spec/support/model.rb
|
224
|
+
- spec/support/test_ids.rb
|