vault 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +73 -0
- data/Rakefile +42 -0
- data/lib/vault/associations.rb +39 -0
- data/lib/vault/attribute_accessors.rb +29 -0
- data/lib/vault/bulk_attributes.rb +17 -0
- data/lib/vault/dirty.rb +37 -0
- data/lib/vault/finders.rb +24 -0
- data/lib/vault/persistance.rb +47 -0
- data/lib/vault/properties.rb +68 -0
- data/lib/vault/scoping.rb +64 -0
- data/lib/vault/storage/in_memory_store.rb +14 -0
- data/lib/vault/storage/yaml_store.rb +52 -0
- data/lib/vault/storage.rb +4 -0
- data/lib/vault/validations.rb +13 -0
- data/lib/vault/version.rb +8 -0
- data/lib/vault.rb +53 -0
- data/spec/active_model_compliance_spec.rb +33 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/helpers.rb +16 -0
- data/spec/support/storage_api.rb +14 -0
- data/spec/vault/associations_spec.rb +73 -0
- data/spec/vault/finders_spec.rb +69 -0
- data/spec/vault/persistance_spec.rb +126 -0
- data/spec/vault/properties_spec.rb +59 -0
- data/spec/vault/scoping_spec.rb +53 -0
- data/spec/vault/storage/in_memory_store_spec.rb +5 -0
- data/spec/vault/storage/yaml_store_spec.rb +29 -0
- data/spec/vault_spec.rb +33 -0
- metadata +119 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010-* Nicolás Sanguinetti
|
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.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
Vault, a lightweight Object Document Mapper
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
class User
|
5
|
+
include Vault
|
6
|
+
|
7
|
+
key :id
|
8
|
+
property :name
|
9
|
+
property :email
|
10
|
+
end
|
11
|
+
|
12
|
+
user = User.new(:id => 1, :name => "John", :email => "john@example.org")
|
13
|
+
user.save
|
14
|
+
|
15
|
+
User.find(1) #=> user
|
16
|
+
User.all #=> [user]
|
17
|
+
User.size #=> 1
|
18
|
+
|
19
|
+
Storage
|
20
|
+
-------
|
21
|
+
|
22
|
+
By default Vault stores everything in an in-memory hash. Each model (class in
|
23
|
+
which you included the `Vault` module) has its own, independent storage.
|
24
|
+
|
25
|
+
Subclasses of a model share their storage with their parent.
|
26
|
+
|
27
|
+
To change the storage you can call `store_objects_in(store)` in the class
|
28
|
+
definition. A store is any object that implements this API:
|
29
|
+
|
30
|
+
* `Store#size` returns an integer with the total amount of elements in the store
|
31
|
+
* `Store#each(&block)` a store must implement `each` and include Enumerable
|
32
|
+
* `Store#[](key)` receiving the key it should return a hash of all attributes
|
33
|
+
**except for the key**.
|
34
|
+
* `Store#[]=(key, attributes)` attributes will be a hash with the attributes
|
35
|
+
**except** for the key.
|
36
|
+
* `Store#delete(key)` shall delete the item without the given key.
|
37
|
+
* `Store#filter(hash)` should return a new object of the same class as the
|
38
|
+
original store, but only with objects whose properties match those of the
|
39
|
+
argument.
|
40
|
+
|
41
|
+
This library provides two storage mechanisms out of the box:
|
42
|
+
|
43
|
+
* `Vault::Storage::InMemoryStore` is a simple hash storage (and the default)
|
44
|
+
* `Vault::Storage::YamlStore` serializes the contents to disk as a YAML file
|
45
|
+
|
46
|
+
TODO
|
47
|
+
----
|
48
|
+
|
49
|
+
* Relationships/Associations
|
50
|
+
|
51
|
+
More docs?
|
52
|
+
----------
|
53
|
+
|
54
|
+
I will get to it eventually. For now, read the specs and the source—it is a
|
55
|
+
small library. Or help and write some docs :)
|
56
|
+
|
57
|
+
Note on Patches/Pull Requests
|
58
|
+
-----------------------------
|
59
|
+
|
60
|
+
* Fork the project.
|
61
|
+
* Make your feature addition or bug fix.
|
62
|
+
* Add tests for it. This is important so I don’t break it in a future version
|
63
|
+
unintentionally.
|
64
|
+
* Commit, do not mess with Rakefile, version, or history. (if you want to have
|
65
|
+
your own version, that is fine but bump version in a commit by itself I can
|
66
|
+
ignore when I pull.)
|
67
|
+
* Send me a pull request. Bonus points for topic branches.
|
68
|
+
|
69
|
+
Copyright
|
70
|
+
---------
|
71
|
+
|
72
|
+
Copyright © 2010 [Nicolás Sanguinetti](http://github.com/foca), licensed under
|
73
|
+
an MIT license. See MIT-LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/gempackagetask"
|
5
|
+
require "rake/rdoctask"
|
6
|
+
|
7
|
+
require "vault/version"
|
8
|
+
|
9
|
+
begin
|
10
|
+
require "spec/rake/spectask"
|
11
|
+
|
12
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
13
|
+
spec.pattern = "spec/**/*_spec.rb"
|
14
|
+
spec.spec_opts << "--color" << "--format specdoc"
|
15
|
+
end
|
16
|
+
|
17
|
+
task :default => ["spec"]
|
18
|
+
rescue LoadError
|
19
|
+
end
|
20
|
+
|
21
|
+
spec = Gem::Specification.new do |s|
|
22
|
+
s.name = "vault"
|
23
|
+
s.version = Vault::Version::STRING
|
24
|
+
s.summary = "Provides a very lightweight ODM"
|
25
|
+
s.author = "Nicolás Sanguinetti"
|
26
|
+
s.email = "hi@nicolassanguinetti.info"
|
27
|
+
s.homepage = "http://github.com/foca/vault"
|
28
|
+
|
29
|
+
s.has_rdoc = true
|
30
|
+
s.extra_rdoc_files = %w(README.md MIT-LICENSE)
|
31
|
+
|
32
|
+
s.files = Dir['Rakefile', '{bin,lib,man,test,spec}/**/*', 'README*', '*LICENSE*'] & `git ls-files -z`.split("\0")
|
33
|
+
|
34
|
+
s.require_paths = ["lib"]
|
35
|
+
|
36
|
+
s.add_dependency("activemodel", "3.0.0.beta.3")
|
37
|
+
s.add_development_dependency("rspec", "~> 1.3")
|
38
|
+
end
|
39
|
+
|
40
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
41
|
+
pkg.gem_spec = spec
|
42
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Vault
|
2
|
+
module Associations
|
3
|
+
def has_many(name, klass_name=name.to_s.classify, foreign_key="#{self.to_s.underscore.singularize}_key")
|
4
|
+
define_method name do
|
5
|
+
model = klass_name.is_a?(Class) ? klass_name : self.class.const_get(klass_name)
|
6
|
+
HasManyProxy.new(self, model, foreign_key, foreign_key => key)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def belongs_to(name, klass_name=name.to_s.classify)
|
11
|
+
foreign_key = "#{name}_key"
|
12
|
+
|
13
|
+
property(foreign_key)
|
14
|
+
|
15
|
+
define_method name do
|
16
|
+
model = klass_name.is_a?(Class) ? klass_name : self.class.const_get(klass_name)
|
17
|
+
model[send(foreign_key)]
|
18
|
+
end
|
19
|
+
|
20
|
+
define_method "#{name}=" do |object|
|
21
|
+
send("#{foreign_key}=", object.key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class HasManyProxy < Scoping::Scope
|
26
|
+
def initialize(owner, model, foreign_key, conditions={})
|
27
|
+
super(model, conditions)
|
28
|
+
@owner = owner
|
29
|
+
@foreign_key = foreign_key
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<(object)
|
33
|
+
object.send("#{@foreign_key}=", @owner.key)
|
34
|
+
object.save
|
35
|
+
self
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Vault
|
2
|
+
module AttributeAccessors
|
3
|
+
def initialize(*)
|
4
|
+
@_attributes = attributes_from_model_properties
|
5
|
+
super
|
6
|
+
end
|
7
|
+
|
8
|
+
def write_attribute(name, value)
|
9
|
+
@_attributes[name] = value
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_attribute(name)
|
13
|
+
@_attributes[name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def attributes
|
17
|
+
@_attributes
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def attributes_from_model_properties
|
23
|
+
self.class.properties.inject(HashWithIndifferentAccess.new) do |props, prop|
|
24
|
+
props[prop.name] = prop.default
|
25
|
+
props
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Vault
|
2
|
+
module BulkAttributes
|
3
|
+
def initialize(attrs={})
|
4
|
+
update(attrs)
|
5
|
+
changed_attributes.clear
|
6
|
+
end
|
7
|
+
|
8
|
+
def update(attrs={})
|
9
|
+
attrs.each do |key, value|
|
10
|
+
method = "#{key}="
|
11
|
+
__send__(method, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
self
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/vault/dirty.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module Vault
|
2
|
+
module Dirty
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
include ActiveModel::Dirty
|
7
|
+
end
|
8
|
+
|
9
|
+
def save(*)
|
10
|
+
super.tap do |saved|
|
11
|
+
if saved
|
12
|
+
if attribute_changed?(self.class.key)
|
13
|
+
self.class.store.delete(attribute_was(self.class.key))
|
14
|
+
end
|
15
|
+
|
16
|
+
@previously_changed = changes
|
17
|
+
changed_attributes.clear
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_attribute(name, value)
|
23
|
+
name = name.to_s
|
24
|
+
|
25
|
+
if attribute_changed?(name)
|
26
|
+
old = changed_attributes[name]
|
27
|
+
changed_attributes.delete(name) if old == value
|
28
|
+
else
|
29
|
+
old = read_attribute(name)
|
30
|
+
old = old.dup if old.duplicable?
|
31
|
+
changed_attributes[name] = old if old != value
|
32
|
+
end
|
33
|
+
|
34
|
+
super(name, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Vault
|
2
|
+
module Finders
|
3
|
+
delegate :size, :to => :store
|
4
|
+
alias_method :count, :size
|
5
|
+
|
6
|
+
def all(query={})
|
7
|
+
query.delete(key)
|
8
|
+
store.filter(query).map do |key_value, properties|
|
9
|
+
build(key_value, properties)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key_value)
|
14
|
+
properties = store[key_value]
|
15
|
+
build(key_value, properties) if properties
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build(key_value, properties)
|
21
|
+
new(properties.update(key => key_value))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Vault
|
2
|
+
module Persistance
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
cattr_accessor :store
|
7
|
+
extend Storage
|
8
|
+
|
9
|
+
store_objects_in Vault::Storage::InMemoryStore.new
|
10
|
+
end
|
11
|
+
|
12
|
+
module Storage
|
13
|
+
def store_objects_in(store)
|
14
|
+
self.store = store
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(*)
|
19
|
+
@_new = true
|
20
|
+
@_destroyed = false
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def persisted?
|
25
|
+
!@_new && !@_destroyed
|
26
|
+
end
|
27
|
+
|
28
|
+
def save(run_validations=true)
|
29
|
+
return false if run_validations && !valid?
|
30
|
+
self.class.store[key] = attributes_except_key
|
31
|
+
@_new = false
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def destroy
|
36
|
+
self.class.store.delete(key)
|
37
|
+
@_destroyed = true
|
38
|
+
freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def attributes_except_key
|
44
|
+
attributes.except(self.class.key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module Vault
|
2
|
+
module Properties
|
3
|
+
def key(key=nil)
|
4
|
+
return @_key if key.nil?
|
5
|
+
@_key = property(key, true).name
|
6
|
+
end
|
7
|
+
|
8
|
+
def property(name, primary=false, &block)
|
9
|
+
Property.new(name, primary, &block).tap do |prop|
|
10
|
+
properties << prop
|
11
|
+
define_property_methods(prop.name)
|
12
|
+
|
13
|
+
# FIXME: Ugh, ActiveModel fails with this, I can't do incremental method
|
14
|
+
# definition, you have to define them all at once (so undefine/define
|
15
|
+
# each time if you don't know all the properties upfront, like here.)
|
16
|
+
undefine_attribute_methods
|
17
|
+
define_attribute_methods(property_names)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def properties
|
22
|
+
@_properties ||= Set.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def property_names
|
26
|
+
properties.map(&:name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def define_property_methods(name)
|
30
|
+
class_eval <<-ruby, __FILE__, __LINE__
|
31
|
+
def #{name}
|
32
|
+
read_attribute(:#{name})
|
33
|
+
end
|
34
|
+
|
35
|
+
def #{name}=(value)
|
36
|
+
write_attribute(:#{name}, value)
|
37
|
+
end
|
38
|
+
ruby
|
39
|
+
end
|
40
|
+
|
41
|
+
class Property
|
42
|
+
attr_reader :name
|
43
|
+
|
44
|
+
def initialize(name, primary=false, &default)
|
45
|
+
@name = name.to_s
|
46
|
+
@default = default || lambda {}
|
47
|
+
@primary = primary
|
48
|
+
end
|
49
|
+
|
50
|
+
def primary?
|
51
|
+
@primary
|
52
|
+
end
|
53
|
+
|
54
|
+
def default
|
55
|
+
@default.call
|
56
|
+
end
|
57
|
+
|
58
|
+
# Set requires both #hash and #eql? to check for inclusion
|
59
|
+
def hash # :nodoc:
|
60
|
+
name.hash
|
61
|
+
end
|
62
|
+
|
63
|
+
def eql?(other) # :nodoc:
|
64
|
+
name == other.name
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Vault
|
2
|
+
module Scoping
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def scopes
|
6
|
+
@_scopes ||= Hash.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def scope(name, &conditions)
|
10
|
+
name = name.to_s
|
11
|
+
scopes[name] = Scope.new(self, conditions.call)
|
12
|
+
define_scope_method(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def define_scope_method(name)
|
18
|
+
singleton_class.instance_eval do
|
19
|
+
define_method name do
|
20
|
+
scopes[name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Scope
|
26
|
+
include Finders
|
27
|
+
|
28
|
+
delegate :new, :key, :model_name, :to => :model
|
29
|
+
|
30
|
+
attr_reader :conditions, :model
|
31
|
+
|
32
|
+
def initialize(model, conditions)
|
33
|
+
@model = model
|
34
|
+
@conditions = conditions.stringify_keys
|
35
|
+
end
|
36
|
+
|
37
|
+
def store
|
38
|
+
@store ||= @model.store.filter(conditions)
|
39
|
+
end
|
40
|
+
|
41
|
+
def method_missing(method, *)
|
42
|
+
if model_has_scope?(method)
|
43
|
+
merge(model.scopes[method.to_s])
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_to?(method, include_private=false)
|
50
|
+
model_has_scope?(method) || super
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def model_has_scope?(name)
|
56
|
+
model.scopes.include?(name.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge(scope)
|
60
|
+
Scope.new(model, conditions.merge(scope.conditions))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Vault
|
2
|
+
module Storage
|
3
|
+
class InMemoryStore < Hash
|
4
|
+
def filter(query)
|
5
|
+
return self if query.blank?
|
6
|
+
|
7
|
+
inject(InMemoryStore.new) do |result, (key, properties)|
|
8
|
+
result[key] = properties if properties.merge(query) == properties
|
9
|
+
result
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require "active_support/core_ext/module/delegation"
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
module Storage
|
6
|
+
class YamlStore
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
delegate :[], :[]=, :size, :each, :delete, :to => :doc
|
10
|
+
|
11
|
+
def initialize(file=nil)
|
12
|
+
@file = file
|
13
|
+
at_exit { flush if @file.present? }
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize_copy(*)
|
17
|
+
@file = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter(query)
|
21
|
+
return filtered_copy(@doc) if query.blank?
|
22
|
+
|
23
|
+
results = doc.inject(ActiveSupport::OrderedHash.new) do |result, (key, properties)|
|
24
|
+
result[key] = properties if properties.merge(query) == properties
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
filtered_copy(results)
|
29
|
+
end
|
30
|
+
|
31
|
+
def flush
|
32
|
+
File.open(@file, "w+") {|f| YAML.dump(@doc, f) }
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def doc=(doc)
|
38
|
+
@doc = doc
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def filtered_copy(doc)
|
44
|
+
dup.tap {|store| store.doc = doc }
|
45
|
+
end
|
46
|
+
|
47
|
+
def doc
|
48
|
+
@doc ||= ActiveSupport::OrderedHash.new(YAML.load_file(@file))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/vault.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require "set"
|
2
|
+
require "active_model"
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
require "active_support/core_ext/module/delegation"
|
5
|
+
require "active_support/core_ext/class/attribute_accessors"
|
6
|
+
|
7
|
+
module Vault
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
extend ActiveSupport::Autoload
|
10
|
+
|
11
|
+
autoload :Associations
|
12
|
+
autoload :AttributeAccessors
|
13
|
+
autoload :BulkAttributes
|
14
|
+
autoload :Dirty
|
15
|
+
autoload :Finders
|
16
|
+
autoload :Persistance
|
17
|
+
autoload :Properties
|
18
|
+
autoload :Scoping
|
19
|
+
autoload :Storage
|
20
|
+
autoload :Validations
|
21
|
+
|
22
|
+
module Storage
|
23
|
+
extend ActiveSupport::Autoload
|
24
|
+
autoload :InMemoryStore
|
25
|
+
autoload :YamlStore
|
26
|
+
end
|
27
|
+
|
28
|
+
included do
|
29
|
+
extend ActiveModel::Naming
|
30
|
+
extend Properties
|
31
|
+
extend Finders
|
32
|
+
extend Scoping
|
33
|
+
extend Associations
|
34
|
+
|
35
|
+
include BulkAttributes
|
36
|
+
include AttributeAccessors
|
37
|
+
include Persistance
|
38
|
+
include Dirty
|
39
|
+
include Validations
|
40
|
+
|
41
|
+
# Convenience methods to provide ActiveModel's API
|
42
|
+
include ActiveModel::Conversion
|
43
|
+
end
|
44
|
+
|
45
|
+
def key
|
46
|
+
send(self.class.key)
|
47
|
+
end
|
48
|
+
alias_method :id, :key
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
key == other.key
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "ActiveModel compliance" do
|
4
|
+
class Person
|
5
|
+
include Vault
|
6
|
+
|
7
|
+
key :id
|
8
|
+
property :first_name
|
9
|
+
property :last_name
|
10
|
+
end
|
11
|
+
|
12
|
+
include ActiveModel::Lint::Tests
|
13
|
+
|
14
|
+
instance_methods.grep(/^test_/).each do |method|
|
15
|
+
it method.gsub("_", " ").gsub(/to (\w+)/, 'to_\1') do
|
16
|
+
send method
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def model
|
23
|
+
Person.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def assert(cond, msg=nil)
|
27
|
+
flunk msg unless cond
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_kind_of(kind, object, msg=nil)
|
31
|
+
flunk msg unless object.kind_of?(kind)
|
32
|
+
end
|
33
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module SpecHelpers
|
2
|
+
def model(&block)
|
3
|
+
Class.new do
|
4
|
+
include Vault
|
5
|
+
class_eval(&block) if block
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def named_model(name, &block)
|
10
|
+
model = model(&block)
|
11
|
+
self.class.class_eval do
|
12
|
+
remove_const(name) if const_defined?(name)
|
13
|
+
const_set(name, model)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
shared_examples_for "A storage adapter" do
|
2
|
+
it { should respond_to(:size) }
|
3
|
+
it { should respond_to(:delete) }
|
4
|
+
it { should respond_to(:[]) }
|
5
|
+
it { should respond_to(:[]=) }
|
6
|
+
|
7
|
+
it { should respond_to(:each) }
|
8
|
+
it { should be_an(Enumerable) }
|
9
|
+
|
10
|
+
it { should respond_to(:filter) }
|
11
|
+
it "is closed under #filter" do
|
12
|
+
subject.filter({}).should be_a(described_class)
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault, "model" do
|
4
|
+
class Book
|
5
|
+
include Vault
|
6
|
+
key :title
|
7
|
+
has_many :authors
|
8
|
+
end
|
9
|
+
|
10
|
+
class Author
|
11
|
+
include Vault
|
12
|
+
key :name
|
13
|
+
belongs_to :book
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:pickaxe_book) { Book.new(:title => "Programming Ruby") }
|
17
|
+
|
18
|
+
let(:dave_thomas) { Author.new(:name => "Dave Thomas") }
|
19
|
+
let(:chad_fowler) { Author.new(:name => "Chad Fowler") }
|
20
|
+
let(:andy_hunt) { Author.new(:name => "Andy Hunt") }
|
21
|
+
let(:sam_ruby) { Author.new(:name => "Sam Ruby") }
|
22
|
+
|
23
|
+
describe ".belongs_to" do
|
24
|
+
before do
|
25
|
+
dave_thomas.save
|
26
|
+
pickaxe_book.save
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can assign the associated object directly" do
|
30
|
+
dave_thomas.book = pickaxe_book
|
31
|
+
dave_thomas.book.should == pickaxe_book
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can assign the key" do
|
35
|
+
dave_thomas.book_key = pickaxe_book.key
|
36
|
+
dave_thomas.book.should == pickaxe_book
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe ".has_many" do
|
41
|
+
before do
|
42
|
+
dave_thomas.book = pickaxe_book
|
43
|
+
chad_fowler.book = pickaxe_book
|
44
|
+
|
45
|
+
dave_thomas.save
|
46
|
+
chad_fowler.save
|
47
|
+
|
48
|
+
pickaxe_book.save
|
49
|
+
end
|
50
|
+
|
51
|
+
subject { pickaxe_book.authors.all }
|
52
|
+
|
53
|
+
it "can find all associated objects" do
|
54
|
+
should include(dave_thomas, chad_fowler)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "doesn't find objects that haven't been associated to the model" do
|
58
|
+
should_not include(sam_ruby)
|
59
|
+
end
|
60
|
+
|
61
|
+
context "adding objects directly to the association" do
|
62
|
+
it "persists both the container and containee" do
|
63
|
+
pickaxe_book.authors << andy_hunt
|
64
|
+
should include(andy_hunt)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can chain additions" do
|
68
|
+
pickaxe_book.authors << andy_hunt << sam_ruby
|
69
|
+
should include(andy_hunt, sam_ruby)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault do
|
4
|
+
let :person_klass do
|
5
|
+
model do
|
6
|
+
key :name
|
7
|
+
property :email
|
8
|
+
property :age
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let :john do
|
13
|
+
person_klass.new(:name => "John", :email => "john@example.org", :age => 28)
|
14
|
+
end
|
15
|
+
|
16
|
+
let :mary do
|
17
|
+
person_klass.new(:name => "Mary", :email => "mary@example.org", :age => 23)
|
18
|
+
end
|
19
|
+
|
20
|
+
let :bob do
|
21
|
+
person_klass.new(:name => "Bob", :email => "bob@example.org", :age => 29)
|
22
|
+
end
|
23
|
+
|
24
|
+
let :jane do
|
25
|
+
person_klass.new(:name => "Jane", :email => "jane@example.org", :age => 28)
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
john.save
|
30
|
+
mary.save
|
31
|
+
bob.save
|
32
|
+
jane.save
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#all" do
|
36
|
+
subject { person_klass.all }
|
37
|
+
|
38
|
+
it { should have(4).elements }
|
39
|
+
|
40
|
+
it "includes all the persisted objects" do
|
41
|
+
should include(john)
|
42
|
+
should include(mary)
|
43
|
+
should include(bob)
|
44
|
+
should include(jane)
|
45
|
+
end
|
46
|
+
|
47
|
+
it { should be_an(Enumerable) }
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#[]" do
|
51
|
+
it "finds an object by key" do
|
52
|
+
person_klass["Bob"].should == bob
|
53
|
+
end
|
54
|
+
|
55
|
+
it "returns nil if it doesn't find it" do
|
56
|
+
person_klass["Jamie"].should be_nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#size" do
|
61
|
+
it "returns the amount of elements in the store" do
|
62
|
+
person_klass.size.should == 4
|
63
|
+
end
|
64
|
+
|
65
|
+
it "is aliased to #count" do
|
66
|
+
person_klass.count.should == person_klass.size
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault do
|
4
|
+
let :book_klass do
|
5
|
+
model do
|
6
|
+
key :isbn
|
7
|
+
property :title
|
8
|
+
property :author
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let :store do
|
13
|
+
book_klass.store
|
14
|
+
end
|
15
|
+
|
16
|
+
let :alice do
|
17
|
+
book_klass.new("title" => "Alice in Wonderland",
|
18
|
+
"author" => "Lewis Carroll",
|
19
|
+
"isbn" => "978-0862033248")
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#save" do
|
23
|
+
it "returns true" do
|
24
|
+
alice.save.should be(true)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "marks the object as persisted" do
|
28
|
+
alice.save
|
29
|
+
alice.should be_persisted
|
30
|
+
end
|
31
|
+
|
32
|
+
it "stores the object in the model's store" do
|
33
|
+
alice.save
|
34
|
+
store[alice.isbn].should include("title", "author")
|
35
|
+
end
|
36
|
+
|
37
|
+
it "doesn't store the key among the object properties" do
|
38
|
+
alice.save
|
39
|
+
store[alice.isbn].should_not include("isbn")
|
40
|
+
end
|
41
|
+
|
42
|
+
it "updates an already saved object's attributes" do
|
43
|
+
alice.save
|
44
|
+
alice.update(:title => "Alice in Wonderland Illustrated")
|
45
|
+
alice.save
|
46
|
+
|
47
|
+
store[alice.isbn]["title"].should == "Alice in Wonderland Illustrated"
|
48
|
+
end
|
49
|
+
|
50
|
+
it "clears the tracked changed attributes" do
|
51
|
+
alice.save
|
52
|
+
alice.should_not be_changed
|
53
|
+
end
|
54
|
+
|
55
|
+
it "doesn't keep the old key when you change it" do
|
56
|
+
alice.save
|
57
|
+
alice.update("isbn" => "978-0517223628")
|
58
|
+
old_isbn = alice.isbn_was
|
59
|
+
alice.save
|
60
|
+
|
61
|
+
store[old_isbn].should be_blank
|
62
|
+
store[alice.isbn].should_not be_blank
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when the object is invalid" do
|
66
|
+
let :person_klass do
|
67
|
+
named_model :Person do
|
68
|
+
key :email
|
69
|
+
property :name
|
70
|
+
|
71
|
+
validates :email, :presence => true
|
72
|
+
validates :name, :presence => true
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
let :store do
|
77
|
+
person_klass.store
|
78
|
+
end
|
79
|
+
|
80
|
+
let :nameless_person do
|
81
|
+
person_klass.new(:email => "john.doe@example.org")
|
82
|
+
end
|
83
|
+
|
84
|
+
it "returns false" do
|
85
|
+
nameless_person.save.should be(false)
|
86
|
+
end
|
87
|
+
|
88
|
+
it "doesn't persist the object" do
|
89
|
+
nameless_person.save
|
90
|
+
nameless_person.should_not be_persisted
|
91
|
+
end
|
92
|
+
|
93
|
+
context "but you skip validations" do
|
94
|
+
it "returns true" do
|
95
|
+
nameless_person.save(false).should be(true)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "persists the object anyway" do
|
99
|
+
nameless_person.save(false)
|
100
|
+
nameless_person.should be_persisted
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "#destroy" do
|
107
|
+
before do
|
108
|
+
alice.save
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should no longer be persisted after being destroyed" do
|
112
|
+
alice.destroy
|
113
|
+
alice.should_not be_persisted
|
114
|
+
end
|
115
|
+
|
116
|
+
it "removes the object from the model's store" do
|
117
|
+
alice.destroy
|
118
|
+
store[alice.isbn].should be_blank
|
119
|
+
end
|
120
|
+
|
121
|
+
it "freezes the model" do
|
122
|
+
alice.destroy
|
123
|
+
alice.should be_frozen
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault, "defining properties" do
|
4
|
+
it "defines accessors for each property" do
|
5
|
+
person_klass = model do
|
6
|
+
property :name
|
7
|
+
property :age
|
8
|
+
end
|
9
|
+
|
10
|
+
person = person_klass.new
|
11
|
+
person.should respond_to(:name, :name=, :age, :age=)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "doesn't define duplicated properties" do
|
15
|
+
person_klass = model do
|
16
|
+
property :name
|
17
|
+
property :name
|
18
|
+
end
|
19
|
+
|
20
|
+
person_klass.should have(1).properties
|
21
|
+
end
|
22
|
+
|
23
|
+
it "doesn't support more than one key" do
|
24
|
+
person_klass = model do
|
25
|
+
key :name
|
26
|
+
key :age
|
27
|
+
end
|
28
|
+
|
29
|
+
person = person_klass.new(:name => "John", :age => 28)
|
30
|
+
person.save
|
31
|
+
person.to_key.should == [person.age]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can specify default values" do
|
35
|
+
person_klass = model do
|
36
|
+
property(:name) { "John" }
|
37
|
+
property(:age) { 28 }
|
38
|
+
end
|
39
|
+
|
40
|
+
person = person_klass.new
|
41
|
+
person.name.should == "John"
|
42
|
+
person.age.should == 28
|
43
|
+
end
|
44
|
+
|
45
|
+
it "doesn't flag an attribute as changed if you revert the change" do
|
46
|
+
person_klass = model do
|
47
|
+
property :name
|
48
|
+
end
|
49
|
+
|
50
|
+
person = person_klass.new(:name => "John")
|
51
|
+
person.name = "Joe"
|
52
|
+
person.name = "John"
|
53
|
+
person.should_not be_changed
|
54
|
+
end
|
55
|
+
|
56
|
+
it "provides 'dirty' attribute tracking" do
|
57
|
+
model.included_modules.should include(ActiveModel::Dirty)
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault do
|
4
|
+
let :book_klass do
|
5
|
+
model do
|
6
|
+
scope :by_lewis_carroll do
|
7
|
+
{ "author" => "Lewis Carroll" }
|
8
|
+
end
|
9
|
+
|
10
|
+
scope :titled_alice do
|
11
|
+
{ "title" => "Alice in Wonderland" }
|
12
|
+
end
|
13
|
+
|
14
|
+
key :isbn
|
15
|
+
property :title
|
16
|
+
property :author
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let :alice_in_wonderland do
|
21
|
+
book_klass.new(:isbn => "978-0517223628",
|
22
|
+
:title => "Alice in Wonderland",
|
23
|
+
:author => "Lewis Carroll")
|
24
|
+
end
|
25
|
+
|
26
|
+
let :through_the_looking_glass do
|
27
|
+
book_klass.new(:isbn => "978-0140367096",
|
28
|
+
:title => "Through the Looking Glass",
|
29
|
+
:author => "Lewis Carroll")
|
30
|
+
end
|
31
|
+
|
32
|
+
let :the_wonderful_wizard_of_oz do
|
33
|
+
book_klass.new(:isbn => "978-0451530295",
|
34
|
+
:title => "The Wonderful Wizard of Oz",
|
35
|
+
:author => "Lyman Frank Baum")
|
36
|
+
end
|
37
|
+
|
38
|
+
before do
|
39
|
+
alice_in_wonderland.save
|
40
|
+
through_the_looking_glass.save
|
41
|
+
the_wonderful_wizard_of_oz.save
|
42
|
+
end
|
43
|
+
|
44
|
+
it "filters the collection by the given scope" do
|
45
|
+
scoped = book_klass.by_lewis_carroll
|
46
|
+
scoped.all.should =~ [alice_in_wonderland, through_the_looking_glass]
|
47
|
+
end
|
48
|
+
|
49
|
+
it "lets you chain scopes" do
|
50
|
+
scoped = book_klass.by_lewis_carroll.titled_alice
|
51
|
+
scoped.all.should == [alice_in_wonderland]
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
require "tempfile"
|
3
|
+
|
4
|
+
describe Vault::Storage::YamlStore do
|
5
|
+
let :path do
|
6
|
+
begin
|
7
|
+
file = Tempfile.new("store.yml")
|
8
|
+
file.path
|
9
|
+
ensure
|
10
|
+
file.close
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
subject do
|
15
|
+
described_class.new(path)
|
16
|
+
end
|
17
|
+
|
18
|
+
before do
|
19
|
+
subject["key_a"] = { "attr_1" => "value_1", "attr_2" => "value_2" }
|
20
|
+
subject["key_b"] = { "attr_3" => "value_3" }
|
21
|
+
end
|
22
|
+
|
23
|
+
it_should_behave_like "A storage adapter"
|
24
|
+
|
25
|
+
it "serializes the file to disk on #flush" do
|
26
|
+
subject.flush
|
27
|
+
YAML.load_file(path).should include("key_a", "key_b")
|
28
|
+
end
|
29
|
+
end
|
data/spec/vault_spec.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Vault do
|
4
|
+
let :book_klass do
|
5
|
+
model do
|
6
|
+
key :isbn
|
7
|
+
property :title
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
it "can initialize a new model from a hash of attributes" do
|
12
|
+
book = book_klass.new(:title => "Some Book", :isbn => "1234567890")
|
13
|
+
book.title.should == "Some Book"
|
14
|
+
book.isbn.should == "1234567890"
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#update" do
|
18
|
+
let :book do
|
19
|
+
book_klass.new(:title => "Some Book", :isbn => "1234567890")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "effectively changes the attribute values" do
|
23
|
+
book.update(:title => "Awesome Book", :isbn => "0987654321")
|
24
|
+
book.title.should == "Awesome Book"
|
25
|
+
book.isbn.should == "0987654321"
|
26
|
+
end
|
27
|
+
|
28
|
+
it "flags attributes as changed when bulk-updating" do
|
29
|
+
book.update(:title => "Awesome Book")
|
30
|
+
book.changes.should include(:title)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vault
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- "Nicol\xC3\xA1s Sanguinetti"
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-14 00:00:00 -03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activemodel
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 3
|
29
|
+
- 0
|
30
|
+
- 0
|
31
|
+
- beta
|
32
|
+
- 3
|
33
|
+
version: 3.0.0.beta.3
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rspec
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 3
|
46
|
+
version: "1.3"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id002
|
49
|
+
description:
|
50
|
+
email: hi@nicolassanguinetti.info
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files:
|
56
|
+
- README.md
|
57
|
+
- MIT-LICENSE
|
58
|
+
files:
|
59
|
+
- Rakefile
|
60
|
+
- lib/vault/associations.rb
|
61
|
+
- lib/vault/attribute_accessors.rb
|
62
|
+
- lib/vault/bulk_attributes.rb
|
63
|
+
- lib/vault/dirty.rb
|
64
|
+
- lib/vault/finders.rb
|
65
|
+
- lib/vault/persistance.rb
|
66
|
+
- lib/vault/properties.rb
|
67
|
+
- lib/vault/scoping.rb
|
68
|
+
- lib/vault/storage/in_memory_store.rb
|
69
|
+
- lib/vault/storage/yaml_store.rb
|
70
|
+
- lib/vault/storage.rb
|
71
|
+
- lib/vault/validations.rb
|
72
|
+
- lib/vault/version.rb
|
73
|
+
- lib/vault.rb
|
74
|
+
- spec/active_model_compliance_spec.rb
|
75
|
+
- spec/spec_helper.rb
|
76
|
+
- spec/support/helpers.rb
|
77
|
+
- spec/support/storage_api.rb
|
78
|
+
- spec/vault/associations_spec.rb
|
79
|
+
- spec/vault/finders_spec.rb
|
80
|
+
- spec/vault/persistance_spec.rb
|
81
|
+
- spec/vault/properties_spec.rb
|
82
|
+
- spec/vault/scoping_spec.rb
|
83
|
+
- spec/vault/storage/in_memory_store_spec.rb
|
84
|
+
- spec/vault/storage/yaml_store_spec.rb
|
85
|
+
- spec/vault_spec.rb
|
86
|
+
- README.md
|
87
|
+
- MIT-LICENSE
|
88
|
+
has_rdoc: true
|
89
|
+
homepage: http://github.com/foca/vault
|
90
|
+
licenses: []
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
version: "0"
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
segments:
|
109
|
+
- 0
|
110
|
+
version: "0"
|
111
|
+
requirements: []
|
112
|
+
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.3.6
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: Provides a very lightweight ODM
|
118
|
+
test_files: []
|
119
|
+
|