⚡ Do not repeat anymore your objects transformations.
Branch: master
Clone or download
emyann Merge pull request #49 from nobrainr/feat/update-deps
fix: update dev dependencies and remove no more used webpack dashboard
Latest commit 4ff239b Feb 11, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci feat: remove useless build step and make the most of node_modules cached Sep 20, 2018
.github chore: Upload PULL_REQUEST_TEMPLATE.MD Oct 8, 2017
.vscode feat: update format styles Apr 28, 2018
docs/images chore: Add full example image Sep 7, 2018
src fix: update package.json description Jan 20, 2019
tests/unit feat(Rewrite): First set of files to be migrated Jan 10, 2018
.editorconfig feat Style formatting Apr 28, 2018
.eslintrc.json feat(tests): coverage to 97% Sep 28, 2016
.gitattributes chore: first commit Sep 28, 2016
.gitignore chore: update gitignore Nov 14, 2018
CODE_OF_CONDUCT.md Create Code Of Conduct Nov 11, 2017
LICENSE chore: first commit Sep 28, 2016
README.md chore: Update readme.md Jan 20, 2019
_config.yml Set theme jekyll-theme-cayman Dec 6, 2017
karma.conf.js feat(Tests): Configure webpack coverage with istanbul and karma Jan 10, 2018
now.json feat: CI test Sep 7, 2018
package-lock.json fix: update dev dependencies and remove no more used webpack dashboard Feb 11, 2019
package.json fix: update dev dependencies and remove no more used webpack dashboard Feb 11, 2019
tsconfig.json chore: enable down level iteration in tsconfig Oct 3, 2018
tslint.json feat Style formatting Apr 28, 2018
typedoc.js chore: exclude helpers from generated documentation Sep 6, 2018
typings.json feat(Rewrite): First set of files to be migrated Jan 10, 2018
webpack.config.js fix: update dev dependencies and remove no more used webpack dashboard Feb 11, 2019

README.md

Morphism

npm npm bundle size (minified) npm Coveralls github CircleCI (all branches) Deps

In many fields of mathematics, morphism refers to a structure-preserving map from one mathematical structure to another. A morphism f with source X and target Y is written f : X → Y. Thus a morphism is represented by an arrow from its source to its target.

https://en.wikipedia.org/wiki/Morphism

  • ⚛️ Write your schema once, Transform your data everywhere
  • 0️⃣ Zero dependencies
  • 💪🏽 Typescript Support

Getting started

Installation

npm install --save morphism

Example (JavaScript)

import { morphism } from 'morphism';

// Source data coming from an API.
const source = {
  foo: 'baz',
  bar: ['bar', 'foo'],
  baz: {
    qux: 'bazqux'
  }
};

// Target Class in which to morph the source data. (Optional)
class Destination {
  foo = null;
  bar = null;
  bazqux = null;
}

// A structure-preserving object from a source data towards a target data.
const schema = {
  foo: 'bar[1]', // Grab the property value by his path
  bar: (iteratee, source, destination) => {
    // Apply a Function on the current element
    return iteratee.bar[0];
  },
  bazqux: {
    // Apply a function on property value
    path: 'baz.qux',
    fn: (propertyValue, source) => {
      return propertyValue;
    }
  }
};

const classObjects = morphism(schema, source, Destination);
// Destination {foo: "foo", bar: "bar", bazqux: "bazqux"}

const jsObjects = morphism(schema, source);
// Object {foo: "foo", bar: "bar", bazqux: "bazqux"}

Example (TypeScript)

import { morphism, StrictSchema } from 'morphism';

// What we have
interface Source {
  ugly_field: string;
}

// What we want
interface Destination {
  field: string;
}

const source: Source = {
  ugly_field: 'field value'
};

// Destination and Source types are optional
morphism<Destination, Source>({ field: 'ugly_field' }, source);
// => {field: "field value"}

// Or
const sources = [source];
const schema: StrictSchema<Destination, Source> = { field: 'ugly_field' };
morphism<Destination, Source>(schema, sources);
// => [{field: "field value"}]

▶️ Test with Repl.it

Motivation

