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

fix(execute): refactor encodeDisallowedCharacters function #3463

Merged
merged 4 commits into from
Apr 8, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/execute/oas3/parameter-builders.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import stylize, { encodeDisallowedCharacters } from './style-serializer.js';
import stylize, { encodeCharacters } from './style-serializer.js';
import serialize from './content-serializer.js';

export function path({ req, value, parameter }) {
Expand All @@ -11,14 +11,14 @@ export function path({ req, value, parameter }) {

req.url = req.url
.split(`{${name}}`)
.join(encodeDisallowedCharacters(serialize(value, effectiveMediaType), { escape: true }));
.join(encodeCharacters(serialize(value, effectiveMediaType)));
} else {
const styledValue = stylize({
key: parameter.name,
value,
style: style || 'simple',
explode: explode || false,
escape: true,
escape: 'reserved',
});

req.url = req.url.replace(new RegExp(`{${name}}`, 'g'), styledValue);
Expand Down
36 changes: 12 additions & 24 deletions src/execute/oas3/style-serializer.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
const isRfc3986Reserved = (char) => ":/?#[]@!$&'()*+,;=".indexOf(char) > -1;
const isRrc3986Unreserved = (char) => /^[a-z0-9\-._~]+$/i.test(char);
const isRfc3986Unreserved = (char) => /^[a-z0-9\-._~]+$/i.test(char);

// eslint-disable-next-line default-param-last
export function encodeDisallowedCharacters(str, { escape } = {}, parse) {
if (typeof str === 'number') {
str = str.toString();
}

if (typeof str !== 'string' || !str.length) {
return str;
}

if (!escape) {
return str;
}

if (parse) {
return JSON.parse(str);
}

export function encodeCharacters(str, characterSet = 'reserved') {
// In ES6 you can do this quite easily by using the new ... spread operator.
// This causes the string iterator (another new ES6 feature) to be used internally,
// and because that iterator is designed to deal with
// code points rather than UCS-2/UTF-16 code units.
return [...str]
.map((char) => {
if (isRrc3986Unreserved(char)) {
if (isRfc3986Unreserved(char)) {
return char;
}

if (isRfc3986Reserved(char) && escape === 'unsafe') {
if (isRfc3986Reserved(char) && characterSet === 'unsafe') {
return char;
}

Expand All @@ -56,13 +40,17 @@ export default function stylize(config) {
return encodePrimitive(config);
}

export function valueEncoder(value, escape) {
export function valueEncoder(value, escape = false) {
if (Array.isArray(value) || (value !== null && typeof value === 'object')) {
value = JSON.stringify(value);
} else if (typeof value === 'number' || typeof value === 'boolean') {
char0n marked this conversation as resolved.
Show resolved Hide resolved
value = String(value);
}

if (escape && value.length > 0) {
return encodeCharacters(value, escape);
}
return encodeDisallowedCharacters(value, {
escape,
});
return value;
}

function encodeArray({ key, value, style, explode, escape }) {
Expand Down
153 changes: 146 additions & 7 deletions test/oas3/execute/style-serializer.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { encodeDisallowedCharacters } from '../../../src/execute/oas3/style-serializer.js';
import { encodeCharacters, valueEncoder } from '../../../src/execute/oas3/style-serializer.js';

describe('OAS3 style serializer', () => {
char0n marked this conversation as resolved.
Show resolved Hide resolved
describe('encodeDisallowedCharacters', () => {
describe('encodeCharacters', () => {
test('should correctly encode ASCII characters', () => {
const tested = (str) => encodeDisallowedCharacters(str, { escape: true });
const tested = (str) => encodeCharacters(str);

expect(tested('!')).toEqual('%21');
expect(tested('#')).toEqual('%23');
Expand All @@ -28,7 +28,7 @@ describe('OAS3 style serializer', () => {
});

test('should correctly encode non-ASCII characters', () => {
const tested = (str) => encodeDisallowedCharacters(str, { escape: true });
const tested = (str) => encodeCharacters(str);
expect(tested('♥')).toEqual('%E2%99%A5');
expect(tested('テスト')).toEqual('%E3%83%86%E3%82%B9%E3%83%88');
expect(tested('𩸽')).toEqual('%F0%A9%B8%BD');
Expand All @@ -37,9 +37,109 @@ describe('OAS3 style serializer', () => {
'%F0%9F%91%A9%E2%80%8D%F0%9F%91%A9%E2%80%8D%F0%9F%91%A7%E2%80%8D%F0%9F%91%A7'
);
});
});

describe('valueEncoder', () => {
test('should correctly encode primitive values with `escape` set to `reserved`', () => {
const tested = (value) => valueEncoder(value, 'reserved');

expect(tested(123)).toEqual('123');
expect(tested('a#b$c%d[e]_~1')).toEqual('a%23b%24c%25d%5Be%5D_~1');
expect(tested(false)).toEqual('false');
});

test('should correctly encode primitive values with `escape` set to `unsafe`', () => {
const tested = (value) => valueEncoder(value, 'unsafe');

expect(tested(123)).toEqual('123');
expect(tested('a#b$c%d[e]_~1')).toEqual('a#b$c%25d[e]_~1');
expect(tested(false)).toEqual('false');
});

test('should skip encoding if `escape` is not set to true', () => {
const tested = (str) => encodeDisallowedCharacters(str);
test('should correctly encode objects with `escape` set to `reserved`', () => {
const tested = (value) => valueEncoder(value, 'reserved');

expect(tested({ a: 'a#b$c%d[e]_~1' })).toEqual(
'%7B%22a%22%3A%22a%23b%24c%25d%5Be%5D_~1%22%7D'
);
expect(tested({ a: { b: { c: 'd' } } })).toEqual(
'%7B%22a%22%3A%7B%22b%22%3A%7B%22c%22%3A%22d%22%7D%7D%7D'
);
expect(tested({ a: 123 })).toEqual('%7B%22a%22%3A123%7D');
expect(tested({ a: false })).toEqual('%7B%22a%22%3Afalse%7D');
});

test('should correctly encode objects with `escape` set to `unsafe`', () => {
const tested = (value) => valueEncoder(value, 'unsafe');

expect(tested({ a: 'a#b$c%d[e]_~1' })).toEqual('%7B%22a%22:%22a#b$c%25d[e]_~1%22%7D');
expect(tested({ a: { b: { c: 'd' } } })).toEqual(
'%7B%22a%22:%7B%22b%22:%7B%22c%22:%22d%22%7D%7D%7D'
);
expect(tested({ a: 123 })).toEqual('%7B%22a%22:123%7D');
expect(tested({ a: false })).toEqual('%7B%22a%22:false%7D');
});

test('should correctly encode arrays with `escape` set to `reserved`', () => {
const tested = (value) => valueEncoder(value, 'reserved');

expect(tested([1, 2, 3])).toEqual('%5B1%2C2%2C3%5D');
expect(tested(['1', '2', 'a#b$c%d[e]_~1'])).toEqual(
'%5B%221%22%2C%222%22%2C%22a%23b%24c%25d%5Be%5D_~1%22%5D'
);
expect(
tested([
{
a: {
b: 'c',
},
},
])
).toEqual('%5B%7B%22a%22%3A%7B%22b%22%3A%22c%22%7D%7D%5D');
expect(
tested([
[
{
a: {
b: 'c',
},
},
],
])
).toEqual('%5B%5B%7B%22a%22%3A%7B%22b%22%3A%22c%22%7D%7D%5D%5D');
});

test('should correctly encode arrays with `escape` set to `unsafe`', () => {
const tested = (value) => valueEncoder(value, 'unsafe');

expect(tested([1, 2, 3])).toEqual('[1,2,3]');
expect(tested(['1', '2', 'a#b$c%d[e]_~1'])).toEqual(
'[%221%22,%222%22,%22a#b$c%25d[e]_~1%22]'
);
expect(
tested([
{
a: {
b: 'c',
},
},
])
).toEqual('[%7B%22a%22:%7B%22b%22:%22c%22%7D%7D]');
expect(
tested([
[
{
a: {
b: 'c',
},
},
],
])
).toEqual('[[%7B%22a%22:%7B%22b%22:%22c%22%7D%7D]]');
});

test('should skip encoding if `escape` is not set', () => {
const tested = (value) => valueEncoder(value);

expect(tested('!')).toEqual('!');
expect(tested('#')).toEqual('#');
Expand All @@ -61,12 +161,51 @@ describe('OAS3 style serializer', () => {
expect(tested(']')).toEqual(']');
expect(tested('\n')).toEqual('\n');

// Non-ASCII too!
// Non-ASCII
expect(tested('♥')).toEqual('♥');
expect(tested('テスト')).toEqual('テスト');
expect(tested('𩸽')).toEqual('𩸽');
expect(tested('🍣')).toEqual('🍣');
expect(tested('👩‍👩‍👧‍👧')).toEqual('👩‍👩‍👧‍👧');

// Primitive
expect(tested(123)).toEqual('123');
expect(tested(false)).toEqual('false');

// Objects
expect(tested({ a: 'a#b$c%d[e]_~1' })).toEqual('{"a":"a#b$c%d[e]_~1"}');
expect(tested({ a: { b: { c: 'd' } } })).toEqual('{"a":{"b":{"c":"d"}}}');
expect(tested({ a: 123 })).toEqual('{"a":123}');
expect(tested({ a: false })).toEqual('{"a":false}');

// Arrays
expect(tested([1, 2, 3])).toEqual('[1,2,3]');
expect(tested(['1', '2', 'a#b$c%d[e]_~1'])).toEqual('["1","2","a#b$c%d[e]_~1"]');
expect(
tested([
{
a: {
b: 'c',
},
},
{
d: {
e: 'f',
},
},
])
).toEqual('[{"a":{"b":"c"}},{"d":{"e":"f"}}]');
expect(
tested([
[
{
a: {
b: 'c',
},
},
],
])
).toEqual('[[{"a":{"b":"c"}}]]');
});
});
});