vault 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,4 @@
1
+ module Vault
2
+ module Storage
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ module Vault
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ include ActiveModel::Validations
7
+ end
8
+
9
+ def errors
10
+ @_errors ||= ActiveModel::Errors.new
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ module Vault
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+ STRING = [MAJOR, MINOR, TINY].join(".").freeze
7
+ end
8
+ 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
@@ -0,0 +1,8 @@
1
+ require "spec"
2
+ require "vault"
3
+
4
+ Dir["spec/support/**/*.rb"].each {|f| require f }
5
+
6
+ Spec::Runner.configure do |config|
7
+ config.include SpecHelpers
8
+ end
@@ -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,5 @@
1
+ require "spec_helper"
2
+
3
+ describe Vault::Storage::InMemoryStore do
4
+ it_should_behave_like "A storage adapter"
5
+ 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
@@ -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
+