background image

AutoMapper in javascript

Just like the title says, I created a small javascript implementation of AutoMapper. Because I am cool like that I am calling it jsAutomapper. This is a very, very basic implementation and only has a few of the functions from the original framework but it is more than sufficient for my needs. I am working in Appcelerator and I implemented a MVC framework to clean up my code. Because of this I needed an easy way to convert between objects stored in the database and my view models. The benefit of using this implementation is it also forms a contract between your models and view models. The current implementation only supports basic object mappings. If anyone shows interest or I need it for my current project, I will implement mappings for complex objects.

The automapper javascript implementation:

var automapper = (function (app) {
	if(app.automapper) {
		return app.automapper;
	}
	
	var dictionary = {};
	
	app.automapper = {
		createMap : function (sourceKey, destinationKey) {
			var combinedKey = sourceKey + destinationKey, functions;
			dictionary[combinedKey] = {};
			
			functions = {
				forMember : function (key, e) {
					dictionary[combinedKey][key] = e;
					return functions;
				}, 
				forAllMembers : function(func) {
					dictionary[combinedKey].__forAllMembers = func;
					return functions;
				}
			};
			return functions;
		},
		map : function (sourceKey, destinationKey, sourceValue, destinationValue, lazy) {
			if(!sourceValue && sourceValue !== false) {
				return;
			}
						
			function getValue(item) {
				if(typeof item === "function" && !lazy) {
					return item();
				}
				return item;
			}
			var combinedKey = sourceKey + destinationKey;
			var mappings = dictionary[combinedKey], output, key,
				    extensions = {
						ignore : function () {
							// don't do anything
						},
						mapFrom : function (sourceMemberKey) {
							if (!this.__sourceValue.hasOwnProperty(sourceMemberKey)) {
								throw sourceKey + "." + sourceMemberKey + " has not been defined.";
							}
							var value = getValue(this.__sourceValue[sourceMemberKey]);
							if(mappings.__forAllMembers) {
								mappings.__forAllMembers(this.__destinationValue, this.__key, value);
							} else {
								this.__destinationValue[this.__key] = value;
							}
						}
					};
			
			if(!mappings) {
				throw "Could not find mapping with a source of " + sourceKey + " and a destination of " + destinationKey;
			}
			
			function mapItem(destinationValue, sourceValue) {
				for (key in destinationValue) {
					if (!destinationValue.hasOwnProperty(key)) {
						continue;
					}			
						
					if (mappings.hasOwnProperty(key) && mappings[key]) {	
						if (typeof mappings[key] === "function") {
							extensions.__key = key;
							extensions.__sourceValue = sourceValue;
							extensions.__destinationValue = destinationValue;
							
							output = mappings[key].call(extensions);
						} else {  // forMember second parameter was not a function
							output = mappings[key];
						}
						// object was returned from the 'forMember' call
						if (output) {
							var value = getValue(output);
							if(mappings.__forAllMembers) {
								mappings.__forAllMembers(destinationValue, key, value);
							} else {
								destinationValue[key] = value;
							}
						}
					}
					else if (!sourceValue.hasOwnProperty(key)) {
						throw sourceKey + "." + key + " has not been defined.";
					} else {
						var value = getValue(sourceValue[key]);
						if(mappings.__forAllMembers) {
							mappings.__forAllMembers(destinationValue, key, value);
						} else {
							destinationValue[key] = value;
						}
					}
				}
			}
			
			// actually do the mapping here
			if(sourceValue instanceof Array) {
				if(destinationValue instanceof Array) {
					// loop
					for(var i = 0; i < sourceValue.length; i += 1) {
						if(!destinationValue[i]) {
							if(typeof destinationKey !== "function") {
								throw "destinationKey of mapping must be a function in order to initialize the array";
							}
							destinationValue[i] = destinationKey();
						}
						mapItem(destinationValue[i], sourceValue[i]);
					}
				} else {
					throw "Cannot map array to object";
				}
			}
			else if(destinationValue instanceof Array) {
				throw "Cannot map object to array";
			} else {
				mapItem(destinationValue, sourceValue);
			}
		}
	};
	
	return app.automapper;
}(this));

Usage:

var atest = {
	same : 10,
	bleh : 4
};
var btest = {
	dumb: null,
	func: null,
	foo : null,
	bar : null,
	same : null
};
/* 
*  call 'createMap' where "a" and "b" are the name of your
*  models.  These values can be whatever you want but you 
*  must match the values when you call 'map'.
*/
automapper.createMap("a","b")
	   // forAllMembers isn't necessary here but it allows you to
	   // customize how you apply values to the destination model
	  .forAllMembers(function(model, prop, value) { 
		model[prop] = value; 
	  })
	  .forMember("dumb", 12)
	  .forMember("func", function() { return 8 })
	  .forMember("foo", function() { this.ignore(); })
	  .forMember("bar", function() { this.mapFrom("bleh"); });
	
