Skip to content

Latest commit

 

History

History
722 lines (563 loc) · 11.5 KB

File metadata and controls

722 lines (563 loc) · 11.5 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": "[email protected]",
    "plan": "premium",
    "logins": 5,
    "address": {
      "street": "6th St",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94103",
      "country": "USA"
    }
  }
}


Output:

{
  "action": "create",
  "userId": "[email protected]",
  "userProperties": {
    "name": "Peter Gibbons",
    "email": "[email protected]",
    "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.

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.
  • "blank": If the given value is 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": {
    "blank": { "@path": "$.c" },
    "then": "yep",
    "else": "nope"
  }
}
=>
"yep"

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"

@merge

The @merge directive resolves a list of objects to a single object. It accepts a list of one or more objects (either raw objects or directives that resolve to objects), and a direction that determines how overwrites will be applied for duplicate keys. The resolved object is built by combining each object in turn, moving in the specified direction, overwriting any duplicate keys.

Input:

{
  "traits": {
    "name": "Mr. Rogers",
    "greeting": "Neighbor",
    "neighborhood": "Latrobe"

  },
  "properties": {
    "neighborhood": "Make Believe"
  }
}

Mappings:

{
  "@merge": {
    "objects": [
      { "@path": "traits" },
      { "@path": "properties" }
    ],
    "direction": "right"
  }
}
=>
{
  "name": "Mr. Rogers",
  "greeting": "Neighbor",
  "neighborhood": "Make Believe"
}

{
  "@merge": {
    "objects": [
      { "@path": "properties" },
      { "@path": "traits" }
    ],
    "direction": "right"
  }
}
=>
{
  "name": "Mr. Rogers",
  "greeting": "Neighbor",
  "neighborhood": "Latrobe"
}

The @merge directive is especially useful for providing default values:

Input:

{
  "traits": {
    "name": "Mr. Rogers"
  }
}

Mapping:

{
  "@merge": {
    "objects": [
      {
        "name": "Missing name",
        "neighborhood": "Missing neighborhood"
      },
      { "@path": "traits" }
    ],
    "direction": "right"
  }
}

Output:

{
  "name": "Mr. Rogers",
  "neighborhood": "Missing neighborhood"
}

@transform

The @transform directive allows you to operate on the result of a mapping-kit transformation. It accepts an apply parameter, which is the mapping to apply to the original payload, and a mapping parameter, which will be run on the resulting payload. The @transform directive is useful when you need to run mappings in sequence.

Input:

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

Mappings:
{
  "@transform": {
    "apply": {
      "foo": {
        "@path": "$.a"
      }
    },
    "mapping": {
      "newValue": { "@path": "$.foo" }
    }
  }
}
=>
{
  "newValue": 1
}

@excludeWhenNull

The @excludeWhenNull directive will exclude the field from the output if the resolved value is null.

Input:

{
  "a": null,
  "b": "hello"
}

Mappings:

{
  "a": {
    "@excludeWhenNull": {
      "@path": "$.a"
    }
  },
  "b": {
    "@excludeWhenNull": {
      "@path": "$.b"
    }
  }
}
=>
{
  "b": "hello"
}