Skip to content

KazanExpress/parakeet-mapper

Repository files navigation

Parakeet Mapper

npm npm bundle size (minified) dependencies (minified)

Simple data conversion library


npm install --save parakeet-mapper

For more options see installation


What is this?

It's a small collection of utility functions and types that help with mapping (transforming) data.

You can find it useful if:

  • You work with complex data objects and tired of it
  • You need to quickly and efficiently transform objects or tuples regularly
  • Your data models need additional functionality
  • You need a type-safe way of initializing a class from an object
  • You need to convert data types back and forth multiple times
  • You don't like your backend's API and GraphQL is not an option 😁

Installation

Install as dependency

npm install --save parakeet-mapper
# or
yarn add parakeet-mapper

Import and use

ES

import { mapTypes, mapFactory } from 'parakeet-mapper'

CommonJS

const { mapTypes, mapFactory } = require('parakeet-mapper');

Script tag

<!-- Modern es-modules: -->
<script>
  import { mapTypes, mapFactory } from 'https://unpkg.com/parakeet-mapper';
</script>

<!-- Or a legacy IIFE script: -->
<script src="https://unpkg.com/parakeet-mapper/dist/parakeet-mapper.iife.js"></script>
<script>
  // global variable parakeetMapper
  const { mapTypes, mapFactory } = parakeetMapper;
</script>

API

parakeet-mapper exposes several helpers to deal with type conversions.
All of them use the TypeMap interface to communicate.

name overloads description
mapFactory 3 Accepts a TypeMap. Returns a function that accepts one input and returns an output converted using rules defined in the TypeMap.
mapTypes 3 Same as mapFactory, but instead of returning a function, accepts input as its first argument and returns an output right away.
Convertable 2 Class mixin. Allows creation of convertable classes.

TypeMap

object

It is a set of rules that define how the output type is made from the input type.\

The rules are simple:\

  • Each key corresponds to a key in the output.
  • Each value tells what to assign to that key from the input.
    • true = the value is assigned from the same key in the input.
    • A string = the value is assigned from this string key in the input.
    • An object = the value is assigned from this object's first key in the input and is processed using the value as converter.
    • An array of a single element = the first element in the array is used as converter for the input value by the output key.
    • A function = the value is mapped using this function from the input.

Examples

const input = {
  transferred: 'foo',
  renamed: 'bar',
  converted: '42',
  mapped: [1, 1, 2],
  omitted: 'won\'t transfer to output'
};

const TypeMap = {
  // Transferred straight to the output
  transferred: true,

  // Renamed from `renamed` into `outputRenamed`
  outputRenamed: 'renamed',

  // Renamed from `converted` into `convertedNumber`
  // and converted from string to number
  convertedNumber: { converted: Number },

  // Mapped using a function and also renamed.
  mappedSum: input => input.mapped.reduce((a, b) => a + b),

  // Output object is also accessible in the function as a second parameter
  // This allows to reuse operations for already converted values
  // (like mappedSum and convertedNumber, in this example)
  mappedPlusConverted: (_input, output) => output.mappedSum + output.convertedNumber
};

/* output */ {
  transferred: 'foo',
  outputRenamed: 'bar',
  convertedNumber: 42, // Number('42')
  mappedSum: 4,
  mappedPlusConverted: 46 // 4 + 42
  // notice the absence of `ommited` property
}

object/array shorthand with the same key

new in v2.1.2

It's not necessary to specify the correct key in the conversion object:

const input = {
  number: '42',
};

const TypeMap = {
  // Simply converts from string to number using the `Number` function
  number: { Number },
};

/* output */ {
  number: 42, // Number('42')
  // Notice that the property name stayed the same,
  // even though we used a `{ Number: Number }` shorthand.
}

It only works if the input has the same property key as the output.

Can also be written using the array (tuple) syntax:

const input = {
  number: '42',
};

const TypeMap = {
  // Simply converts from string to number using the `Number` function
  number: [Number],
};

/* output */ {
  number: 42, // Number('42')
  // Notice that the property name stayed the same,
  // even though we used a `{ Number: Number }` shorthand.
}

mapFactory

function

A factory function that produces a converter from a TypeMap:

import { mapFactory } from 'parakeet-mapper';

const inputToOutput = mapFactory(TypeMap);

const output = inputToOutput(input); /* {
  transferred: 'foo',
  outputRenamed: 'bar',
  convertedNumber: 42, // Number('42')
  mappedSum: 4,
  mappedPlusConverted: 46 // 4 + 42
  // notice the absence of `ommited` property
} */

Overloads

This function has 3 overloads, all of which are needed for type safety and type inference (TypeScript).

There are basically 3 typed use-cases of using this function (hence 3 overloads):

  1. Input and output types are known, and TypeMap needs to convert them precisely.
    declare const input: InputType;
    const inputConverter = mapFactory<InputType, OutputType>(TypeMap);
    // output is OutputType now
  2. Input type is known, output needs to be inferred from the TypeMap.
    The first call without arguments is a noop. Reasons: first, second.
    declare const input: InputType; // The empty braces are here due to TS issues.
    const inputConverter = mapFactory<InputType>()(TypeMap);
  3. Input and output types are known, but the output type needs to be modified slightly.
    The first call without arguments is a noop. Reasons: first, second.
    declare const input: InputType; // The empty braces are here due to TS issues.
    const inputConverter = mapFactory<InputType, OutputType>()(TypeMap);

