Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit of goodies pulled in from http://gaug.es.

  • Loading branch information...
commit 049010e2b8326eb159e3a393088368e5329187d8 1 parent 6df6f7e
@jnunemaker authored
View
1  .gitignore
@@ -2,3 +2,4 @@
.bundle
Gemfile.lock
pkg/*
+log
View
13 Gemfile
@@ -0,0 +1,13 @@
+source "http://rubygems.org"
+
+# Specify your gem's dependencies in toystore-mongo.gemspec
+gem 'toystore', :path => '/Users/jnunemaker/projects/toystore'
+gemspec
+
+group(:development) do
+ gem 'bson_ext'
+ gem 'SystemTimer'
+ gem 'rake', '~> 0.8.7'
+ gem 'rspec', '~> 2.3'
+ gem 'log_buddy', '~> 0.5.0'
+end
View
7 Rakefile
@@ -0,0 +1,7 @@
+require 'bundler'
+Bundler::GemHelper.install_tasks
+
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new
+
+task :default => :spec
View
16 lib/toy/mongo.rb
@@ -0,0 +1,16 @@
+require 'plucky'
+require 'toy'
+require 'toy/mongo/querying'
+require 'adapter/mongo'
+
+module Toy
+ module Mongo
+ extend ActiveSupport::Concern
+
+ included do
+ include Querying
+ end
+ end
+end
+
+Toy.plugin(Toy::Mongo)
View
80 lib/toy/mongo/querying.rb
@@ -0,0 +1,80 @@
+module Toy
+ module Mongo
+ module Querying
+ extend ActiveSupport::Concern
+
+ PluckyMethods = [
+ :where, :filter, :limit, :skip, :offset, :sort, :order,
+ :fields, :ignore, :only,
+ :each, :find_each,
+ :count, :size, :distinct,
+ :last, :first, :all, :paginate,
+ :exists?, :exist?, :empty?,
+ :to_a, :remove,
+ ]
+
+ module ClassMethods
+ def transformer
+ @transformer ||= lambda do |doc|
+ load(doc.delete('_id'), doc)
+ end
+ end
+
+ def object_id_attributes
+ attributes.values.select do |attribute|
+ attribute.type == BSON::ObjectId
+ end.map do |attribute|
+ sym = attribute.name.to_sym
+ sym == :id ? :_id : sym
+ end
+ end
+
+ def query
+ # TODO: add object id keys to convert
+ Plucky::Query.new(store.client, :transformer => transformer).object_ids(object_id_attributes)
+ end
+
+ PluckyMethods.each do |name|
+ define_method(name) do |*args|
+ query.send(name, *args)
+ end
+ end
+ end
+
+ # Very basic method for determining what has changed locally
+ # so we can just update changes instead of entire document
+ #
+ # Does not work with complex objects (array, hash, set, etc.)
+ # as it does not attempt to determine what has changed in them,
+ # just whether or not they have changed at all.
+ def persistable_changes
+ attrs = {}
+ pattrs = persisted_attributes
+ changed.each do |key|
+ attribute = self.class.attributes[key.to_s]
+ next if attribute.virtual?
+ attrs[attribute.store_key] = pattrs[attribute.store_key]
+ end
+ attrs
+ end
+
+ def atomic_update_attributes(attrs={})
+ self.attributes = attrs
+ if valid?
+ atomic_update('$set' => persistable_changes)
+ true
+ else
+ false
+ end
+ end
+
+ def atomic_update(update, opts={})
+ options = {}
+ criteria = {'_id' => id}
+ criteria.update(opts[:criteria]) if opts[:criteria]
+ options[:safe] = opts.key?(:safe) ? opts[:safe] : store.options[:safe]
+ store.client.update(criteria, update, options)
+ end
+ end
+ end
+end
View
5 lib/toy/mongo/version.rb
@@ -0,0 +1,5 @@
+module Toy
+ module Mongo
+ VERSION = '0.0.1'
+ end
+end
View
1  lib/toystore-mongo.rb
@@ -0,0 +1 @@
+require 'toy/mongo'
View
32 spec/helper.rb
@@ -0,0 +1,32 @@
+$:.unshift(File.expand_path('../../lib', __FILE__))
+
+require 'pathname'
+require 'logger'
+
+root_path = Pathname(__FILE__).dirname.join('..').expand_path
+lib_path = root_path.join('lib')
+log_path = root_path.join('log')
+log_path.mkpath
+
+require 'rubygems'
+require 'bundler'
+
+Bundler.require(:development)
+
+require 'toy/mongo'
+require 'support/constants'
+
+STORE = Mongo::Connection.new.db('testing')['toystore-mongo']
+
+Logger.new(log_path.join('test.log')).tap do |log|
+ LogBuddy.init(:logger => log)
+ Toy.logger = log
+end
+
+Rspec.configure do |c|
+ c.include(Support::Constants)
+
+ c.before(:each) do
+ STORE.remove
+ end
+end
View
3  spec/spec.opts
@@ -0,0 +1,3 @@
+--colour
+--timeout
+20
View
42 spec/support/constants.rb
@@ -0,0 +1,42 @@
+module Support
+ module Constants
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def uses_constants(*constants)
+ before { create_constants(*constants) }
+ end
+ end
+
+ def create_constants(*constants)
+ constants.each { |constant| create_constant(constant) }
+ end
+
+ def remove_constants(*constants)
+ constants.each { |constant| remove_constant(constant) }
+ end
+
+ def create_constant(constant)
+ remove_constant(constant)
+ Kernel.const_set(constant, Model(constant))
+ end
+
+ def remove_constant(constant)
+ Kernel.send(:remove_const, constant) if Kernel.const_defined?(constant)
+ end
+
+ def Model(name=nil)
+ Class.new.tap do |model|
+ model.class_eval """
+ def self.name; '#{name}' end
+ def self.to_s; '#{name}' end
+ """ if name
+ model.send(:include, Toy::Store)
+ model.store(:mongo, STORE)
+ model.key(:object_id)
+ end
+ end
+ end
+end
View
135 spec/toy/mongo/querying_spec.rb
@@ -0,0 +1,135 @@
+require 'helper'
+
+describe Toy::Mongo::Querying do
+ uses_constants('User')
+
+ before(:each) do
+ User.identity_map_off
+ User.attribute(:name, String)
+ User.attribute(:bio, String)
+ end
+
+ describe "#query" do
+ it "returns a plucky query instance" do
+ User.query.should be_instance_of(Plucky::Query)
+ end
+ end
+
+ Toy::Mongo::Querying::PluckyMethods.each do |name|
+ it "delegates ##{name} to #query" do
+ query = User.query
+ query.should_receive(name)
+ User.should_receive(:query).and_return(query)
+ User.send(name)
+ end
+ end
+
+ describe "#atomic_update" do
+ before(:each) do
+ @user = User.create(:name => 'John')
+ end
+
+ it "performs update" do
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+ @user.reload
+ @user.name.should == 'Frank'
+ end
+
+ it "defaults to store's :safe option" do
+ @user.store.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => nil)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+
+ User.store(:mongo, STORE, :safe => false)
+ @user.store.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+
+ User.store(:mongo, STORE, :safe => true)
+ @user.store.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
+ @user.atomic_update('$set' => {'name' => 'Frank'})
+ end
+
+ context "with :safe option" do
+ it "overrides store's :safe option" do
+ User.store(:mongo, STORE, :safe => false)
+ @user.store.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => true)
+ @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => true)
+
+ User.store(:mongo, STORE, :safe => true)
+ @user.store.client.should_receive(:update).with(kind_of(Hash), kind_of(Hash), :safe => false)
+ @user.atomic_update({'$set' => {'name' => 'Frank'}}, :safe => false)
+ end
+ end
+
+ context "with :criteria option" do
+ uses_constants('Site')
+
+ it "allows updating embedded documents using $ positional operator" do
+ User.attribute(:sites, Array)
+ site1 = Site.create
+ site2 = Site.create
+ @user.update_attributes(:sites => [{'id' => site1.id, 'ui' => 1}, {'id' => site2.id, 'ui' => 2}])
+
+ @user.atomic_update(
+ {'$set' => {'sites.$.ui' => 2}},
+ {:criteria => {'sites.id' => site1.id}}
+ )
+ @user.reload
+ @user.sites.map { |s| s['ui'] }.should == [2, 2]
+ end
+ end
+ end
+
+ describe "#persistable_changes" do
+ before(:each) do
+ User.attribute(:password, String, :virtual => true)
+ User.attribute(:email, String, :abbr => :e)
+ @user = User.create(:name => 'John', :password => 'secret', :email => 'nunemaker@gmail.com')
+ end
+
+ it "returns only changed attributes" do
+ @user.name = 'Frank'
+ @user.persistable_changes.should == {'name' => 'Frank'}
+ end
+
+ it "returns typecast values" do
+ @user.name = 1234
+ @user.persistable_changes.should == {'name' => '1234'}
+ end
+
+ it "ignores virtual attributes" do
+ @user.password = 'ignore me'
+ @user.persistable_changes.should be_empty
+ end
+
+ it "uses abbreviated key" do
+ @user.email = 'john@orderedlist.com'
+ @user.persistable_changes.should == {'e' => 'john@orderedlist.com'}
+ end
+ end
+
+ describe "#atomic_update_attributes" do
+ before(:each) do
+ @user = User.create(:name => 'John', :bio => 'I make things simple.')
+ end
+
+ it "updates document" do
+ @user.atomic_update_attributes(:name => 'Frank')
+ @user.reload
+ @user.name.should == 'Frank'
+ end
+
+ it "returns true if valid" do
+ @user.atomic_update_attributes(:name => 'Frank').should be_true
+ end
+
+ it "returns false if invalid" do
+ User.validates_presence_of(:name)
+ @user.atomic_update_attributes(:name => '').should be_false
+ end
+
+ it "only persists changes" do
+ @user.should_receive(:atomic_update).with('$set' => {'name' => 'Frank'})
+ @user.atomic_update_attributes(:name => 'Frank')
+ end
+ end
+end
View
23 toystore-mongo.gemspec
@@ -0,0 +1,23 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "toy/mongo/version"
+
+Gem::Specification.new do |s|
+ s.name = "toystore-mongo"
+ s.version = Toy::Mongo::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ['John Nunemaker']
+ s.email = ['nunemaker@gmail.com']
+ s.homepage = ''
+ s.summary = %q{TODO: Mongo integration for Toystore}
+ s.description = %q{TODO: Mongo integration for Toystore}
+
+ s.add_dependency('plucky', '~> 0.4.0')
+ s.add_dependency('toystore', '~> 0.6.5')
+ s.add_dependency('adapter-mongo', '~> 0.5.2')
+
+ s.files = `git ls-files`.split("\n")
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end

0 comments on commit 049010e

Please sign in to comment.
Something went wrong with that request. Please try again.