We live in a era where we deal with mutiple data contracts coming from several sources (Rest API, Services, Raw JSON...). When it comes to transform multiple data contracts to match with your domain objects, it's common to create your objects with Object.assign, new Object(sourceProperty1, sourceProperty2) or by simply assigning each source properties to your destination. This can leads you to have your business logic spread all over the place.

Morphism allows you to keep this business logic centralized and brings you a top-down view of your data transformation. When a contract change occurs, it helps to track the bug since you just need to refer to your schema

Docs

📚 API documentation

Morphism comes with 3 artifacts to achieve your transformations:

1. The Schema

A schema is an object-preserving map from one data structure to another.

The keys of the schema match the desired destination structure. Each value corresponds to an Action applied by Morphism when iterating over the input data.

You can use 4 kind of values in your schema:

  • ActionString: A string that allows to perform a projection from a property
  • ActionSelector: An Object that allows to perform a function over a source property's value
  • ActionFunction: A Function that allows to perform a function over source property
  • ActionAggregator: An Array of Strings that allows to perform a function over source property

Schema Example

import { morphism } from 'morphism';

const input = {
  foo: {
    baz: 'value1'
  }
};

const schema = {
  bar: 'foo', // ActionString: Allows to perform a projection from a property
  qux: ['foo', 'foo.baz'], // ActionAggregator: Allows to aggregate multiple properties
  quux: (iteratee, source, destination) => {
    // ActionFunction: Allows to perform a function over source property
    return iteratee.foo;
  },
  corge: {
    // ActionSelector: Allows to perform a function over a source property's value
    path: 'foo.baz',
    fn: (propertyValue, source) => {
      return propertyValue;
    }
  }
};

morphism(schema, input);
// {
//   "bar": {
//     "baz": "value1"
//   },
//   "qux": {
//     "foo": {
//       "baz": "value1"
//     }
//   },
//   "quux": {
//     "baz": "value1"
//   },
//   "corge": "value1"
// }

▶️ Test with Repl.it

More Schema examples

📚 Schema Docs

1.1 Using a strict Schema

You might want to enforce the keys provided in your schema using Typescript. This is possible using a StrictSchema. Doing so will require to map every field of the Target type provided.

interface IFoo {
  foo: string;
  bar: number;
}
const schema: StrictSchema<IFoo> = { foo: 'qux', bar: () => 'test' };
const source = { qux: 'foo' };
const target = morphism(schema, source);
// {
//   "foo": "qux",
//   "bar": "test"
// }

2. Morphism as Currying Function

The simplest way to use morphism is to import the currying function:

import { morphism } from 'morphism';

morphism either outputs a mapping function or the transformed data depending on the usage:

API

morphism(schema: Schema, items?: any, type?: any): any

📚 Currying Function Docs

Currying Function Example

// Outputs a function when only a schema is provided
const fn = morphism(schema);
const result = fn(data);

// Outputs the transformed data when a schema and the source data are provided
const result = morphism(schema, data);

// Outputs the transformed data as an ES6 Class Object when a schema, the source data and an ES6 Class are provided
const result = morphism(schema, data, Foo);
// => Items in result are instance of Foo

3. Morphism as Function Decorators

You can also use Function Decorators on your method or functions to transform the return value using Morphism:

toJsObject Decorator

import { toJSObject } from 'morphism';

class Service {
  @toJSObject({
    foo: currentItem => currentItem.foo,
    baz: 'bar.baz'
  })
  async fetch() {
    const response = await fetch('https://api.com');
    return response.json();
    // =>
    // {
    //   foo: 'fooValue'
    //   bar: {
    //     baz: 'bazValue'
    //   }
    // };
  }
}

// await service.fetch() will return
// =>
// {
//   foo: 'fooValue',
//   baz: 'bazValue'
// }

--------------------------------

// Using Typescript will enforce the key from the target to be required
class Target {
  a: string = null;
  b: string = null;
}
class Service {
  // By Using <Target>, Mapping for Properties `a` and `b` will be required
  @toJSObject<Target>({
    a: currentItem => currentItem.foo,
    b: 'bar.baz'
  })
  fetch();
}

toClassObject Decorator

import { toClassObject } from 'morphism';

