Created
May 9, 2011 21:54
-
-
Save robertsosinski/963498 to your computer and use it in GitHub Desktop.
Mongo typecasting library for defining field and embed types.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module Document | |
| class CannotLoadConfig < Exception; end | |
| class DocumentNotValid < Exception; end | |
| class DocumentNotFound < Exception; end | |
| class InvalidFieldType < Exception; end | |
| class InvalidEmbedType < Exception; end | |
| def self.hookup | |
| @config ||= YAML.load(File.read(Rails.root + "config" + "mongo.yml"))[Rails.env] | |
| @db ||= Mongo::Connection.new(@config["host"], @config["port"]).db(@config["database"]) | |
| rescue | |
| raise CannotLoadConfig.new("You must specify a 'host', 'port' and 'database' in your 'config/mongo.yml' file.") | |
| end | |
| class Base < BSON::OrderedHash | |
| def self.inherited(child) | |
| child.send :extend, ClassMethods | |
| child.send :include, InstanceMethods | |
| unless self == Document::Base | |
| child.fields.update(self.fields) | |
| child.embeds.update(self.embeds) | |
| end | |
| end | |
| end | |
| module RootDocumentMethods | |
| def hookup | |
| @hookup ||= Document.hookup[collection_name] | |
| end | |
| def [](id, options = {}) | |
| doc = hookup.find_one(BSON::ObjectId.from_string(id.to_s), options) | |
| if doc.nil? | |
| raise DocumentNotFound.new("Document could not be found with the id '#{id}'.") | |
| else | |
| new(doc) | |
| end | |
| end | |
| end | |
| module ClassMethods | |
| def collection(name) | |
| @collection_name = name.to_s | |
| self.send :extend, RootDocumentMethods | |
| end | |
| def collection_name | |
| @collection_name | |
| end | |
| def root_document? | |
| !!collection_name | |
| end | |
| def typecaster(field_type) | |
| case field_type.to_s | |
| when "BSON::ObjectId" | |
| Proc.new{|value| value.instance_of?(BSON::ObjectId) ? value : BSON::ObjectId.from_string(value.to_s)} | |
| when "Time" | |
| Proc.new{|value| value.instance_of?(Time) ? value : Time.parse(value.to_s)} | |
| when "String" | |
| Proc.new{|value| value.to_s} | |
| when "Integer" | |
| Proc.new{|value| value.to_i} | |
| when "Float" | |
| Proc.new{|value| value.to_f} | |
| when "TrueClass", "FalseClass", "Boolean" | |
| Proc.new{|value| (%w{false nil null undefined 0 NaN}+[""]).include?(value.to_s) ? false : !!value} | |
| else | |
| raise InvalidFieldType.new("Document fields can only be of type `BSON::ObjectId`, `Time`, `String`," + \ | |
| "`Integer`, `Float` or (`TrueClass`, `FalseClass`, `Boolean`).") | |
| end | |
| end | |
| def fields | |
| @fields ||= {} | |
| end | |
| def field(field_name, field_type) | |
| field = self.fields[field_name] = {:type => field_type, :typecaster => typecaster(field_type)} | |
| define_method(field_name) do | |
| self[field_name.to_s] | |
| end | |
| define_method("#{field_name}=") do |value| | |
| self[field_name.to_s] = field[:typecaster].call(value) | |
| end | |
| end | |
| def embeds | |
| @embeds ||= {} | |
| end | |
| def embed(embed_name, embed_type, options = {}) | |
| factory = lambda do |object| | |
| if options[:polymorphic] | |
| self.const_get(object[options[:polymorphic].to_s]) | |
| else | |
| self.const_get(embed_name.to_s.classify) | |
| end | |
| end | |
| embed = self.embeds[embed_name] = {:type => embed_type, :factory => factory, :polymorphic => !!options[:polymorphic]} | |
| define_method(embed_name) do | |
| self[embed_name.to_s] | |
| end | |
| case embed_type.to_s | |
| when "Document" | |
| define_method("#{embed_name}=") do |value| | |
| self[embed_name.to_s] = embed[:factory].call(self).new(value) | |
| end | |
| when "Array" | |
| define_method("#{embed_name}=") do |value| | |
| self[embed_name.to_s] = value.map{|object| embed[:factory].call(self).new(object)} | |
| end | |
| else | |
| raise InvalidEmbedType.new("Documents can only embed a `Document` or an `Array`.") | |
| end | |
| end | |
| end | |
| module InstanceMethods | |
| def initialize(hash = {}) | |
| if hash.is_a?(Hash) | |
| super() | |
| update(hash).tap do |new_hash| | |
| new_hash.default = hash.default | |
| end | |
| else | |
| raise DocumentNotValid.new("A properly structured `Document` must be passed when initialized.") | |
| end | |
| end | |
| def [](name) | |
| value = super(name.to_s) | |
| value = super(name.to_sym) if value.nil? | |
| value | |
| end | |
| def []=(name, value) | |
| super(name.to_s, convert(name, value)) | |
| end | |
| def update(new_hash) | |
| polymorphs = {} # Ensure that polymorphic embeds are always updated last. | |
| new_hash.each_pair do |name, value| | |
| embed = self.class.embeds[name.to_sym] | |
| if embed && embed[:polymorphic] | |
| polymorphs[name] = value | |
| else | |
| self[name] = value | |
| end | |
| end | |
| polymorphs.each_pair do |name, value| | |
| self[name] = value | |
| end | |
| self | |
| end | |
| def dup | |
| self.class.new(self) | |
| end | |
| def merge(hash) | |
| dup.update(hash) | |
| end | |
| def to_hash | |
| BSON::OrderedHash.new(self.default).update(self) | |
| end | |
| def convert(name, value) | |
| case value | |
| when Hash | |
| self.class.embeds[name.to_sym][:factory].call(self).new(value) | |
| when Array | |
| value.map do |object| | |
| self.class.embeds[name.to_sym][:factory].call(self).new(object) | |
| end | |
| when Symbol | |
| value.to_s # Symbols should always be converted to Strings. | |
| else | |
| self.class.fields[name.to_sym][:typecaster].call(value) | |
| end | |
| rescue | |
| value # Just return the value if there is any problem converting it. | |
| end | |
| end | |
| end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| require 'spec_helper' | |
| class TestDocument < Document::Base | |
| collection :test_documents | |
| field :_id, BSON::ObjectId | |
| field :date, Time | |
| field :text, String | |
| field :number, Integer | |
| field :percent, Float | |
| field :boolean, TrueClass | |
| field :type_a, String | |
| field :type_b, String | |
| embed :e_document, Document | |
| embed :e_documents, Array | |
| embed :p_document, Document, :polymorphic => :type_a | |
| embed :p_documents, Array, :polymorphic => :type_b | |
| end | |
| class TestDocument | |
| class EDocument < Document::Base | |
| field :value, String | |
| end | |
| class PDocument < Document::Base | |
| field :value, String | |
| end | |
| class PDocumentA < PDocument | |
| field :value_a, String | |
| end | |
| class PDocumentB < PDocument | |
| field :value_b, String | |
| end | |
| end | |
| describe Document do | |
| describe '.hookup' do | |
| it 'should be of type `Mongo::DB`' do | |
| Document.hookup.should be_instance_of Mongo::DB | |
| end | |
| end | |
| describe Document::RootDocumentMethods do | |
| describe '#hookup' do | |
| it 'should be of type `Mongo::Collection`' do | |
| TestDocument.hookup.should be_instance_of Mongo::Collection | |
| end | |
| end | |
| end | |
| describe Document::ClassMethods do | |
| describe '#collection_name' do | |
| it 'should return the collection name for root documents' do | |
| TestDocument.collection_name.should eql 'test_documents' | |
| end | |
| it 'should return `nil` for non-root documents' do | |
| TestDocument::EDocument.collection_name.should be_nil | |
| end | |
| end | |
| describe '#root_document?' do | |
| it 'should return `true` for root documents' do | |
| TestDocument.root_document?.should be_true | |
| end | |
| it 'should return `false` for non-root documents' do | |
| TestDocument::EDocument.root_document?.should be_false | |
| end | |
| end | |
| describe '#typecaster' do | |
| it 'should create a `Proc` that will typecast strings into their specified type' do | |
| TestDocument.typecaster(BSON::ObjectId).call("4dc9ac68bd72240286000001").should eql BSON::ObjectId("4dc9ac68bd72240286000001") | |
| TestDocument.typecaster(Time).call("Tue May 10 12:00:00 UTC 2011").should eql Time.parse("Tue May 10 12:00:00 UTC 2011") | |
| TestDocument.typecaster(String).call("Hello World!").should eql "Hello World!" | |
| TestDocument.typecaster(Integer).call("1234567890").should eql 1234567890 | |
| TestDocument.typecaster(Float).call("1234.5678").should eql 1234.5678 | |
| TestDocument.typecaster(TrueClass).call("true").should eql true | |
| TestDocument.typecaster(TrueClass).call("1").should eql true | |
| TestDocument.typecaster(TrueClass).call(1).should eql true | |
| TestDocument.typecaster(FalseClass).call("false").should eql false | |
| TestDocument.typecaster(FalseClass).call("nil").should eql false | |
| TestDocument.typecaster(FalseClass).call(nil).should eql false | |
| TestDocument.typecaster(FalseClass).call("null").should eql false | |
| TestDocument.typecaster(FalseClass).call("undefined").should eql false | |
| TestDocument.typecaster(FalseClass).call("0").should eql false | |
| TestDocument.typecaster(FalseClass).call(0).should eql false | |
| TestDocument.typecaster(FalseClass).call("NaN").should eql false | |
| end | |
| it 'should raise an `InvalidFieldType` error if an unsupported type is given' do | |
| lambda{TestDocument.typecaster(Hash).call("{}").should}.should raise_error Document::InvalidFieldType | |
| end | |
| end | |
| describe '#fields' do | |
| it 'should return field objects' do | |
| TestDocument.fields[:_id][:type].should be BSON::ObjectId | |
| TestDocument.fields[:_id][:typecaster].should be_instance_of Proc | |
| end | |
| it 'should return all fields specified' do | |
| TestDocument.fields.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:_id, :boolean, :date, :number, :percent, :text, :type_a, :type_b] | |
| TestDocument::EDocument.fields.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:value] | |
| TestDocument::PDocument.fields.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:value] | |
| TestDocument::PDocumentA.fields.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:value, :value_a] | |
| TestDocument::PDocumentB.fields.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:value, :value_b] | |
| end | |
| end | |
| describe '#field' do | |
| it 'should create a typecasted field' do | |
| document = TestDocument.new({ | |
| "_id" => "4dc9ac68bd72240286000001", | |
| "date" => "Tue May 10 12:00:00 UTC 2011", | |
| "text" => "Hello World!", | |
| "number" => "1234567890", | |
| "percent" => "1234.5678", | |
| "boolean" => "true" | |
| }) | |
| document.should eql(BSON::OrderedHash.new.update({ | |
| "_id" => BSON::ObjectId('4dc9ac68bd72240286000001'), | |
| "date" => Time.parse("Tue May 10 12:00:00 UTC 2011"), | |
| "text" => "Hello World!", | |
| "number" => 1234567890, | |
| "percent" => 1234.5678, | |
| "boolean" => true | |
| })) | |
| end | |
| end | |
| describe '#embeds' do | |
| it 'should return embed objects' do | |
| TestDocument.embeds[:e_document][:type].should be Document | |
| TestDocument.embeds[:e_document][:factory].should be_instance_of Proc | |
| TestDocument.embeds[:e_document][:polymorphic].should be_false | |
| TestDocument.embeds[:p_document][:polymorphic].should be_true | |
| end | |
| it 'should return all embeds specified' do | |
| TestDocument.embeds.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [:e_document, :e_documents, :p_document, :p_documents] | |
| TestDocument::EDocument.embeds.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [] | |
| TestDocument::PDocument.embeds.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [] | |
| TestDocument::PDocumentA.embeds.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [] | |
| TestDocument::PDocumentB.embeds.keys.sort{|a, b| a.to_s <=> b.to_s}.should eql [] | |
| end | |
| end | |
| describe '#embed' do | |
| it 'should created a factory built embed' do | |
| document = TestDocument.new({ | |
| "_id" => BSON::ObjectId('4dc9ac68bd72240286000001'), | |
| "type_a" => "PDocumentA", | |
| "type_b" => "PDocumentB", | |
| "e_document" => {"value" => "a"}, | |
| "e_documents" => [{"value" => "b"}, {"value" => "c"}], | |
| "p_document" => {"value" => "d", "value_a" => "dd"}, | |
| "p_documents" => [{"value" => "e", "value_b" => "ee"}, {"value" => "f", "value_b" => "ff"}] | |
| }) | |
| document.should be_instance_of TestDocument | |
| document.e_document.should be_instance_of TestDocument::EDocument | |
| document.e_documents.should be_instance_of Array | |
| document.e_documents.first.should be_instance_of TestDocument::EDocument | |
| document.p_document.should be_instance_of TestDocument::PDocumentA | |
| document.p_documents.should be_instance_of Array | |
| document.p_documents.first.should be_instance_of TestDocument::PDocumentB | |
| end | |
| end | |
| end | |
| describe Document::InstanceMethods do | |
| describe '#initialize' do | |
| describe 'when not given a Hash' do | |
| it 'should return an empty Document' do | |
| document = TestDocument.new | |
| document.should be_instance_of TestDocument | |
| document.should eql(BSON::OrderedHash.new.update({})) | |
| end | |
| end | |
| describe 'when given a Hash' do | |
| it 'should return a converted object' do | |
| document = TestDocument.new({"text" => "Hello World!", "number" => "123"}) | |
| document.should be_instance_of TestDocument | |
| document.should eql(BSON::OrderedHash.new.update({"text" => "Hello World!", "number" => 123})) | |
| end | |
| end | |
| describe 'when given anything else' do | |
| it 'should raise an `DocumentNotValid` error' do | |
| lambda{TestDocument.new(nil)}.should raise_error Document::DocumentNotValid | |
| end | |
| end | |
| end | |
| describe '#[]' do | |
| it 'should get a value when a `String` or `Symbol` is given' do | |
| document = TestDocument.new({"text" => "Hello World!", "number" => "123"}) | |
| document["text"].should eql("Hello World!") | |
| document[:number].should eql(123) | |
| end | |
| end | |
| describe '#[]=' do | |
| it 'should set and convert a value when a `String` or `Symbol` is used as a key' do | |
| document = TestDocument.new | |
| document["text"] = "Hello World!" | |
| document[:number] = "123" | |
| document["text"].should eql("Hello World!") | |
| document[:number].should eql(123) | |
| end | |
| end | |
| describe '#update' do | |
| it 'should set and convert each pair given' do | |
| document = TestDocument.new | |
| document.update({"text" => "Hello World!", "number" => "123"}) | |
| document.should eql(BSON::OrderedHash.new.update({"text" => "Hello World!", "number" => 123})) | |
| end | |
| end | |
| describe '#dup' do | |
| it 'should make a duplicate document' do | |
| document = TestDocument.new({"text" => "Hello World!", "number" => "123"}) | |
| duplicate = document.dup | |
| document.to_hash.should eql(duplicate.to_hash) | |
| document.object_id.should_not eql(duplicate.object_id) | |
| end | |
| end | |
| describe '#merge' do | |
| it 'should make a duplicate document and set and convert each pair given' do | |
| document = TestDocument.new | |
| duplicate = document.merge({"text" => "Hello World!", "number" => "123"}) | |
| document.should eql(BSON::OrderedHash.new.update({})) | |
| duplicate.should eql(BSON::OrderedHash.new.update({"text" => "Hello World!", "number" => 123})) | |
| document.object_id.should_not eql(duplicate.object_id) | |
| end | |
| end | |
| describe '#to_hash' do | |
| it 'should convert a `Document` into a `BSON::OrderedHash`' do | |
| document = TestDocument.new({"text" => "Hello World!", "number" => "123"}) | |
| document.to_hash.should eql(BSON::OrderedHash.new.update({"text" => "Hello World!", "number" => 123})) | |
| document.to_hash.should be_instance_of BSON::OrderedHash | |
| end | |
| end | |
| describe '#convert' do | |
| it 'should typecast fields, use a factory to convert embeds, and set other fields' do | |
| document = TestDocument.new("type_a" => "PDocumentA", "type_b" => "PDocumentB") | |
| document.convert("_id", "4dc9ac68bd72240286000001").should eql BSON::ObjectId("4dc9ac68bd72240286000001") | |
| document.convert("date", "Tue May 10 12:00:00 UTC 2011").should eql Time.parse("Tue May 10 12:00:00 UTC 2011") | |
| document.convert("text", "Hello World!").should eql("Hello World!") | |
| document.convert("number", "1234").should eql(1234) | |
| document.convert("percent", "12.34").should eql(12.34) | |
| document.convert("boolean", "true").should be_true | |
| document.convert("e_document", {}).should be_instance_of TestDocument::EDocument | |
| document.convert("e_documents", [{}]).should be_instance_of Array | |
| document.convert("e_documents", [{}]).first.should be_instance_of TestDocument::EDocument | |
| document.convert("p_document", {}).should be_instance_of TestDocument::PDocumentA | |
| document.convert("p_documents", [{}]).should be_instance_of Array | |
| document.convert("p_documents", [{}]).first.should be_instance_of TestDocument::PDocumentB | |
| end | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment