Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
265 changes: 265 additions & 0 deletions packages/comms/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
"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`);
let 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

if (type.endsWith("Request")) {
typeString = typeString.replace(/:/g, "?:"); // make request 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;
}