Skip to content

Escapin is a JS/TS transpiler for escaping from complicated usage of cloud services and APIs

License

Notifications You must be signed in to change notification settings

FujitsuLaboratories/escapin

Repository files navigation

Escapin

the transpiler for escaping from complicated usage
of cloud services and APIs

npm version Build Status Renovate enabled codecov code style: prettier semantic-release MIT License

Table of Contents

Prerequisites

  1. Node.js 10.x or later
  2. Serverless Framework

Installation

npm install --save-dev escapin

Usage

Escapin provides CLI escapin that works on Node.js project directories containing ./package.json.

First, append the following scripts in package.json:

{
  "scripts": {
    "build": "escapin",
    "start": "cd build && serverless deploy"
  }
}

Then, run build and start on the project folder:

npm run build
npm start

Escapin transpiles your source code into executable one as a serverless application, and generates serverless.yml that can be used for deploying the programs to cloud services by Serverless Framework.

CLI options

Usage: escapin [options]

Options:
  -V, --version         output the version number
  -d, --dir <dir>       working directory (default: ".")
  --ignore-path <path>  specify path of ignore file (default: ".gitignore")
  -h, --help            output usage information

Configuration

You can give configuration information to Escapin CLI by using the following ways:

Place Format
escapin property in package.json JSON
.escapinrc JSON or YAML
.escapinrc.json JSON
.escapinrc.yaml or .escapinrc.yml YAML
.escapinrc.js or escapin.config.js JavaScript

Here is the example of JSON configuration file .escapinrc.

{
  "name": "sendmail",
  "api_spec": "swagger.yaml",
  "credentials": [{ "api": "mailgun API", "basicAuth": "api:<YOUR_API_KEY>" }],
  "platform": "aws",
  "default_storage": "table",
  "output_dir": "build",
  "http_client": "axios"
}
module.exports = {
  name: "sendmail",
  api_spec: "swagger.yaml",
  credentials: [{ api: "mailgun API", basicAuth: "api:<YOUR_API_KEY>" }],
  platform: "aws",
  default_storage: "table",
  output_dir: "build",
  http_client: "axios",
};
Name Description Options Default
name name of the application
api_spec path of the specification file of the API published by the application
credentials credentials required in calling external APIs
platform cloud platform where the application is being deployed aws aws
default_storage the storage type that are selected by default table
bucket
table
output_dir directory where the transpilcation artifacts are being stored build
http_client http client used in generated code for requesting apis defined by OAS axios
request
axios

Transpilation features


Storage

You can use several kinds of storage services just like a first-class object in JavaScript. By declaring an empty object placing a special type annotation (e.g., bucket) you can create a resource in that type of storage services.

You can use both canonical type platform.storageType (e.g., aws.bucket) and shorthand type storageType (e.g., bucket) for storage objects; platform in the configuration file is used in shorthand types. If you omit a type annotation, default_storage is used as that type by default. In v0.2.x, bucket and table is available for storage types; bucket represents a bucket in object storage, and table represents a table in NoSQL datastore service.

export const foo: aws.bucket = {}; // AWS S3 Bucket
export const bar: bucket = {}; // AWS S3 Bucket
export const baz: table = {}; // AWS DynamoDB Table
export const qux = {}; // AWS DyanmoDB Table

Here are the usage example of storage objects:

export const foo: bucket = {};

foo[id] = bar; // uploading data
baz = foo[id]; // downloading data
qux = Object.keys(foo); // obtaining keys of data
delete foo[id]; // deleting existing data

Input


index.js
export const foo: table = {};

foo[id] = bar;
.escapinrc.js
module.exports = {
  platform: "aws",
  ...
};

Output


index.js
import { DynamoDB } from "aws-sdk";

await new Promise((resolve, reject) => {
  new DynamoDB().putItem(
    {
      TableName: "foo-9fe932f9-32e7-49f7-a341-0dca29a8bb32",
      Item: {
        key: { S: id },
        type: { S: typeof bar },
        value: {
          S:
            typeof bar === "object" || typeof bar === "function"
              ? JSON.stringify(bar)
              : bar,
        },
      },
    },
    (err, _temp) => {
      if (err) {
        reject(err);
      } else {
        resolve(_temp);
      }
    }
  );
});
serverless.yml
resources:
  Resources:
    fooTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: foo-9fe932f9-32e7-49f7-a341-0dca29a8bb32
        KeySchema:
          - AttributeName: key
            KeyType: HASH
        AttributeDefinitions:
          - AttributeName: key
            AttributeType: S
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
    escapinFunctionRole:
      Properties:
        Policies:
          - PolicyName: foo-9fe932f9-32e7-49f7-a341-0dca29a8bb32-FullAccess
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                - Effect: Allow
                  Action:
                    - "dynamodb:ListGlobalTables"
                    - "dynamodb:ListTables"
                  Resource: "*"
                - Effect: Allow
                  Action: "dynamodb:*"
                  Resource:
                    "Fn::GetAtt":
                      - fooTable
                      - Arn

