diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index d840f79eb92b..4fb4404aacfc 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -13,6 +13,7 @@ dependencies: '@rush-temp/core-lro': 'file:projects/core-lro.tgz' '@rush-temp/core-paging': 'file:projects/core-paging.tgz' '@rush-temp/core-tracing': 'file:projects/core-tracing.tgz' + '@rush-temp/core-xml': 'file:projects/core-xml.tgz' '@rush-temp/cosmos': 'file:projects/cosmos.tgz' '@rush-temp/dev-tool': 'file:projects/dev-tool.tgz' '@rush-temp/eslint-plugin-azure-sdk': 'file:projects/eslint-plugin-azure-sdk.tgz' @@ -818,7 +819,7 @@ packages: integrity: sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== /@types/resolve/0.0.8: dependencies: - '@types/node': 8.10.61 + '@types/node': 10.17.13 dev: false resolution: integrity: sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== @@ -8277,7 +8278,7 @@ packages: dev: false name: '@rush-temp/core-client' resolution: - integrity: sha512-bP3AP2HOPvoVFsGY8BltZY5uoNhd3xheIM1XcJKB37qqYcaSnB85rW69LDv7brKgJxo6IoyUE0o1legUXpb+uw== + integrity: sha512-CtQXB25I+V7aFkvhCoEVdodBnzO0ko4pPXTZbJS8j8JOsSQpJ31mq1zUjZ8LlVO7z8WKSk4nY4s8IGpAcFXBgw== tarball: 'file:projects/core-client.tgz' version: 0.0.0 'file:projects/core-http.tgz': @@ -8523,6 +8524,60 @@ packages: integrity: sha512-DXwjq+nIqEQLgnm1tiM6e+eyySwsWYp0Kdzli3RjPzmxAmD382fIrChGURS7SU8TbXu8YzPJSq5aCAAg+RLYIg== tarball: 'file:projects/core-tracing.tgz' version: 0.0.0 + 'file:projects/core-xml.tgz': + dependencies: + '@microsoft/api-extractor': 7.7.11 + '@rollup/plugin-commonjs': 11.0.2_rollup@1.32.1 + '@rollup/plugin-json': 4.1.0_rollup@1.32.1 + '@rollup/plugin-multi-entry': 3.0.1_rollup@1.32.1 + '@rollup/plugin-node-resolve': 8.1.0_rollup@1.32.1 + '@rollup/plugin-replace': 2.3.3_rollup@1.32.1 + '@types/chai': 4.2.11 + '@types/mocha': 7.0.2 + '@types/node': 8.10.61 + '@types/sinon': 9.0.4 + '@types/xml2js': 0.4.5 + '@typescript-eslint/eslint-plugin': 2.34.0_3787943315ebc5ea524d5c102dc9e452 + '@typescript-eslint/parser': 2.34.0_eslint@6.8.0+typescript@3.9.6 + chai: 4.2.0 + cross-env: 7.0.2 + downlevel-dts: 0.4.0 + eslint: 6.8.0 + eslint-config-prettier: 6.11.0_eslint@6.8.0 + eslint-plugin-no-null: 1.0.2_eslint@6.8.0 + eslint-plugin-no-only-tests: 2.4.0 + eslint-plugin-promise: 4.2.1 + inherits: 2.0.4 + karma: 5.1.0 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.2 + karma-edge-launcher: 0.4.2_karma@5.1.0 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@5.1.0 + karma-junit-reporter: 2.0.1_karma@5.1.0 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@5.1.0 + karma-remap-istanbul: 0.6.0_karma@5.1.0 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + prettier: 1.19.1 + rimraf: 3.0.2 + rollup: 1.32.1 + rollup-plugin-sourcemaps: 0.4.2_rollup@1.32.1 + rollup-plugin-terser: 5.3.0_rollup@1.32.1 + rollup-plugin-visualizer: 4.0.4_rollup@1.32.1 + sinon: 9.0.2 + tslib: 2.0.0 + typescript: 3.9.6 + util: 0.12.3 + xml2js: 0.4.23 + dev: false + name: '@rush-temp/core-xml' + resolution: + integrity: sha512-ailMjJ1lg/5smpH1qPSW4wXRSCofLz95inLHbp1lGaZ1K44jmJM4m0lGVgOMfLFtzCae8uLFJGG5Us8SoITlLg== + tarball: 'file:projects/core-xml.tgz' + version: 0.0.0 'file:projects/cosmos.tgz': dependencies: '@microsoft/api-extractor': 7.7.11 @@ -9961,6 +10016,7 @@ specifiers: '@rush-temp/core-lro': 'file:./projects/core-lro.tgz' '@rush-temp/core-paging': 'file:./projects/core-paging.tgz' '@rush-temp/core-tracing': 'file:./projects/core-tracing.tgz' + '@rush-temp/core-xml': 'file:./projects/core-xml.tgz' '@rush-temp/cosmos': 'file:./projects/cosmos.tgz' '@rush-temp/dev-tool': 'file:./projects/dev-tool.tgz' '@rush-temp/eslint-plugin-azure-sdk': 'file:./projects/eslint-plugin-azure-sdk.tgz' diff --git a/dataplane.code-workspace b/dataplane.code-workspace index 6e6ac10bccc0..59865ba60078 100644 --- a/dataplane.code-workspace +++ b/dataplane.code-workspace @@ -48,6 +48,10 @@ "name": "core-tracing", "path": "sdk\\core\\core-tracing" }, + { + "name": "core-xml", + "path": "sdk\\core\\core-xml" + }, { "name": "cosmos", "path": "sdk\\cosmosdb\\cosmos" diff --git a/rush.json b/rush.json index d1e280ac465f..a6ed7f6b4b0b 100644 --- a/rush.json +++ b/rush.json @@ -397,6 +397,11 @@ "projectFolder": "sdk/core/core-tracing", "versionPolicyName": "core" }, + { + "packageName": "@azure/core-xml", + "projectFolder": "sdk/core/core-xml", + "versionPolicyName": "core" + }, { "packageName": "@azure/cosmos", "projectFolder": "sdk/cosmosdb/cosmos", diff --git a/sdk/core/core-client/package.json b/sdk/core/core-client/package.json index 3eba7cb85e44..32ce9e3a25ac 100644 --- a/sdk/core/core-client/package.json +++ b/sdk/core/core-client/package.json @@ -85,6 +85,7 @@ "tslib": "^2.0.0" }, "devDependencies": { + "@azure/core-xml": "1.0.0-preview.1", "@microsoft/api-extractor": "7.7.11", "@rollup/plugin-commonjs": "11.0.2", "@rollup/plugin-json": "^4.0.0", diff --git a/sdk/core/core-client/test/deserializationPolicy.spec.ts b/sdk/core/core-client/test/deserializationPolicy.spec.ts index 4992964907ac..f758ab3e616f 100644 --- a/sdk/core/core-client/test/deserializationPolicy.spec.ts +++ b/sdk/core/core-client/test/deserializationPolicy.spec.ts @@ -18,6 +18,7 @@ import { SendRequest, RawHttpHeaders } from "@azure/core-https"; +import { parseXML } from "@azure/core-xml"; describe("deserializationPolicy", function() { it(`should not modify a request that has no request body mapper`, async function() { @@ -68,7 +69,7 @@ describe("deserializationPolicy", function() { assert.isUndefined(response.parsedHeaders); }); - it.skip(`with xml response body, application/xml content-type, but no operation spec`, async function() { + it(`with xml response body, application/xml content-type, but no operation spec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "application/xml" }, bodyAsText: `3` @@ -76,13 +77,12 @@ describe("deserializationPolicy", function() { assert.exists(response); assert.isUndefined(response.readableStreamBody); assert.isUndefined(response.blobBody); - assert.isUndefined(response.parsedBody); assert.isUndefined(response.parsedHeaders); assert.strictEqual(response.bodyAsText, `3`); assert.deepEqual(response.parsedBody, { apples: "3" }); }); - it.skip(`with xml response body with child element with attributes and value, application/xml content-type, but no operation spec`, async function() { + it(`with xml response body with child element with attributes and value, application/xml content-type, but no operation spec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "application/xml" }, bodyAsText: `3` @@ -103,7 +103,7 @@ describe("deserializationPolicy", function() { assert.isUndefined(response.parsedHeaders); }); - it.skip(`with xml response body, application/xml content-type, and operation spec for only String value`, async function() { + it(`with xml response body, application/xml content-type, and operation spec for only String value`, async function() { const operationSpec: OperationSpec = { httpMethod: "GET", serializer: createSerializer({}, true), @@ -144,7 +144,7 @@ describe("deserializationPolicy", function() { assert.isUndefined(response.parsedHeaders); }); - it.skip(`with xml response body, application/xml content-type, and operation spec for only number value`, async function() { + it(`with xml response body, application/xml content-type, and operation spec for only number value`, async function() { const operationSpec: OperationSpec = { httpMethod: "GET", serializer: createSerializer({}, true), @@ -180,11 +180,11 @@ describe("deserializationPolicy", function() { assert.isUndefined(response.readableStreamBody); assert.isUndefined(response.blobBody); assert.strictEqual(response.bodyAsText, `3`); - assert.deepEqual(response.parsedBody, { apples: "3" }); + assert.deepEqual(response.parsedBody, { apples: 3 }); assert.isUndefined(response.parsedHeaders); }); - it.skip(`with xml response body, application/xml content-type, and operation spec for only headers`, async function() { + it(`with xml response body, application/xml content-type, and operation spec for only headers`, async function() { const operationSpec: OperationSpec = { httpMethod: "GET", serializer: createSerializer({}, true), @@ -235,7 +235,7 @@ describe("deserializationPolicy", function() { assert.deepEqual(response.parsedBody, { apples: { tasty: "yes" } }); }); - it.skip(`with xml response body, application/atom+xml content-type, but no operation spec`, async function() { + it(`with xml response body, application/atom+xml content-type, but no operation spec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "application/xml" }, bodyAsText: `3` @@ -249,7 +249,7 @@ describe("deserializationPolicy", function() { assert.deepEqual(response.parsedBody, { apples: "3" }); }); - it.skip(`with xml property with attribute and value, application/atom+xml content-type, but no operation spec`, async function() { + it(`with xml property with attribute and value, application/atom+xml content-type, but no operation spec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "application/atom+xml" }, bodyAsText: `3` @@ -270,7 +270,7 @@ describe("deserializationPolicy", function() { }); }); - it.skip(`with xml property with attribute and value, my/weird-xml content-type, but no operation spec`, async function() { + it(`with xml property with attribute and value, my/weird-xml content-type, but no operation spec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "my/weird-xml" }, bodyAsText: `3`, @@ -292,7 +292,7 @@ describe("deserializationPolicy", function() { }); }); - it.skip(`with service bus response body, application/atom+xml content-type, and no operationSpec`, async function() { + it(`with service bus response body, application/atom+xml content-type, and no operationSpec`, async function() { const response = await getDeserializedResponse({ headers: { "content-type": "application/atom+xml;type=entry;charset=utf-8" }, bodyAsText: `https://daschulttest1.servicebus.windows.net/testQueuePath/?api-version=2017-04&enrich=FalsetestQueuePath2018-10-09T19:56:34Z2018-10-09T19:56:35Zdaschulttest1PT1M1024falsefalseP14DfalsePT10M10true00falseActive2018-10-09T19:56:34.903Z2018-10-09T19:56:35.013Z0001-01-01T00:00:00Ztrue00000P10675199DT2H48M5.4775807SfalseAvailablefalse` @@ -444,7 +444,10 @@ async function getDeserializedResponse( xmlContentTypes?: string[]; } = {} ): Promise { - const policy = deserializationPolicy({ expectedContentTypes: { xml: options.xmlContentTypes } }); + const policy = deserializationPolicy({ + expectedContentTypes: { xml: options.xmlContentTypes }, + parseXML + }); const request: OperationRequest = createPipelineRequest({ url: "https://example.com" }); request.additionalInfo = { operationSpec: options.operationSpec diff --git a/sdk/core/core-client/test/serviceClient.spec.ts b/sdk/core/core-client/test/serviceClient.spec.ts index 306deb5a917d..5eb10b91bfd7 100644 --- a/sdk/core/core-client/test/serviceClient.spec.ts +++ b/sdk/core/core-client/test/serviceClient.spec.ts @@ -21,6 +21,7 @@ import { HttpsClient, createPipelineRequest } from "@azure/core-https"; +import { stringifyXML } from "@azure/core-xml"; import { serializeRequestBody } from "../src/serviceClient"; import { getOperationArgumentValueFromParameter } from "../src/operationHelpers"; import { deserializationPolicy } from "../src/deserializationPolicy"; @@ -279,12 +280,13 @@ describe("ServiceClient", function() { }, responses: { 200: {} }, serializer: createSerializer() - } + }, + stringifyXML ); assert.strictEqual(httpRequest.body, "body value"); }); - it.skip("should serialize an XML String request body", () => { + it("should serialize an XML String request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( httpRequest, @@ -306,7 +308,8 @@ describe("ServiceClient", function() { responses: { 200: {} }, serializer: createSerializer(), isXML: true - } + }, + stringifyXML ); assert.strictEqual( httpRequest.body, @@ -314,7 +317,7 @@ describe("ServiceClient", function() { ); }); - it.skip("should serialize an XML ByteArray request body", () => { + it("should serialize an XML ByteArray request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( httpRequest, @@ -336,7 +339,8 @@ describe("ServiceClient", function() { responses: { 200: {} }, serializer: createSerializer(), isXML: true - } + }, + stringifyXML ); assert.strictEqual( httpRequest.body, @@ -344,7 +348,7 @@ describe("ServiceClient", function() { ); }); - it.skip("should serialize an XML Stream request body", () => { + it("should serialize an XML Stream request body", () => { const httpRequest = createPipelineRequest({ url: "https://example.com" }); serializeRequestBody( httpRequest, @@ -366,7 +370,8 @@ describe("ServiceClient", function() { responses: { 200: {} }, serializer: createSerializer(), isXML: true - } + }, + stringifyXML ); assert.strictEqual(httpRequest.body, "body value"); }); diff --git a/sdk/core/core-xml/.eslintrc.json b/sdk/core/core-xml/.eslintrc.json new file mode 100644 index 000000000000..47d9fc35be4a --- /dev/null +++ b/sdk/core/core-xml/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "plugins": ["@azure/azure-sdk"], + "extends": ["plugin:@azure/azure-sdk/azure-sdk-base"] +} diff --git a/sdk/core/core-xml/CHANGELOG.md b/sdk/core/core-xml/CHANGELOG.md new file mode 100644 index 000000000000..b6532e70d7be --- /dev/null +++ b/sdk/core/core-xml/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 1.0.0.preview.1 (UNRELEASED) diff --git a/sdk/core/core-xml/LICENSE b/sdk/core/core-xml/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/core/core-xml/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/core/core-xml/README.md b/sdk/core/core-xml/README.md new file mode 100644 index 000000000000..95e3e1e8e749 --- /dev/null +++ b/sdk/core/core-xml/README.md @@ -0,0 +1,35 @@ +# Azure Core XML client library for JavaScript (Experimental) + +This library is primarily intended to be used in code generated by [AutoRest](https://github.com/Azure/Autorest) and [`autorest.typescript`](https://github.com/Azure/autorest.typescript) for APIs that require parsing XML payloads. + +## Getting started + +### Requirements + +- [Node.js](https://nodejs.org) version > 8.x + +### Installation + +This package is primarily used in generated code and not meant to be consumed directly by end users. + +## Key concepts + +XML parsing is mostly delegated to the browser and `xml2js`. + +## Examples + +Examples can be found in the `samples` folder. + +## Next steps + +See `@azure/core-client` for actual usage. + +## Troubleshooting + +If you run into issues while using this library, please feel free to [file an issue](https://github.com/Azure/azure-sdk-for-js/issues/new). + +## Contributing + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/master/CONTRIBUTING.md) to learn more about how to build and test the code. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Fcore%2Fcore-client%2FREADME.png) diff --git a/sdk/core/core-xml/api-extractor.json b/sdk/core/core-xml/api-extractor.json new file mode 100644 index 000000000000..7b2706ba7fe5 --- /dev/null +++ b/sdk/core/core-xml/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/latest/src/index.d.ts", + "docModel": { + "enabled": false + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/latest/core-xml.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/core/core-xml/karma.conf.js b/sdk/core/core-xml/karma.conf.js new file mode 100644 index 000000000000..6c429c6c876c --- /dev/null +++ b/sdk/core/core-xml/karma.conf.js @@ -0,0 +1,120 @@ +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-remap-istanbul", + "karma-junit-reporter" + ], + + // list of files / patterns to load in the browser + files: [ + // Uncomment the cdn link below for the polyfill service to support IE11 missing features + // Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys + // "https://cdn.polyfill.io/v2/polyfill.js?features=Symbol,Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys|always", + "dist-test/index.browser.js" + ], + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["env"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + "test-browser/index.js": ["coverage"] + }, + + // inject following environment values into browser testing with window.__env__ + // environment values MUST be exported or set with same console running "karma start" + // https://www.npmjs.com/package/karma-env-preprocessor + // EXAMPLE: envPreprocessor: ["ACCOUNT_NAME", "ACCOUNT_SAS"], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "karma-remap-istanbul", "junit"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }] + }, + + remapIstanbulReporter: { + src: "coverage-browser/coverage.json", + reports: { + lcovonly: "coverage-browser/lcov.info", + html: "coverage-browser/html/report", + "text-summary": null, + cobertura: "./coverage-browser/cobertura-coverage.xml" + } + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {} // key value pair of properties to add to the section of the report + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + // 'ChromeHeadless', 'Chrome', 'Firefox', 'Edge', 'IE' + browsers: ["ChromeHeadless"], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000" + } + } + }); +}; diff --git a/sdk/core/core-xml/package.json b/sdk/core/core-xml/package.json new file mode 100644 index 000000000000..558bc296a094 --- /dev/null +++ b/sdk/core/core-xml/package.json @@ -0,0 +1,129 @@ +{ + "name": "@azure/core-xml", + "version": "1.0.0-preview.1", + "private": true, + "description": "Core library for interacting with XML payloads", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "browser": { + "./dist-esm/src/xml.js": "./dist-esm/src/xml.browser.js" + }, + "types": "types/latest/core-xml.d.ts", + "typesVersions": { + "<3.6": { + "types/latest/*": [ + "types/3.1/*" + ] + } + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:browser": "npm run build:ts && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "npm run build:ts && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "cd samples && tsc -p .", + "build:test": "npm run build:ts && npm run bundle:test", + "build:test:browser": "npm run build:ts && npm run bundle:test:browser", + "build:test:node": "npm run build:ts && npm run bundle:test:node", + "build:ts": "tsc -p .", + "build:types": "downlevel-dts types/latest/ types/3.1/", + "build": "npm run build:ts && rollup -c 2>&1 && api-extractor run --local && npm run build:types", + "bundle:test": "rollup -c rollup.test.config.js 2>&1", + "bundle:test:browser": "cross-env ONLY_BROWSER=true rollup -c rollup.test.config.js 2>&1", + "bundle:test:node": "cross-env ONLY_NODE=true rollup -c rollup.test.config.js 2>&1", + "check-format": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* types *.tgz *.log", + "execute:samples": "echo skipped", + "extract-api": "npm run build:ts && api-extractor run --local", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "echo skipped", + "integration-test:node": "echo skipped", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json tsconfig.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json tsconfig.json api-extractor.json src test --ext .ts -f html -o coreXml-lintReport.html || exit 0", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run build:test:browser && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test:node && npm run unit-test:node && npm run integration-test:node", + "test": "npm run clean && npm run build:ts && npm run bundle:test:node && npm run unit-test:node && npm run bundle:test:browser && npm run unit-test:browser && npm run integration-test:node && npm run integration-test:browser", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js dist-test/index.node.js", + "unit-test": "npm run unit-test:node && npm run unit-test:browser" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/latest/core-xml.d.ts", + "types/3.1/core-xml.d.ts", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "keywords": [ + "azure", + "cloud", + "Azure" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "engines": { + "node": ">=8.0.0" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-xml/", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "tslib": "^2.0.0", + "xml2js": "^0.4.19" + }, + "devDependencies": { + "@microsoft/api-extractor": "7.7.11", + "@rollup/plugin-commonjs": "11.0.2", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-multi-entry": "^3.0.0", + "@rollup/plugin-node-resolve": "^8.0.0", + "@rollup/plugin-replace": "^2.2.0", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^8.0.0", + "@types/sinon": "^9.0.4", + "@types/xml2js": "^0.4.3", + "@typescript-eslint/eslint-plugin": "^2.0.0", + "@typescript-eslint/parser": "^2.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "chai": "^4.2.0", + "downlevel-dts": "~0.4.0", + "cross-env": "^7.0.2", + "eslint": "^6.1.0", + "eslint-config-prettier": "^6.0.0", + "eslint-plugin-no-null": "^1.0.2", + "eslint-plugin-no-only-tests": "^2.3.0", + "eslint-plugin-promise": "^4.1.1", + "inherits": "^2.0.3", + "karma": "^5.1.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-remap-istanbul": "^0.6.0", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "prettier": "^1.16.4", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-terser": "^5.1.1", + "rollup-plugin-visualizer": "^4.0.4", + "sinon": "^9.0.2", + "typescript": "~3.9.3", + "util": "^0.12.1" + } +} diff --git a/sdk/core/core-xml/review/core-xml.api.md b/sdk/core/core-xml/review/core-xml.api.md new file mode 100644 index 000000000000..f02a4f7c64e9 --- /dev/null +++ b/sdk/core/core-xml/review/core-xml.api.md @@ -0,0 +1,20 @@ +## API Report File for "@azure/core-xml" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export function parseXML(str: string, opts?: { + includeRoot?: boolean; +}): Promise; + +// @public +export function stringifyXML(obj: any, opts?: { + rootName?: string; +}): string; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/core/core-xml/rollup.base.config.js b/sdk/core/core-xml/rollup.base.config.js new file mode 100644 index 000000000000..7f8dc473b25b --- /dev/null +++ b/sdk/core/core-xml/rollup.base.config.js @@ -0,0 +1,120 @@ +import path from "path"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import multiEntry from "@rollup/plugin-multi-entry"; +import cjs from "@rollup/plugin-commonjs"; +import replace from "@rollup/plugin-replace"; +import { terser } from "rollup-plugin-terser"; +import sourcemaps from "rollup-plugin-sourcemaps"; +import viz from "rollup-plugin-visualizer"; + +const pkg = require("./package.json"); +const depNames = Object.keys(pkg.dependencies); +const devDepNames = Object.keys(pkg.devDependencies); +const input = "dist-esm/src/index.js"; +const production = process.env.NODE_ENV === "production"; + +export function nodeConfig(test = false) { + const externalNodeBuiltins = ["url"]; + const baseConfig = { + input: input, + external: depNames.concat(externalNodeBuiltins), + output: { file: "dist/index.js", format: "cjs", sourcemap: true }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (true) since this is for node only. + // Allows rollup's dead code elimination to be more aggressive. + "if (isNode)": "if (true)" + } + }), + nodeResolve({ preferBuiltins: true }), + cjs() + ] + }; + + if (test) { + // Entry points - test files under the `test` folder(common for both browser and node), node specific test files + baseConfig.input = ["dist-esm/test/*.spec.js", "dist-esm/test/node/*.spec.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + + // different output file + baseConfig.output.file = "dist-test/index.node.js"; + + // mark devdeps as external + baseConfig.external.push(...devDepNames); + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } else if (production) { + baseConfig.plugins.push(terser()); + } + + return baseConfig; +} + +export function browserConfig(test = false) { + const baseConfig = { + input: input, + output: { + file: "dist-browser/azure-core-client.js", + format: "umd", + name: "Azure.Core.Client", + sourcemap: true + }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (false) since this is for + // browser only. Rollup's dead code elimination will remove + // any code guarded by if (isNode) { ... } + "if (isNode)": "if (false)" + } + }), + nodeResolve({ + mainFields: ["module", "browser"], + preferBuiltins: false + }), + cjs({ + namedExports: { + chai: ["assert"], + "@opentelemetry/api": ["CanonicalCode", "SpanKind", "TraceFlags"] + } + }), + viz({ filename: "dist-browser/browser-stats.html", sourcemap: false }) + ] + }; + + if (test) { + // Entry points - test files under the `test` folder(common for both browser and node), browser specific test files + baseConfig.input = ["dist-esm/test/*.spec.js", "dist-esm/test/browser/*.spec.js"]; + baseConfig.plugins.unshift(multiEntry({ exports: false })); + baseConfig.output.file = "dist-test/index.browser.js"; + + baseConfig.onwarn = (warning) => { + if ( + warning.code === "CIRCULAR_DEPENDENCY" && + warning.importer.indexOf(path.normalize("node_modules/chai/lib") === 0) + ) { + // Chai contains circular references, but they are not fatal and can be ignored. + return; + } + + console.error(`(!) ${warning.message}`); + }; + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + } + + return baseConfig; +} diff --git a/sdk/core/core-xml/rollup.config.js b/sdk/core/core-xml/rollup.config.js new file mode 100644 index 000000000000..14652aa67ed8 --- /dev/null +++ b/sdk/core/core-xml/rollup.config.js @@ -0,0 +1,13 @@ +import * as base from "./rollup.base.config"; + +const inputs = []; + +if (!process.env.ONLY_BROWSER) { + inputs.push(base.nodeConfig()); +} + +if (!process.env.ONLY_NODE) { + inputs.push(base.browserConfig()); +} + +export default inputs; diff --git a/sdk/core/core-xml/rollup.test.config.js b/sdk/core/core-xml/rollup.test.config.js new file mode 100644 index 000000000000..fc843df2b5a3 --- /dev/null +++ b/sdk/core/core-xml/rollup.test.config.js @@ -0,0 +1,13 @@ +import * as base from "./rollup.base.config"; + +const inputs = []; + +if (!process.env.ONLY_BROWSER) { + inputs.push(base.nodeConfig(true)); +} + +if (!process.env.ONLY_NODE) { + inputs.push(base.browserConfig(true)); +} + +export default inputs; diff --git a/sdk/core/core-xml/samples/.gitkeep b/sdk/core/core-xml/samples/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sdk/core/core-xml/src/index.ts b/sdk/core/core-xml/src/index.ts new file mode 100644 index 000000000000..c2671da880b5 --- /dev/null +++ b/sdk/core/core-xml/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { stringifyXML, parseXML } from "./xml"; diff --git a/sdk/core/core-xml/src/xml.browser.ts b/sdk/core/core-xml/src/xml.browser.ts new file mode 100644 index 000000000000..91059acb0874 --- /dev/null +++ b/sdk/core/core-xml/src/xml.browser.ts @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +const doc = document.implementation.createDocument(null, null, null); + +const parser = new DOMParser(); +export function parseXML(str: string, opts?: { includeRoot?: boolean }): Promise { + try { + const dom = parser.parseFromString(str, "application/xml"); + throwIfError(dom); + + let obj; + if (opts && opts.includeRoot) { + obj = domToObject(dom); + } else { + obj = domToObject(dom.childNodes[0]); + } + + return Promise.resolve(obj); + } catch (err) { + return Promise.reject(err); + } +} + +let errorNS = ""; +try { + errorNS = parser.parseFromString("INVALID", "text/xml").getElementsByTagName("parsererror")[0] + .namespaceURI!; +} catch (ignored) { + // Most browsers will return a document containing , but IE will throw. +} + +function throwIfError(dom: Document): void { + if (errorNS) { + const parserErrors = dom.getElementsByTagNameNS(errorNS, "parsererror"); + if (parserErrors.length) { + throw new Error(parserErrors.item(0)!.innerHTML); + } + } +} + +function isElement(node: Node): node is Element { + return !!(node as Element).attributes; +} + +/** + * Get the Element-typed version of the provided Node if the provided node is an element with + * attributes. If it isn't, then undefined is returned. + */ +function asElementWithAttributes(node: Node): Element | undefined { + return isElement(node) && node.hasAttributes() ? node : undefined; +} + +function domToObject(node: Node): any { + let result: any = {}; + + const childNodeCount: number = node.childNodes.length; + + const firstChildNode: Node = node.childNodes[0]; + const onlyChildTextValue: string | undefined = + (firstChildNode && + childNodeCount === 1 && + firstChildNode.nodeType === Node.TEXT_NODE && + firstChildNode.nodeValue) || + undefined; + + const elementWithAttributes: Element | undefined = asElementWithAttributes(node); + if (elementWithAttributes) { + result["$"] = {}; + + for (let i = 0; i < elementWithAttributes.attributes.length; i++) { + const attr = elementWithAttributes.attributes[i]; + result["$"][attr.nodeName] = attr.nodeValue; + } + + if (onlyChildTextValue) { + result["_"] = onlyChildTextValue; + } + } else if (childNodeCount === 0) { + result = ""; + } else if (onlyChildTextValue) { + result = onlyChildTextValue; + } + + if (!onlyChildTextValue) { + for (let i = 0; i < childNodeCount; i++) { + const child = node.childNodes[i]; + // Ignore leading/trailing whitespace nodes + if (child.nodeType !== Node.TEXT_NODE) { + const childObject: any = domToObject(child); + if (!result[child.nodeName]) { + result[child.nodeName] = childObject; + } else if (Array.isArray(result[child.nodeName])) { + result[child.nodeName].push(childObject); + } else { + result[child.nodeName] = [result[child.nodeName], childObject]; + } + } + } + } + + return result; +} + +const serializer = new XMLSerializer(); + +export function stringifyXML(content: any, opts?: { rootName?: string }): string { + const rootName = (opts && opts.rootName) || "root"; + const dom = buildNode(content, rootName)[0]; + return ( + '' + serializer.serializeToString(dom) + ); +} + +function buildAttributes(attrs: { [key: string]: { toString(): string } }): Attr[] { + const result = []; + for (const key of Object.keys(attrs)) { + const attr = doc.createAttribute(key); + attr.value = attrs[key].toString(); + result.push(attr); + } + return result; +} + +function buildNode(obj: any, elementName: string): Node[] { + if ( + obj === undefined || + obj === null || + typeof obj === "string" || + typeof obj === "number" || + typeof obj === "boolean" + ) { + const elem = doc.createElement(elementName); + elem.textContent = obj === undefined || obj === null ? "" : obj.toString(); + return [elem]; + } else if (Array.isArray(obj)) { + const result = []; + for (const arrayElem of obj) { + for (const child of buildNode(arrayElem, elementName)) { + result.push(child); + } + } + return result; + } else if (typeof obj === "object") { + const elem = doc.createElement(elementName); + for (const key of Object.keys(obj)) { + if (key === "$") { + for (const attr of buildAttributes(obj[key])) { + elem.attributes.setNamedItem(attr); + } + } else if (key === "_") { + elem.textContent = obj[key].toString(); + } else { + for (const child of buildNode(obj[key], key)) { + elem.appendChild(child); + } + } + } + return [elem]; + } else { + throw new Error(`Illegal value passed to buildObject: ${obj}`); + } +} diff --git a/sdk/core/core-xml/src/xml.ts b/sdk/core/core-xml/src/xml.ts new file mode 100644 index 000000000000..e1d877793857 --- /dev/null +++ b/sdk/core/core-xml/src/xml.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as xml2js from "xml2js"; + +// Note: The reason we re-define all of the xml2js default settings (version 2.0) here is because the default settings object exposed +// by the xm2js library is mutable. See https://github.com/Leonidas-from-XIV/node-xml2js/issues/536 +// By creating a new copy of the settings each time we instantiate the parser, +// we are safeguarding against the possibility of the default settings being mutated elsewhere unintentionally. +const xml2jsDefaultOptionsV2 = { + explicitCharkey: false, + trim: false, + normalize: false, + normalizeTags: false, + attrkey: "$", + charkey: "_", + explicitArray: true, + ignoreAttrs: false, + mergeAttrs: false, + explicitRoot: true, + validator: null, + xmlns: false, + explicitChildren: false, + preserveChildrenOrder: false, + childkey: "$$", + charsAsChildren: false, + includeWhiteChars: false, + async: false, + strict: true, + attrNameProcessors: null, + attrValueProcessors: null, + tagNameProcessors: null, + valueProcessors: null, + rootName: "root", + xmldec: { + version: "1.0", + encoding: "UTF-8", + standalone: true + }, + doctype: null, + renderOpts: { + pretty: true, + indent: " ", + newline: "\n" + }, + headless: false, + chunkSize: 10000, + emptyTag: "", + cdata: false +}; + +// The xml2js settings for general XML parsing operations. +const xml2jsParserSettings: any = Object.assign({}, xml2jsDefaultOptionsV2); +xml2jsParserSettings.explicitArray = false; + +// The xml2js settings for general XML building operations. +const xml2jsBuilderSettings: any = Object.assign({}, xml2jsDefaultOptionsV2); +xml2jsBuilderSettings.explicitArray = false; +xml2jsBuilderSettings.renderOpts = { + pretty: false +}; + +/** + * Converts given JSON object to XML string + * @param obj JSON object to be converted into XML string + * @param opts Options that govern the parsing of given JSON object + * `rootName` indicates the name of the root element in the resulting XML + */ +export function stringifyXML(obj: any, opts?: { rootName?: string }): string { + xml2jsBuilderSettings.rootName = (opts || {}).rootName; + const builder = new xml2js.Builder(xml2jsBuilderSettings); + return builder.buildObject(obj); +} + +/** + * Converts given XML string into JSON + * @param str String containing the XML content to be parsed into JSON + * @param opts Options that govern the parsing of given xml string + * `includeRoot` indicates whether the root element is to be included or not in the output + */ +export function parseXML(str: string, opts?: { includeRoot?: boolean }): Promise { + xml2jsParserSettings.explicitRoot = !!(opts && opts.includeRoot); + const xmlParser = new xml2js.Parser(xml2jsParserSettings); + return new Promise((resolve, reject) => { + if (!str) { + reject(new Error("Document is empty")); + } else { + xmlParser.parseString(str, (err?: Error, res?: any) => { + if (err) { + reject(err); + } else { + resolve(res); + } + }); + } + }); +} diff --git a/sdk/core/core-xml/test/xml.spec.ts b/sdk/core/core-xml/test/xml.spec.ts new file mode 100644 index 000000000000..d125f308c634 --- /dev/null +++ b/sdk/core/core-xml/test/xml.spec.ts @@ -0,0 +1,417 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { parseXML, stringifyXML } from "../src"; +import { assert } from "chai"; + +describe("XML serializer", function() { + describe("parseXML(string)", function() { + it("with undefined", async function() { + try { + // @ts-expect-error + await parseXML(undefined); + assert.fail("Expected error"); + } catch (error) { + assert.ok( + error.message.indexOf("Document is empty") !== -1 || // Chrome + (error.message.startsWith("XML Parsing Error: syntax error") && + error.message.indexOf("undefined") !== -1), // Firefox + `error.message ("${error.message}") should have contained "Document is empty" or "undefined"` + ); + } + }); + + it("with null", async function() { + try { + // @ts-expect-error + await parseXML(null); + assert.fail("Expected error"); + } catch (error) { + assert.ok( + error.message.indexOf("Document is empty") !== -1 || // Chrome + (error.message.startsWith("XML Parsing Error: syntax error") && + error.message.indexOf("null") !== -1), // Firefox + `error.message ("${error.message}") should have contained "Document is empty" or "null"` + ); + } + }); + + it("with empty", async function() { + try { + await parseXML(""); + assert.fail("Expected error"); + } catch (error) { + // ignored + } + }); + + it("with text", async function() { + try { + await parseXML(""); + assert.fail("Hello World!"); + } catch (error) { + // ignored + } + }); + + it("with empty element", async function() { + const xml = await parseXML(""); + assert.deepStrictEqual(xml, ``); + }); + + it("with empty element with attribute", async function() { + const xml = await parseXML(``); + assert.deepStrictEqual(xml, { + $: { + healthy: "true" + } + }); + }); + + it("with element", async function() { + const xml = await parseXML(""); + assert.deepStrictEqual(xml, ``); + }); + + it("with element with value", async function() { + const xml = await parseXML("hurray"); + assert.deepStrictEqual(xml, `hurray`); + }); + + it("with element with attribute", async function() { + const xml = await parseXML(``); + assert.deepStrictEqual(xml, { + $: { + healthy: "true" + } + }); + }); + + it("with element with attribute and value", async function() { + const xml = await parseXML(`yum`); + assert.deepStrictEqual(xml, { + $: { + healthy: "true" + }, + _: "yum" + }); + }); + + it("with element with child empty element", async function() { + const xml = await parseXML(``); + assert.deepStrictEqual(xml, { + apples: `` + }); + }); + + it("with element with child empty element with attribute", async function() { + const xml = await parseXML(``); + assert.deepStrictEqual(xml, { + apples: { + $: { + tasty: "true" + } + } + }); + }); + + it("with element with child element with value", async function() { + const xml = await parseXML(`yum`); + assert.deepStrictEqual(xml, { + apples: "yum" + }); + }); + + it("with element with child element with attribute and value", async function() { + const xml = await parseXML(`yum`); + assert.deepStrictEqual(xml, { + apples: { + $: { + tasty: "true" + }, + _: "yum" + } + }); + }); + }); + + describe("parseXML(string) with root", function() { + it("with empty element", async function() { + const json: any = await parseXML("", { includeRoot: true }); + assert.deepStrictEqual(json, { fruit: `` }); + }); + + it("with empty element with attribute", async function() { + const json: any = await parseXML(``, { + includeRoot: true + }); + assert.deepStrictEqual(json, { + fruit: { + $: { + healthy: "true" + } + } + }); + }); + + it("with element", async function() { + const json: any = await parseXML("", { includeRoot: true }); + assert.deepStrictEqual(json, { fruit: `` }); + }); + + it("with element with value", async function() { + const json: any = await parseXML("hurray", { includeRoot: true }); + assert.deepStrictEqual(json, { fruit: `hurray` }); + }); + + it("with unwanted BOM characters", async function() { + const json: any = await parseXML("\uFEFFapple", { + includeRoot: true + }); + assert.deepStrictEqual(json, { fruit: "apple" }); + }); + + it("with element with attribute", async function() { + const json: any = await parseXML(``, { + includeRoot: true + }); + assert.deepStrictEqual(json, { + fruit: { + $: { + healthy: "true" + } + } + }); + }); + + it("with element with attribute and value", async function() { + const json: any = await parseXML(`yum`, { + includeRoot: true + }); + assert.deepStrictEqual(json, { + fruit: { + $: { + healthy: "true" + }, + _: "yum" + } + }); + }); + + it("with element with child empty element", async function() { + const json: any = await parseXML(``, { + includeRoot: true + }); + assert.deepStrictEqual(json, { + fruit: { + apples: `` + } + }); + }); + + it("with element with child empty element with attribute", async function() { + const json: any = await parseXML(``, { includeRoot: true }); + assert.deepStrictEqual(json, { + apples: { + $: { + tasty: "true" + } + } + }); + }); + + it("with element with child element with value", async function() { + const json: any = await parseXML(`yum`, { includeRoot: true }); + assert.deepStrictEqual(json, { + apples: "yum" + }); + }); + + it("with element with child element with attribute and value", async function() { + const json: any = await parseXML(`yum`, { + includeRoot: true + }); + assert.deepStrictEqual(json, { + apples: { + $: { + tasty: "true" + }, + _: "yum" + } + }); + }); + + it("should handle errors gracefully", async function() { + try { + await parseXML("INVALID", { includeRoot: true }); + throw new Error("did not throw"); + } catch (err) { + if (err.message === "did not throw") { + throw err; + } + } + }); + }); + + describe("stringifyXML(JSON) with root", function() { + it("with empty element with attribute", async function() { + const xml = await stringifyXML( + { + fruit: { + $: { + healthy: "true" + } + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `` + ); + }); + + it("with element", async function() { + const xml = await stringifyXML({ fruit: `` }, { rootName: "fruits" }); + assert.deepStrictEqual( + xml, + `` + ); + }); + + it("with element with value", async function() { + const xml = await stringifyXML({ fruit: `hurray` }, { rootName: "fruits" }); + assert.deepStrictEqual( + xml, + `hurray` + ); + }); + + it("with element with attribute", async function() { + const xml = await stringifyXML( + { + fruit: { + $: { + healthy: "true" + } + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `` + ); + }); + + it("with element with attribute and value", async function() { + const xml = await stringifyXML( + { + fruit: { + $: { + healthy: "true" + }, + _: "yum" + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `yum` + ); + }); + + it("with element with attribute and value", async function() { + const xml = await stringifyXML( + { + fruit: { + $: { + healthy: "true" + }, + _: "yum" + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `yum` + ); + }); + + it("with element with child undefined element", async function() { + const xml = await stringifyXML( + { + fruit: { + apples: undefined + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `` + ); + }); + + it("with element with child empty element with attribute", async function() { + const xml = await stringifyXML( + { + apples: { + $: { + tasty: "true" + } + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `` + ); + }); + + it("with element with child element with value", async function() { + const xml = await stringifyXML( + { + apples: "yum" + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `yum` + ); + }); + + it("with element with child element with attribute and value", async function() { + const xml = await stringifyXML( + { + apples: { + $: { + tasty: "true" + }, + _: "yum" + } + }, + { rootName: "fruits" } + ); + assert.deepStrictEqual( + xml, + `yum` + ); + }); + }); + + it("should handle errors gracefully", async function() { + try { + await parseXML("INVALID"); + throw new Error("did not throw"); + } catch (err) { + if (err.message === "did not throw") { + throw err; + } + } + }); +}); diff --git a/sdk/core/core-xml/tsconfig.json b/sdk/core/core-xml/tsconfig.json new file mode 100644 index 000000000000..4a53cc511418 --- /dev/null +++ b/sdk/core/core-xml/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types/latest" + }, + "exclude": ["node_modules", "types", "temp", "browser", "dist", "dist-esm", "./samples/**/*.ts"] +}