jferris-hashpipe 0.0.5.0.1247583913
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/LICENSE +20 -0
- data/README.rdoc +71 -0
- data/Rakefile +47 -0
- data/config/hashpipe.yml +23 -0
- data/init.rb +1 -0
- data/lib/hashpipe/archived_attribute.rb +60 -0
- data/lib/hashpipe/global_configuration.rb +43 -0
- data/lib/hashpipe.rb +96 -0
- data/spec/hashpipe/archived_attribute_spec.rb +170 -0
- data/spec/hashpipe/global_configuration_spec.rb +72 -0
- data/spec/hashpipe_spec.rb +57 -0
- data/spec/spec_helper.rb +19 -0
- metadata +86 -0
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2007 Justin S. Leitgeb
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
= HashPipe
|
|
2
|
+
|
|
3
|
+
Transparently maintains an attribute of an ActiveRecord-backed model in a separate key-value data store such
|
|
4
|
+
as Amazon S3, memcached, Tokyo Cabinet, or the filesystem.
|
|
5
|
+
|
|
6
|
+
== Description
|
|
7
|
+
|
|
8
|
+
HashPipe makes it easy to use an external key-value based data storage system alongside a traditional ActiveRecord-
|
|
9
|
+
backed model. In this sense, it acts as a pipe from the ActiveRecord model to a hash - a hash pipe!
|
|
10
|
+
|
|
11
|
+
It functions by hooking into the native callbacks provided by ActiveRecord models. HashPipe keeps track of when the
|
|
12
|
+
hashed attribute has been written to, and flushes it out to the external data source on saves of the ActiveRecord
|
|
13
|
+
model. Hashed attributes are lazy-loaded, so they aren't pulled from the external data source until they're requested.
|
|
14
|
+
Finally, when the ActiveRecord model is deleted, the hashed attributes for that model are also deleted automatically.
|
|
15
|
+
|
|
16
|
+
HashPipe supports transparent gzip compression/decompression of attribute data and serialization/deserialization
|
|
17
|
+
using Marshal in case you wish to store Ruby objects in the attribute.
|
|
18
|
+
|
|
19
|
+
The backend of HashPipe is implemented using Moneta, which is a universal library for key-value storage systems.
|
|
20
|
+
This means that it supports all of the backend storage types supported by this common library, and that new
|
|
21
|
+
backends are added regularly.
|
|
22
|
+
|
|
23
|
+
== Use Cases
|
|
24
|
+
|
|
25
|
+
There are certain cases where it makes more sense to store data objects in a separate storage system from the RDBMS:
|
|
26
|
+
|
|
27
|
+
* Certain parts of your data set require frequent reads and writes, and need to be fast without the overhead introduced by the ACID properties of traditional relational database systems
|
|
28
|
+
* You store large objects, and don't want to worry about increasing the size of your RDBMS when the size grows out of the space you've allocated
|
|
29
|
+
|
|
30
|
+
= Quick Start
|
|
31
|
+
|
|
32
|
+
Getting started with HashPipe is easy.
|
|
33
|
+
|
|
34
|
+
* Install the gem 'hashpipe' available from github as jsl-hashpipe. If you're using a recent version of Rails you may
|
|
35
|
+
want to add a line like the following to your environment.rb:
|
|
36
|
+
|
|
37
|
+
config.gem "jsl-hashpipe", :lib => 'hashpipe', :source => 'http://gems.github.com'
|
|
38
|
+
|
|
39
|
+
* If you wanted to serialize an attribute 'elephant' in model Foo, you would call hattr :elephant in your model.
|
|
40
|
+
For example, class 'Foo' may look like:
|
|
41
|
+
|
|
42
|
+
class Foo < ActiveRecord::Base
|
|
43
|
+
hattr :elephant
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
* Configure hashpipe by creating a file hashpipe.yml in your projects' config directory according to the
|
|
47
|
+
instructions in the 'Configuration' section of this document
|
|
48
|
+
|
|
49
|
+
* After following the above steps, saving to the model and retrieving from it saves to the backend store
|
|
50
|
+
defined in your options. Likewise, deleting the model triggers a callback to delete the element in the
|
|
51
|
+
associated store.
|
|
52
|
+
|
|
53
|
+
== Configuration
|
|
54
|
+
|
|
55
|
+
HashPipe supports the backends supported by Moneta. Configure the backend that you want to use in your YAML
|
|
56
|
+
configuration file 'hashpipe.yml':
|
|
57
|
+
|
|
58
|
+
test:
|
|
59
|
+
moneta_klass: Moneta::Memory
|
|
60
|
+
|
|
61
|
+
development:
|
|
62
|
+
moneta_klass: Moneta::Memcache
|
|
63
|
+
moneta_options:
|
|
64
|
+
server: localhost:1978
|
|
65
|
+
|
|
66
|
+
You can define a default backend globally, and override it in individual models as needed (e.g., store one
|
|
67
|
+
attribute in the filesystem and another in S3).
|
|
68
|
+
|
|
69
|
+
== Author
|
|
70
|
+
|
|
71
|
+
Justin S. Leitgeb, <justin@phq.org>
|
data/Rakefile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'spec'
|
|
3
|
+
|
|
4
|
+
require 'rake'
|
|
5
|
+
require 'spec/rake/spectask'
|
|
6
|
+
require 'rake/rdoctask'
|
|
7
|
+
|
|
8
|
+
desc 'Test the plugin.'
|
|
9
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
|
10
|
+
t.spec_opts = ["--format", "specdoc", "--colour"]
|
|
11
|
+
t.libs << 'lib'
|
|
12
|
+
t.verbose = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Run all the tests"
|
|
16
|
+
task :default => :spec
|
|
17
|
+
|
|
18
|
+
desc 'Generate documentation for the hashpipe plugin.'
|
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
21
|
+
rdoc.title = 'HashPipe'
|
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
23
|
+
rdoc.rdoc_files.include('README')
|
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
gem_spec = Gem::Specification.new do |s|
|
|
28
|
+
s.name = "hashpipe"
|
|
29
|
+
s.version = "0.0.5.0.#{Time.now.to_i}"
|
|
30
|
+
s.summary = "ActiveRecord plugin to save content to a pluggable, hash-style backend"
|
|
31
|
+
s.email = "justin@phq.org"
|
|
32
|
+
s.homepage = "http://github.com/jsl/hashpipe"
|
|
33
|
+
s.description = "HashPipe connects an AR-backed model to a key-value store"
|
|
34
|
+
s.has_rdoc = true
|
|
35
|
+
s.authors = ["Justin Leitgeb"]
|
|
36
|
+
s.files = FileList['[A-Z]*', '{lib,spec}/**/*.rb', 'init.rb', 'config/hashpipe.yml']
|
|
37
|
+
s.test_files = FileList['spec/**/*.rb']
|
|
38
|
+
s.add_dependency("wycats-moneta", ["> 0.0.0"])
|
|
39
|
+
s.add_dependency("activesupport", ["> 0.0.0"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "Generate a gemspec file"
|
|
43
|
+
task :gemspec do
|
|
44
|
+
File.open("#{gem_spec.name}.gemspec", 'w') do |f|
|
|
45
|
+
f.write gem_spec.to_yaml
|
|
46
|
+
end
|
|
47
|
+
end
|
data/config/hashpipe.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
test:
|
|
2
|
+
storage: filesystem
|
|
3
|
+
s3:
|
|
4
|
+
bucket: test_archived_attributes
|
|
5
|
+
access_key: your access key
|
|
6
|
+
secret_key: your secret key
|
|
7
|
+
protocol: https
|
|
8
|
+
|
|
9
|
+
development:
|
|
10
|
+
storage: s3
|
|
11
|
+
s3:
|
|
12
|
+
bucket: development_archived_attributes
|
|
13
|
+
access_key: your access key
|
|
14
|
+
secret_key: your secret key
|
|
15
|
+
|
|
16
|
+
anotherenv:
|
|
17
|
+
storage: filesystem
|
|
18
|
+
filesystem:
|
|
19
|
+
archive_root: /tmp/archived_attributes
|
|
20
|
+
|
|
21
|
+
qa:
|
|
22
|
+
storage: memcache
|
|
23
|
+
namespace: hashit
|
data/init.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require 'hashpipe'
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'activesupport'
|
|
2
|
+
require 'moneta'
|
|
3
|
+
|
|
4
|
+
module HashPipe
|
|
5
|
+
|
|
6
|
+
class ArchivedAttribute
|
|
7
|
+
attr_reader :name, :backend
|
|
8
|
+
attr_accessor :scope
|
|
9
|
+
|
|
10
|
+
def initialize(name, scope, backend, opts = {})
|
|
11
|
+
@name = name
|
|
12
|
+
@scope = scope
|
|
13
|
+
@dirty = false
|
|
14
|
+
@options = HashPipe::GlobalConfiguration.instance.to_hash.merge(opts)
|
|
15
|
+
@backend = backend
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def value
|
|
19
|
+
val = defined?(@stashed_value) ? @stashed_value : backend[key]
|
|
20
|
+
val = compress? && !val.nil? ? Zlib::Inflate.inflate(val) : val
|
|
21
|
+
val = marshal? ? Marshal.load(val) : val
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def value=(other)
|
|
25
|
+
other = marshal? ? Marshal.dump(other) : other
|
|
26
|
+
other = compress? && !other.nil? ? Zlib::Deflate.deflate(other) : other
|
|
27
|
+
@stashed_value = other
|
|
28
|
+
@dirty = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def dirty?
|
|
32
|
+
@dirty
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save
|
|
36
|
+
backend[key] = @stashed_value if self.dirty?
|
|
37
|
+
@dirty = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def destroy
|
|
41
|
+
backend.delete(key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def options
|
|
45
|
+
@options
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def key
|
|
49
|
+
[scope, name].join('_')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
[:marshal, :compress].each do |sym|
|
|
53
|
+
define_method("#{sym}?") do # def marshal?
|
|
54
|
+
options[sym].nil? ? false : options[sym] # options[:marshal].nil? ? false : options[:marshal]
|
|
55
|
+
end # end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module HashPipe
|
|
2
|
+
|
|
3
|
+
# Singleton class for reading the defaults archived attribute configuration
|
|
4
|
+
# for this environment.
|
|
5
|
+
class GlobalConfiguration
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
DEFAULTS = HashWithIndifferentAccess.new({
|
|
9
|
+
:moneta_klass => 'Moneta::Memory',
|
|
10
|
+
:marshal => false,
|
|
11
|
+
:compress => false
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
def [](val)
|
|
15
|
+
config[val]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_hash
|
|
19
|
+
config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
config.inspect
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def config
|
|
29
|
+
@config ||= HashWithIndifferentAccess.new(
|
|
30
|
+
DEFAULTS.merge(load_yaml_configuration)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@config.dup
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def load_yaml_configuration
|
|
37
|
+
YAML.load_file(
|
|
38
|
+
File.join( RAILS_ROOT, 'config', 'hashpipe.yml' )
|
|
39
|
+
)[RAILS_ENV]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
data/lib/hashpipe.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w[hashpipe archived_attribute] ))
|
|
2
|
+
require File.expand_path(File.join(File.dirname(__FILE__), %w[hashpipe global_configuration] ))
|
|
3
|
+
|
|
4
|
+
module HashPipe
|
|
5
|
+
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(SingletonMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module SingletonMethods
|
|
11
|
+
|
|
12
|
+
def hattr(*args)
|
|
13
|
+
attribute = args.first
|
|
14
|
+
|
|
15
|
+
options = args.extract_options!
|
|
16
|
+
options.reverse_merge! :marshalled => false
|
|
17
|
+
|
|
18
|
+
if archived_attribute_definitions.nil?
|
|
19
|
+
write_inheritable_attribute(:archived_attribute_definitions, {})
|
|
20
|
+
|
|
21
|
+
after_save :save_archived_attributes
|
|
22
|
+
before_destroy :destroy_archived_attributes
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
archived_attribute_definitions[attribute] = options
|
|
26
|
+
|
|
27
|
+
include InstanceMethods
|
|
28
|
+
|
|
29
|
+
define_method attribute do
|
|
30
|
+
archive_stash_for(attribute).value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method "#{attribute}=" do |value|
|
|
34
|
+
archive_stash_for(attribute).value = value
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the attachment definitions defined by each call to
|
|
39
|
+
# has_attached_file.
|
|
40
|
+
def archived_attribute_definitions
|
|
41
|
+
read_inheritable_attribute(:archived_attribute_definitions)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def backend
|
|
45
|
+
@backend ||= initialize_cache_klass(HashPipe::GlobalConfiguration.instance[:moneta_klass])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize_cache_klass(cache_klass)
|
|
49
|
+
require_moneta_library_for(cache_klass)
|
|
50
|
+
klass_const = cache_klass.respond_to?(:constantize) ? cache_klass.constantize : cache_klass
|
|
51
|
+
klass_const.new HashPipe::GlobalConfiguration.instance[:moneta_options]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def require_moneta_library_for(cache_klass)
|
|
55
|
+
require cache_klass.to_s.gsub(/::/, '/').downcase
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
module InstanceMethods
|
|
60
|
+
def archive_stash_for(attribute)
|
|
61
|
+
@_archived_attribute_stashes ||= {}
|
|
62
|
+
@_archived_attribute_stashes[attribute] ||= ArchivedAttribute.new(
|
|
63
|
+
attribute,
|
|
64
|
+
archived_attribute_scope,
|
|
65
|
+
self.class.backend,
|
|
66
|
+
self.class.archived_attribute_definitions[attribute]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def archived_attribute_scope
|
|
71
|
+
"#{self.class.table_name}_#{id}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def each_archived_stash
|
|
75
|
+
self.class.archived_attribute_definitions.each do |name, definition|
|
|
76
|
+
yield(name, archive_stash_for(name))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def save_archived_attributes
|
|
81
|
+
each_archived_stash do |name, stash|
|
|
82
|
+
stash.scope = archived_attribute_scope
|
|
83
|
+
stash.save
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def destroy_archived_attributes
|
|
88
|
+
each_archived_stash do |name, stash|
|
|
89
|
+
stash.destroy
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
ActiveRecord::Base.send(:include, HashPipe)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[ .. spec_helper ])
|
|
2
|
+
|
|
3
|
+
module ArchivedAttributeHelpers
|
|
4
|
+
def build_attribute(opts = {})
|
|
5
|
+
opts[:name] ||= :glorp
|
|
6
|
+
opts[:backend] ||= Moneta::Memory.new
|
|
7
|
+
opts[:scope] ||= 'unique-id'
|
|
8
|
+
HashPipe::ArchivedAttribute.new(opts.delete(:name),
|
|
9
|
+
opts.delete(:scope),
|
|
10
|
+
opts.delete(:backend),
|
|
11
|
+
opts)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def clone_attribute(attribute)
|
|
15
|
+
build_attribute(:marshal => attribute.marshal?,
|
|
16
|
+
:compress => attribute.compress?,
|
|
17
|
+
:backend => attribute.backend,
|
|
18
|
+
:scope => attribute.scope,
|
|
19
|
+
:name => attribute.name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def backend
|
|
23
|
+
subject.backend
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe HashPipe::ArchivedAttribute do
|
|
28
|
+
|
|
29
|
+
include ArchivedAttributeHelpers
|
|
30
|
+
|
|
31
|
+
before { @attribute = build_attribute }
|
|
32
|
+
|
|
33
|
+
subject { @attribute }
|
|
34
|
+
|
|
35
|
+
it "should not be dirty when a value has not been set" do
|
|
36
|
+
should_not be_dirty
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should join the scope and the attribute name as a hash key" do
|
|
40
|
+
subject.key.should == "#{subject.scope}_#{subject.name}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "should allow the scope to be set" do
|
|
44
|
+
new_scope = 'another scope'
|
|
45
|
+
subject.scope = new_scope
|
|
46
|
+
subject.scope.should == new_scope
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "should use the provided backend" do
|
|
50
|
+
backend = Moneta::Memory.new
|
|
51
|
+
build_attribute(:backend => backend).backend.should == backend
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "should return when the value has been set" do
|
|
55
|
+
subject.value = 'stuff'
|
|
56
|
+
should be_dirty
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "should not be dirty after save is called" do
|
|
60
|
+
subject.value = 'stuff'
|
|
61
|
+
subject.save
|
|
62
|
+
should_not be_dirty
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "should add itself to the backend when saved" do
|
|
66
|
+
value = 'a value'
|
|
67
|
+
|
|
68
|
+
backend.should_not have_key(subject.key)
|
|
69
|
+
|
|
70
|
+
subject.value = value
|
|
71
|
+
subject.save
|
|
72
|
+
|
|
73
|
+
backend[subject.key].should == value
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "should retrieve itself from the backend" do
|
|
77
|
+
value = 'a value'
|
|
78
|
+
backend[subject.key] = value
|
|
79
|
+
|
|
80
|
+
subject.value.should == value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "should remove itself from the backend when destroyed" do
|
|
84
|
+
backend[subject.key] = 'value'
|
|
85
|
+
subject.destroy
|
|
86
|
+
backend.should_not have_key(subject.key)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "should correctly store a marshalled, compressed value" do
|
|
90
|
+
value = 'the value'
|
|
91
|
+
attribute = build_attribute(:marshal => true, :compress => true)
|
|
92
|
+
attribute.value = value
|
|
93
|
+
attribute.save
|
|
94
|
+
|
|
95
|
+
clone_attribute(attribute).value.should == value
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
describe HashPipe::ArchivedAttribute, "when marshal is on" do
|
|
101
|
+
|
|
102
|
+
include ArchivedAttributeHelpers
|
|
103
|
+
|
|
104
|
+
before { @attribute = build_attribute(:marshal => true) }
|
|
105
|
+
subject { @attribute }
|
|
106
|
+
|
|
107
|
+
it "should be marshalled" do
|
|
108
|
+
subject.marshal?.should be
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "should store a marshalled value" do
|
|
112
|
+
stored = 'stored value'
|
|
113
|
+
actual = 'a value'
|
|
114
|
+
|
|
115
|
+
Marshal.expects(:dump).with(actual).returns(stored)
|
|
116
|
+
|
|
117
|
+
subject.value = actual
|
|
118
|
+
subject.save
|
|
119
|
+
|
|
120
|
+
backend[subject.key].should == stored
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "should retrieve a marshalled value" do
|
|
124
|
+
stored = 'stored value'
|
|
125
|
+
actual = 'a value'
|
|
126
|
+
backend[subject.key] = stored
|
|
127
|
+
|
|
128
|
+
Marshal.expects(:load).with(stored).returns(actual)
|
|
129
|
+
|
|
130
|
+
subject.value.should == actual
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "should correctly store a nil value" do
|
|
134
|
+
subject.value = nil
|
|
135
|
+
subject.save
|
|
136
|
+
clone_attribute(subject).value.should be_nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe HashPipe::ArchivedAttribute, "when compression is on" do
|
|
141
|
+
|
|
142
|
+
include ArchivedAttributeHelpers
|
|
143
|
+
|
|
144
|
+
before { @attribute = build_attribute(:compress => true) }
|
|
145
|
+
subject { @attribute }
|
|
146
|
+
|
|
147
|
+
it "should be compressed" do
|
|
148
|
+
subject.compress?.should be
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "should store a compressed value" do
|
|
152
|
+
value = 'a value'
|
|
153
|
+
subject.value = value
|
|
154
|
+
subject.save
|
|
155
|
+
backend[subject.key].should == Zlib::Deflate.deflate(value)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "should retrieve a compressed value" do
|
|
159
|
+
value = 'a value'
|
|
160
|
+
backend[subject.key] = Zlib::Deflate.deflate(value)
|
|
161
|
+
subject.value.should == value
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "should correctly store a nil value" do
|
|
165
|
+
subject.value = nil
|
|
166
|
+
subject.save
|
|
167
|
+
clone_attribute(subject).value.should be_nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), %w[ .. spec_helper ])
|
|
2
|
+
|
|
3
|
+
describe HashPipe::GlobalConfiguration do
|
|
4
|
+
before do
|
|
5
|
+
@conf = HashPipe::GlobalConfiguration.instance
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe "defaults" do
|
|
9
|
+
it "should set default marshal value to false" do
|
|
10
|
+
HashPipe::GlobalConfiguration::DEFAULTS[:marshal].should == false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "should set gzip value to false" do
|
|
14
|
+
HashPipe::GlobalConfiguration::DEFAULTS[:marshal].should == false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "should read default access key from the configuration file" do
|
|
19
|
+
@conf[:s3]['access_key'].should == 'your access key'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "should the default secret key from configuration" do
|
|
23
|
+
@conf[:s3]['secret_key'].should == 'your secret key'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "should read the default bucket from the configuration file" do
|
|
27
|
+
@conf[:s3]['bucket'].should == 'test_archived_attributes'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "#to_s" do
|
|
31
|
+
it "should return a string" do
|
|
32
|
+
@conf.to_s.should be_an_instance_of(String)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
describe "hash cloning" do
|
|
37
|
+
it "should be able to alter a Hash without affecting the original object" do
|
|
38
|
+
conf = @conf.to_hash
|
|
39
|
+
previous = @conf[:storage]
|
|
40
|
+
conf[:storage] = 'foo'
|
|
41
|
+
@conf[:storage].should == previous
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "should not affect deeply nested attributes when values are changed" do
|
|
45
|
+
conf = @conf.to_hash
|
|
46
|
+
previous = @conf[:s3][:protocol]
|
|
47
|
+
conf[:s3][:protocol] = 'puddle'
|
|
48
|
+
@conf[:s3][:protocol].should == previous
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "#to_hash" do
|
|
53
|
+
it "should return an instance of Hash" do
|
|
54
|
+
@conf.to_hash.should be_an_instance_of(HashWithIndifferentAccess)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "when an option is not specified in the yaml config file" do
|
|
59
|
+
it "should have a section for s3 options in hash" do
|
|
60
|
+
@conf[:s3].should_not be_nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should default to https for protocol" do
|
|
64
|
+
@conf[:s3][:protocol].should == 'https'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "should set default storage attribute to :filesystem" do
|
|
68
|
+
@conf[:storage].should == 'filesystem'
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
|
2
|
+
|
|
3
|
+
ActiveRecord::Base.establish_connection(
|
|
4
|
+
:adapter => 'sqlite3',
|
|
5
|
+
:dbfile => ":memory:"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
ActiveRecord::Migration.verbose = false
|
|
9
|
+
|
|
10
|
+
ActiveRecord::Base.silence do
|
|
11
|
+
ActiveRecord::Schema.define do
|
|
12
|
+
create_table :stories do |table|
|
|
13
|
+
table.string :title
|
|
14
|
+
table.string :content_key
|
|
15
|
+
table.string :description_key
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Story < ActiveRecord::Base
|
|
21
|
+
hattr :content
|
|
22
|
+
hattr :description, { :marshalled => true }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe HashPipe do
|
|
26
|
+
before do
|
|
27
|
+
@lamb_text = 'Baaah!'
|
|
28
|
+
@bear_struct = OpenStruct.new(:blah => 'arg')
|
|
29
|
+
@lamb_story = Story.create!(:title => 'lamb story', :content => @lamb_text)
|
|
30
|
+
@bear_story = Story.create!(:title => 'bear story', :content => 'Raaaar!', :description => @bear_struct)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def refind(instance)
|
|
34
|
+
instance.class.find(instance.id)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "should retrieve textual content" do
|
|
38
|
+
refind(@lamb_story).content.should == @lamb_text
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "should load marshalled objects" do
|
|
42
|
+
refind(@bear_story).description.should == @bear_struct
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
[:save_archived_attributes, :destroy_archived_attributes].each do |sym|
|
|
46
|
+
it "should respond to attached storage method #{sym}" do
|
|
47
|
+
Story.new.should respond_to(sym)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "#destroy_archived_attributes" do
|
|
52
|
+
it "should be called on destruction of object" do
|
|
53
|
+
@bear_story.expects(:destroy_archived_attributes).at_least_once
|
|
54
|
+
@bear_story.destroy
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'moneta'
|
|
3
|
+
require 'moneta/memory'
|
|
4
|
+
require 'ostruct'
|
|
5
|
+
require 'mocha'
|
|
6
|
+
require 'spec'
|
|
7
|
+
require 'activerecord'
|
|
8
|
+
require 'zlib'
|
|
9
|
+
|
|
10
|
+
Spec::Runner.configure do |config|
|
|
11
|
+
config.mock_with(:mocha)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
|
15
|
+
require File.join(File.dirname(__FILE__), %w[.. init])
|
|
16
|
+
|
|
17
|
+
RAILS_DEFAULT_LOGGER = Logger.new(STDOUT) unless defined?(RAILS_DEFAULT_LOGGER)
|
|
18
|
+
RAILS_ROOT = File.join(File.dirname(__FILE__), '..') unless defined?(RAILS_ROOT)
|
|
19
|
+
RAILS_ENV = 'test' unless defined?(RAILS_ENV)
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: jferris-hashpipe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.5.0.1247583913
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Justin Leitgeb
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-07-13 21:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: wycats-moneta
|
|
17
|
+
type: :runtime
|
|
18
|
+
version_requirement:
|
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">"
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 0.0.0
|
|
24
|
+
version:
|
|
25
|
+
- !ruby/object:Gem::Dependency
|
|
26
|
+
name: activesupport
|
|
27
|
+
type: :runtime
|
|
28
|
+
version_requirement:
|
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.0.0
|
|
34
|
+
version:
|
|
35
|
+
description: HashPipe connects an AR-backed model to a key-value store
|
|
36
|
+
email: justin@phq.org
|
|
37
|
+
executables: []
|
|
38
|
+
|
|
39
|
+
extensions: []
|
|
40
|
+
|
|
41
|
+
extra_rdoc_files: []
|
|
42
|
+
|
|
43
|
+
files:
|
|
44
|
+
- LICENSE
|
|
45
|
+
- Rakefile
|
|
46
|
+
- README.rdoc
|
|
47
|
+
- lib/hashpipe/archived_attribute.rb
|
|
48
|
+
- lib/hashpipe/global_configuration.rb
|
|
49
|
+
- lib/hashpipe.rb
|
|
50
|
+
- spec/hashpipe/archived_attribute_spec.rb
|
|
51
|
+
- spec/hashpipe/global_configuration_spec.rb
|
|
52
|
+
- spec/hashpipe_spec.rb
|
|
53
|
+
- spec/spec_helper.rb
|
|
54
|
+
- init.rb
|
|
55
|
+
- config/hashpipe.yml
|
|
56
|
+
has_rdoc: true
|
|
57
|
+
homepage: http://github.com/jsl/hashpipe
|
|
58
|
+
post_install_message:
|
|
59
|
+
rdoc_options: []
|
|
60
|
+
|
|
61
|
+
require_paths:
|
|
62
|
+
- lib
|
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: "0"
|
|
68
|
+
version:
|
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: "0"
|
|
74
|
+
version:
|
|
75
|
+
requirements: []
|
|
76
|
+
|
|
77
|
+
rubyforge_project:
|
|
78
|
+
rubygems_version: 1.2.0
|
|
79
|
+
signing_key:
|
|
80
|
+
specification_version: 3
|
|
81
|
+
summary: ActiveRecord plugin to save content to a pluggable, hash-style backend
|
|
82
|
+
test_files:
|
|
83
|
+
- spec/hashpipe/archived_attribute_spec.rb
|
|
84
|
+
- spec/hashpipe/global_configuration_spec.rb
|
|
85
|
+
- spec/hashpipe_spec.rb
|
|
86
|
+
- spec/spec_helper.rb
|