Skip to content

Commit

Permalink
feat(utils): Wsdl to TS typings
Browse files Browse the repository at this point in the history
Signed-off-by: Jeremy Clements <[email protected]>
  • Loading branch information
jeclrsg committed Jan 10, 2022
1 parent 8554698 commit 11c8bd2
Show file tree
Hide file tree
Showing 7 changed files with 330 additions and 1 deletion.
3 changes: 2 additions & 1 deletion packages/comms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"lint": "eslint src/**/*.ts",
"lint-fix": "eslint --fix src/**/*.ts",
"test": "./node_modules/.bin/mocha lib-umd/__tests__ --reporter spec",
"docs": "typedoc --options tdoptions.json ."
"docs": "typedoc --options tdoptions.json .",
"wsdl-to-ts": "node ./utils/wsdl-to-ts/esm/index.js"
},
"dependencies": {
"@hpcc-js/ddl-shim": "^2.17.19",
Expand Down
3 changes: 3 additions & 0 deletions packages/comms/utils/wsdl-to-ts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package-lock.json
wsdl
esm
36 changes: 36 additions & 0 deletions packages/comms/utils/wsdl-to-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## wsdl-to-ts

A command line tool for generating TypeScript types from a WSDL definition.

## Usage

From root of the hpcc-js comms package `.../hpcc-js/packages/comms/` navigate to `./utils/wsdl-to-ts` and execute `npm run build`.

Then to use the tool, there is an npm script defined in the comms package.json:

```
npm run wsdl-to-ts
```

Produces the output:

```
No WSDL Url provided.
Usage: npm run wsdl-to-ts -- --url=someUrl
Available flags:
====================
--url=someUrl A URL for a WSDL to be converted to TypeScript interfaces
--outDir=./some/path The directory into which the generated TS interfaces will be written (defaults to "./wsdl").
--print Rather than writing files, print the generated TS interfaces to the CLI
```

Specifiying a url:

```
npm run wsdl-to-ts -- --url=http://play.hpccsystems.com:8010/Ws_Account/?wsdl
```

Would result in the creation of `./wsdl/WsAccount.ts`, containing TypeScript interfaces, enums and potentially primitives, eg. TS type wrappers for things like `int`, `long`, etc.

20 changes: 20 additions & 0 deletions packages/comms/utils/wsdl-to-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"private": true,
"name": "@hpcc-js/wsdl-to-ts",
"version": "0.0.1",
"description": "Convert ESP WSDLs to TypeScript",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npx tsc -p tsconfig.json --outDir esm"
},
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"minimist": "^1.2.5",
"soap": "^0.43.0",
"typescript": "^4.5.3"
}
}
197 changes: 197 additions & 0 deletions packages/comms/utils/wsdl-to-ts/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
"use strict";

import { mkdirp, writeFile } from "fs-extra";
import * as path from "path";
import * as soap from "soap";
import minimist from "minimist";

import { Case, changeCase } from "./util";

type JsonObj = { [name: string]: any };

const lines: string[] = [];

const args = minimist(process.argv.slice(2));

const knownTypes: string[] = [];
const parsedTypes: JsonObj = {};

const primitiveMap: { [key: string]: string } = {
"int": "number",
"integer": "number",
"unsignedInt": "number",
"nonNegativeInteger": "number",
"long": "number",
"double": "number",
"base64Binary": "number[]",
"dateTime": "string",
}
const knownPrimitives: string[] = [];

const parsedEnums: JsonObj = {};

const debug = args?.debug ?? false;
const printToConsole = args?.print ?? false;
const outDir = args?.outDir ? "./utils/wsdl-to-ts/wsdl/" + args?.outDir : "./utils/wsdl-to-ts/wsdl/";

const ignoredWords = ["targetNSAlias", "targetNamespace"];

function printDbg(...args: any[]) {
if (debug) {
console.log(...args);
}
}

function wsdlToTs(uri: string): Promise<any> {
return new Promise<soap.Client>((resolve, reject) => {
soap.createClient(uri, {}, (err, client) => {
if (err) reject(err);
resolve(client);
});
}).then(client => {
const wsdlDescr = client.describe();
return wsdlDescr;
});
}

function printUsage() {
console.log("Usage: npm run wsdl-to-ts -- --url=someUrl\n");
console.log("Available flags: ");
console.log("====================");
console.log("--url=someUrl\t\t\tA URL for a WSDL to be converted to TypeScript interfaces");
console.log("--outDir=./some/path\t\tThe directory into which the generated TS interfaces will be written (defaults to \"./wsdl\").");
console.log("--print\t\t\t\tRather than writing files, print the generated TS interfaces to the CLI");
}

if (!args.url) {
console.error("No WSDL Url provided.\n");
printUsage();
process.exit(0);
}

if (args.help) {
printUsage();
process.exit(0);
}

function parseEnum(enumString: string) {
const enumParts = enumString.split("|");
return {
type: enumParts[0],
enumType: enumParts[1].replace(/xsd:/, ""),
values: enumParts[2].split(",").map(v => {
const member = v.split(" ").map(w => changeCase(w, Case.PascalCase)).join("");
return `${member} = "${member}"`;
})
};
}

