Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: parseFromUrl does not resolve relative references (#504) #572

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/');
});
});