class Target {
  foo = null;
  bar = null;
}
const schema = {
  foo: currentItem => currentItem.foo,
  baz: 'bar.baz'
};
class Service {
  @toClassObject(schema, Target)
  async fetch() {
    const response = await fetch('https://api.com');
    return response.json();
    // =>
    // {
    //   foo: 'fooValue'
    //   bar: {
    //     baz: 'bazValue'
    //   }
    // };
  }
}

// await service.fetch() will be instanceof Target
// =>
// Target {
//   foo: 'fooValue',
//   baz: 'bazValue'
// }

- morph Decorator

Utility decorator wrapping toClassObject and toJSObject decorators

import { toClassObject } from 'morphism';

class Target {
  foo = null;
  bar = null;
}
const schema = {
  foo: currentItem => currentItem.foo,
  baz: 'bar.baz'
};
class Service {
  @morph(schema)
  async fetch() {
    const response = await fetch('https://api.com');
    return response.json();
    // =>
    // {
    //   foo: 'fooValue'
    //   bar: {
    //     baz: 'bazValue'
    //   }
    // };
  }
  @morph(schema, Target)
  async fetch2() {
    const response = await fetch('https://api.com');
    return response.json();
  }
}
// await service.fetch() will be
// =>
// {
//   foo: 'fooValue',
//   baz: 'bazValue'
// }

// await service.fetch() will be instanceof Target
// =>
// Target {
//   foo: 'fooValue',
//   baz: 'bazValue'
// }

4. Morphism as Mixin

Morphism comes along with an internal registry you can use to save your schema attached to a specific ES6 Class.

In order to use the registry, you might want to use the default export:

import Morphism from 'morphism';

All features available with the currying function are also available when using the mixin plus the internal registry:

// Currying Function
Morphism(schema: Schema, items?: any, type?: any): any

// Registry API
Morphism.register(type: any, schema?: Schema);
Morphism.map(type: any, data?: any);
Morphism.setMapper(type: any, schema: Schema);
Morphism.getMapper(type);
Morphism.deleteMapper(type);
Morphism.mappers

🔗 Registry API Documentation

More Schema examples

Flattening or Projection

import { morphism } from 'morphism';
// Source data coming from an API.
const source = {
  foo: 'baz',
  bar: ['bar', 'foo'],
  baz: {
    qux: 'bazqux'
  }
};
const schema = {
  foo: 'foo', // Simple Projection
  bazqux: 'baz.qux' // Grab a value from a deep path
};

morphism(schema, source);
//=> { foo: 'baz', bazqux: 'bazqux' }

▶️ Test with Repl.it

Function over a source property's value

import { morphism } from 'morphism';
// Source data coming from an API.
const source = {
  foo: {
    bar: 'bar'
  }
};
let schema = {
  barqux: {
    path: 'foo.bar',
    fn: value => `${value}qux` // Apply a function over the source property's value
  }
};

morphism(schema, source);
//=> { barqux: 'barqux' }

▶️ Test with Repl.it

Function over a source property

import { morphism } from 'morphism';
// Source data coming from an API.
const source = {
  foo: {
    bar: 'bar'
  }
};
let schema = {
  bar: iteratee => {
    // Apply a function over the source propery
    return iteratee.foo.bar;
  }
};

morphism(schema, source);
//=> { bar: 'bar' }

▶️ Test with Repl.it

Properties Aggregation

import { morphism } from 'morphism';
// Source data coming from an API.
const source = {
  foo: 'foo',
  bar: 'bar'
};
let schema = {
  fooAndBar: ['foo', 'bar'] // Grab these properties into fooAndBar
};

morphism(schema, source);
//=> { fooAndBar: { foo: 'foo', bar: 'bar' } }

▶️ Test with Repl.it

Registry API

📚 Registry API Documentation

Register

Register a mapper for a specific type. The schema is optional.

Morphism.register(type: any, schema?: Schema);

Map

Map a collection of objects to the specified type

Morphism.map(type: any, data?: any);

Get or Set an existing mapper configuration

Morphism.setMapper(type: any, schema: Schema);
Morphism.getMapper(type);

Delete a registered mapper

Morphism.deleteMapper(type);

List registered mappers

Morphism.mappers;

Contribution

Similar Projects

License

MIT © Yann Renaudin