function parseTypeDefinition(operation: JsonObj, opName: string) {

const typeDefn: JsonObj = {};
printDbg(`processing ${opName}`, operation);
for (const prop in operation) {
const propName = (prop.indexOf("[]") < 0) ? prop : prop.slice(0, -2);
if (typeof operation[prop] === "object") {
const op = operation[prop];
if (knownTypes.indexOf(propName) < 0) {
knownTypes.push(propName);
const defn = parseTypeDefinition(op, propName);
if (prop.indexOf("[]") > -1) {
typeDefn[propName] = prop;
} else {
typeDefn[propName] = defn;
}
parsedTypes[propName] = defn;
} else {
typeDefn[propName] = prop;
}

} else {
if (ignoredWords.indexOf(prop) < 0) {
const primitiveType = operation[prop].replace(/xsd:/gi, "");
if (prop.indexOf("[]") > 0) {
typeDefn[prop.slice(0, -2)] = primitiveType + "[]";
} else if (operation[prop].match(/.*\|.*\|.*/)) {
const { type, enumType, values } = parseEnum(operation[prop]);
parsedEnums[type] = values;
typeDefn[prop] = type;
} else {
typeDefn[prop] = primitiveType;
}
if (Object.keys(primitiveMap).indexOf(primitiveType) > -1 && knownPrimitives.indexOf(primitiveType) < 0) {
knownPrimitives.push(primitiveType);
}
}
}
}

if (knownTypes.indexOf(opName) < 0) {
knownTypes.push(opName);
parsedTypes[opName] = typeDefn;
}
return typeDefn;
}

wsdlToTs(args.url)
.then(descr => {
let namespace = "";
for (const ns in descr) {
namespace = changeCase(ns, Case.PascalCase);
const service = descr[ns];
printDbg("namespace: ", namespace, "\n");
for (const op in service) {
printDbg("binding: ", changeCase(op, Case.PascalCase), "\n");
const binding = service[op];
for (const svc in binding) {
const operation = binding[svc];
const request = operation["input"];
const reqName = svc + "Request";
const response = operation["output"];
const respName = svc + "Response";

parseTypeDefinition(request, reqName);
parseTypeDefinition(response, respName);
}
}
}

knownPrimitives.forEach(primitive => {
printDbg(`${primitive} = ${primitiveMap[primitive]}`);
lines.push(`type ${primitive} = ${primitiveMap[primitive]};`);
});
lines.push("\n\n");

for (const name in parsedEnums) {
lines.push(`export enum ${name} {\n\n`);
lines.push(parsedEnums[name].join(",\n"));
lines.push("\n\n}");
lines.push("\n\n");
}

lines.push(`export namespace ${namespace} {\n`);

for (const type in parsedTypes) {
lines.push(`export interface ${type} {\n`);
const typeString = JSON.stringify(parsedTypes[type], null, 4).replace(/"/g, "");
lines.push(typeString.substring(1, typeString.length - 1) + "\n");
// lines.push(parsedTypes[type]);
lines.push("}\n");
}

lines.push("}\n");

if (printToConsole) {
console.log(lines.join("\n").replace(/\n\n\n/g, "\n"));
} else {
mkdirp(outDir).then(() => {
const tsFile = path.join(outDir, namespace + ".ts");
writeFile(tsFile, lines.join("\n").replace(/\n\n\n/g, "\n"), (err) => {
if (err) throw err;
})
})
}
}).catch(err => {
console.error(err);
process.exitCode = -1;
});
49 changes: 49 additions & 0 deletions packages/comms/utils/wsdl-to-ts/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export enum Case {
CamelCase,
PascalCase,
SnakeCase
}

function splitWords(input: string) {
/* regex from lodash words() function */
const reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;
return input.match(reAsciiWord) || [];
}

function capitalizeWord(input: string) {
return input.charAt(0).toUpperCase() + input.substring(1);
}

export function changeCase(input: string, toCase: Case) {
let output = input;
let convertString;
switch (toCase) {
case Case.PascalCase:
convertString = (_in: string) => {
const words = splitWords(_in).map(w => {
return capitalizeWord(w);
}) || [];
return words.join("");
};
break;
case Case.CamelCase:
convertString = (_in: string) => {
const words = splitWords(_in).map((w, idx) => {
if (idx === 0) return w;
return capitalizeWord(w);
}) || [];
return words.join("");
}
break;
case Case.SnakeCase:
convertString = (_in: string) => {
return splitWords(_in)
.map(w => w.toLowerCase())
.join("_");
}
}
if (typeof convertString === "function") {
output = convertString(input);
}
return output;
}
23 changes: 23 additions & 0 deletions packages/comms/utils/wsdl-to-ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": [
"es2016"
],
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"outDir": "./lib", /* Specify an output folder for all emitted files. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"node_modules",
"lib",
"esm",
"wsdl"
]
}

0 comments on commit 11c8bd2

Please sign in to comment.