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 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
@@ -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
- raise "implement me in subclass!"
27
+ count_request do
28
+ write_doc(doc)
29
+ end
22
30
  end
23
31
 
24
- def get(key)
25
- raise "implement me in subclass!"
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
- raise "implement me in subclass!"
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
- raise "implement me in subclass!"
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
- protected
62
+ private
37
63
 
38
- def count_request(num_requests = 1)
39
- @request_counter += num_requests
40
- result = yield
41
- @documents_loaded_counter += Array === result ? result.size : 1
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
@@ -1,6 +1,6 @@
1
- require 'kvom'
1
+ require 'kvom/adapter'
2
2
 
3
- module Kvom
3
+ module Kvom; module Adapter
4
4
 
5
5
  class Document
6
6
  def key
@@ -16,4 +16,4 @@ class Document
16
16
  end
17
17
  end
18
18
 
19
- end # module Kvom
19
+ 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, item_for_key(key))
9
+ DynamodbDocument.new_document(attributes, key, item_for_hash_and_range(key.first, key[1]))
10
10
  end
11
11
 
12
- def get(key)
13
- item = item_for_key(key)
14
- count_request {
15
- (attributes = item.attributes.to_h).empty? and raise NotFound.for_key(key)
16
- DynamodbDocument.document_from_query(attributes, key, item)
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 save(doc)
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 destroy(doc)
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 query(hash_value, range_value)
29
- count_request do
30
- case range_value
31
- when String
32
- item = item_for_hash_and_range(hash_value, range_value)
33
- attributes = item.attributes.to_h
34
- if attributes.empty?
35
- []
36
- else
37
- key = key_from_item(item)
38
- [DynamodbDocument.document_from_query(attributes, key, item)]
39
- end
40
- when Range
41
- dynamo_range_start = dynamo_range_value(range_value.begin)
42
- dynamo_range_end = dynamo_range_value(range_value.end)
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 = "" if range_value == " " # range_from_dynamo_range
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].join("|")
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
- def get(key)
19
- count_request { document_for_key(key) } or raise NotFound.for_key(key)
20
- end
18
+ private
21
19
 
22
- def destroy(doc)
23
- FileUtils.rm_f(filepath_for_document(doc))
20
+ def read_doc(key)
21
+ document_for_key(key) or raise NotFound.for_key(key)
24
22
  end
25
23
 
26
- def save(doc)
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 query(hash_value, range_value)
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
- count_request do
36
- case range_value
37
- when Range
38
- range_file_query(hash_value, range_value.first, range_value.last)
39
- else
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
- key = [hash_value, doc_range_value].join("|")
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, filepath_for_key(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
- begin
67
- serialized_doc = File.read(path)
68
- rescue ::Errno::ENOENT
69
- return nil
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
- filepath_for_key(doc.key)
77
- end
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
- # value should not have a pipe character
93
- value.gsub("/", "|")
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.to_s.gsub("|", "/")
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
- hash_dir = dir_for_hash_value(hash_value)
115
- Dir.foreach(hash_dir).inject([]) do |memo, entry|
116
- if entry[-EXT_SIZE..-1] == EXT
117
- range_value = range_value_from_path_component(entry)
118
- if start_range_value <= range_value && range_value <= end_range_value
119
- memo << [hash_dir + entry, range_value]
120
- end
121
- end
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
@@ -0,0 +1,5 @@
1
+ require 'kvom'
2
+
3
+ module Kvom::Model
4
+ Kvom.setup_autoload(self, __FILE__)
5
+ end
@@ -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 key_for(id)
55
- "#{key_prefix}/id/#{id}|"
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(key_for(id))
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.key_for(attrs["id"] ||= SecureRandom.hex(8))
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,5 @@
1
+ require 'kvom/model'
2
+
3
+ module Kvom::Model::Feature
4
+ Kvom.setup_autoload(self, __FILE__)
5
+ end
@@ -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
@@ -2,6 +2,6 @@ require 'kvom'
2
2
 
3
3
  class Kvom::NotFound < ::StandardError
4
4
  def self.for_key(key)
5
- new(%(document with key "#{key}" not found))
5
+ new(%(document with key "#{key.first}|#{key[1]}" not found))
6
6
  end
7
7
  end
@@ -1,8 +1,12 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Kvom::Base do
3
+ describe Kvom::Model::Base do
4
4
  let(:model) { ExampleModel.create }
5
5
 
6
+ before do
7
+ model
8
+ end
9
+
6
10
  it "should count the number of requests" do
7
11
  expect {
8
12
  ExampleModel.find(model.id)
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
- class CustomKeyPrefixModel < TestModel
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(id)
182
- document = adapter.new_document(random_id(id), {})
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|001")}
190
- let(:one_002) {create_document("one|002")}
191
- let(:one_0021) {create_document("one|0021")}
192
- let(:one_0022) {create_document("one|0022")}
193
- let(:one_3) {create_document("one|3")}
194
- let(:two_two) {create_document("two|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, 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(random_id("one"), "001")).should == set_of_keys([one_001])
205
- set_of_keys(adapter.query(random_id("three"), "")).should == set_of_keys([three_empty])
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(random_id("unknown_hash"), "001").should == []
212
- adapter.query(random_id("one"), "unknown_range").should == []
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(random_id("one"), Range.new("0021", "0022"))).
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(random_id("one"), Range.new("0021", "0022"))).
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(random_id("one"), Range.new("5", "8")).
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(random_id("one"), Range.new("", "~"))).
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
- set_of_keys(adapter.query(random_id("three"), Range.new("", "~"))).
243
- should == set_of_keys([three_empty])
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(random_id("none_of_these"), Range.new("", "~")).should == []
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 => rand.to_s)}
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) {"static" + rand.to_s}
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: -746145115
4
+ hash: 730055358
5
5
  prerelease: 9
6
6
  segments:
7
7
  - 6
8
8
  - 8
9
9
  - 0
10
- - 23
11
- - da
12
- - 7
13
- - f
10
+ - 72
11
+ - d
12
+ - 18
13
+ - d
14
14
  - 96
15
- - b
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-09-18 00:00:00 +02:00
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
- requirement: &id005 !ruby/object:Gem::Requirement
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
- requirement: &id006 !ruby/object:Gem::Requirement
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/adaptor/base_spec.rb
148
- - spec/adaptor/counter_spec.rb
149
- - spec/adaptor/dynamodb_adaptor_spec.rb
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/adaptor/base_spec.rb
195
- - spec/adaptor/counter_spec.rb
196
- - spec/adaptor/dynamodb_adaptor_spec.rb
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