mcp-openapi-schema-explorer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devcontainer/devcontainer.json +24 -0
- package/.github/dependabot.yml +13 -0
- package/.github/workflows/ci.yml +111 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +3 -0
- package/.prettierrc.json +12 -0
- package/.releaserc.json +23 -0
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +3 -0
- package/LICENSE +21 -0
- package/README.md +127 -0
- package/dist/src/config.d.ts +15 -0
- package/dist/src/config.js +19 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/handlers/component-detail-handler.d.ts +14 -0
- package/dist/src/handlers/component-detail-handler.js +87 -0
- package/dist/src/handlers/component-detail-handler.js.map +1 -0
- package/dist/src/handlers/component-map-handler.d.ts +14 -0
- package/dist/src/handlers/component-map-handler.js +63 -0
- package/dist/src/handlers/component-map-handler.js.map +1 -0
- package/dist/src/handlers/handler-utils.d.ts +69 -0
- package/dist/src/handlers/handler-utils.js +180 -0
- package/dist/src/handlers/handler-utils.js.map +1 -0
- package/dist/src/handlers/operation-handler.d.ts +14 -0
- package/dist/src/handlers/operation-handler.js +86 -0
- package/dist/src/handlers/operation-handler.js.map +1 -0
- package/dist/src/handlers/path-item-handler.d.ts +14 -0
- package/dist/src/handlers/path-item-handler.js +66 -0
- package/dist/src/handlers/path-item-handler.js.map +1 -0
- package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
- package/dist/src/handlers/top-level-field-handler.js +72 -0
- package/dist/src/handlers/top-level-field-handler.js.map +1 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +177 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/rendering/components.d.ts +67 -0
- package/dist/src/rendering/components.js +177 -0
- package/dist/src/rendering/components.js.map +1 -0
- package/dist/src/rendering/document.d.ts +36 -0
- package/dist/src/rendering/document.js +147 -0
- package/dist/src/rendering/document.js.map +1 -0
- package/dist/src/rendering/path-item.d.ts +45 -0
- package/dist/src/rendering/path-item.js +141 -0
- package/dist/src/rendering/path-item.js.map +1 -0
- package/dist/src/rendering/paths.d.ts +26 -0
- package/dist/src/rendering/paths.js +78 -0
- package/dist/src/rendering/paths.js.map +1 -0
- package/dist/src/rendering/types.d.ts +50 -0
- package/dist/src/rendering/types.js +12 -0
- package/dist/src/rendering/types.js.map +1 -0
- package/dist/src/rendering/utils.d.ts +31 -0
- package/dist/src/rendering/utils.js +79 -0
- package/dist/src/rendering/utils.js.map +1 -0
- package/dist/src/services/formatters.d.ts +36 -0
- package/dist/src/services/formatters.js +52 -0
- package/dist/src/services/formatters.js.map +1 -0
- package/dist/src/services/reference-transform.d.ts +27 -0
- package/dist/src/services/reference-transform.js +75 -0
- package/dist/src/services/reference-transform.js.map +1 -0
- package/dist/src/services/spec-loader.d.ts +27 -0
- package/dist/src/services/spec-loader.js +77 -0
- package/dist/src/services/spec-loader.js.map +1 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/uri-builder.d.ts +81 -0
- package/dist/src/utils/uri-builder.js +121 -0
- package/dist/src/utils/uri-builder.js.map +1 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +4 -0
- package/dist/src/version.js.map +1 -0
- package/eslint.config.js +88 -0
- package/jest.config.js +32 -0
- package/justfile +66 -0
- package/memory-bank/activeContext.md +139 -0
- package/memory-bank/productContext.md +39 -0
- package/memory-bank/progress.md +141 -0
- package/memory-bank/projectbrief.md +50 -0
- package/memory-bank/systemPatterns.md +224 -0
- package/memory-bank/techContext.md +131 -0
- package/package.json +76 -0
- package/scripts/generate-version.js +49 -0
- package/src/config.ts +33 -0
- package/src/handlers/component-detail-handler.ts +121 -0
- package/src/handlers/component-map-handler.ts +92 -0
- package/src/handlers/handler-utils.ts +230 -0
- package/src/handlers/operation-handler.ts +114 -0
- package/src/handlers/path-item-handler.ts +88 -0
- package/src/handlers/top-level-field-handler.ts +92 -0
- package/src/index.ts +222 -0
- package/src/rendering/components.ts +228 -0
- package/src/rendering/document.ts +167 -0
- package/src/rendering/path-item.ts +157 -0
- package/src/rendering/paths.ts +87 -0
- package/src/rendering/types.ts +63 -0
- package/src/rendering/utils.ts +107 -0
- package/src/services/formatters.ts +71 -0
- package/src/services/reference-transform.ts +105 -0
- package/src/services/spec-loader.ts +88 -0
- package/src/types.ts +17 -0
- package/src/utils/uri-builder.ts +134 -0
- package/src/version.ts +4 -0
- package/test/__tests__/e2e/format.test.ts +224 -0
- package/test/__tests__/e2e/resources.test.ts +369 -0
- package/test/__tests__/e2e/spec-loading.test.ts +172 -0
- package/test/__tests__/unit/config.test.ts +39 -0
- package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
- package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
- package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
- package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
- package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
- package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
- package/test/__tests__/unit/rendering/components.test.ts +269 -0
- package/test/__tests__/unit/rendering/document.test.ts +172 -0
- package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
- package/test/__tests__/unit/rendering/paths.test.ts +115 -0
- package/test/__tests__/unit/services/formatters.test.ts +109 -0
- package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
- package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
- package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
- package/test/fixtures/complex-endpoint.json +146 -0
- package/test/fixtures/empty-api.json +8 -0
- package/test/fixtures/multi-component-types.json +55 -0
- package/test/fixtures/paths-test.json +61 -0
- package/test/fixtures/sample-api.json +68 -0
- package/test/fixtures/sample-v2-api.json +39 -0
- package/test/setup.ts +32 -0
- package/test/utils/console-helpers.ts +48 -0
- package/test/utils/mcp-test-helpers.ts +66 -0
- package/test/utils/test-types.ts +54 -0
- package/tsconfig.json +25 -0
- package/tsconfig.test.json +5 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { RenderableComponents, RenderableComponentMap } from '../../../../src/rendering/components';
|
|
3
|
+
import { RenderContext } from '../../../../src/rendering/types';
|
|
4
|
+
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
|
|
5
|
+
|
|
6
|
+
// Mock Formatter & Context
|
|
7
|
+
const mockFormatter: IFormatter = new JsonFormatter();
|
|
8
|
+
const mockContext: RenderContext = {
|
|
9
|
+
formatter: mockFormatter,
|
|
10
|
+
baseUri: 'openapi://',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Sample Components Object Fixture
|
|
14
|
+
const sampleComponents: OpenAPIV3.ComponentsObject = {
|
|
15
|
+
schemas: {
|
|
16
|
+
User: { type: 'object', properties: { name: { type: 'string' } } },
|
|
17
|
+
Error: { type: 'object', properties: { message: { type: 'string' } } },
|
|
18
|
+
},
|
|
19
|
+
parameters: {
|
|
20
|
+
userIdParam: { name: 'userId', in: 'path', required: true, schema: { type: 'integer' } },
|
|
21
|
+
},
|
|
22
|
+
responses: {
|
|
23
|
+
NotFound: { description: 'Resource not found' },
|
|
24
|
+
},
|
|
25
|
+
// Intentionally empty type
|
|
26
|
+
examples: {},
|
|
27
|
+
// Intentionally missing type
|
|
28
|
+
// securitySchemes: {}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const emptyComponents: OpenAPIV3.ComponentsObject = {};
|
|
32
|
+
|
|
33
|
+
describe('RenderableComponents (List Types)', () => {
|
|
34
|
+
it('should list available component types correctly', () => {
|
|
35
|
+
const renderable = new RenderableComponents(sampleComponents);
|
|
36
|
+
const result = renderable.renderList(mockContext);
|
|
37
|
+
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
expect(result[0].uriSuffix).toBe('components');
|
|
40
|
+
expect(result[0].renderAsList).toBe(true);
|
|
41
|
+
expect(result[0].isError).toBeUndefined();
|
|
42
|
+
expect(result[0].data).toContain('Available Component Types:');
|
|
43
|
+
// Check sorted types
|
|
44
|
+
expect(result[0].data).toMatch(/-\s+examples\n/); // Empty but present
|
|
45
|
+
expect(result[0].data).toMatch(/-\s+parameters\n/);
|
|
46
|
+
expect(result[0].data).toMatch(/-\s+responses\n/);
|
|
47
|
+
expect(result[0].data).toMatch(/-\s+schemas\n/);
|
|
48
|
+
expect(result[0].data).not.toContain('- securitySchemes'); // Missing type
|
|
49
|
+
expect(result[0].data).toContain("Hint: Use 'openapi://components/{type}'");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle empty components object', () => {
|
|
53
|
+
const renderable = new RenderableComponents(emptyComponents);
|
|
54
|
+
const result = renderable.renderList(mockContext);
|
|
55
|
+
expect(result).toHaveLength(1);
|
|
56
|
+
expect(result[0]).toMatchObject({
|
|
57
|
+
uriSuffix: 'components',
|
|
58
|
+
isError: true, // Changed expectation: should be an error if no components
|
|
59
|
+
errorText: 'No components found in the specification.',
|
|
60
|
+
renderAsList: true,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle components object with no valid types', () => {
|
|
65
|
+
// Create object with only an extension property but no valid component types
|
|
66
|
+
const invalidComponents = { 'x-custom': {} } as OpenAPIV3.ComponentsObject;
|
|
67
|
+
const renderable = new RenderableComponents(invalidComponents);
|
|
68
|
+
const result = renderable.renderList(mockContext);
|
|
69
|
+
expect(result).toHaveLength(1);
|
|
70
|
+
expect(result[0]).toMatchObject({
|
|
71
|
+
uriSuffix: 'components',
|
|
72
|
+
isError: true,
|
|
73
|
+
errorText: 'No valid component types found.',
|
|
74
|
+
renderAsList: true,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle undefined components object', () => {
|
|
79
|
+
const renderable = new RenderableComponents(undefined);
|
|
80
|
+
const result = renderable.renderList(mockContext);
|
|
81
|
+
expect(result).toHaveLength(1);
|
|
82
|
+
expect(result[0]).toMatchObject({
|
|
83
|
+
uriSuffix: 'components',
|
|
84
|
+
isError: true,
|
|
85
|
+
errorText: 'No components found in the specification.',
|
|
86
|
+
renderAsList: true,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('renderDetail should delegate to renderList', () => {
|
|
91
|
+
const renderable = new RenderableComponents(sampleComponents);
|
|
92
|
+
const listResult = renderable.renderList(mockContext);
|
|
93
|
+
const detailResult = renderable.renderDetail(mockContext);
|
|
94
|
+
expect(detailResult).toEqual(listResult);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('getComponentMap should return correct map', () => {
|
|
98
|
+
const renderable = new RenderableComponents(sampleComponents);
|
|
99
|
+
expect(renderable.getComponentMap('schemas')).toBe(sampleComponents.schemas);
|
|
100
|
+
expect(renderable.getComponentMap('parameters')).toBe(sampleComponents.parameters);
|
|
101
|
+
expect(renderable.getComponentMap('examples')).toBe(sampleComponents.examples);
|
|
102
|
+
expect(renderable.getComponentMap('securitySchemes')).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('RenderableComponentMap (List/Detail Names)', () => {
|
|
107
|
+
const schemasMap = sampleComponents.schemas;
|
|
108
|
+
const parametersMap = sampleComponents.parameters;
|
|
109
|
+
const emptyMap = sampleComponents.examples;
|
|
110
|
+
const schemasUriSuffix = 'components/schemas';
|
|
111
|
+
const paramsUriSuffix = 'components/parameters';
|
|
112
|
+
|
|
113
|
+
describe('renderList (List Names)', () => {
|
|
114
|
+
it('should list component names correctly (schemas)', () => {
|
|
115
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
116
|
+
const result = renderable.renderList(mockContext);
|
|
117
|
+
expect(result).toHaveLength(1);
|
|
118
|
+
expect(result[0].uriSuffix).toBe(schemasUriSuffix);
|
|
119
|
+
expect(result[0].renderAsList).toBe(true);
|
|
120
|
+
expect(result[0].isError).toBeUndefined();
|
|
121
|
+
expect(result[0].data).toContain('Available schemas:');
|
|
122
|
+
expect(result[0].data).toMatch(/-\s+Error\n/); // Sorted
|
|
123
|
+
expect(result[0].data).toMatch(/-\s+User\n/);
|
|
124
|
+
expect(result[0].data).toContain("Hint: Use 'openapi://components/schemas/{name}'");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should list component names correctly (parameters)', () => {
|
|
128
|
+
const renderable = new RenderableComponentMap(parametersMap, 'parameters', paramsUriSuffix);
|
|
129
|
+
const result = renderable.renderList(mockContext);
|
|
130
|
+
expect(result).toHaveLength(1);
|
|
131
|
+
expect(result[0].uriSuffix).toBe(paramsUriSuffix);
|
|
132
|
+
expect(result[0].data).toContain('Available parameters:');
|
|
133
|
+
expect(result[0].data).toMatch(/-\s+userIdParam\n/);
|
|
134
|
+
expect(result[0].data).toContain("Hint: Use 'openapi://components/parameters/{name}'");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle empty component map', () => {
|
|
138
|
+
const renderable = new RenderableComponentMap(emptyMap, 'examples', 'components/examples');
|
|
139
|
+
const result = renderable.renderList(mockContext);
|
|
140
|
+
expect(result).toHaveLength(1);
|
|
141
|
+
expect(result[0]).toMatchObject({
|
|
142
|
+
uriSuffix: 'components/examples',
|
|
143
|
+
isError: true,
|
|
144
|
+
errorText: 'No components of type "examples" found.',
|
|
145
|
+
renderAsList: true,
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle undefined component map', () => {
|
|
150
|
+
const renderable = new RenderableComponentMap(
|
|
151
|
+
undefined,
|
|
152
|
+
'securitySchemes',
|
|
153
|
+
'components/securitySchemes'
|
|
154
|
+
);
|
|
155
|
+
const result = renderable.renderList(mockContext);
|
|
156
|
+
expect(result).toHaveLength(1);
|
|
157
|
+
expect(result[0]).toMatchObject({
|
|
158
|
+
uriSuffix: 'components/securitySchemes',
|
|
159
|
+
isError: true,
|
|
160
|
+
errorText: 'No components of type "securitySchemes" found.',
|
|
161
|
+
renderAsList: true,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('renderComponentDetail (Get Component Detail)', () => {
|
|
167
|
+
it('should return detail for a single valid component', () => {
|
|
168
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
169
|
+
const result = renderable.renderComponentDetail(mockContext, ['User']);
|
|
170
|
+
expect(result).toHaveLength(1);
|
|
171
|
+
expect(result[0]).toEqual({
|
|
172
|
+
uriSuffix: `${schemasUriSuffix}/User`,
|
|
173
|
+
data: schemasMap?.User, // Expect raw component object
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should return details for multiple valid components', () => {
|
|
178
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
179
|
+
const result = renderable.renderComponentDetail(mockContext, ['Error', 'User']);
|
|
180
|
+
expect(result).toHaveLength(2);
|
|
181
|
+
expect(result).toContainEqual({
|
|
182
|
+
uriSuffix: `${schemasUriSuffix}/Error`,
|
|
183
|
+
data: schemasMap?.Error,
|
|
184
|
+
});
|
|
185
|
+
expect(result).toContainEqual({
|
|
186
|
+
uriSuffix: `${schemasUriSuffix}/User`,
|
|
187
|
+
data: schemasMap?.User,
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return error for non-existent component', () => {
|
|
192
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
193
|
+
const result = renderable.renderComponentDetail(mockContext, ['NonExistent']);
|
|
194
|
+
expect(result).toHaveLength(1);
|
|
195
|
+
expect(result[0]).toEqual({
|
|
196
|
+
uriSuffix: `${schemasUriSuffix}/NonExistent`,
|
|
197
|
+
data: null,
|
|
198
|
+
isError: true,
|
|
199
|
+
errorText: 'Component "NonExistent" of type "schemas" not found.',
|
|
200
|
+
renderAsList: true,
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should handle mix of valid and invalid components', () => {
|
|
205
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
206
|
+
const result = renderable.renderComponentDetail(mockContext, ['User', 'Invalid']);
|
|
207
|
+
expect(result).toHaveLength(2);
|
|
208
|
+
expect(result).toContainEqual({
|
|
209
|
+
uriSuffix: `${schemasUriSuffix}/User`,
|
|
210
|
+
data: schemasMap?.User,
|
|
211
|
+
});
|
|
212
|
+
expect(result).toContainEqual({
|
|
213
|
+
uriSuffix: `${schemasUriSuffix}/Invalid`,
|
|
214
|
+
data: null,
|
|
215
|
+
isError: true,
|
|
216
|
+
errorText: 'Component "Invalid" of type "schemas" not found.',
|
|
217
|
+
renderAsList: true,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should return error if component map is undefined', () => {
|
|
222
|
+
const renderable = new RenderableComponentMap(
|
|
223
|
+
undefined,
|
|
224
|
+
'securitySchemes',
|
|
225
|
+
'components/securitySchemes'
|
|
226
|
+
);
|
|
227
|
+
const result = renderable.renderComponentDetail(mockContext, ['apiKey']);
|
|
228
|
+
expect(result).toHaveLength(1);
|
|
229
|
+
expect(result[0]).toEqual({
|
|
230
|
+
uriSuffix: 'components/securitySchemes/apiKey',
|
|
231
|
+
data: null,
|
|
232
|
+
isError: true,
|
|
233
|
+
errorText: 'Component map for type "securitySchemes" not found.',
|
|
234
|
+
renderAsList: true,
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('renderDetail (Interface Method)', () => {
|
|
240
|
+
it('should delegate to renderList', () => {
|
|
241
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
242
|
+
const listResult = renderable.renderList(mockContext);
|
|
243
|
+
const detailResult = renderable.renderDetail(mockContext);
|
|
244
|
+
expect(detailResult).toEqual(listResult);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('getComponent', () => {
|
|
249
|
+
it('should return correct component object', () => {
|
|
250
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
251
|
+
expect(renderable.getComponent('User')).toBe(schemasMap?.User);
|
|
252
|
+
expect(renderable.getComponent('Error')).toBe(schemasMap?.Error);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should return undefined for non-existent component', () => {
|
|
256
|
+
const renderable = new RenderableComponentMap(schemasMap, 'schemas', schemasUriSuffix);
|
|
257
|
+
expect(renderable.getComponent('NonExistent')).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should return undefined if component map is undefined', () => {
|
|
261
|
+
const renderable = new RenderableComponentMap(
|
|
262
|
+
undefined,
|
|
263
|
+
'securitySchemes',
|
|
264
|
+
'components/securitySchemes'
|
|
265
|
+
);
|
|
266
|
+
expect(renderable.getComponent('apiKey')).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { RenderableDocument } from '../../../../src/rendering/document';
|
|
3
|
+
import { RenderContext } from '../../../../src/rendering/types';
|
|
4
|
+
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
|
|
5
|
+
|
|
6
|
+
// Mock Formatter
|
|
7
|
+
const mockFormatter: IFormatter = new JsonFormatter(); // Use JSON for predictable output
|
|
8
|
+
|
|
9
|
+
const mockContext: RenderContext = {
|
|
10
|
+
formatter: mockFormatter,
|
|
11
|
+
baseUri: 'openapi://',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Sample OpenAPI Document Fixture
|
|
15
|
+
const sampleDoc: OpenAPIV3.Document = {
|
|
16
|
+
openapi: '3.0.0',
|
|
17
|
+
info: {
|
|
18
|
+
title: 'Test API',
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
},
|
|
21
|
+
paths: {
|
|
22
|
+
'/test': {
|
|
23
|
+
get: {
|
|
24
|
+
summary: 'Test GET',
|
|
25
|
+
responses: {
|
|
26
|
+
'200': { description: 'OK' },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
components: {
|
|
32
|
+
schemas: {
|
|
33
|
+
TestSchema: { type: 'string' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
servers: [{ url: 'http://localhost:3000' }],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe('RenderableDocument', () => {
|
|
40
|
+
let renderableDoc: RenderableDocument;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
renderableDoc = new RenderableDocument(sampleDoc);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should instantiate correctly', () => {
|
|
47
|
+
expect(renderableDoc).toBeInstanceOf(RenderableDocument);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Test the internal detail rendering method
|
|
51
|
+
describe('renderTopLevelFieldDetail', () => {
|
|
52
|
+
it('should render detail for a valid top-level field (info)', () => {
|
|
53
|
+
const fieldObject = renderableDoc.getTopLevelField('info');
|
|
54
|
+
const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'info');
|
|
55
|
+
|
|
56
|
+
expect(result).toHaveLength(1);
|
|
57
|
+
expect(result[0]).toEqual({
|
|
58
|
+
uriSuffix: 'info',
|
|
59
|
+
data: sampleDoc.info, // Expect raw data
|
|
60
|
+
isError: undefined, // Should default to false implicitly
|
|
61
|
+
renderAsList: undefined, // Should default to false implicitly
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should render detail for another valid field (servers)', () => {
|
|
66
|
+
const fieldObject = renderableDoc.getTopLevelField('servers');
|
|
67
|
+
const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'servers');
|
|
68
|
+
|
|
69
|
+
expect(result).toHaveLength(1);
|
|
70
|
+
expect(result[0]).toEqual({
|
|
71
|
+
uriSuffix: 'servers',
|
|
72
|
+
data: sampleDoc.servers,
|
|
73
|
+
isError: undefined,
|
|
74
|
+
renderAsList: undefined,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return error for non-existent field', () => {
|
|
79
|
+
const fieldObject = renderableDoc.getTopLevelField('nonexistent');
|
|
80
|
+
const result = renderableDoc.renderTopLevelFieldDetail(
|
|
81
|
+
mockContext,
|
|
82
|
+
fieldObject, // Will be undefined
|
|
83
|
+
'nonexistent'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(result).toHaveLength(1);
|
|
87
|
+
expect(result[0]).toEqual({
|
|
88
|
+
uriSuffix: 'nonexistent',
|
|
89
|
+
data: null,
|
|
90
|
+
isError: true,
|
|
91
|
+
errorText: 'Error: Field "nonexistent" not found in the OpenAPI document.',
|
|
92
|
+
renderAsList: true,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return error when trying to render "paths" via detail method', () => {
|
|
97
|
+
const fieldObject = renderableDoc.getTopLevelField('paths');
|
|
98
|
+
const result = renderableDoc.renderTopLevelFieldDetail(mockContext, fieldObject, 'paths');
|
|
99
|
+
|
|
100
|
+
expect(result).toHaveLength(1);
|
|
101
|
+
expect(result[0]).toEqual({
|
|
102
|
+
uriSuffix: 'paths',
|
|
103
|
+
data: null,
|
|
104
|
+
isError: true,
|
|
105
|
+
errorText: `Error: Field "paths" should be accessed via its list view (${mockContext.baseUri}paths). Use the list view first.`,
|
|
106
|
+
renderAsList: true,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return error when trying to render "components" via detail method', () => {
|
|
111
|
+
const fieldObject = renderableDoc.getTopLevelField('components');
|
|
112
|
+
const result = renderableDoc.renderTopLevelFieldDetail(
|
|
113
|
+
mockContext,
|
|
114
|
+
fieldObject,
|
|
115
|
+
'components'
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(1);
|
|
119
|
+
expect(result[0]).toEqual({
|
|
120
|
+
uriSuffix: 'components',
|
|
121
|
+
data: null,
|
|
122
|
+
isError: true,
|
|
123
|
+
errorText: `Error: Field "components" should be accessed via its list view (${mockContext.baseUri}components). Use the list view first.`,
|
|
124
|
+
renderAsList: true,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Test the interface methods (which currently return errors)
|
|
130
|
+
describe('Interface Methods', () => {
|
|
131
|
+
it('renderList should return error', () => {
|
|
132
|
+
const result = renderableDoc.renderList(mockContext);
|
|
133
|
+
expect(result).toHaveLength(1);
|
|
134
|
+
expect(result[0]).toMatchObject({
|
|
135
|
+
uriSuffix: 'error',
|
|
136
|
+
isError: true,
|
|
137
|
+
errorText: expect.stringContaining(
|
|
138
|
+
'List rendering is only supported for specific fields'
|
|
139
|
+
) as string,
|
|
140
|
+
renderAsList: true,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('renderDetail should return error', () => {
|
|
145
|
+
const result = renderableDoc.renderDetail(mockContext);
|
|
146
|
+
expect(result).toHaveLength(1);
|
|
147
|
+
expect(result[0]).toMatchObject({
|
|
148
|
+
uriSuffix: 'error',
|
|
149
|
+
isError: true,
|
|
150
|
+
errorText: expect.stringContaining(
|
|
151
|
+
'Detail rendering requires specifying a top-level field'
|
|
152
|
+
) as string,
|
|
153
|
+
renderAsList: true,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Test helper methods
|
|
159
|
+
describe('Helper Methods', () => {
|
|
160
|
+
it('getPathsObject should return paths', () => {
|
|
161
|
+
expect(renderableDoc.getPathsObject()).toBe(sampleDoc.paths);
|
|
162
|
+
});
|
|
163
|
+
it('getComponentsObject should return components', () => {
|
|
164
|
+
expect(renderableDoc.getComponentsObject()).toBe(sampleDoc.components);
|
|
165
|
+
});
|
|
166
|
+
it('getTopLevelField should return correct field', () => {
|
|
167
|
+
expect(renderableDoc.getTopLevelField('info')).toBe(sampleDoc.info);
|
|
168
|
+
expect(renderableDoc.getTopLevelField('servers')).toBe(sampleDoc.servers);
|
|
169
|
+
expect(renderableDoc.getTopLevelField('nonexistent')).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { RenderablePathItem } from '../../../../src/rendering/path-item';
|
|
3
|
+
import { RenderContext } from '../../../../src/rendering/types';
|
|
4
|
+
import { IFormatter, JsonFormatter } from '../../../../src/services/formatters';
|
|
5
|
+
|
|
6
|
+
// Mock Formatter & Context
|
|
7
|
+
const mockFormatter: IFormatter = new JsonFormatter();
|
|
8
|
+
const mockContext: RenderContext = {
|
|
9
|
+
formatter: mockFormatter,
|
|
10
|
+
baseUri: 'openapi://',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Sample PathItem Object Fixture
|
|
14
|
+
const samplePathItem: OpenAPIV3.PathItemObject = {
|
|
15
|
+
get: {
|
|
16
|
+
summary: 'Get Item',
|
|
17
|
+
responses: { '200': { description: 'OK' } },
|
|
18
|
+
},
|
|
19
|
+
post: {
|
|
20
|
+
summary: 'Create Item',
|
|
21
|
+
responses: { '201': { description: 'Created' } },
|
|
22
|
+
},
|
|
23
|
+
delete: {
|
|
24
|
+
// No summary
|
|
25
|
+
responses: { '204': { description: 'No Content' } },
|
|
26
|
+
},
|
|
27
|
+
parameters: [
|
|
28
|
+
// Example path-level parameter
|
|
29
|
+
{ name: 'commonParam', in: 'query', schema: { type: 'string' } },
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Define both the raw path and the expected suffix (built using the builder logic)
|
|
34
|
+
const rawPath = '/items';
|
|
35
|
+
const pathUriSuffix = 'paths/items'; // Builder removes leading '/' and encodes, but '/items' has no special chars
|
|
36
|
+
|
|
37
|
+
describe('RenderablePathItem', () => {
|
|
38
|
+
describe('renderList (List Methods)', () => {
|
|
39
|
+
it('should render a list of methods correctly', () => {
|
|
40
|
+
// Provide all 3 arguments to constructor
|
|
41
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
42
|
+
const result = renderable.renderList(mockContext);
|
|
43
|
+
|
|
44
|
+
expect(result).toHaveLength(1);
|
|
45
|
+
expect(result[0].uriSuffix).toBe(pathUriSuffix);
|
|
46
|
+
expect(result[0].renderAsList).toBe(true);
|
|
47
|
+
expect(result[0].isError).toBeUndefined();
|
|
48
|
+
|
|
49
|
+
// Define expected output lines based on the new format and builder logic
|
|
50
|
+
// generateListHint uses buildOperationUriSuffix which encodes the path
|
|
51
|
+
// Since rawPath is '/items', encoded is 'items'.
|
|
52
|
+
const expectedHint =
|
|
53
|
+
"Hint: Use 'openapi://paths/items/{method}' to view details for a specific operation.";
|
|
54
|
+
const expectedLineDelete = 'DELETE'; // No summary/opId
|
|
55
|
+
const expectedLineGet = 'GET: Get Item'; // Summary exists
|
|
56
|
+
const expectedLinePost = 'POST: Create Item'; // Summary exists
|
|
57
|
+
const expectedOutput = `${expectedHint}\n\n${expectedLineDelete}\n${expectedLineGet}\n${expectedLinePost}`;
|
|
58
|
+
|
|
59
|
+
// Check the full output string
|
|
60
|
+
expect(result[0].data).toBe(expectedOutput);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle path item with no standard methods', () => {
|
|
64
|
+
const noMethodsPathItem: OpenAPIV3.PathItemObject = {
|
|
65
|
+
parameters: samplePathItem.parameters,
|
|
66
|
+
};
|
|
67
|
+
// Provide all 3 arguments to constructor
|
|
68
|
+
const renderable = new RenderablePathItem(noMethodsPathItem, rawPath, pathUriSuffix);
|
|
69
|
+
const result = renderable.renderList(mockContext);
|
|
70
|
+
expect(result).toHaveLength(1);
|
|
71
|
+
expect(result[0]).toEqual({
|
|
72
|
+
uriSuffix: pathUriSuffix,
|
|
73
|
+
data: 'No standard HTTP methods found for path: items',
|
|
74
|
+
renderAsList: true,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return error if path item is undefined', () => {
|
|
79
|
+
// Provide all 3 arguments to constructor
|
|
80
|
+
const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
|
|
81
|
+
const result = renderable.renderList(mockContext);
|
|
82
|
+
expect(result).toHaveLength(1);
|
|
83
|
+
expect(result[0]).toMatchObject({
|
|
84
|
+
uriSuffix: pathUriSuffix,
|
|
85
|
+
isError: true,
|
|
86
|
+
errorText: 'Path item not found.',
|
|
87
|
+
renderAsList: true,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('renderOperationDetail (Get Operation Detail)', () => {
|
|
93
|
+
it('should return detail for a single valid method', () => {
|
|
94
|
+
// Provide all 3 arguments to constructor
|
|
95
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
96
|
+
const result = renderable.renderOperationDetail(mockContext, ['get']);
|
|
97
|
+
expect(result).toHaveLength(1);
|
|
98
|
+
expect(result[0]).toEqual({
|
|
99
|
+
uriSuffix: `${pathUriSuffix}/get`,
|
|
100
|
+
data: samplePathItem.get, // Expect raw operation object
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should return details for multiple valid methods', () => {
|
|
105
|
+
// Provide all 3 arguments to constructor
|
|
106
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
107
|
+
const result = renderable.renderOperationDetail(mockContext, ['post', 'delete']);
|
|
108
|
+
expect(result).toHaveLength(2);
|
|
109
|
+
expect(result).toContainEqual({
|
|
110
|
+
uriSuffix: `${pathUriSuffix}/post`,
|
|
111
|
+
data: samplePathItem.post,
|
|
112
|
+
});
|
|
113
|
+
expect(result).toContainEqual({
|
|
114
|
+
uriSuffix: `${pathUriSuffix}/delete`,
|
|
115
|
+
data: samplePathItem.delete,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return error for non-existent method', () => {
|
|
120
|
+
// Provide all 3 arguments to constructor
|
|
121
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
122
|
+
const result = renderable.renderOperationDetail(mockContext, ['put']);
|
|
123
|
+
expect(result).toHaveLength(1);
|
|
124
|
+
expect(result[0]).toEqual({
|
|
125
|
+
uriSuffix: `${pathUriSuffix}/put`,
|
|
126
|
+
data: null,
|
|
127
|
+
isError: true,
|
|
128
|
+
errorText: 'Method "PUT" not found for path.',
|
|
129
|
+
renderAsList: true,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle mix of valid and invalid methods', () => {
|
|
134
|
+
// Provide all 3 arguments to constructor
|
|
135
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
136
|
+
const result = renderable.renderOperationDetail(mockContext, ['get', 'patch']);
|
|
137
|
+
expect(result).toHaveLength(2);
|
|
138
|
+
expect(result).toContainEqual({
|
|
139
|
+
uriSuffix: `${pathUriSuffix}/get`,
|
|
140
|
+
data: samplePathItem.get,
|
|
141
|
+
});
|
|
142
|
+
expect(result).toContainEqual({
|
|
143
|
+
uriSuffix: `${pathUriSuffix}/patch`,
|
|
144
|
+
data: null,
|
|
145
|
+
isError: true,
|
|
146
|
+
errorText: 'Method "PATCH" not found for path.',
|
|
147
|
+
renderAsList: true,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return error if path item is undefined', () => {
|
|
152
|
+
// Provide all 3 arguments to constructor
|
|
153
|
+
const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
|
|
154
|
+
const result = renderable.renderOperationDetail(mockContext, ['get']);
|
|
155
|
+
expect(result).toHaveLength(1);
|
|
156
|
+
expect(result[0]).toEqual({
|
|
157
|
+
uriSuffix: `${pathUriSuffix}/get`,
|
|
158
|
+
data: null,
|
|
159
|
+
isError: true,
|
|
160
|
+
errorText: 'Path item not found.',
|
|
161
|
+
renderAsList: true,
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('renderDetail (Interface Method)', () => {
|
|
167
|
+
it('should delegate to renderList', () => {
|
|
168
|
+
// Provide all 3 arguments to constructor
|
|
169
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
170
|
+
const listResult = renderable.renderList(mockContext);
|
|
171
|
+
const detailResult = renderable.renderDetail(mockContext);
|
|
172
|
+
expect(detailResult).toEqual(listResult);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('getOperation', () => {
|
|
177
|
+
it('should return correct operation object (case-insensitive)', () => {
|
|
178
|
+
// Provide all 3 arguments to constructor
|
|
179
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
180
|
+
expect(renderable.getOperation('get')).toBe(samplePathItem.get);
|
|
181
|
+
expect(renderable.getOperation('POST')).toBe(samplePathItem.post);
|
|
182
|
+
expect(renderable.getOperation('Delete')).toBe(samplePathItem.delete);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should return undefined for non-existent method', () => {
|
|
186
|
+
// Provide all 3 arguments to constructor
|
|
187
|
+
const renderable = new RenderablePathItem(samplePathItem, rawPath, pathUriSuffix);
|
|
188
|
+
expect(renderable.getOperation('put')).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should return undefined if path item is undefined', () => {
|
|
192
|
+
// Provide all 3 arguments to constructor
|
|
193
|
+
const renderable = new RenderablePathItem(undefined, rawPath, pathUriSuffix);
|
|
194
|
+
expect(renderable.getOperation('get')).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|