// Copyright 2015-2016 Tachyus Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you
// may not use this file except in compliance with the License. You may
// obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
// implied. See the License for the specific language governing
// permissions and limitations under the License.
//
export namespace Schema {
export type HttpMethod = "Delete" | "Get" | "Post" | "Put";
export interface HttpCallingConvention {
tag: "HttpCallingConvention";
Item1: HttpMethod;
path: string;
}
export type CallingConvention = HttpCallingConvention;
export interface ArrayType {
tag: "ArrayType";
Item: DataType;
}
export interface BooleanType {
tag: "BooleanType";
}
export interface BytesType {
tag: "BytesType";
}
export interface DateTimeType {
tag: "DateTimeType";
}
export interface DateTimeOffsetType {
tag: "DateTimeOffsetType"
}
export interface GuidType {
tag: "GuidType"
}
export interface DoubleType {
tag: "DoubleType";
}
export interface IntType {
tag: "IntType";
}
export interface JsonType {
tag: "JsonType";
}
export interface ListType {
tag: "ListType";
Item: DataType;
}
export interface OptionType {
tag: "OptionType";
Item: DataType;
}
export interface SequenceType {
tag: "SequenceType";
Item: DataType;
}
export interface StringDictType {
tag: "StringDictType";
Item: DataType;
}
export interface StringType {
tag: "StringType";
}
export interface TupleType {
tag: "TupleType";
Item: DataType[];
}
export interface TypeReference {
tag: "TypeReference";
Item: string;
}
export type DataType = ArrayType | BooleanType | BytesType | DateTimeType | DateTimeOffsetType | GuidType | DoubleType | IntType | JsonType | ListType | OptionType | SequenceType | StringDictType | StringType | TupleType | TypeReference;
export class Parameter {
ParameterName: string;
ParameterType: DataType;
}
export class Method {
CallingConvention: CallingConvention;
MethodName: string;
MethodParameters: Parameter[];
MethodReturnType: Option;
}
export class EnumCase {
EnumCaseName: string;
EnumCaseValue: number;
}
export class Enum {
EnumName: string;
EnumCases: EnumCase[];
}
export class Field {
FieldName: string;
FieldType: DataType;
}
export class Record {
RecordName: string;
RecordFields: Field[];
}
export class UnionCase {
CaseName: string;
CaseFields: Field[];
}
export class Union {
UnionName: string;
UnionCases: UnionCase[];
}
export interface DefineEnum {
tag: "DefineEnum";
Item: Enum;
}
export interface DefineRecord {
tag: "DefineRecord";
Item: Record;
}
export interface DefineUnion {
tag: "DefineUnion";
Item: Union;
}
export type TypeDefinition = DefineEnum | DefineRecord | DefineUnion;
export interface Service {
Methods: Method[];
TypeDefinitions: TypeDefinition[];
}
}
//
/** Implements the client side of the Gluon connector. */
// Option support ------------------------------------------------------
/** Represents optional values, just as F# does. */
export type Option = T | null | undefined;
/** Option operators. */
export namespace Option {
/** Constructs a Some(value) option. */
export function some(value: T): Option {
return value;
}
/** Returns true if the value is Some value and false otherwise. */
export function isSome(value: Option): value is T {
return value !== undefined && value !== null;
}
/** Constructs a None option. */
export function none(): Option {
return null;
}
/** Returns true if the value is null or undefined and false otherwise. */
export function isNone(value: Option): value is null | undefined {
return value === undefined || value === null;
}
/** Recovers an Option from JSON object representation. */
export function fromJSON(json: any): Option {
return isSome(json) ? (json[0]) : null;
}
/** Converts to a JSON representation. */
export function toJSON(value: Option): any {
return isSome(value) ? [value] : null;
}
/** Unpacks with a default value. */
export function withDefault(value: Option, defaultValue: T): T {
return isSome(value) ? value : defaultValue;
}
}
// Dict support --------------------------------------------------------
export class Dict {
private data: {[key: string]: T} = {};
private check(key: string) {
if (typeof key !== "string") {
throw new Error("Invalid or null key");
}
}
containsKey(key: string): boolean {
this.check(key);
return this.data.hasOwnProperty(key);
}
forEach(visit: (key: string, element: T) => void): void {
for (const prop in this.data) {
if (this.data.hasOwnProperty(prop)) {
visit(prop, this.data[prop]);
}
}
}
copy(): Dict {
const result = new Dict();
this.forEach((key, el) => result.setAt(key, el));
return result;
}
at(key: string): T {
this.check(key);
if (this.data.hasOwnProperty(key)) {
return this.data[key];
} else {
throw new Error("Missing key: " + key);
}
}
tryFind(key: string): Option {
this.check(key);
return this.data.hasOwnProperty(key) ? this.data[key] : null;
}
setAt(key: string, value: T): void {
this.check(key);
this.data[key] = value;
}
toJSON(): {[key: string]: T} {
return this.data;
}
}
// Schema -----------------------------------------------------------------
namespace DataType {
export function children(d: Schema.DataType): Schema.DataType[] {
switch (d.tag) {
case "ArrayType": return [d.Item];
case "ListType": return [d.Item];
case "OptionType": return [d.Item];
case "SequenceType": return [d.Item];
case "StringDictType": return [d.Item];
case "TupleType": return d.Item;
default: return [];
}
}
}
interface Visitor {
visitDataType(dt: Schema.DataType): void;
visitRecord(r: Schema.Record): void;
visitUnion(u: Schema.Union): void;
visitEnum(e: Schema.Enum): void;
}
function defaultVisitor(): Visitor {
return {
visitDataType: t => { },
visitRecord: r => { },
visitUnion: u => { },
visitEnum: e => { }
};
}
function visitDataType(dt: Schema.DataType, visitor: Visitor) {
visitor.visitDataType(dt);
DataType.children(dt).forEach(x => visitDataType(x, visitor));
}
function visitTypes(types: Schema.TypeDefinition[], visitor: Visitor) {
function visitField(f: Schema.Field) {
visitDataType(f.FieldType, visitor);
}
function visitRecord(r: Schema.Record) {
visitor.visitRecord(r);
r.RecordFields.forEach(visitField);
}
function visitCase(c: Schema.UnionCase) {
c.CaseFields.forEach(visitField);
}
function visitUnion(u: Schema.Union) {
visitor.visitUnion(u);
u.UnionCases.forEach(visitCase);
}
function visitEnum(e: Schema.Enum) {
visitor.visitEnum(e);
}
function visitTD(td: Schema.TypeDefinition) {
switch (td.tag) {
case "DefineUnion": return visitUnion(td.Item);
case "DefineRecord": return visitRecord(td.Item);
case "DefineEnum": return visitEnum(td.Item)
default: throw new Error("match failed");
}
}
types.forEach(visitTD);
}
function visitServiceMethods(methods: Schema.Method[], visitor: Visitor) {
function visitParam(p: Schema.Parameter) {
visitDataType(p.ParameterType, visitor);
}
function visitMethod(m: Schema.Method) {
m.MethodParameters.forEach(visitParam);
if (m.MethodParameters.length > 1) {
const t = tupleType(m.MethodParameters.map(p => p.ParameterType));
visitDataType(t, visitor);
}
if (!!m.MethodReturnType) {
visitDataType(m.MethodReturnType, visitor);
}
}
methods.forEach(visitMethod);
}
function dataTypeKey(dataType: Schema.DataType): string {
function key(dataType: Schema.DataType): any {
switch (dataType.tag) {
case "ArrayType": return [":array", key(dataType.Item)];
case "BooleanType": return ":bool";
case "BytesType": return ":bytes";
case "DateTimeType": return ":datetime";
case "DateTimeOffsetType": return ":datetime";
case "GuidType": return ":str";
case "DoubleType": return ":double";
case "IntType": return ":int";
case "JsonType": return ":json";
case "ListType": return [":list", key(dataType.Item)];
case "OptionType": return [":option", key(dataType.Item)];
case "SequenceType": return [":seq", key(dataType.Item)];
case "StringDictType": return [":sdict", key(dataType.Item)];
case "StringType": return ":str";
case "TupleType": return [":tup"].concat(dataType.Item.map(i => key(i)));
case "TypeReference": return dataType.Item;
default: throw new Error("match failed");
}
}
return JSON.stringify(key(dataType));
}
function typeDefName(td: Schema.TypeDefinition) {
switch (td.tag) {
case "DefineEnum": return td.Item.EnumName;
case "DefineRecord": return td.Item.RecordName;
case "DefineUnion": return td.Item.UnionName;
default: throw new Error("match failed");
}
}
function findTypeDefinition(svc: Schema.Service, name: string) {
return svc.TypeDefinitions.filter(x => typeDefName(x) === name)[0];
}
// Serialization ----------------------------------------------------------
interface SerializerFactory {
getSerializer(dataType: Schema.DataType): Serializer;
}
interface Serializer {
init(factory: SerializerFactory): any;
toJSON(value: T): any;
fromJSON(json: any): T;
}
const booleanSerializer: Serializer =
{
init: f => { },
toJSON: x => x,
fromJSON: x => x
};
function serializeNumber(n: number): any {
if (isFinite(n)) {
return n;
} else {
return String(n);
}
}
function deserializeNumber(json: any): number {
return Number(json);
}
const numberSerializer: Serializer =
{
init: f => { },
toJSON: serializeNumber,
fromJSON: deserializeNumber
};
const DateFormat = new Intl.DateTimeFormat("en-GB", {weekday: "short", day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false})
const dateSerializer: Serializer =
{
init: f => { },
toJSON: date => {
// if .unspecified marker set before by Gluon ..
return (date).unspecified ?
// format without an associated time zone
DateFormat.format(date) :
date.toISOString();
},
fromJSON: (str: string) => {
// check if timezone marker is given ..
const unspecified = str.charAt(str.length - 1).toLowerCase() != "z";
const d = new Date(str);
// propagate the timezone marker info to allow turnaround
(d).unspecified = unspecified;
return d;
}
};
const dateTimeOffsetSerializer: Serializer =
{
init: f => { },
toJSON: date => {
let d = date.toISOString();
return d;
},
fromJSON: (str: string) => {
const d = new Date(str);
return d;
}
};
const rawJsonSerializer: Serializer =
{
init: f => { },
toJSON: x => x,
fromJSON: x => x
};
function b64encode(bytes: Uint8Array): string {
let s: string = "";
for (let i = 0; i < bytes.length; i++) {
s = s + String.fromCharCode(bytes[i]);
}
return btoa(s);
}
function b64decode(b64: string): Uint8Array {
const input = atob(b64);
const r = new Uint8Array(input.length);
for (let i = 0; i < r.length; i++) {
r[i] = input.charCodeAt(i);
}
return r;
}
const bytesSerializer: Serializer =
{
init: f => { },
toJSON: x => b64encode(x),
fromJSON: x => b64decode(x)
};
const stringSerializer: Serializer =
{
init: f => { },
toJSON: x => x,
fromJSON: x => x
};
class ArraySerializer {
private inner: Serializer;
constructor(public element: Schema.DataType) {
}
init(factory: SerializerFactory) {
this.inner = factory.getSerializer(this.element);
}
toJSON(value: any[]) {
return value.map(x => this.inner.toJSON(x));
}
fromJSON(json: any[]): any[] {
return json.map(x => this.inner.fromJSON(x));
}
}
class DictSerializer {
private inner: Serializer;
constructor(public element: Schema.DataType) { }
init(factory: SerializerFactory) {
this.inner = factory.getSerializer(this.element);
}
toJSON(dict: Dict): any {
const result: {[key: string]: any} = {};
dict.forEach((key, value) => {
result[key] = this.inner.toJSON(value);
});
return result;
}
fromJSON(json: any): Dict {
const result = new Dict();
for (let key in json) {
result.setAt(key, this.inner.fromJSON(json[key]));
}
return result;
}
}
class OptionSerializer {
private inner: Serializer;
constructor(public element: Schema.DataType) { }
init(factory: SerializerFactory) {
this.inner = factory.getSerializer(this.element);
}
toJSON(opt: Option): any {
return opt === null ? null : [this.inner.toJSON(opt)];
}
fromJSON(json: any[]): Option {
return json === null ? null : this.inner.fromJSON(json[0]);
}
}
class TupleSerializer {
private inner: Serializer[];
constructor(public elements: Schema.DataType[]) {
}
length(): number {
return this.elements.length;
}
init(factory: SerializerFactory) {
this.inner = this.elements.map(x => factory.getSerializer(x));
}
toJSON(tup: any[]): any[] {
const n = this.length();
const res = new Array(n);
for (let i = 0; i < n; i++) {
res[i] = this.inner[i].toJSON(tup[i]);
}
return res;
}
fromJSON(json: any[]): any[] {
const n = this.length();
const res = new Array(n);
for (let i = 0; i < n; i++) {
res[i] = this.inner[i].fromJSON(json[i]);
}
return res;
}
}
function buildDataTypeSerializer(dt: Schema.DataType): Serializer {
switch (dt.tag) {
case "ArrayType": return new ArraySerializer(dt.Item);
case "ListType": return new ArraySerializer(dt.Item);
case "SequenceType": return new ArraySerializer(dt.Item);
case "BooleanType": return booleanSerializer;
case "BytesType": return bytesSerializer;
case "DateTimeType": return dateSerializer;
case "DateTimeOffsetType": return dateTimeOffsetSerializer;
case "DoubleType": return numberSerializer;
case "IntType": return numberSerializer;
case "JsonType": return rawJsonSerializer;
case "OptionType": return new OptionSerializer(dt.Item);
case "StringDictType": return new DictSerializer(dt.Item);
case "StringType": return stringSerializer;
case "GuidType": return stringSerializer;
case "TupleType": return new TupleSerializer(dt.Item);
default: throw new Error("Invalid DataType");
}
}
/// Builds instances of a specific type based on a boxed argument list.
export interface IActivator {
createInstance(args: any[]): any;
typeId: string;
}
class TypeRegistry {
/// Activators indexed by type identity.
private activators: Dict;
constructor() {
this.activators = new Dict();
}
registerActivators(activators: IActivator[]) {
activators.forEach(a => {
this.activators.setAt(a.typeId, a);
});
}
fullCaseName(typeId: string, caseName: string) {
const i = typeId.lastIndexOf('.');
if (i === -1) {
return caseName;
} else {
return typeId.substr(0, i) + '.' + caseName;
}
}
createRecord(typeId: string, args: any[]): any {
return this.activators.at(typeId).createInstance(args);
}
createUnion(typeId: string, caseName: string, args: any[]): any {
return this.activators.at(this.fullCaseName(typeId, caseName)).createInstance(args);
}
}
class EnumSerializer {
constructor() { }
init(factory: SerializerFactory) { }
toJSON(value: any) { return value; }
fromJSON(json: any) { return json; }
}
class RecordSerializer {
fields: { name: string; ser: Serializer }[];
constructor(
public record: Schema.Record,
public typeRegistry: TypeRegistry) { }
init(factory: SerializerFactory) {
this.fields = this.record.RecordFields.map(f => {
return {
name: f.FieldName,
ser: factory.getSerializer(f.FieldType)
}
});
}
toJSON(value: any) {
const result: {[key: string]: any} = {};
this.fields.forEach(fld => {
result[fld.name] = fld.ser.toJSON(value[fld.name]);
});
return result;
}
fromJSON(json: any) {
const len = this.fields.length;
const args = new Array(len);
for (let i = 0; i < len; i++) {
const fld = this.fields[i];
args[i] = fld.ser.fromJSON(json[fld.name]);
}
return this.typeRegistry.createRecord(
this.record.RecordName, args);
}
}
class FieldInfo {
fieldName: string;
fieldSerializer: Serializer
}
class CaseInfo {
caseName: string;
fields: FieldInfo[];
}
class UnionSerializer {
cases: CaseInfo[];
constructor(public union: Schema.Union, public typeRegistry: TypeRegistry) {
}
init(factory: SerializerFactory) {
this.cases = this.union.UnionCases.map(c => {
return {
caseName: c.CaseName,
fields: c.CaseFields.map(f => {
return {
fieldName: f.FieldName,
fieldSerializer: factory.getSerializer(f.FieldType)
};
})
};
});
}
findCase(name: string): CaseInfo | undefined {
for (let i = 0; i < this.cases.length; i++) {
const c = this.cases[i];
if (c.caseName === name) {
return c;
}
}
}
toJSON(value: any): any[] | null {
const isStringLiteralUnion = typeof value === "string";
const tag: string = isStringLiteralUnion ? value : value.tag;
const uCase = this.findCase(tag);
if (uCase !== undefined) {
if (isStringLiteralUnion) {
return [tag];
} else {
const res = new Array(uCase.fields.length + 1);
res[0] = tag;
for (let i = 0; i < uCase.fields.length; i++) {
const f = uCase.fields[i];
const v = value[f.fieldName];
res[i + 1] = f.fieldSerializer.toJSON(v);
}
return res;
}
}
return null;
}
fromJSON(json: any): any | null {
const c = this.findCase(json[0]);
if (c !== undefined) {
const args = new Array(json.length - 1);
for (let i = 0; i < args.length; i++) {
const fld = c.fields[i];
args[i] = fld.fieldSerializer.fromJSON(json[i + 1]);
}
return this.typeRegistry.createUnion(this.union.UnionName, c.caseName, args);
} else {
return null;
}
}
}
function typeReference(typeId: string): Schema.DataType {
return { tag: "TypeReference", Item: typeId };
}
function tupleType(dataTypes: Schema.DataType[]): Schema.DataType {
return { tag: "TupleType", Item: dataTypes };
}
class SerializerService {
private dict: Dict>;
private registry: TypeRegistry;
constructor() {
this.dict = new Dict>();
this.registry = new TypeRegistry();
}
private add(dt: Schema.DataType, ser: Serializer) {
const key = dataTypeKey(dt);
this.dict.setAt(key, ser);
}
getSerializer(dt: Schema.DataType): Serializer {
const key = dataTypeKey(dt);
return this.dict.at(key);
}
private contains(dt: Schema.DataType) {
const key = dataTypeKey(dt);
return this.dict.containsKey(key);
}
private init() {
this.dict.forEach((k, ser) => {
ser.init(this);
});
}
registerActivators(activators: IActivator[]) {
this.registry.registerActivators(activators);
}
private createVisitor() {
const vis = defaultVisitor();
const add = (dt: Schema.DataType) => {
if (!this.contains(dt)) {
this.add(dt, buildDataTypeSerializer(dt));
}
}
vis.visitDataType = dt => {
if (dt.tag !== "TypeReference") {
add(dt);
}
};
vis.visitRecord = r => {
const dt = typeReference(r.RecordName);
this.add(dt, new RecordSerializer(r, this.registry));
};
vis.visitUnion = u => {
const dt = typeReference(u.UnionName);
this.add(dt, new UnionSerializer(u, this.registry));
};
vis.visitEnum = e => {
const dt = typeReference(e.EnumName);
this.add(dt, new EnumSerializer());
};
return vis;
}
registerTypes(types: Schema.TypeDefinition[]) {
visitTypes(types, this.createVisitor());
this.init();
}
registerServiceMethods(methods: Schema.Method[]) {
visitServiceMethods(methods, this.createVisitor());
this.init();
}
}
/** Client for the HTTP transport. */
export class Client {
/** Constructs a client, with an optional URL prefix. */
constructor(public httpClient: IHttpClient = new FetchClient(), public prefix = "/gluon-api") {
}
}
/** Proxies a remote method. */
export interface RemoteMethod {
(client: Client): T;
}
export interface IHttpClient {
httpGet(url: string, queryParams: {[key:string]: string}, parseJsonResponse: (json: any) => T): Promise