Skip to content

Instantly share code, notes, and snippets.

@robertsosinski
Created May 9, 2011 21:54
Show Gist options
  • Select an option

  • Save robertsosinski/963498 to your computer and use it in GitHub Desktop.

Select an option

Save robertsosinski/963498 to your computer and use it in GitHub Desktop.
Mongo typecasting library for defining field and embed types.
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
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