Created
May 22, 2012 18:41
-
-
Save mbj/2770849 to your computer and use it in GitHub Desktop.
Experimental mapper, with much to verbose setup.
This file contains 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
# This is a mapper I created when I was in hurry/spiking using: | |
# github.com/mbj/mapper (a stupid try-to-be-most-generic mapper implementation, to be rewritten) | |
# github.com/mbj/session (a database session/IdentityMap that can possibly work with any (document) mapper) | |
# the Session thing is more or less stable, and I like it! | |
# This code is not used anymore, it was replaced by a non generic application specific mapper to elasticsearch. | |
# Also this code never went to production. | |
# The whole point of this literal mess was to transform a tree of objects into a document that could be stored within mongo. | |
# The virtus object tree should be reconstructed 1:1. | |
# This mapper uses Mapper::Reference quite often. This acts like a forcefully loaded belongs_to in dm-1. | |
# Mapper::Reference was an addition I made in this project. It will do an additional db roundtrip to load the referenced object (Not if key is present in IM of course). | |
# This is subjected to n+1 or ripple loading. | |
# For this and several other reasons I dropped this mapper. | |
# Maybe it has historic value :D and as an example for not well thought code. The exposed API was okay, but the mapping declarations below are just horrible. | |
# My current "non gerneric/application-specific mappers" work as a hand written mapping decorator for each "root object", looks more nicely and bets to be tested in depth. | |
# Usage: | |
# include MyApp | |
# # registred "regular" access with state tracking | |
# DB.with_session do | |
# product = Product.fetch(:permalink => "foo-bar") # will blow up if missing | |
# DB.track?(product) # => true | |
# DB.dirty?(product) # => false | |
# product.title = "Foo-Bar" | |
# DB.persist_now(product) # executes: collection['products'].update(:_id => Bson::ObjectId("some-id"), $set => { :title =>"Foo-Bar" }) | |
# DB.dirty?(product) # => true | |
# DB.untrack(product) # nice on bulk operations | |
# DB.track?(product) # false | |
# DB.dirty?(product) # raises exception (untracked object) | |
# end | |
# | |
# # unregistred "dirty" access without state tracking, used internally by Session. | |
# # Showing this to illustrate the ideas. | |
# mapper = MAPPER.for(Product) | |
# # query dumped representation (references not resolved) | |
# dump = mapper.first_dump(:permalink => "permalink") | |
# # dump looks like | |
# # { :_id => BSON::ObjectId("some-id"), :title => "some title", :properties => [{"color" => "Green","Controls" => "4-way"}] ... } | |
# # load dump into virtus backed product (references resolved with additional round trips), does not hit IM | |
# # The load translates :_id back to :id, and also instanciates properties from array of hashes. | |
# # Sure virtus would support this, but this mapper was meant for POROs! | |
# product = mapper.load(dump) | |
# product.title = "Foo-Bar" | |
# new_dump = mapper.dump(product) | |
# key = mapper.load_key(dump) | |
# # key looks like | |
# # { :_id => BSON::ObjectId("some-id") }, idea was to support CPKs out of the box. | |
# # In this app I'd overriden this to only dump do the BSON::ObjectId not a one element hash. | |
# dump == new_dump # => false (the dump has changed!) | |
# mapper.update(key,new_dump,dump) # updates the product in db with minimal update set. | |
# # When providing the old dump the mapper is able to generate the minimal update in the first level. | |
# # Advanced support for mongos operators, especially $push { array: [new_items] }, was not needed for this app. | |
# mapper.update(key,new_dump) # this would replace the whole document since there is no source for difference. | |
require 'mapper/virtus' | |
require 'mapper/mongo' | |
require 'session/registry' | |
module MyApp | |
module Mapper | |
MAPPER = ::Session::Registry.new | |
# Mapper should be loadable without triggering a mongodb connection | |
# As the mongo_db object on MyApp::DB connects on access this hack is needed. | |
class FakeCollection | |
def initialize(name) | |
@name = name | |
end | |
def method_missing(*args,&block) | |
MyApp::DB.mongo_db.collection(@name).send(*args,&block) | |
end | |
def self.for(name) | |
new(name) | |
end | |
end | |
# Hooking for some domain specific sanity checks. | |
class MyMongo < ::Mapper::Mapper::Mongo | |
def dump(object) | |
if object.class == MyApp::Product | |
unless object.valid_for_state? | |
raise Helpers.format_errors(object) | |
end | |
end | |
if object.respond_to?(:valid_for_state?) and !object.valid_for_state? | |
raise Helpers.format_errors(object) | |
end | |
if object.respond_to?(:valid?) and !object.valid? | |
raise Helpers.format_errors(object) | |
end | |
super(object) | |
end | |
end | |
PRODUCT_REFERENCE_MAPPER = Mapper::Reference.new( | |
:product, | |
:model => Product | |
) | |
SHIPMENT_METHOD_MAPPER = Mapper::ObjectReference.new( | |
:shipment_method, | |
:symbol_method => :type, | |
:collection => ShipmentMethod::ALL | |
) | |
PAYMENT_METHOD_MAPPER = Mapper::ObjectReference.new( | |
:payment_method, | |
:symbol_method => :type, | |
:collection => PaymentMethod::ALL | |
) | |
SHOP_MAPPER = Mapper::ObjectReference.new( | |
:shop, | |
:symbol_method => :code, | |
:collection => Shop::ALL | |
) | |
PRODUCT_SOURCE_MAPPER = Mapper::ObjectReference.new( | |
:product_source, | |
:symbol_method => :code, | |
:collection => ProductSource::ALL | |
) | |
VAT_RATE_MAPPER = Mapper::Rational.new(:vat_rate) | |
SHIPMENT_ADJUSTMENT_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Adjustment::Shipment, | |
[ | |
Mapper::SUnits.new(:amount_net), | |
VAT_RATE_MAPPER, | |
SHIPMENT_METHOD_MAPPER | |
] | |
) | |
CUSTOMER_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Customer, | |
[ | |
SHOP_MAPPER, | |
::Mapper::Mapper::Attribute.new(:salutation), | |
::Mapper::Mapper::Attribute.new(:firstname), | |
::Mapper::Mapper::Attribute.new(:lastname), | |
::Mapper::Mapper::Attribute.new(:company_name), | |
::Mapper::Mapper::Attribute.new(:email_address), | |
] | |
) | |
ADJUSTMENT_MAPPER = Object.new | |
class << ADJUSTMENT_MAPPER | |
def dump(object) | |
case object | |
when Adjustment::Shipment | |
SHIPMENT_ADJUSTMENT_MAPPER.dump(object).merge(:type => :shipment) | |
else | |
raise "unkown mapping for: #{object.class}" | |
end | |
end | |
def load(dump) | |
type = dump.delete(:type) | |
case type | |
when :shipment | |
SHIPMENT_ADJUSTMENT_MAPPER.load(dump) | |
else | |
raise "unknow mapping for: #{type.inspect}" | |
end | |
end | |
end | |
MAIL_LOG_MAPPER = ::Mapper::Mapper::Virtus.new( | |
MailLog, | |
[ | |
::Mapper::Mapper::Attribute.new(:raw), | |
::Mapper::Mapper::Attribute.new(:type), | |
::Mapper::Mapper::Attribute.new(:send_at) | |
] | |
) | |
ADDRESS_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Address, | |
[ | |
::Mapper::Mapper::Attribute.new(:lines), | |
::Mapper::Mapper::Attribute.new(:postcode), | |
::Mapper::Mapper::Attribute.new(:city_name), | |
Mapper::ObjectReference.new( | |
:state, | |
:symbol_method => :code, | |
:collection => State::ALL | |
), | |
Mapper::ObjectReference.new( | |
:country, | |
:symbol_method => :code, | |
:collection => Country::ALL | |
) | |
] | |
) | |
CART_LINE_ITEM_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Cart::LineItem, | |
[ | |
::Mapper::Mapper::Attribute.new(:quantity), | |
PRODUCT_REFERENCE_MAPPER, | |
SHOP_MAPPER, | |
] | |
) | |
ORDER_LINE_ITEM_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Order::LineItem, | |
[ | |
::Mapper::Mapper::Attribute.new(:quantity), | |
PRODUCT_REFERENCE_MAPPER, | |
Mapper::SUnits.new(:amount_net), | |
Mapper::SUnits.new(:product_price_net), | |
VAT_RATE_MAPPER, | |
] | |
) | |
PRODUCT_PROPERTY_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Product::Property, | |
[ | |
::Mapper::Mapper::Attribute.new(:name), | |
::Mapper::Mapper::Attribute.new(:value) | |
] | |
) | |
PRODUCT_IMAGE_MAPPER = ::Mapper::Mapper::Virtus.new( | |
Product::Image, | |
[ | |
::Mapper::Mapper::Attribute.new(:url) | |
] | |
) | |
MAPPER.register( | |
Taxon, | |
MyMongo.new( | |
:collection => FakeCollection.for(:taxons), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Taxon, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
Mapper::Reference.new(:parent,:model => Taxon), | |
Mapper::Rational.new(:pricing_factor), | |
::Mapper::Mapper::Attribute.new(:permalink), | |
::Mapper::Mapper::Attribute.new(:name) | |
] | |
) | |
) | |
) | |
MAPPER.register( | |
Product, | |
MyMongo.new( | |
:collection => FakeCollection.for(:products), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Product, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
::Mapper::Mapper::Attribute.new(:bulky_good), | |
::Mapper::Mapper::Attribute.new(:created_at), | |
::Mapper::Mapper::Attribute.new(:description), | |
::Mapper::Mapper::Attribute.new(:format), | |
::Mapper::Mapper::Attribute.new(:gtin), | |
::Mapper::Mapper::EmbeddedCollection.new(:images,:mapper => PRODUCT_IMAGE_MAPPER), | |
::Mapper::Mapper::Attribute.new(:keywords), | |
::Mapper::Mapper::Attribute.new(:manufacturer_product_id), | |
::Mapper::Mapper::Attribute.new(:meta_description), | |
::Mapper::Mapper::Attribute.new(:on_stock), | |
::Mapper::Mapper::Attribute.new(:name), | |
::Mapper::Mapper::Attribute.new(:permalink), | |
Mapper::Rational.new(:pricing_factor), | |
Mapper::SUnits.new(:base_list_price_net), | |
Mapper::SUnits.new(:base_purchase_price_net), | |
Mapper::Pricing.new(:pricing), | |
::Mapper::Mapper::EmbeddedCollection.new(:properties,:mapper => PRODUCT_PROPERTY_MAPPER), | |
Mapper::SUnits.new(:quantity_unit), | |
::Mapper::Mapper::Attribute.new(:source_product_id), | |
::Mapper::Mapper::Attribute.new(:state), | |
Mapper::Reference.new(:taxon,:model => Taxon), | |
::Mapper::Mapper::Attribute.new(:type_name), | |
::Mapper::Mapper::Attribute.new(:updated_at), | |
VAT_RATE_MAPPER, | |
Mapper::Reference.new(:import,:model => Import), | |
Mapper::Reference.new(:manufacturer,:model => Manufacturer), | |
PRODUCT_SOURCE_MAPPER, | |
] | |
) | |
) | |
) | |
MAPPER.register( | |
Manufacturer, | |
MyMongo.new( | |
:collection => FakeCollection.for(:manufacturers), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Manufacturer, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
::Mapper::Mapper::Attribute.new(:name), | |
::Mapper::Mapper::Attribute.new(:permalink), | |
::Mapper::Mapper::Attribute.new(:display_name), | |
::Mapper::Mapper::Attribute.new(:wikipedia), | |
::Mapper::Mapper::Attribute.new(:website), | |
::Mapper::Mapper::Attribute.new(:pricing_factor), | |
] | |
) | |
) | |
) | |
MAPPER.register( | |
Import, | |
MyMongo.new( | |
:collection => FakeCollection.for(:imports), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Import, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
::Mapper::Mapper::Attribute.new(:active), | |
::Mapper::Mapper::Attribute.new(:created_at), | |
::Mapper::Mapper::Attribute.new(:updated_at), | |
::Mapper::Mapper::Attribute.new(:active_count), | |
::Mapper::Mapper::Attribute.new(:error_count), | |
::Mapper::Mapper::Attribute.new(:incomplete_count), | |
::Mapper::Mapper::Attribute.new(:disabled_count), | |
::Mapper::Mapper::Attribute.new(:noop_count), | |
::Mapper::Mapper::Attribute.new(:start), | |
::Mapper::Mapper::Attribute.new(:estimation), | |
PRODUCT_SOURCE_MAPPER, | |
] | |
) | |
) | |
) | |
MAPPER.register( | |
Cart, | |
MyMongo.new( | |
:collection => FakeCollection.for(:carts), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Cart, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
SHOP_MAPPER, | |
::Mapper::Mapper::EmbeddedCollection.new(:line_items, :mapper => CART_LINE_ITEM_MAPPER), | |
::Mapper::Mapper::Attribute.new(:updated_at), | |
::Mapper::Mapper::Attribute.new(:created_at) | |
] | |
) | |
) | |
) | |
MAPPER.register( | |
Order, | |
MyMongo.new( | |
:collection => FakeCollection.for(:orders), | |
:mapper => ::Mapper::Mapper::Virtus.new( | |
Order, | |
[ | |
::Mapper::Mapper::Attribute.new(:id, :as => :_id, :key => true), | |
SHOP_MAPPER, | |
::Mapper::Mapper::EmbeddedDocument.new(:customer,:mapper => CUSTOMER_MAPPER), | |
::Mapper::Mapper::Attribute.new(:state), | |
::Mapper::Mapper::EmbeddedCollection.new(:mails,:mapper => MAIL_LOG_MAPPER), | |
PAYMENT_METHOD_MAPPER, | |
SHIPMENT_METHOD_MAPPER, | |
::Mapper::Mapper::EmbeddedDocument.new(:shipment_address, :mapper => ADDRESS_MAPPER), | |
::Mapper::Mapper::EmbeddedDocument.new(:invoice_address, :mapper => ADDRESS_MAPPER), | |
::Mapper::Mapper::EmbeddedCollection.new(:line_items, :mapper => ORDER_LINE_ITEM_MAPPER), | |
::Mapper::Mapper::EmbeddedCollection.new(:adjustments, :mapper => ADJUSTMENT_MAPPER), | |
::Mapper::Mapper::Attribute.new(:updated_at), | |
::Mapper::Mapper::Attribute.new(:created_at) | |
] | |
) | |
) | |
) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment