@redocly/openapi-core 1.0.0-beta.68 → 1.0.0-beta.72

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 (61) hide show
  1. package/__tests__/lint.test.ts +1 -1
  2. package/__tests__/login.test.ts +17 -0
  3. package/lib/bundle.d.ts +4 -0
  4. package/lib/bundle.js +9 -3
  5. package/lib/config/all.js +2 -0
  6. package/lib/config/config.d.ts +10 -0
  7. package/lib/config/config.js +7 -1
  8. package/lib/config/load.js +17 -8
  9. package/lib/index.d.ts +2 -2
  10. package/lib/lint.js +2 -0
  11. package/lib/redocly/index.d.ts +26 -20
  12. package/lib/redocly/index.js +83 -214
  13. package/lib/redocly/registry-api-types.d.ts +28 -0
  14. package/lib/redocly/registry-api-types.js +2 -0
  15. package/lib/redocly/registry-api.d.ts +14 -0
  16. package/lib/redocly/registry-api.js +105 -0
  17. package/lib/rules/common/no-invalid-parameter-examples.d.ts +1 -0
  18. package/lib/rules/common/no-invalid-parameter-examples.js +25 -0
  19. package/lib/rules/common/no-invalid-schema-examples.d.ts +1 -0
  20. package/lib/rules/common/no-invalid-schema-examples.js +23 -0
  21. package/lib/rules/common/paths-kebab-case.js +1 -1
  22. package/lib/rules/common/registry-dependencies.js +4 -7
  23. package/lib/rules/oas2/index.d.ts +2 -0
  24. package/lib/rules/oas2/index.js +4 -0
  25. package/lib/rules/oas3/index.js +4 -0
  26. package/lib/rules/oas3/no-invalid-media-type-examples.js +5 -26
  27. package/lib/rules/utils.d.ts +3 -0
  28. package/lib/rules/utils.js +26 -1
  29. package/lib/typings/openapi.d.ts +3 -0
  30. package/lib/utils.d.ts +1 -0
  31. package/lib/utils.js +5 -1
  32. package/lib/walk.d.ts +2 -0
  33. package/lib/walk.js +7 -0
  34. package/package.json +1 -1
  35. package/src/bundle.ts +25 -3
  36. package/src/config/__tests__/load.test.ts +35 -0
  37. package/src/config/all.ts +2 -0
  38. package/src/config/config.ts +11 -0
  39. package/src/config/load.ts +20 -9
  40. package/src/index.ts +2 -8
  41. package/src/lint.ts +2 -0
  42. package/src/redocly/__tests__/redocly-client.test.ts +120 -0
  43. package/src/redocly/index.ts +101 -227
  44. package/src/redocly/registry-api-types.ts +31 -0
  45. package/src/redocly/registry-api.ts +110 -0
  46. package/src/rules/common/__tests__/paths-kebab-case.test.ts +23 -0
  47. package/src/rules/common/no-invalid-parameter-examples.ts +36 -0
  48. package/src/rules/common/no-invalid-schema-examples.ts +27 -0
  49. package/src/rules/common/paths-kebab-case.ts +1 -1
  50. package/src/rules/common/registry-dependencies.ts +6 -8
  51. package/src/rules/oas2/index.ts +4 -0
  52. package/src/rules/oas3/index.ts +4 -0
  53. package/src/rules/oas3/no-invalid-media-type-examples.ts +16 -36
  54. package/src/rules/utils.ts +43 -2
  55. package/src/typings/openapi.ts +4 -0
  56. package/src/utils.ts +5 -1
  57. package/src/walk.ts +10 -0
  58. package/tsconfig.tsbuildinfo +1 -1
  59. package/lib/redocly/query.d.ts +0 -4
  60. package/lib/redocly/query.js +0 -44
  61. package/src/redocly/query.ts +0 -38
