@redocly/openapi-core 1.9.0 → 1.9.1

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 (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/lib/bundle.d.ts +1 -1
  3. package/lib/lint.d.ts +1 -0
  4. package/lib/lint.js +1 -1
  5. package/lib/oas-types.d.ts +1 -1
  6. package/lib/ref-utils.js +2 -2
  7. package/lib/resolve.js +9 -1
  8. package/lib/types/index.d.ts +7 -7
  9. package/lib/types/json-schema-adapter.d.ts +3 -0
  10. package/lib/types/json-schema-adapter.js +173 -0
  11. package/lib/types/oas2.d.ts +3 -2
  12. package/lib/types/oas3.d.ts +3 -2
  13. package/lib/types/oas3_1.d.ts +3 -2
  14. package/lib/types/portal-config-schema.d.ts +5261 -52
  15. package/lib/types/portal-config-schema.js +71 -55
  16. package/lib/types/redocly-yaml.d.ts +13 -1
  17. package/lib/types/redocly-yaml.js +101 -39
  18. package/lib/types/theme-config.d.ts +819 -36
  19. package/lib/types/theme-config.js +67 -29
  20. package/lib/utils.d.ts +2 -2
  21. package/lib/visitors.js +1 -1
  22. package/lib/walk.js +7 -1
  23. package/package.json +1 -1
  24. package/src/__tests__/lint.test.ts +1218 -36
  25. package/src/__tests__/ref-utils.test.ts +22 -0
  26. package/src/config/__tests__/load.test.ts +13 -13
  27. package/src/decorators/oas2/remove-unused-components.ts +3 -2
  28. package/src/decorators/oas3/remove-unused-components.ts +3 -2
  29. package/src/lint.ts +2 -1
  30. package/src/ref-utils.ts +2 -2
  31. package/src/resolve.ts +13 -1
  32. package/src/types/index.ts +7 -12
  33. package/src/types/json-schema-adapter.ts +217 -0
  34. package/src/types/oas2.ts +5 -2
  35. package/src/types/oas3.ts +6 -2
  36. package/src/types/oas3_1.ts +5 -2
  37. package/src/types/portal-config-schema.ts +111 -61
  38. package/src/types/redocly-yaml.ts +118 -43
  39. package/src/types/theme-config.ts +125 -27
  40. package/src/utils.ts +2 -2
  41. package/src/visitors.ts +1 -1
  42. package/src/walk.ts +7 -1
  43. package/tsconfig.tsbuildinfo +1 -1
@@ -96,6 +96,28 @@ describe('ref-utils', () => {
96
96
  expect(result).toMatchInlineSnapshot(`[]`);
97
97
  });
98
98
 
99
+ it('should parse a ref correctly', () => {
100
+ expect(parseRef('./info.yaml#/description')).toEqual({
101
+ uri: './info.yaml',
102
+ pointer: ['description'],
103
+ });
104
+ });
105
+
106
+ it('should parse a ref which contain a hash in the middle', () => {
107
+ // Here `info#description.md` is a file name
108
+ expect(parseRef('./info#description.md')).toEqual({
109
+ uri: './info#description.md',
110
+ pointer: [],
111
+ });
112
+ });
113
+
114
+ it('should parse a ref which ends with a hash', () => {
115
+ expect(parseRef('./info.yaml#')).toEqual({
116
+ uri: './info.yaml',
117
+ pointer: [],
118
+ });
119
+ });
120
+
99
121
  describe('refBaseName', () => {
100
122
  it('returns base name for file reference', () => {
101
123
  expect(refBaseName('../testcase/Pet.yaml')).toStrictEqual('Pet');
@@ -137,6 +137,19 @@ describe('getConfig', () => {
137
137
  "severity": "warn",
138
138
  "suggest": [],
139
139
  },
140
+ {
141
+ "location": [
142
+ {
143
+ "pointer": "#/theme",
144
+ "reportOnKey": false,
145
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
146
+ },
147
+ ],
148
+ "message": "Can't resolve $ref: ENOENT: no such file or directory 'fixtures/resolve-refs-in-config/wrong-ref.yaml'",
149
+ "ruleId": "configuration no-unresolved-refs",
150
+ "severity": "warn",
151
+ "suggest": [],
152
+ },
140
153
  {
141
154
  "from": {
142
155
  "pointer": "#/rules",
@@ -154,19 +167,6 @@ describe('getConfig', () => {
154
167
  "severity": "warn",
155
168
  "suggest": [],
156
169
  },
157
- {
158
- "location": [
159
- {
160
- "pointer": "#/theme",
161
- "reportOnKey": false,
162
- "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
163
- },
164
- ],
165
- "message": "Can't resolve $ref: ENOENT: no such file or directory 'fixtures/resolve-refs-in-config/wrong-ref.yaml'",
166
- "ruleId": "configuration no-unresolved-refs",
167
- "severity": "warn",
168
- "suggest": [],
169
- },
170
170
  ]
171
171
  `);
172
172
  });
@@ -1,8 +1,9 @@
1
- import type { Oas2Decorator } from '../../visitors';
2
1
  import { Location } from '../../ref-utils';
3
- import type { Oas2Components } from '../../typings/swagger';
4
2
  import { isEmptyObject } from '../../utils';
5
3
 
4
+ import type { Oas2Decorator } from '../../visitors';
5
+ import type { Oas2Components } from '../../typings/swagger';
6
+
6
7
  export const RemoveUnusedComponents: Oas2Decorator = () => {
7
8
  const components = new Map<
8
9
  string,
@@ -1,8 +1,9 @@
1
- import type { Oas3Decorator } from '../../visitors';
2
1
  import { Location } from '../../ref-utils';
3
- import type { Oas3Components } from '../../typings/openapi';
4
2
  import { isEmptyObject } from '../../utils';
5
3
 
4
+ import type { Oas3Decorator } from '../../visitors';
5
+ import type { Oas3Components } from '../../typings/openapi';
6
+
6
7
  export const RemoveUnusedComponents: Oas3Decorator = () => {
7
8
  const components = new Map<
8
9
  string,
package/src/lint.ts CHANGED
@@ -112,6 +112,7 @@ export async function lintConfig(opts: {
112
112
  resolvedRefMap?: ResolvedRefMap;
113
113
  severity?: ProblemSeverity;
114
114
  externalRefResolver?: BaseResolver;
115
+ externalConfigTypes?: Record<string, NodeType>;
115
116
  }) {
116
117
  const { document, severity, externalRefResolver = new BaseResolver() } = opts;
117
118
 
@@ -126,7 +127,7 @@ export async function lintConfig(opts: {
126
127
  rules: { spec: 'error' },
127
128
  });
128
129
 
129
- const types = normalizeTypes(ConfigTypes, config);
130
+ const types = normalizeTypes(opts.externalConfigTypes || ConfigTypes, config);
130
131
  const rules: (RuleInstanceConfig & {
131
132
  visitor: NestedVisitObject<unknown, Oas3Visitor | Oas3Visitor[]>;
132
133
  })[] = [
package/src/ref-utils.ts CHANGED
@@ -43,9 +43,9 @@ export function escapePointer<T extends string | number>(fragment: T): T {
43
43
  }
44
44
 
45
45
  export function parseRef(ref: string): { uri: string | null; pointer: string[] } {
46
- const [uri, pointer = ''] = ref.split('#');
46
+ const [uri, pointer = ''] = ref.split('#/');
47
47
  return {
48
- uri: uri || null,
48
+ uri: (uri.endsWith('#') ? uri.slice(0, -1) : uri) || null,
49
49
  pointer: parsePointer(pointer),
50
50
  };
51
51
  }
package/src/resolve.ts CHANGED
@@ -269,8 +269,20 @@ export async function resolveDocument(opts: {
269
269
  if (itemsType === undefined && type !== unknownType && type !== SpecExtension) {
270
270
  return;
271
271
  }
272
+ const isTypeAFunction = typeof itemsType === 'function';
272
273
  for (let i = 0; i < node.length; i++) {
273
- walk(node[i], itemsType || unknownType, joinPointer(nodeAbsoluteRef, i));
274
+ const itemType = isTypeAFunction
275
+ ? itemsType(node[i], joinPointer(nodeAbsoluteRef, i))
276
+ : itemsType;
277
+ // we continue resolving unknown types, but stop early on known scalars
278
+ if (itemType === undefined && type !== unknownType && type !== SpecExtension) {
279
+ continue;
280
+ }
281
+ walk(
282
+ node[i],
283
+ isNamedType(itemType) ? itemType : unknownType,
284
+ joinPointer(nodeAbsoluteRef, i)
285
+ );
274
286
  }
275
287
  return;
276
288
  }
@@ -21,31 +21,28 @@ export type NormalizedScalarSchema = {
21
21
  export type NodeType = {
22
22
  properties: Record<string, PropType | ResolveTypeFn>;
23
23
  additionalProperties?: PropType | ResolveTypeFn;
24
- items?: string;
24
+ items?: PropType | ResolveTypeFn;
25
25
  required?: string[] | ((value: any, key: string | number | undefined) => string[]);
26
26
  requiredOneOf?: string[];
27
27
  allowed?: (value: any) => string[] | undefined;
28
28
  extensionsPrefix?: string;
29
29
  };
30
- type PropType = string | NodeType | ScalarSchema | undefined | null;
31
- type ResolveTypeFn = (value: any, key: string) => string | PropType;
30
+ export type PropType = string | NodeType | ScalarSchema | undefined | null;
31
+ export type ResolveTypeFn = (value: any, key: string) => string | PropType;
32
32
 
33
33
  export type NormalizedNodeType = {
34
34
  name: string;
35
35
  properties: Record<string, NormalizedPropType | NormalizedResolveTypeFn>;
36
36
  additionalProperties?: NormalizedPropType | NormalizedResolveTypeFn;
37
- items?: NormalizedNodeType;
37
+ items?: NormalizedPropType | NormalizedResolveTypeFn;
38
38
  required?: string[] | ((value: any, key: string | number | undefined) => string[]);
39
39
  requiredOneOf?: string[];
40
40
  allowed?: (value: any) => string[] | undefined;
41
41
  extensionsPrefix?: string;
42
42
  };
43
43
 
44
- type NormalizedPropType = NormalizedNodeType | NormalizedScalarSchema | undefined | null;
45
- type NormalizedResolveTypeFn = (
46
- value: any,
47
- key: string
48
- ) => NormalizedNodeType | NormalizedScalarSchema | undefined | null;
44
+ type NormalizedPropType = NormalizedNodeType | NormalizedScalarSchema | null | undefined;
45
+ type NormalizedResolveTypeFn = (value: any, key: string) => NormalizedPropType;
49
46
 
50
47
  export function listOf(typeName: string) {
51
48
  return {
@@ -142,8 +139,6 @@ export function normalizeTypes(
142
139
  }
143
140
  }
144
141
 
145
- export function isNamedType(
146
- t: NormalizedNodeType | NormalizedScalarSchema | null | undefined
147
- ): t is NormalizedNodeType {
142
+ export function isNamedType(t: NormalizedPropType): t is NormalizedNodeType {
148
143
  return typeof t?.name === 'string';
149
144
  }
@@ -0,0 +1,217 @@
1
+ // For internal usage only
2
+
3
+ import Ajv from '@redocly/ajv/dist/2020';
4
+ import { isPlainObject } from '../utils';
5
+
6
+ import type { NodeType, PropType, ResolveTypeFn } from '.';
7
+ import type { JSONSchema } from 'json-schema-to-ts';
8
+ import { Oas3Schema } from '../typings/openapi';
9
+
10
+ const ajv = new Ajv({
11
+ strictSchema: false,
12
+ allowUnionTypes: true,
13
+ useDefaults: true,
14
+ allErrors: true,
15
+ discriminator: true,
16
+ strictTypes: false,
17
+ verbose: true,
18
+ });
19
+
20
+ function findOneOf(schemaOneOf: JSONSchema[], oneOfs: (PropType | ResolveTypeFn)[]): ResolveTypeFn {
21
+ if (oneOfs.some((option) => typeof option === 'function')) {
22
+ throw new Error('Unexpected oneOf inside oneOf.');
23
+ }
24
+
25
+ return (value: unknown) => {
26
+ let index = schemaOneOf.findIndex((option) => ajv.validate(option, value));
27
+ if (index === -1) {
28
+ index = 0;
29
+ }
30
+ return oneOfs[index] as PropType;
31
+ };
32
+ }
33
+
34
+ function transformJSONSchemaToNodeType(
35
+ propertyName: string,
36
+ schema: JSONSchema,
37
+ ctx: Record<string, NodeType>
38
+ ): PropType | ResolveTypeFn {
39
+ if (!schema || typeof schema === 'boolean') {
40
+ throw new Error(`Unexpected schema in ${propertyName}.`);
41
+ }
42
+
43
+ if (schema instanceof Array) {
44
+ throw new Error(`Unexpected array schema in ${propertyName}. Try using oneOf instead.`);
45
+ }
46
+
47
+ if (schema.type === 'null') {
48
+ throw new Error(`Unexpected null schema type in ${propertyName} schema.`);
49
+ }
50
+
51
+ if (schema.type instanceof Array) {
52
+ throw new Error(
53
+ `Unexpected array schema type in ${propertyName} schema. Try using oneOf instead.`
54
+ );
55
+ }
56
+
57
+ if (
58
+ schema.type === 'string' ||
59
+ schema.type === 'number' ||
60
+ schema.type === 'integer' ||
61
+ schema.type === 'boolean'
62
+ ) {
63
+ const { default: _, format: _format, ...rest } = schema;
64
+ return rest as PropType;
65
+ }
66
+
67
+ if (schema.type === 'object' && !schema.properties && !schema.oneOf) {
68
+ if (schema.additionalProperties === undefined || schema.additionalProperties === true) {
69
+ return { type: 'object' };
70
+ } else if (schema.additionalProperties === false) {
71
+ return { type: 'object', properties: {} };
72
+ }
73
+ }
74
+
75
+ if (schema.allOf) {
76
+ throw new Error(`Unexpected allOf in ${propertyName}.`);
77
+ }
78
+
79
+ if (schema.anyOf) {
80
+ throw new Error(`Unexpected anyOf in ${propertyName}.`);
81
+ }
82
+
83
+ if (
84
+ isPlainObject(schema.properties) ||
85
+ isPlainObject(schema.additionalProperties) ||
86
+ (isPlainObject(schema.items) &&
87
+ (isPlainObject(schema.items.properties) ||
88
+ isPlainObject(schema.items.additionalProperties) ||
89
+ schema.items.oneOf)) // exclude scalar array types
90
+ ) {
91
+ return extractNodeToContext(propertyName, schema, ctx);
92
+ }
93
+
94
+ if (schema.oneOf) {
95
+ if ((schema as Oas3Schema).discriminator) {
96
+ const discriminatedPropertyName = (schema as Oas3Schema).discriminator?.propertyName;
97
+ if (!discriminatedPropertyName) {
98
+ throw new Error(`Unexpected discriminator without a propertyName in ${propertyName}.`);
99
+ }
100
+ const oneOfs = schema.oneOf.map((option, i) => {
101
+ if (typeof option === 'boolean') {
102
+ throw new Error(
103
+ `Unexpected boolean schema in ${propertyName} at position ${i} in oneOf.`
104
+ );
105
+ }
106
+ const discriminatedProperty = option?.properties?.[discriminatedPropertyName];
107
+ if (!discriminatedProperty || typeof discriminatedProperty === 'boolean') {
108
+ throw new Error(
109
+ `Unexpected property '${discriminatedProperty}' schema in ${propertyName} at position ${i} in oneOf.`
110
+ );
111
+ }
112
+ const name = discriminatedProperty.const as string;
113
+ return transformJSONSchemaToNodeType(name, option, ctx);
114
+ });
115
+
116
+ return (value: unknown, key: string) => {
117
+ if (isPlainObject(value)) {
118
+ const discriminatedTypeName = value[discriminatedPropertyName];
119
+ if (typeof discriminatedTypeName === 'string' && ctx[discriminatedTypeName]) {
120
+ return discriminatedTypeName;
121
+ }
122
+ }
123
+ return findOneOf(schema.oneOf as JSONSchema[], oneOfs)(value, key);
124
+ };
125
+ } else {
126
+ const oneOfs = schema.oneOf.map((option, i) =>
127
+ transformJSONSchemaToNodeType(propertyName + '_' + i, option, ctx)
128
+ );
129
+ return findOneOf(schema.oneOf as JSONSchema[], oneOfs);
130
+ }
131
+ }
132
+
133
+ return schema as PropType;
134
+ }
135
+
136
+ function extractNodeToContext(
137
+ propertyName: string,
138
+ schema: JSONSchema,
139
+ ctx: Record<string, NodeType>
140
+ ): string {
141
+ if (!schema || typeof schema === 'boolean') {
142
+ throw new Error(`Unexpected schema in ${propertyName}.`);
143
+ }
144
+
145
+ if (schema instanceof Array) {
146
+ throw new Error(`Unexpected array schema in ${propertyName}. Try using oneOf instead.`);
147
+ }
148
+
149
+ if (schema.type === 'null') {
150
+ throw new Error(`Unexpected null schema type in ${propertyName} schema.`);
151
+ }
152
+
153
+ if (schema.type instanceof Array) {
154
+ throw new Error(
155
+ `Unexpected array schema type in ${propertyName} schema. Try using oneOf instead.`
156
+ );
157
+ }
158
+
159
+ const properties: Record<string, PropType | ResolveTypeFn> = {};
160
+ for (const [name, property] of Object.entries(schema.properties || {})) {
161
+ properties[name] = transformJSONSchemaToNodeType(propertyName + '.' + name, property, ctx);
162
+ }
163
+
164
+ let additionalProperties;
165
+ if (isPlainObject(schema.additionalProperties)) {
166
+ additionalProperties = transformJSONSchemaToNodeType(
167
+ propertyName + '_additionalProperties',
168
+ schema.additionalProperties,
169
+ ctx
170
+ );
171
+ }
172
+ if (schema.additionalProperties === true) {
173
+ additionalProperties = {};
174
+ }
175
+
176
+ let items;
177
+ if (
178
+ isPlainObject(schema.items) &&
179
+ (isPlainObject(schema.items.properties) ||
180
+ isPlainObject(schema.items.additionalProperties) ||
181
+ schema.items.oneOf) // exclude scalar array types
182
+ ) {
183
+ items = transformJSONSchemaToNodeType(propertyName + '_items', schema.items, ctx);
184
+ }
185
+
186
+ let required = schema.required as NodeType['required'];
187
+ // Translate required in oneOfs into a ResolveTypeFn.
188
+ if (schema.oneOf && schema.oneOf.every((option) => !!(option as Oas3Schema).required)) {
189
+ required = (value): string[] => {
190
+ const requiredList: string[][] = schema.oneOf!.map((option) => [
191
+ ...(schema.required || []),
192
+ ...(option as Oas3Schema).required!,
193
+ ]);
194
+
195
+ let index = requiredList.findIndex((r) =>
196
+ r.every((requiredProp) => value[requiredProp] !== undefined)
197
+ );
198
+ if (index === -1) {
199
+ index = 0;
200
+ }
201
+
202
+ return requiredList[index];
203
+ };
204
+ }
205
+
206
+ ctx[propertyName] = { properties, additionalProperties, items, required };
207
+ return propertyName;
208
+ }
209
+
210
+ export function getNodeTypesFromJSONSchema(
211
+ schemaName: string,
212
+ entrySchema: JSONSchema
213
+ ): Record<string, NodeType> {
214
+ const ctx: Record<string, NodeType> = {};
215
+ transformJSONSchemaToNodeType(schemaName, entrySchema, ctx);
216
+ return ctx;
217
+ }
package/src/types/oas2.ts CHANGED
@@ -1,4 +1,7 @@
1
- import { NodeType, listOf, mapOf } from '.';
1
+ import { listOf, mapOf } from '.';
2
+
3
+ import type { NodeType } from '.';
4
+ import type { Oas2NodeType } from './redocly-yaml';
2
5
 
3
6
  const responseCodeRegexp = /^[0-9][0-9Xx]{2}$/;
4
7
 
@@ -437,7 +440,7 @@ const Example: NodeType = {
437
440
  extensionsPrefix: 'x-',
438
441
  };
439
442
 
440
- export const Oas2Types: Record<string, NodeType> = {
443
+ export const Oas2Types: Record<Oas2NodeType, NodeType> = {
441
444
  Root,
442
445
  Tag,
443
446
  TagList: listOf('Tag'),
package/src/types/oas3.ts CHANGED
@@ -1,5 +1,9 @@
1
- import { NodeType, listOf, mapOf } from '.';
1
+ import { listOf, mapOf } from '.';
2
2
  import { isMappingRef } from '../ref-utils';
3
+
4
+ import type { NodeType } from '.';
5
+ import type { Oas3NodeType } from './redocly-yaml';
6
+
3
7
  const responseCodeRegexp = /^[0-9][0-9Xx]{2}$/;
4
8
 
5
9
  const Root: NodeType = {
@@ -531,7 +535,7 @@ const XUsePkce: NodeType = {
531
535
  },
532
536
  };
533
537
 
534
- export const Oas3Types: Record<string, NodeType> = {
538
+ export const Oas3Types: Record<Oas3NodeType, NodeType> = {
535
539
  Root,
536
540
  Tag,
537
541
  TagList: listOf('Tag'),
@@ -1,6 +1,9 @@
1
- import { NodeType, listOf, mapOf } from '.';
1
+ import { listOf, mapOf } from '.';
2
2
  import { Oas3Types } from './oas3';
3
3
 
4
+ import type { NodeType } from '.';
5
+ import type { Oas3_1NodeType } from './redocly-yaml';
6
+
4
7
  const Root: NodeType = {
5
8
  properties: {
6
9
  openapi: null,
@@ -262,7 +265,7 @@ const SecurityScheme: NodeType = {
262
265
  extensionsPrefix: 'x-',
263
266
  };
264
267
 
265
- export const Oas3_1Types: Record<string, NodeType> = {
268
+ export const Oas3_1Types: Record<Oas3_1NodeType, NodeType> = {
266
269
  ...Oas3Types,
267
270
  Info,
268
271
  Root,