Skip to content

Commit f824be1

Browse files
feat: add option to disable external reference resolution
- Add resolveExternal option to ResolverOptions - Allow disabling external (http/https/file) reference resolution - Internal JSON pointer references still work when disabled - Defaults to true for backward compatibility Fixes: #1098
1 parent 6d06dd4 commit f824be1

File tree

2 files changed

+179
-1
lines changed

2 files changed

+179
-1
lines changed

packages/parser/src/resolver.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ export interface Resolver {
1414
export interface ResolverOptions {
1515
cache?: boolean;
1616
resolvers?: Array<Resolver>;
17+
resolveExternal?: boolean;
1718
}
1819

1920
export function createResolver(options: ResolverOptions = {}): SpectralResolver {
21+
// Default to true for backward compatibility
22+
const resolveExternal = options.resolveExternal !== false;
23+
const defaultResolvers = resolveExternal ? createDefaultResolvers() : [];
24+
2025
const availableResolvers: Array<Resolver> = [
21-
...createDefaultResolvers(),
26+
...defaultResolvers,
2227
...(options.resolvers || [])
2328
].map(r => ({
2429
...r,
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import { Parser } from '../src/parser';
2+
3+
describe('external reference resolution disabled', function() {
4+
it('should not resolve external http references when disabled in constructor', async function() {
5+
const parser = new Parser({
6+
__unstable: {
7+
resolver: {
8+
resolveExternal: false
9+
}
10+
}
11+
});
12+
13+
const documentRaw = {
14+
asyncapi: '2.0.0',
15+
info: {
16+
title: 'Test AsyncApi document',
17+
version: '1.0',
18+
},
19+
channels: {
20+
channel: {
21+
publish: {
22+
operationId: 'publish',
23+
message: {
24+
$ref: 'https://example.com/nonexistent-schema.json'
25+
}
26+
},
27+
}
28+
},
29+
};
30+
31+
// When external resolution is disabled, validation should fail with an error about unresolved reference
32+
const diagnostics = await parser.validate(documentRaw);
33+
34+
// Should have errors about unresolved references
35+
expect(diagnostics.length).toBeGreaterThan(0);
36+
const hasRefError = diagnostics.some(d =>
37+
d.message?.toLowerCase().includes('reference') ||
38+
d.message?.toLowerCase().includes('resolve') ||
39+
d.message?.toLowerCase().includes('$ref') ||
40+
d.message?.toLowerCase().includes('unable') ||
41+
d.message?.toLowerCase().includes('unresolved')
42+
);
43+
expect(hasRefError).toBe(true);
44+
});
45+
46+
it('should not resolve external https references when disabled in validate method', async function() {
47+
const parser = new Parser();
48+
49+
const documentRaw = {
50+
asyncapi: '2.0.0',
51+
info: {
52+
title: 'Test AsyncApi document',
53+
version: '1.0',
54+
},
55+
channels: {
56+
channel: {
57+
publish: {
58+
operationId: 'publish',
59+
message: {
60+
$ref: 'https://example.com/schema.json'
61+
}
62+
},
63+
}
64+
},
65+
};
66+
67+
const diagnostics = await parser.validate(documentRaw, {
68+
__unstable: {
69+
resolver: {
70+
resolveExternal: false
71+
}
72+
}
73+
});
74+
75+
expect(diagnostics.length).toBeGreaterThan(0);
76+
const hasRefError = diagnostics.some(d =>
77+
d.message?.toLowerCase().includes('reference') ||
78+
d.message?.toLowerCase().includes('resolve') ||
79+
d.message?.toLowerCase().includes('unable') ||
80+
d.message?.toLowerCase().includes('unresolved')
81+
);
82+
expect(hasRefError).toBe(true);
83+
});
84+
85+
it('should not resolve external file references when disabled', async function() {
86+
const parser = new Parser({
87+
__unstable: {
88+
resolver: {
89+
resolveExternal: false
90+
}
91+
}
92+
});
93+
94+
const documentRaw = {
95+
asyncapi: '2.0.0',
96+
info: {
97+
title: 'Test AsyncApi document',
98+
version: '1.0',
99+
},
100+
channels: {
101+
channel: {
102+
publish: {
103+
operationId: 'publish',
104+
message: {
105+
$ref: './some-external-file.yaml'
106+
}
107+
},
108+
}
109+
},
110+
};
111+
112+
const diagnostics = await parser.validate(documentRaw);
113+
expect(diagnostics.length).toBeGreaterThan(0);
114+
const hasRefError = diagnostics.some(d =>
115+
d.message?.toLowerCase().includes('reference') ||
116+
d.message?.toLowerCase().includes('resolve') ||
117+
d.message?.toLowerCase().includes('unable') ||
118+
d.message?.toLowerCase().includes('unresolved')
119+
);
120+
expect(hasRefError).toBe(true);
121+
});
122+
123+
it('should still resolve internal references when external resolution is disabled', async function() {
124+
const parser = new Parser({
125+
__unstable: {
126+
resolver: {
127+
resolveExternal: false
128+
}
129+
}
130+
});
131+
132+
const documentRaw = {
133+
asyncapi: '2.0.0',
134+
info: {
135+
title: 'Test AsyncApi document',
136+
version: '1.0',
137+
},
138+
channels: {
139+
channel: {
140+
publish: {
141+
operationId: 'publish',
142+
message: {
143+
$ref: '#/components/messages/message'
144+
}
145+
},
146+
}
147+
},
148+
components: {
149+
messages: {
150+
message: {
151+
payload: {
152+
type: 'string'
153+
}
154+
}
155+
}
156+
}
157+
};
158+
159+
const { document, diagnostics } = await parser.parse(documentRaw);
160+
161+
// Document should be parsed successfully
162+
expect(document).toBeDefined();
163+
expect(document).toBeInstanceOf(Object);
164+
165+
// Internal reference should be resolved - the message should not have $ref anymore
166+
const refMessage = document?.channels().get('channel')?.operations().get('publish')?.messages()[0];
167+
expect(refMessage).toBeDefined();
168+
expect(refMessage?.json('$ref' as any)).toBeUndefined();
169+
170+
// The message should have been resolved to the component
171+
expect(refMessage?.json()).toEqual(document?.components().messages().get('message')?.json());
172+
});
173+
});

0 commit comments

Comments
 (0)