Skip to content

Latest commit

 

History

History
629 lines (482 loc) · 9.49 KB

File metadata and controls

629 lines (482 loc) · 9.49 KB

Mapping Kit

Mapping Kit is a library for mapping and transforming JSON payloads. It exposes a function that accepts a mapping configuration object and a payload object and outputs a mapped and transformed payload. A mapping configuration is a mixture of raw values (values that appear in the output payload as they appear in the mapping configuration) and directives, which can fetch and transform data from the input payload.

For example:

Mapping:

{
  "name": "Mr. Rogers",
  "neighborhood": { "@path": "$.properties.neighborhood" },
  "greeting": { "@template": "Won't you be my {{properties.noun}}?" }
}

Input:

{
  "type": "track",
  "event": "Sweater On",
  "context": {
    "library": {
      "name": "analytics.js",
      "version": "2.11.1"
    }
  },
  "properties": {
    "neighborhood": "Latrobe",
    "noun": "neighbor",
    "sweaterColor": "red"
  }
}

Output:

{
  "name": "Mr. Rogers",
  "neighborhood": "Latrobe",
  "greeting": "Won't you be my neighbor?"
}

Table of contents

Usage

import { transform } from '../mapping-kit'

const mapping = { '@path': '$.foo.bar' }
const input = { foo: { bar: 'Hello!' } }

const output = transform(mapping, input)
// => "Hello!"

Terms

In Mapping Kit, there are only two kinds of values: raw values and directives. Raw values can be any JSON value and Mapping Kit will return them in the output payload untouched:

42

"Hello, world!"

{ "foo": "bar" }

["product123", "product456"]

Directives are objects with a single @-prefixed key that tell Mapping Kit to fetch data from the input payload or transform some data:

{ "@path": "$.properties.name" }

{ "@template": "Hello there, {{traits.name}}" }

In this document, the act of converting a directive to its final raw value is called "resolving" the directive.

Mixing raw values and directives

Directives and raw values can be mixed to create complex mappings. For example:

Mapping:

{
  "action": "create",
  "userId": {
    "@path": "$.traits.email"
  },
  "userProperties": {
    "@path": "$.traits"
  }
}

Input:

{
  "traits": {
    "name": "Peter Gibbons",
    "email": "peter@example.com",
    "plan": "premium",
    "logins": 5,
    "address": {
      "street": "6th St",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94103",
      "country": "USA"
    }
  }
}


Output:

{
  "action": "create",
  "userId": "peter@example.com",
  "userProperties": {
    "name": "Peter Gibbons",
    "email": "peter@example.com",
    "plan": "premium",
    "logins": 5,
    "address": {
      "street": "6th St",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94103",
      "country": "USA"
    }
  }
}

A directive may not, however, be mixed in at the same level as a raw value:

Invalid:

{
  "foo": "bar",
  "@path": "$.properties.biz"
}

Valid:

{
  "foo": "bar",
  "baz": { "@path": "$.properties.biz" }
}

And a directive may only have one @-prefixed directive in it:

Invalid:

{
  "@path": "$.foo.bar",
  "@template": "{{biz.baz}"
}

Valid:

{
  "foo": { "@path": "$.foo.bar" },
  "baz": {
    "@template": "{{biz.baz}}"
  }
}

Validation

Mapping configurations can be validated using JSON Schema. The test suite is a good source-of-truth for current implementation behavior.

Options

Options can be passed to the transform() function as the third parameter:

const output = transform(mapping, input, options)

Available options:

{
  merge: true // default false
}

merge

If true, merge will cause the mapped value to be merged onto the input payload. This is useful when you only want to map/transform a small number of fields:

Input:

{
  "a": {
    "b": 1
  },
  "c": 2
}

Options:

{
  "merge": true
}

Mappings:

{}
=>
{
  "a": {
    "b": 1
  },
  "c": 2
}

{
  "a": 3
}
=>
{
  "a": 3,
  "c": 2
}

{
  "a": {
    "c": 3
  }
}
=>
{
  "a": {
    "b": 1,
    "c": 3
  },
  "c": 2
}

Removing values from object

undefined values in objects are removed from the mapped output while null is not:

Input:

{
  "a": 1
}

Mappings:

{
  "foo": {
    "@path": "$.a"
  },
  "bar": {
    "@path": "$.b"
  },
  "baz": null
}
=>
{
  "foo": 1,
  "baz": null
}

Directives

@if

The @if directive resolves to different values based on a given conditional. It must have at least one conditional (see below) and one branch ("then" or "else").

