kvom 6.8.0.beta.200.566.d1df6eb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +7 -0
- data/Rakefile +1 -0
- data/kvom.gemspec +26 -0
- data/lib/kvom.rb +3 -0
- data/lib/kvom/adapter/base.rb +43 -0
- data/lib/kvom/adapter/couchdb_adapter.rb +82 -0
- data/lib/kvom/adapter/couchdb_document.rb +23 -0
- data/lib/kvom/adapter/dynamodb_adapter.rb +116 -0
- data/lib/kvom/adapter/dynamodb_document.rb +159 -0
- data/lib/kvom/base.rb +116 -0
- data/lib/kvom/document.rb +17 -0
- data/lib/kvom/model_identity.rb +23 -0
- data/spec/base_spec.rb +217 -0
- data/spec/counter_spec.rb +17 -0
- data/spec/model_identity_spec.rb +30 -0
- data/spec/spec_helper.rb +37 -0
- metadata +166 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/kvom.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path("../../rake_support/git_based_version", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "kvom"
|
6
|
+
s.version = RakeSupport.git_based_version(__FILE__)
|
7
|
+
s.authors = ["Kristian Hanekamp, Infopark AG"]
|
8
|
+
s.email = ["kristian.hanekamp@infopark.de"]
|
9
|
+
s.homepage = ""
|
10
|
+
s.summary = %q{Key Value Object Mapper}
|
11
|
+
s.description = %q{Use it to build object models in ruby on top of a key value store.}
|
12
|
+
|
13
|
+
s.rubyforge_project = "kvom"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency "rspec"
|
21
|
+
s.add_development_dependency "helpful_configuration"
|
22
|
+
|
23
|
+
s.add_runtime_dependency "couchrest"
|
24
|
+
s.add_runtime_dependency "activemodel"
|
25
|
+
s.add_runtime_dependency "aws-sdk"
|
26
|
+
end
|
data/lib/kvom.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
|
3
|
+
module Kvom; module Adapter
|
4
|
+
|
5
|
+
class Base
|
6
|
+
attr_reader :connection_spec
|
7
|
+
|
8
|
+
attr_reader :documents_loaded_counter, :request_counter
|
9
|
+
|
10
|
+
def initialize(connection_spec_raw)
|
11
|
+
@connection_spec = connection_spec_raw.symbolize_keys
|
12
|
+
@documents_loaded_counter = @request_counter = 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def new_document(key, attributes)
|
16
|
+
raise "implement me in subclass!"
|
17
|
+
end
|
18
|
+
|
19
|
+
def save(doc)
|
20
|
+
raise "implement me in subclass!"
|
21
|
+
end
|
22
|
+
|
23
|
+
def get(key)
|
24
|
+
raise "implement me in subclass!"
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy(doc)
|
28
|
+
raise "implement me in subclass!"
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def count_request(num_requests = 1)
|
34
|
+
@request_counter += num_requests
|
35
|
+
result = yield
|
36
|
+
@documents_loaded_counter += Array === result ? result.size : 1
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end ; end # module Kvom
|
43
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'couchrest'
|
2
|
+
require 'kvom/adapter/couchdb_document'
|
3
|
+
|
4
|
+
module Kvom; module Adapter
|
5
|
+
|
6
|
+
class CouchdbAdapter < Kvom::Adapter::Base
|
7
|
+
def new_document(key, attributes)
|
8
|
+
new_document_from_attributes(attributes.merge("_id" => key))
|
9
|
+
end
|
10
|
+
|
11
|
+
def save(doc)
|
12
|
+
doc.couchrest_document.save
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key)
|
16
|
+
doc =
|
17
|
+
begin
|
18
|
+
count_request {database.get(key) }
|
19
|
+
rescue RestClient::ResourceNotFound
|
20
|
+
raise Kvom::NotFound.for_key(key)
|
21
|
+
end
|
22
|
+
CouchdbDocument.new(doc)
|
23
|
+
end
|
24
|
+
|
25
|
+
def query(hash_value, range_value)
|
26
|
+
params =
|
27
|
+
case range_value
|
28
|
+
when Range
|
29
|
+
{
|
30
|
+
:startkey => "#{hash_value}|#{range_value.begin}",
|
31
|
+
:endkey => "#{hash_value}|#{range_value.end}",
|
32
|
+
}
|
33
|
+
else
|
34
|
+
{
|
35
|
+
:key => "#{hash_value}|#{range_value}",
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
params[:include_docs] = true
|
40
|
+
count_request {database.all_docs(params)["rows"]}.map do |entry|
|
41
|
+
new_document_from_attributes(entry["doc"])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy(doc)
|
46
|
+
doc.couchrest_document.destroy
|
47
|
+
end
|
48
|
+
|
49
|
+
module BlobSupport
|
50
|
+
|
51
|
+
def blob_url(id)
|
52
|
+
"#{ database_url }/#{ id }/blob"
|
53
|
+
end
|
54
|
+
|
55
|
+
def blob_attachment(id)
|
56
|
+
document = count_request {database.get(id)}
|
57
|
+
document["_attachments"]["blob"]
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
include BlobSupport
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def database_url
|
67
|
+
connection_spec[:url]
|
68
|
+
end
|
69
|
+
|
70
|
+
def new_document_from_attributes(attributes)
|
71
|
+
couch_doc = CouchRest::Document.new(attributes)
|
72
|
+
couch_doc.database = database
|
73
|
+
CouchdbDocument.new(couch_doc)
|
74
|
+
end
|
75
|
+
|
76
|
+
def database
|
77
|
+
@database ||= CouchRest.database(database_url)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end ; end # module Kvom
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Kvom; module Adapter
|
2
|
+
|
3
|
+
class CouchdbDocument < Document
|
4
|
+
attr_accessor :couchrest_document
|
5
|
+
|
6
|
+
def initialize(couchrest_document)
|
7
|
+
self.couchrest_document = couchrest_document
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](name)
|
11
|
+
couchrest_document[name]
|
12
|
+
end
|
13
|
+
|
14
|
+
def []=(name, value)
|
15
|
+
couchrest_document[name] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def key
|
19
|
+
couchrest_document.id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end ; end # module Kvom
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'aws-sdk'
|
2
|
+
require 'kvom/adapter/dynamodb_document'
|
3
|
+
|
4
|
+
module Kvom; module Adapter
|
5
|
+
|
6
|
+
class DynamodbAdapter < Kvom::Adapter::Base
|
7
|
+
def new_document(key, attributes = nil)
|
8
|
+
DynamodbDocument.new_document(attributes, key, item_for_key(key))
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(key)
|
12
|
+
item = item_for_key(key)
|
13
|
+
count_request {item.exists?} or raise Kvom::NotFound.for_key(key)
|
14
|
+
DynamodbDocument.document_with_item(key, item)
|
15
|
+
end
|
16
|
+
|
17
|
+
def save(doc)
|
18
|
+
doc.save
|
19
|
+
end
|
20
|
+
|
21
|
+
def destroy(doc)
|
22
|
+
doc.destroy
|
23
|
+
end
|
24
|
+
|
25
|
+
def query(hash_value, range_value)
|
26
|
+
count_request do
|
27
|
+
case range_value
|
28
|
+
when String
|
29
|
+
item = item_for_hash_and_range(hash_value, range_value)
|
30
|
+
attributes = item.attributes.to_h
|
31
|
+
if attributes.empty?
|
32
|
+
[]
|
33
|
+
else
|
34
|
+
key = key_from_item(item)
|
35
|
+
[DynamodbDocument.document_from_query(attributes, key, item)]
|
36
|
+
end
|
37
|
+
when Range
|
38
|
+
dynamo_range_start = dynamo_range_value(range_value.begin)
|
39
|
+
dynamo_range_end = dynamo_range_value(range_value.end)
|
40
|
+
|
41
|
+
query_options = {
|
42
|
+
:hash_value => partitioned_hash_value(hash_value),
|
43
|
+
:range_value => Range.new(dynamo_range_start, dynamo_range_end),
|
44
|
+
:select => :all,
|
45
|
+
}
|
46
|
+
table.items.query(query_options).map do |item_data|
|
47
|
+
item = item_data.item
|
48
|
+
key = key_from_item(item)
|
49
|
+
DynamodbDocument.document_from_query(item_data.attributes, key, item)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def table
|
58
|
+
@table ||= begin
|
59
|
+
# TODO define dynamo endpoint externally?
|
60
|
+
AWS.config(:dynamo_db_endpoint => 'dynamodb.eu-west-1.amazonaws.com')
|
61
|
+
|
62
|
+
# TODO: explicit nil testing
|
63
|
+
credentials = {
|
64
|
+
:access_key_id => connection_spec[:access_key_id],
|
65
|
+
:secret_access_key => connection_spec[:secret_access_key],
|
66
|
+
}
|
67
|
+
table = AWS::DynamoDB.new(credentials).tables[connection_spec[:table]]
|
68
|
+
table.hash_key = [:hash_key, :string]
|
69
|
+
table.range_key = [:range_key, :string]
|
70
|
+
table
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def key_from_item(item)
|
75
|
+
hash_value, range_value = item.hash_value, item.range_value
|
76
|
+
range_value = "" if range_value == " " # range_from_dynamo_range
|
77
|
+
hash_value = hash_value[(partition.length + 1)..-1] if partition # unpartitioned_hash_value
|
78
|
+
[hash_value, range_value].join("|")
|
79
|
+
end
|
80
|
+
|
81
|
+
def item_for_key(key)
|
82
|
+
item_for_hash_and_range(*(key.split("|", 2)))
|
83
|
+
end
|
84
|
+
|
85
|
+
def item_for_hash_and_range(hash, range)
|
86
|
+
table.items[partitioned_hash_value(hash), dynamo_item_range_value(range)]
|
87
|
+
end
|
88
|
+
|
89
|
+
def dynamo_item_range_value(range_value)
|
90
|
+
# conflict with the empty range value
|
91
|
+
range_value == " " and raise "Unexpected range <single space>"
|
92
|
+
dynamo_range_value(range_value)
|
93
|
+
end
|
94
|
+
|
95
|
+
def dynamo_range_value(range_value)
|
96
|
+
case range_value || ""
|
97
|
+
when ""
|
98
|
+
# range_key is required and must not be empty
|
99
|
+
" "
|
100
|
+
else
|
101
|
+
range_value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def partitioned_hash_value(hash_value)
|
106
|
+
return hash_value unless partition
|
107
|
+
"#{partition}|#{hash_value}"
|
108
|
+
end
|
109
|
+
|
110
|
+
def partition
|
111
|
+
@partition ||= connection_spec[:partition]
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end ; end # module Kvom
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Kvom; module Adapter
|
2
|
+
|
3
|
+
class DynamodbDocument < Document
|
4
|
+
class << self
|
5
|
+
def new_document(attributes, key, item)
|
6
|
+
new(key, item, :attributes => attributes)
|
7
|
+
end
|
8
|
+
|
9
|
+
def document_with_item(key, item)
|
10
|
+
new(key, item)
|
11
|
+
end
|
12
|
+
|
13
|
+
def document_from_query(attributes, key, item)
|
14
|
+
new(key, item, :item_attributes => attributes)
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :key
|
20
|
+
|
21
|
+
def initialize(key, item, provided_attributes = nil)
|
22
|
+
@key = key
|
23
|
+
@dynamo_item = item
|
24
|
+
if provided_attributes
|
25
|
+
if provided_attributes.key?(:attributes)
|
26
|
+
@kv = provided_attributes[:attributes] || {}
|
27
|
+
@modified = true
|
28
|
+
elsif provided_attributes.key?(:item_attributes)
|
29
|
+
@kv = attributes_from_item_attributes(provided_attributes[:item_attributes])
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def [](name)
|
35
|
+
kv[name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def []=(name, value)
|
39
|
+
case value
|
40
|
+
when String, Fixnum, nil
|
41
|
+
# okay ...
|
42
|
+
else
|
43
|
+
raise "Unsupported value type: #{value.class.name}"
|
44
|
+
end
|
45
|
+
@modified = true
|
46
|
+
kv[name] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def save
|
50
|
+
return unless modified?
|
51
|
+
dynamo_attributes = @kv.inject({}) do |memo, (key, value)|
|
52
|
+
case value
|
53
|
+
when nil
|
54
|
+
# skip
|
55
|
+
when String, Fixnum, BigDecimal
|
56
|
+
memo[key] = value
|
57
|
+
else
|
58
|
+
raise "Unsupported value type #{value.class.to_s}"
|
59
|
+
end
|
60
|
+
memo
|
61
|
+
end
|
62
|
+
dynamo_attributes["hash_key"] = @dynamo_item.hash_value
|
63
|
+
dynamo_attributes["range_key"] = @dynamo_item.range_value
|
64
|
+
dynamo_attributes["@rev"] = new_revision = (@rev || 0) + 1
|
65
|
+
# might be improved to saving only changed attributes
|
66
|
+
@dynamo_item.table.items.put(dynamo_attributes, revision_condition)
|
67
|
+
@rev = new_revision
|
68
|
+
@modified = false
|
69
|
+
end
|
70
|
+
|
71
|
+
def destroy
|
72
|
+
@dynamo_item.delete(revision_condition)
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def modified?
|
78
|
+
@modified
|
79
|
+
end
|
80
|
+
|
81
|
+
def attributes_from_item_attributes(item_attributes)
|
82
|
+
attributes = AttributesFromItemAttributes.from(item_attributes)
|
83
|
+
@rev = attributes["@rev"]
|
84
|
+
attributes
|
85
|
+
end
|
86
|
+
|
87
|
+
def kv
|
88
|
+
@kv ||= attributes_from_item_attributes(@dynamo_item.attributes.to_h)
|
89
|
+
end
|
90
|
+
|
91
|
+
def revision
|
92
|
+
@rev
|
93
|
+
end
|
94
|
+
|
95
|
+
def revision_condition
|
96
|
+
dynamo_condition("@rev" => @rev)
|
97
|
+
end
|
98
|
+
|
99
|
+
def dynamo_condition(condition)
|
100
|
+
present_condition = {}
|
101
|
+
missing_condition = []
|
102
|
+
condition.each do |(key, value)|
|
103
|
+
if value
|
104
|
+
present_condition[key] = value
|
105
|
+
else
|
106
|
+
missing_condition << key
|
107
|
+
end
|
108
|
+
end
|
109
|
+
options = {}
|
110
|
+
options[:if] = present_condition unless present_condition.empty?
|
111
|
+
options[:unless] = missing_condition unless missing_condition.empty?
|
112
|
+
options
|
113
|
+
end
|
114
|
+
|
115
|
+
class AttributesFromItemAttributes
|
116
|
+
def self.from(attributes)
|
117
|
+
attributes.delete("hash_key")
|
118
|
+
attributes.delete("range_key")
|
119
|
+
meta = attributes.delete("@meta.json")
|
120
|
+
return attributes unless meta
|
121
|
+
meta = MultiJson.decode(meta)
|
122
|
+
coding = meta["coding"]
|
123
|
+
return attributes if !coding || coding.empty?
|
124
|
+
new(attributes, coding)
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(attributes, coding)
|
128
|
+
@attributes = attributes
|
129
|
+
@coding = coding
|
130
|
+
end
|
131
|
+
|
132
|
+
def [](name)
|
133
|
+
value = @attributes[name]
|
134
|
+
coding = @coding[name]
|
135
|
+
return value unless coding
|
136
|
+
coding == "json" or raise "Unexpected coding #{coding} for attribute #{name}"
|
137
|
+
value = parse_json_value(value)
|
138
|
+
self[name] = value
|
139
|
+
end
|
140
|
+
|
141
|
+
def []=(name, value)
|
142
|
+
@coding.delete(name)
|
143
|
+
@attributes[name] = value
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def parse_json_value(value)
|
149
|
+
case value[0]
|
150
|
+
when ?{
|
151
|
+
MultiJson.decode(value)
|
152
|
+
else
|
153
|
+
MultiJson.decode("{\"key\":#{value}}")["key"]
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end ; end # module Kvom
|
data/lib/kvom/base.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
require 'active_support/core_ext/object/blank'
|
3
|
+
require 'active_support/core_ext/class/attribute'
|
4
|
+
require 'active_model/naming'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
require 'kvom/model_identity'
|
8
|
+
|
9
|
+
module Kvom
|
10
|
+
|
11
|
+
class NotFound < ::StandardError
|
12
|
+
def self.for_key(key)
|
13
|
+
new(%(document with key "#{key}" not found))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Base
|
18
|
+
include ModelIdentity
|
19
|
+
extend ActiveModel::Naming
|
20
|
+
|
21
|
+
class_attribute :key_prefix
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def create(attributes = {})
|
25
|
+
new(attributes).tap { |model| model.save }
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(id)
|
29
|
+
raise "no id given" if id.blank?
|
30
|
+
new(find_document(id))
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_by_id(id)
|
34
|
+
find(id)
|
35
|
+
rescue NotFound
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def property(name_raw)
|
40
|
+
name = name_raw.to_s
|
41
|
+
|
42
|
+
define_method name do
|
43
|
+
read_attribute(name)
|
44
|
+
end
|
45
|
+
define_method "#{name}=" do |value|
|
46
|
+
write_attribute(name, value)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# default implementation for the class_attribute
|
51
|
+
def key_prefix
|
52
|
+
to_s
|
53
|
+
end
|
54
|
+
|
55
|
+
def adapter
|
56
|
+
raise "must be overwritten in subclasses"
|
57
|
+
end
|
58
|
+
|
59
|
+
def key_for(id)
|
60
|
+
"#{key_prefix}/id/#{id}|"
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def find_document(id)
|
66
|
+
adapter.get(key_for(id))
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(doc_or_attrs = {})
|
72
|
+
if doc_or_attrs.is_a?(Document)
|
73
|
+
@document = doc_or_attrs
|
74
|
+
else
|
75
|
+
attrs = doc_or_attrs.stringify_keys
|
76
|
+
|
77
|
+
attrs["id"] ||= SecureRandom.hex(8)
|
78
|
+
|
79
|
+
id = attrs["id"]
|
80
|
+
@document = self.class.adapter.new_document(self.class.key_for(id), attrs)
|
81
|
+
@new = true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def document
|
86
|
+
@document
|
87
|
+
end
|
88
|
+
|
89
|
+
def save
|
90
|
+
self.class.adapter.save(@document)
|
91
|
+
@new = false
|
92
|
+
end
|
93
|
+
|
94
|
+
def id
|
95
|
+
@document["id"]
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_attribute(name)
|
99
|
+
@document[name]
|
100
|
+
end
|
101
|
+
|
102
|
+
def write_attribute(name, value)
|
103
|
+
@document[name] = value
|
104
|
+
end
|
105
|
+
|
106
|
+
def new?
|
107
|
+
@new
|
108
|
+
end
|
109
|
+
|
110
|
+
def destroy
|
111
|
+
self.class.adapter.destroy(@document)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end # module Kvom
|
116
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Kvom
|
2
|
+
|
3
|
+
module ModelIdentity # :nodoc:
|
4
|
+
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
|
5
|
+
# is of the same type and +self+ has an ID and it is equal to +comparison_object.id+.
|
6
|
+
def ==(comparison_object)
|
7
|
+
comparison_object.equal?(self) ||
|
8
|
+
(comparison_object.instance_of?(self.class) && comparison_object.id == id)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Delegates to ==
|
12
|
+
def eql?(comparison_object)
|
13
|
+
self == (comparison_object)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Delegates to id in order to allow two records of the same type and id to work with something like:
|
17
|
+
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
|
18
|
+
def hash
|
19
|
+
id.hash
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end # module Kvom
|
data/spec/base_spec.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'spec_helper'
|
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
|
15
|
+
it "should complain when no adapter is specified" do
|
16
|
+
expect { ModelWithoutAdapter.find("spam") }.to raise_error(/overwritten/)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should provide access to the underlying document" do
|
20
|
+
example = ExampleModel.create(:spam => "foo")
|
21
|
+
example.document["spam"].should == "foo"
|
22
|
+
example.document.should be_a(Kvom::Document)
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "getters and setters" do
|
26
|
+
it "work for freshly created models" do
|
27
|
+
example = ExampleModel.create(:spam => "foo")
|
28
|
+
example.spam.should == "foo"
|
29
|
+
example.spam = "bar"
|
30
|
+
example.spam.should == "bar"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "handle access via string as well as symbols" do
|
34
|
+
example = ExampleModel.create(:spam => "foo")
|
35
|
+
example.spam.should == "foo"
|
36
|
+
example = ExampleModel.create("spam" => "foo")
|
37
|
+
example.spam.should == "foo"
|
38
|
+
end
|
39
|
+
|
40
|
+
it "works for models loaded from database" do
|
41
|
+
example = ExampleModel.create(:spam => "foo")
|
42
|
+
example.save
|
43
|
+
|
44
|
+
loaded = ExampleModel.find(example.id)
|
45
|
+
loaded.spam.should == "foo"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should accept an id as string or symbol" do
|
50
|
+
ExampleModel.create(:id => (first_id = random_id))
|
51
|
+
ExampleModel.create("id" => (second_id = random_id))
|
52
|
+
ExampleModel.find(first_id)
|
53
|
+
ExampleModel.find(second_id)
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "model id's" do
|
57
|
+
it "should use a custom prefix when given" do
|
58
|
+
CustomKeyPrefixModel.new.document.key.should =~ /^spam_prefix\/id\/.+/
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should use a default prefix" do
|
62
|
+
ExampleModel.new.document.key.should =~ /^ExampleModel\/id\/.+/
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "BaseModel#find" do
|
67
|
+
it "should refuse to find with an empty id" do
|
68
|
+
expect { ExampleModel.find("") }.to raise_error /no id given/
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should find model instances" do
|
72
|
+
example = ExampleModel.create(:spam => "foo")
|
73
|
+
ExampleModel.find(example.id).id.should == example.id
|
74
|
+
ExampleModel.find(example.id).spam.should == "foo"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should raise exception when instances cannot be found" do
|
78
|
+
model_id = random_id
|
79
|
+
expect {
|
80
|
+
ExampleModel.find(model_id)
|
81
|
+
}.to raise_error(Kvom::NotFound, %r(document.*"ExampleModel/id/#{model_id}|"/))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "BaseModel#find_by_id" do
|
86
|
+
context "when called with an empty id" do
|
87
|
+
it "raises an error complaining about the missing id" do
|
88
|
+
expect { ExampleModel.find_by_id("") }.to raise_error /no id given/
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context "when called with an id of an existing model" do
|
93
|
+
let(:existing_model) {
|
94
|
+
ExampleModel.create
|
95
|
+
}
|
96
|
+
|
97
|
+
it "returns the model instance" do
|
98
|
+
id = existing_model.id
|
99
|
+
result = ExampleModel.find_by_id(id)
|
100
|
+
result.should be_instance_of(ExampleModel)
|
101
|
+
result.id.should == id
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context "when called with an id where no model exists for" do
|
106
|
+
it "should return nil" do
|
107
|
+
ExampleModel.find_by_id(random_id).should be_nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "destroying models" do
|
113
|
+
|
114
|
+
context "when the model has been created without id" do
|
115
|
+
|
116
|
+
let(:model_without_specified_id) {ExampleModel.create}
|
117
|
+
|
118
|
+
it "destroys the model" do
|
119
|
+
id = model_without_specified_id.id
|
120
|
+
expect {
|
121
|
+
model_without_specified_id.destroy
|
122
|
+
}.to change {
|
123
|
+
ExampleModel.find_by_id(id)
|
124
|
+
}.to nil
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
context "when the model has been created with an id" do
|
130
|
+
|
131
|
+
let(:model_with_id) {ExampleModel.create(:id => rand.to_s)}
|
132
|
+
|
133
|
+
it "destroys the model" do
|
134
|
+
id = model_with_id.id
|
135
|
+
expect {
|
136
|
+
model_with_id.destroy
|
137
|
+
}.to change {
|
138
|
+
ExampleModel.find_by_id(id)
|
139
|
+
}.to nil
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
context "when the model instance has been loaded from database" do
|
145
|
+
|
146
|
+
let(:model_from_db) {ExampleModel.find_by_id(ExampleModel.create.id)}
|
147
|
+
|
148
|
+
it "destroys the model" do
|
149
|
+
id = model_from_db.id
|
150
|
+
expect {
|
151
|
+
model_from_db.destroy
|
152
|
+
}.to change {
|
153
|
+
ExampleModel.find_by_id(id)
|
154
|
+
}.to nil
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#new?" do
|
162
|
+
|
163
|
+
context "when instantiated from attributes" do
|
164
|
+
|
165
|
+
let(:model_from_attributes) {ExampleModel.new({})}
|
166
|
+
|
167
|
+
it "is new" do
|
168
|
+
model_from_attributes.should be_new
|
169
|
+
end
|
170
|
+
|
171
|
+
context "and saved" do
|
172
|
+
|
173
|
+
before do
|
174
|
+
model_from_attributes.save
|
175
|
+
end
|
176
|
+
|
177
|
+
it "is not new" do
|
178
|
+
model_from_attributes.should_not be_new
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
context "including id" do
|
184
|
+
|
185
|
+
let(:model_id) {"static" + rand.to_s}
|
186
|
+
let(:model_from_attributes_including_id) {ExampleModel.new({"id" => model_id})}
|
187
|
+
|
188
|
+
before do
|
189
|
+
model_from_attributes_including_id.id.should == model_id
|
190
|
+
end
|
191
|
+
|
192
|
+
it "is new" do
|
193
|
+
model_from_attributes.should be_new
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
context "when instantiated from database" do
|
200
|
+
|
201
|
+
let(:model_from_database) {ExampleModel.find_by_id(ExampleModel.create.id)}
|
202
|
+
|
203
|
+
it "returns falsy" do
|
204
|
+
model_from_database.should_not be_new
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
|
210
|
+
it "should use ModelIdentity" do
|
211
|
+
ExampleModel.should include(Kvom::ModelIdentity)
|
212
|
+
end
|
213
|
+
|
214
|
+
it "should be extended to use ActiveModel::Naming " do
|
215
|
+
ExampleModel.model_name.should == "ExampleModel"
|
216
|
+
end
|
217
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Kvom::Base do
|
4
|
+
let(:model) { ExampleModel.create }
|
5
|
+
|
6
|
+
it "should count the number of requests" do
|
7
|
+
expect {
|
8
|
+
ExampleModel.find(model.id)
|
9
|
+
}.to change(ExampleModel.adapter, :request_counter).by(1)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should count the number of document loaded" do
|
13
|
+
expect {
|
14
|
+
ExampleModel.find(model.id)
|
15
|
+
}.to change(ExampleModel.adapter, :documents_loaded_counter).by(1)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
require "kvom/model_identity"
|
4
|
+
|
5
|
+
describe Kvom::ModelIdentity do
|
6
|
+
class IdTest
|
7
|
+
include Kvom::ModelIdentity
|
8
|
+
attr_accessor :id
|
9
|
+
|
10
|
+
def initialize(id)
|
11
|
+
self.id = id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should be identical iff it's id is identical" do
|
16
|
+
IdTest.new("foo").should == IdTest.new("foo")
|
17
|
+
IdTest.new("foo").should_not == IdTest.new("bar")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should be eql iff it's id is identical" do
|
21
|
+
IdTest.new("foo").should be_eql IdTest.new("foo")
|
22
|
+
IdTest.new("foo").should_not be_eql IdTest.new("bar")
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be usable as hash keys" do
|
26
|
+
hash = {IdTest.new("foo") => "FOO", IdTest.new("bar") => "BAR"}
|
27
|
+
hash[IdTest.new("foo")].should == "FOO"
|
28
|
+
hash[IdTest.new("bar")].should == "BAR"
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
$LOAD_PATH.unshift(Pathname(__FILE__) + "../lib")
|
2
|
+
|
3
|
+
require 'pry'
|
4
|
+
|
5
|
+
require 'kvom'
|
6
|
+
|
7
|
+
class TestModel < Kvom::Base
|
8
|
+
def self.adapter
|
9
|
+
@adapter ||=
|
10
|
+
case (type = ENV['KVOM_ADAPTER_TYPE'] || "couch")
|
11
|
+
when "couch"
|
12
|
+
require "kvom/adapter/couchdb_adapter"
|
13
|
+
require 'multi_json'
|
14
|
+
MultiJson.engine = :yajl
|
15
|
+
database_name = "kvom-unit_tests"
|
16
|
+
CouchRest.database!(database_name)
|
17
|
+
Kvom::Adapter::CouchdbAdapter.new({
|
18
|
+
:url => database_name,
|
19
|
+
})
|
20
|
+
when /dynamo/
|
21
|
+
require "kvom/adapter/dynamodb_adapter"
|
22
|
+
require File.expand_path("../../../tasks/support/local_config", __FILE__)
|
23
|
+
options = {
|
24
|
+
:table => "test-kvom",
|
25
|
+
:access_key_id => local_config["aws_access_key_id"],
|
26
|
+
:secret_access_key => local_config["aws_secret_access_key"],
|
27
|
+
}
|
28
|
+
options[:partition] = "korb" if type == "partitioned_dynamo"
|
29
|
+
Kvom::Adapter::DynamodbAdapter.new(options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ExampleModel < TestModel
|
35
|
+
property :spam
|
36
|
+
end
|
37
|
+
|
metadata
ADDED
@@ -0,0 +1,166 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: kvom
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: -215055160403
|
5
|
+
prerelease: 6
|
6
|
+
segments:
|
7
|
+
- 6
|
8
|
+
- 8
|
9
|
+
- 0
|
10
|
+
- beta
|
11
|
+
- 200
|
12
|
+
- 566
|
13
|
+
- d
|
14
|
+
- 1
|
15
|
+
- df
|
16
|
+
- 6
|
17
|
+
- eb
|
18
|
+
version: 6.8.0.beta.200.566.d1df6eb
|
19
|
+
platform: ruby
|
20
|
+
authors:
|
21
|
+
- Kristian Hanekamp, Infopark AG
|
22
|
+
autorequire:
|
23
|
+
bindir: bin
|
24
|
+
cert_chain: []
|
25
|
+
|
26
|
+
date: 2012-04-26 00:00:00 +02:00
|
27
|
+
default_executable:
|
28
|
+
dependencies:
|
29
|
+
- !ruby/object:Gem::Dependency
|
30
|
+
name: rspec
|
31
|
+
prerelease: false
|
32
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
hash: 3
|
38
|
+
segments:
|
39
|
+
- 0
|
40
|
+
version: "0"
|
41
|
+
type: :development
|
42
|
+
version_requirements: *id001
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: helpful_configuration
|
45
|
+
prerelease: false
|
46
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
type: :development
|
56
|
+
version_requirements: *id002
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: couchrest
|
59
|
+
prerelease: false
|
60
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 3
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
type: :runtime
|
70
|
+
version_requirements: *id003
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: activemodel
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
hash: 3
|
80
|
+
segments:
|
81
|
+
- 0
|
82
|
+
version: "0"
|
83
|
+
type: :runtime
|
84
|
+
version_requirements: *id004
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
name: aws-sdk
|
87
|
+
prerelease: false
|
88
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
hash: 3
|
94
|
+
segments:
|
95
|
+
- 0
|
96
|
+
version: "0"
|
97
|
+
type: :runtime
|
98
|
+
version_requirements: *id005
|
99
|
+
description: Use it to build object models in ruby on top of a key value store.
|
100
|
+
email:
|
101
|
+
- kristian.hanekamp@infopark.de
|
102
|
+
executables: []
|
103
|
+
|
104
|
+
extensions: []
|
105
|
+
|
106
|
+
extra_rdoc_files: []
|
107
|
+
|
108
|
+
files:
|
109
|
+
- .gitignore
|
110
|
+
- Gemfile
|
111
|
+
- Rakefile
|
112
|
+
- kvom.gemspec
|
113
|
+
- lib/kvom.rb
|
114
|
+
- lib/kvom/adapter/base.rb
|
115
|
+
- lib/kvom/adapter/couchdb_adapter.rb
|
116
|
+
- lib/kvom/adapter/couchdb_document.rb
|
117
|
+
- lib/kvom/adapter/dynamodb_adapter.rb
|
118
|
+
- lib/kvom/adapter/dynamodb_document.rb
|
119
|
+
- lib/kvom/base.rb
|
120
|
+
- lib/kvom/document.rb
|
121
|
+
- lib/kvom/model_identity.rb
|
122
|
+
- spec/base_spec.rb
|
123
|
+
- spec/counter_spec.rb
|
124
|
+
- spec/model_identity_spec.rb
|
125
|
+
- spec/spec_helper.rb
|
126
|
+
has_rdoc: true
|
127
|
+
homepage: ""
|
128
|
+
licenses: []
|
129
|
+
|
130
|
+
post_install_message:
|
131
|
+
rdoc_options: []
|
132
|
+
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
hash: 3
|
141
|
+
segments:
|
142
|
+
- 0
|
143
|
+
version: "0"
|
144
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ">"
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 25
|
150
|
+
segments:
|
151
|
+
- 1
|
152
|
+
- 3
|
153
|
+
- 1
|
154
|
+
version: 1.3.1
|
155
|
+
requirements: []
|
156
|
+
|
157
|
+
rubyforge_project: kvom
|
158
|
+
rubygems_version: 1.6.2
|
159
|
+
signing_key:
|
160
|
+
specification_version: 3
|
161
|
+
summary: Key Value Object Mapper
|
162
|
+
test_files:
|
163
|
+
- spec/base_spec.rb
|
164
|
+
- spec/counter_spec.rb
|
165
|
+
- spec/model_identity_spec.rb
|
166
|
+
- spec/spec_helper.rb
|