Skip to content

Instantly share code, notes, and snippets.

@jlyman
Created January 20, 2018 04:56
Show Gist options
  • Save jlyman/381eb9569de74abf107c80460e2cf84c to your computer and use it in GitHub Desktop.
Save jlyman/381eb9569de74abf107c80460e2cf84c to your computer and use it in GitHub Desktop.
Javascript ES6 API to model mapper functions
import assign from 'lodash/assign';
/**
* Converts one type of object to another, in this case to/from our Models to
* plain API response objects.
* @param {object} sourceObj The source object, either a Model or a plain API response object
* @param {array} mapping An array of arrays, the inner arrays containing key mappings between objects.
* Inner array format can be either ['modelPropertyKey', ['apiPropertyKey'] or
* ['modelPropertyKey', apiToModelTransformer(), modelToApiTransformer()]
* @param {bool} isMappingModelToApi True if we are taking a Model object an
* mapping to an API response object, otherwise false.
*/
function mappingReducer(sourceObj, mapping, isMappingModelToApi) {
const sourceMapIndex = isMappingModelToApi ? 0 : 1;
const targetMapIndex = isMappingModelToApi ? 1 : 0;
const lambdaMapIndex = isMappingModelToApi ? 2 : 1;
// Iterate through each element of the `mapping` object,
// and map the source property to the target property.
return mapping.reduce((targetObj, mapEl) => {
if (mapEl.length === 3) {
// We are using mapping functions to generate the result.
// Process and Object.assign the result.
if (mapEl[lambdaMapIndex] !== null) {
const result = mapEl[lambdaMapIndex](sourceObj);
assign(targetObj, result);
}
} else {
// Just a simple straight mapping conversion.
targetObj[mapEl[targetMapIndex]] = sourceObj[mapEl[sourceMapIndex]];
}
return targetObj;
}, {});
}
/**
* Converts a Model to an API response object
* @param {object} model The model to convert to a POJO
* @param {array} modelMap An array of arrays, the inner arrays containing key mappings between objects.
* Inner array format can be either ['modelPropertyKey', ['apiPropertyKey'] or
* ['modelPropertyKey', apiToModelTransformer(), modelToApiTransformer()]
*/
export function mapModelToApi(model, modelMap) {
return mappingReducer(model, modelMap, true);
}
/**
* Converts a plain API response object to a Model.
* @param {object} apiObject The API response to convert to a Model
* @param {array} modelMap An array of arrays, the inner arrays containing key mappings between objects.
* Inner array format can be either ['modelPropertyKey', ['apiPropertyKey'] or
* ['modelPropertyKey', apiToModelTransformer(), modelToApiTransformer()]
* @param {Prototype} modelPrototype The type of model we are creating (e.g., Referral)
*/
export function mapApiToModel(apiObject, modelMap, modelPrototype) {
const data = mappingReducer(apiObject, modelMap, false);
return new modelPrototype(data);
}
import expect from 'expect';
import { mapModelToApi, mapApiToModel } from './modelMapper';
function TestModel(data) {
this.testElement = data ? data.testElement : 'WRONG1';
this.nestedElement = data ? data.nestedElement : 'WRONG2';
}
const exampleTestModel = new TestModel({
testElement: 'GOOD1',
nestedElement: {
nestedElement1: 'GOOD2',
nestedElement2: 'GOOD3',
},
});
const apiResponse = {
test_element: 'GOOD1',
nested_element_1: 'GOOD2',
nested_element_2: 'GOOD3',
};
const mapping = [
['testElement', 'test_element'],
[
'nestedElement',
apiObj => ({
nestedElement: {
nestedElement1: apiObj.nested_element_1,
nestedElement2: apiObj.nested_element_2,
},
}),
modelObj => ({
nested_element_1: modelObj.nestedElement.nestedElement1,
nested_element_2: modelObj.nestedElement.nestedElement2,
}),
],
];
describe('Model mapper functions', () => {
describe('Model --> API response', () => {
it('should map basic elements directly', () => {
const result = mapModelToApi(exampleTestModel, mapping);
expect(result).toHaveProperty('test_element');
expect(result.test_element).toBe(exampleTestModel.testElement);
});
it('should map nested elements correctly', () => {
const result = mapModelToApi(exampleTestModel, mapping);
expect(result).toHaveProperty('nested_element_1');
expect(result).toHaveProperty('nested_element_2');
expect(result.nested_element_1).toBe(
exampleTestModel.nestedElement.nestedElement1
);
expect(result.nested_element_2).toBe(
exampleTestModel.nestedElement.nestedElement2
);
});
});
describe('API Response --> Model', () => {
it('should map basic elements directly', () => {
const result = mapApiToModel(apiResponse, mapping, TestModel);
expect(result).toHaveProperty('testElement');
expect(result.testElement).toBe(apiResponse.test_element);
});
it('should map flat properties to nested elements', () => {
const result = mapApiToModel(apiResponse, mapping, TestModel);
expect(result).toHaveProperty('nestedElement');
expect(result.nestedElement).toHaveProperty('nestedElement1');
expect(result.nestedElement).toHaveProperty('nestedElement2');
expect(result.nestedElement.nestedElement1).toBe(
apiResponse.nested_element_1
);
expect(result.nestedElement.nestedElement2).toBe(
apiResponse.nested_element_2
);
});
it('should return an object of the correct prototype', () => {
const result = mapApiToModel(apiResponse, mapping, TestModel);
expect(result).toBeInstanceOf(TestModel);
});
});
});
@jlyman
Copy link
Author

jlyman commented Jan 20, 2018

I found myself needing to consistently translate roughly equivalent models from and to API responses into Javascript, and needed a way to do so reliably and repeatedly. These small utility functions are the result.

@pedrogquirino
Copy link

Great job, man. ;)

@jlyman
Copy link
Author

jlyman commented Jun 22, 2020

Thanks @pedrogquirino, much appreciated. There's a little more detail over on the repo page too, if you're interested: https://github.com/jlyman/simple-model-mapper

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment