-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Jeremy Clements <[email protected]>
- Loading branch information
Showing
7 changed files
with
330 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package-lock.json | ||
wsdl | ||
esm |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |