From 751ca0a442f2023cbf69fd3ac7db2fc0227fedae Mon Sep 17 00:00:00 2001 From: Maksim Terentev Date: Thu, 4 Jun 2026 11:25:53 -0700 Subject: [PATCH] Adds support for cases. That PR fixes behavior. Before that, the WSDL marshaller could resolve this only as two (or more) separate type lists. The following JSON object has two separate lists of As and Bs: Assuming we have the following definitions of some Document: <> // Old behavior { // Parent document JSON representation someOtherProperty: { ... SomeOtherType-type object ... }, a: [ { ... A-type object 1 ... }, { ... A-type object 2 ... }, ...], b: [ { ... B-type object 1 ... }, { ... B-type object 2 ... }, { ... B-type object 3 ... }, ...], } that resulted into: <> ... SomeOtherType properties ... ... A-type properties 1 ... ... A-type properties 2 ... ... B-type properties 1 ... ... B-type properties 2 ... ... B-type properties 3 ... In case we want to preserve the choice children order for the following JSON object: // New behavior { // Parent document JSON representation someOtherProperty: { ... SomeOtherType-type object ... }, '$sequence': [ // <- Note this special transparent key that wraps a flat array object a: { ... A-type object 1 ... }, b: { ... B-type object 1 ... }, b: { ... B-type object 2 ... }, a: { ... A-type object 2 ... }, b: { ... B-type object 3 ... }, ] } to be encoded as: <> ... SomeOtherType properties ... ... A-type properties 1 ... ... B-type properties 1 ... ... B-type properties 2 ... ... A-type properties 2 ... ... B-type properties 3 ... In case of interfere with $sequence property of some SOAP Service property, the special key name can be replaced with options arrayWithChoiceTag: string value. --- src/types.ts | 2 + src/wsdl/index.ts | 46 ++++---- test/wsdl-parse-test.js | 147 ++++++++++++++++++++++++++ test/wsdl/complex/mixed-sequence.wsdl | 127 ++++++++++++++++++++++ 4 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 test/wsdl/complex/mixed-sequence.wsdl diff --git a/src/types.ts b/src/types.ts index 896a0bf08..80396ab8b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -99,6 +99,8 @@ export interface IWsdlBaseOptions { preserveWhitespace?: boolean; /** provides support for nonstandard array semantics. If true, JSON arrays of the form {list: [{elem: 1}, {elem: 2}]} are marshalled into xml as 1 2. If false, marshalls into 1 2 . Default: true. */ namespaceArrayElements?: boolean; + /** provides support for array with choice semantics. If array key matches, JSON arrays of the form {$sequence: [{elem: 1}, {elem: 2}]} are marshalled into xml as 12. Default: $sequence. */ + arrayWithChoiceTag?: string; useEmptyTag?: boolean; strict?: boolean; /** custom HTTP headers to be sent on WSDL requests. */ diff --git a/src/wsdl/index.ts b/src/wsdl/index.ts index 68fc61705..f80ee00a2 100644 --- a/src/wsdl/index.ts +++ b/src/wsdl/index.ts @@ -750,31 +750,36 @@ export class WSDL { for (i = 0, n = obj.length; i < n; i++) { const item = obj[i]; - const arrayAttr = this.processAttributes(item, nsContext); + const isArrayWithChoiceTagContainer = name === this.options.arrayWithChoiceTag; + const arrayAttr = isArrayWithChoiceTagContainer ? '' : this.processAttributes(item, nsContext); const correctOuterNsPrefix = nonSubNameSpace || parentNsPrefix || ns; // using the parent namespace prefix if given const body = this.objectToXML(item, name, nsPrefix, nsURI, false, null, schemaObject, nsContext); - let openingTagParts = ['<', name, arrayAttr, xmlnsAttrib]; - if (!emptyNonSubNameSpaceForArray) { - openingTagParts = ['<', appendColon(correctOuterNsPrefix), name, arrayAttr, xmlnsAttrib]; - } - - if (body === '' && this.options.useEmptyTag) { - // Use empty (self-closing) tags if no contents - openingTagParts.push(' />'); - parts.push(openingTagParts.join('')); + if (isArrayWithChoiceTagContainer) { + parts.push(body); } else { - openingTagParts.push('>'); - if (this.options.namespaceArrayElements || i === 0) { - parts.push(openingTagParts.join('')); + let openingTagParts = ['<', name, arrayAttr, xmlnsAttrib]; + if (!emptyNonSubNameSpaceForArray) { + openingTagParts = ['<', appendColon(correctOuterNsPrefix), name, arrayAttr, xmlnsAttrib]; } - parts.push(body); - if (this.options.namespaceArrayElements || i === n - 1) { - if (emptyNonSubNameSpaceForArray) { - parts.push([''].join('')); - } else { - parts.push([''].join('')); + + if (body === '' && this.options.useEmptyTag) { + // Use empty (self-closing) tags if no contents + openingTagParts.push(' />'); + parts.push(openingTagParts.join('')); + } else { + openingTagParts.push('>'); + if (this.options.namespaceArrayElements || i === 0) { + parts.push(openingTagParts.join('')); + } + parts.push(body); + if (this.options.namespaceArrayElements || i === n - 1) { + if (emptyNonSubNameSpaceForArray) { + parts.push([''].join('')); + } else { + parts.push([''].join('')); + } } } } @@ -939,7 +944,7 @@ export class WSDL { } } - value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, null, nsContext); + value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, name === this.options.arrayWithChoiceTag ? schemaObject : null, nsContext); } } else { value = this.objectToXML(child, name, nsPrefix, nsURI, false, null, null, nsContext); @@ -1178,6 +1183,7 @@ export class WSDL { } else { this.options.namespaceArrayElements = true; } + this.options.arrayWithChoiceTag = options.arrayWithChoiceTag || '$sequence'; // Allow any request headers to keep passing through this.options.wsdl_headers = options.wsdl_headers; diff --git a/test/wsdl-parse-test.js b/test/wsdl-parse-test.js index 87709f26f..d46fd0930 100644 --- a/test/wsdl-parse-test.js +++ b/test/wsdl-parse-test.js @@ -144,4 +144,151 @@ describe(__filename, function () { done(); }); }); + + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with custom $sequence', function (done) { + open_wsdl( + path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), + { + arrayWithChoiceTag: '$arrayChoice', + }, + function (err, def) { + if (err) { + return done(err); + } + + if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { + return done('Unable to find "getDataResponse" complex type'); + } + + var requestBody = { + getDataResult: { + a: 0, + b: 10, + $arrayChoice: [ + { + c: { + id: '1', + value1: 'test 1', + }, + }, + { + d: { + id: '3', + value2: 'test 3', + }, + }, + { + c: { + id: '2', + value1: 'test 2', + }, + }, + ], + }, + }; + + var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); + + /** + * Expected XML: + * + * + * 0 + * 10 + * + * 1 + * test 1 + * + * + * 3 + * test 3 + * + * + * 2 + * test 2 + * + * + * + */ + assert.strictEqual( + requestAsXML, + '0101test 13test 32test 2', + ); + + done(); + }, + ); + }); + + it('should parse complex wsdls with mixed choice and minOccurs maxOccurs with default $sequence', function (done) { + open_wsdl( + path.resolve(__dirname, 'wsdl/complex/mixed-sequence.wsdl'), + function (err, def) { + if (err) { + return done(err); + } + + if (null === def.findSchemaType('getDataResponse', 'http://test-soap.com/api/mixedsequence')) { + return done('Unable to find "getDataResponse" complex type'); + } + + var requestBody = { + getDataResult: { + a: 0, + b: 10, + $sequence: [ + { + c: { + id: '1', + value1: 'test 1', + }, + }, + { + d: { + id: '3', + value2: 'test 3', + }, + }, + { + c: { + id: '2', + value1: 'test 2', + }, + }, + ], + }, + }; + + var requestAsXML = def.objectToDocumentXML('getDataResponse', requestBody, 'acme', 'http://test-soap.com/api/mixedsequence', 'getDataResponse'); + + /** + * Expected XML: + * + * + * 0 + * 10 + * + * 1 + * test 1 + * + * + * 3 + * test 3 + * + * + * 2 + * test 2 + * + * + * + */ + assert.strictEqual( + requestAsXML, + '0101test 13test 32test 2', + ); + + done(); + }, + ); + }); }); diff --git a/test/wsdl/complex/mixed-sequence.wsdl b/test/wsdl/complex/mixed-sequence.wsdl new file mode 100644 index 000000000..bcae576d8 --- /dev/null +++ b/test/wsdl/complex/mixed-sequence.wsdl @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +