vault 0.1.0

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/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
+