@@ -0,0 +1,110 @@
1
+ import fetch, { RequestInit, HeadersInit } from 'node-fetch';
2
+ import { RegistryApiTypes } from './registry-api-types';
3
+ import { AccessTokens, Region, DEFAULT_REGION, DOMAINS } from '../config/config';
4
+ import { isNotEmptyObject } from '../utils';
5
+ const version = require('../../package.json').version;
6
+
7
+ export class RegistryApi {
8
+ constructor(private accessTokens: AccessTokens, private region: Region) {}
9
+
10
+ get accessToken() {
11
+ return isNotEmptyObject(this.accessTokens) && this.accessTokens[this.region];
12
+ }
13
+
14
+ getBaseUrl(region: Region = DEFAULT_REGION) {
15
+ return `https://api.${DOMAINS[region]}/registry`;
16
+ }
17
+
18
+ setAccessTokens(accessTokens: AccessTokens) {
19
+ this.accessTokens = accessTokens;
20
+ return this;
21
+ }
22
+
23
+ private async request(path = '', options: RequestInit = {}, region?: Region) {
24
+ const headers = Object.assign({}, options.headers || {}, { 'x-redocly-cli-version': version });
25
+ if (!headers.hasOwnProperty('authorization')) { throw new Error('Unauthorized'); }
26
+ const response = await fetch(`${this.getBaseUrl(region)}${path}`, Object.assign({}, options, { headers }));
27
+ if (response.status === 401) { throw new Error('Unauthorized'); }
28
+ if (response.status === 404) {
29
+ const body: RegistryApiTypes.NotFoundProblemResponse = await response.json();
30
+ throw new Error(body.code);
31
+ }
32
+ return response;
33
+ }
34
+
35
+ async authStatus(accessToken: string, region: Region, verbose = false) {
36
+ try {
37
+ const response = await this.request('', { headers: { authorization: accessToken }}, region);
38
+ return response.ok;
39
+ } catch (error) {
40
+ if (verbose) {
41
+ console.log(error);
42
+ }
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async prepareFileUpload({
48
+ organizationId,
49
+ name,
50
+ version,
51
+ filesHash,
52
+ filename,
53
+ isUpsert,
54
+ }: RegistryApiTypes.PrepareFileuploadParams): Promise<RegistryApiTypes.PrepareFileuploadOKResponse> {
55
+ const response = await this.request(
56
+ `/${organizationId}/${name}/${version}/prepare-file-upload`,
57
+ {
58
+ method: 'POST',
59
+ headers: {
60
+ 'content-type': 'application/json',
61
+ authorization: this.accessToken,
62
+ } as HeadersInit,
63
+ body: JSON.stringify({
64
+ filesHash,
65
+ filename,
66
+ isUpsert,
67
+ }),
68
+ },
69
+ this.region
70
+ );
71
+
72
+ if (response.ok) {
73
+ return response.json();
74
+ }
75
+
76
+ throw new Error('Could not prepare file upload');
77
+ }
78
+
79
+ async pushApi({
80
+ organizationId,
81
+ name,
82
+ version,
83
+ rootFilePath,
84
+ filePaths,
85
+ branch,
86
+ isUpsert,
87
+ }: RegistryApiTypes.PushApiParams) {
88
+ const response = await this.request(`/${organizationId}/${name}/${version}`, {
89
+ method: 'PUT',
90
+ headers: {
91
+ 'content-type': 'application/json',
92
+ authorization: this.accessToken
93
+ } as HeadersInit,
94
+ body: JSON.stringify({
95
+ rootFilePath,
96
+ filePaths,
97
+ branch,
98
+ isUpsert,
99
+ }),
100
+ },
101
+ this.region
102
+ );
103
+
104
+ if (response.ok) {
105
+ return;
106
+ }
107
+
108
+ throw new Error('Could not push api');
109
+ }
110
+ }
@@ -83,4 +83,27 @@ describe('Oas3 paths-kebab-case', () => {
83
83
  ]
84
84
  `);
85
85
  });
86
+
87
+ it('should allow trailing slash in path with "paths-kebab-case" rule', async () => {
88
+ const document = parseYamlToDocument(
89
+ outdent`
90
+ openapi: 3.0.0
91
+ paths:
92
+ /some/:
93
+ get:
94
+ summary: List all pets
95
+ `,
96
+ 'foobar.yaml',
97
+ );
98
+
99
+ const results = await lintDocument({
100
+ externalRefResolver: new BaseResolver(),
101
+ document,
102
+ config: makeConfig({
103
+ 'paths-kebab-case': 'error',
104
+ 'no-path-trailing-slash': 'off',
105
+ }),
106
+ });
107
+ expect(replaceSourceWithRef(results)).toMatchInlineSnapshot(`Array []`);
108
+ });
86
109
  });
@@ -0,0 +1,36 @@
1
+ import { UserContext } from '../../walk';
2
+ import { Oas3Parameter } from '../../typings/openapi';
3
+ import { validateExample } from '../utils';
4
+
5
+ export const NoInvalidParameterExamples: any = (opts: any) => {
6
+ const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
7
+ return {
8
+ Parameter: {
9
+ leave(parameter: Oas3Parameter, ctx: UserContext) {
10
+ if (parameter.example) {
11
+ validateExample(
12
+ parameter.example,
13
+ parameter.schema!,
14
+ ctx.location.child('example'),
15
+ ctx,
16
+ disallowAdditionalProperties,
17
+ );
18
+ }
19
+
20
+ if (parameter.examples) {
21
+ for (const [key, example] of Object.entries(parameter.examples)) {
22
+ if ('value' in example) {
23
+ validateExample(
24
+ example.value,
25
+ parameter.schema!,
26
+ ctx.location.child(['examples', key]),
27
+ ctx,
28
+ false,
29
+ );
30
+ }
31
+ }
32
+ }
33
+ },
34
+ },
35
+ };
36
+ };
@@ -0,0 +1,27 @@
1
+ import { UserContext } from '../../walk';
2
+ import { Oas3_1Schema } from '../../typings/openapi';
3
+ import { validateExample } from '../utils';
4
+
5
+ export const NoInvalidSchemaExamples: any = (opts: any) => {
6
+ const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
7
+ return {
8
+ Schema: {
9
+ leave(schema: Oas3_1Schema, ctx: UserContext) {
10
+ if (schema.examples) {
11
+ for (const example of schema.examples) {
12
+ validateExample(
13
+ example,
14
+ schema,
15
+ ctx.location.child(['examples', schema.examples.indexOf(example)]),
16
+ ctx,
17
+ disallowAdditionalProperties,
18
+ );
19
+ }
20
+ }
21
+ if (schema.example) {
22
+ validateExample(schema.example, schema, ctx.location.child('example'), ctx, false);
23
+ }
24
+ },
25
+ },
26
+ };
27
+ };
@@ -4,7 +4,7 @@ import { UserContext } from '../../walk';
4
4
  export const PathsKebabCase: Oas3Rule | Oas2Rule = () => {
5
5
  return {
6
6
  PathItem(_path: object, { report, key }: UserContext) {
7
- const segments = (key as string).substr(1).split('/');
7
+ const segments = (key as string).substr(1).split('/').filter(s => s !== ''); // filter out empty segments
8
8
  if (!segments.every((segment) => /^{.+}$/.test(segment) || /^[a-z0-9-.]+$/.test(segment))) {
9
9
  report({
10
10
  message: `\`${key}\` does not use kebab-case.`,
@@ -1,24 +1,22 @@
1
- import { RedoclyClient } from '../../redocly';
1
+ import { UserContext } from '../../walk';
2
+ import { isRedoclyRegistryURL } from '../../redocly';
2
3
 
3
4
  import { Oas3Decorator, Oas2Decorator } from '../../visitors';
4
5
 
5
6
  export const RegistryDependencies: Oas3Decorator | Oas2Decorator = () => {
6
- let redoclyClient: RedoclyClient;
7
7
  let registryDependencies = new Set<string>();
8
8
 
9
9
  return {
10
10
  DefinitionRoot: {
11
- leave() {
12
- redoclyClient = new RedoclyClient();
13
- if (process.env.UPDATE_REGISTRY && redoclyClient.hasToken()) {
14
- redoclyClient.updateDependencies(Array.from(registryDependencies.keys()));
15
- }
11
+ leave(_: any, ctx: UserContext) {
12
+ const data = ctx.getVisitorData();
13
+ data.links = Array.from(registryDependencies);
16
14
  },
17
15
  },
18
16
  ref(node) {
19
17
  if (node.$ref) {
20
18
  const link = node.$ref.split('#/')[0];
21
- if (RedoclyClient.isRegistryURL(link)) {
19
+ if (isRedoclyRegistryURL(link)) {
22
20
  registryDependencies.add(link);
23
21
  }
24
22
  }
@@ -1,4 +1,6 @@
1
1
  import { OasSpec } from '../common/spec';
2
+ import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
3
+ import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
2
4
  import { InfoDescription } from '../common/info-description';
3
5
  import { InfoContact } from '../common/info-contact';
4
6
  import { InfoLicense } from '../common/info-license-url';
@@ -41,6 +43,8 @@ import { InfoDescriptionOverride } from '../common/info-description-override';
41
43
 
42
44
  export const rules = {
43
45
  spec: OasSpec as Oas2Rule,
46
+ 'no-invalid-schema-examples': NoInvalidSchemaExamples,
47
+ 'no-invalid-parameter-examples': NoInvalidParameterExamples,
44
48
  'info-description': InfoDescription as Oas2Rule,
45
49
  'info-contact': InfoContact as Oas2Rule,
46
50
  'info-license': InfoLicense as Oas2Rule,
@@ -47,6 +47,8 @@ import { OperationDescriptionOverride } from '../common/operation-description-ov
47
47
  import { TagDescriptionOverride } from '../common/tag-description-override';
48
48
  import { InfoDescriptionOverride } from '../common/info-description-override';
49
49
  import { PathExcludesPatterns } from '../common/path-excludes-patterns';
50
+ import { NoInvalidSchemaExamples } from '../common/no-invalid-schema-examples';
51
+ import { NoInvalidParameterExamples } from '../common/no-invalid-parameter-examples';
50
52
 
51
53
  export const rules = {
52
54
  spec: OasSpec,
@@ -93,6 +95,8 @@ export const rules = {
93
95
  'request-mime-type': RequestMimeType,
94
96
  'response-mime-type': ResponseMimeType,
95
97
  'path-segment-plural': PathSegmentPlural,
98
+ 'no-invalid-schema-examples': NoInvalidSchemaExamples,
99
+ 'no-invalid-parameter-examples': NoInvalidParameterExamples,
96
100
  } as Oas3RuleSet;
97
101
 
98
102
  export const preprocessors = {};
@@ -1,18 +1,25 @@
1
1
  import { Oas3Rule } from '../../visitors';
2
- import { validateJsonSchema } from '../ajv';
3
2
  import { Location, isRef } from '../../ref-utils';
4
3
  import { Oas3Example } from '../../typings/openapi';
4
+ import { validateExample } from '../utils';
5
+ import { UserContext } from '../../walk';
5
6
 
6
7
  export const ValidContentExamples: Oas3Rule = (opts) => {
7
8
  const disallowAdditionalProperties = opts.disallowAdditionalProperties ?? true;
8
9
 
9
10
  return {
10
11
  MediaType: {
11
- leave(mediaType, { report, location, resolve }) {
12
+ leave(mediaType, ctx: UserContext) {
13
+ const { location, resolve } = ctx;
12
14
  if (!mediaType.schema) return;
13
-
14
15
  if (mediaType.example) {
15
- validateExample(mediaType.example, location.child('example'));
16
+ validateExample(
17
+ mediaType.example,
18
+ mediaType.schema,
19
+ location.child('example'),
20
+ ctx,
21
+ disallowAdditionalProperties,
22
+ );
16
23
  } else if (mediaType.examples) {
17
24
  for (const exampleName of Object.keys(mediaType.examples)) {
18
25
  let example = mediaType.examples[exampleName];
@@ -23,40 +30,13 @@ export const ValidContentExamples: Oas3Rule = (opts) => {
23
30
  dataLoc = resolved.location.child('value');
24
31
  example = resolved.node;
25
32
  }
26
-
27
- validateExample(example.value, dataLoc);
28
- }
29
- }
30
-
31
- function validateExample(example: any, dataLoc: Location) {
32
- try {
33
- const { valid, errors } = validateJsonSchema(
34
- example,
35
- mediaType.schema!,
36
- location.child('schema'),
37
- dataLoc.pointer,
38
- resolve,
33
+ validateExample(
34
+ example.value,
35
+ mediaType.schema,
36
+ dataLoc,
37
+ ctx,
39
38
  disallowAdditionalProperties,
40
39
  );
41
- if (!valid) {
42
- for (let error of errors) {
43
- report({
44
- message: `Example value must conform to the schema: ${error.message}.`,
45
- location: {
46
- ...new Location(dataLoc.source, error.instancePath),
47
- reportOnKey: error.keyword === 'additionalProperties',
48
- },
49
- from: location,
50
- suggest: error.suggest,
51
- });
52
- }
53
- }
54
- } catch(e) {
55
- report({
56
- message: `Example validation errored: ${e.message}.`,
57
- location: location.child('schema'),
58
- from: location
59
- });
60
40
  }
61
41
  }
62
42
  },
@@ -1,5 +1,8 @@
1
1
  import levenshtein = require('js-levenshtein');
2
2
  import { UserContext } from '../walk';
3
+ import { Location } from '../ref-utils';
4
+ import { validateJsonSchema } from './ajv';
5
+ import { Oas3Schema, Referenced } from '../typings/openapi';
3
6
 
4
7
  export function oasTypeOf(value: unknown) {
5
8
  if (Array.isArray(value)) {
@@ -20,7 +23,7 @@ export function oasTypeOf(value: unknown) {
20
23
  */
21
24
  export function matchesJsonSchemaType(value: unknown, type: string, nullable: boolean): boolean {
22
25
  if (nullable && value === null) {
23
- return value === null
26
+ return value === null;
24
27
  }
25
28
 
26
29
  switch (type) {
@@ -79,4 +82,42 @@ export function getSuggest(given: string, variants: string[]): string[] {
79
82
 
80
83
  // if (bestMatch.distance <= 4) return bestMatch.string;
81
84
  return distances.map((d) => d.variant);
82
- }
85
+ }
86
+
87
+ export function validateExample(
88
+ example: any,
89
+ schema: Referenced<Oas3Schema>,
90
+ dataLoc: Location,
91
+ { resolve, location, report }: UserContext,
92
+ disallowAdditionalProperties: boolean,
93
+ ) {
94
+ try {
95
+ const { valid, errors } = validateJsonSchema(
96
+ example,
97
+ schema,
98
+ location.child('schema'),
99
+ dataLoc.pointer,
100
+ resolve,
101
+ disallowAdditionalProperties,
102
+ );
103
+ if (!valid) {
104
+ for (let error of errors) {
105
+ report({
106
+ message: `Example value must conform to the schema: ${error.message}.`,
107
+ location: {
108
+ ...new Location(dataLoc.source, error.instancePath),
109
+ reportOnKey: error.keyword === 'additionalProperties',
110
+ },
111
+ from: location,
112
+ suggest: error.suggest,
113
+ });
114
+ }
115
+ }
116
+ } catch (e) {
117
+ report({
118
+ message: `Example validation errored: ${e.message}.`,
119
+ location: location.child('schema'),
120
+ from: location,
121
+ });
122
+ }
123
+ }
@@ -150,6 +150,10 @@ export interface Oas3Schema {
150
150
  xml?: Oas3Xml;
151
151
  }
152
152
 
153
+ export interface Oas3_1Schema extends Oas3Schema {
154
+ examples?: any[];
155
+ }
156
+
153
157
  export interface Oas3Discriminator {
154
158
  propertyName: string;
155
159
  mapping?: { [name: string]: string };
package/src/utils.ts CHANGED
@@ -159,4 +159,8 @@ export function isPathParameter(pathSegment: string) {
159
159
  }
160
160
 
161
161
  return path.replace(/\\/g, '/');
162
- }
162
+ }
163
+
164
+ export function isNotEmptyObject(obj: any) {
165
+ return !!obj && Object.keys(obj).length > 0;
166
+ }
package/src/walk.ts CHANGED
@@ -33,6 +33,7 @@ export type UserContext = {
33
33
  key: string | number;
34
34
  parent: any;
35
35
  oasVersion: OasVersion;
36
+ getVisitorData: () => Record<string, unknown>;
36
37
  };
37
38
 
38
39
  export type Loc = {
@@ -77,6 +78,7 @@ export type NormalizedProblem = {
77
78
  export type WalkContext = {
78
79
  problems: NormalizedProblem[];
79
80
  oasVersion: OasVersion;
81
+ visitorsData: Record<string, Record<string, unknown>>; // custom data store that visitors can use for various purposes
80
82
  refTypes?: Map<string, NormalizedNodeType>;
81
83
  };
82
84
 
@@ -141,6 +143,7 @@ export function walkDocument<T>(opts: {
141
143
  key,
142
144
  parentLocations: {},
143
145
  oasVersion: ctx.oasVersion,
146
+ getVisitorData: getVisitorDataFn.bind(undefined, ruleId)
144
147
  },
145
148
  { node: resolvedNode, location: resolvedLocation, error },
146
149
  );
@@ -325,6 +328,7 @@ export function walkDocument<T>(opts: {
325
328
  key,
326
329
  parentLocations: {},
327
330
  oasVersion: ctx.oasVersion,
331
+ getVisitorData: getVisitorDataFn.bind(undefined, ruleId)
328
332
  },
329
333
  { node: resolvedNode, location: resolvedLocation, error },
330
334
  );
@@ -357,6 +361,7 @@ export function walkDocument<T>(opts: {
357
361
  ignoreNextVisitorsOnNode: () => {
358
362
  ignoreNextVisitorsOnNode = true;
359
363
  },
364
+ getVisitorData: getVisitorDataFn.bind(undefined, ruleId),
360
365
  },
361
366
  collectParents(context),
362
367
  context,
@@ -408,5 +413,10 @@ export function walkDocument<T>(opts: {
408
413
  }),
409
414
  });
410
415
  }
416
+
417
+ function getVisitorDataFn(ruleId: string) {
418
+ ctx.visitorsData[ruleId] = ctx.visitorsData[ruleId] || {};
419
+ return ctx.visitorsData[ruleId];
420
+ }
411
421
  }
412
422
  }