kvom 6.8.0.110.6570b45 → 6.8.0.210.ed204b0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/kvom.rb CHANGED
@@ -11,4 +11,11 @@ module Kvom
11
11
  end
12
12
 
13
13
  setup_autoload(self, __FILE__)
14
+
15
+ class NotFound < ::StandardError
16
+ def self.for_key(key)
17
+ new(%(document with key "#{key.first}|#{key[1]}" not found))
18
+ end
19
+ end
20
+
14
21
  end
data/lib/kvom/adapter.rb CHANGED
@@ -2,4 +2,7 @@ require 'kvom'
2
2
 
3
3
  module Kvom::Adapter
4
4
  Kvom.setup_autoload(self, __FILE__)
5
+
6
+ class UnsupportedValue < ArgumentError; end
7
+
5
8
  end
@@ -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
@@ -24,8 +24,10 @@ class Base
24
24
  end
25
25
 
26
26
  def save(doc)
27
- count_request do
28
- write_doc(doc)
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
- def key
7
- raise "implement me in subclass!"
8
- end
7
+
8
+ attr_reader :key, :persisted_revision
9
9
 
10
10
  def [](name)
11
- raise "implement me in subclass!"
11
+ attributes[name]
12
12
  end
13
13
 
14
14
  def []=(name, value)
15
- raise "implement me in subclass!"
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.new_document(attributes, key, item_for_hash_and_range(key.first, key[1]))
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(attributes, key, item)
18
+ DynamodbDocument.document_from_query(key, attributes, item)
19
19
  end
20
20
 
21
21
  def write_doc(doc)
22
- # doc is only saved if modified - might be one request more than sent to Dynamo
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(attributes, key, item)]
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, key, item)
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 < KeyAttributesDocument
5
+ class DynamodbDocument < Document
6
6
  class << self
7
- def new_document(attributes, key, item)
8
- new(key, item, attributes, nil)
9
- end
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, item, attributes, revision)
19
- super(key, attributes)
15
+ def initialize(key, attributes, item, from_db = false)
20
16
  @dynamo_item = item
21
- @revision = revision
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 save
39
- return unless modified?
40
- raise "Not yet implemented" if AttributesFromItemAttributes === attributes
41
- dynamo_attributes = attributes.inject({}) do |memo, (name, value)|
42
- case value
43
- when nil
44
- # skip
45
- when ""
46
- # will not raise an error when persisted, but will be returned as nil
47
- raise unsupported_value(value)
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" => @revision)
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[:unless] = missing_condition unless missing_condition.empty?
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
- FileUtils.rm_f(filepath_for_document(doc))
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 < KeyAttributesDocument
5
+ class FilesystemDocument < Document
6
6
  def to_json
7
- # supported values restricted to the ones of dynamo_document
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
 
@@ -0,0 +1,3 @@
1
+ require 'kvom'
2
+
3
+ class Kvom::Conflict < ::StandardError; end
@@ -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")
@@ -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
- expect {TestModelWithIndex.find(model_id)}.to_not raise_error
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
@@ -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
- example = ExampleModel.create(:spam => "foo")
11
- example.spam.should == "foo"
12
- example.spam = "bar"
13
- example.spam.should == "bar"
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 "handle access via string as well as symbols" do
17
- example = ExampleModel.create(:spam => "foo")
18
- example.spam.should == "foo"
19
- example = ExampleModel.create("spam" => "foo")
20
- example.spam.should == "foo"
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
- example = ExampleModel.create(:spam => "foo")
25
- example.save
26
+ model = ExampleModel.create(:spam => "foo")
27
+ model.save
26
28
 
27
- loaded = ExampleModel.find(example.id)
29
+ loaded = ExampleModel.find(model.id)
28
30
  loaded.spam.should == "foo"
29
31
  end
30
32
 
31
- context "when setting an empty string as property value" do
33
+ context "when setting" do
34
+ def raise_unsupported_value(*message)
35
+ raise_error(Kvom::Adapter::UnsupportedValue, *message)
36
+ end
32
37
 
33
- context "when created" do
34
- it "raises an error" do
35
- expect {ExampleModel.create(:spam => "")}.to raise_error "Unsupported value: empty string"
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
- context "when saved" do
40
- let(:example) {ExampleModel.create(:property => "not empty")}
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
- it "raises an error" do
43
- expect {
44
- example.spam = ""
45
- example.save
46
- }.to raise_error "Unsupported value: empty string"
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 "when setting a value of an unsupported value class" do
54
- context "when saved" do
55
- let(:example) {ExampleModel.create(:property => "valid type")}
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
- it "raises an error" do
58
- expect {
59
- example.spam = %w[invalid type array]
60
- example.save
61
- }.to raise_error "Unsupported value type: Array"
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(:example_id) {ExampleModel.create(:spam => "ham").id}
75
- let(:example_from_db) {ExampleModel.find(example_id)}
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
- example_from_db.spam = "and eggs"
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
- example_from_db.save
221
+ model_from_db.save
85
222
  }.to change {
86
- ExampleModel.find(example_id).spam
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
- example = ExampleModel.create(:spam => "foo")
118
- ExampleModel.find(example.id).id.should == example.id
119
- ExampleModel.find(example.id).spam.should == "foo"
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
@@ -1,4 +1,4 @@
1
- # require 'pry'
1
+ require 'pry'
2
2
  require File.expand_path("../../lib/kvom", __FILE__)
3
3
 
4
4
  Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f[0..-4]}
@@ -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
@@ -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
@@ -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: -497236305
5
- prerelease: 14
4
+ hash: -268426508
5
+ prerelease: 10
6
6
  segments:
7
7
  - 6
8
8
  - 8
9
9
  - 0
10
- - 110
11
- - 6570
10
+ - 210
11
+ - ed
12
+ - 204
12
13
  - b
13
- - 45
14
- version: 6.8.0.110.6570b45
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-10-26 00:00:00 +02:00
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/adapter/key_attributes_document.rb
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
@@ -1,7 +0,0 @@
1
- require 'kvom'
2
-
3
- class Kvom::NotFound < ::StandardError
4
- def self.for_key(key)
5
- new(%(document with key "#{key.first}|#{key[1]}" not found))
6
- end
7
- end