├── VERSION ├── .gitignore ├── spec ├── spec_helper.rb └── mongo_hydrator_spec.rb ├── Gemfile ├── LICENSE.txt ├── Rakefile ├── lib └── mongo_hydrator.rb ├── mongo_hydrator.gemspec └── README.markdown /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'mongo_hydrator' 3 | 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | 3 | gem 'mongo' 4 | gem 'document_hydrator' 5 | 6 | group :development do 7 | gem 'bson_ext', :platforms => :ruby 8 | gem 'system_timer', :platforms => :ruby_18 9 | gem 'rspec', '~> 2.6.0' 10 | gem 'jeweler' 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Greg Spurrier 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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | begin 4 | Bundler.setup(:default, :development) 5 | rescue Bundler::BundlerError => e 6 | $stderr.puts e.message 7 | $stderr.puts "Run `bundle install` to install missing gems" 8 | exit e.status_code 9 | end 10 | require 'rake' 11 | 12 | require 'jeweler' 13 | Jeweler::Tasks.new do |gem| 14 | # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options 15 | gem.name = "mongo_hydrator" 16 | gem.homepage = "http://github.com/gregspurrier/mongo_hydrator" 17 | gem.license = "MIT" 18 | gem.summary = %Q{MongoHydrator makes expanding embedded MongoDB IDs into embedded subdocuments quick and easy.} 19 | gem.description = %Q{MongoHydrator takes a document, represented as a Ruby Hash, and efficiently updates it so that embedded references to MongoDB documents are replaced with their corresponding subdocuments.} 20 | gem.email = "greg.spurrier@gmail.com" 21 | gem.authors = ["Greg Spurrier"] 22 | # dependencies defined in Gemfile 23 | end 24 | Jeweler::RubygemsDotOrgTasks.new 25 | 26 | require 'rspec/core/rake_task' 27 | RSpec::Core::RakeTask.new 28 | 29 | task :default => :spec 30 | -------------------------------------------------------------------------------- /lib/mongo_hydrator.rb: -------------------------------------------------------------------------------- 1 | require 'mongo' 2 | require 'document_hydrator' 3 | 4 | class MongoHydrator 5 | # Create a new MongoHydrator instance 6 | # 7 | # collection -- The Mongo::Collection instance from which to fetch 8 | # subdocuments during hydration 9 | # options -- Optional hash containing options to pass to 10 | # collection.find. Typically used to specify a :fields 11 | # option to limit the fields included in the subdocuments. 12 | # 13 | # Returns the new MongoHydrator instance. 14 | def initialize(collection, options = {}) 15 | @hydration_proc = Proc.new do |ids| 16 | if options[:fields] 17 | # We need the_id key in order to assemble the results hash. 18 | # If the caller has requested that it be omitted from the 19 | # result, re-enable it and then strip later. 20 | field_selectors = options[:fields] 21 | id_key = field_selectors.keys.detect { |k| k.to_s == '_id' } 22 | if id_key && field_selectors[id_key] == 0 23 | field_selectors.delete(id_key) 24 | strip_id = true 25 | end 26 | end 27 | subdocuments = collection.find({ '_id' => { '$in' => ids } }, options) 28 | subdocuments.inject({}) do |hash, subdocument| 29 | hash[subdocument['_id']] = subdocument 30 | subdocument.delete('_id') if strip_id 31 | hash 32 | end 33 | end 34 | end 35 | 36 | def hydrate_document(document, path_or_paths) 37 | DocumentHydrator.hydrate_document(document, path_or_paths, @hydration_proc) 38 | end 39 | 40 | def hydrate_document(document, path_or_paths) 41 | DocumentHydrator.hydrate_document(document, path_or_paths, @hydration_proc) 42 | end 43 | end -------------------------------------------------------------------------------- /mongo_hydrator.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec' 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{mongo_hydrator} 8 | s.version = "0.1.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Greg Spurrier"] 12 | s.date = %q{2011-06-20} 13 | s.description = %q{MongoHydrator takes a document, represented as a Ruby Hash, and efficiently updates it so that embedded references to MongoDB documents are replaced with their corresponding subdocuments.} 14 | s.email = %q{greg.spurrier@gmail.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE.txt", 17 | "README.markdown" 18 | ] 19 | s.files = [ 20 | "Gemfile", 21 | "LICENSE.txt", 22 | "README.markdown", 23 | "Rakefile", 24 | "VERSION", 25 | "lib/mongo_hydrator.rb", 26 | "mongo_hydrator.gemspec", 27 | "spec/mongo_hydrator_spec.rb", 28 | "spec/spec_helper.rb" 29 | ] 30 | s.homepage = %q{http://github.com/gregspurrier/mongo_hydrator} 31 | s.licenses = ["MIT"] 32 | s.require_paths = ["lib"] 33 | s.rubygems_version = %q{1.6.2} 34 | s.summary = %q{MongoHydrator makes expanding embedded MongoDB IDs into embedded subdocuments quick and easy.} 35 | 36 | if s.respond_to? :specification_version then 37 | s.specification_version = 3 38 | 39 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 40 | s.add_runtime_dependency(%q, [">= 0"]) 41 | s.add_runtime_dependency(%q, [">= 0"]) 42 | s.add_development_dependency(%q, [">= 0"]) 43 | s.add_development_dependency(%q, [">= 0"]) 44 | s.add_development_dependency(%q, ["~> 2.6.0"]) 45 | s.add_development_dependency(%q, [">= 0"]) 46 | else 47 | s.add_dependency(%q, [">= 0"]) 48 | s.add_dependency(%q, [">= 0"]) 49 | s.add_dependency(%q, [">= 0"]) 50 | s.add_dependency(%q, [">= 0"]) 51 | s.add_dependency(%q, ["~> 2.6.0"]) 52 | s.add_dependency(%q, [">= 0"]) 53 | end 54 | else 55 | s.add_dependency(%q, [">= 0"]) 56 | s.add_dependency(%q, [">= 0"]) 57 | s.add_dependency(%q, [">= 0"]) 58 | s.add_dependency(%q, [">= 0"]) 59 | s.add_dependency(%q, ["~> 2.6.0"]) 60 | s.add_dependency(%q, [">= 0"]) 61 | end 62 | end 63 | 64 | -------------------------------------------------------------------------------- /spec/mongo_hydrator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # The heavy lifting is tested in DocumentHydrator's tests. Here we just 4 | # make sure that things are fetched from the database as expected. 5 | 6 | describe MongoHydrator, '#hydrate_document' do 7 | before(:each) do 8 | db = Mongo::Connection.new.db('mongo_hydrator_test') 9 | @users_collection = db['users'] 10 | @users_collection.remove 11 | @users_collection.insert(:_id => 1, :name => 'Obi-Wan Kenobi', :occupation => 'Hermit') 12 | @users_collection.insert(:_id => 2, :name => 'Han Solo', :occupation => 'Smuggler') 13 | @users_collection.insert(:_id => 3, :name => 'Luke Skywalker', :occupation => 'Farmer') 14 | @users_collection.insert(:_id => 4, :name => 'Yoda', :occupation => 'Jedi Master') 15 | end 16 | 17 | context 'for a hydrator with no options' do 18 | before(:each) do 19 | @document = { 20 | "user_id" => 1, 21 | "text" => "May the Force be with you.", 22 | "liker_ids" => [3, 4], 23 | "comments" => [ 24 | { "user_id" => 2, 25 | "text" => "Thanks, but I'll stick with my blaster." 26 | }, 27 | { "user_id" => 3, 28 | "text" => "Hey, show some respect!" 29 | } 30 | ] 31 | } 32 | @hydrator = MongoHydrator.new(@users_collection) 33 | end 34 | 35 | it 'hydrates the document' do 36 | expected = @document.dup 37 | expected['user'] = @users_collection.find_one(:_id => expected.delete('user_id')) 38 | expected['likers'] = expected.delete('liker_ids').map do |user_id| 39 | @users_collection.find_one(:_id => user_id) 40 | end 41 | expected['comments'].each do |comment| 42 | comment['user'] = @users_collection.find_one(:_id => comment.delete('user_id')) 43 | end 44 | 45 | @hydrator.hydrate_document(@document, ['user_id', 'liker_ids', 'comments.user_ids']) 46 | @document.should == expected 47 | end 48 | end 49 | 50 | context 'for a hydrator with a limited field set' do 51 | before(:each) do 52 | @document = { 53 | "user_id" => 1, 54 | "text" => "May the Force be with you.", 55 | "liker_ids" => [3, 4], 56 | "comments" => [ 57 | { "user_id" => 2, 58 | "text" => "Thanks, but I'll stick with my blaster." 59 | }, 60 | { "user_id" => 3, 61 | "text" => "Hey, show some respect!" 62 | } 63 | ] 64 | } 65 | @options = { :fields => { :_id => 0, :name => 1 } } 66 | @hydrator = MongoHydrator.new(@users_collection, @options) 67 | end 68 | 69 | it 'hydrates the document, using only the requested fields' do 70 | expected = @document.dup 71 | expected['user'] = @users_collection.find_one({ :_id => expected.delete('user_id') }, @options) 72 | expected['likers'] = expected.delete('liker_ids').map do |user_id| 73 | @users_collection.find_one({ :_id => user_id }, @options) 74 | end 75 | expected['comments'].each do |comment| 76 | comment['user'] = @users_collection.find_one({ :_id => comment.delete('user_id') }, @options) 77 | end 78 | 79 | @hydrator.hydrate_document(@document, ['user_id', 'liker_ids', 'comments.user_ids']) 80 | @document.should == expected 81 | end 82 | end 83 | end -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # MongoHydrator 2 | MongoHydrator makes turning a document with embedded references like this: 3 | 4 | status_update = { 5 | "_id" => 37, 6 | "user_id" => 1, 7 | "text" => "May the Force be with you.", 8 | "liker_ids" => [3, 4], 9 | "comments" => [ 10 | { "user_id" => 2, 11 | "text" => "Thanks, but I'll stick with my blaster." 12 | }, 13 | { "user_id" => 3, 14 | "text" => "Hey, show some respect!" 15 | } 16 | ] 17 | } 18 | 19 | into a document with expanded subdocuments like this: 20 | 21 | { 22 | "_id" => 37, 23 | "user" => { "_id"=>1, "name"=>"Obi-Wan Kenobi", "occupation"=>"Hermit" }, 24 | "text" => "May the Force be with you.", 25 | "likers" => [ 26 | {"_id" => 3, "name" => "Luke Skywalker", "occupation" => "Farmer"}, 27 | {"_id" => 4, "name" => "Yoda", "occupation" => "Jedi Master"} 28 | ], 29 | "comments" => [ 30 | { "user" => { "_id" => 2, "name" => "Han Solo", "occupation" => "Smuggler" }, 31 | "text" => "Thanks, but I'll stick with my blaster." 32 | }, 33 | { "text" => "Hey, show some respect!", 34 | "user" => { "_id" => 3, "name" => "Luke Skywalker", "occupation" => "Farmer"} 35 | } 36 | ] 37 | } 38 | 39 | as simple as this: 40 | 41 | # users is an instance of Mongo::Collection 42 | hydrator = MongoHydrator.new(users) 43 | hydrator.hydrate_document(status_update, 44 | ['user_id', 'liker_ids', 'comments.user_id']) 45 | 46 | Behind the scenes, a single MongoDB query is used to retrieve the user 47 | documents corresponding to the IDs referenced by the specified paths: 48 | 'user_id', 'liker_ids', and 'comments.user_id'. 49 | 50 | Integers are used above to make the example cleaner, but, of course, any form of valid MongoDB IDs can be used. 51 | 52 | ## Installation 53 | Install the gem: 54 | 55 | gem install mongo_hydrator 56 | 57 | Require the file: 58 | 59 | require 'mongo_hydrator' 60 | 61 | Or, if you use Bundler, add this to your Gemfile: 62 | 63 | gem 'mongo_hydrator' 64 | 65 | ## Paths 66 | A call to MongoHydrator#hydrate_document requires one or more paths to tell the hydrator which key or keys to replace in the original document. Paths use the same dot notation used in MongoDB queries. The example above uses three paths: 67 | 68 | * user_id -- a top-level key holding an ID 69 | * liker_ids -- an top-level key holding an array of IDs 70 | * comments.user_id -- an array of objects, each with an embedded ID 71 | 72 | Intermediate steps in the path may be hashes or arrays of hashes. The final step in the path may be an ID or an array of IDs. 73 | 74 | MongoHydrate#hydrate_document accepts either a single path or an array of paths. E.g.: 75 | 76 | hydrator.hydrate_document(document, 'user_id') 77 | hydrator.hydrate_document(document, ['user_id', 'liker_ids']) 78 | 79 | ## ID Suffix Stripping 80 | If the paths in the original dehydrated document end in '_id' or '_ids', those suffixes will be stripped during hydration so that the key names continue to make sense. Pluralization is taken into account, so 'user_id' becomes 'user' and 'user_ids' becomes 'users'. 81 | 82 | ## Limiting Fields 83 | To limit the fields that are included in the hydrated subdocuments, use the `:fields` option when creating the hydrator: 84 | 85 | hydrator = MongoHydrator.new(users_collection, :fields => { :_id => 0, :name => 1 }) 86 | 87 | Then only the specified fields will show up in the hydrated result. E.g.,: 88 | 89 | hydrator.hydrate_document(status_update, 90 | ['user_id', 'liker_ids', 'comments.user_id']) 91 | # => { 92 | # "_id" => 37, 93 | # "user" => { "name"=>"Obi-Wan Kenobi" }, 94 | # "text" => "May the Force be with you.", 95 | # "likers" => [ 96 | # { "name" => "Luke Skywalker" }, 97 | # { "name" => "Yoda" } 98 | # ], 99 | # "comments" => [ 100 | # { "user" => { "name" => "Han Solo" }, 101 | # "text" => "Thanks, but I'll stick with my blaster." 102 | # }, 103 | # { "text" => "Hey, show some respect!", 104 | # "user" => { "name" => "Luke Skywalker" } 105 | # } 106 | # ] 107 | # } 108 | 109 | The `:fields` option takes the same format as it does for Mongo::Collection#find. 110 | 111 | ## Hydrating Multiple Documents 112 | To hydrate multiple documents at once, use `hydrate_documents`. The arguments are the same as for `hydrate_document` with the exception that the first argument is an array of documents to hydrate. As with `hydrate_document` a single MongoDB query will be used to retrieve the required documents. 113 | 114 | ## Additional Notes 115 | MongoHydrator expects the document being hydrated to have strings for keys. This will already be the case if the document came from the Mongo driver. If, however, the document is using symbols for keys, you will need to convert the keys to strings before hydration. 116 | 117 | ## Supported Rubies 118 | MongoHydrator has been tested with: 119 | 120 | * Ruby 1.8.7 (p334) 121 | * Ruby 1.9.2 (p180) 122 | * JRuby 1.6.2 123 | 124 | ## Copyright 125 | Copyright (c) 2011 Greg Spurrier. Released under the MIT license. See LICENSE.txt for further details. 126 | --------------------------------------------------------------------------------