├── .rspec ├── lib ├── adapter-mongo.rb └── adapter │ ├── mongo │ └── version.rb │ ├── mongo_atomic.rb │ └── mongo.rb ├── .gitignore ├── Rakefile ├── .travis.yml ├── Gemfile ├── Guardfile ├── spec ├── helper.rb ├── mongo_spec.rb ├── mongo_atomic_spec.rb └── support │ └── shared_mongo_adapter.rb ├── examples ├── object_for_key.rb ├── mongo.rb └── mongo_atomic.rb ├── adapter-mongo.gemspec ├── LICENSE └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /lib/adapter-mongo.rb: -------------------------------------------------------------------------------- 1 | require 'adapter/mongo' 2 | require 'adapter/mongo_atomic' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | *.gem 3 | .bundle 4 | log/* 5 | Gemfile.lock 6 | bin 7 | vendor/gems 8 | -------------------------------------------------------------------------------- /lib/adapter/mongo/version.rb: -------------------------------------------------------------------------------- 1 | module Adapter 2 | module Mongo 3 | VERSION = "0.8.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new 6 | 7 | task :default => :spec -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0 5 | - 2.1 6 | - 2.2 7 | bundler_args: --without development 8 | services: mongodb 9 | before_install: gem install bundler 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | gem 'rake' 5 | gem 'bson_ext', '~> 1.8' 6 | 7 | group(:test) do 8 | gem 'rspec', '~> 3.4.0' 9 | end 10 | 11 | group(:guard) do 12 | gem 'guard' 13 | gem 'guard-rspec' 14 | gem 'guard-bundler' 15 | end 16 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'bundler' do 2 | watch('Gemfile') 3 | watch(/^.+\.gemspec/) 4 | end 5 | 6 | guard 'rspec', :version => 2 do 7 | watch('spec/support/shared_mongo_adapter.rb') { 'spec' } 8 | watch(%r{^spec/.+_spec\.rb$}) 9 | watch(%r{^lib/adapter/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 10 | watch('spec/spec_helper.rb') { "spec" } 11 | end 12 | -------------------------------------------------------------------------------- /spec/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path('../../lib', __FILE__)) 2 | $:.unshift(File.expand_path('../', __FILE__)) 3 | 4 | require 'rubygems' 5 | require 'bundler' 6 | 7 | Bundler.require(:default, :test) 8 | 9 | require 'adapter/spec/an_adapter' 10 | require 'adapter-mongo' 11 | 12 | require 'support/shared_mongo_adapter' 13 | 14 | RSpec.configure do |c| 15 | 16 | end 17 | -------------------------------------------------------------------------------- /lib/adapter/mongo_atomic.rb: -------------------------------------------------------------------------------- 1 | require 'adapter/mongo' 2 | 3 | Adapter.define(:mongo_atomic, Adapter::Mongo) do 4 | # Public 5 | def write(key, attributes, options = nil) 6 | criteria = {:_id => key} 7 | updates = {'$set' => attributes} 8 | options = operation_options(options).merge(:upsert => true) 9 | client.update(criteria, updates, options) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/mongo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe "Mongo adapter" do 4 | before do 5 | @client = Mongo::MongoClient.new.db('test')['test'] 6 | @adapter = Adapter[adapter_name].new(@client) 7 | @adapter.clear 8 | end 9 | 10 | let(:adapter_name) { :mongo } 11 | 12 | let(:adapter) { @adapter } 13 | let(:client) { @client } 14 | 15 | it_should_behave_like 'a mongo adapter' 16 | end 17 | -------------------------------------------------------------------------------- /examples/object_for_key.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'pathname' 3 | 4 | root_path = Pathname(__FILE__).dirname.join('..').expand_path 5 | lib_path = root_path.join('lib') 6 | $:.unshift(lib_path) 7 | 8 | require 'adapter/mongo' 9 | 10 | key = BSON::OrderedHash['s' => 1, 'n' => 1] 11 | client = Mongo::Connection.new.db('adapter')['testing'] 12 | adapter = Adapter[:mongo].new(client) 13 | adapter.clear 14 | 15 | adapter.write(key, :v => 1) 16 | 17 | doc = adapter.read(key) 18 | 19 | puts doc.inspect 20 | -------------------------------------------------------------------------------- /spec/mongo_atomic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | describe "Mongo atomic adapter" do 4 | before do 5 | @client = Mongo::MongoClient.new.db('test')['test'] 6 | @adapter = Adapter[adapter_name].new(@client) 7 | @adapter.clear 8 | end 9 | 10 | let(:adapter_name) { :mongo_atomic } 11 | 12 | let(:adapter) { @adapter } 13 | let(:client) { @client } 14 | 15 | it_should_behave_like 'a mongo adapter' 16 | 17 | it "allows updating only part of a document" do 18 | oid = BSON::ObjectId.new 19 | adapter.write(oid, {'a' => 'c', 'b' => 'd'}) 20 | adapter.write(oid, {'a' => 'z'}) 21 | expect(adapter.read(oid)).to eq({ 22 | 'a' => 'z', 23 | 'b' => 'd', 24 | }) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /examples/mongo.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'pathname' 3 | 4 | root_path = Pathname(__FILE__).dirname.join('..').expand_path 5 | lib_path = root_path.join('lib') 6 | $:.unshift(lib_path) 7 | 8 | require 'adapter/mongo' 9 | 10 | key = BSON::ObjectId.new 11 | client = Mongo::MongoClient.new.db('adapter')['testing'] 12 | adapter = Adapter[:mongo].new(client) 13 | adapter.clear 14 | 15 | adapter.write(key, {'some' => 'thing'}) 16 | puts 'Should be {"some" => "thing"}: ' + adapter.read(key).inspect 17 | 18 | adapter.delete(key) 19 | puts 'Should be nil: ' + adapter.read(key).inspect 20 | 21 | adapter.write(key, {'some' => 'thing'}) 22 | adapter.clear 23 | puts 'Should be nil: ' + adapter.read(key).inspect 24 | 25 | puts 'Should be {"some" => "thing"}: ' + adapter.fetch(key, {'some' => 'thing'}).inspect 26 | -------------------------------------------------------------------------------- /adapter-mongo.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "adapter/mongo/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "adapter-mongo" 7 | s.version = Adapter::Mongo::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["John Nunemaker"] 10 | s.email = ["nunemaker@gmail.com"] 11 | s.homepage = "" 12 | s.summary = %q{Adapter for mongo} 13 | s.description = %q{Adapter for mongo} 14 | 15 | s.files = `git ls-files`.split("\n") 16 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 17 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 18 | s.require_paths = ["lib"] 19 | 20 | s.add_dependency 'adapter', '~> 0.7.1' 21 | s.add_dependency 'mongo', '~> 1.8' 22 | end 23 | -------------------------------------------------------------------------------- /examples/mongo_atomic.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'pathname' 3 | 4 | root_path = Pathname(__FILE__).dirname.join('..').expand_path 5 | lib_path = root_path.join('lib') 6 | $:.unshift(lib_path) 7 | 8 | require 'adapter/mongo_atomic' 9 | 10 | key = BSON::ObjectId.new 11 | full_doc = {'a' => 'c', 'b' => 'd'} 12 | partial_doc = {'a' => 'z'} 13 | client = Mongo::MongoClient.new.db('adapter')['testing'] 14 | adapter = Adapter[:mongo].new(client) 15 | atomic_adapter = Adapter[:mongo_atomic].new(client) 16 | 17 | adapter.clear 18 | atomic_adapter.clear 19 | 20 | adapter.write(key, full_doc) 21 | adapter.write(key, partial_doc) 22 | 23 | doc = adapter.read(key) 24 | doc.delete('_id') 25 | 26 | # full doc must always be written with :mongo adapter 27 | puts 'Should be {"a"=>"z"}: ' + doc.inspect 28 | 29 | atomic_adapter.write(key, full_doc) 30 | atomic_adapter.write(key, partial_doc) 31 | 32 | doc = atomic_adapter.read(key) 33 | doc.delete('_id') 34 | 35 | # partial updates can be written with atomic adapter as $set is used 36 | puts 'Should be {"a"=>"z", "b"=>"d"}: ' + doc.inspect 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 John Nunemaker 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. 21 | -------------------------------------------------------------------------------- /lib/adapter/mongo.rb: -------------------------------------------------------------------------------- 1 | require 'adapter' 2 | require 'mongo' 3 | 4 | module Adapter 5 | module Mongo 6 | 7 | # Public 8 | def read(key, options = nil) 9 | if doc = client.find_one('_id' => key) 10 | clean(doc) 11 | end 12 | end 13 | 14 | # Public 15 | def read_multiple(keys, options = nil) 16 | ids = keys.map { |key| key } 17 | docs = client.find('_id' => {'$in' => ids}).to_a 18 | keys_and_values = docs.map { |doc| [doc.delete('_id'), doc] } 19 | 20 | docs_by_id = Hash[keys_and_values] 21 | 22 | result = {} 23 | keys.each do |key| 24 | key = key 25 | result[key] = docs_by_id[key] 26 | end 27 | result 28 | end 29 | 30 | # Public 31 | def write(key, attributes, options = nil) 32 | options = operation_options(options) 33 | client.save(attributes.merge('_id' => key), options) 34 | end 35 | 36 | # Public 37 | def delete(key, options = nil) 38 | options = operation_options(options) 39 | client.remove({:_id => key}, options) 40 | end 41 | 42 | # Public 43 | def clear(options = nil) 44 | options = operation_options(options) 45 | client.remove({}, options) 46 | end 47 | 48 | # Private 49 | def clean(doc) 50 | doc.delete('_id') 51 | doc 52 | end 53 | 54 | # Private 55 | def operation_options(options) 56 | write_concern.merge(options || {}) 57 | end 58 | 59 | # Private 60 | def write_concern 61 | if options[:write_concern] 62 | options[:write_concern] 63 | else 64 | if options[:safe] 65 | {:w => 1} 66 | else 67 | {:w => 0} 68 | end 69 | end 70 | end 71 | end 72 | end 73 | 74 | Adapter.define(:mongo, Adapter::Mongo) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # adapter-mongo 2 | 3 | Mongo adapter for adapter gem. 4 | 5 | ```ruby 6 | require 'adapter/mongo' 7 | 8 | key = BSON::ObjectId.new 9 | client = Mongo::MongoClient.new.db('adapter')['testing'] 10 | adapter = Adapter[:mongo].new(client) 11 | adapter.clear 12 | 13 | adapter.write(key, {'some' => 'thing'}) 14 | puts 'Should be {"some" => "thing"}: ' + adapter.read(key).inspect 15 | 16 | adapter.delete(key) 17 | puts 'Should be nil: ' + adapter.read(key).inspect 18 | 19 | adapter.write(key, {'some' => 'thing'}) 20 | adapter.clear 21 | puts 'Should be nil: ' + adapter.read(key).inspect 22 | 23 | puts 'Should be {"some" => "thing"}: ' + adapter.fetch(key, {'some' => 'thing'}).inspect 24 | ``` 25 | 26 | ## Flavors 27 | 28 | There are two adapters included with this gem -- `:mongo` and `:mongo_atomic`. `:mongo` assumes that you are writing the full document each time. `:mongo_atomic` allows for partially updating documents. The difference is best shown with a bit of code. 29 | 30 | ```ruby 31 | require 'adapter/mongo_atomic' 32 | 33 | key = BSON::ObjectId.new 34 | full_doc = {'a' => 'c', 'b' => 'd'} 35 | partial_doc = {'a' => 'z'} 36 | client = Mongo::MongoClient.new.db('adapter')['testing'] 37 | adapter = Adapter[:mongo].new(client) 38 | atomic_adapter = Adapter[:mongo_atomic].new(client) 39 | 40 | adapter.clear 41 | atomic_adapter.clear 42 | 43 | adapter.write(key, full_doc) 44 | adapter.write(key, partial_doc) 45 | 46 | doc = adapter.read(key) 47 | doc.delete('_id') 48 | 49 | # full doc must always be written with :mongo adapter 50 | puts 'Should be {"a"=>"z"}: ' + doc.inspect 51 | 52 | atomic_adapter.write(key, full_doc) 53 | atomic_adapter.write(key, partial_doc) 54 | 55 | doc = atomic_adapter.read(key) 56 | doc.delete('_id') 57 | 58 | # partial updates can be written with atomic adapter as $set is used 59 | puts 'Should be {"a"=>"z", "b"=>"d"}: ' + doc.inspect 60 | ``` 61 | 62 | See examples/ or specs/ for more usage. 63 | 64 | ## Contributing 65 | 66 | * Fork the project. 67 | * Make your feature addition or bug fix. 68 | * Add tests for it. This is important so we don't break it in a future version unintentionally. 69 | * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine, but bump version in a commit by itself so we can ignore when we pull) 70 | * Send us a pull request. Bonus points for topic branches. 71 | -------------------------------------------------------------------------------- /spec/support/shared_mongo_adapter.rb: -------------------------------------------------------------------------------- 1 | shared_examples_for "a mongo adapter" do 2 | it_should_behave_like 'an adapter' 3 | 4 | it "allows using object id's as keys in correct type" do 5 | id = BSON::ObjectId.new 6 | attributes = {'one' => 'two'} 7 | adapter.write(id, attributes) 8 | expect(client.find_one('_id' => id)).not_to be_nil 9 | expect(adapter.read(id)).to eq(attributes) 10 | end 11 | 12 | it "allows using ordered hashes as keys" do 13 | key = BSON::OrderedHash['d', 1, 'n', 1] 14 | attributes = {'one' => 'two'} 15 | adapter.write(key, attributes) 16 | expect(client.find_one('_id' => key)).not_to be_nil 17 | expect(adapter.read(key)).to eq(attributes) 18 | end 19 | 20 | it "allows using hashes as keys" do 21 | key = {:d => 1} 22 | attributes = {'one' => 'two'} 23 | adapter.write(key, attributes) 24 | expect(client.find_one('_id' => key)).not_to be_nil 25 | expect(adapter.read(key)).to eq(attributes) 26 | end 27 | 28 | it "stores hashes right in document" do 29 | adapter.write('foo', 'steak' => 'bacon') 30 | expect(client.find_one('_id' => 'foo')).to eq({'_id' => 'foo', 'steak' => 'bacon'}) 31 | end 32 | 33 | describe "with safe option" do 34 | context "set to true" do 35 | before do 36 | client.ensure_index([['email', 1]], :unique => true) 37 | @adapter = Adapter[adapter_name].new(client, :safe => true) 38 | end 39 | 40 | after do 41 | client.drop_index('email_1') 42 | end 43 | 44 | it "does not raise operation failure on write if operation succeeds" do 45 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 46 | expect { 47 | adapter.write(BSON::ObjectId.new, {'email' => 'steve@orderedlist.com'}) 48 | }.not_to raise_error 49 | end 50 | 51 | it "raises operation failure on write if operation fails" do 52 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 53 | expect { 54 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 55 | }.to raise_error(Mongo::OperationFailure) 56 | end 57 | end 58 | 59 | context "set to false" do 60 | before do 61 | client.ensure_index([['email', 1]], :unique => true) 62 | @adapter = Adapter[adapter_name].new(client, :safe => false) 63 | end 64 | 65 | after do 66 | client.drop_index('email_1') 67 | end 68 | 69 | it "does not raise operation failure on write if operation succeeds" do 70 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 71 | expect { 72 | adapter.write(BSON::ObjectId.new, {'email' => 'steve@orderedlist.com'}) 73 | }.not_to raise_error 74 | end 75 | 76 | it "does not raise operation failure on write if operation fails" do 77 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 78 | expect { 79 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 80 | }.not_to raise_error 81 | end 82 | end 83 | end 84 | 85 | describe "with :write_concern" do 86 | before do 87 | client.ensure_index([['email', 1]], :unique => true) 88 | @adapter = Adapter[adapter_name].new(client, :write_concern => {:w => 1}) 89 | end 90 | 91 | after do 92 | client.drop_index('email_1') 93 | end 94 | 95 | it "does not raise operation failure on write if operation succeeds" do 96 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 97 | expect { 98 | adapter.write(BSON::ObjectId.new, {'email' => 'steve@orderedlist.com'}) 99 | }.not_to raise_error 100 | end 101 | 102 | it "raises operation failure on write if operation fails" do 103 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 104 | expect { 105 | adapter.write(BSON::ObjectId.new, {'email' => 'john@orderedlist.com'}) 106 | }.to raise_error(Mongo::OperationFailure) 107 | end 108 | 109 | it "allows overriding write concern for write" do 110 | id = BSON::ObjectId.new 111 | expect(client).to receive(:update). 112 | with( 113 | hash_including(:_id), 114 | kind_of(Hash), 115 | hash_including(:w => 0) 116 | ) 117 | adapter.write(id, {:foo => 'bar'}, :w => 0) 118 | end 119 | 120 | it "uses write concern for delete" do 121 | id = BSON::ObjectId.new 122 | expect(client).to receive(:remove).with({:_id => id}, :w => 1) 123 | adapter.delete(id) 124 | end 125 | 126 | it "allows overriding write concern for delete" do 127 | id = BSON::ObjectId.new 128 | expect(client).to receive(:remove).with({:_id => id}, :w => 0) 129 | adapter.delete(id, :w => 0) 130 | end 131 | 132 | it "uses write concern for clear" do 133 | id = BSON::ObjectId.new 134 | expect(client).to receive(:remove).with({}, :w => 1) 135 | adapter.clear 136 | end 137 | 138 | it "allows overriding write concern for clear" do 139 | id = BSON::ObjectId.new 140 | expect(client).to receive(:remove).with({}, :w => 0) 141 | adapter.clear(:w => 0) 142 | end 143 | end 144 | end 145 | --------------------------------------------------------------------------------