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.
Files changed (133) hide show
  1. package/.devcontainer/devcontainer.json +24 -0
  2. package/.github/dependabot.yml +13 -0
  3. package/.github/workflows/ci.yml +111 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.prettierignore +3 -0
  6. package/.prettierrc.json +12 -0
  7. package/.releaserc.json +23 -0
  8. package/CHANGELOG.md +32 -0
  9. package/CONTRIBUTING.md +67 -0
  10. package/Dockerfile +3 -0
  11. package/LICENSE +21 -0
  12. package/README.md +127 -0
  13. package/dist/src/config.d.ts +15 -0
  14. package/dist/src/config.js +19 -0
  15. package/dist/src/config.js.map +1 -0
  16. package/dist/src/handlers/component-detail-handler.d.ts +14 -0
  17. package/dist/src/handlers/component-detail-handler.js +87 -0
  18. package/dist/src/handlers/component-detail-handler.js.map +1 -0
  19. package/dist/src/handlers/component-map-handler.d.ts +14 -0
  20. package/dist/src/handlers/component-map-handler.js +63 -0
  21. package/dist/src/handlers/component-map-handler.js.map +1 -0
  22. package/dist/src/handlers/handler-utils.d.ts +69 -0
  23. package/dist/src/handlers/handler-utils.js +180 -0
  24. package/dist/src/handlers/handler-utils.js.map +1 -0
  25. package/dist/src/handlers/operation-handler.d.ts +14 -0
  26. package/dist/src/handlers/operation-handler.js +86 -0
  27. package/dist/src/handlers/operation-handler.js.map +1 -0
  28. package/dist/src/handlers/path-item-handler.d.ts +14 -0
  29. package/dist/src/handlers/path-item-handler.js +66 -0
  30. package/dist/src/handlers/path-item-handler.js.map +1 -0
  31. package/dist/src/handlers/top-level-field-handler.d.ts +14 -0
  32. package/dist/src/handlers/top-level-field-handler.js +72 -0
  33. package/dist/src/handlers/top-level-field-handler.js.map +1 -0
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/index.js +177 -0
  36. package/dist/src/index.js.map +1 -0
  37. package/dist/src/rendering/components.d.ts +67 -0
  38. package/dist/src/rendering/components.js +177 -0
  39. package/dist/src/rendering/components.js.map +1 -0
  40. package/dist/src/rendering/document.d.ts +36 -0
  41. package/dist/src/rendering/document.js +147 -0
  42. package/dist/src/rendering/document.js.map +1 -0
  43. package/dist/src/rendering/path-item.d.ts +45 -0
  44. package/dist/src/rendering/path-item.js +141 -0
  45. package/dist/src/rendering/path-item.js.map +1 -0
  46. package/dist/src/rendering/paths.d.ts +26 -0
  47. package/dist/src/rendering/paths.js +78 -0
  48. package/dist/src/rendering/paths.js.map +1 -0
  49. package/dist/src/rendering/types.d.ts +50 -0
  50. package/dist/src/rendering/types.js +12 -0
  51. package/dist/src/rendering/types.js.map +1 -0
  52. package/dist/src/rendering/utils.d.ts +31 -0
  53. package/dist/src/rendering/utils.js +79 -0
  54. package/dist/src/rendering/utils.js.map +1 -0
  55. package/dist/src/services/formatters.d.ts +36 -0
  56. package/dist/src/services/formatters.js +52 -0
  57. package/dist/src/services/formatters.js.map +1 -0
  58. package/dist/src/services/reference-transform.d.ts +27 -0
  59. package/dist/src/services/reference-transform.js +75 -0
  60. package/dist/src/services/reference-transform.js.map +1 -0
  61. package/dist/src/services/spec-loader.d.ts +27 -0
  62. package/dist/src/services/spec-loader.js +77 -0
  63. package/dist/src/services/spec-loader.js.map +1 -0
  64. package/dist/src/types.d.ts +11 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/dist/src/utils/uri-builder.d.ts +81 -0
  68. package/dist/src/utils/uri-builder.js +121 -0
  69. package/dist/src/utils/uri-builder.js.map +1 -0
  70. package/dist/src/version.d.ts +1 -0
  71. package/dist/src/version.js +4 -0
  72. package/dist/src/version.js.map +1 -0
  73. package/eslint.config.js +88 -0
  74. package/jest.config.js +32 -0
  75. package/justfile +66 -0
  76. package/memory-bank/activeContext.md +139 -0
  77. package/memory-bank/productContext.md +39 -0
  78. package/memory-bank/progress.md +141 -0
  79. package/memory-bank/projectbrief.md +50 -0
  80. package/memory-bank/systemPatterns.md +224 -0
  81. package/memory-bank/techContext.md +131 -0
  82. package/package.json +76 -0
  83. package/scripts/generate-version.js +49 -0
  84. package/src/config.ts +33 -0
  85. package/src/handlers/component-detail-handler.ts +121 -0
  86. package/src/handlers/component-map-handler.ts +92 -0
  87. package/src/handlers/handler-utils.ts +230 -0
  88. package/src/handlers/operation-handler.ts +114 -0
  89. package/src/handlers/path-item-handler.ts +88 -0
  90. package/src/handlers/top-level-field-handler.ts +92 -0
  91. package/src/index.ts +222 -0
  92. package/src/rendering/components.ts +228 -0
  93. package/src/rendering/document.ts +167 -0
  94. package/src/rendering/path-item.ts +157 -0
  95. package/src/rendering/paths.ts +87 -0
  96. package/src/rendering/types.ts +63 -0
  97. package/src/rendering/utils.ts +107 -0
  98. package/src/services/formatters.ts +71 -0
  99. package/src/services/reference-transform.ts +105 -0
  100. package/src/services/spec-loader.ts +88 -0
  101. package/src/types.ts +17 -0
  102. package/src/utils/uri-builder.ts +134 -0
  103. package/src/version.ts +4 -0
  104. package/test/__tests__/e2e/format.test.ts +224 -0
  105. package/test/__tests__/e2e/resources.test.ts +369 -0
  106. package/test/__tests__/e2e/spec-loading.test.ts +172 -0
  107. package/test/__tests__/unit/config.test.ts +39 -0
  108. package/test/__tests__/unit/handlers/component-detail-handler.test.ts +241 -0
  109. package/test/__tests__/unit/handlers/component-map-handler.test.ts +187 -0
  110. package/test/__tests__/unit/handlers/handler-utils.test.ts +255 -0
  111. package/test/__tests__/unit/handlers/operation-handler.test.ts +202 -0
  112. package/test/__tests__/unit/handlers/path-item-handler.test.ts +153 -0
  113. package/test/__tests__/unit/handlers/top-level-field-handler.test.ts +182 -0
  114. package/test/__tests__/unit/rendering/components.test.ts +269 -0
  115. package/test/__tests__/unit/rendering/document.test.ts +172 -0
  116. package/test/__tests__/unit/rendering/path-item.test.ts +197 -0
  117. package/test/__tests__/unit/rendering/paths.test.ts +115 -0
  118. package/test/__tests__/unit/services/formatters.test.ts +109 -0
  119. package/test/__tests__/unit/services/reference-transform.test.ts +320 -0
  120. package/test/__tests__/unit/services/spec-loader.test.ts +214 -0
  121. package/test/__tests__/unit/utils/uri-builder.test.ts +103 -0
  122. package/test/fixtures/complex-endpoint.json +146 -0
  123. package/test/fixtures/empty-api.json +8 -0
  124. package/test/fixtures/multi-component-types.json +55 -0
  125. package/test/fixtures/paths-test.json +61 -0
  126. package/test/fixtures/sample-api.json +68 -0
  127. package/test/fixtures/sample-v2-api.json +39 -0
  128. package/test/setup.ts +32 -0
  129. package/test/utils/console-helpers.ts +48 -0
  130. package/test/utils/mcp-test-helpers.ts +66 -0
  131. package/test/utils/test-types.ts +54 -0
  132. package/tsconfig.json +25 -0
  133. 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
+ });