Skip to content
This repository has been archived by the owner on Apr 13, 2019. It is now read-only.

transformers

Konstantin Sobolev edited this page Jun 16, 2017 · 5 revisions

Transformers are the foundation of federation, they define data transformations that can be applied by the federator in order to achieve desired result starting with some initial data. For example there can be transformers for resolving PersonRecord from PersonId, or for building fullName field from firstName and lastName.

Definition

Every transformer is defined for some type T and is described by two projections: input and output. Transformer implementation takes data of type T conforming the input projection and produces another instance of T conforming the output projection.

Transfomers can be defined in Epigraph schema using transformer keyword and the following syntax:

transformer <name> : <T> {
  <optional annotations>
  inputProjection <input path of type T>
  outputProjection <output path of type T>
}  

Examples

Lets consider the following types

entity Person {
  id: PersonId
  `record`: PersonRecord
}

long PersonId

record PersonRecord {
  firstName: String
  lastName: String
  fullName: String
  bestFriend: Person
}

Transformer that converts PersonId to PersonRecord could look like this:

transformer person: Person {
  inputProjection :+id  // '+' makes it 'required' meaning it can't be an error or null
  outputProjection :`record`(firstName, lastName)
}

This transformer applies to Person entity type and can convert id model to record model, with firstName and lastName fields populated.

Another transformer could be responsible for best friend lookup:

transformer person: Person {
  inputProjection :+id
  outputProjection :`record`(bestFriend:id)
}

This says that given person's ID this transformer can find best friend's ID. Previously described 'person' transformer could then be applied to build best friend's record model.

Yet another transformer can be used to compose full names out of first and last names:

transformer fullName: PersonRecord {
  inputProjection (firstName, lastName)
  outputProjection (fullName)
}  

Deriving transformers from read operations

Read operations on map-typed resources can be treated as keys to values conversions. Transformers can be automatically derived from such operations as soon as we know a relationship between keys and values.

For example consider people resource:

resource people: map[PersonId,Person] {
  read {
    outputProjection [required]:`record`(firstName,lastName)
  }
}

Here we have a single read operation that takes (required) map keys as an argument and produces record models. This looks very close to the person transformer described in the examples section above. We can derive it as soon as we know that PersonId key and Person map value are the same entity, like this:

resource people: map[PersonId,Person] {
  read {
    outputProjection [required]:`record`(firstName,lastName)
    
    transformer: Person {
      inputProjection :+id
    }
  }
}

This says that

  • we are defining a transformer of Person type
  • it takes id models as an input and translates them to map keys
  • operation output projection without key part becomes transformer output projection, resulting in :record(firstName, lastName)

Generated trasformer implementation will be calling read operation with proper parameters translation, with calls batching when possible.

What if people resource was defined as a map from PersonId to PersonRecord? Read operation's output projection wouldn't work out of the box anymore:

resource people: map[PersonId,PersonRecord] {
  read {
    outputProjection [required](firstName,lastName)
    
    transformer: Person {
      inputProjection :+id
      outputProjection :`record`
    }
  }
}

Now we have to explictly tell that map values are record models of Person type. Operation's output projection will be appended to the transformer's :record output projection, with key part removed, giving the same result as before.

Best friend operation can be modelled in two different ways. First one would be the same people resource, but a separate read operation with a path:

resource people: map[PersonId,Person] {
  read bestFriend {
    path /.:record/bestFriend
    outputProjection :id
    
    transformer: Person {
      inputProjection :+id
    }
  }
}

In this case operation defines a path relative to map value type that it can be applied to. For instance this operation will be called for

GET /people/1:record/bestFriend:id

request, but not for

GET /people/1:record(firstName,bestFriend:id)

Transformer's output projection will consist of operation path with key part removed, followed by operation's output projection, resulting in :record/bestFriend:id

Another way to define best friend operation is

resource bestFriends: map[PersonId,PersonId] {
  read {
    outputProjection [required]
    
    transformer: Person {
      inputProjection :id
      outputProjection :record/bestFriend:id
    }
  }
}

This operation defines a straightforward mapping, and in this case transformer must be fully explicit about the relationship between map key and value.

Transformer deriving rules

Given the following schema

resource RES_NAME: map[K,V] {
  read OP_NAME {
    path OP_PATH
    outputProjection [ projection OP_KEY ] OP_VALUE

    transformer T_TYPE {
      inputProjection T_INPUT_PREFIX
      outputProjection T_OUTPUT_PREFIX
    }
  }
}

where

  • OP_PATH, OP_KEY and OP_VALUE are optional (by resource syntax)
  • T_INPUT_PREFIX is optional if K is T_TYPE
  • T_INPUT_PREFIX is a path starting with type T_TYPE and ending with K type
  • T_OUTPUT_PREFIX is optional if V is T_TYPE
  • T_OUTPUT_PREFIX is a path starting with type T_TYPE and ending with V type
  • Resource map keys may be required, but may not be forbidden

a transformer can be derived

transformer T_NAME: T_TYPE {
  inputProjection T_INPUT
  outputProjection T_OUTPUT
}

where

  • T_NAME is ${RES_NAME}_${OP_NAME or "read" if no name}_transformer
  • T_INPUT is T_INPUT_PREFIX + OP_KEY
  • T_OUTPUT is T_OUTPUT_PREFIX + noKey(OP_PATH) + OP_VALUE2
    • noKey removes leading key part from projection (essentially returns map projection's value part)
    • OP_VALUE2 is OP_VALUE if OP_PATH is present, noKey(OP_VALUE) otherwise