mapTypes

function

Basically, a mapFactory, called in-place.

First Argument Second Argument
The input object Corresponding TypeMap
import { mapTypes } from 'parakeet-mapper';

const output = mapTypes(input, TypeMap); /* {
  transferred: 'foo',
  outputRenamed: 'bar',
  convertedNumber: 42, // Number('42')
  mappedSum: 4,
  mappedPlusConverted: 46 // 4 + 42
  // notice the absence of `ommited` property
} */

// Same as
// const output = mapFactory(TypeMap)(input);

Overloads

There are 3. All are semantically the same as the overloads of mapFactory, including the noop call:

/* 1 */ mapTypes<InputType, OutputType>(input, TypeMap);
/* 2 */ mapTypes<InputType>()(input, TypeMap);
/* 3 */ mapTypes<InputType, OutputType>()(input, TypeMap);

Wait

function
new in v2.1

A complementary function to mapFactory that helps to flatten out promises

It produces a converter that returns a flat promise from a converter that returns an object with promises.

In a typical situation, when some convertations are asyncronous, you'd end up with this:

import { mapTypes } from 'parakeet-mapper';

// Imagine that this is requesting something from an API and returns a promise
const requestFromApi = (value) => Promise.resolve(value);

const input = {
  a: ['a'],
  b: 42,
  c: 'c'
};

const getAandBfromAPI = mapFactory({
  b: true,
  a: { a: requestFromApi },
  c: { c: requestFromApi }
});

const output = getAandBfromAPI(input);
// Result:
/* {
  a: Promise<['a']>,
  b: 42,
  c: Promise<'b'>
} */
// Not very comfortable to await every single value after this

Now with wait:

import { wait } from 'parakeet-mapper';

const waitForAandB = wait(getAandBfromAPI);


const output = getAandBfromAPI(input);
// Result:
/* Promise<{
  a: ['a'],
  b: 42,
  c: 'b'
}> */
// Much more useful now

flattenPromises

function
new in v2.1

Internally used in wait, flattens top-level promises in an object:

import { flattenPromises } from 'parakeet-mapper';

const objWithPromises = {
  a: [Promise.resolve('a')],
  b: 42,
  c: Promise.resolve('b')
};

const flat = flattenPromises(objWithPromises);
// Result
/* Promise<{
  a: ['a'],
  b: 42,
  c: 'b'
}> */

Convertable

function
new in v2.0

Allows to create classes from converters.
This makes possible adding extra functionality, including reverse convertations.

import { Convertable, mapFactory } from 'parakeet-mapper';

const inputConverter = () => mapFactory(mapTypes);

class Output extends Convertable(inputConverter) {}

const output = new Output(input);

console.log(output); /*
> Output {
    transferred: 'foo',
    outputRenamed: 'bar',
    convertedNumber: 42,
    mappedSum: 4,
    mappedPlusConverted: 46
  }
*/

Accepts a function that returns a converter as its only argument.
Returns a Convertable class with all the required functionality.

Can also infer arguments from its converter factory:

import { Convertable, mapFactory } from 'parakeet-mapper';

const input: InputType = {
  foo: 'foo',
  bar: 'bar'
}

const inputConverter = (convertFoo?: boolean) => mapFactory<InputType>()({
  zoo: convertFoo,
  zar: convertFoo ? 'bar' : 'foo'
});

class Output extends Convertable(inputConverter) {}

const outputWithFoo = new Output(input, /* convertFoo? */ true);
console.log(outputWithFoo); /*
> Output {
    zoo: 'foo',
    zar: 'bar'
  }
*/

const outputWithoutFoo = new Output(input, /* convertFoo? */ false);
console.log(outputWithoutFoo); /*
> Output {
    zar: 'foo'
  }
*/

And accept a reverse converter:

import { Convertable, mapFactory } from 'parakeet-mapper';

const input: InputType = {
  foo: 'foo',
  bar: 'bar'
}

const inputConverter = (convertFoo?: boolean) => mapFactory<InputType>()({
  zoo: convertFoo,
  zar: convertFoo ? 'bar' : 'foo'
});

const outputConverter = (convertZoo?: boolean) => mapFactory<OutputType>()({
  foo: convertZoo,
  bar: convertZoo ? 'zar' : 'zoo'
});

class Output extends Convertable(inputConverter, outputConverter) {}

const output = new Output(input, /* convertFoo? */ true);
console.log(output); /*
> Output {
    zoo: 'foo',
    zar: 'bar'
  }
*/

// Convert back to input type
const newInput = output.toInput(/* convertZoo? */ true);
console.log(newInput); /*
> {
    foo: 'foo',
    bar: 'bar'
  }
*/

Convertable class

constructor

Accepts an input as its first argument and converter factory parameters as other spread arguments.

toInput

Available only if reverse converter was passed into the Convertable.

Accepts a spread of reverse converter arguments

Convertable.createConverter

static

A converter factory, passed to the Convertable.

Convertable.reverseConverter

static

A reverse converter factory, passed to the Convertable.