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 Feb 22, 2022
1 parent 11a8f4b commit 12dd607
Show file tree
Hide file tree
Showing 4 changed files with 327 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ types/
types-3.4/
packages/**/package-lock.json
packages/common/font-awesome
packages/comms/temp
packages/ddl-shim/src/ddlSchema.*
packages/ddl-shim/src/ddl2Schema.*
tests/**/package-lock.json
Expand Down
16 changes: 15 additions & 1 deletion packages/comms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"compile-es6-watch": "npm run compile-es6 -- -w",
"compile-umd": "tsc --module umd --outDir ./lib-umd",
"compile-umd-watch": "npm run compile-umd -- -w",
"compile-util": "tsc --esModuleInterop --target es2019 --skipLibCheck --module commonjs --outDir ./lib-cjs ./utils/index.ts",
"bundle": "rollup -c",
"bundle-watch": "npm run bundle -- -w",
"minimize-browser": "terser dist/index.js -c -m --source-map \"content='dist/index.js.map',url='index.min.js.map'\" -o dist/index.min.js",
Expand All @@ -35,7 +36,19 @@
"lint-fix": "eslint --fix src/**/*.ts",
"test": "./node_modules/.bin/mocha lib-umd/__tests__ --reporter spec",
"docs": "typedoc --options tdoptions.json .",
"update": "npx npm-check-updates -u -t minor"
"update": "npx npm-check-updates -u -t minor",
"wsdl-access": "node ./lib-cjs/index.js --url=http://localhost:8010/ws_access?wsdl --outDir=./src/services/wsdl",
"wsdl-account": "node ./lib-cjs/index.js --url=http://localhost:8010/Ws_Account?wsdl --outDir=./src/services/wsdl",
"wsdl-codesign": "node ./lib-cjs/index.js --url=http://localhost:8010/ws_codesign?wsdl --outDir=./src/services/wsdl",
"wsdl-dfu": "node ./lib-cjs/index.js --url=http://localhost:8010/WsDfu?wsdl --outDir=./src/services/wsdl",
"wsdl-dfuxref": "node ./lib-cjs/index.js --url=http://localhost:8010/WsDFUXRef?wsdl --outDir=./src/services/wsdl",
"wsdl-logaccess": "node ./lib-cjs/index.js --url=http://localhost:8010/Ws_logaccess?wsdl --outDir=./src/services/wsdl",
"wsdl-machine": "node ./lib-cjs/index.js --url=http://localhost:8010/ws_machine?wsdl --outDir=./src/services/wsdl",
"wsdl-smc": "node ./lib-cjs/index.js --url=http://localhost:8010/WsSMC?wsdl --outDir=./src/services/wsdl",
"wsdl-store": "node ./lib-cjs/index.js --url=http://localhost:8010/WsStore?wsdl --outDir=./src/services/wsdl",
"wsdl-topology": "node ./lib-cjs/index.js --url=http://localhost:8010/WsTopology?wsdl --outDir=./src/services/wsdl",
"wsdl-workunits": "node ./lib-cjs/index.js --url=http://localhost:8010/WsWorkunits?wsdl --outDir=./src/services/wsdl",
"wsdl": "npm-run-all --serial compile-util --parallel wsdl-account wsdl-codesign wsdl-dfu wsdl-dfuxref wsdl-machine wsdl-smc wsdl-store wsdl-topology wsdl-workunits"
},
"dependencies": {
"@hpcc-js/ddl-shim": "^2.17.24",
Expand Down Expand Up @@ -71,6 +84,7 @@
"rollup": "2.10.7",
"rollup-plugin-postcss": "3.1.1",
"rollup-plugin-sourcemaps": "0.6.2",
"soap": "0.43.0",
"terser": "4.0.0",
"tslib": "2.3.0",
"typedoc": "0.14.2",
Expand Down
262 changes: 262 additions & 0 deletions packages/comms/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
"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 cwd = process.cwd();

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 ? args?.outDir : "./temp/wsdl";

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

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

function wsdlToTs(uri: string) {
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 [client.wsdl, wsdlDescr];
});
}

function printUsage() {
console.log("Usage: node ./lib-cjs/index.ts --uri=someUri\n");
console.log("Available flags: ");
console.log("====================");
console.log("--uri=someUri\t\t\tA URI for a WSDL to be converted to TypeScript interfaces (either URL or /path/to/file)");
console.log("--outDir=./some/path\t\tThe directory into which the generated TS interfaces will be written (defaults to \"./temp/wsdl/{version}/\").");
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 URI provided.\n");
printUsage();
process.exit(0);
}

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

