@redocly/openapi-core 1.2.0 → 1.2.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.
@@ -0,0 +1,10 @@
1
+ openapi: 3.0.0
2
+ info:
3
+ title: Test API
4
+ version: '1.0'
5
+ description: Test
6
+ license: Fail
7
+
8
+ servers:
9
+ - url: http://redocly-example.com
10
+ paths: {}
@@ -0,0 +1,2 @@
1
+ extends:
2
+ - recommended
@@ -1,7 +1,7 @@
1
1
  import * as path from 'path';
2
2
  import { outdent } from 'outdent';
3
3
 
4
- import { lintFromString, lintConfig, lintDocument } from '../lint';
4
+ import { lintFromString, lintConfig, lintDocument, lint } from '../lint';
5
5
  import { BaseResolver } from '../resolve';
6
6
  import { loadConfig } from '../config/load';
7
7
  import { parseYamlToDocument, replaceSourceWithRef, makeConfig } from '../../__tests__/utils';
@@ -20,7 +20,7 @@ describe('lint', () => {
20
20
  license: Fail
21
21
 
22
22
  servers:
23
- - url: http://example.com
23
+ - url: http://redocly-example.com
24
24
  paths: {}
25
25
  `,
26
26
  config: await loadConfig(),
@@ -46,6 +46,35 @@ describe('lint', () => {
46
46
  `);
47
47
  });
48
48
 