Function


Input


index.js
export function handler(req) {
  if (errorOccured()) {
    throw new Error("[400] An error occured");
  }

  return { message: "Succeeded" };
}
.escapinrc.js
module.exports = {
  name: "myapp",
  platform: "aws",
  api_spec: "swagger.yaml",
  ...
};
swagger.yaml
swagger: "2.0"
info:
  version: "1.0.0"
  title: "myapp"
host: "myapp.org"
basePath: "/v1"
schemes:
  - "http"
produces:
  - "application/json"
paths:
  /handle:
    get:
      summary: "handler"
      x-escapin-handler: "index.handler"
      parameters: []
      responses:
        200:
          schema:
            $ref: "#/definitions/Message"
        400:
          schema:
            $ref: "#/definitions/Error"
  ...

Output


index.js
export function handler(req, context, callback) {
  if (errorOccured()) {
    callback(new Error("[400] An error occured."));
    return;
  }

  callback(null, { message: "Succeeded" });
  return;
}
serverless.yml
functions:
  handlerFunction:
    handler: index.handler
    runtime: nodejs10.x
    role: escapinFunctionRole
    events:
      - http:
          path: handle
          method: get
          cors: true
          integration: lambda
resources:
  Resources:
    escapinFunctionRole:
      Type: "AWS::IAM::Role"
      Properties:
        Path: /escapin/
        RoleName: myappEscapinFunctionRole
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            - Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action: "sts:AssumeRole"
        Policies: ...

Importing open APIs


Usage

import api from "http://path/to/swagger.yaml";
Method Path Header Body Example
GET /items items = api.items;
GET /items/:id item = api.items[id];
GET /items/:id/props props = api.items[id].props;
GET /items/:id?foo=bar item = api.items[id] [ { foo: 'bar' } ] ;
GET /items/:id?foo=bar baz: qux item = api.items[id] [ { foo: 'bar', baz: 'qux' } ] ;
POST /:domain/messages { quux: 'corge' } api.domain[domain].messages ( { quux: 'corge' } ) ;
POST /items { quux: 'corge' } api.items ( { quux: 'corge' } ) ;
POST /items/:id?foo=bar baz: qux { quux: 'corge' } api.items[id] [ { foo: 'bar', baz: 'qux' } ] ( { quux: 'corge' } ) ;
PUT /items/:id baz: qux { quux: 'corge' } api.items[id] [ { baz: 'qux' } ] = { quux: 'corge' };
DELETE /items/:id delete api.items[id];

Input


index.js
import api from "http://path/to/swagger.yaml";
api.items[id][{ foo: "bar", baz: "qux" }]({ quux: "corge" });
.escapinrc.js
module.exports = {
  http_client: "request",
  ...
};
http://path/to/swagger.yaml
swagger: "2.0"
info:
  title: Awesome API
  description: An awesome API
  version: "1.0.0"
host: "api.endpoint.com"
schemes:
  - http
basePath: /v1
produces:
  - application/json
consumes:
  - application/json
paths:
  /items/{id}:
    post:
      description: Do some task regarding an item
      parameters:
        - name: id
          in: path
          type: string
          required: true
          description: Item ID
        - name: foo
          in: query
          type: string
          required: true
        - name: baz
          in: header
          type: string
          required: true
        - name: params
          in: body
          schema:
            $ref: "#/definitions/Params"
      responses:
        "200":
          description: Succeeded
          schema:
            $ref: "#/definitions/Message"
definitions:
  Params:
    type: object
    properties:
      quux:
        type: string
  Message:
    type: object
    properties:
      message:
        type: string

Output


index.js
import request from "request";
const { _res, _body } = request({
  uri: `http://api.endpoint.com/v1/items/${id}`,
  method: "post",
  contentType: "application/json",
  json: true,
  qs: {
    foo: "bar",
  },
  headers: {
    baz: "qux",
  },
  body: {
    quux: "corge",
  },
});

Publishing your API


Input


index.js
export function handleItem(req) {
  const id = req.path.id;
  const foo = req.query.foo;
  const baz = req.header.baz;
  const quux = req.body.quux;

  if (errorOccured()) {
    throw new Error("[400] An error occured.");
  }

  return { message: "Succeeded" };
}
swagger.yaml
swagger: "2.0"
info:
  title: Awesome API
  description: An awesome API
  version: "1.0.0"
host: "api.endpoint.com"
schemes:
  - https
basePath: /v1
produces:
  - application/json
consumes:
  - application/json
paths:
  /items/{id}:
    post:
      x-escapin-handler: index.handleItem
      description: Do some task regarding an item
      parameters:
        - name: id
          in: path
          type: string
          required: true
          description: Item ID
        - name: foo
          in: query
          type: string
          required: true
        - name: baz
          in: header
          type: string
          required: true
        - name: params
          in: body
          schema:
            $ref: "#/definitions/Params"
      responses:
        "200":
          schema:
            $ref: "#/definitions/Message"
        "400":
          schema:
            $ref: "#/definitions/Error"
