Skip to content

Commit 92eb22d

Browse files
tatomyrDmitryAnansky
authored andcommitted
apply review suggestinos
1 parent d01a521 commit 92eb22d

File tree

5 files changed

+58
-28
lines changed

5 files changed

+58
-28
lines changed

packages/core/src/__tests__/ref-utils.test.ts

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import outdent from 'outdent';
22
import { parseYamlToDocument } from '../../__tests__/utils.js';
3-
import { escapePointer, parseRef, refBaseName, unescapePointer } from '../ref-utils.js';
3+
import {
4+
escapePointerFragment,
5+
parseRef,
6+
refBaseName,
7+
unescapePointerFragment,
8+
} from '../ref-utils.js';
49
import { lintDocument } from '../lint.js';
510
import { createConfig } from '../config/index.js';
611
import { BaseResolver } from '../resolve.js';
@@ -35,8 +40,8 @@ describe('ref-utils', () => {
3540
});
3641

3742
it(`should unescape complex urlencoded paths`, () => {
38-
const referene = 'somefile.yaml#/components/schemas/scope~1complex~0name';
39-
expect(parseRef(referene)).toMatchInlineSnapshot(`
43+
const reference = 'somefile.yaml#/components/schemas/scope%2Fcomplex~name';
44+
expect(parseRef(reference)).toMatchInlineSnapshot(`
4045
{
4146
"pointer": [
4247
"components",
@@ -48,6 +53,20 @@ describe('ref-utils', () => {
4853
`);
4954
});
5055

56+
it(`should unescape escaped paths`, () => {
57+
const reference = 'somefile.yaml#/components/schemas/scope~1complex~0name with spaces';
58+
expect(parseRef(reference)).toMatchInlineSnapshot(`
59+
{
60+
"pointer": [
61+
"components",
62+
"schemas",
63+
"scope/complex~name with spaces",
64+
],
65+
"uri": "somefile.yaml",
66+
}
67+
`);
68+
});
69+
5170
it(`should validate definition with urlencoded paths`, async () => {
5271
const document = parseYamlToDocument(
5372
outdent`
@@ -140,32 +159,37 @@ describe('ref-utils', () => {
140159
});
141160
});
142161

143-
describe('escapePointer', () => {
144-
it('should escape a pointer correctly', () => {
145-
expect(escapePointer('scope/complex~name')).toStrictEqual('scope~1complex~0name');
162+
describe('escapePointerFragment', () => {
163+
it('should escape a simple pointer fragment with ~ and / correctly', () => {
164+
expect(escapePointerFragment('scope/complex~name')).toStrictEqual('scope~1complex~0name');
146165
});
147166

148-
it('should escape a pointer with a number correctly', () => {
149-
expect(escapePointer(123)).toStrictEqual(123);
167+
it('should escape a pointer fragment with a number correctly', () => {
168+
expect(escapePointerFragment(123)).toStrictEqual(123);
150169
});
151170

152-
it('should escape a pointer with a string correctly', () => {
153-
expect(escapePointer('scope/complex~name')).toStrictEqual('scope~1complex~0name');
171+
it('should URI-encode special characters when escaping pointer fragments per https://datatracker.ietf.org/doc/html/rfc6901#section-6', () => {
172+
expect(escapePointerFragment('percent%')).toStrictEqual('percent%25');
173+
expect(escapePointerFragment('caret^')).toStrictEqual('caret%5E');
174+
expect(escapePointerFragment('pipe|')).toStrictEqual('pipe%7C');
175+
expect(escapePointerFragment('doublebackslash\\\\')).toStrictEqual('doublebackslash%5C');
176+
expect(escapePointerFragment('backslash\\')).toStrictEqual('backslash%22');
177+
expect(escapePointerFragment('space ')).toStrictEqual('space%20');
154178
});
155179

156-
it('should not URI-encode special characters when escaping pointers', () => {
157-
// escapePointer should only handle JSON Pointer escaping, not URI encoding
158-
expect(escapePointer('activity_level_%')).toStrictEqual('activity_level_%25');
180+
it('should not URI-encode other special characters when escaping pointer fragments per https://datatracker.ietf.org/doc/html/rfc6901#section-6', () => {
181+
expect(escapePointerFragment('curly{braces}')).toStrictEqual('curly{braces}');
182+
expect(escapePointerFragment('plus+')).toStrictEqual('plus+');
159183
});
160184
});
161185

162-
describe('unescapePointer', () => {
186+
describe('unescapePointerFragment', () => {
163187
it('should unescape a pointer with a percent sign correctly', () => {
164-
expect(unescapePointer('activity_level_%25')).toStrictEqual('activity_level_%');
188+
expect(unescapePointerFragment('activity_level_%25')).toStrictEqual('activity_level_%');
165189
});
166190

167191
it('should unescape a pointer correctly', () => {
168-
expect(unescapePointer('scope~1complex~0name')).toStrictEqual('scope/complex~name');
192+
expect(unescapePointerFragment('scope~1complex~0name')).toStrictEqual('scope/complex~name');
169193
});
170194
});
171195
});

packages/core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ export {
5252
export { YamlParseError } from './errors/yaml-parse-error.js';
5353
export { parseYaml, stringifyYaml } from './js-yaml/index.js';
5454
export {
55-
unescapePointer,
55+
unescapePointerFragment,
5656
isRef,
5757
isAbsoluteUrl,
58-
escapePointer,
58+
escapePointerFragment,
5959
type Location,
6060
} from './ref-utils.js';
6161
export { detectSpec } from './detect-spec.js';

packages/core/src/ref-utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class Location {
2626
this.source,
2727
joinPointer(
2828
this.pointer,
29-
(Array.isArray(components) ? components : [components]).map(escapePointer).join('/')
29+
(Array.isArray(components) ? components : [components]).map(escapePointerFragment).join('/')
3030
)
3131
);
3232
}
@@ -40,11 +40,11 @@ export class Location {
4040
}
4141
}
4242

43-
export function unescapePointer(fragment: string): string {
43+
export function unescapePointerFragment(fragment: string): string {
4444
return decodeURIComponent(fragment).replaceAll('~1', '/').replaceAll('~0', '~');
4545
}
4646

47-
export function escapePointer<T extends string | number>(fragment: T): T {
47+
export function escapePointerFragment<T extends string | number>(fragment: T): T {
4848
if (typeof fragment === 'number') return fragment;
4949
return encodeURIFragmentIdentifier(fragment.replaceAll('~', '~0').replaceAll('/', '~1')) as T;
5050
}
@@ -69,7 +69,7 @@ export function parseRef(ref: string): { uri: string | null; pointer: string[] }
6969
}
7070

7171
export function parsePointer(pointer: string) {
72-
return pointer.split('/').map(unescapePointer).filter(isTruthy);
72+
return pointer.split('/').map(unescapePointerFragment).filter(isTruthy);
7373
}
7474

7575
export function pointerBaseName(pointer: string) {

packages/core/src/resolve.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as path from 'node:path';
33
import {
44
isRef,
55
joinPointer,
6-
escapePointer,
6+
escapePointerFragment,
77
parseRef,
88
isAbsoluteUrl,
99
isAnchor,
@@ -315,7 +315,7 @@ export async function resolveDocument(opts: {
315315
continue;
316316
}
317317

318-
walk(propValue, propType, joinPointer(nodeAbsoluteRef, escapePointer(propName)));
318+
walk(propValue, propType, joinPointer(nodeAbsoluteRef, escapePointerFragment(propName)));
319319
}
320320

321321
if (isRef(node)) {
@@ -426,7 +426,10 @@ export async function resolveDocument(opts: {
426426
break;
427427
} else if (target[segment] !== undefined) {
428428
target = target[segment];
429-
resolvedRef.nodePointer = joinPointer(resolvedRef.nodePointer!, escapePointer(segment));
429+
resolvedRef.nodePointer = joinPointer(
430+
resolvedRef.nodePointer!,
431+
escapePointerFragment(segment)
432+
);
430433
} else if (isRef(target)) {
431434
resolvedRef = await followRef(targetDoc, target, pushRef(refStack, target));
432435
targetDoc = resolvedRef.document || targetDoc;
@@ -437,7 +440,10 @@ export async function resolveDocument(opts: {
437440
}
438441

439442
target = resolvedRef.node[segment];
440-
resolvedRef.nodePointer = joinPointer(resolvedRef.nodePointer!, escapePointer(segment));
443+
resolvedRef.nodePointer = joinPointer(
444+
resolvedRef.nodePointer!,
445+
escapePointerFragment(segment)
446+
);
441447
} else {
442448
target = undefined;
443449
break;

packages/core/src/rules/ajv.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import addFormats from 'ajv-formats';
22
import Ajv from '@redocly/ajv/dist/2020.js';
3-
import { escapePointer } from '../ref-utils.js';
3+
import { escapePointerFragment } from '../ref-utils.js';
44

55
import type { Location } from '../ref-utils.js';
66
import type { ValidateFunction, ErrorObject } from '@redocly/ajv/dist/2020.js';
@@ -95,7 +95,7 @@ export function validateJsonSchema(
9595
if (error.keyword === 'additionalProperties' || error.keyword === 'unevaluatedProperties') {
9696
const property = error.params.additionalProperty || error.params.unevaluatedProperty;
9797
message = `${message} \`${property}\``;
98-
error.instancePath += '/' + escapePointer(property);
98+
error.instancePath += '/' + escapePointerFragment(property);
9999
}
100100

101101
return {

0 commit comments

Comments
 (0)