@apidiff/core 1.0.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.
- package/dist/ast/hash.d.ts +2 -0
- package/dist/ast/hash.d.ts.map +1 -0
- package/dist/ast/index.d.ts +3 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/traverse.d.ts +13 -0
- package/dist/ast/traverse.d.ts.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/diff/auth-differ.d.ts +4 -0
- package/dist/diff/auth-differ.d.ts.map +1 -0
- package/dist/diff/endpoint-differ.d.ts +3 -0
- package/dist/diff/endpoint-differ.d.ts.map +1 -0
- package/dist/diff/index.d.ts +3 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/schema-differ.d.ts +3 -0
- package/dist/diff/schema-differ.d.ts.map +1 -0
- package/dist/diff/server-differ.d.ts +3 -0
- package/dist/diff/server-differ.d.ts.map +1 -0
- package/dist/index.cjs +2902 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2855 -0
- package/dist/loader/file-loader.d.ts +5 -0
- package/dist/loader/file-loader.d.ts.map +1 -0
- package/dist/loader/git-loader.d.ts +5 -0
- package/dist/loader/git-loader.d.ts.map +1 -0
- package/dist/loader/index.d.ts +7 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/url-loader.d.ts +5 -0
- package/dist/loader/url-loader.d.ts.map +1 -0
- package/dist/output/html.d.ts +3 -0
- package/dist/output/html.d.ts.map +1 -0
- package/dist/output/index.d.ts +3 -0
- package/dist/output/index.d.ts.map +1 -0
- package/dist/output/json.d.ts +3 -0
- package/dist/output/json.d.ts.map +1 -0
- package/dist/output/markdown.d.ts +3 -0
- package/dist/output/markdown.d.ts.map +1 -0
- package/dist/output/terminal.d.ts +3 -0
- package/dist/output/terminal.d.ts.map +1 -0
- package/dist/parsers/base.d.ts +8 -0
- package/dist/parsers/base.d.ts.map +1 -0
- package/dist/parsers/graphql/index.d.ts +13 -0
- package/dist/parsers/graphql/index.d.ts.map +1 -0
- package/dist/parsers/index.d.ts +7 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/openapi2/index.d.ts +9 -0
- package/dist/parsers/openapi2/index.d.ts.map +1 -0
- package/dist/parsers/openapi3/index.d.ts +9 -0
- package/dist/parsers/openapi3/index.d.ts.map +1 -0
- package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
- package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
- package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
- package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
- package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
- package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
- package/dist/parsers/protobuf/index.d.ts +14 -0
- package/dist/parsers/protobuf/index.d.ts.map +1 -0
- package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
- package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
- package/dist/rules/auth/security-added.d.ts +9 -0
- package/dist/rules/auth/security-added.d.ts.map +1 -0
- package/dist/rules/auth/security-removed.d.ts +9 -0
- package/dist/rules/auth/security-removed.d.ts.map +1 -0
- package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
- package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
- package/dist/rules/base.d.ts +19 -0
- package/dist/rules/base.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
- package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
- package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
- package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
- package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
- package/dist/rules/endpoint/path-changed.d.ts +9 -0
- package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
- package/dist/rules/index.d.ts +4 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/meta/server-removed.d.ts +9 -0
- package/dist/rules/meta/server-removed.d.ts.map +1 -0
- package/dist/rules/param/param-added.d.ts +9 -0
- package/dist/rules/param/param-added.d.ts.map +1 -0
- package/dist/rules/param/param-deprecated.d.ts +9 -0
- package/dist/rules/param/param-deprecated.d.ts.map +1 -0
- package/dist/rules/param/param-enum-value-added.d.ts +9 -0
- package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
- package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
- package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
- package/dist/rules/param/param-location-changed.d.ts +9 -0
- package/dist/rules/param/param-location-changed.d.ts.map +1 -0
- package/dist/rules/param/param-removed.d.ts +9 -0
- package/dist/rules/param/param-removed.d.ts.map +1 -0
- package/dist/rules/param/param-required-added.d.ts +9 -0
- package/dist/rules/param/param-required-added.d.ts.map +1 -0
- package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
- package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
- package/dist/rules/param/param-type-changed.d.ts +9 -0
- package/dist/rules/param/param-type-changed.d.ts.map +1 -0
- package/dist/rules/request/request-body-added-required.d.ts +9 -0
- package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
- package/dist/rules/request/request-body-removed.d.ts +9 -0
- package/dist/rules/request/request-body-removed.d.ts.map +1 -0
- package/dist/rules/request/request-content-type-added.d.ts +9 -0
- package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
- package/dist/rules/request/request-content-type-removed.d.ts +9 -0
- package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
- package/dist/rules/request/request-field-added-required.d.ts +9 -0
- package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
- package/dist/rules/request/request-field-removed.d.ts +9 -0
- package/dist/rules/request/request-field-removed.d.ts.map +1 -0
- package/dist/rules/request/request-field-type-changed.d.ts +9 -0
- package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
- package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
- package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
- package/dist/rules/response/response-field-added.d.ts +9 -0
- package/dist/rules/response/response-field-added.d.ts.map +1 -0
- package/dist/rules/response/response-field-removed.d.ts +9 -0
- package/dist/rules/response/response-field-removed.d.ts.map +1 -0
- package/dist/rules/response/response-field-type-changed.d.ts +9 -0
- package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
- package/dist/rules/response/response-header-added-required.d.ts +9 -0
- package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
- package/dist/rules/response/response-header-removed.d.ts +9 -0
- package/dist/rules/response/response-header-removed.d.ts.map +1 -0
- package/dist/rules/response/response-media-type-added.d.ts +9 -0
- package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
- package/dist/rules/response/response-media-type-removed.d.ts +9 -0
- package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
- package/dist/rules/response/response-status-added.d.ts +9 -0
- package/dist/rules/response/response-status-added.d.ts.map +1 -0
- package/dist/rules/response/response-status-removed.d.ts +9 -0
- package/dist/rules/response/response-status-removed.d.ts.map +1 -0
- package/dist/types/ast.d.ts +140 -0
- package/dist/types/ast.d.ts.map +1 -0
- package/dist/types/config.d.ts +17 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/diff.d.ts +43 -0
- package/dist/types/diff.d.ts.map +1 -0
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/semantic.d.ts +45 -0
- package/dist/types/semantic.d.ts.map +1 -0
- package/package.json +31 -0
- package/src/ast/hash.ts +26 -0
- package/src/ast/index.ts +2 -0
- package/src/ast/traverse.ts +59 -0
- package/src/config/index.ts +44 -0
- package/src/diff/auth-differ.ts +72 -0
- package/src/diff/endpoint-differ.ts +144 -0
- package/src/diff/index.ts +13 -0
- package/src/diff/schema-differ.ts +70 -0
- package/src/diff/server-differ.ts +22 -0
- package/src/index.ts +92 -0
- package/src/loader/file-loader.ts +19 -0
- package/src/loader/git-loader.ts +22 -0
- package/src/loader/index.ts +32 -0
- package/src/loader/url-loader.ts +25 -0
- package/src/output/html.ts +177 -0
- package/src/output/index.ts +16 -0
- package/src/output/json.ts +5 -0
- package/src/output/markdown.ts +29 -0
- package/src/output/terminal.ts +37 -0
- package/src/parsers/base.ts +8 -0
- package/src/parsers/graphql/index.ts +181 -0
- package/src/parsers/index.ts +61 -0
- package/src/parsers/openapi2/index.ts +218 -0
- package/src/parsers/openapi3/index.ts +223 -0
- package/src/parsers/openapi3/ref-resolver.ts +101 -0
- package/src/parsers/openapi3/schema-normalizer.ts +52 -0
- package/src/parsers/openapi3/security-normalizer.ts +18 -0
- package/src/parsers/protobuf/index.ts +208 -0
- package/src/rules/auth/oauth-scope-removed.ts +34 -0
- package/src/rules/auth/security-added.ts +32 -0
- package/src/rules/auth/security-removed.ts +47 -0
- package/src/rules/auth/security-scheme-type-changed.ts +30 -0
- package/src/rules/base.ts +29 -0
- package/src/rules/endpoint/endpoint-added.ts +26 -0
- package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
- package/src/rules/endpoint/endpoint-removed.ts +26 -0
- package/src/rules/endpoint/http-method-changed.ts +29 -0
- package/src/rules/endpoint/path-changed.ts +29 -0
- package/src/rules/index.ts +83 -0
- package/src/rules/meta/server-removed.ts +26 -0
- package/src/rules/param/param-added.ts +52 -0
- package/src/rules/param/param-deprecated.ts +34 -0
- package/src/rules/param/param-enum-value-added.ts +32 -0
- package/src/rules/param/param-enum-value-removed.ts +32 -0
- package/src/rules/param/param-location-changed.ts +43 -0
- package/src/rules/param/param-removed.ts +52 -0
- package/src/rules/param/param-required-added.ts +34 -0
- package/src/rules/param/param-required-true-to-false.ts +34 -0
- package/src/rules/param/param-type-changed.ts +32 -0
- package/src/rules/request/request-body-added-required.ts +33 -0
- package/src/rules/request/request-body-removed.ts +30 -0
- package/src/rules/request/request-content-type-added.ts +31 -0
- package/src/rules/request/request-content-type-removed.ts +31 -0
- package/src/rules/request/request-field-added-required.ts +41 -0
- package/src/rules/request/request-field-removed.ts +35 -0
- package/src/rules/request/request-field-type-changed.ts +37 -0
- package/src/rules/request/request-required-false-to-true.ts +56 -0
- package/src/rules/response/response-field-added.ts +34 -0
- package/src/rules/response/response-field-removed.ts +34 -0
- package/src/rules/response/response-field-type-changed.ts +37 -0
- package/src/rules/response/response-header-added-required.ts +47 -0
- package/src/rules/response/response-header-removed.ts +32 -0
- package/src/rules/response/response-media-type-added.ts +32 -0
- package/src/rules/response/response-media-type-removed.ts +32 -0
- package/src/rules/response/response-status-added.ts +31 -0
- package/src/rules/response/response-status-removed.ts +31 -0
- package/src/types/ast.ts +164 -0
- package/src/types/config.ts +31 -0
- package/src/types/diff.ts +49 -0
- package/src/types/errors.ts +26 -0
- package/src/types/index.ts +5 -0
- package/src/types/semantic.ts +60 -0
- package/test/integration/stripe-v1.yaml +36 -0
- package/test/integration/stripe-v2.yaml +35 -0
- package/tests/ast.test.ts +60 -0
- package/tests/config.test.ts +43 -0
- package/tests/diff/auth-differ.test.ts +55 -0
- package/tests/diff/endpoint-differ.test.ts +42 -0
- package/tests/diff/schema-differ.test.ts +46 -0
- package/tests/diff/server-differ.test.ts +23 -0
- package/tests/diff.test.ts +116 -0
- package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
- package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
- package/tests/integration.test.ts +21 -0
- package/tests/loader-more.test.ts +75 -0
- package/tests/loader.test.ts +61 -0
- package/tests/output.test.ts +58 -0
- package/tests/parsers/openapi3-coverage.test.ts +77 -0
- package/tests/parsers/openapi3.test.ts +41 -0
- package/tests/parsers/parsers-other.test.ts +135 -0
- package/tests/parsers/ref-resolver.test.ts +78 -0
- package/tests/parsers/schema-normalizer.test.ts +30 -0
- package/tests/rules/auth-meta.test.ts +87 -0
- package/tests/rules/base.test.ts +44 -0
- package/tests/rules/endpoint.test.ts +122 -0
- package/tests/rules/param.test.ts +242 -0
- package/tests/rules/request.test.ts +147 -0
- package/tests/rules/response.test.ts +161 -0
- package/tests/rules/rules-coverage.test.ts +64 -0
- package/tests/types-errors.test.ts +22 -0
- package/tsconfig.json +8 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import protobuf from 'protobufjs';
|
|
2
|
+
import type { NormalizedAST, Endpoint, Parameter, RequestBody, ResponseDef, ComponentMap, Schema } from '../../types/index.js';
|
|
3
|
+
import type { RawSpec } from '../../loader/index.js';
|
|
4
|
+
import type { ISpecParser } from '../base.js';
|
|
5
|
+
import { ParseError } from '../../types/errors.js';
|
|
6
|
+
|
|
7
|
+
export class ProtobufParser implements ISpecParser {
|
|
8
|
+
readonly format = 'protobuf';
|
|
9
|
+
|
|
10
|
+
canParse(raw: RawSpec): boolean {
|
|
11
|
+
return raw.format === 'protobuf' || raw.content.trim().startsWith('syntax = "proto');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async parse(raw: RawSpec): Promise<NormalizedAST> {
|
|
15
|
+
let root: protobuf.Root;
|
|
16
|
+
try {
|
|
17
|
+
// Create a virtual file to parse the content string
|
|
18
|
+
const parsed = protobuf.parse(raw.content, { keepCase: true });
|
|
19
|
+
root = parsed.root;
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
throw new ParseError('Failed to parse Protobuf spec', err.line, err.column, raw.sourcePath, err);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const meta = {
|
|
25
|
+
title: 'Protobuf API',
|
|
26
|
+
version: '1.0.0', // Protobuf doesn't have a standard version field
|
|
27
|
+
format: 'protobuf' as const,
|
|
28
|
+
rawVersion: '3' // Assume proto3 for now
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const components: ComponentMap = {
|
|
32
|
+
schemas: {},
|
|
33
|
+
securitySchemes: {},
|
|
34
|
+
parameters: {},
|
|
35
|
+
responses: {},
|
|
36
|
+
headers: {},
|
|
37
|
+
requestBodies: {}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const endpoints: Endpoint[] = [];
|
|
41
|
+
|
|
42
|
+
// Recursively walk the AST
|
|
43
|
+
this.walkRoot(root, '', components, endpoints);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
meta,
|
|
47
|
+
servers: [],
|
|
48
|
+
endpoints,
|
|
49
|
+
components,
|
|
50
|
+
security: []
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private walkRoot(node: protobuf.ReflectionObject, namespace: string, components: ComponentMap, endpoints: Endpoint[]) {
|
|
55
|
+
if (node instanceof protobuf.Type) {
|
|
56
|
+
// It's a Message
|
|
57
|
+
components.schemas[`${namespace}${node.name}`] = this.messageToSchema(node);
|
|
58
|
+
} else if (node instanceof protobuf.Enum) {
|
|
59
|
+
// It's an Enum
|
|
60
|
+
components.schemas[`${namespace}${node.name}`] = this.enumToSchema(node);
|
|
61
|
+
} else if (node instanceof protobuf.Service) {
|
|
62
|
+
// It's a Service
|
|
63
|
+
for (const method of node.methodsArray) {
|
|
64
|
+
endpoints.push(this.methodToEndpoint(method, `${namespace}${node.name}`));
|
|
65
|
+
}
|
|
66
|
+
} else if (node instanceof protobuf.Namespace) {
|
|
67
|
+
// Walk namespace children
|
|
68
|
+
const newNamespace = node.name ? `${namespace}${node.name}.` : namespace;
|
|
69
|
+
for (const child of node.nestedArray) {
|
|
70
|
+
this.walkRoot(child, newNamespace, components, endpoints);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private messageToSchema(message: protobuf.Type): Schema {
|
|
76
|
+
const properties: Record<string, Schema> = {};
|
|
77
|
+
const required: string[] = [];
|
|
78
|
+
|
|
79
|
+
for (const field of message.fieldsArray) {
|
|
80
|
+
const fieldSchema = this.typeToSchema(field);
|
|
81
|
+
// We attach the protobuf field number to an internal property so we can diff it
|
|
82
|
+
(fieldSchema as any).$protobufNumber = field.id;
|
|
83
|
+
|
|
84
|
+
properties[field.name] = fieldSchema;
|
|
85
|
+
if (field.required) {
|
|
86
|
+
required.push(field.name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const schema: Schema = {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (required.length > 0) {
|
|
96
|
+
schema.required = required;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return schema;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private enumToSchema(enumObj: protobuf.Enum): Schema {
|
|
103
|
+
return {
|
|
104
|
+
type: 'string',
|
|
105
|
+
enum: Object.keys(enumObj.values)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private typeToSchema(field: protobuf.Field): Schema {
|
|
110
|
+
let schema: Schema = {};
|
|
111
|
+
|
|
112
|
+
switch (field.type) {
|
|
113
|
+
case 'double':
|
|
114
|
+
case 'float':
|
|
115
|
+
schema.type = 'number';
|
|
116
|
+
schema.format = field.type;
|
|
117
|
+
break;
|
|
118
|
+
case 'int32':
|
|
119
|
+
case 'uint32':
|
|
120
|
+
case 'sint32':
|
|
121
|
+
case 'fixed32':
|
|
122
|
+
case 'sfixed32':
|
|
123
|
+
schema.type = 'integer';
|
|
124
|
+
schema.format = field.type;
|
|
125
|
+
break;
|
|
126
|
+
case 'int64':
|
|
127
|
+
case 'uint64':
|
|
128
|
+
case 'sint64':
|
|
129
|
+
case 'fixed64':
|
|
130
|
+
case 'sfixed64':
|
|
131
|
+
schema.type = 'integer';
|
|
132
|
+
schema.format = 'int64'; // or field.type
|
|
133
|
+
break;
|
|
134
|
+
case 'bool':
|
|
135
|
+
schema.type = 'boolean';
|
|
136
|
+
break;
|
|
137
|
+
case 'string':
|
|
138
|
+
schema.type = 'string';
|
|
139
|
+
break;
|
|
140
|
+
case 'bytes':
|
|
141
|
+
schema.type = 'string';
|
|
142
|
+
schema.format = 'byte';
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
// It's a reference to another message or enum
|
|
146
|
+
// In a real implementation we'd use a $ref, but we are flattening
|
|
147
|
+
// For simplicity, we just mark it as object and we could potentially reference it.
|
|
148
|
+
// Or if we can find the resolved type in the parent namespace:
|
|
149
|
+
schema.type = 'object'; // It's a message type ref
|
|
150
|
+
(schema as any).$ref = `#/components/schemas/${field.type}`;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (field.repeated) {
|
|
155
|
+
return {
|
|
156
|
+
type: 'array',
|
|
157
|
+
items: schema
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return schema;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private methodToEndpoint(method: protobuf.Method, servicePath: string): Endpoint {
|
|
165
|
+
const id = `RPC:${servicePath}/${method.name}`;
|
|
166
|
+
const path = `${servicePath}/${method.name}`;
|
|
167
|
+
|
|
168
|
+
const requestBody: RequestBody = {
|
|
169
|
+
required: true,
|
|
170
|
+
content: {
|
|
171
|
+
'application/protobuf': {
|
|
172
|
+
schema: {
|
|
173
|
+
$ref: `#/components/schemas/${method.requestType}`
|
|
174
|
+
} as any
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const responses: ResponseDef[] = [
|
|
180
|
+
{
|
|
181
|
+
statusCode: '200',
|
|
182
|
+
description: 'Successful response',
|
|
183
|
+
content: {
|
|
184
|
+
'application/protobuf': {
|
|
185
|
+
schema: {
|
|
186
|
+
$ref: `#/components/schemas/${method.responseType}`
|
|
187
|
+
} as any
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
headers: {}
|
|
191
|
+
}
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
path,
|
|
197
|
+
method: 'RPC',
|
|
198
|
+
summary: method.comment || undefined,
|
|
199
|
+
tags: [servicePath],
|
|
200
|
+
deprecated: !!method.options?.deprecated,
|
|
201
|
+
security: [], // Protobuf doesn't specify security natively
|
|
202
|
+
parameters: [],
|
|
203
|
+
requestBody,
|
|
204
|
+
responses,
|
|
205
|
+
extensions: {}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class OauthScopeRemovedRule extends BaseRule {
|
|
5
|
+
id = 'OAUTH_SCOPE_REMOVED';
|
|
6
|
+
description = 'An OAuth scope was removed from a security requirement';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
|
|
12
|
+
for (const ed of diff.endpointDiffs) {
|
|
13
|
+
if (ed.type !== 'changed') continue;
|
|
14
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
15
|
+
|
|
16
|
+
for (const fc of ed.fieldChanges) {
|
|
17
|
+
if (fc.fieldPath[0] === 'security' && fc.fieldPath[2] === 'scopes' && fc.changeType === 'removed') {
|
|
18
|
+
const schemeId = fc.fieldPath[1];
|
|
19
|
+
const scope = fc.oldValue as string;
|
|
20
|
+
|
|
21
|
+
changes.push(this.makeChange({
|
|
22
|
+
severity: 'breaking',
|
|
23
|
+
category: 'authentication',
|
|
24
|
+
message: `OAuth scope '${scope}' was removed from security requirement '${schemeId}'.`,
|
|
25
|
+
consequence: `Tokens granted with only the '${scope}' scope may no longer be accepted.`,
|
|
26
|
+
migration: `Ensure clients request one of the remaining supported scopes.`,
|
|
27
|
+
location: { path: ed.path, method: ed.method, field: fc.fieldPath.join('.') }
|
|
28
|
+
}, context));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return changes;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class SecurityAddedRule extends BaseRule {
|
|
5
|
+
id = 'SECURITY_ADDED';
|
|
6
|
+
description = 'Authentication added to previously public endpoint';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'changed') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
const oldSec = ed.oldEndpoint?.security ?? [];
|
|
16
|
+
const newSec = ed.newEndpoint?.security ?? [];
|
|
17
|
+
|
|
18
|
+
if (oldSec.length === 0 && newSec.length > 0) {
|
|
19
|
+
const schemes = newSec.map(s => s.schemeId).join(', ');
|
|
20
|
+
changes.push(this.makeChange({
|
|
21
|
+
severity: 'breaking',
|
|
22
|
+
category: 'authentication',
|
|
23
|
+
message: `${ed.method} ${ed.path} now requires authentication (${schemes}).`,
|
|
24
|
+
consequence: 'Clients without credentials receive 401 Unauthorized.',
|
|
25
|
+
migration: `Add Authorization header with required credentials to all ${ed.method} ${ed.path} requests.`,
|
|
26
|
+
location: { path: ed.path, method: ed.method }
|
|
27
|
+
}, context));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return changes;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class SecurityRemovedRule extends BaseRule {
|
|
5
|
+
id = 'SECURITY_REMOVED';
|
|
6
|
+
description = 'A global security scheme was removed';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
|
|
12
|
+
// Check global security scheme definitions
|
|
13
|
+
for (const sd of diff.securityDiffs) {
|
|
14
|
+
if (sd.changeType === 'removed') {
|
|
15
|
+
changes.push(this.makeChange({
|
|
16
|
+
severity: 'breaking',
|
|
17
|
+
category: 'authentication',
|
|
18
|
+
message: `Security scheme '${sd.schemeId}' was removed.`,
|
|
19
|
+
consequence: `Clients using '${sd.schemeId}' for authentication will fail to authenticate.`,
|
|
20
|
+
migration: `Update clients to use a different, supported security scheme.`,
|
|
21
|
+
location: { field: `securitySchemes['${sd.schemeId}']` }
|
|
22
|
+
}, context));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check endpoint-level security requirements removed
|
|
27
|
+
for (const ed of diff.endpointDiffs) {
|
|
28
|
+
if (ed.type !== 'changed') continue;
|
|
29
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
30
|
+
|
|
31
|
+
for (const fc of ed.fieldChanges) {
|
|
32
|
+
if (fc.fieldPath[0] === 'security' && fc.fieldPath.length === 2 && fc.changeType === 'removed') {
|
|
33
|
+
const schemeId = fc.oldValue as string;
|
|
34
|
+
changes.push(this.makeChange({
|
|
35
|
+
severity: 'breaking',
|
|
36
|
+
category: 'authentication',
|
|
37
|
+
message: `Security requirement '${schemeId}' was removed from the endpoint.`,
|
|
38
|
+
consequence: `Clients trying to authenticate with '${schemeId}' might fail if the server no longer accepts it.`,
|
|
39
|
+
migration: `Update clients to use one of the remaining security requirements.`,
|
|
40
|
+
location: { path: ed.path, method: ed.method, field: `security['${schemeId}']` }
|
|
41
|
+
}, context));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return changes;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class SecuritySchemeTypeChangedRule extends BaseRule {
|
|
5
|
+
id = 'SECURITY_SCHEME_TYPE_CHANGED';
|
|
6
|
+
description = 'The type of a security scheme was changed';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
|
|
12
|
+
for (const sd of diff.securityDiffs) {
|
|
13
|
+
if (sd.changeType === 'changed') {
|
|
14
|
+
for (const fc of sd.fieldChanges) {
|
|
15
|
+
if (fc.fieldPath[0] === 'type') {
|
|
16
|
+
changes.push(this.makeChange({
|
|
17
|
+
severity: 'breaking',
|
|
18
|
+
category: 'authentication',
|
|
19
|
+
message: `Security scheme '${sd.schemeId}' changed type from '${fc.oldValue}' to '${fc.newValue}'.`,
|
|
20
|
+
consequence: `Clients using the old authentication type will fail to authenticate.`,
|
|
21
|
+
migration: `Update clients to authenticate using the new type '${fc.newValue}'.`,
|
|
22
|
+
location: { field: `securitySchemes['${sd.schemeId}'].type` }
|
|
23
|
+
}, context));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return changes;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DiffSet, RuleContext, SemanticChange, IRule, Severity, RuleCategory } from '../types/index.js';
|
|
2
|
+
|
|
3
|
+
export abstract class BaseRule implements IRule {
|
|
4
|
+
abstract id: string;
|
|
5
|
+
abstract description: string;
|
|
6
|
+
abstract severity: Severity;
|
|
7
|
+
|
|
8
|
+
abstract apply(diff: DiffSet, context: RuleContext): SemanticChange[];
|
|
9
|
+
|
|
10
|
+
protected isIgnored(path: string, context: RuleContext): boolean {
|
|
11
|
+
for (const glob of context.config.ignorePaths) {
|
|
12
|
+
if (this.matchGlob(path, glob)) return true;
|
|
13
|
+
}
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private matchGlob(path: string, glob: string): boolean {
|
|
18
|
+
const regex = glob.replace(/\*/g, '.*').replace(/\//g, '\\/');
|
|
19
|
+
return new RegExp(`^${regex}$`).test(path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected makeChange(details: { severity: Severity; category: RuleCategory; message: string; consequence: string; migration: string; location: SemanticChange['location']; ruleId?: string }, context: RuleContext): SemanticChange {
|
|
23
|
+
const { ruleId, ...rest } = details;
|
|
24
|
+
return {
|
|
25
|
+
ruleId: ruleId || this.id,
|
|
26
|
+
...rest
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class EndpointAddedRule extends BaseRule {
|
|
5
|
+
id = 'ENDPOINT_ADDED';
|
|
6
|
+
description = 'A new endpoint was added';
|
|
7
|
+
severity = 'info' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'added') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
changes.push(this.makeChange({
|
|
16
|
+
severity: 'info',
|
|
17
|
+
category: 'endpoint',
|
|
18
|
+
message: `Endpoint ${ed.method} ${ed.path} was added.`,
|
|
19
|
+
consequence: 'Clients can now use this new endpoint.',
|
|
20
|
+
migration: 'No migration required.',
|
|
21
|
+
location: { path: ed.path, method: ed.method }
|
|
22
|
+
}, context));
|
|
23
|
+
}
|
|
24
|
+
return changes;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class EndpointDeprecatedRule extends BaseRule {
|
|
5
|
+
id = 'ENDPOINT_DEPRECATED';
|
|
6
|
+
description = 'An existing endpoint was marked as deprecated';
|
|
7
|
+
severity = 'warning' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'changed') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
const deprecatedChange = ed.fieldChanges.find(c => c.fieldPath[0] === 'deprecated' && c.newValue === true && c.oldValue === false);
|
|
16
|
+
if (deprecatedChange) {
|
|
17
|
+
changes.push(this.makeChange({
|
|
18
|
+
severity: 'warning',
|
|
19
|
+
category: 'endpoint',
|
|
20
|
+
message: `Endpoint ${ed.method} ${ed.path} was deprecated.`,
|
|
21
|
+
consequence: 'This endpoint is slated for future removal.',
|
|
22
|
+
migration: 'Plan to migrate away from this endpoint to its recommended alternative.',
|
|
23
|
+
location: { path: ed.path, method: ed.method }
|
|
24
|
+
}, context));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return changes;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class EndpointRemovedRule extends BaseRule {
|
|
5
|
+
id = 'ENDPOINT_REMOVED';
|
|
6
|
+
description = 'An existing endpoint was removed entirely';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'removed') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
changes.push(this.makeChange({
|
|
16
|
+
severity: 'breaking',
|
|
17
|
+
category: 'endpoint',
|
|
18
|
+
message: `Endpoint ${ed.method} ${ed.path} was removed.`,
|
|
19
|
+
consequence: 'Clients calling this endpoint will receive a 404 Not Found error.',
|
|
20
|
+
migration: 'Migrate clients to use an alternative endpoint before removing this one.',
|
|
21
|
+
location: { path: ed.path, method: ed.method }
|
|
22
|
+
}, context));
|
|
23
|
+
}
|
|
24
|
+
return changes;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class HttpMethodChangedRule extends BaseRule {
|
|
5
|
+
id = 'HTTP_METHOD_CHANGED';
|
|
6
|
+
description = 'The HTTP method for an endpoint changed';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'changed') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
const methodChange = ed.fieldChanges.find(c => c.fieldPath[0] === 'method');
|
|
16
|
+
if (methodChange) {
|
|
17
|
+
changes.push(this.makeChange({
|
|
18
|
+
severity: 'breaking',
|
|
19
|
+
category: 'endpoint',
|
|
20
|
+
message: `Endpoint HTTP method changed from ${methodChange.oldValue} to ${methodChange.newValue}.`,
|
|
21
|
+
consequence: 'Clients using the old HTTP method will fail.',
|
|
22
|
+
migration: `Update client requests to use the ${methodChange.newValue} method.`,
|
|
23
|
+
location: { path: ed.path, method: ed.method }
|
|
24
|
+
}, context));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return changes;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class PathChangedRule extends BaseRule {
|
|
5
|
+
id = 'PATH_CHANGED';
|
|
6
|
+
description = 'The path string for an endpoint changed (e.g. parameter renamed)';
|
|
7
|
+
severity = 'info' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
for (const ed of diff.endpointDiffs) {
|
|
12
|
+
if (ed.type !== 'changed') continue;
|
|
13
|
+
if (this.isIgnored(ed.path, context)) continue;
|
|
14
|
+
|
|
15
|
+
const pathChange = ed.fieldChanges.find(c => c.fieldPath[0] === 'path');
|
|
16
|
+
if (pathChange) {
|
|
17
|
+
changes.push(this.makeChange({
|
|
18
|
+
severity: 'info',
|
|
19
|
+
category: 'endpoint',
|
|
20
|
+
message: `Endpoint path changed from ${pathChange.oldValue} to ${pathChange.newValue}.`,
|
|
21
|
+
consequence: 'Clients may need to update routing or parameter names.',
|
|
22
|
+
migration: 'Review the path syntax changes.',
|
|
23
|
+
location: { path: ed.path, method: ed.method }
|
|
24
|
+
}, context));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return changes;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IRule, DiffSet, RuleContext, SemanticChange } from '../types/index.js';
|
|
2
|
+
import { SecurityAddedRule } from './auth/security-added.js';
|
|
3
|
+
import { EndpointRemovedRule } from './endpoint/endpoint-removed.js';
|
|
4
|
+
import { EndpointAddedRule } from './endpoint/endpoint-added.js';
|
|
5
|
+
import { EndpointDeprecatedRule } from './endpoint/endpoint-deprecated.js';
|
|
6
|
+
import { HttpMethodChangedRule } from './endpoint/http-method-changed.js';
|
|
7
|
+
import { PathChangedRule } from './endpoint/path-changed.js';
|
|
8
|
+
import { ResponseFieldRemovedRule } from './response/response-field-removed.js';
|
|
9
|
+
import { ParamRequiredAddedRule } from './param/param-required-added.js';
|
|
10
|
+
import { ParamRemovedRule } from './param/param-removed.js';
|
|
11
|
+
import { ParamAddedRule } from './param/param-added.js';
|
|
12
|
+
import { ParamDeprecatedRule } from './param/param-deprecated.js';
|
|
13
|
+
import { ParamTypeChangedRule } from './param/param-type-changed.js';
|
|
14
|
+
import { ParamLocationChangedRule } from './param/param-location-changed.js';
|
|
15
|
+
import { ParamEnumValueRemovedRule } from './param/param-enum-value-removed.js';
|
|
16
|
+
import { ParamEnumValueAddedRule } from './param/param-enum-value-added.js';
|
|
17
|
+
import { ParamRequiredTrueToFalseRule } from './param/param-required-true-to-false.js';
|
|
18
|
+
import { RequestBodyAddedRequiredRule } from './request/request-body-added-required.js';
|
|
19
|
+
import { RequestBodyRemovedRule } from './request/request-body-removed.js';
|
|
20
|
+
import { RequestContentTypeAddedRule } from './request/request-content-type-added.js';
|
|
21
|
+
import { RequestContentTypeRemovedRule } from './request/request-content-type-removed.js';
|
|
22
|
+
import { RequestFieldRemovedRule } from './request/request-field-removed.js';
|
|
23
|
+
import { RequestFieldAddedRequiredRule } from './request/request-field-added-required.js';
|
|
24
|
+
import { RequestFieldTypeChangedRule } from './request/request-field-type-changed.js';
|
|
25
|
+
import { RequestRequiredFalseToTrueRule } from './request/request-required-false-to-true.js';
|
|
26
|
+
import { ResponseFieldAddedRule } from './response/response-field-added.js';
|
|
27
|
+
import { ResponseFieldTypeChangedRule } from './response/response-field-type-changed.js';
|
|
28
|
+
import { ResponseStatusRemovedRule } from './response/response-status-removed.js';
|
|
29
|
+
import { ResponseStatusAddedRule } from './response/response-status-added.js';
|
|
30
|
+
import { ResponseMediaTypeRemovedRule } from './response/response-media-type-removed.js';
|
|
31
|
+
import { ResponseMediaTypeAddedRule } from './response/response-media-type-added.js';
|
|
32
|
+
import { ResponseHeaderRemovedRule } from './response/response-header-removed.js';
|
|
33
|
+
import { ResponseHeaderAddedRequiredRule } from './response/response-header-added-required.js';
|
|
34
|
+
import { SecurityRemovedRule } from './auth/security-removed.js';
|
|
35
|
+
import { SecuritySchemeTypeChangedRule } from './auth/security-scheme-type-changed.js';
|
|
36
|
+
import { OauthScopeRemovedRule } from './auth/oauth-scope-removed.js';
|
|
37
|
+
import { ServerRemovedRule } from './meta/server-removed.js';
|
|
38
|
+
|
|
39
|
+
export const BUILT_IN_RULES: IRule[] = [
|
|
40
|
+
new SecurityAddedRule(),
|
|
41
|
+
new EndpointRemovedRule(),
|
|
42
|
+
new EndpointAddedRule(),
|
|
43
|
+
new EndpointDeprecatedRule(),
|
|
44
|
+
new HttpMethodChangedRule(),
|
|
45
|
+
new PathChangedRule(),
|
|
46
|
+
new ResponseFieldRemovedRule(),
|
|
47
|
+
new ParamRequiredAddedRule(),
|
|
48
|
+
new ParamRemovedRule(),
|
|
49
|
+
new ParamAddedRule(),
|
|
50
|
+
new ParamDeprecatedRule(),
|
|
51
|
+
new ParamTypeChangedRule(),
|
|
52
|
+
new ParamLocationChangedRule(),
|
|
53
|
+
new ParamEnumValueRemovedRule(),
|
|
54
|
+
new ParamEnumValueAddedRule(),
|
|
55
|
+
new ParamRequiredTrueToFalseRule(),
|
|
56
|
+
new RequestBodyAddedRequiredRule(),
|
|
57
|
+
new RequestBodyRemovedRule(),
|
|
58
|
+
new RequestContentTypeAddedRule(),
|
|
59
|
+
new RequestContentTypeRemovedRule(),
|
|
60
|
+
new RequestFieldRemovedRule(),
|
|
61
|
+
new RequestFieldAddedRequiredRule(),
|
|
62
|
+
new RequestFieldTypeChangedRule(),
|
|
63
|
+
new RequestRequiredFalseToTrueRule(),
|
|
64
|
+
new ResponseFieldAddedRule(),
|
|
65
|
+
new ResponseFieldTypeChangedRule(),
|
|
66
|
+
new ResponseStatusRemovedRule(),
|
|
67
|
+
new ResponseStatusAddedRule(),
|
|
68
|
+
new ResponseMediaTypeRemovedRule(),
|
|
69
|
+
new ResponseMediaTypeAddedRule(),
|
|
70
|
+
new ResponseHeaderRemovedRule(),
|
|
71
|
+
new ResponseHeaderAddedRequiredRule(),
|
|
72
|
+
new SecurityRemovedRule(),
|
|
73
|
+
new SecuritySchemeTypeChangedRule(),
|
|
74
|
+
new OauthScopeRemovedRule(),
|
|
75
|
+
new ServerRemovedRule()
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
export function runRules(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
79
|
+
const enabledRules = BUILT_IN_RULES.filter(
|
|
80
|
+
r => !context.config.disabledRules.includes(r.id)
|
|
81
|
+
);
|
|
82
|
+
return enabledRules.flatMap(r => r.apply(diff, context));
|
|
83
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BaseRule } from '../base.js';
|
|
2
|
+
import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
|
|
3
|
+
|
|
4
|
+
export class ServerRemovedRule extends BaseRule {
|
|
5
|
+
id = 'SERVER_REMOVED';
|
|
6
|
+
description = 'A server was removed from the API';
|
|
7
|
+
severity = 'breaking' as const;
|
|
8
|
+
|
|
9
|
+
apply(diff: DiffSet, context: RuleContext): SemanticChange[] {
|
|
10
|
+
const changes: SemanticChange[] = [];
|
|
11
|
+
|
|
12
|
+
for (const sd of diff.serverDiffs) {
|
|
13
|
+
if (sd.changeType === 'removed' && sd.oldServer) {
|
|
14
|
+
changes.push(this.makeChange({
|
|
15
|
+
severity: 'breaking',
|
|
16
|
+
category: 'server',
|
|
17
|
+
message: `Server URL '${sd.oldServer.url}' was removed.`,
|
|
18
|
+
consequence: `Clients routing traffic to '${sd.oldServer.url}' will eventually fail if the server is decommissioned.`,
|
|
19
|
+
migration: `Update clients to use one of the remaining server URLs.`,
|
|
20
|
+
location: { field: `servers` }
|
|
21
|
+
}, context));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return changes;
|
|
25
|
+
}
|
|
26
|
+
}
|