@redocly/openapi-core 1.16.0 → 1.17.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.
@@ -58,6 +58,88 @@ describe('loadConfig', () => {
58
58
  expect(mockFn).toHaveBeenCalled();
59
59
  });
60
60
 
61
+ it('should resolve config and call processRawConfig', async () => {
62
+ let problems: NormalizedProblem[];
63
+ let doc: any;
64
+
65
+ await loadConfig({
66
+ configPath: path.join(__dirname, './fixtures/resolve-refs-in-config/config-with-refs.yaml'),
67
+ processRawConfig: async ({ document, parsed, resolvedRefMap, config }) => {
68
+ doc = parsed;
69
+ problems = await lintConfig({
70
+ document,
71
+ severity: 'warn',
72
+ resolvedRefMap,
73
+ config,
74
+ });
75
+ },
76
+ });
77
+
78
+ expect(replaceSourceWithRef(problems!, __dirname)).toMatchInlineSnapshot(`
79
+ [
80
+ {
81
+ "from": {
82
+ "pointer": "#/seo",
83
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
84
+ },
85
+ "location": [
86
+ {
87
+ "pointer": "#/title",
88
+ "reportOnKey": false,
89
+ "source": "fixtures/resolve-refs-in-config/seo.yaml",
90
+ },
91
+ ],
92
+ "message": "Expected type \`string\` but got \`integer\`.",
93
+ "ruleId": "configuration spec",
94
+ "severity": "warn",
95
+ "suggest": [],
96
+ },
97
+ {
98
+ "from": {
99
+ "pointer": "#/rules",
100
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
101
+ },
102
+ "location": [
103
+ {
104
+ "pointer": "#/non-existing-rule",
105
+ "reportOnKey": true,
106
+ "source": "fixtures/resolve-refs-in-config/rules.yaml",
107
+ },
108
+ ],
109
+ "message": "Property \`non-existing-rule\` is not expected here.",
110
+ "ruleId": "configuration spec",
111
+ "severity": "warn",
112
+ "suggest": [],
113
+ },
114
+ {
115
+ "location": [
116
+ {
117
+ "pointer": "#/theme",
118
+ "reportOnKey": false,
119
+ "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
120
+ },
121
+ ],
122
+ "message": "Can't resolve $ref: ENOENT: no such file or directory 'fixtures/resolve-refs-in-config/wrong-ref.yaml'",
123
+ "ruleId": "configuration no-unresolved-refs",
124
+ "severity": "warn",
125
+ "suggest": [],
126
+ },
127
+ ]
128
+ `);
129
+ expect(doc).toMatchInlineSnapshot(`
130
+ {
131
+ "rules": {
132
+ "info-license": "error",
133
+ "non-existing-rule": "warn",
134
+ },
135
+ "seo": {
136
+ "title": 1,
137
+ },
138
+ "theme": undefined,
139
+ }
140
+ `);
141
+ });
142
+
61
143
  it('should call externalRefResolver if such passed', async () => {
62
144
  const externalRefResolver = new BaseResolver();
63
145
  const resolverSpy = jest.spyOn(externalRefResolver, 'resolveDocument');
@@ -104,22 +186,16 @@ describe('findConfig', () => {
104
186
  describe('getConfig', () => {
105
187
  jest.spyOn(fs, 'hasOwnProperty').mockImplementation(() => false);
106
188
  it('should return empty object if there is no configPath and config file is not found', () => {
107
- expect(getConfig()).toEqual(Promise.resolve({}));
189
+ expect(getConfig()).toEqual(Promise.resolve({ rawConfig: {} }));
108
190
  });
109
191
 
110
192
  it('should resolve refs in config', async () => {
111
193
  let problems: NormalizedProblem[];
112
- const result = await getConfig({
194
+
195
+ const { rawConfig } = await getConfig({
113
196
  configPath: path.join(__dirname, './fixtures/resolve-refs-in-config/config-with-refs.yaml'),
114
- processRawConfig: async (config, resolvedRefMap) => {
115
- problems = await lintConfig({
116
- document: config,
117
- severity: 'warn',
118
- resolvedRefMap,
119
- });
120
- },
121
197
  });
122
- expect(result).toEqual({
198
+ expect(rawConfig).toEqual({
123
199
  seo: {
124
200
  title: 1,
125
201
  },
@@ -130,57 +206,6 @@ describe('getConfig', () => {
130
206
  },
131
207
  },
132
208
  });
133
- expect(replaceSourceWithRef(problems!, __dirname)).toMatchInlineSnapshot(`
134
- [
135
- {
136
- "from": {
137
- "pointer": "#/seo",
138
- "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
139
- },
140
- "location": [
141
- {
142
- "pointer": "#/title",
143
- "reportOnKey": false,
144
- "source": "fixtures/resolve-refs-in-config/seo.yaml",
145
- },
146
- ],
147
- "message": "Expected type \`string\` but got \`integer\`.",
148
- "ruleId": "configuration spec",
149
- "severity": "warn",
150
- "suggest": [],
151
- },
152
- {
153
- "from": {
154
- "pointer": "#/rules",
155
- "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
156
- },
157
- "location": [
158
- {
159
- "pointer": "#/non-existing-rule",
160
- "reportOnKey": true,
161
- "source": "fixtures/resolve-refs-in-config/rules.yaml",
162
- },
163
- ],
164
- "message": "Property \`non-existing-rule\` is not expected here.",
165
- "ruleId": "configuration spec",
166
- "severity": "warn",
167
- "suggest": [],
168
- },
169
- {
170
- "location": [
171
- {
172
- "pointer": "#/theme",
173
- "reportOnKey": false,
174
- "source": "fixtures/resolve-refs-in-config/config-with-refs.yaml",
175
- },
176
- ],
177
- "message": "Can't resolve $ref: ENOENT: no such file or directory 'fixtures/resolve-refs-in-config/wrong-ref.yaml'",
178
- "ruleId": "configuration no-unresolved-refs",
179
- "severity": "warn",
180
- "suggest": [],
181
- },
182
- ]
183
- `);
184
209
  });
185
210
  });
186
211
 
@@ -4,7 +4,7 @@ import { RedoclyClient } from '../redocly';
4
4
  import { isEmptyObject } from '../utils';
5
5
  import { parseYaml } from '../js-yaml';
6
6
  import { Config } from './config';
7
- import { ConfigValidationError, transformConfig } from './utils';
7
+ import { ConfigValidationError, transformConfig, deepCloneMapWithJSON } from './utils';
8
8
  import { resolveConfig, resolveConfigFileAndRefs } from './config-resolvers';
9
9
  import { bundleConfig } from '../bundle';
10
10
  import { BaseResolver } from '../resolve';
@@ -80,10 +80,12 @@ async function addConfigMetadata({
80
80
  });
81
81
  }
82
82
 
83
- export type RawConfigProcessor = (
84
- rawConfig: Document,
85
- resolvedRefMap: ResolvedRefMap
86
- ) => void | Promise<void>;
83
+ export type RawConfigProcessor = (params: {
84
+ document: Document;
85
+ resolvedRefMap: ResolvedRefMap;
86
+ config: Config;
87
+ parsed: Document['parsed'];
88
+ }) => void | Promise<void>;
87
89
 
88
90
  export async function loadConfig(
89
91
  options: {
@@ -103,12 +105,16 @@ export async function loadConfig(
103
105
  region,
104
106
  externalRefResolver,
105
107
  } = options;
106
- const rawConfig = await getConfig({ configPath, processRawConfig, externalRefResolver });
108
+
109
+ const { rawConfig, document, parsed, resolvedRefMap } = await getConfig({
110
+ configPath,
111
+ externalRefResolver,
112
+ });
107
113
 
108
114
  const redoclyClient = isBrowser ? undefined : new RedoclyClient();
109
115
  const tokens = redoclyClient && redoclyClient.hasTokens() ? redoclyClient.getAllTokens() : [];
110
116
 
111
- return addConfigMetadata({
117
+ const config = await addConfigMetadata({
112
118
  rawConfig,
113
119
  customExtends,
114
120
  configPath,
@@ -117,6 +123,24 @@ export async function loadConfig(
117
123
  region,
118
124
  externalRefResolver,
119
125
  });
126
+
127
+ if (document && parsed && resolvedRefMap && typeof processRawConfig === 'function') {
128
+ try {
129
+ await processRawConfig({
130
+ document,
131
+ resolvedRefMap,
132
+ config,
133
+ parsed,
134
+ });
135
+ } catch (e) {
136
+ if (e instanceof ConfigValidationError) {
137
+ throw e;
138
+ }
139
+ throw new Error(`Error parsing config file at '${configPath}': ${e.message}`);
140
+ }
141
+ }
142
+
143
+ return config;
120
144
  }
121
145
 
122
146
  export const CONFIG_FILE_NAMES = ['redocly.yaml', 'redocly.yml', '.redocly.yaml', '.redocly.yml'];
@@ -139,31 +163,33 @@ export function findConfig(dir?: string): string | undefined {
139
163
  export async function getConfig(
140
164
  options: {
141
165
  configPath?: string;
142
- processRawConfig?: RawConfigProcessor;
143
166
  externalRefResolver?: BaseResolver;
144
167
  } = {}
145
- ): Promise<RawConfig> {
146
- const {
147
- configPath = findConfig(),
148
- processRawConfig,
149
- externalRefResolver = new BaseResolver(),
150
- } = options;
151
- if (!configPath) return {};
168
+ ): Promise<{
169
+ rawConfig: RawConfig;
170
+ document?: Document;
171
+ parsed?: Document['parsed'];
172
+ resolvedRefMap?: ResolvedRefMap;
173
+ }> {
174
+ const { configPath = findConfig(), externalRefResolver = new BaseResolver() } = options;
175
+ if (!configPath) return { rawConfig: {} };
152
176
 
153
177
  try {
154
178
  const { document, resolvedRefMap } = await resolveConfigFileAndRefs({
155
179
  configPath,
156
180
  externalRefResolver,
157
181
  });
158
- if (typeof processRawConfig === 'function') {
159
- await processRawConfig(document, resolvedRefMap);
160
- }
161
- const bundledConfig = await bundleConfig(document, resolvedRefMap);
162
- return transformConfig(bundledConfig);
182
+
183
+ const bundledRefMap = deepCloneMapWithJSON(resolvedRefMap);
184
+ const parsed = await bundleConfig(JSON.parse(JSON.stringify(document)), bundledRefMap);
185
+
186
+ return {
187
+ rawConfig: transformConfig(parsed),
188
+ document,
189
+ parsed,
190
+ resolvedRefMap,
191
+ };
163
192
  } catch (e) {
164
- if (e instanceof ConfigValidationError) {
165
- throw e;
166
- }
167
193
  throw new Error(`Error parsing config file at '${configPath}': ${e.message}`);
168
194
  }
169
195
  }
@@ -364,3 +364,7 @@ export function getUniquePlugins(plugins: Plugin[]): Plugin[] {
364
364
  }
365
365
 
366
366
  export class ConfigValidationError extends Error {}
367
+
368
+ export function deepCloneMapWithJSON<K, V>(originalMap: Map<K, V>): Map<K, V> {
369
+ return new Map(JSON.parse(JSON.stringify([...originalMap])));
370
+ }
package/src/lint.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { BaseResolver, resolveDocument, makeDocumentFromString } from './resolve';
2
2
  import { normalizeVisitors } from './visitors';
3
3
  import { walkDocument } from './walk';
4
- import { StyleguideConfig, Config, initRules, defaultPlugin, resolvePlugins } from './config';
4
+ import { StyleguideConfig, Config, initRules } from './config';
5
5
  import { normalizeTypes } from './types';
6
6
  import { releaseAjvInstance } from './rules/ajv';
7
7
  import { SpecVersion, getMajorSpecVersion, detectSpec, getTypes } from './oas-types';
8
- import { ConfigTypes } from './types/redocly-yaml';
8
+ import { createConfigTypes } from './types/redocly-yaml';
9
9
  import { Spec } from './rules/common/spec';
10
10
  import { NoUnresolvedRefs } from './rules/no-unresolved-refs';
11
11
 
@@ -13,6 +13,7 @@ import type { Document, ResolvedRefMap } from './resolve';
13
13
  import type { ProblemSeverity, WalkContext } from './walk';
14
14
  import type { NodeType } from './types';
15
15
  import type { NestedVisitObject, Oas3Visitor, RuleInstanceConfig } from './visitors';
16
+ import { rootRedoclyConfigSchema } from '@redocly/config';
16
17
 
17
18
  export async function lint(opts: {
18
19
  ref: string;
@@ -109,25 +110,25 @@ export async function lintDocument(opts: {
109
110
 
110
111
  export async function lintConfig(opts: {
111
112
  document: Document;
113
+ config: Config;
112
114
  resolvedRefMap?: ResolvedRefMap;
113
115
  severity?: ProblemSeverity;
114
116
  externalRefResolver?: BaseResolver;
115
117
  externalConfigTypes?: Record<string, NodeType>;
116
118
  }) {
117
- const { document, severity, externalRefResolver = new BaseResolver() } = opts;
119
+ const { document, severity, externalRefResolver = new BaseResolver(), config } = opts;
118
120
 
119
121
  const ctx: WalkContext = {
120
122
  problems: [],
121
123
  oasVersion: SpecVersion.OAS3_0,
122
124
  visitorsData: {},
123
125
  };
124
- const plugins = resolvePlugins([defaultPlugin]);
125
- const config = new StyleguideConfig({
126
- plugins,
127
- rules: { spec: 'error' },
128
- });
129
126
 
130
- const types = normalizeTypes(opts.externalConfigTypes || ConfigTypes, config);
127
+ const types = normalizeTypes(
128
+ opts.externalConfigTypes || createConfigTypes(rootRedoclyConfigSchema, config),
129
+ { doNotResolveExamples: config.styleguide.doNotResolveExamples }
130
+ );
131
+
131
132
  const rules: (RuleInstanceConfig & {
132
133
  visitor: NestedVisitObject<unknown, Oas3Visitor | Oas3Visitor[]>;
133
134
  })[] = [
@@ -134,6 +134,7 @@ const Schema: NodeType = {
134
134
  then: 'Schema',
135
135
  else: 'Schema',
136
136
  dependentSchemas: listOf('Schema'),
137
+ dependentRequired: 'DependentRequired',
137
138
  prefixItems: listOf('Schema'),
138
139
  contains: 'Schema',
139
140
  minContains: { type: 'integer', minimum: 0 },
@@ -170,6 +171,7 @@ const Schema: NodeType = {
170
171
  format: { type: 'string' },
171
172
  contentEncoding: { type: 'string' },
172
173
  contentMediaType: { type: 'string' },
174
+ contentSchema: 'Schema',
173
175
  default: null,
174
176
  readOnly: { type: 'boolean' },
175
177
  writeOnly: { type: 'boolean' },
@@ -265,6 +267,11 @@ const SecurityScheme: NodeType = {
265
267
  extensionsPrefix: 'x-',
266
268
  };
267
269
 
270
+ const DependentRequired: NodeType = {
271
+ properties: {},
272
+ additionalProperties: { type: 'array', items: { type: 'string' } },
273
+ };
274
+
268
275
  export const Oas3_1Types: Record<Oas3_1NodeType, NodeType> = {
269
276
  ...Oas3Types,
270
277
  Info,
@@ -276,4 +283,5 @@ export const Oas3_1Types: Record<Oas3_1NodeType, NodeType> = {
276
283
  NamedPathItems: mapOf('PathItem'),
277
284
  SecurityScheme,
278
285
  Operation,
286
+ DependentRequired,
279
287
  };
@@ -5,6 +5,8 @@ import { getNodeTypesFromJSONSchema } from './json-schema-adapter';
5
5
 
6
6
  import type { NodeType } from '.';
7
7
  import type { JSONSchema } from 'json-schema-to-ts';
8
+ import { SpecVersion, getTypes } from '../oas-types';
9
+ import { Config } from '../config';
8
10
 
9
11
  const builtInCommonRules = [
10
12
  'spec',
@@ -218,12 +220,11 @@ const oas3_1NodeTypesList = [
218
220
  'NamedPathItems',
219
221
  'SecurityScheme',
220
222
  'Operation',
223
+ 'DependentRequired',
221
224
  ] as const;
222
225
 
223
226
  export type Oas3_1NodeType = typeof oas3_1NodeTypesList[number];
224
227
 
225
- const asyncNodeTypesList = ['Message'] as const;
226
-
227
228
  const ConfigStyleguide: NodeType = {
228
229
  properties: {
229
230
  extends: {
@@ -350,35 +351,28 @@ const Schema: NodeType = {
350
351
  additionalProperties: {},
351
352
  };
352
353
 
353
- const AssertionDefinitionSubject: NodeType = {
354
- properties: {
355
- type: {
356
- enum: [
357
- ...new Set([
358
- 'any',
359
- ...oas2NodeTypesList,
360
- ...oas3NodeTypesList,
361
- ...oas3_1NodeTypesList,
362
- ...asyncNodeTypesList,
363
- 'SpecExtension',
364
- ]),
365
- ],
366
- },
367
- property: (value: unknown) => {
368
- if (Array.isArray(value)) {
369
- return { type: 'array', items: { type: 'string' } };
370
- } else if (value === null) {
371
- return null;
372
- } else {
373
- return { type: 'string' };
374
- }
354
+ function createAssertionDefinitionSubject(nodeNames: string[]): NodeType {
355
+ return {
356
+ properties: {
357
+ type: {
358
+ enum: [...new Set(['any', ...nodeNames, 'SpecExtension'])],
359
+ },
360
+ property: (value: unknown) => {
361
+ if (Array.isArray(value)) {
362
+ return { type: 'array', items: { type: 'string' } };
363
+ } else if (value === null) {
364
+ return null;
365
+ } else {
366
+ return { type: 'string' };
367
+ }
368
+ },
369
+ filterInParentKeys: { type: 'array', items: { type: 'string' } },
370
+ filterOutParentKeys: { type: 'array', items: { type: 'string' } },
371
+ matchParentKeys: { type: 'string' },
375
372
  },
376
- filterInParentKeys: { type: 'array', items: { type: 'string' } },
377
- filterOutParentKeys: { type: 'array', items: { type: 'string' } },
378
- matchParentKeys: { type: 'string' },
379
- },
380
- required: ['type'],
381
- };
373
+ required: ['type'],
374
+ };
375
+ }
382
376
 
383
377
  const AssertionDefinitionAssertions: NodeType = {
384
378
  properties: {
@@ -1057,7 +1051,13 @@ const ConfigMockServer: NodeType = {
1057
1051
  },
1058
1052
  };
1059
1053
 
1060
- export const createConfigTypes = (extraSchemas: JSONSchema) => {
1054
+ export function createConfigTypes(extraSchemas: JSONSchema, config?: Config) {
1055
+ const nodeNames = Object.values(SpecVersion).flatMap((version) => {
1056
+ const types = config?.styleguide
1057
+ ? config.styleguide.extendTypes(getTypes(version), version)
1058
+ : getTypes(version);
1059
+ return Object.keys(types);
1060
+ });
1061
1061
  // Create types based on external schemas
1062
1062
  const nodeTypes = getNodeTypesFromJSONSchema('rootRedoclyConfigSchema', extraSchemas);
1063
1063
 
@@ -1065,9 +1065,10 @@ export const createConfigTypes = (extraSchemas: JSONSchema) => {
1065
1065
  ...CoreConfigTypes,
1066
1066
  ConfigRoot: createConfigRoot(nodeTypes), // This is the REAL config root type
1067
1067
  ConfigApisProperties: createConfigApisProperties(nodeTypes),
1068
+ AssertionDefinitionSubject: createAssertionDefinitionSubject(nodeNames),
1068
1069
  ...nodeTypes,
1069
1070
  };
1070
- };
1071
+ }
1071
1072
 
1072
1073
  const CoreConfigTypes: Record<string, NodeType> = {
1073
1074
  Assert,
@@ -1130,7 +1131,6 @@ const CoreConfigTypes: Record<string, NodeType> = {
1130
1131
  Heading,
1131
1132
  Typography,
1132
1133
  AssertionDefinitionAssertions,
1133
- AssertionDefinitionSubject,
1134
1134
  };
1135
1135
 
1136
1136
  export const ConfigTypes: Record<string, NodeType> = createConfigTypes(rootRedoclyConfigSchema);