49
+ it('lint should work', async () => {
50
+ const results = await lint({
51
+ ref: path.join(__dirname, 'fixtures/lint/openapi.yaml'),
52
+ config: await loadConfig({
53
+ configPath: path.join(__dirname, 'fixtures/redocly.yaml'),
54
+ }),
55
+ });
56
+
57
+ expect(replaceSourceWithRef(results, path.join(__dirname, 'fixtures/lint/')))
58
+ .toMatchInlineSnapshot(`
59
+ Array [
60
+ Object {
61
+ "from": undefined,
62
+ "location": Array [
63
+ Object {
64
+ "pointer": "#/info/license",
65
+ "reportOnKey": false,
66
+ "source": "openapi.yaml",
67
+ },
68
+ ],
69
+ "message": "Expected type \`License\` (object) but got \`string\`",
70
+ "ruleId": "spec",
71
+ "severity": "error",
72
+ "suggest": Array [],
73
+ },
74
+ ]
75
+ `);
76
+ });
77
+
49
78
  it('lintConfig should work', async () => {
50
79
  const document = parseYamlToDocument(
51
80
  outdent`
package/src/bundle.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import isEqual = require('lodash.isequal');
2
- import { BaseResolver, resolveDocument, Document, ResolvedRefMap, makeRefId } from './resolve';
2
+ import {
3
+ BaseResolver,
4
+ resolveDocument,
5
+ Document,
6
+ ResolvedRefMap,
7
+ makeRefId,
8
+ makeDocumentFromString,
9
+ } from './resolve';
3
10
  import { Oas3Rule, normalizeVisitors, Oas3Visitor, Oas2Visitor } from './visitors';
4
11
  import { NormalizedNodeType, normalizeTypes, NodeType } from './types';
5
12
  import { WalkContext, walkDocument, UserContext, ResolveResult, NormalizedProblem } from './walk';
@@ -22,10 +29,7 @@ export enum OasVersion {
22
29
  Version3_0 = 'oas3_0',
23
30
  Version3_1 = 'oas3_1',
24
31
  }
25
-
26
- export async function bundle(opts: {
27
- ref?: string;
28
- doc?: Document;
32
+ export type BundleOptions = {
29
33
  externalRefResolver?: BaseResolver;
30
34
  config: Config;
31
35
  dereference?: boolean;
@@ -33,7 +37,14 @@ export async function bundle(opts: {
33
37
  skipRedoclyRegistryRefs?: boolean;
34
38
  removeUnusedComponents?: boolean;
35
39
  keepUrlRefs?: boolean;
36
- }) {
40
+ };
41
+
42
+ export async function bundle(
43
+ opts: {
44
+ ref?: string;
45
+ doc?: Document;
46
+ } & BundleOptions
47
+ ) {
37
48
  const {
38
49
  ref,
39
50
  doc,
@@ -45,7 +56,7 @@ export async function bundle(opts: {
45
56
  }
46
57
 
47
58
  const document =
48
- doc !== undefined ? doc : await externalRefResolver.resolveDocument(base, ref!, true);
59
+ doc === undefined ? await externalRefResolver.resolveDocument(base, ref!, true) : doc;
49
60
 
50
61
  if (document instanceof Error) {
51
62
  throw document;
@@ -59,6 +70,23 @@ export async function bundle(opts: {
59
70
  });
60
71
  }
61
72
 
73
+ export async function bundleFromString(
74
+ opts: {
75
+ source: string;
76
+ absoluteRef?: string;
77
+ } & BundleOptions
78
+ ) {
79
+ const { source, absoluteRef, externalRefResolver = new BaseResolver(opts.config.resolve) } = opts;
80
+ const document = makeDocumentFromString(source, absoluteRef || '/');
81
+
82
+ return bundleDocument({
83
+ document,
84
+ ...opts,
85
+ externalRefResolver,
86
+ config: opts.config.styleguide,
87
+ });
88
+ }
89
+
62
90
  type BundleContext = WalkContext;
63
91
 
64
92
  export type BundleResult = {
@@ -73,8 +73,8 @@ describe('findConfig', () => {
73
73
  .spyOn(fs, 'existsSync')
74
74
  .mockImplementation((name) => name === 'redocly.yaml' || name === '.redocly.yaml');
75
75
  expect(findConfig).toThrow(`
76
- Multiple configuration files are not allowed.
77
- Found the following files: redocly.yaml, .redocly.yaml.
76
+ Multiple configuration files are not allowed.
77
+ Found the following files: redocly.yaml, .redocly.yaml.
78
78
  Please use 'redocly.yaml' instead.
79
79
  `);
80
80
  });
@@ -124,6 +124,39 @@ describe('createConfig', () => {
124
124
  overridesRules: rawConfig.rules as Record<string, RuleConfig>,
125
125
  });
126
126
  });
127
+
128
+ it('should create config from object with a custom plugin', async () => {
129
+ const testCustomRule = jest.fn();
130
+ const rawConfig: FlatRawConfig = {
131
+ extends: [],
132
+ plugins: [
133
+ {
134
+ id: 'my-plugin',
135
+ rules: {
136
+ oas3: {
137
+ 'test-rule': testCustomRule,
138
+ },
139
+ },
140
+ },
141
+ ],
142
+ rules: {
143
+ 'my-plugin/test-rule': 'error',
144
+ },
145
+ };
146
+ const config = await createConfig(rawConfig);
147
+
148
+ expect(config.styleguide.plugins[0]).toEqual({
149
+ id: 'my-plugin',
150
+ rules: {
151
+ oas3: {
152
+ 'my-plugin/test-rule': testCustomRule,
153
+ },
154
+ },
155
+ });
156
+ expect(config.styleguide.rules.oas3_0).toEqual({
157
+ 'my-plugin/test-rule': 'error',
158
+ });
159
+ });
127
160
  });
128
161
 
129
162
  function verifyExtendedConfig(
@@ -7,7 +7,13 @@ import { Config, DOMAINS } from './config';
7
7
  import { transformConfig } from './utils';
8
8
  import { resolveConfig } from './config-resolvers';
9
9
 
10
- import type { DeprecatedInRawConfig, FlatRawConfig, RawConfig, Region } from './types';
10
+ import type {
11
+ DeprecatedInRawConfig,
12
+ FlatRawConfig,
13
+ RawConfig,
14
+ RawUniversalConfig,
15
+ Region,
16
+ } from './types';
11
17
  import { RegionalTokenWithValidity } from '../redocly/redocly-client-types';
12
18
 
13
19
  async function addConfigMetadata({
@@ -101,8 +107,8 @@ export function findConfig(dir?: string): string | undefined {
101
107
  ).filter(fs.existsSync);
102
108
  if (existingConfigFiles.length > 1) {
103
109
  throw new Error(`
104
- Multiple configuration files are not allowed.
105
- Found the following files: ${existingConfigFiles.join(', ')}.
110
+ Multiple configuration files are not allowed.
111
+ Found the following files: ${existingConfigFiles.join(', ')}.
106
112
  Please use 'redocly.yaml' instead.
107
113
  `);
108
114
  }
@@ -129,10 +135,11 @@ export async function getConfig(
129
135
  type CreateConfigOptions = {
130
136
  extends?: string[];
131
137
  tokens?: RegionalTokenWithValidity[];
138
+ configPath?: string;
132
139
  };
133
140
 
134
141
  export async function createConfig(
135
- config: string | RawConfig,
142
+ config: string | RawUniversalConfig,
136
143
  options?: CreateConfigOptions
137
144
  ): Promise<Config> {
138
145
  return addConfigMetadata({
@@ -185,6 +185,9 @@ export type RawConfig = {
185
185
  telemetry?: Telemetry;
186
186
  } & ThemeConfig;
187
187
 
188
+ // RawConfig is legacy, use RawUniversalConfig in public APIs
189
+ export type RawUniversalConfig = Omit<RawConfig, 'styleguide'> & StyleguideRawConfig;
190
+
188
191
  export type FlatApi = Omit<Api, 'styleguide'> &
189
192
  Omit<ApiStyleguideRawConfig, 'doNotResolveExamples'>;
190
193
 
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { BundleOutputFormat, readFileFromUrl, slash, doesYamlFileExist, isTruthy
2
2
  export { Oas3_1Types } from './types/oas3_1';
3
3
  export { Oas3Types } from './types/oas3';
4
4
  export { Oas2Types } from './types/oas2';
5
+ export { AsyncApi2Types } from './types/asyncapi';
5
6
  export { ConfigTypes } from './types/redocly-yaml';
6
7
  export type {
7
8
  Oas3Definition,
@@ -26,6 +27,7 @@ export {
26
27
  Config,
27
28
  StyleguideConfig,
28
29
  RawConfig,
30
+ RawUniversalConfig,
29
31
  IGNORE_FILE,
30
32
  Region,
31
33
  getMergedConfig,
@@ -74,4 +76,4 @@ export {
74
76
  export { getAstNodeByPointer, getLineColLocation } from './format/codeframes';
75
77
  export { formatProblems, OutputFormat, getTotals, Totals } from './format/format';
76
78
  export { lint, lint as validate, lintDocument, lintFromString, lintConfig } from './lint';
77
- export { bundle, bundleDocument, mapTypeToComponent } from './bundle';
79
+ export { bundle, bundleDocument, mapTypeToComponent, bundleFromString } from './bundle';
@@ -66,7 +66,11 @@ export const runOnValuesSet = new Set<keyof Asserts>([
66
66
  ]);
67
67
 
68
68
  export const asserts: Asserts = {
69
- pattern: (value: string | string[], condition: string, { baseLocation }: AssertionFnContext) => {
69
+ pattern: (
70
+ value: string | string[],
71
+ condition: string,
72
+ { baseLocation, rawValue }: AssertionFnContext
73
+ ) => {
70
74
  if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
71
75
  const values = Array.isArray(value) ? value : [value];
72
76
  const regex = regexFromString(condition);
@@ -76,7 +80,11 @@ export const asserts: Asserts = {
76
80
  (_val) =>
77
81
  !regex?.test(_val) && {
78
82
  message: `"${_val}" should match a regex ${condition}`,
79
- location: runOnValue(value) ? baseLocation : baseLocation.key(),
83
+ location: runOnValue(value)
84
+ ? baseLocation
85
+ : isPlainObject(rawValue)
86
+ ? baseLocation.child(_val).key()
87
+ : baseLocation.key(),
80
88
  }
81
89
  )
82
90
  .filter(isTruthy);
@@ -84,7 +92,7 @@ export const asserts: Asserts = {
84
92
  notPattern: (
85
93
  value: string | string[],
86
94
  condition: string,
87
- { baseLocation }: AssertionFnContext
95
+ { baseLocation, rawValue }: AssertionFnContext
88
96
  ) => {
89
97
  if (typeof value === 'undefined' || isPlainObject(value)) return []; // property doesn't exist or is an object, no need to lint it with this assert
90
98
  const values = Array.isArray(value) ? value : [value];
@@ -95,7 +103,11 @@ export const asserts: Asserts = {
95
103
  (_val) =>
96
104
  regex?.test(_val) && {
97
105
  message: `"${_val}" should not match a regex ${condition}`,
98
- location: runOnValue(value) ? baseLocation : baseLocation.key(),
106
+ location: runOnValue(value)
107
+ ? baseLocation
108
+ : isPlainObject(rawValue)
109
+ ? baseLocation.child(_val).key()
110
+ : baseLocation.key(),
99
111
  }
100
112
  )
101
113
  .filter(isTruthy);
@@ -30,7 +30,7 @@ describe('Oas3 oas3-no-server-example.com', () => {
30
30
  "source": "foobar.yaml",
31
31
  },
32
32
  ],
33
- "message": "Server \`url\` should not point at example.com.",
33
+ "message": "Server \`url\` should not point to example.com or localhost.",
34
34
  "ruleId": "no-server-example.com",
35
35
  "severity": "error",
36
36
  "suggest": Array [],
@@ -57,4 +57,39 @@ describe('Oas3 oas3-no-server-example.com', () => {
57
57
 
58
58
  expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
59
59
  });
60
+
61
+ it('oas3-no-server-example.com: should report on server object with "foo.example.com" url', async () => {
62
+ const document = parseYamlToDocument(
63
+ outdent`
64
+ openapi: 3.0.0
65
+ servers:
66
+ - url: foo.example.com
67
+ `,
68
+ 'foobar.yaml'
69
+ );
70
+
71
+ const results = await lintDocument({
72
+ externalRefResolver: new BaseResolver(),
73
+ document,
74
+ config: await makeConfig({ 'no-server-example.com': 'error' }),
75
+ });
76
+
77
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`
78
+ Array [
79
+ Object {
80
+ "location": Array [
81
+ Object {
82
+ "pointer": "#/servers/0/url",
83
+ "reportOnKey": false,
84
+ "source": "foobar.yaml",
85
+ },
86
+ ],
87
+ "message": "Server \`url\` should not point to example.com or localhost.",
88
+ "ruleId": "no-server-example.com",
89
+ "severity": "error",
90
+ "suggest": Array [],
91
+ },
92
+ ]
93
+ `);
94
+ });
60
95
  });
@@ -17,7 +17,7 @@ describe('Oas3 Structural visitor basic', () => {
17
17
  - 25.3
18
18
  - test
19
19
  servers:
20
- - url: 'http://example.com'
20
+ - url: 'http://redocly-example.com'
21
21
  variables:
22
22
  a:
23
23
  default: test
@@ -3,9 +3,10 @@ import { Oas3Rule } from '../../visitors';
3
3
  export const NoServerExample: Oas3Rule = () => {
4
4
  return {
5
5
  Server(server, { report, location }) {
6
- if (['example.com', 'localhost'].indexOf(server.url) !== -1) {
6
+ const pattern = /^(.*[\/.])?(example\.com|localhost)([\/:?].*|$)/;
7
+ if (server.url && pattern.test(server.url)) {
7
8
  report({
8
- message: 'Server `url` should not point at example.com.',
9
+ message: 'Server `url` should not point to example.com or localhost.',
9
10
  location: location.child(['url']),
10
11
  });
11
12
  }
@@ -390,7 +390,13 @@ const Schema: NodeType = {
390
390
  maxContains: { type: 'integer', minimum: 0 },
391
391
  patternProperties: { type: 'object' },
392
392
  propertyNames: 'Schema',
393
- unevaluatedItems: 'Schema',
393
+ unevaluatedItems: (value: unknown) => {
394
+ if (typeof value === 'boolean') {
395
+ return { type: 'boolean' };
396
+ } else {
397
+ return 'Schema';
398
+ }
399
+ },
394
400
  unevaluatedProperties: (value: unknown) => {
395
401
  if (typeof value === 'boolean') {
396
402
  return { type: 'boolean' };
@@ -137,7 +137,13 @@ const Schema: NodeType = {
137
137
  maxContains: { type: 'integer', minimum: 0 },
138
138
  patternProperties: { type: 'object' },
139
139
  propertyNames: 'Schema',
140
- unevaluatedItems: 'Schema',
140
+ unevaluatedItems: (value: unknown) => {
141
+ if (typeof value === 'boolean') {
142
+ return { type: 'boolean' };
143
+ } else {
144
+ return 'Schema';
145
+ }
146
+ },
141
147
  unevaluatedProperties: (value: unknown) => {
142
148
  if (typeof value === 'boolean') {
143
149
  return { type: 'boolean' };