definitions:
  Params:
    type: object
    properties:
      quux:
        type: string

Output


index.js
export function handleItem(req, context, callback) {
  const id = req.path.id;
  const foo = req.query.foo;
  const baz = req.header.baz;
  const quux = req.body.quux;

  if (errorOccured()) {
    callback(new Error("[400] An error occured."));
    return;
  }

  callback(null, { message: "Succeeded" });
  return;
}
serverless.yml
functions:
  handleItemFunction:
    handler: index.handleItem
    runtime: nodejs10.x
    role: escapinFunctionRole
    events:
      - http:
          path: 'items/{id}'
          method: post
          cors: true
          integration: lambda
  ...

Auto-completing asynchronous features


Destructuring nesting callbacks


Original
function func() {
  call(arg, (err, data1, data2) => {
    if (err) {
      handleError(err);
    } else {
      doSomething(data1, data2);
    }
  });
}

Destructured
function func() {
  try {
    const { data1, data2 } = call(arg);
    doSomething(data1, data2);
  } catch (err) {
    handleError(err);
  }
}

Asynchronized
async function func() {
  try {
    const _data = await new Promise((resolve, reject) => {
      call(arg, (err, _data1, _data2) => {
        if (err) reject(err);
        else resolve({ _data1, _data2 });
      });
    });
    doSomething(_data._data1, _data._data2);
  } catch (err) {
    handleError(err);
  }
}

for, for-in, for-of (collection should be obtained asynchronously)


Input
for (const item of api.call(arg)) {
  doSomething(item);
}

Output
const _data = await new Promise((resolve, reject) => {
  api.call(arg, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});
for (const item of _data) {
  doSomething(item);
}

for, for-in, for-of (executable in parallel)


Input
for (const arg of args) {
  api.call(arg);
}

Output
const _promises = [];
for (const arg of args) {
  _promises.push(
    (async () => {
      await new Promise((resolve, reject) => {
        api.call(arg, (err, data) => {
          if (err) reject(err);
          else resolve(data);
        });
      });
    })()
  );
}
await Promise.all(_promises);

for, for-in, for-of (NOT executable in parallel)


Input
let sum = 0;
for (const arg of args) {
  sum += api.call(arg);
}

Output
let sum = 0;
for (const arg of args) {
  const _data = await new Promise((resolve, reject) => {
    api.call(arg, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
  sum += _data;
}

while,do-while


Input
while ((data = api.call(arg)) === null) {
  doSomething(data);
}

Output
let _data = await new Promise((resolve, reject) => {
  api.call(arg, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});
while ((data = _data) === null) {
  doSomething(data);
  _data = await new Promise((resolve, reject) => {
    api.call(arg, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

if-else


Input
if (api.call(arg)) {
  doSomething();
} else if (api.call2(arg)) {
  doSomething2();
}

Output
const _data = await new Promise((resolve, reject) => {
  api.call(arg, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});
if (_data) {
  doSomething();
} else {
  let _data2 = await new Promise((resolve, reject) => {
    api.call2(arg, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
  if (_data2) {
    doSomething2();
  }
}

switch-case


Input
switch (api.call(arg)) {
  case "foo":
    api.call2(arg);
    break;
  case "bar":
    api.call3(arg);
    break;
  default:
    break;
}

Output
let _promise;
const _data = await new Promise((resolve, reject) => {
  api.call(arg, (err, data) => {
    if (err) reject(err);
    else resolve(data);
  });
});
switch (_data) {
  case "foo":
    await new Promise((resolve, reject) => {
      api.call2(arg, (err, data) => {
        if (err) reject(err);
        else resolve(data);
      });
    });
    break;
  case "bar":
    await new Promise((resolve, reject) => {
      api.call3(arg, (err, data) => {
        if (err) reject(err);
        else resolve(data);
      });
    });
    break;
  default:
    break;
}

functions that require callback functions as an argument (e.g., Array#forEach)


Input
// special rules are applied for Array#map and Array#forEach
args.map((arg) => api.call(arg));
args.forEach((arg) => api.call(arg));

args.some((arg) => api.call(arg));

Output
import deasync from "deasync";

await Promise.all(args.map(async (arg) => await api.call(arg)));
args.forEach(async (arg) => await api.call(arg));

args.some((arg) => {
  let _data;
  let done = false;
  new Promise((resolve, reject) => {
    api.call(arg, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  }).then((data) => {
    _data = data;
    done = true;
  });
  deasync.loopWhile((_) => !done);
  return _data;
});

asynchronous features appearing in input


Input
args.map(arg => await promisifiedFunc(arg));

Output
await Promise.all(args.map(async (arg) => await promisifiedFunc(arg)));

Publications

License

MIT

FOSSA Status