Skip to content

Commit

Permalink
feat: parseFromUrl does not resolve relative references (#572)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeworxet authored Sep 14, 2022
1 parent c6c6c77 commit d7ea01a
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 42 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ env:
node: true
es6: true
mocha: true
browser: true

plugins:
- sonarjs
Expand Down
21 changes: 18 additions & 3 deletions lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ const $RefParser = require('@apidevtools/json-schema-ref-parser');
const mergePatch = require('tiny-merge-patch').apply;
const ParserError = require('./errors/parser-error');
const { validateChannels, validateTags, validateServerVariables, validateOperationId, validateServerSecurity, validateMessageId } = require('./customValidators.js');
const { toJS, findRefs, getLocationOf, improveAjvErrors, getDefaultSchemaFormat } = require('./utils');
const {
toJS,
findRefs,
getLocationOf,
improveAjvErrors,
getDefaultSchemaFormat,
getBaseUrl,
} = require('./utils');
const AsyncAPIDocument = require('./models/asyncapi');

const OPERATIONS = ['publish', 'subscribe'];
Expand Down Expand Up @@ -56,7 +63,11 @@ async function parse(asyncapiYAMLorJSON, options = {}) {
let parsedJSON;
let initialFormat;

options.path = options.path || `${process.cwd()}${path.sep}`;
if (typeof window !== 'undefined' && !options.hasOwnProperty('path')) {
options.path = getBaseUrl(window.location.href);
} else {
options.path = options.path || `${process.cwd()}${path.sep}`;
}

try {
({ initialFormat, parsedJSON } = toJS(asyncapiYAMLorJSON));
Expand Down Expand Up @@ -128,12 +139,16 @@ async function parse(asyncapiYAMLorJSON, options = {}) {
* @param {ParserOptions=} [options] Configuration to pass to the {@link #asyncapiparserparseroptions--object|ParserOptions} method.
* @returns {Promise<AsyncAPIDocument>} The parsed AsyncAPI document.
*/
function parseFromUrl(url, fetchOptions, options) {
function parseFromUrl(url, fetchOptions, options = {}) {
//Why not just addinga default to the arguments list?
//All function parameters with default values should be declared after the function parameters without default values. Otherwise, it makes it impossible for callers to take advantage of defaults; they must re-specify the defaulted values or pass undefined in order to "get to" the non-default parameters.
//To not break the API by changing argument position and to silet the linter it is just better to move adding
if (!fetchOptions) fetchOptions = {};

if (!options.hasOwnProperty('path')) {
options = { ...options, path: getBaseUrl(url) };
}

return new Promise((resolve, reject) => {
fetch(url, fetchOptions)
.then(res => res.text())
Expand Down
17 changes: 17 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,23 @@ utils.parseUrlQueryParameters = str => {
return str.match(/\?((.*=.*)(&?))/g);
};

/**
* Returns base URL parsed from location of AsyncAPI document
*
* @function getBaseUrl
* @private
* @param {String} url URL of AsyncAPI document
*/
utils.getBaseUrl = url => {
url = typeof url !== 'string' ? String(url) : url;
//URL validation is not performed because 'node-fetch' performs its own
//validation at fetch time, so no repetition of this task is made.
//Only ensuring that 'url' has type of 'string' and letting 'node-fetch' deal
//with the rest.

return url.substring(0, url.lastIndexOf('/') + 1);
};

/**
* Returns an array of not existing properties in provided object with names specified in provided array
* @function getMissingProps
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"release": "semantic-release",
"lint": "eslint --max-warnings 0 --config \".eslintrc\" \".\"",
"lint:fix": "eslint --max-warnings 0 --config \".eslintrc\" \".\" --fix",
"test:lib": "nyc --silent --no-clean mocha --exclude \"test/browser_test.js\" --exclude \"test/parseFromUrl_test.js\" --recursive",
"test:lib": "npm run test:browser:cleanup && nyc --silent --no-clean mocha --exclude \"test/browser_test.js\" --exclude \"test/parseFromUrl_test.js\" --recursive",
"test:parseFromUrl": "nyc --silent --no-clean start-server-and-test \"http-server test/sample_browser --cors -s\" 8080 \"mocha test/parseFromUrl_test.js\"",
"cover:report": "nyc report --reporter=text --reporter=html",
"test:browser": "npm run test:browser:cleanup && npm run bundle && shx cp \"dist/bundle.js\" \"test/sample_browser/\" && start-server-and-test \"http-server test/sample_browser --cors -s\" 8080 \"mocha --timeout 20000 test/browser_test.js\" && npm run test:browser:cleanup",
Expand Down Expand Up @@ -66,7 +66,7 @@
"mkdirp": "^1.0.4",
"mocha": "^6.1.4",
"nyc": "^15.1.0",
"puppeteer": "^7.0.1",
"puppeteer": "^17.0.0",
"rimraf": "^3.0.2",
"semantic-release": "19.0.3",
"shx": "^0.3.3",
Expand Down
5 changes: 3 additions & 2 deletions test/browser_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ describe('Check Parser in the browser', function() {
try {
console.info('getting fromString element');
const specString = await page.$('#fromString');

const content = await page.evaluate(element => element.textContent, specString);
expect(content).to.be.equal('2.0.0');
expect(content).to.be.equal('{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}');
} catch (e) {
throw new Error(e);
}
Expand All @@ -47,7 +48,7 @@ describe('Check Parser in the browser', function() {
const specUrl = await page.$('#fromUrl');
const content = await page.evaluate(element => element.textContent, specUrl);

expect(content).to.be.equal('2.0.0');
expect(content).to.be.equal('{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}');
} catch (e) {
throw new Error(e);
}
Expand Down
8 changes: 4 additions & 4 deletions test/parseFromUrl_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ const { checkErrorWrapper } = require('./testsUtils');
chai.use(chaiAsPromised);
const expect = chai.expect;

const validOutputJSON = '{"asyncapi":"2.0.0","info":{"title":"My API","version":"1.0.0"},"channels":{"/test/tester":{"subscribe":{"message":{"schemaFormat":"application/vnd.aai.asyncapi;version=2.0.0","x-parser-message-parsed":true,"x-parser-message-name":"<anonymous-message-1>"}}}},"x-parser-spec-parsed":true}';
const validOutputJSON = '{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user","x-parser-schema-id":"<anonymous-schema-2>"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}},"x-parser-schema-id":"<anonymous-schema-3>"}},"x-parser-schema-id":"<anonymous-schema-1>"},"x-parser-original-schema-format":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-original-payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"type":"object","properties":{"testing1":{"type":"string"},"testing2":{"type":"string"}}}}}},"schemaFormat":"application/vnd.aai.asyncapi;version=2.4.0","x-parser-message-parsed":true,"x-parser-message-name":"UserSignedUp"}}},"x-parser-spec-parsed":true}';

describe('parseFromUrl()', function() {
it('should parse YAML correctly from URL', async function() {
const result = await parser.parseFromUrl('http://localhost:8080/asyncapi.yaml');
const result = await parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml');
expect(JSON.stringify(result.json())).to.equal(validOutputJSON);
});

it('should parse 2 AsyncAPI specs in Promise.all() from URL', async function() {
const input = [
parser.parseFromUrl('http://localhost:8080/asyncapi.yaml'),
parser.parseFromUrl('http://localhost:8080/asyncapi.yaml')
parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml'),
parser.parseFromUrl('http://localhost:8080/main/asyncapi.yaml')
];
const result = await Promise.all(input);
expect(JSON.stringify(result[0].json())).to.equal(validOutputJSON);
Expand Down
2 changes: 1 addition & 1 deletion test/refs/refed.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type: object
properties:
testing:
$ref: 'refed2.yaml'
$ref: './refed2.yaml'
8 changes: 0 additions & 8 deletions test/sample_browser/asyncapi.yaml

This file was deleted.

75 changes: 53 additions & 22 deletions test/sample_browser/index.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
<body>
<div id="fromString">
</div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AsyncAPI document parsing test</title>
<script src="bundle.js"></script>
<script>
const convertChars2HTMLEntities = (jsonString) => {
// Some characters in JSON received from the parser, might have special
// meaning for browsers during their tokenization and DOM tree
// construction stages.
// This function allows to convert such characters, which introduce unexpected
// bugs while they shouldn't, to HTML entities before inserting them into DOM.
// During insertion into the DOM, browser converts such HTML entities back
// to chars, inserting into DOM only characters which the next tool needs
// to see (e.g. test automation framework which queries content of an
// element, to assert the element contains exactly needed chars).
jsonString = jsonString.replace(/</g, '&lt;')
jsonString = jsonString.replace(/>/g, '&gt;');

return jsonString;
}

<div style="visibility: hidden;" id="fromUrl">
</div>
</body>
<script src="bundle.js"></script>
<script defer>
const execute = async() => {
const parser = window.AsyncAPIParser;
const execute = async () => {
const parser = window.AsyncAPIParser;

try {
const specFromString = '{"asyncapi":"2.4.0","info":{"title":"Account Service","version":"1.0.0","description":"This service is in charge of processing user signups"},"channels":{"user/signedup":{"subscribe":{"message":{"$ref":"#/components/messages/UserSignedUp"}}}},"components":{"messages":{"UserSignedUp":{"payload":{"type":"object","properties":{"displayName":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user","test":{"$ref":"../refs/refed.yaml"}}}}}}}}';
let parsedFromString = await parser.parse(specFromString);
parsedFromString = convertChars2HTMLEntities(JSON.stringify(parsedFromString.json()));
document.getElementById('fromString').innerHTML = parsedFromString;

try {
const specFromString = '{ "asyncapi": "2.0.0", "info": { "title": "My API", "version": "1.0.0" }, "channels": { "/test/tester": { "subscribe": { "message": { } } } } }';
const parsedFromString = await parser.parse(specFromString);
document.getElementById('fromString').innerHTML = parsedFromString.version();
let parsedFromUrl = await parser.parseFromUrl('http://127.0.0.1:8080/main/asyncapi.yaml');
parsedFromUrl = convertChars2HTMLEntities(JSON.stringify(parsedFromUrl._json));
const fromUrlElement = document.getElementById('fromUrl');
fromUrlElement.innerHTML = parsedFromUrl;
fromUrlElement.style.visibility = 'visible';

const parsedFromUrl = await parser.parseFromUrl('http://127.0.0.1:8080/asyncapi.yaml');
const divElement = document.getElementById('fromUrl');
divElement.style.visibility = 'visible';
divElement.innerHTML = parsedFromUrl.version();
} catch (error) {
console.error(error)
}
} catch (error) {
console.error(error)
}
}

execute();
</script>
</script>
</head>
<body>
<p>#fromString</p>
<div id="fromString">
</div>
<br />
<p>#fromUrl</p>
<div id="fromUrl" style="visibility: hidden;">
</div>
</body>
</html>
25 changes: 25 additions & 0 deletions test/sample_browser/main/asyncapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
asyncapi: '2.4.0'
info:
title: Account Service
version: 1.0.0
description: This service is in charge of processing user signups
channels:
user/signedup:
subscribe:
message:
$ref: '#/components/messages/UserSignedUp'
components:
messages:
UserSignedUp:
payload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
test:
$ref: '../refs/refed.yaml'
6 changes: 6 additions & 0 deletions test/sample_browser/refs/refed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: object
properties:
testing1:
$ref: "./refed2.yaml"
testing2:
$ref: "http://localhost:8080/refs/refed2.yaml"
1 change: 1 addition & 0 deletions test/sample_browser/refs/refed2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type: string
14 changes: 14 additions & 0 deletions test/utils_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const { getBaseUrl } = require('../lib/utils');

chai.use(chaiAsPromised);
const expect = chai.expect;

describe('getBaseUrl()', function () {
it('should accept totally valid absolute URL of an AsyncAPI document', async function () {
await expect(
getBaseUrl('https://asyncapi.com/good/asyncapi.yaml')
).to.equal('https://asyncapi.com/good/');
});
});

0 comments on commit d7ea01a

Please sign in to comment.