kvom 6.8.0.beta.200.681.7c84f4a → 6.8.0.beta.200.713.e5c3150

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  *.gem
2
2
  .bundle
3
- Gemfile.lock
4
- pkg/*
3
+ /Gemfile.lock
4
+ /pkg/*
5
+ /spec/spec.opts
@@ -23,4 +23,6 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency "couchrest"
24
24
  s.add_runtime_dependency "activemodel"
25
25
  s.add_runtime_dependency "aws-sdk"
26
+ s.add_runtime_dependency "multi_json"
27
+ s.add_runtime_dependency "rack" # rack/utils by active_support/cache/file_store
26
28
  end
@@ -0,0 +1,5 @@
1
+ module Kvom
2
+ module Storage
3
+ Kvom.setup_autoload(self, __FILE__)
4
+ end
5
+ end
@@ -0,0 +1,88 @@
1
+ require 'securerandom'
2
+ require 'multi_json'
3
+ require 'active_support/core_ext/hash/keys'
4
+
5
+ module Kvom
6
+ module Storage
7
+ if MultiJson.respond_to?(:dump)
8
+ module MultiJsonCoder
9
+ def encode(value)
10
+ MultiJson.dump(value)
11
+ end
12
+
13
+ def decode(value)
14
+ MultiJson.load(value)
15
+ end
16
+ end
17
+ else
18
+ module MultiJsonCoder
19
+ def encode(value)
20
+ MultiJson.encode(value)
21
+ end
22
+
23
+ def decode(value)
24
+ MultiJson.decode(value)
25
+ end
26
+ end
27
+ end
28
+
29
+ class Base
30
+
31
+ include MultiJsonCoder
32
+
33
+ attr_reader :config
34
+
35
+ def initialize(config = nil)
36
+ @config =
37
+ if config
38
+ config.symbolize_keys
39
+ else
40
+ {}
41
+ end
42
+ end
43
+
44
+ def put(data)
45
+ id = generate_id
46
+ json_string = encode(data)
47
+ normalized = decode(json_string)
48
+ write(id, normalized, json_string)
49
+ {"id" => id}
50
+ end
51
+
52
+ def get(spec)
53
+ raise "Invalid key spec (not a hash): #{spec.inspect}" unless Hash === spec
54
+ id =
55
+ case
56
+ when spec.key?("id")
57
+ spec["id"]
58
+ when spec.key?(:id)
59
+ spec[:id]
60
+ else
61
+ raise "Invalid key spec (no id): #{spec.inspect}"
62
+ end
63
+ read(id)
64
+ end
65
+
66
+ private
67
+
68
+ def write(id, data, json_data)
69
+ raise "implement me in subclass!"
70
+ end
71
+
72
+ def read(id)
73
+ raise "implement me in subclass!"
74
+ end
75
+
76
+ if RUBY_VERSION < "1.9.2"
77
+ def generate_id
78
+ SecureRandom.base64(15).tr("+/", "-_")
79
+ end
80
+ else
81
+ def generate_id
82
+ SecureRandom.urlsafe_base64(15)
83
+ end
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,24 @@
1
+ class Kvom::Storage::CacheWithPrefix
2
+
3
+ def initialize(cache, prefix)
4
+ @prefix = prefix
5
+ @cache = cache
6
+ end
7
+
8
+ def read(id)
9
+ cache.read(cache_id(id))
10
+ end
11
+
12
+ def write(id, value)
13
+ cache.write(cache_id(id), value)
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :cache, :prefix
19
+
20
+ def cache_id(id)
21
+ "#{prefix}#{id}"
22
+ end
23
+
24
+ end
@@ -0,0 +1,27 @@
1
+ # require 'active_support/cache/file_store' # fails to autoload Cache::Store (no separate file)
2
+ require 'active_support/cache'
3
+
4
+ module Kvom
5
+ module Storage
6
+ class FileSystemStorage < Base
7
+ def initialize(options)
8
+ super
9
+ path = config[:path] or raise "No path specified"
10
+ @storage = ActiveSupport::Cache::FileStore.new(path, options)
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :storage
16
+
17
+ def read(id)
18
+ storage.read(id)
19
+ end
20
+
21
+ def write(id, value, ignored_json_string)
22
+ storage.write(id, value)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,6 @@
1
+ module Kvom
2
+ module Storage
3
+ module NotFound
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,73 @@
1
+ require 'multi_json'
2
+ require 'aws-sdk'
3
+
4
+ module ::Kvom::Storage
5
+ class S3Storage < Base
6
+
7
+ def initialize(options)
8
+ super
9
+ @bucket = config[:bucket] or raise "No bucket specified"
10
+ if "" != (bucket_prefix = config[:bucket_prefix].to_s)
11
+ @bucket_prefix = bucket_prefix
12
+ end
13
+
14
+ if (cache = config[:cache])
15
+ cache_prefix = config[:cache_prefix].to_s
16
+ @cache =
17
+ if "" != cache_prefix
18
+ CacheWithPrefix.new(cache, cache_prefix)
19
+ else
20
+ cache
21
+ end
22
+ extend CacheAware
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :bucket, :bucket_prefix, :cache
29
+
30
+ def read(id)
31
+ decode(s3_read(id))
32
+ end
33
+
34
+ def write(id, object, json_string)
35
+ s3_object(id).write(json_string)
36
+ end
37
+
38
+ def s3_read(id)
39
+ s3_object(id).read
40
+ rescue AWS::S3::Errors::NoSuchKey => e
41
+ e.extend NotFound
42
+ raise e
43
+ end
44
+
45
+ def s3_object(id)
46
+ @s3_objects ||= bucket.objects
47
+ s3_id = bucket_prefix ? "#{bucket_prefix}/#{id}" : id
48
+ @s3_objects[s3_id]
49
+ end
50
+
51
+ module CacheAware
52
+
53
+ private
54
+
55
+ def read(id)
56
+ object = cache.read(id)
57
+ if nil == object
58
+ object = super
59
+ cache.write(id, object)
60
+ end
61
+ object
62
+ end
63
+
64
+ def write(id, object, json_string)
65
+ super
66
+ cache.write(id, object)
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kvom::Storage::CacheWithPrefix do
4
+ let(:wrapped_cache) {mock("cache", :read => nil, :write => nil)}
5
+ let(:cache_with_prefix) {Kvom::Storage::CacheWithPrefix.new(wrapped_cache, "some_prefix&")}
6
+
7
+ describe "#read" do
8
+ it "delegates to the wrapped cache with a prefixed cache id" do
9
+ wrapped_cache.should_receive(:read).with("some_prefix&x_id").and_return("cached_value")
10
+ cache_with_prefix.read("x_id").should == "cached_value"
11
+ end
12
+ end
13
+
14
+ describe "#write" do
15
+ it "delegates to the wrapped cache with a prefixed cache id" do
16
+ wrapped_cache.should_receive(:write).with("some_prefix&x_id", "the value")
17
+ cache_with_prefix.write("x_id", "the value")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe Kvom::Storage::FileSystemStorage do
5
+ STORAGE_TEST_DIR = "/tmp/kvom_fs_storage"
6
+
7
+ before(:all) do
8
+ FileUtils.rm_rf(STORAGE_TEST_DIR)
9
+ end
10
+
11
+ after(:all) do
12
+ FileUtils.rm_rf(STORAGE_TEST_DIR)
13
+ end
14
+
15
+ let(:storage) {Kvom::Storage::FileSystemStorage.new(:path => "/tmp/kvom_fs_storage")}
16
+
17
+ it "persists data at the given file system path" do
18
+ expect {
19
+ storage.put({"some" => "data"})
20
+ }.to change {
21
+ Dir.glob("#{STORAGE_TEST_DIR}/**").count
22
+ }
23
+ end
24
+
25
+ it "returns persisted data" do
26
+ (reference = storage.put({"more" => "data"})).should have_key("id")
27
+ storage.get(reference).should == {"more" => "data"}
28
+ end
29
+
30
+ context "when instantiated without a path" do
31
+ it "raises an error" do
32
+ expect {
33
+ Kvom::Storage::FileSystemStorage.new(nil)
34
+ }.to raise_error(/\bpath\b/)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,178 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kvom::Storage::S3Storage do
4
+
5
+ let(:bucket) { mock(:objects => s3_objects) }
6
+ let(:s3_objects) { mock("s3_objects", :[] => s3_object) }
7
+ let(:s3_object) { mock("s3_object", :read => "{}", :write => nil) }
8
+ let(:different_s3_object) { mock("another_s3_object", :read => "{}", :write => nil) }
9
+
10
+ let(:storage) { Kvom::Storage::S3Storage.new(:bucket => bucket) }
11
+ let(:value) {[{:foo => :bar}, {'baz' => 123}]}
12
+ let(:desymbolized_value) {[{"foo" => "bar"}, {'baz' => 123}]}
13
+ let(:json_value) {'[{"foo":"bar"},{"baz":123}]'}
14
+ let(:secure_random_method) {RUBY_VERSION < "1.9.2" ? :base64 : :urlsafe_base64}
15
+
16
+
17
+ before do
18
+ SecureRandom.stub(secure_random_method).and_return('random_id')
19
+ end
20
+
21
+
22
+ describe '#put' do
23
+ it 'stores the value as JSON to S3 with a new random id and returns the id' do
24
+ SecureRandom.should_receive(secure_random_method).and_return('random_id')
25
+ bucket.objects.should_receive(:[]).with('random_id').and_return(s3_object)
26
+ s3_object.should_receive(:write).with(json_value)
27
+ storage.put(value)
28
+ end
29
+
30
+ it 'returns the random id' do
31
+ SecureRandom.should_receive(secure_random_method).and_return('unique_id')
32
+ storage.put([{:foo => :bar}, {'baz' => 123}]).should == {"id" => 'unique_id'}
33
+ end
34
+ end
35
+
36
+ describe '#get' do
37
+ context "when the lookup with the id in the bucket is successful" do
38
+ before do
39
+ bucket.objects.should_receive(:[]).with('the_id').and_return(s3_object)
40
+ s3_object.should_receive(:read).and_return(json_value)
41
+ end
42
+
43
+ it "returns the value" do
44
+ storage.get({"id" => "the_id"}).should == desymbolized_value
45
+ end
46
+ end
47
+
48
+ context "when the id does not exist in the bucket" do
49
+ before do
50
+ bucket.objects.should_receive(:[]).with('the_id').and_return(different_s3_object)
51
+ different_s3_object.should_receive(:read).and_raise(AWS::S3::Errors::NoSuchKey.new(nil, nil))
52
+ end
53
+
54
+ it "raise a Kvom::Storage::NotFound" do
55
+ expect {
56
+ storage.get({"id" => "the_id"})
57
+ }.to raise_error(Kvom::Storage::NotFound)
58
+ end
59
+ end
60
+ end
61
+
62
+ context "when the storage has been configured with a bucket prefix" do
63
+ let(:storage) { Kvom::Storage::S3Storage.new(:bucket => bucket, :bucket_prefix => "bp") }
64
+
65
+ describe '#put' do
66
+ it "stores with a random id within the s3 folder with the given prefix" do
67
+ bucket.objects.should_receive(:[]).with('bp/random_id').and_return(different_s3_object)
68
+ different_s3_object.should_receive(:write)
69
+ storage.put(value)
70
+ end
71
+
72
+ it 'returns the random id without a prefix' do
73
+ SecureRandom.should_receive(secure_random_method).and_return('unique_id')
74
+ storage.put(value).should == {"id" => "unique_id"}
75
+ end
76
+ end
77
+
78
+ describe '#get' do
79
+ it "does the s3 lookup with the bucket prefix" do
80
+ bucket.objects.should_receive(:[]).with('bp/the_id').and_return(different_s3_object)
81
+ different_s3_object.should_receive(:read).and_return('{"some":"data"}')
82
+ storage.get({"id" => "the_id"}).should == {"some" => "data"}
83
+ end
84
+ end
85
+ end
86
+
87
+ context "with an additional cache" do
88
+ let(:cache) { mock("cache", :read => {}, :write => nil) }
89
+ let(:storage) { Kvom::Storage::S3Storage.new(:bucket => bucket, :cache => cache) }
90
+
91
+ describe "#put" do
92
+
93
+ it 'writes the stored value to the cache with the same random id' do
94
+ cache.should_receive(:write).with('random_id', anything).ordered
95
+ storage.put(value)
96
+ end
97
+
98
+ it 'writes the value desymbolized to the cache in order to have'\
99
+ ' a cache hit return the value identical the one returned by S3 in case of a cache miss' do
100
+ value.first.should have_key(:foo)
101
+ value.first.should_not have_key("foo")
102
+ desymbolized_value.first.should_not have_key(:foo)
103
+ desymbolized_value.first.should have_key("foo")
104
+
105
+ cache.should_receive(:write).with('random_id', desymbolized_value)
106
+ storage.put(value)
107
+ end
108
+
109
+ it 'writes the stored value to the cache after it has been stored to S3' do
110
+ s3_object.should_receive(:write).ordered
111
+ cache.should_receive(:write).with('random_id', anything).ordered
112
+
113
+ storage.put(value)
114
+ end
115
+ end
116
+
117
+ describe "#get" do
118
+ context "on cache hit" do
119
+ before do
120
+ cache.should_receive(:read).with('the_id').and_return("the cached value")
121
+ end
122
+
123
+ it "returns the cached value without ever accessing s3" do
124
+ s3_object.should_not_receive(:read)
125
+ storage.get({"id" => 'the_id'}).should == "the cached value"
126
+ end
127
+ end
128
+
129
+ context "on cache miss" do
130
+ before do
131
+ cache.should_receive(:read).and_return(nil)
132
+ end
133
+
134
+ it "returns the S3 value" do
135
+ s3_object.should_receive(:read).and_return('{"s3":"value"}')
136
+ storage.get({"id" => 'the_id'}).should == {"s3" => "value"}
137
+ end
138
+
139
+ it "remembers the returned value in the cache" do
140
+ s3_object.should_receive(:read).and_return('{"s3":"value"}')
141
+ cache.should_receive(:write).with("the_id", {"s3" => "value"})
142
+
143
+ storage.get({"id" => 'the_id'})
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ context "with an additional cache with a cache prefix" do
150
+
151
+ let(:cache) { mock("cache", :read => nil, :write => nil) }
152
+ let(:storage) { Kvom::Storage::S3Storage.new(:bucket => bucket, :cache => cache, :cache_prefix => "cp-") }
153
+
154
+ it "reads from cache with the given prefix" do
155
+ cache.should_receive(:read).with("cp-an id")
156
+ storage.get({"id" => "an id"})
157
+ end
158
+
159
+ it "copies to cache with the given prefix" do
160
+ cache.should_receive(:write).with("cp-an id", anything)
161
+
162
+ storage.get({"id" => "an id"})
163
+ end
164
+
165
+ it "writes to cache with the given prefix" do
166
+ cache.should_receive(:write).with("cp-random_id", ["the value"])
167
+ storage.put(["the value"])
168
+ end
169
+ end
170
+
171
+ context "when instantiated without a bucket" do
172
+ it "raises an error" do
173
+ expect {
174
+ Kvom::Storage::S3Storage.new(nil)
175
+ }.to raise_error(/\bbucket\b/)
176
+ end
177
+ end
178
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kvom
3
3
  version: !ruby/object:Gem::Version
4
- hash: 194079586554
4
+ hash: 27133439521
5
5
  prerelease: 6
6
6
  segments:
7
7
  - 6
@@ -9,14 +9,12 @@ version: !ruby/object:Gem::Version
9
9
  - 0
10
10
  - beta
11
11
  - 200
12
- - 681
13
- - 7
12
+ - 713
13
+ - e
14
+ - 5
14
15
  - c
15
- - 84
16
- - f
17
- - 4
18
- - a
19
- version: 6.8.0.beta.200.681.7c84f4a
16
+ - 3150
17
+ version: 6.8.0.beta.200.713.e5c3150
20
18
  platform: ruby
21
19
  authors:
22
20
  - Kristian Hanekamp, Infopark AG
@@ -24,7 +22,7 @@ autorequire:
24
22
  bindir: bin
25
23
  cert_chain: []
26
24
 
27
- date: 2012-06-26 00:00:00 +02:00
25
+ date: 2012-06-29 00:00:00 +02:00
28
26
  default_executable:
29
27
  dependencies:
30
28
  - !ruby/object:Gem::Dependency
@@ -97,6 +95,34 @@ dependencies:
97
95
  name: aws-sdk
98
96
  prerelease: false
99
97
  type: :runtime
98
+ - !ruby/object:Gem::Dependency
99
+ requirement: &id006 !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ hash: 3
105
+ segments:
106
+ - 0
107
+ version: "0"
108
+ version_requirements: *id006
109
+ name: multi_json
110
+ prerelease: false
111
+ type: :runtime
112
+ - !ruby/object:Gem::Dependency
113
+ requirement: &id007 !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ hash: 3
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ version_requirements: *id007
123
+ name: rack
124
+ prerelease: false
125
+ type: :runtime
100
126
  description: Use it to build object models in ruby on top of a key value store.
101
127
  email:
102
128
  - kristian.hanekamp@infopark.de
@@ -122,10 +148,19 @@ files:
122
148
  - lib/kvom/document.rb
123
149
  - lib/kvom/model_identity.rb
124
150
  - lib/kvom/not_found.rb
125
- - spec/base_spec.rb
126
- - spec/counter_spec.rb
127
- - spec/model_identity_spec.rb
151
+ - lib/kvom/storage.rb
152
+ - lib/kvom/storage/base.rb
153
+ - lib/kvom/storage/cache_with_prefix.rb
154
+ - lib/kvom/storage/file_system_storage.rb
155
+ - lib/kvom/storage/not_found.rb
156
+ - lib/kvom/storage/s3_storage.rb
157
+ - spec/adaptor/base_spec.rb
158
+ - spec/adaptor/counter_spec.rb
159
+ - spec/adaptor/model_identity_spec.rb
160
+ - spec/cache_with_prefix_spec.rb
128
161
  - spec/spec_helper.rb
162
+ - spec/storage/file_system_spec.rb
163
+ - spec/storage/s3_spec.rb
129
164
  has_rdoc: true
130
165
  homepage: ""
131
166
  licenses: []
@@ -163,7 +198,10 @@ signing_key:
163
198
  specification_version: 3
164
199
  summary: Key Value Object Mapper
165
200
  test_files:
166
- - spec/base_spec.rb
167
- - spec/counter_spec.rb
168
- - spec/model_identity_spec.rb
201
+ - spec/adaptor/base_spec.rb
202
+ - spec/adaptor/counter_spec.rb
203
+ - spec/adaptor/model_identity_spec.rb
204
+ - spec/cache_with_prefix_spec.rb
169
205
  - spec/spec_helper.rb
206
+ - spec/storage/file_system_spec.rb
207
+ - spec/storage/s3_spec.rb