function parseEnum(enumString: string, enumEl) {
const enumParts = enumString.split("|");
printDbg(`parsing enum parts ${enumParts[0]}`, enumParts);
return {
type: enumParts[0],
enumType: enumParts[1].replace(/xsd:/, ""),
values: enumParts[2].split(",").map((v, idx) => {
const member = v.split(" ").map(w => changeCase(w, Case.PascalCase)).join("");
if (enumParts[1].replace(/xsd:/, "") === "int") {
let memberName = "";
enumEl.children.filter(el => el.name === "annotation")[0].children.forEach(el => {
memberName = changeCase(el.children[idx].$description, Case.PascalCase).replace(/ /g, "");
});
return `${memberName} = ${member}`;
}
return `${member} = "${member}"`;
})
};
}

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

const typeDefn: JsonObj = {};
printDbg(`processing ${opName}`, operation);
for (const prop in operation) {
const propName = (!prop.endsWith("[]")) ? 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, types);
if (prop.endsWith("[]")) {
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(/[.*\|.*\|.*]/)) {
// note: the above regex is matching the node soap stringified
// structure of enums, parsed by client.describe(),
// e.g.: SomeEnumIdentifier|xsd:int|1,2,3,4
const enumTypeName = operation[prop].split("|")[0]
const { type, enumType, values } = parseEnum(operation[prop], types[enumTypeName]);
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(clientObjs => {
const [wsdl, descr] = clientObjs;
const bindings = wsdl.definitions.bindings;
const wsdlNS = wsdl.definitions.$targetNamespace;
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 types = wsdl.definitions.schemas[wsdlNS].types;
const request = operation["input"];
const reqName = bindings[op].methods[svc].input.$name;
const response = operation["output"];
const respName = bindings[op].methods[svc].output.$name;

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

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

for (const name in parsedEnums) {
lines.push(`export enum ${name} {
${parsedEnums[name].join(",\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) // convert object to string
.replace(/"/g, "") // remove double-quotes from JSON keys & values
.replace(/,?\n/g, ";\n") // replace comma delimiters with semi-colons
.replace(/\{;/g, "{") // correct lines where ; added erroneously
.replace(/:/g, "?:"); // make properties optional
lines.push(typeString.substring(1, typeString.length - 1) + "\n");
lines.push("}\n");
}

lines.push("}");

lines.push("\n\n");

lines.push(`export class ${namespace.replace("Ws", "")}Service extends Service {\n`);

const methods = [];

for (const service in bindings) {
const binding = bindings[service];
for (const method in binding.methods) {
const soapAction = binding.methods[method].soapAction;
// a domain name is required by Node's URL object for parsing searchParams, etc
const url = `https://example.org/${soapAction}`;
const inputName = binding.methods[method].input["$name"];
const outputName = binding.methods[method].output["$name"];
methods.push({
url: soapAction,
version: new URL(url).searchParams.get("ver_"),
name: method,
input: inputName,
output: outputName
});
}
}

const serviceVersion = `v${methods[0]?.version}` ?? "";
const finalPath = path.join(outDir, namespace, serviceVersion);
const relativePath = path.relative(path.join(cwd, finalPath), path.join(cwd, "./src")).replace(/\\/g, "/");
lines.unshift("\n\n");
lines.unshift(`import { Service } from "${relativePath}/espConnection";`);
lines.unshift(`import { IConnection, IOptions } from "${relativePath}/connection";`);

if (methods.length > 0) {
lines.push("constructor(optsConnection: IOptions | IConnection) {");
lines.push(`super(optsConnection, "${namespace}", "${methods[0].version}");`);
lines.push("}");
lines.push("\n\n");

methods.forEach(method => {
lines.push(`${method.name}(request: ${namespace}.${method.input}): Promise<${namespace}.${method.output}> {`);
lines.push(`\treturn this._connection.send("${method.name}", request);`);
lines.push("}\n");
})
}

lines.push("}\n");

if (printToConsole) {
console.log(lines.join("\n").replace(/\n\n\n/g, "\n"));
} else {
mkdirp(finalPath).then(() => {
const tsFile = path.join(finalPath, 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/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;
}

0 comments on commit 12dd607

Please sign in to comment.