Skip to content

Commit

Permalink
Add key aliasing
Browse files Browse the repository at this point in the history
Add spec for key names which don't map to Ruby names easily.

Add :field_name as alternate for :abbr/:alias

Closes #353
  • Loading branch information
cheald committed Jul 8, 2013
1 parent 9b3bd27 commit f001de0
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 14 deletions.
2 changes: 1 addition & 1 deletion lib/mongo_mapper/plugins/document.rb
Expand Up @@ -24,7 +24,7 @@ def reload
get_proxy(association).reset
end
instance_variables.each { |ivar| remove_instance_variable(ivar) }
load_from_database(doc)
initialize_from_database(doc)
self
else
raise DocumentNotFound, "Document match #{_id.inspect} does not exist in #{collection.name} collection"
Expand Down
29 changes: 19 additions & 10 deletions lib/mongo_mapper/plugins/keys.rb
Expand Up @@ -23,16 +23,26 @@ def keys
@keys ||= {}
end

def unaliased_keys
keys.inject({}) {|h, (n, k)| h[k.name] = k if n == k.name; h }
end

def key(*args)
Key.new(*args).tap do |key|
keys[key.name] = key
keys[key.abbr] = key if key.abbr
create_accessors_for(key)
create_key_in_descendants(*args)
create_indexes_for(key)
create_validations_for(key)
end
end

def persisted_name(name)
keys[name.to_s].persisted_name
end
alias_method :abbr, :persisted_name

def key?(key)
keys.key? key.to_s
end
Expand All @@ -42,7 +52,7 @@ def using_object_id?
end

def object_id_keys
@object_id_keys ||= keys.keys.select { |key| keys[key].type == ObjectId }.map(&:to_sym)
@object_id_keys ||= unaliased_keys.keys.select { |key| keys[key].type == ObjectId }.map(&:to_sym)
end

def object_id_key?(name)
Expand Down Expand Up @@ -169,13 +179,17 @@ def create_validations_for(key)

def initialize(attrs={})
@_new = true
@_mm_keys = self.class.keys

initialize_default_values(attrs)
self.attributes = attrs
yield self if block_given?
end

def initialize_from_database(attrs={})
@_new = false
@_mm_keys = self.class.keys

initialize_default_values(attrs)
load_from_database(attrs)
self
Expand All @@ -197,12 +211,11 @@ def attributes=(attrs)
end
end

def to_mongo
def to_mongo(include_abbreviatons = true)
BSON::OrderedHash.new.tap do |attrs|
keys.each do |name, key|
self.class.unaliased_keys.each do |name, key|
if key.type == ObjectId || !self[key.name].nil?
value = key.set(self[key.name])
attrs[name] = value
attrs[include_abbreviatons && key.persisted_name || name] = key.set(self[key.name])
end
end

Expand All @@ -219,7 +232,7 @@ def to_mongo
end

def attributes
to_mongo.with_indifferent_access
to_mongo(false).with_indifferent_access
end

def assign(attrs={})
Expand Down Expand Up @@ -290,9 +303,6 @@ def embedded_keys
def load_from_database(attrs)
return if attrs == nil || attrs.blank?

# Init the keys ivar. Due to the volume of times this method is called, we don't want it in a method.
@_mm_keys ||= self.class.keys

attrs.each do |key, value|
if !@_mm_keys.key?(key) && respond_to?(:"#{key}=")
self.send(:"#{key}=", value)
Expand All @@ -315,7 +325,6 @@ def write_key(name, value)
end

def internal_write_key(name, value, cast = true)
@_mm_keys ||= self.class.keys
key = @_mm_keys[name] || self.class.key(name)
as_mongo = cast ? key.set(value) : value
as_typecast = key.get(as_mongo)
Expand Down
11 changes: 9 additions & 2 deletions lib/mongo_mapper/plugins/keys/key.rb
Expand Up @@ -3,7 +3,7 @@ module MongoMapper
module Plugins
module Keys
class Key
attr_accessor :name, :type, :options, :default, :ivar
attr_accessor :name, :type, :options, :default, :ivar, :abbr

ID_STR = '_id'

