Skip to content

Commit 5148676

Browse files
committed
Initial commit
0 parents  commit 5148676

17 files changed

+1804
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
generated
3+
yarn-error.log

LICENSE

+674
Large diffs are not rendered by default.

README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<img src="images/header.svg" height="250px" />
2+
3+
# Code Generation With TypeScript
4+
5+
These are the slides and the code examples for our [Berlin.JS](https://berlinjs.org) talk "Code Generation With TypeScript".
6+
7+
## Slides
8+
9+
- see [Code Generation with TypeScript.pdf](./slides/Code%20Generation%20with%20TypeScript.pdf)
10+
11+
## Code Examples
12+
13+
- see [code-examples](./code-examples)
14+
15+
## Authors
16+
17+
- Benny Neugebauer ([Twitter](https://twitter.com/bennycode), [GitHub](https://github.com/bennyn))
18+
- Florian Keller ([GitHub](https://github.com/ffflorian))

code-examples/.npmrc

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
registry=https://registry.npmjs.org/
2+
save-exact=true

code-examples/.yarnrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--registry "https://registry.npmjs.org"

code-examples/LICENSE

+674
Large diffs are not rendered by default.

code-examples/README.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Code Generation With TypeScript
2+
3+
These are the code examples for our talk "Code Generation With TypeScript".
4+
5+
## Prerequisites
6+
7+
- [Node.js](https://nodejs.org) >= 12.4.0
8+
- [yarn](https://yarnpkg.com)
9+
10+
## Installation
11+
12+
- Run `yarn`
13+
14+
## Usage
15+
16+
- Run `yarn start:ast` to generate code with a simple AST
17+
- Run `yarn start:hbs` to generate code with Handlebars
18+
- Run `yarn start:ts` to generate code with TypeScript

code-examples/package.json

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"dependencies": {
3+
"@types/node": "13.1.7",
4+
"handlebars": "4.7.2",
5+
"typescript": "3.7.4"
6+
},
7+
"devDependencies": {
8+
"@ffflorian/prettier-config": "0.0.6",
9+
"prettier": "1.19.1",
10+
"ts-node": "8.6.2"
11+
},
12+
"engines": {
13+
"node": ">= 12.4.0"
14+
},
15+
"license": "GPL-3.0",
16+
"main": "index.js",
17+
"name": "code-generation",
18+
"prettier": "@ffflorian/prettier-config",
19+
"private": true,
20+
"scripts": {
21+
"fix": "yarn prettier --write",
22+
"prettier": "prettier \"src/*.{json,ts}\"",
23+
"start:ast": "ts-node src/generate-from-ast.ts",
24+
"start:hbs": "ts-node src/build-with-handlebars.ts",
25+
"start:ts": "ts-node src/build-with-ts.ts",
26+
"test": "yarn prettier --list-different && tsc --noEmit"
27+
},
28+
"version": "1.0.0"
29+
}
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Handlebars from 'handlebars';
2+
3+
import jsonContent from './swagger.json';
4+
import {writeFile} from './fileUtil';
5+
6+
type InterfaceDeclaration = {isRequired: boolean; name: string; type: string};
7+
type PropertiesSchema = Record<string, {type: string} | undefined>;
8+
9+
interface TypeDeclaration {
10+
basicType: string;
11+
name: string;
12+
properties: InterfaceDeclaration[];
13+
}
14+
15+
const handlebarsTemplate = `{{{basicType}}} {{{name}}} {
16+
{{#each properties}}
17+
{{{this.name}}}{{#if this.isRequired}}{{else}}?{{/if}}: {{{this.type}}};
18+
{{/each}}
19+
}`;
20+
21+
function mapSwaggerTypeToKeyword(type: string): string {
22+
switch (type) {
23+
case 'integer':
24+
case 'number':
25+
return 'number';
26+
case 'string':
27+
return 'string';
28+
default:
29+
return 'unknown';
30+
}
31+
}
32+
33+
function createDeclaration(
34+
declarationName: string,
35+
propertiesSchema: PropertiesSchema,
36+
requiredProperties: string[]
37+
): TypeDeclaration {
38+
const mappedProperties = Object.entries(propertiesSchema).map(([propertyName, propertySchema]) => {
39+
const typeKeyword = mapSwaggerTypeToKeyword(propertySchema!.type);
40+
const isRequired = requiredProperties.includes(propertyName);
41+
return {name: propertyName, isRequired, type: typeKeyword};
42+
});
43+
44+
return {
45+
basicType: 'interface',
46+
name: declarationName,
47+
properties: mappedProperties,
48+
};
49+
}
50+
51+
function saveDeclarations(declarations: TypeDeclaration[]): Promise<void> {
52+
const fileName = 'interfaces-hbs.ts';
53+
54+
const sourceCode = declarations
55+
.map(declaration => {
56+
const templateDelegate = Handlebars.compile(handlebarsTemplate);
57+
return templateDelegate(declaration);
58+
})
59+
.join('\n\n');
60+
61+
return writeFile(fileName, sourceCode);
62+
}
63+
64+
const declarations = Object.entries(jsonContent.definitions).map(([definitionName, definitionSchema]) =>
65+
createDeclaration(definitionName, definitionSchema.properties, definitionSchema.required)
66+
);
67+
68+
saveDeclarations(declarations).catch(error => console.error(error));

code-examples/src/build-with-ts.ts

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import ts from 'typescript';
2+
3+
import jsonContent from './swagger.json';
4+
import {writeFile} from './fileUtil';
5+
6+
type PropertiesSchema = Record<string, {type: string} | undefined>;
7+
8+
function mapSwaggerTypeToKeyword(swaggerType: string): number {
9+
switch (swaggerType) {
10+
case 'integer':
11+
case 'number':
12+
return ts.SyntaxKind.NumberKeyword;
13+
case 'string':
14+
return ts.SyntaxKind.StringKeyword;
15+
default:
16+
return ts.SyntaxKind.UnknownKeyword;
17+
}
18+
}
19+
20+
function createDeclaration(
21+
declarationName: string,
22+
propertiesSchema: PropertiesSchema,
23+
requiredProperties: string[] = []
24+
): ts.InterfaceDeclaration {
25+
const mappedProperties = Object.entries(propertiesSchema).map(([propertyName, propertySchema]) => {
26+
// e.g. SyntaxKind.StringKeyword
27+
const typeSyntaxKind = mapSwaggerTypeToKeyword(propertySchema!.type);
28+
29+
// e.g. 'string'
30+
const typeKeyword = ts.createKeywordTypeNode(typeSyntaxKind);
31+
32+
// e.g. '?'
33+
const questionMarkToken = requiredProperties.includes(propertyName)
34+
? undefined
35+
: ts.createToken(ts.SyntaxKind.QuestionToken);
36+
37+
return ts.createPropertySignature(
38+
undefined, // readonly?
39+
propertyName, // property name
40+
questionMarkToken, // required?
41+
typeKeyword, // property type
42+
undefined, // expression, e.g. '++'
43+
);
44+
});
45+
46+
return ts.createInterfaceDeclaration(
47+
undefined, // private?
48+
undefined, // readonly?
49+
declarationName, // interface name
50+
undefined, // generics?
51+
undefined, // extends?
52+
mappedProperties, // properties
53+
);
54+
}
55+
56+
function saveDeclarations(declarations: ts.InterfaceDeclaration[]): Promise<void> {
57+
const fileName = 'interfaces-ts.ts';
58+
59+
const sourceFile = ts.createSourceFile(fileName, '', ts.ScriptTarget.ESNext);
60+
const sourceCode = declarations
61+
.map(declaration => ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, sourceFile))
62+
.join('\n\n');
63+
64+
return writeFile(fileName, sourceCode);
65+
}
66+
67+
const declarations = Object.entries(jsonContent.definitions).map(([definitionName, definitionSchema]) =>
68+
createDeclaration(definitionName, definitionSchema.properties, definitionSchema.required)
69+
);
70+
71+
saveDeclarations(declarations).catch(error => console.error(error));

code-examples/src/fileUtil.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {promises as fs} from 'fs';
2+
import * as path from 'path';
3+
4+
export async function writeFile(fileName: string, content: string): Promise<void> {
5+
const dirName = path.join(__dirname, '../generated');
6+
7+
try {
8+
await fs.mkdir(dirName);
9+
} catch (error) {}
10+
11+
await fs.writeFile(path.join(dirName, fileName), content, 'utf-8');
12+
}
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import ts from 'typescript';
2+
3+
const nodes: any = {}; // add your AST here
4+
5+
const sourceFile = ts.createSourceFile('', '', ts.ScriptTarget.ESNext);
6+
const printer = ts.createPrinter();
7+
const sourceCode = printer.printNode(ts.EmitHint.Unspecified, nodes, sourceFile);
8+
9+
console.log(sourceCode);

code-examples/src/swagger.json

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"basePath": "/",
3+
"definitions": {
4+
"Pet": {
5+
"properties": {
6+
"food": {
7+
"type": "string"
8+
},
9+
"id": {
10+
"type": "number"
11+
},
12+
"name": {
13+
"type": "string"
14+
}
15+
},
16+
"required": ["id", "name"],
17+
"type": "object"
18+
},
19+
"Home": {
20+
"properties": {
21+
"costs": {
22+
"type": "number"
23+
},
24+
"rooms": {
25+
"type": "number"
26+
},
27+
"id": {
28+
"type": "number"
29+
}
30+
},
31+
"required": ["id", "name"],
32+
"type": "object"
33+
}
34+
},
35+
"info": {
36+
"description": "This is a sample server Petstore server.",
37+
"title": "Swagger Petstore",
38+
"version": "1.0.3"
39+
},
40+
"paths": {
41+
"/pet/{petId}": {
42+
"get": {
43+
"parameters": [
44+
{
45+
"description": "ID of pet to return",
46+
"in": "path",
47+
"name": "petId",
48+
"required": true,
49+
"type": "integer"
50+
}
51+
],
52+
"produces": ["application/json"],
53+
"responses": {
54+
"200": {
55+
"description": "successful operation",
56+
"schema": {
57+
"$ref": "#/definitions/Pet"
58+
}
59+
}
60+
}
61+
}
62+
}
63+
},
64+
"schemes": ["https"],
65+
"swagger": "2.0",
66+
"tags": []
67+
}

code-examples/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"alwaysStrict": true,
4+
"declaration": true,
5+
"esModuleInterop": true,
6+
"forceConsistentCasingInFileNames": true,
7+
"lib": ["es2018"],
8+
"module": "commonjs",
9+
"moduleResolution": "node",
10+
"noEmitOnError": true,
11+
"noImplicitAny": true,
12+
"noImplicitReturns": true,
13+
"noImplicitThis": true,
14+
"noUnusedLocals": true,
15+
"outDir": "dist",
16+
"resolveJsonModule": true,
17+
"rootDir": "src",
18+
"sourceMap": true,
19+
"strict": true,
20+
"strictFunctionTypes": true,
21+
"target": "es6"
22+
},
23+
"exclude": ["node_modules", "generated"]
24+
}

0 commit comments

Comments
 (0)