The supported conditional values are:

  • "exists": If the given value is not undefined or null, the @if directive resolves to the "then" value. Otherwise, the "else" value is used.
Input:

{
  "a": "cool",
  "b": true
}

Mappings:

{
  "@if": {
    "exists": { "@path": "$.a" },
    "then": "yep",
    "else": "nope"
  }
}
=>
"yep"

{
  "@if": {
    "exists": { "@path": "$.nope" },
    "then": "yep",
    "else": "nope"
  }
}
=>
"nope"

If "then" or "else" are not defined and the conditional indicates that their value should be used, the field will not appear in the resolved output. This is useful for including a field only if it (or some other field) exists:

Input:

{
  "a": "cool"
}

Mappings:

{
  "foo-exists": {
    "@if": {
      "exists": { "@path": "$.foo" },
      "then": true
    }
  }
}
=>
{}

{
  "a": {
    "@if": {
      "exists": { "@path": "$.oops" },
      "then": { "@path": "$.a" }
    }
  }
}
=>
{}

@path

The @path directive resolves to the value at the given path. @path supports basic dot notation. Like JSONPath, you can include or omit the leading $.

Input:

{
  "foo": {
    "bar": 42,
    "baz": [{ "num": 1 }, { "num": 2 }]
  },
  "hello": "world"
}

Mappings:

{ "@path": "$.hello" } => "world"

{ "@path": "$.foo.bar" } => 42

{ "@path": "$.foo.baz[0].num" } => 1

@template

The @template directive resolves to a string replacing curly brace {{}} placeholders.

Input:

{
  "traits": {
    "name": "Mr. Rogers"
  },
  "userId": "abc123"
}

Mappings:

{ "@template": "Hello, {{traits.name}}!" } => "Hello, Mr. Rogers!"

{ "@template": "Hello, {{traits.fullName}}!" } => "Hello, !"

{ "@template": "{{traits.name}} ({{userId}})" } => "Mr.Rogers (abc123)"

@literal

The @literal directive resolves to the value with no modification. This is needed primarily to work around literal values being interpreted incorrectly as invalid templates.

Input:
n/a

Mappings:
{ "@literal": true } => true

@arrayPath

The @arrayPath directive resolves a value at a given path (much like @path), but allows you to specify the shape of each item in the resulting array. You can use directives for each key in the given shape, relative to the root object.

Typically, the root object is expected to be an array, which will be iterated to produce the resulting array from the specified item shape. It is not required that the root object be an array.

For the item shape to be respected, the root object must be either an array of plain objects OR a singular plain object. If the root object is a singular plain object, it will be arrified into an array of 1.

Input:

{
  "properties": {
    "products": [{ "productId": 1 }, { "productId": 2 }]
  }
}

Mapping:

{
  "@arrayPath": ["$.properties.products"]
}

Result:

[
  {
    "productId": 1
  },
  {
    "productId": 2
  }
]

Mappings with item shape:

{
  "@arrayPath": [
    "$.properties.products",
    {
      "some_other_key": { "@path": "$.productId" }
    }
  ]
}

Result:

[
  {
    "some_other_key": 1
  },
  {
    "some_other_key": 2
  }
]

@case

The @case directive changes a string value at a given path to its respective lowercase() or uppercase() representation.

While this directive does expect a string value at the given path, it can handle other types and will simply resolve to whatever is found if it is not a string.

Input:

{
  "properties": {
    "message": "THIS STRING IS IN ALL CAPS"
  }
}

Mapping:

{
  "@case": {
    "operator": "lower",
    "value": { "@path": "$.properties.message" }
  }
}

Result:

"this is a string in all caps"

@replace

The @replace directive replaces to the given pattern value with a replacement string. Both "pattern" and "replacement" fields are required but replacement can be an empty string.

Input:

{
  "a": "cool-story",
}

Mappings:

{
  "@replace": {
    "pattern": "-",
    "replacement": ""
  }
}
=>
"coolstory"

```json
Input:

{
  "a": "cool-story",
}

Mappings:

{
  "@replace": {
    "pattern": "-",
    "replacement": "nice"
  }
}
=>
"coolnicestory"
Input:

{
  "a": "cWWl-story-ww",
}

Mappings:

{
  "@replace": {
    "pattern": "WW",
    "replacement": "oo",
    "ignorecase": false
  }
}
=>
"cool-story-ww"
Input:

{
  "a": "just-the-first",
}

Mappings:

{
  "@replace": {
    "pattern": "-",
    "replacement": "@",
    "global": false
  }
}
=>
"just@the-first"