kvom 6.8.0.beta.200.809.bdfa8c3 → 6.8.0.beta.200.856.8c0fec5
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +8 -3
- data/kvom.gemspec +6 -6
- data/lib/kvom.rb +2 -1
- data/lib/kvom/adapter.rb +4 -4
- data/lib/kvom/adapter/base.rb +5 -0
- data/lib/kvom/adapter/couchdb_adapter.rb +18 -16
- data/lib/kvom/adapter/couchdb_document.rb +6 -2
- data/lib/kvom/adapter/dynamodb_adapter.rb +6 -2
- data/lib/kvom/adapter/dynamodb_document.rb +27 -51
- data/lib/kvom/adapter/filesystem_adapter.rb +130 -0
- data/lib/kvom/adapter/filesystem_document.rb +11 -0
- data/lib/kvom/adapter/key_attributes_document.rb +26 -0
- data/lib/kvom/base.rb +3 -0
- data/lib/kvom/document.rb +7 -1
- data/lib/kvom/lib.rb +5 -0
- data/lib/kvom/lib/json.rb +14 -0
- data/lib/kvom/lib/json_value.rb +39 -0
- data/lib/kvom/model_identity.rb +2 -0
- data/lib/kvom/not_found.rb +2 -0
- data/lib/kvom/storage.rb +4 -4
- data/lib/kvom/storage/base.rb +50 -72
- data/lib/kvom/storage/cache_with_prefix.rb +7 -1
- data/lib/kvom/storage/file_system_storage.rb +18 -17
- data/lib/kvom/storage/not_found.rb +3 -6
- data/lib/kvom/storage/s3_storage.rb +55 -53
- data/spec/adaptor/base_spec.rb +87 -0
- data/spec/lib/json_spec.rb +30 -0
- data/spec/spec_helper.rb +15 -3
- data/tmp/.gitignore +3 -0
- metadata +47 -52
data/Gemfile
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
2
|
|
3
|
-
gem "
|
4
|
-
gem "
|
3
|
+
gem "activesupport" # adapter (core), storage (core), file_system_storage
|
4
|
+
gem "couchrest" # couchdb_adapter
|
5
|
+
gem "aws-sdk" # dynamodb_adapter, s3_storage
|
6
|
+
gem "rack" # file_system_storage
|
7
|
+
# active_support/cache/file_store requires rack/utils
|
8
|
+
|
9
|
+
# not that if any kvom source depends on it ;-)
|
10
|
+
gem "pry" # the debugging shell
|
5
11
|
|
6
|
-
# Specify your gem's dependencies in kvom.gemspec
|
7
12
|
gemspec
|
data/kvom.gemspec
CHANGED
@@ -20,12 +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
|
-
s.
|
23
|
+
s.add_runtime_dependency "multi_json", "~> 1"
|
24
|
+
s.add_runtime_dependency "activesupport", ">= 3" # does not support rational versioning, does it?
|
25
|
+
s.add_runtime_dependency "activemodel", ">= 3" # adapters
|
26
|
+
|
27
|
+
s.add_development_dependency "rspec", "~> 2.8"
|
24
28
|
s.add_development_dependency "helpful_configuration"
|
29
|
+
s.add_development_dependency "yajl-ruby" # a specific json provider
|
25
30
|
|
26
|
-
s.add_runtime_dependency "couchrest"
|
27
|
-
s.add_runtime_dependency "activemodel"
|
28
|
-
s.add_runtime_dependency "aws-sdk"
|
29
|
-
s.add_runtime_dependency "multi_json"
|
30
|
-
s.add_runtime_dependency "rack" # rack/utils by active_support/cache/file_store
|
31
31
|
end
|
data/lib/kvom.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
module Kvom
|
2
2
|
# :nodoc:
|
3
3
|
def self.setup_autoload(mod, mod_source)
|
4
|
+
@base_dir ||= File.expand_path("..", __FILE__).to_s + "/"
|
4
5
|
dir = File.expand_path(".", mod_source)[0..-4]
|
5
6
|
pattern = "#{dir}/*.rb"
|
6
7
|
Dir.glob(pattern).each do |file|
|
7
8
|
const = file[dir.length..-4].gsub(%r{[_/](.)}) {$1.upcase}
|
8
|
-
mod.autoload const.to_sym, file
|
9
|
+
mod.autoload const.to_sym, file[@base_dir.length..-1]
|
9
10
|
end
|
10
11
|
end
|
11
12
|
|
data/lib/kvom/adapter.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
require 'kvom'
|
2
|
+
|
3
|
+
module Kvom::Adapter
|
4
|
+
Kvom.setup_autoload(self, __FILE__)
|
5
5
|
end
|
data/lib/kvom/adapter/base.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'kvom/adapter'
|
1
2
|
require 'active_support/core_ext/hash/keys'
|
2
3
|
|
3
4
|
module Kvom; module Adapter
|
@@ -28,6 +29,10 @@ class Base
|
|
28
29
|
raise "implement me in subclass!"
|
29
30
|
end
|
30
31
|
|
32
|
+
def query(hash_value, range_value)
|
33
|
+
raise "implement me in subclass!"
|
34
|
+
end
|
35
|
+
|
31
36
|
protected
|
32
37
|
|
33
38
|
def count_request(num_requests = 1)
|
@@ -1,8 +1,10 @@
|
|
1
|
+
require 'kvom/adapter'
|
1
2
|
require 'couchrest'
|
2
3
|
|
3
4
|
module Kvom; module Adapter
|
4
5
|
|
5
6
|
class CouchdbAdapter < Base
|
7
|
+
|
6
8
|
def new_document(key, attributes)
|
7
9
|
new_document_from_attributes(attributes.merge("_id" => key))
|
8
10
|
end
|
@@ -16,7 +18,7 @@ class CouchdbAdapter < Base
|
|
16
18
|
begin
|
17
19
|
count_request {database.get(key) }
|
18
20
|
rescue RestClient::ResourceNotFound
|
19
|
-
raise
|
21
|
+
raise NotFound.for_key(key)
|
20
22
|
end
|
21
23
|
CouchdbDocument.new(doc)
|
22
24
|
end
|
@@ -45,21 +47,6 @@ class CouchdbAdapter < Base
|
|
45
47
|
doc.couchrest_document.destroy
|
46
48
|
end
|
47
49
|
|
48
|
-
module BlobSupport
|
49
|
-
|
50
|
-
def blob_url(id)
|
51
|
-
"#{ database_url }/#{ id }/blob"
|
52
|
-
end
|
53
|
-
|
54
|
-
def blob_attachment(id)
|
55
|
-
document = count_request {database.get(id)}
|
56
|
-
document["_attachments"]["blob"]
|
57
|
-
end
|
58
|
-
|
59
|
-
end
|
60
|
-
|
61
|
-
include BlobSupport
|
62
|
-
|
63
50
|
private
|
64
51
|
|
65
52
|
def database_url
|
@@ -76,6 +63,21 @@ class CouchdbAdapter < Base
|
|
76
63
|
@database ||= CouchRest.database(database_url)
|
77
64
|
end
|
78
65
|
|
66
|
+
module BlobSupport
|
67
|
+
|
68
|
+
def blob_url(id)
|
69
|
+
"#{ database_url }/#{ id }/blob"
|
70
|
+
end
|
71
|
+
|
72
|
+
def blob_attachment(id)
|
73
|
+
document = count_request {database.get(id)}
|
74
|
+
document["_attachments"]["blob"]
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
include BlobSupport
|
80
|
+
|
79
81
|
end
|
80
82
|
|
81
83
|
end; end # module Kvom::Adapter
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require 'kvom/adapter'
|
2
|
+
|
1
3
|
module Kvom; module Adapter
|
2
4
|
|
3
|
-
class
|
5
|
+
class CouchdbDocument < Document
|
6
|
+
|
4
7
|
attr_accessor :couchrest_document
|
5
8
|
|
6
9
|
def initialize(couchrest_document)
|
@@ -18,6 +21,7 @@ class ::CouchdbDocument < Kvom::Document
|
|
18
21
|
def key
|
19
22
|
couchrest_document.id
|
20
23
|
end
|
24
|
+
|
21
25
|
end
|
22
26
|
|
23
|
-
end; end # module Kvom
|
27
|
+
end; end # module Kvom::Adapter
|
@@ -1,16 +1,20 @@
|
|
1
|
+
require 'kvom/adapter'
|
1
2
|
require 'aws-sdk'
|
2
3
|
|
3
4
|
module Kvom; module Adapter
|
4
5
|
|
5
6
|
class DynamodbAdapter < Base
|
7
|
+
|
6
8
|
def new_document(key, attributes = nil)
|
7
9
|
DynamodbDocument.new_document(attributes, key, item_for_key(key))
|
8
10
|
end
|
9
11
|
|
10
12
|
def get(key)
|
11
13
|
item = item_for_key(key)
|
12
|
-
count_request {
|
13
|
-
|
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
|
+
}
|
14
18
|
end
|
15
19
|
|
16
20
|
def save(doc)
|
@@ -1,38 +1,25 @@
|
|
1
|
+
require 'kvom/adapter'
|
2
|
+
|
1
3
|
module Kvom; module Adapter
|
2
4
|
|
3
|
-
class DynamodbDocument <
|
5
|
+
class DynamodbDocument < KeyAttributesDocument
|
4
6
|
class << self
|
5
7
|
def new_document(attributes, key, item)
|
6
|
-
new(key, item,
|
7
|
-
end
|
8
|
-
|
9
|
-
def document_with_item(key, item)
|
10
|
-
new(key, item)
|
8
|
+
new(key, item, attributes, nil)
|
11
9
|
end
|
12
10
|
|
13
|
-
def document_from_query(
|
14
|
-
|
11
|
+
def document_from_query(item_attributes, key, item)
|
12
|
+
attributes = AttributesFromItemAttributes.from(item_attributes)
|
13
|
+
new(key, item, attributes, attributes["@rev"])
|
15
14
|
end
|
16
15
|
|
17
16
|
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
def initialize(key, item, provided_attributes = nil)
|
22
|
-
@key = key
|
18
|
+
def initialize(key, item, attributes, revision)
|
19
|
+
super(key, attributes)
|
23
20
|
@dynamo_item = item
|
24
|
-
|
25
|
-
|
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]
|
21
|
+
@revision = revision
|
22
|
+
@modified = @revision == nil
|
36
23
|
end
|
37
24
|
|
38
25
|
def []=(name, value)
|
@@ -42,18 +29,21 @@ class DynamodbDocument < Document
|
|
42
29
|
else
|
43
30
|
raise "Unsupported value type: #{value.class.name}"
|
44
31
|
end
|
45
|
-
|
46
|
-
|
32
|
+
unless self[name] == value
|
33
|
+
@modified = true
|
34
|
+
super
|
35
|
+
end
|
47
36
|
end
|
48
37
|
|
49
38
|
def save
|
50
39
|
return unless modified?
|
51
|
-
|
40
|
+
raise "Not yet implemented" if AttributesFromItemAttributes === attributes
|
41
|
+
dynamo_attributes = attributes.inject({}) do |memo, (name, value)|
|
52
42
|
case value
|
53
43
|
when nil
|
54
44
|
# skip
|
55
45
|
when String, Fixnum, BigDecimal
|
56
|
-
memo[
|
46
|
+
memo[name] = value
|
57
47
|
else
|
58
48
|
raise "Unsupported value type #{value.class.to_s}"
|
59
49
|
end
|
@@ -64,7 +54,7 @@ class DynamodbDocument < Document
|
|
64
54
|
dynamo_attributes["@rev"] = new_revision = (@rev || 0) + 1
|
65
55
|
# might be improved to saving only changed attributes
|
66
56
|
@dynamo_item.table.items.put(dynamo_attributes, revision_condition)
|
67
|
-
@
|
57
|
+
@revision = new_revision
|
68
58
|
@modified = false
|
69
59
|
end
|
70
60
|
|
@@ -78,32 +68,18 @@ class DynamodbDocument < Document
|
|
78
68
|
@modified
|
79
69
|
end
|
80
70
|
|
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
71
|
def revision_condition
|
96
|
-
dynamo_condition("@rev" => @
|
72
|
+
dynamo_condition("@rev" => @revision)
|
97
73
|
end
|
98
74
|
|
99
75
|
def dynamo_condition(condition)
|
100
76
|
present_condition = {}
|
101
77
|
missing_condition = []
|
102
|
-
condition.each do |(
|
78
|
+
condition.each do |(name, value)|
|
103
79
|
if value
|
104
|
-
present_condition[
|
80
|
+
present_condition[name] = value
|
105
81
|
else
|
106
|
-
missing_condition <<
|
82
|
+
missing_condition << name
|
107
83
|
end
|
108
84
|
end
|
109
85
|
options = {}
|
@@ -118,7 +94,7 @@ class DynamodbDocument < Document
|
|
118
94
|
attributes.delete("range_key")
|
119
95
|
meta = attributes.delete("@meta.json")
|
120
96
|
return attributes unless meta
|
121
|
-
meta =
|
97
|
+
meta = Lib::Json.load(meta)
|
122
98
|
coding = meta["coding"]
|
123
99
|
return attributes if !coding || coding.empty?
|
124
100
|
new(attributes, coding)
|
@@ -136,10 +112,9 @@ class DynamodbDocument < Document
|
|
136
112
|
self[name] =
|
137
113
|
case coding
|
138
114
|
when "json"
|
139
|
-
|
115
|
+
Lib::Json.load(value)
|
140
116
|
when "jsonvalue"
|
141
|
-
|
142
|
-
MultiJson.decode("{\"k\":#{value}}")["k"]
|
117
|
+
Lib::JsonValue.load(value)
|
143
118
|
else
|
144
119
|
raise "Unexpected coding #{coding} for attribute #{name}"
|
145
120
|
end
|
@@ -151,6 +126,7 @@ class DynamodbDocument < Document
|
|
151
126
|
end
|
152
127
|
|
153
128
|
end
|
129
|
+
|
154
130
|
end
|
155
131
|
|
156
132
|
end; end # module Kvom::Adapter
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'kvom/adapter'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Kvom; module Adapter
|
6
|
+
|
7
|
+
class FilesystemAdapter < Base
|
8
|
+
|
9
|
+
EXT = ".doc"
|
10
|
+
EXT_SIZE = EXT.length
|
11
|
+
EXT_OFFSET = -(EXT_SIZE + 1)
|
12
|
+
|
13
|
+
def new_document(key, attributes)
|
14
|
+
# there is no "was loaded from db" yet (e.g. for conditional put)
|
15
|
+
FilesystemDocument.new(key, attributes)
|
16
|
+
end
|
17
|
+
|
18
|
+
def get(key)
|
19
|
+
count_request { document_for_key(key) } or raise NotFound.for_key(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def destroy(doc)
|
23
|
+
FileUtils.rm_f(filepath_for_document(doc))
|
24
|
+
end
|
25
|
+
|
26
|
+
def save(doc)
|
27
|
+
path = filepath_for_document(doc)
|
28
|
+
FileUtils.mkdir_p(path.dirname)
|
29
|
+
# TODO: conditional put
|
30
|
+
File.open(path, "w") {|f| f.write(doc.to_json)}
|
31
|
+
end
|
32
|
+
|
33
|
+
def query(hash_value, range_value)
|
34
|
+
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
|
42
|
+
end
|
43
|
+
pathes_and_range_values.inject([]) do |memo, (path, doc_range_value)|
|
44
|
+
key = [hash_value, doc_range_value].join("|")
|
45
|
+
doc = document_for_key_and_path(key, path)
|
46
|
+
memo << doc if doc
|
47
|
+
memo
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def base_dir
|
54
|
+
@dir ||= begin
|
55
|
+
dir = Pathname.new(connection_spec[:path])
|
56
|
+
FileUtils.mkdir_p(dir)
|
57
|
+
dir
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def document_for_key(key)
|
62
|
+
document_for_key_and_path(key, filepath_for_key(key))
|
63
|
+
end
|
64
|
+
|
65
|
+
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)
|
72
|
+
FilesystemDocument.new(key, Lib::Json.load(serialized_doc))
|
73
|
+
end
|
74
|
+
|
75
|
+
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)))
|
81
|
+
end
|
82
|
+
|
83
|
+
def filepath_for_hash_and_range(hash_value, range_value)
|
84
|
+
dir_for_hash_value(hash_value) + path_component_for_range_value(range_value)
|
85
|
+
end
|
86
|
+
|
87
|
+
def dir_for_hash_value(hash_value)
|
88
|
+
base_dir + path_component_for_value(hash_value)
|
89
|
+
end
|
90
|
+
|
91
|
+
def path_component_for_value(value)
|
92
|
+
# value should not have a pipe character
|
93
|
+
value.gsub("/", "|")
|
94
|
+
end
|
95
|
+
|
96
|
+
def path_component_for_range_value(range_value)
|
97
|
+
path_component_for_value(range_value) << EXT
|
98
|
+
end
|
99
|
+
|
100
|
+
def value_from_path_component(path_component)
|
101
|
+
path_component.to_s.gsub("|", "/")
|
102
|
+
end
|
103
|
+
|
104
|
+
def range_value_from_path_component(path_component)
|
105
|
+
value_from_path_component(path_component.to_s[0..EXT_OFFSET])
|
106
|
+
end
|
107
|
+
|
108
|
+
def single_file_query(hash_value, range_value)
|
109
|
+
path = filepath_for_hash_and_range(hash_value, range_value)
|
110
|
+
[[path, range_value]]
|
111
|
+
end
|
112
|
+
|
113
|
+
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
|
123
|
+
end
|
124
|
+
rescue Errno::ENOENT
|
125
|
+
[]
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
end; end # module Kvom::Adapter
|