Mapping Examples
Each example on this page is a complete Portable Data Module (PDM). Each example shows sample input and the output that Precept returns. For the syntax, see Mapping Engine and PDM Language.
The tabs under each example show the data that Precept reads (Input) and the data that Precept returns (Output). The model name in each tab title is the model of that data.
We ran each example on this page through the Precept mapping engine.
Rename fields
Use <-> to copy a field to a field with a different name. The rule works in both directions.
vendor acme
name rename
model Contact {
first_name: string
last_name: string
date_of_birth: string
}
model Person {
firstName: string
lastName: string
dob: string
}
mapping Contact <-> Person {
first_name <-> firstName
last_name <-> lastName
date_of_birth <-> dob
}{ "first_name": "Jane", "last_name": "Smith", "date_of_birth": "1985-05-15" }{ "firstName": "Jane", "lastName": "Smith", "dob": "1985-05-15" }{ "firstName": "John", "lastName": "Doe", "dob": "1990-01-01" }{ "first_name": "John", "last_name": "Doe", "date_of_birth": "1990-01-01" }Map nested fields
Use a dotted path to read or write a field inside an object. Precept creates the object when it writes.
vendor acme
name nested
model FullName {
first: string
last: string
}
model Contact {
name: FullName
}
model Person {
firstName: string
lastName: string
}
mapping Contact <-> Person {
name.first <-> firstName
name.last <-> lastName
}{ "firstName": "John", "lastName": "Doe" }{ "name": { "first": "John", "last": "Doe" } }{ "name": { "first": "Jane", "last": "Smith" } }{ "firstName": "Jane", "lastName": "Smith" }Nest and flatten an object
Use a field list on the right side to group top-level fields into one object.
vendor acme
name geo
model Coordinates {
lat: number
lon: number
}
model Contact {
coordinates: Coordinates
}
model Person {
lat: number
lon: number
}
mapping Contact <-> Person {
coordinates <-> {lat, lon}
}{ "lat": 42.3601, "lon": -71.0589 }{ "coordinates": { "lat": 42.3601, "lon": -71.0589 } }{ "coordinates": { "lat": 42.3601, "lon": -71.0589 } }{ "lat": 42.3601, "lon": -71.0589 }Skip empty optional fields
Put ? after a field to skip the rule when the source value is null or missing. Without ?, Precept writes null.
vendor acme
name optional
model Contact {
maiden_name?: string
}
model Person {
previousName?: string
}
mapping Contact <-> Person {
maiden_name? <-> previousName
}{ "previousName": "Smith" }{ "maiden_name": "Smith" }{ "previousName": null }{}Translate values with match
Use a match block to translate a set of values. The _ pair gives the value for all other input.
vendor acme
name status
model Contact {
status: string
}
model Person {
is_active: boolean
}
mapping Contact <-> Person {
status <- match {
"Active" <-> true
"Inactive" <-> false
_ <-> null
} -> is_active
}{ "status": "Active" }{ "is_active": true }{ "status": "Pending" }{ "is_active": null }{ "is_active": false }{ "status": "Inactive" }Split and join a name with separator
separator with a field list splits one string into an object. In the other direction, it joins the fields into one string.
vendor acme
name names
model Contact {
fullName: string
}
model Name {
first: string
last: string
}
model Person {
name: Name
}
mapping Contact <-> Person {
fullName <- separator(" ") | {first, last} -> name
}{ "fullName": "Jane Smith" }{ "name": { "first": "Jane", "last": "Smith" } }{ "name": { "first": "John", "last": "Doe" } }{ "fullName": "John Doe" }If the string has fewer parts than the field list, the remaining fields are null:
{ "fullName": "Cher" }{ "name": { "first": "Cher", "last": null } }Split an address into top-level fields
If the rule has no target field after the field list, Precept reads and writes the fields at the top level of the right model.
vendor acme
name address
model Contact {
address_line: string
}
model Address {
street: string
city: string
state: string
zip: string
}
mapping Contact <-> Address {
address_line <- separator(", ") | {street, city, state, zip}
}{ "address_line": "1 Main St, Boston, MA, 02110" }{ "street": "1 Main St", "city": "Boston", "state": "MA", "zip": "02110" }{ "street": "1 Main St", "city": "Boston", "state": "MA", "zip": "02110" }{ "address_line": "1 Main St, Boston, MA, 02110" }Select a fallback value with coalesce
coalesce writes the first value that is not empty, or the default.
vendor acme
name display
model Contact {
displayName: string
}
model Person {
fullName: string
email: string
}
mapping Contact <-> Person {
displayName <- coalesce(fullName, email, "Unknown")
}{ "fullName": "Alice Smith", "email": "alice@example.com" }{ "displayName": "Alice Smith" }{ "fullName": "", "email": "bob@example.com" }{ "displayName": "bob@example.com" }{}{ "displayName": "Unknown" }In the other direction, a value that contains @ goes to the second field. Another value goes to the first field.
{ "displayName": "user@example.com" }{ "email": "user@example.com" }{ "displayName": "John Doe" }{ "fullName": "John Doe" }Parse a phone number with template
template extracts named parts from a structured string. Put the raw string on the right side of the rule.
vendor acme
name phones
model PhoneParts {
countryCode: string
areaCode: string
localNumber: string
}
model Contact {
parts: PhoneParts
}
model PhoneRecord {
phone: string
}
mapping Contact <-> PhoneRecord {
parts <- template(`{countryCode:num(1-3)}-{areaCode:num(3)}-{localNumber:num(4)}`) -> phone
}{ "phone": "44-555-1234" }{ "parts": { "countryCode": "44", "areaCode": "555", "localNumber": "1234" } }{ "parts": { "countryCode": "1", "areaCode": "212", "localNumber": "5550" } }{ "phone": "1-212-5550" }Read fixed-width fields with slice
slice reads a part of a string by position. This example reads a security identifier and a code from one packed column. Put the model with the packed column on the right side of the mapping header.
vendor acme
name positions
model Parsed {
cusip: string
code: string
amount: number
}
model RawFile {
packed_data: string
amount: number
}
mapping Parsed <-> RawFile {
packed_data | slice(1, 9) <-> cusip
packed_data | slice(10, 5) <-> code
amount <-> amount
}{ "packed_data": "IBM123456ABCDE", "amount": 500 }{ "cusip": "IBM123456", "code": "ABCDE", "amount": 500 }slice removes spaces from the start and end of each part:
{ "packed_data": "IBM Hello", "amount": 100 }{ "cusip": "IBM", "code": "Hello", "amount": 100 }In data ingestion, Precept compiles the same rules to SQL:
SELECT "amount", TRIM(SUBSTR("packed_data", 1, 9)) AS "cusip", TRIM(SUBSTR("packed_data", 10, 5)) AS "code"Chain functions in a pipeline
A pipeline is a list of functions with | between them. Precept gives the result of each function to the next function. This example uses two list functions:
headreads the first item of a list.tailreads the last item of a list.
The grid field is a list of lists. head | tail reads the first inner list, and then the last item of that list.
vendor acme
name grid
model Cell {
cell: string
}
model Grid {
grid: string[][]
}
mapping Cell <-> Grid {
cell | head | tail <-> grid
}{ "grid": [["a", "b"], ["c"]] }{ "cell": "b" }In the other direction, each function writes its value back as a list, in the opposite order. The result is a list that holds one list:
{ "cell": "z" }{ "grid": [["z"]] }Run a function on one side with sort_by
A pipeline is attached to the field next to it. In ranked | sort_by(priority) <-> queue, the pipeline is attached to ranked.
Some functions work in two directions: head, tail, slice, separator, coalesce and template. Precept runs these functions in both directions. Other functions, for example sort_by, work in one direction only. Precept runs a one-way function only when it writes the field that the function is attached to.
sort_by(key) sorts a list of records by the field key. Records with the same key stay in their original order. Records with a null key go to the end.
vendor acme
name queue
model Assignee {
name: string
priority: number
}
model Ranked {
ranked: Assignee[]
}
model Queue {
queue: Assignee[]
}
mapping Ranked <-> Queue {
ranked | sort_by(priority) <-> queue
}When Precept writes ranked, it sorts the list:
{
"queue": [
{ "name": "A", "priority": 2 },
{ "name": "B", "priority": 1 },
{ "name": "C", "priority": 1 },
{ "name": "D", "priority": null }
]
}{
"ranked": [
{ "name": "B", "priority": 1 },
{ "name": "C", "priority": 1 },
{ "name": "A", "priority": 2 },
{ "name": "D", "priority": null }
]
}When Precept writes queue, sort_by does not run. Precept copies the list without changes.
Attach a function to the right field
To attach a pipeline to the field on the right side, put the pipeline after <-> and before the field: code <-> slice(3, 4) | packed. This rule reads characters 3 to 6 of packed.
vendor acme
name parts
model Part {
code: string
}
model Line {
packed: string
}
mapping Part <-> Line {
code <-> slice(3, 4) | packed
}{ "packed": "xxABCDyy" }{ "code": "ABCD" }In the other direction, slice pads the value with spaces to 4 characters and puts it at position 3:
{ "code": "AB" }{ "packed": " AB " }Attach functions to both fields
A rule can have a pipeline on each side. In this example, the old model has one assignee and the new model has a list of assignees. head is attached to assignee, and sort_by is attached to assignees.
head("clear") sets what Precept writes when the value is null. With "clear", Precept writes an empty list.
vendor acme
name tracker
model Assignee {
name: string
priority: number
}
model Old {
assignee: Assignee
}
model New {
assignees: Assignee[]
}
mapping Old <-> New {
assignee | head("clear") <-> sort_by(priority) | assignees
}When Precept reads New data, head reads the first assignee. sort_by is attached to assignees, so it does not run in this direction. The result is Bob, not Alice:
{
"assignees": [
{ "name": "Bob", "priority": 2 },
{ "name": "Alice", "priority": 1 }
]
}{ "assignee": { "name": "Bob", "priority": 2 } }When Precept reads Old data, head writes the assignee back as a list with one item. Then sort_by sorts the list:
{ "assignee": { "name": "Alice", "priority": 1 } }{ "assignees": [{ "name": "Alice", "priority": 1 }] }If assignee is null, the output is { "assignees": [] }.
Convert a record with mapFn
A mapFn holds JavaScript. Use it when the declarative rules cannot express the conversion. A mapFn converts data in one direction only.
vendor acme
name users
model User {
id: string
name: string
email: string
active: boolean
}
model Person {
firstName: string
lastName: string
email: string
}
mapFn (user: User): Person {=
const [firstName, ...rest] = user.name.split(' ')
return {
firstName: firstName || '',
lastName: rest.join(' '),
email: user.email
}
=}{ "id": "u-1", "name": "Ann Marie Cole", "email": "ann@example.com", "active": true }{ "firstName": "Ann", "lastName": "Marie Cole", "email": "ann@example.com" }Convert a list with mapFn
A mapping block converts one record. If an action returns an array, use a mapFn that maps each item. Precept passes the full action result to the mapFn.
vendor acme
name contacts
model Contact {
first_name: string
last_name: string
}
model Person {
firstName: string
lastName: string
}
mapFn (contacts: Contact): Person[] {=
return contacts.map((c) => ({
firstName: c.first_name,
lastName: c.last_name
}))
=}[
{ "first_name": "Jane", "last_name": "Smith" },
{ "first_name": "John", "last_name": "Doe" }
][
{ "firstName": "Jane", "lastName": "Smith" },
{ "firstName": "John", "lastName": "Doe" }
]A complete module
This module connects to a REST API, defines two models, and maps one model to the other. Precept calls GET https://api.acme.example/contacts/{id} when you run the get action.
vendor acme
name crm
version "1.0"
meta {
productName: "Acme CRM"
description: "Contacts from the Acme CRM API"
}
defaults {
http {
baseUrl: "https://api.acme.example"
headers: {
Authorization: `Bearer {config.apiKey}`
}
}
}
config {
apiKey: secret string
}
model Contact {
id: string primary
first_name: string
last_name: string
email?: string format("email")
status: "Active" | "Inactive"
resource {
query get(id: string): Contact -> {
http.get(`/contacts/{id}`)
}
}
}
model Person {
contactId: string
firstName: string
lastName: string
email?: string
isActive: boolean
}
mapping Contact <-> Person {
id <-> contactId
first_name <-> firstName
last_name <-> lastName
email? <-> email
status <- match {
"Active" <-> true
"Inactive" <-> false
} -> isActive
}{ "id": "c-42", "first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "status": "Active" }{ "contactId": "c-42", "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "isActive": true }The match block has no _ pair. If no pair matches, Precept does not write isActive:
{ "id": "c-43", "first_name": "John", "last_name": "Doe", "status": "Pending" }{ "contactId": "c-43", "firstName": "John", "lastName": "Doe" }To get the result as a Person, send this request:
GET /rest/acme/crm/resource/Contact/get?id=c-42
Accept: application/json; precept-mapping-target="acme/crm.Person"