Expand All @@ -15,6 +15,9 @@ def initialize(*args)
self.options = (options_from_args || {}).symbolize_keys
@ivar = :"@#{name}" # Optimization - used to avoid spamming #intern from internal_write_keys
@embeddable = nil
if abbr = @options[:abbr] || @options[:alias] || @options[:field_name]
@abbr = abbr.to_s
end

# We'll use this to reduce the number of operations #get has to perform, which improves load speeds
@is_id = @name == ID_STR
Expand All @@ -25,8 +28,12 @@ def initialize(*args)
end
end

def persisted_name
@abbr || @name
end

def ==(other)
@name == other.name && @type == other.type
@name == other.name && @type == other.type && @abbr == other.abbr
end

def embeddable?
Expand Down
2 changes: 1 addition & 1 deletion lib/mongo_mapper/plugins/rails.rb
Expand Up @@ -54,7 +54,7 @@ def has_many(*args, &extension)
end

def column_names
keys.keys
unaliased_keys.keys
end

# Returns returns an ActiveRecordAssociationAdapter for an association. This adapter has an API that is a
Expand Down
103 changes: 103 additions & 0 deletions spec/functional/keys_spec.rb
@@ -0,0 +1,103 @@
require 'spec_helper'

describe "Keys" do
describe "with aliases" do
AliasedKeyModel = Doc do
key :foo, :abbr => :f
key :with_underscores, :alias => "with-hyphens"
key :field_name, :field_name => "alternate_field_name"
key :bar
end

before { AliasedKeyModel.collection.drop }

context "standard key operations" do
before do
AliasedKeyModel.create(:foo => "whee!", :bar => "whoo!")
end

it "should serialize with aliased keys" do
AliasedKeyModel.collection.find_one.keys.should =~ %w(_id f bar)

AliasedKeyModel.first.tap do |d|
d.foo.should == "whee!"
d.bar.should == "whoo!"
end
end

it "should permit querying via aliases" do
AliasedKeyModel.where(AliasedKeyModel.abbr(:f) => "whee!").first.foo.should == "whee!"
end

it "should serialize to JSON with full keys" do
AliasedKeyModel.first.as_json.tap do |json|
json.should have_key "foo"
json.should_not have_key "f"
end
end
end

context "given field which are not valid Ruby method names" do
before { AliasedKeyModel.create(:with_underscores => "foobar") }
it "should work" do
AliasedKeyModel.first.with_underscores.should == "foobar"
AliasedKeyModel.collection.find_one["with-hyphens"].should == "foobar"
end
end

context "given a field aliased with :field_name" do
before { AliasedKeyModel.create(:field_name => "foobar") }
it "should work" do
AliasedKeyModel.first.field_name.should == "foobar"
AliasedKeyModel.collection.find_one["alternate_field_name"].should == "foobar"
end
end

context "associations" do
AssociatedKeyWithAlias = Doc do
set_collection_name "associated_documents"
key :name, String, :abbr => :n
key :association_id, ObjectId, :abbr => :aid
end

OwnerDocWithKeyAliases = Doc do
set_collection_name "owner_documents"
key :name, String, :abbr => :n
many :associated_documents, :class_name => "AssociatedKeyWithAlias", :foreign_key => AssociatedKeyWithAlias.abbr(:association_id)
many :other_documents, :class_name => "EmbeddedDocWithAliases"
end

EmbeddedDocWithAliases = EDoc do
key :embedded_name, String, :abbr => :en
end

before do
AssociatedKeyWithAlias.collection.drop
OwnerDocWithKeyAliases.collection.drop
end

it "should work" do
owner = OwnerDocWithKeyAliases.create(:name => "Big Boss")

associated_documents = 3.times.map {|i| AssociatedKeyWithAlias.new(:name => "Associated Record #{i}") }
owner.associated_documents = associated_documents
owner.save

owner.reload
owner.associated_documents.should =~ associated_documents

AssociatedKeyWithAlias.collection.find_one.keys.should =~ %w(_id n aid)
end

it "should work with embedded documents" do
owner = OwnerDocWithKeyAliases.create(:name => "Big Boss")
owner.other_documents << EmbeddedDocWithAliases.new(:embedded_name => "Underling")
owner.save

owner.reload
owner.other_documents[0].embedded_name.should == "Underling"
owner.collection.find_one["other_documents"][0]["en"].should == "Underling"
end
end
end
end

0 comments on commit f001de0

Please sign in to comment.