json-ts
Version:
Automatically generate Typescript Definition files or Flow types from JSON input
244 lines (205 loc) • 8.69 kB
text/typescript
import * as ts from 'typescript';
import {ParsedNode} from "./parser";
import {Set as ImmutableSet} from "immutable";
import needsQuotes = require('needsquotes');
import {JsonTsOptions} from "./index";
import {collapseInterfaces} from "./collapse-interfaces";
import {Node} from "typescript";
const {startCase, toLower} = require('../_');
export const log = (input) => console.log('--\n', JSON.stringify(input, null, 2));
export interface MemberNode {
types: ImmutableSet<string>
members: MemberNode[]
name: string
optional: boolean
}
export interface InterfaceNode {
name: string;
original: string;
members: MemberNode[];
}
export const kindMap = {
[ts.SyntaxKind.NullKeyword]: ts.SyntaxKind.NullKeyword,
[ts.SyntaxKind.StringLiteral]: ts.SyntaxKind.StringKeyword,
[ts.SyntaxKind.FirstLiteralToken]: ts.SyntaxKind.NumberKeyword,
[ts.SyntaxKind.TrueKeyword]: ts.SyntaxKind.BooleanKeyword,
[ts.SyntaxKind.FalseKeyword]: ts.SyntaxKind.BooleanKeyword,
[ts.SyntaxKind.NumericLiteral]: ts.SyntaxKind.NumberKeyword,
};
export function namedProp(member) {
const qs = needsQuotes(member.name);
const output = qs.needsQuotes ? qs.quotedValue : member.name;
const prop: any = ts.createNode(ts.SyntaxKind.PropertySignature);
prop.name = ts.createIdentifier(output);
if (member.optional) {
prop.questionToken = ts.createNode(ts.SyntaxKind.QuestionToken);
}
return prop;
}
const safeUnions = ImmutableSet([
ts.SyntaxKind.TrueKeyword,
ts.SyntaxKind.FalseKeyword,
ts.SyntaxKind.StringLiteral,
ts.SyntaxKind.NumericLiteral,
ts.SyntaxKind.PrefixUnaryExpression,
ts.SyntaxKind.NullKeyword,
]);
export function transform(stack: ParsedNode[], options: JsonTsOptions): InterfaceNode[] {
const wrapper = [{
kind: ts.SyntaxKind.ObjectLiteralExpression,
_kind: 'ObjectLiteralExpression',
name: options.rootName,
interfaceCandidate: true,
body: stack
}];
const interfaces = getInterfaces(wrapper);
return collapseInterfaces(interfaces);
function createOne(node: ParsedNode): InterfaceNode {
const thisMembers = getMembers(node.body);
const item: any = ts.createNode(ts.SyntaxKind.InterfaceDeclaration);
item.name = ts.createIdentifier(newInterfaceName(node));
item.members = ts.createNodeArray(thisMembers, false);
return item;
}
function getInterfaces(nodes: ParsedNode[]): any[] {
return nodes.reduce((acc, node) => {
if (node.kind === ts.SyntaxKind.ObjectLiteralExpression) {
const newInterface = createOne(node);
// const asMap = fromJS(newInterface);
if (node.interfaceCandidate) {
return acc.concat([newInterface], getInterfaces(node.body));
}
return acc.concat(getInterfaces(node.body));
}
if (node.kind === ts.SyntaxKind.ArrayLiteralExpression) {
const decorated = node.body.map(arrayNode => {
arrayNode.name = getArrayItemName(node.name);
return arrayNode;
});
const other = getInterfaces(decorated);
return acc.concat(other);
}
return acc;
}, []);
}
function getMembers(stack: ParsedNode[]) {
const members = stack.map(node => {
switch(node.kind) {
case ts.SyntaxKind.FalseKeyword:
case ts.SyntaxKind.TrueKeyword: {
const item = namedProp({name: node.name});
item.type = ts.createNode(ts.SyntaxKind.BooleanKeyword);
return item;
}
case ts.SyntaxKind.StringLiteral: {
const item = namedProp({name: node.name});
item.type = ts.createNode(ts.SyntaxKind.StringKeyword);
return item;
}
case ts.SyntaxKind.NullKeyword: {
const item = namedProp({name: node.name});
item.type = ts.createNode(ts.SyntaxKind.NullKeyword);
return item;
}
case ts.SyntaxKind.NumericLiteral: {
const item = namedProp({name: node.name});
item.type = ts.createNode(ts.SyntaxKind.NumberKeyword);
return item;
}
case ts.SyntaxKind.ObjectLiteralExpression: {
if (node.interfaceCandidate) {
const item = namedProp({name: node.name});
item.type = ts.createTypeReferenceNode(newInterfaceName(node), undefined);
return item;
} else {
const item = namedProp({name: node.name});
item.type = ts.createTypeLiteralNode(getMembers(node.body));
return item;
}
}
case ts.SyntaxKind.ArrayLiteralExpression: {
if (node.body.length) {
const item = namedProp({name: node.name});
const elements = getArrayElementsType(node);
item.type = ts.createArrayTypeNode(elements);
return item;
} else {
const item = namedProp({name: node.name});
const anyNode: any = ts.createNode(ts.SyntaxKind.AnyKeyword);
item.type = ts.createArrayTypeNode(anyNode);
return item;
}
}
}
});
return members
}
function getArrayElementsType(node: ParsedNode): any {
const kinds = ImmutableSet(node.body.map(x => x.kind));
if (kinds.size === 1) { // if there's only 1 kind in the array, it's safe to use type[];
const kind = kinds.first();
switch(kind) {
case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
case ts.SyntaxKind.NumericLiteral:
return ts.createNode(kindMap[kind]);
case ts.SyntaxKind.ObjectLiteralExpression:
const item = ts.createTypeReferenceNode(getArrayInterfaceItemName(node.name), undefined);
return item;
default: return ts.createNode(ts.SyntaxKind.AnyKeyword);
}
} else if (kinds.size === 2) { // a mix of true/false is still a boolean[];
if (kinds.has(ts.SyntaxKind.TrueKeyword) && kinds.has(ts.SyntaxKind.FalseKeyword)) {
return ts.createNode(ts.SyntaxKind.BooleanKeyword);
}
}
// console.log(node.body);
if (kinds.every(kind => safeUnions.has(kind))) {
// console.log(node.body);
const types = kinds.map(x => {
return ts.createNode(kindMap[x]);
}).toJS();
const item = ts.createNode(ts.SyntaxKind.ParenthesizedType);
(item as any).type = ts.createUnionOrIntersectionTypeNode(ts.SyntaxKind.UnionType, types);
return item;
} else {
// console.log('Not creating union as this array contains mixed complexr types');
}
return ts.createNode(ts.SyntaxKind.AnyKeyword);
}
function newInterfaceName(node: ParsedNode) {
const base = node.name[0].toUpperCase() + node.name.slice(1);
if (options.prefix) {
return options.prefix + base;
}
const qs = needsQuotes(base);
if (qs.needsQuotes) {
return `_` + base;
}
return base;
}
function upper(string) {
return string[0].toUpperCase() + string.slice(1);
}
function pascalCase(input): string {
return startCase(input).replace(/ /g, '');
}
function getArrayInterfaceItemName(input): string {
if (options.prefix) {
return pascalCase(`${options.prefix}_${input}_Item`);
}
const qs = needsQuotes(input);
if (qs.needsQuotes) {
return '_' + pascalCase(`${input}_Item`);
}
return pascalCase(`${input}_Item`)
}
function getArrayItemName(input) {
return pascalCase(`${input}_Item`)
}
// function getArrayInterfaceItemName(input) {
// return pascalCase(`I_${input}_Item`)
// }
}