@redocly/openapi-core 1.10.6 → 1.12.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 (36) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/lib/config/config-resolvers.d.ts +5 -1
  3. package/lib/config/config-resolvers.js +4 -4
  4. package/lib/config/load.d.ts +1 -0
  5. package/lib/config/load.js +7 -2
  6. package/lib/config/utils.js +1 -1
  7. package/lib/decorators/oas2/remove-unused-components.js +36 -22
  8. package/lib/decorators/oas3/remove-unused-components.js +34 -19
  9. package/lib/format/format.d.ts +1 -1
  10. package/lib/format/format.js +57 -0
  11. package/lib/index.d.ts +1 -1
  12. package/lib/index.js +3 -2
  13. package/lib/rules/common/assertions/utils.js +2 -2
  14. package/lib/rules/oas3/no-invalid-media-type-examples.js +3 -0
  15. package/lib/utils.d.ts +1 -0
  16. package/lib/utils.js +7 -1
  17. package/package.json +2 -2
  18. package/src/__tests__/format.test.ts +34 -0
  19. package/src/bundle.ts +1 -1
  20. package/src/config/__tests__/config-resolvers.test.ts +4 -4
  21. package/src/config/__tests__/fixtures/load-external.yaml +2 -0
  22. package/src/config/__tests__/load.test.ts +14 -0
  23. package/src/config/config-resolvers.ts +13 -7
  24. package/src/config/load.ts +13 -4
  25. package/src/config/utils.ts +1 -1
  26. package/src/decorators/oas2/__tests__/remove-unused-components.test.ts +74 -1
  27. package/src/decorators/oas2/remove-unused-components.ts +46 -24
  28. package/src/decorators/oas3/__tests__/remove-unused-components.test.ts +142 -0
  29. package/src/decorators/oas3/remove-unused-components.ts +45 -20
  30. package/src/format/format.ts +62 -1
  31. package/src/index.ts +8 -1
  32. package/src/rules/common/assertions/utils.ts +2 -2
  33. package/src/rules/oas3/__tests__/no-invalid-media-type-examples.test.ts +145 -0
  34. package/src/rules/oas3/no-invalid-media-type-examples.ts +3 -0
  35. package/src/utils.ts +4 -0
  36. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @redocly/openapi-core
2
2
 
