Skip to content

Instantly share code, notes, and snippets.

@mbj
Created May 22, 2012 18:41
Show Gist options
  • Save mbj/2770849 to your computer and use it in GitHub Desktop.
Save mbj/2770849 to your computer and use it in GitHub Desktop.
Experimental mapper, with much to verbose setup.
# 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