automapper.map("a", "b", atest, btest);

Leave any suggestions or bug-fixes in the comments. Thanks.

UPDATE

Joel Brage was kind enough to port jsAutomapper to coffeescript. You can download it here



9 views shared on this article. Join in...

  1. Joel Brage says:

    Nice automapper. Converted it to CoffeeScript and wrapped it in a Dojo 1.7 AMD wrapper so it can be sent as dependency or required alone.


    define [], () ->
    dictionary = {}
    dojo.declare "Automapper", null,
    createMap: (sourceKey, destinationKey) ->
    combinedKey = sourceKey + destinationKey
    dictionary[combinedKey] = {}

    functions =
    forMember: (key, e) ->
    dictionary[combinedKey][key] = e
    functions
    forAllMembers: (func) ->
    dictionary[combinedKey].__forAllMembers = func
    functions
    functions

    map: (sourceKey, destinationKey, sourceValue, destinationValue, lazy) ->
    return null if not sourceValue && sourceValue isnt false

    getValue = (item) ->
    return item() if typeof item is "function" && not lazy
    return item

    combinedKey = sourceKey + destinationKey
    mappings = dictionary[combinedKey]
    extensions =
    ignore: () ->
    mapFrom: (sourceMemberKey) ->
    throw sourceKey + "." + sourceMemberKey + " has not been defined." if not @.__sourceValue.hasOwnProperty sourceMemberKey

    value = getValue @.__sourceValue[sourceMemberKey]
    if mappings.__forAllMembers
    mappings.__forAllMembers @.__destinationValue, @.__key, value
    else
    @.__destinationValue[@.__key] = value

    throw "Could not find mapping with a source of " + sourceKey + " and a destination of " + destinationKey if not mappings

    mapItem = (destinationValue, sourceValue) ->
    for own key of destinationValue
    if mappings.hasOwnProperty(key) && mappings[key]
    if typeof mappings[key] is "function"
    extensions.__key = key
    extensions.__sourceValue = sourceValue
    extensions.__destinationValue = destinationValue

    output = mappings[key].call extensions
    else # forMember second parameter was not a function
    output = mappings[key]
    # object was returned from the 'forMember' call
    if output
    value = getValue output
    if mappings.__forAllMembers
    mappings.__forAllMembers destinationValue, key, value
    else
    destinationValue[key] = value
    else
    throw sourceKey + "." + key + " has not been defined." if not sourceValue.hasOwnProperty key

    value = getValue sourceValue[key]
    if mappings.__forAllMembers
    mappings.__forAllMembers destinationValue, key, value
    else
    destinationValue[key] = value

    # actually do the mapping here
    if sourceValue instanceof Array
    throw "Cannot map array to object" if not destinationValue instanceof Array
    # loop
    for i in [0..sourceValue.length]
    if not destinationValue[i]?
    throw "destinationKey of mapping must be a function in order to initialize the array" if typeof destinationKey isnt "function"
    destinationValue[i] = destinationKey()
    mapItem destinationValue[i], sourceValue[i]
    else
    throw "Cannot map object to array" if destinationValue instanceof Array
    mapItem destinationValue, sourceValue

  2. Joel Brage says:

    And thats totally useless without indents 😉 well ask me for the code if you like it.

  3. Premier says:

    I think it could be better if it will ignore unmapped property from destination to source

  4. Sugendran says:

    Any chance you could make that into an NPM module for node.js? Or if you don’t mind I can turn it into a module.

  5. Great idea. I’ve been wanting something like this, for mapping my view-models to database objects in NodeJS.

    Ended up using underscorejs:

    var objectA = { ‘name’: ‘Jonathan’, ‘city’: ‘Sydney’ };
    var objectB = { ‘name’: ‘Jonathan Conway’, ‘city’: ‘Sydney’, ‘errors’: [] }
    _.extend(objectA, _.pick(objectB, _.keys(objectA)));
    => { ‘name’: ‘Jonathan Conway’, ‘city’: ‘Sydney’ };

    (See: http://stackoverflow.com/questions/18140902/underscore-js-object-object-mapper/18141114?noredirect=1#18141114)

  6. Paulo Segundo says:

    Great Job, would be nice to have this in github and as a npm module.

  7. Idrees Haddad says:

    Hello John,

    I’ve added the mapper under github on this repository: https://github.com/idreeshaddad/jsAutomapper
    I’ve credited you for it and put a link to this post.

    Thank you :)

  8. OzBob says:

    I’ve created a Plunk, as a play area for jsAutoMapper:

    http://plnkr.co/edit/NXGPs4yScPOxK6RyKkbF

Leave a Reply

Your email address will not be published. Required fields are marked *

Comment

You may use these tags : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>