3
+ ## 1.12.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Improved caching for external configuration resources.
8
+
9
+ ## 1.11.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Added support for a `github-actions` output format for the `lint` command to annotate reported problems on files when used in a GitHub Actions workflow.
14
+
15
+ ### Patch Changes
16
+
17
+ - Fixed [`no-invalid-media-type-examples`](https://redocly.com/docs/cli/rules/no-invalid-media-type-examples/) rule `externalValue` example validation.
18
+ - Process remove-unused-components rule transitively; components are now removed if they were previously referenced by a removed component.
19
+
3
20
  ## 1.10.6
4
21
 
5
22
  ### Patch Changes
@@ -9,7 +9,11 @@ export declare function resolveConfigFileAndRefs({ configPath, externalRefResolv
9
9
  document: Document;
10
10
  resolvedRefMap: ResolvedRefMap;
11
11
  }>;
12
- export declare function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config>;
12
+ export declare function resolveConfig({ rawConfig, configPath, externalRefResolver, }: {
13
+ rawConfig: RawConfig;
14
+ configPath?: string;
15
+ externalRefResolver?: BaseResolver;
16
+ }): Promise<Config>;
13
17
  export declare function resolvePlugins(plugins: (string | Plugin)[] | null, configPath?: string): Plugin[];
14
18
  export declare function resolveApis({ rawConfig, configPath, resolver, }: {
15
19
  rawConfig: RawConfig;
@@ -53,13 +53,13 @@ function resolveConfigFileAndRefs({ configPath, externalRefResolver = new resolv
53
53
  });
54
54
  }
55
55
  exports.resolveConfigFileAndRefs = resolveConfigFileAndRefs;
56
- function resolveConfig(rawConfig, configPath) {
56
+ function resolveConfig({ rawConfig, configPath, externalRefResolver, }) {
57
57
  var _a, _b;
58
58
  return __awaiter(this, void 0, void 0, function* () {
59
59
  if ((_b = (_a = rawConfig.styleguide) === null || _a === void 0 ? void 0 : _a.extends) === null || _b === void 0 ? void 0 : _b.some(utils_3.isNotString)) {
60
60
  throw new Error(`Error configuration format not detected in extends value must contain strings`);
61
61
  }
62
- const resolver = new resolve_1.BaseResolver((0, utils_2.getResolveConfig)(rawConfig.resolve));
62
+ const resolver = externalRefResolver !== null && externalRefResolver !== void 0 ? externalRefResolver : new resolve_1.BaseResolver((0, utils_2.getResolveConfig)(rawConfig.resolve));
63
63
  const apis = yield resolveApis({
64
64
  rawConfig,
65
65
  configPath,
@@ -263,8 +263,8 @@ exports.resolvePreset = resolvePreset;
263
263
  function loadExtendStyleguideConfig(filePath, resolver) {
264
264
  return __awaiter(this, void 0, void 0, function* () {
265
265
  try {
266
- const fileSource = yield resolver.loadExternalRef(filePath);
267
- const rawConfig = (0, utils_2.transformConfig)((0, utils_3.parseYaml)(fileSource.body));
266
+ const { parsed } = (yield resolver.resolveDocument(null, filePath));
267
+ const rawConfig = (0, utils_2.transformConfig)(parsed);
268
268
  if (!rawConfig.styleguide) {
269
269
  throw new Error(`Styleguide configuration format not detected: "${filePath}"`);
270
270
  }
@@ -23,6 +23,7 @@ type CreateConfigOptions = {
23
23
  extends?: string[];
24
24
  tokens?: RegionalTokenWithValidity[];
25
25
  configPath?: string;
26
+ externalRefResolver?: BaseResolver;
26
27
  };
27
28
  export declare function createConfig(config: string | RawUniversalConfig, options?: CreateConfigOptions): Promise<Config>;
28
29
  export {};
@@ -19,7 +19,7 @@ const config_1 = require("./config");
19
19
  const utils_2 = require("./utils");
20
20
  const config_resolvers_1 = require("./config-resolvers");
21
21
  const bundle_1 = require("../bundle");
22
- function addConfigMetadata({ rawConfig, customExtends, configPath, tokens, files, region, }) {
22
+ function addConfigMetadata({ rawConfig, customExtends, configPath, tokens, files, region, externalRefResolver, }) {
23
23
  var _a;
24
24
  return __awaiter(this, void 0, void 0, function* () {
25
25
  if (customExtends !== undefined) {
@@ -56,7 +56,11 @@ function addConfigMetadata({ rawConfig, customExtends, configPath, tokens, files
56
56
  : []));
57
57
  }
58
58
  }
59
- return (0, config_resolvers_1.resolveConfig)(Object.assign(Object.assign({}, rawConfig), { files: files !== null && files !== void 0 ? files : rawConfig.files, region: region !== null && region !== void 0 ? region : rawConfig.region }), configPath);
59
+ return (0, config_resolvers_1.resolveConfig)({
60
+ rawConfig: Object.assign(Object.assign({}, rawConfig), { files: files !== null && files !== void 0 ? files : rawConfig.files, region: region !== null && region !== void 0 ? region : rawConfig.region }),
61
+ configPath,
62
+ externalRefResolver,
63
+ });
60
64
  });
61
65
  }
62
66
  function loadConfig(options = {}) {
@@ -72,6 +76,7 @@ function loadConfig(options = {}) {
72
76
  tokens,
73
77
  files,
74
78
  region,
79
+ externalRefResolver,
75
80
  });
76
81
  });
77
82
  }
@@ -114,7 +114,7 @@ function mergeExtends(rulesConfList) {
114
114
  };
115
115
  for (const rulesConf of rulesConfList) {
116
116
  if (rulesConf.extends) {
117
- throw new Error(`'extends' is not supported in shared configs yet: ${JSON.stringify(rulesConf, null, 2)}.`);
117
+ throw new Error(`'extends' is not supported in shared configs yet:\n${JSON.stringify(rulesConf, null, 2)}`);
118
118
  }
119
119
  Object.assign(result.rules, rulesConf.rules);
120
120
  Object.assign(result.oas2Rules, rulesConf.oas2Rules);
@@ -5,16 +5,38 @@ const utils_1 = require("../../utils");
5
5
  const RemoveUnusedComponents = () => {
6
6
  const components = new Map();
7
7
  function registerComponent(location, componentType, name) {
8
- var _a;
8
+ var _a, _b;
9
9
  components.set(location.absolutePointer, {
10
- used: ((_a = components.get(location.absolutePointer)) === null || _a === void 0 ? void 0 : _a.used) || false,
10
+ usedIn: (_b = (_a = components.get(location.absolutePointer)) === null || _a === void 0 ? void 0 : _a.usedIn) !== null && _b !== void 0 ? _b : [],
11
11
  componentType,
12
12
  name,
13
13
  });
14
14
  }
15
+ function removeUnusedComponents(root, removedPaths) {
16
+ const removedLengthStart = removedPaths.length;
17
+ for (const [path, { usedIn, name, componentType }] of components) {
18
+ const used = usedIn.some((location) => !removedPaths.some((removed) =>
19
+ // Check if the current location's absolute pointer starts with the 'removed' path
20
+ // and either its length matches exactly with 'removed' or the character after the 'removed' path is a '/'
21
+ location.absolutePointer.startsWith(removed) &&
22
+ (location.absolutePointer.length === removed.length ||
23
+ location.absolutePointer[removed.length] === '/')));
24
+ if (!used && componentType) {
25
+ removedPaths.push(path);
26
+ delete root[componentType][name];
27
+ components.delete(path);
28
+ if ((0, utils_1.isEmptyObject)(root[componentType])) {
29
+ delete root[componentType];
30
+ }
31
+ }
32
+ }
33
+ return removedPaths.length > removedLengthStart
34
+ ? removeUnusedComponents(root, removedPaths)
35
+ : removedPaths.length;
36
+ }
15
37
  return {
16
38
  ref: {
17
- leave(ref, { type, resolve, key }) {
39
+ leave(ref, { location, type, resolve, key }) {
18
40
  if (['Schema', 'Parameter', 'Response', 'SecurityScheme'].includes(type.name)) {
19
41
  const resolvedRef = resolve(ref);
20
42
  if (!resolvedRef.location)
@@ -22,31 +44,23 @@ const RemoveUnusedComponents = () => {
22
44
  const [fileLocation, localPointer] = resolvedRef.location.absolutePointer.split('#', 2);
23
45
  const componentLevelLocalPointer = localPointer.split('/').slice(0, 3).join('/');
24
46
  const pointer = `${fileLocation}#${componentLevelLocalPointer}`;
25
- components.set(pointer, {
26
- used: true,
27
- name: key.toString(),
28
- });
47
+ const registered = components.get(pointer);
48
+ if (registered) {
49
+ registered.usedIn.push(location);
50
+ }
51
+ else {
52
+ components.set(pointer, {
53
+ usedIn: [location],
54
+ name: key.toString(),
55
+ });
56
+ }
29
57
  }
30
58
  },
31
59
  },
32
60
  Root: {
33
61
  leave(root, ctx) {
34
62
  const data = ctx.getVisitorData();
35
- data.removedCount = 0;
36
- const rootComponents = new Set();
37
- components.forEach((usageInfo) => {
38
- const { used, name, componentType } = usageInfo;
39
- if (!used && componentType) {
40
- rootComponents.add(componentType);
41
- delete root[componentType][name];
42
- data.removedCount++;
43
- }
44
- });
45
- for (const component of rootComponents) {
46
- if ((0, utils_1.isEmptyObject)(root[component])) {
47
- delete root[component];
48
- }
49
- }
63
+ data.removedCount = removeUnusedComponents(root, []);
50
64
  },
51
65
  },
52
66
  NamedSchemas: {
@@ -5,16 +5,36 @@ const utils_1 = require("../../utils");
5
5
  const RemoveUnusedComponents = () => {
6
6
  const components = new Map();
7
7
  function registerComponent(location, componentType, name) {
8
- var _a;
8
+ var _a, _b;
9
9
  components.set(location.absolutePointer, {
10
- used: ((_a = components.get(location.absolutePointer)) === null || _a === void 0 ? void 0 : _a.used) || false,
10
+ usedIn: (_b = (_a = components.get(location.absolutePointer)) === null || _a === void 0 ? void 0 : _a.usedIn) !== null && _b !== void 0 ? _b : [],
11
11
  componentType,
12
12
  name,
13
13
  });
14
14
  }
15
+ function removeUnusedComponents(root, removedPaths) {
16
+ const removedLengthStart = removedPaths.length;
17
+ for (const [path, { usedIn, name, componentType }] of components) {
18
+ const used = usedIn.some((location) => !removedPaths.some((removed) => location.absolutePointer.startsWith(removed) &&
19
+ (location.absolutePointer.length === removed.length ||
20
+ location.absolutePointer[removed.length] === '/')));
21
+ if (!used && componentType && root.components) {
22
+ removedPaths.push(path);
23
+ const componentChild = root.components[componentType];
24
+ delete componentChild[name];
25
+ components.delete(path);
26
+ if ((0, utils_1.isEmptyObject)(componentChild)) {
27
+ delete root.components[componentType];
28
+ }
29
+ }
30
+ }
31
+ return removedPaths.length > removedLengthStart
32
+ ? removeUnusedComponents(root, removedPaths)
33
+ : removedPaths.length;
34
+ }
15
35
  return {
16
36
  ref: {
17
- leave(ref, { type, resolve, key }) {
37
+ leave(ref, { location, type, resolve, key }) {
18
38
  if (['Schema', 'Header', 'Parameter', 'Response', 'Example', 'RequestBody'].includes(type.name)) {
19
39
  const resolvedRef = resolve(ref);
20
40
  if (!resolvedRef.location)
@@ -22,28 +42,23 @@ const RemoveUnusedComponents = () => {
22
42
  const [fileLocation, localPointer] = resolvedRef.location.absolutePointer.split('#', 2);
23
43
  const componentLevelLocalPointer = localPointer.split('/').slice(0, 4).join('/');
24
44
  const pointer = `${fileLocation}#${componentLevelLocalPointer}`;
25
- components.set(pointer, {
26
- used: true,
27
- name: key.toString(),
28
- });
45
+ const registered = components.get(pointer);
46
+ if (registered) {
47
+ registered.usedIn.push(location);
48
+ }
49
+ else {
50
+ components.set(pointer, {
51
+ usedIn: [location],
52
+ name: key.toString(),
53
+ });
54
+ }
29
55
  }
30
56
  },
31
57
  },
32
58
  Root: {
33
59
  leave(root, ctx) {
34
60
  const data = ctx.getVisitorData();
35
- data.removedCount = 0;
36
- components.forEach((usageInfo) => {
37
- const { used, componentType, name } = usageInfo;
38
- if (!used && componentType && root.components) {
39
- const componentChild = root.components[componentType];
40
- delete componentChild[name];
41
- data.removedCount++;
42
- if ((0, utils_1.isEmptyObject)(componentChild)) {
43
- delete root.components[componentType];
44
- }
45
- }
46
- });
61
+ data.removedCount = removeUnusedComponents(root, []);
47
62
  if ((0, utils_1.isEmptyObject)(root.components)) {
48
63
  delete root.components;
49
64
  }
@@ -4,7 +4,7 @@ export type Totals = {
4
4
  warnings: number;
5
5
  ignored: number;
6
6
  };
7
- export type OutputFormat = 'codeframe' | 'stylish' | 'json' | 'checkstyle' | 'codeclimate' | 'summary';
7
+ export type OutputFormat = 'codeframe' | 'stylish' | 'json' | 'checkstyle' | 'codeclimate' | 'summary' | 'github-actions' | 'markdown';
8
8
  export declare function getTotals(problems: (NormalizedProblem & {
9
9
  ignored?: boolean;
10
10
  })[]): Totals;
@@ -103,6 +103,8 @@ function formatProblems(problems, opts) {
103
103
  case 'summary':
104
104
  formatSummary(problems);
105
105
  break;
106
+ case 'github-actions':
107
+ outputForGithubActions(problems, cwd);
106
108
  }
107
109
  if (totalProblems - ignoredProblems > maxProblems) {
108
110
  logger_1.logger.info(`< ... ${totalProblems - maxProblems} more problems hidden > ${logger_1.colorize.gray('increase with `--max-problems N`')}\n`);
@@ -266,3 +268,58 @@ function xmlEscape(s) {
266
268
  }
267
269
  });
268
270
  }
271
+ function outputForGithubActions(problems, cwd) {
272
+ var _a, _b;
273
+ for (const problem of problems) {
274
+ for (const location of problem.location.map(codeframes_1.getLineColLocation)) {
275
+ let command;
276
+ switch (problem.severity) {
277
+ case 'error':
278
+ command = 'error';
279
+ break;
280
+ case 'warn':
281
+ command = 'warning';
282
+ break;
283
+ }
284
+ const suggest = formatDidYouMean(problem);
285
+ const message = suggest !== '' ? problem.message + '\n\n' + suggest : problem.message;
286
+ const properties = {
287
+ title: problem.ruleId,
288
+ file: (0, ref_utils_1.isAbsoluteUrl)(location.source.absoluteRef)
289
+ ? location.source.absoluteRef
290
+ : path.relative(cwd, location.source.absoluteRef),
291
+ line: location.start.line,
292
+ col: location.start.col,
293
+ endLine: (_a = location.end) === null || _a === void 0 ? void 0 : _a.line,
294
+ endColumn: (_b = location.end) === null || _b === void 0 ? void 0 : _b.col,
295
+ };
296
+ output_1.output.write(`::${command} ${formatProperties(properties)}::${escapeMessage(message)}\n`);
297
+ }
298
+ }
299
+ function formatProperties(props) {
300
+ return Object.entries(props)
301
+ .filter(([, v]) => v !== null && v !== undefined)
302
+ .map(([k, v]) => `${k}=${escapeProperty(v)}`)
303
+ .join(',');
304
+ }
305
+ function toString(v) {
306
+ if (v === null || v === undefined) {
307
+ return '';
308
+ }
309
+ else if (typeof v === 'string' || v instanceof String) {
310
+ return v;
311
+ }
312
+ return JSON.stringify(v);
313
+ }
314
+ function escapeMessage(v) {
315
+ return toString(v).replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
316
+ }
317
+ function escapeProperty(v) {
318
+ return toString(v)
319
+ .replace(/%/g, '%25')
320
+ .replace(/\r/g, '%0D')
321
+ .replace(/\n/g, '%0A')
322
+ .replace(/:/g, '%3A')
323
+ .replace(/,/g, '%2C');
324
+ }
325
+ }
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy } from './utils';
1
+ export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy, pause, } from './utils';
2
2
  export { Oas3_1Types } from './types/oas3_1';
3
3
  export { Oas3Types } from './types/oas3';
4
4
  export { Oas2Types } from './types/oas2';
package/lib/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.lintConfig = exports.lintFromString = exports.lintDocument = exports.validate = exports.lint = exports.getTotals = exports.formatProblems = exports.getLineColLocation = exports.getAstNodeByPointer = exports.walkDocument = exports.normalizeVisitors = exports.getTypes = exports.detectSpec = exports.SpecVersion = exports.getMajorSpecVersion = exports.SpecMajorVersion = exports.isAbsoluteUrl = exports.isRef = exports.unescapePointer = exports.stringifyYaml = exports.parseYaml = exports.makeDocumentFromString = exports.YamlParseError = exports.ResolveError = exports.resolveDocument = exports.BaseResolver = exports.Source = exports.isRedoclyRegistryURL = exports.RedoclyClient = exports.createConfig = exports.CONFIG_FILE_NAMES = exports.findConfig = exports.getConfig = exports.loadConfig = exports.transformConfig = exports.getMergedConfig = exports.IGNORE_FILE = exports.StyleguideConfig = exports.Config = exports.Stats = exports.normalizeTypes = exports.ConfigTypes = exports.AsyncApi2Types = exports.Oas2Types = exports.Oas3Types = exports.Oas3_1Types = exports.isTruthy = exports.doesYamlFileExist = exports.slash = exports.readFileFromUrl = void 0;
4
- exports.bundleFromString = exports.mapTypeToComponent = exports.bundleDocument = exports.bundle = void 0;
3
+ exports.lintFromString = exports.lintDocument = exports.validate = exports.lint = exports.getTotals = exports.formatProblems = exports.getLineColLocation = exports.getAstNodeByPointer = exports.walkDocument = exports.normalizeVisitors = exports.getTypes = exports.detectSpec = exports.SpecVersion = exports.getMajorSpecVersion = exports.SpecMajorVersion = exports.isAbsoluteUrl = exports.isRef = exports.unescapePointer = exports.stringifyYaml = exports.parseYaml = exports.makeDocumentFromString = exports.YamlParseError = exports.ResolveError = exports.resolveDocument = exports.BaseResolver = exports.Source = exports.isRedoclyRegistryURL = exports.RedoclyClient = exports.createConfig = exports.CONFIG_FILE_NAMES = exports.findConfig = exports.getConfig = exports.loadConfig = exports.transformConfig = exports.getMergedConfig = exports.IGNORE_FILE = exports.StyleguideConfig = exports.Config = exports.Stats = exports.normalizeTypes = exports.ConfigTypes = exports.AsyncApi2Types = exports.Oas2Types = exports.Oas3Types = exports.Oas3_1Types = exports.pause = exports.isTruthy = exports.doesYamlFileExist = exports.slash = exports.readFileFromUrl = void 0;
4
+ exports.bundleFromString = exports.mapTypeToComponent = exports.bundleDocument = exports.bundle = exports.lintConfig = void 0;
5
5
  var utils_1 = require("./utils");
6
6
  Object.defineProperty(exports, "readFileFromUrl", { enumerable: true, get: function () { return utils_1.readFileFromUrl; } });
7
7
  Object.defineProperty(exports, "slash", { enumerable: true, get: function () { return utils_1.slash; } });
8
8
  Object.defineProperty(exports, "doesYamlFileExist", { enumerable: true, get: function () { return utils_1.doesYamlFileExist; } });
9
9
  Object.defineProperty(exports, "isTruthy", { enumerable: true, get: function () { return utils_1.isTruthy; } });
10
+ Object.defineProperty(exports, "pause", { enumerable: true, get: function () { return utils_1.pause; } });
10
11
  var oas3_1_1 = require("./types/oas3_1");
11
12
  Object.defineProperty(exports, "Oas3_1Types", { enumerable: true, get: function () { return oas3_1_1.Oas3_1Types; } });
12
13
  var oas3_1 = require("./types/oas3");
@@ -32,10 +32,10 @@ function getAssertsToApply(assertion) {
32
32
  const shouldRunOnKeys = assertsToApply.find((assert) => assert.runsOnKeys && !assert.runsOnValues);
33
33
  const shouldRunOnValues = assertsToApply.find((assert) => assert.runsOnValues && !assert.runsOnKeys);
34
34
  if (shouldRunOnValues && !assertion.subject.property) {
35
- throw new Error(`${shouldRunOnValues.name} can't be used on all keys. Please provide a single property`);
35
+ throw new Error(`The '${shouldRunOnValues.name}' assertion can't be used on all keys. Please provide a single property.`);
36
36
  }
37
37
  if (shouldRunOnKeys && assertion.subject.property) {
38
- throw new Error(`${shouldRunOnKeys.name} can't be used on a single property. Please use 'property'.`);
38
+ throw new Error(`The '${shouldRunOnKeys.name}' assertion can't be used on properties. Please remove the 'property' key.`);
39
39
  }
40
40
  return assertsToApply;
41
41
  }
@@ -28,6 +28,9 @@ const ValidContentExamples = (opts) => {
28
28
  location = isMultiple ? resolved.location.child('value') : resolved.location;
29
29
  example = resolved.node;
30
30
  }
31
+ if (isMultiple && typeof example.value === 'undefined') {
32
+ return;
33
+ }
31
34
  (0, utils_1.validateExample)(isMultiple ? example.value : example, mediaType.schema, location, ctx, allowAdditionalProperties);
32
35
  }
33
36
  },
package/lib/utils.d.ts CHANGED
@@ -50,3 +50,4 @@ export declare function identity<T>(value: T): T;
50
50
  export declare function keysOf<T>(obj: T): (keyof T)[];
51
51
  export declare function pickDefined<T extends Record<string, unknown>>(obj?: T): Record<string, unknown> | undefined;
52
52
  export declare function nextTick(): void;
53
+ export declare function pause(ms: number): Promise<void>;
package/lib/utils.js CHANGED
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.nextTick = exports.pickDefined = exports.keysOf = exports.identity = exports.isTruthy = exports.showErrorForDeprecatedField = exports.showWarningForDeprecatedField = exports.doesYamlFileExist = exports.isCustomRuleId = exports.getMatchingStatusCodeRange = exports.assignExisting = exports.isNotString = exports.isString = exports.isNotEmptyObject = exports.slash = exports.isPathParameter = exports.yamlAndJsonSyncReader = exports.readFileAsStringSync = exports.isSingular = exports.validateMimeTypeOAS3 = exports.validateMimeType = exports.splitCamelCaseIntoWords = exports.omitObjectProps = exports.pickObjectProps = exports.readFileFromUrl = exports.isEmptyArray = exports.isEmptyObject = exports.isPlainObject = exports.isDefined = exports.loadYaml = exports.popStack = exports.pushStack = exports.stringifyYaml = exports.parseYaml = void 0;
12
+ exports.pause = exports.nextTick = exports.pickDefined = exports.keysOf = exports.identity = exports.isTruthy = exports.showErrorForDeprecatedField = exports.showWarningForDeprecatedField = exports.doesYamlFileExist = exports.isCustomRuleId = exports.getMatchingStatusCodeRange = exports.assignExisting = exports.isNotString = exports.isString = exports.isNotEmptyObject = exports.slash = exports.isPathParameter = exports.yamlAndJsonSyncReader = exports.readFileAsStringSync = exports.isSingular = exports.validateMimeTypeOAS3 = exports.validateMimeType = exports.splitCamelCaseIntoWords = exports.omitObjectProps = exports.pickObjectProps = exports.readFileFromUrl = exports.isEmptyArray = exports.isEmptyObject = exports.isPlainObject = exports.isDefined = exports.loadYaml = exports.popStack = exports.pushStack = exports.stringifyYaml = exports.parseYaml = void 0;
13
13
  const fs = require("fs");
14
14
  const path_1 = require("path");
15
15
  const minimatch = require("minimatch");
@@ -236,6 +236,12 @@ function nextTick() {
236
236
  });
237
237
  }
238
238
  exports.nextTick = nextTick;
239
+ function pause(ms) {
240
+ return __awaiter(this, void 0, void 0, function* () {
241
+ return new Promise((resolve) => setTimeout(resolve, ms));
242
+ });
243
+ }
244
+ exports.pause = pause;
239
245
  function getUpdatedFieldName(updatedField, updatedObject) {
240
246
  return `${typeof updatedObject !== 'undefined' ? `${updatedObject}.` : ''}${updatedField}`;
241
247
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redocly/openapi-core",
3
- "version": "1.10.6",
3
+ "version": "1.12.0",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "engines": {
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@redocly/ajv": "^8.11.0",
38
- "@redocly/config": "^0.1.4",
38
+ "@redocly/config": "^0.2.0",
39
39
  "colorette": "^1.2.0",
40
40
  "js-levenshtein": "^1.1.6",
41
41
  "js-yaml": "^4.1.0",
@@ -1,6 +1,8 @@
1
1
  import { outdent } from 'outdent';
2
2
 
3
3
  import { formatProblems, getTotals } from '../format/format';
4
+ import { LocationObject, NormalizedProblem } from '../walk';
5
+ import { Source } from '../resolve';
4
6
 
5
7
  describe('format', () => {
6
8
  function replaceColors(log: string) {
@@ -40,6 +42,10 @@ describe('format', () => {
40
42
  output += str;
41
43
  return true;
42
44
  });
45
+ jest.spyOn(process.stdout, 'write').mockImplementation((str: string | Uint8Array) => {
46
+ output += str;
47
+ return true;
48
+ });
43
49
  });
44
50
 
45
51
  it('should correctly format summary output', () => {
@@ -73,4 +79,32 @@ describe('format', () => {
73
79
  "
74
80
  `);
75
81
  });
82
+
83
+ it('should format problems using github-actions', () => {
84
+ const problems = [
85
+ {
86
+ ruleId: 'spec',
87
+ message: 'message',
88
+ severity: 'error' as const,
89
+ location: [
90
+ {
91
+ source: { absoluteRef: 'openapi.yaml' } as Source,
92
+ start: { line: 1, col: 2 },
93
+ end: { line: 3, col: 4 },
94
+ } as LocationObject,
95
+ ],
96
+ suggest: [],
97
+ },
98
+ ];
99
+
100
+ formatProblems(problems, {
101
+ format: 'github-actions',
102
+ version: '1.0.0',
103
+ totals: getTotals(problems),
104
+ });
105
+
106
+ expect(output).toEqual(
107
+ '::error title=spec,file=openapi.yaml,line=1,col=2,endLine=3,endColumn=4::message\n'
108
+ );
109
+ });
76
110
  });
package/src/bundle.ts CHANGED
@@ -232,7 +232,7 @@ export async function bundleDocument(opts: {
232
232
 
233
233
  walkDocument({
234
234
  document,
235
- rootType: types.Root as NormalizedNodeType,
235
+ rootType: types.Root,
236
236
  normalizedVisitors: bundleVisitor,
237
237
  resolvedRefMap,
238
238
  ctx,
@@ -390,7 +390,7 @@ describe('resolveConfig', () => {
390
390
  },
391
391
  };
392
392
 
393
- const { apis } = await resolveConfig(rawConfig, configPath);
393
+ const { apis } = await resolveConfig({ rawConfig, configPath });
394
394
  //@ts-ignore
395
395
  expect(apis['petstore'].styleguide.plugins.length).toEqual(1);
396
396
  //@ts-ignore
@@ -427,7 +427,7 @@ describe('resolveConfig', () => {
427
427
  },
428
428
  };
429
429
 
430
- const { apis } = await resolveConfig(rawConfig, configPath);
430
+ const { apis } = await resolveConfig({ rawConfig, configPath });
431
431
  expect(apis['petstore'].styleguide.rules).toBeDefined();
432
432
  expect(Object.keys(apis['petstore'].styleguide.rules || {}).length).toEqual(7);
433
433
  expect(apis['petstore'].styleguide.rules?.['operation-2xx-response']).toEqual('warn');
@@ -469,7 +469,7 @@ describe('resolveConfig', () => {
469
469
  },
470
470
  };
471
471
 
472
- const { apis } = await resolveConfig(rawConfig, configPath);
472
+ const { apis } = await resolveConfig({ rawConfig, configPath });
473
473
  expect(apis['petstore'].styleguide.rules).toBeDefined();
474
474
  expect(apis['petstore'].styleguide.rules?.['operation-2xx-response']).toEqual('warn');
475
475
  expect(apis['petstore'].styleguide.rules?.['operation-4xx-response']).toEqual('error');
@@ -513,7 +513,7 @@ describe('resolveConfig', () => {
513
513
  },
514
514
  };
515
515
 
516
- const { apis } = await resolveConfig(rawConfig, configPath);
516
+ const { apis } = await resolveConfig({ rawConfig, configPath });
517
517
  expect(apis['petstore'].styleguide.rules).toBeDefined();
518
518
  expect(apis['petstore'].styleguide.rules?.['operation-2xx-response']).toEqual('warn'); // from minimal ruleset
519
519
  });
@@ -0,0 +1,2 @@
1
+ extends:
2
+ - https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/main/rulesets/spec-compliant/redocly.yaml
@@ -5,6 +5,7 @@ import { lintConfig } from '../../lint';
5
5
  import { replaceSourceWithRef } from '../../../__tests__/utils';
6
6
  import type { RuleConfig, FlatRawConfig } from './../types';
7
7
  import type { NormalizedProblem } from '../../walk';
8
+ import { BaseResolver } from '../../resolve';
8
9
 
9
10
  const fs = require('fs');
10
11
  const path = require('path');
@@ -58,6 +59,19 @@ describe('loadConfig', () => {
58
59
  });
59
60
  expect(mockFn).toHaveBeenCalled();
60
61
  });
62
+
63
+ it('should call externalRefResolver if such passed', async () => {
64
+ const externalRefResolver = new BaseResolver();
65
+ const resolverSpy = jest.spyOn(externalRefResolver, 'resolveDocument');
66
+ await loadConfig({
67
+ configPath: path.join(__dirname, './fixtures/load-external.yaml'),
68
+ externalRefResolver: externalRefResolver as any,
69
+ });
70
+ expect(resolverSpy).toHaveBeenCalledWith(
71
+ null,
72
+ 'https://raw.githubusercontent.com/Redocly/redocly-cli-cookbook/main/rulesets/spec-compliant/redocly.yaml'
73
+ );
74
+ });
61
75
  });
62
76
 
63
77
  describe('findConfig', () => {
@@ -12,7 +12,7 @@ import {
12
12
  transformConfig,
13
13
  } from './utils';
14
14
  import { isBrowser } from '../env';
15
- import { isNotString, isString, isDefined, parseYaml, keysOf } from '../utils';
15
+ import { isNotString, isString, isDefined, keysOf } from '../utils';
16
16
  import { Config } from './config';
17
17
  import { colorize, logger } from '../logger';
18
18
  import { asserts, buildAssertCustomFunction } from '../rules/common/assertions/asserts';
@@ -63,14 +63,22 @@ export async function resolveConfigFileAndRefs({
63
63
  return { document, resolvedRefMap };
64
64
  }
65
65
 
66
- export async function resolveConfig(rawConfig: RawConfig, configPath?: string): Promise<Config> {
66
+ export async function resolveConfig({
67
+ rawConfig,
68
+ configPath,
69
+ externalRefResolver,
70
+ }: {
71
+ rawConfig: RawConfig;
72
+ configPath?: string;
73
+ externalRefResolver?: BaseResolver;
74
+ }): Promise<Config> {
67
75
  if (rawConfig.styleguide?.extends?.some(isNotString)) {
68
76
  throw new Error(
69
77
  `Error configuration format not detected in extends value must contain strings`
70
78
  );
71
79
  }
72
80
 
73
- const resolver = new BaseResolver(getResolveConfig(rawConfig.resolve));
81
+ const resolver = externalRefResolver ?? new BaseResolver(getResolveConfig(rawConfig.resolve));
74
82
 
75
83
  const apis = await resolveApis({
76
84
  rawConfig,
@@ -388,10 +396,8 @@ async function loadExtendStyleguideConfig(
388
396
  resolver: BaseResolver
389
397
  ): Promise<StyleguideRawConfig> {
390
398
  try {
391
- const fileSource = await resolver.loadExternalRef(filePath);
392
- const rawConfig = transformConfig(
393
- parseYaml(fileSource.body) as RawConfig & DeprecatedInRawConfig
394
- );
399
+ const { parsed } = (await resolver.resolveDocument(null, filePath)) as Document;
400
+ const rawConfig = transformConfig(parsed as RawConfig & DeprecatedInRawConfig);
395
401
  if (!rawConfig.styleguide) {
396
402
  throw new Error(`Styleguide configuration format not detected: "${filePath}"`);
397
403
  }