@@ -10,7 +10,36 @@ import * as tsfmt from "typescript-formatter";
1010import { Case , changeCase } from "./util" ;
1111import { hashSum } from "@hpcc-js/util" ;
1212
13- type JsonObj = { [ name : string ] : any } ;
13+ interface SoapSchema {
14+ elements ?: Record < string , unknown > ;
15+ complexTypes ?: Record < string , unknown > ;
16+ types ?: Record < string , unknown > ;
17+ }
18+
19+ interface WsdlNode {
20+ name ?: string ;
21+ children ?: WsdlNode [ ] ;
22+ $name ?: string ;
23+ $type ?: string ;
24+ $minOccurs ?: string ;
25+ $description ?: string ;
26+ }
27+
28+ interface SoapBinding {
29+ methods : Record < string , {
30+ soapAction : string ;
31+ input : { $name ?: string } ;
32+ output : { $name ?: string } ;
33+ } > ;
34+ }
35+
36+ interface ServiceMethod {
37+ url : string ;
38+ version : string | null ;
39+ name : string ;
40+ input : string ;
41+ output : string ;
42+ }
1443
1544const lines : string [ ] = [ ] ;
1645
@@ -19,8 +48,9 @@ const cwd = process.cwd();
1948const args = minimist ( process . argv . slice ( 2 ) ) ;
2049const keepGoing = args . k === true || args [ "keep-going" ] === true ;
2150
22- const knownTypes : { [ name : string ] : [ string , any ] } = { } ;
23- const parsedTypes : JsonObj = { } ;
51+ const knownTypes : { [ name : string ] : [ string , Record < string , unknown > | undefined ] } = { } ;
52+ const parsedTypes : Record < string , unknown > = { } ;
53+ const parsedOptionals : { [ name : string ] : Set < string > } = { } ;
2454
2555const primitiveMap : { [ key : string ] : string } = {
2656 "int" : "number" ,
@@ -35,11 +65,11 @@ const primitiveMap: { [key: string]: string } = {
3565} ;
3666const knownPrimitives : string [ ] = [ ] ;
3767
38- const parsedEnums : JsonObj = { } ;
68+ const parsedEnums : Record < string , string [ ] > = { } ;
3969
40- const debug = args ?. debug ?? false ;
41- const printToConsole = args ?. print ?? false ;
42- const outDir = args ?. outDir ? args ?. outDir : "./temp/wsdl" ;
70+ const debug : boolean = ! ! args ?. debug ;
71+ const printToConsole : boolean = ! ! args ?. print ;
72+ const outDir : string = args ?. outDir ?? "./temp/wsdl" ;
4373
4474const ignoredWords = [ "targetNSAlias" , "targetNamespace" ] ;
4575
@@ -48,13 +78,39 @@ const tsFmtOpts = {
4878 editorconfig : true , vscode : true , vscodeFile : null , tsfmt : false , tsfmtFile : null
4979} ;
5080
51- function printDbg ( ...args : any [ ] ) {
81+ function printDbg ( ...args : unknown [ ] ) {
5282 if ( debug ) {
5383 console . log ( ...args ) ;
5484 }
5585}
5686
57- function wsdlToTs ( uri : string ) {
87+ /**
88+ * Recursively collect element names from a node-soap schema node that have minOccurs="0".
89+ * Descends into sequence / all / choice / complexContent / extension / restriction.
90+ */
91+ function extractOptionalFieldNames ( node : WsdlNode | undefined ) : Set < string > {
92+ const result = new Set < string > ( ) ;
93+ if ( ! node ?. children ) return result ;
94+ for ( const child of node . children ) {
95+ if ( child . name === "sequence" || child . name === "all" || child . name === "choice" ) {
96+ for ( const el of ( child . children || [ ] ) ) {
97+ if ( el . name === "element" && el . $name && el . $minOccurs === "0" ) {
98+ result . add ( el . $name ) ;
99+ }
100+ }
101+ } else if (
102+ child . name === "complexContent" ||
103+ child . name === "extension" ||
104+ child . name === "restriction" ||
105+ child . name === "complexType"
106+ ) {
107+ extractOptionalFieldNames ( child ) . forEach ( f => result . add ( f ) ) ;
108+ }
109+ }
110+ return result ;
111+ }
112+
113+ function wsdlToTs ( uri : string ) : Promise < [ soap . WSDL , any ] > {
58114 return new Promise < soap . Client > ( ( resolve , reject ) => {
59115 soap . createClient ( uri , { } , ( err , client ) => {
60116 if ( err ) reject ( err ) ;
@@ -87,7 +143,7 @@ if (args.help) {
87143 process . exit ( 0 ) ;
88144}
89145
90- function parseEnum ( enumString : string , enumEl ) {
146+ function parseEnum ( enumString : string , enumEl : WsdlNode ) {
91147 const enumParts = enumString . split ( "|" ) ;
92148 printDbg ( `parsing enum parts ${ enumParts [ 0 ] } ` , enumParts ) ;
93149 return {
@@ -97,8 +153,8 @@ function parseEnum(enumString: string, enumEl) {
97153 const member = v . split ( " " ) . join ( "" ) ;
98154 if ( enumParts [ 1 ] . replace ( / x s d : / , "" ) === "int" ) {
99155 let memberName = "" ;
100- enumEl . children . filter ( el => el . name === "annotation" ) [ 0 ] . children . forEach ( el => {
101- memberName = changeCase ( el . children [ idx ] . $description , Case . PascalCase ) . replace ( / [ , ] / g, "" ) ;
156+ enumEl . children ? .filter ( ( el : WsdlNode ) => el . name === "annotation" ) [ 0 ] . children ? .forEach ( ( el : WsdlNode ) => {
157+ memberName = changeCase ( el . children ?. [ idx ] ? .$description ?? "" , Case . PascalCase ) . replace ( / [ , ] / g, "" ) ;
102158 } ) ;
103159 return `${ memberName } = ${ member } ` ;
104160 }
@@ -107,7 +163,50 @@ function parseEnum(enumString: string, enumEl) {
107163 } ;
108164}
109165
110- function parseTypeDefinition ( operation : JsonObj , opName : string , types , isResponse : boolean ) {
166+ /**
167+ * Look up a type definition node in the WSDL schema. Types can live in
168+ * schema.elements, schema.complexTypes, or schema.types.
169+ */
170+ function schemaLookup ( schema : SoapSchema , name : string ) : WsdlNode {
171+ return ( schema . elements ?. [ name ] ?? schema . complexTypes ?. [ name ] ?? schema . types ?. [ name ] ) as WsdlNode ;
172+ }
173+
174+ /**
175+ * Given a schema node (element or complexType), find the schema info for a child element by name.
176+ * Returns either:
177+ * - { typeName: string } when the child has a $type attribute (e.g. "tns:TimeRange" -> "TimeRange")
178+ * - { inlineNode: WsdlNode } when the child has an inline anonymous complexType definition
179+ * - undefined when not found
180+ */
181+ function findChildElementSchema ( node : WsdlNode , childName : string ) : { typeName : string } | { inlineNode : WsdlNode } | undefined {
182+ if ( ! node ?. children ) return undefined ;
183+ for ( const child of node . children ) {
184+ if ( child . name === "all" || child . name === "sequence" || child . name === "choice" ) {
185+ for ( const el of ( child . children || [ ] ) ) {
186+ if ( el . name === "element" && el . $name === childName ) {
187+ if ( el . $type ) {
188+ return { typeName : el . $type . replace ( / ^ t n s : / , "" ) } ;
189+ }
190+ // Inline anonymous complexType — return the element node itself
191+ // (it contains the complexType as a child)
192+ return { inlineNode : el } ;
193+ }
194+ }
195+ } else if (
196+ child . name === "complexType" ||
197+ child . name === "complexContent" ||
198+ child . name === "extension" ||
199+ child . name === "restriction"
200+ ) {
201+ const result = findChildElementSchema ( child , childName ) ;
202+ if ( result ) return result ;
203+ }
204+ }
205+ return undefined ;
206+ }
207+
208+ function parseTypeDefinition ( operation : Record < string , unknown > , opName : string , schema : SoapSchema , isResponse : boolean , schemaNodeOverride ?: WsdlNode ) : [ string , Record < string , unknown > | undefined ] {
209+ const types = schema . types ?? { } ;
111210 const hashId = hashSum ( { opName, operation } ) ;
112211 if ( knownTypes [ hashId ] ) {
113212 return knownTypes [ hashId ] ;
@@ -118,17 +217,25 @@ function parseTypeDefinition(operation: JsonObj, opName: string, types, isRespon
118217 newPropName = `${ opName } ${ i ++ } ` ;
119218 }
120219 knownTypes [ hashId ] = [ newPropName , undefined ] ;
121- const typeDefn : JsonObj = { } ;
220+ const typeDefn : Record < string , unknown > = { } ;
221+ const parentSchemaNode = schemaNodeOverride ?? schemaLookup ( schema , opName ) ;
122222 printDbg ( `processing ${ opName } ` , operation ) ;
123223 for ( const prop in operation ) {
124224 const propName = ( ! prop . endsWith ( "[]" ) ) ? prop : prop . slice ( 0 , - 2 ) ;
125225 if ( typeof operation [ prop ] === "object" ) {
126- const op = operation [ prop ] ;
226+ const op = operation [ prop ] as Record < string , unknown > ;
127227 const keys = Object . keys ( op ) ;
128228 if ( ! isResponse && keys ?. length === 1 && keys [ 0 ] . indexOf ( "[]" ) >= 0 && Object . values ( op ) [ 0 ] === "xsd:string" ) {
129229 typeDefn [ propName ] = "string[]" ;
130230 } else {
131- const [ newPropName , defn ] = parseTypeDefinition ( op , propName , types , isResponse ) ;
231+ const childSchema = findChildElementSchema ( parentSchemaNode , propName ) ;
232+ let childOverride : WsdlNode | undefined ;
233+ if ( childSchema && "typeName" in childSchema ) {
234+ childOverride = schemaLookup ( schema , childSchema . typeName ) ;
235+ } else if ( childSchema && "inlineNode" in childSchema ) {
236+ childOverride = childSchema . inlineNode ;
237+ }
238+ const [ newPropName ] = parseTypeDefinition ( op , propName , schema , isResponse , childOverride ) ;
132239 if ( prop . endsWith ( "[]" ) ) {
133240 typeDefn [ propName ] = newPropName + "[]" ;
134241 } else {
@@ -137,15 +244,16 @@ function parseTypeDefinition(operation: JsonObj, opName: string, types, isRespon
137244 }
138245 } else {
139246 if ( ignoredWords . indexOf ( prop ) < 0 ) {
140- const primitiveType = operation [ prop ] . replace ( / x s d : / gi, "" ) ;
247+ const propValue = operation [ prop ] as string ;
248+ const primitiveType = propValue . replace ( / x s d : / gi, "" ) ;
141249 if ( prop . indexOf ( "[]" ) > 0 ) {
142250 typeDefn [ prop . slice ( 0 , - 2 ) ] = primitiveType + "[]" ;
143- } else if ( operation [ prop ] . match ( / [ . * \| . * \| . * ] / ) ) {
251+ } else if ( propValue . match ( / [ . * \| . * \| . * ] / ) ) {
144252 // note: the above regex is matching the node soap stringified
145253 // structure of enums, parsed by client.describe(),
146254 // e.g.: SomeEnumIdentifier|xsd:int|1,2,3,4
147- const enumTypeName = operation [ prop ] . split ( "|" ) [ 0 ] ;
148- const { type, enumType, values } = parseEnum ( operation [ prop ] , types [ enumTypeName ] ) ;
255+ const enumTypeName = propValue . split ( "|" ) [ 0 ] ;
256+ const { type, enumType, values } = parseEnum ( propValue , types [ enumTypeName ] as WsdlNode ) ;
149257 parsedEnums [ type ] = values ;
150258 typeDefn [ prop ] = type ;
151259 } else {
@@ -159,14 +267,15 @@ function parseTypeDefinition(operation: JsonObj, opName: string, types, isRespon
159267 }
160268 knownTypes [ hashId ] = [ newPropName , typeDefn ] ;
161269 parsedTypes [ newPropName ] = typeDefn ;
270+ parsedOptionals [ newPropName ] = extractOptionalFieldNames ( parentSchemaNode ) ;
162271 return [ newPropName , typeDefn ] ;
163272 }
164273}
165274
166275wsdlToTs ( args . url )
167276 . then ( clientObjs => {
168277 const [ wsdl , descr ] = clientObjs ;
169- const bindings = wsdl . definitions . bindings ;
278+ const bindings : Record < string , SoapBinding > = wsdl . definitions . bindings ;
170279 const wsdlNS = wsdl . definitions . $targetNamespace ;
171280 let namespace = "" ;
172281 let origNS = "" ;
@@ -180,17 +289,18 @@ wsdlToTs(args.url)
180289 const binding = service [ op ] ;
181290 for ( const svc in binding ) {
182291 const operation = binding [ svc ] ;
183- const types = wsdl . definitions . schemas [ wsdlNS ] . types ;
292+ const schema = wsdl . definitions . schemas [ wsdlNS ] ;
184293 const request = operation [ "input" ] ;
185294 const reqName = bindings [ op ] . methods [ svc ] . input . $name ;
186295 const response = operation [ "output" ] ;
187296 const respName = bindings [ op ] . methods [ svc ] . output . $name ;
188297
189- parseTypeDefinition ( request , reqName , types , false ) ;
190- parseTypeDefinition ( response , respName , types , true ) ;
298+ parseTypeDefinition ( request , String ( reqName ) , schema , false ) ;
299+ parseTypeDefinition ( response , String ( respName ) , schema , true ) ;
191300 }
192301 }
193302 }
303+
194304 lines . push ( "\n" ) ;
195305
196306 lines . push ( `export namespace ${ namespace } {\n` ) ;
@@ -208,14 +318,18 @@ wsdlToTs(args.url)
208318
209319 for ( const type in parsedTypes ) {
210320 lines . push ( `export interface ${ type } {\n` ) ;
211- let typeString = JSON . stringify ( parsedTypes [ type ] , null , 4 ) // convert object to string
212- . replace ( / " / g, "" ) // remove double-quotes from JSON keys & values
213- . replace ( / , ? \n / g, ";\n" ) // replace comma delimiters with semi-colons
214- . replace ( / \{ ; / g, "{" ) ; // correct lines where ; added erroneously
215-
216- if ( type . endsWith ( "Request" ) ) {
217- typeString = typeString . replace ( / : / g, "?:" ) ; // make request properties optional
218- }
321+ const optionalSet = parsedOptionals [ type ] ?? new Set < string > ( ) ;
322+ let typeString = JSON . stringify ( parsedTypes [ type ] , null , 4 ) // convert object to string
323+ . replace ( / " / g, "" ) // remove double-quotes from JSON keys & values
324+ . replace ( / , ? \n / g, ";\n" ) // replace comma delimiters with semi-colons
325+ . replace ( / \{ ; / g, "{" ) // correct lines where ; added erroneously
326+ . split ( "\n" ) . map ( line => { // mark minOccurs="0" fields as optional
327+ const match = line . match ( / ^ ( \s + ) ( \w + ) : / ) ;
328+ if ( match && optionalSet . has ( match [ 2 ] ) ) {
329+ return line . replace ( match [ 2 ] + ":" , match [ 2 ] + "?:" ) ;
330+ }
331+ return line ;
332+ } ) . join ( "\n" ) ;
219333 lines . push ( typeString . substring ( 1 , typeString . length - 1 ) + "\n" ) ;
220334 lines . push ( "}\n" ) ;
221335 }
@@ -226,7 +340,7 @@ wsdlToTs(args.url)
226340
227341 lines . push ( `export class ${ namespace . replace ( "Ws" , "" ) } ServiceBase extends Service {\n` ) ;
228342
229- const methods : JsonObj = [ ] ;
343+ const methods : ServiceMethod [ ] = [ ] ;
230344
231345 for ( const service in bindings ) {
232346 const binding = bindings [ service ] ;
@@ -240,8 +354,8 @@ wsdlToTs(args.url)
240354 url : soapAction ,
241355 version : new URL ( url ) . searchParams . get ( "ver_" ) ,
242356 name : method ,
243- input : inputName ,
244- output : outputName
357+ input : String ( inputName ) ,
358+ output : String ( outputName )
245359 } ) ;
246360 }
247361 }
@@ -260,8 +374,8 @@ wsdlToTs(args.url)
260374 lines . push ( "\n\n" ) ;
261375
262376 methods . forEach ( method => {
263- lines . push ( `${ method . name } (request: Partial<${ namespace } .${ method . input } >): Promise<${ namespace } .${ method . output } > {` ) ;
264- lines . push ( `\treturn this._connection.send("${ method . name } ", request, "json", false, undefined , "${ method . output } ");` ) ;
377+ lines . push ( `${ method . name } (request: Partial<${ namespace } .${ method . input } >, abortSignal?: AbortSignal ): Promise<${ namespace } .${ method . output } > {` ) ;
378+ lines . push ( `\treturn this._connection.send("${ method . name } ", request, "json", false, abortSignal , "${ method . output } ");` ) ;
265379 lines . push ( "}\n" ) ;
266380 } ) ;
267381 }
0 commit comments