@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.
Files changed (249) hide show
  1. package/dist/ast/hash.d.ts +2 -0
  2. package/dist/ast/hash.d.ts.map +1 -0
  3. package/dist/ast/index.d.ts +3 -0
  4. package/dist/ast/index.d.ts.map +1 -0
  5. package/dist/ast/traverse.d.ts +13 -0
  6. package/dist/ast/traverse.d.ts.map +1 -0
  7. package/dist/config/index.d.ts +9 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/diff/auth-differ.d.ts +4 -0
  10. package/dist/diff/auth-differ.d.ts.map +1 -0
  11. package/dist/diff/endpoint-differ.d.ts +3 -0
  12. package/dist/diff/endpoint-differ.d.ts.map +1 -0
  13. package/dist/diff/index.d.ts +3 -0
  14. package/dist/diff/index.d.ts.map +1 -0
  15. package/dist/diff/schema-differ.d.ts +3 -0
  16. package/dist/diff/schema-differ.d.ts.map +1 -0
  17. package/dist/diff/server-differ.d.ts +3 -0
  18. package/dist/diff/server-differ.d.ts.map +1 -0
  19. package/dist/index.cjs +2902 -0
  20. package/dist/index.d.ts +15 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2855 -0
  23. package/dist/loader/file-loader.d.ts +5 -0
  24. package/dist/loader/file-loader.d.ts.map +1 -0
  25. package/dist/loader/git-loader.d.ts +5 -0
  26. package/dist/loader/git-loader.d.ts.map +1 -0
  27. package/dist/loader/index.d.ts +7 -0
  28. package/dist/loader/index.d.ts.map +1 -0
  29. package/dist/loader/url-loader.d.ts +5 -0
  30. package/dist/loader/url-loader.d.ts.map +1 -0
  31. package/dist/output/html.d.ts +3 -0
  32. package/dist/output/html.d.ts.map +1 -0
  33. package/dist/output/index.d.ts +3 -0
  34. package/dist/output/index.d.ts.map +1 -0
  35. package/dist/output/json.d.ts +3 -0
  36. package/dist/output/json.d.ts.map +1 -0
  37. package/dist/output/markdown.d.ts +3 -0
  38. package/dist/output/markdown.d.ts.map +1 -0
  39. package/dist/output/terminal.d.ts +3 -0
  40. package/dist/output/terminal.d.ts.map +1 -0
  41. package/dist/parsers/base.d.ts +8 -0
  42. package/dist/parsers/base.d.ts.map +1 -0
  43. package/dist/parsers/graphql/index.d.ts +13 -0
  44. package/dist/parsers/graphql/index.d.ts.map +1 -0
  45. package/dist/parsers/index.d.ts +7 -0
  46. package/dist/parsers/index.d.ts.map +1 -0
  47. package/dist/parsers/openapi2/index.d.ts +9 -0
  48. package/dist/parsers/openapi2/index.d.ts.map +1 -0
  49. package/dist/parsers/openapi3/index.d.ts +9 -0
  50. package/dist/parsers/openapi3/index.d.ts.map +1 -0
  51. package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
  52. package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
  53. package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
  54. package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
  55. package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
  56. package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
  57. package/dist/parsers/protobuf/index.d.ts +14 -0
  58. package/dist/parsers/protobuf/index.d.ts.map +1 -0
  59. package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
  60. package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
  61. package/dist/rules/auth/security-added.d.ts +9 -0
  62. package/dist/rules/auth/security-added.d.ts.map +1 -0
  63. package/dist/rules/auth/security-removed.d.ts +9 -0
  64. package/dist/rules/auth/security-removed.d.ts.map +1 -0
  65. package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
  66. package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
  67. package/dist/rules/base.d.ts +19 -0
  68. package/dist/rules/base.d.ts.map +1 -0
  69. package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
  70. package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
  71. package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
  72. package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
  73. package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
  74. package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
  75. package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
  76. package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
  77. package/dist/rules/endpoint/path-changed.d.ts +9 -0
  78. package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
  79. package/dist/rules/index.d.ts +4 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/meta/server-removed.d.ts +9 -0
  82. package/dist/rules/meta/server-removed.d.ts.map +1 -0
  83. package/dist/rules/param/param-added.d.ts +9 -0
  84. package/dist/rules/param/param-added.d.ts.map +1 -0
  85. package/dist/rules/param/param-deprecated.d.ts +9 -0
  86. package/dist/rules/param/param-deprecated.d.ts.map +1 -0
  87. package/dist/rules/param/param-enum-value-added.d.ts +9 -0
  88. package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
  89. package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
  90. package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
  91. package/dist/rules/param/param-location-changed.d.ts +9 -0
  92. package/dist/rules/param/param-location-changed.d.ts.map +1 -0
  93. package/dist/rules/param/param-removed.d.ts +9 -0
  94. package/dist/rules/param/param-removed.d.ts.map +1 -0
  95. package/dist/rules/param/param-required-added.d.ts +9 -0
  96. package/dist/rules/param/param-required-added.d.ts.map +1 -0
  97. package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
  98. package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
  99. package/dist/rules/param/param-type-changed.d.ts +9 -0
  100. package/dist/rules/param/param-type-changed.d.ts.map +1 -0
  101. package/dist/rules/request/request-body-added-required.d.ts +9 -0
  102. package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
  103. package/dist/rules/request/request-body-removed.d.ts +9 -0
  104. package/dist/rules/request/request-body-removed.d.ts.map +1 -0
  105. package/dist/rules/request/request-content-type-added.d.ts +9 -0
  106. package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
  107. package/dist/rules/request/request-content-type-removed.d.ts +9 -0
  108. package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
  109. package/dist/rules/request/request-field-added-required.d.ts +9 -0
  110. package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
  111. package/dist/rules/request/request-field-removed.d.ts +9 -0
  112. package/dist/rules/request/request-field-removed.d.ts.map +1 -0
  113. package/dist/rules/request/request-field-type-changed.d.ts +9 -0
  114. package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
  115. package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
  116. package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
  117. package/dist/rules/response/response-field-added.d.ts +9 -0
  118. package/dist/rules/response/response-field-added.d.ts.map +1 -0
  119. package/dist/rules/response/response-field-removed.d.ts +9 -0
  120. package/dist/rules/response/response-field-removed.d.ts.map +1 -0
  121. package/dist/rules/response/response-field-type-changed.d.ts +9 -0
  122. package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
  123. package/dist/rules/response/response-header-added-required.d.ts +9 -0
  124. package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
  125. package/dist/rules/response/response-header-removed.d.ts +9 -0
  126. package/dist/rules/response/response-header-removed.d.ts.map +1 -0
  127. package/dist/rules/response/response-media-type-added.d.ts +9 -0
  128. package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
  129. package/dist/rules/response/response-media-type-removed.d.ts +9 -0
  130. package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
  131. package/dist/rules/response/response-status-added.d.ts +9 -0
  132. package/dist/rules/response/response-status-added.d.ts.map +1 -0
  133. package/dist/rules/response/response-status-removed.d.ts +9 -0
  134. package/dist/rules/response/response-status-removed.d.ts.map +1 -0
  135. package/dist/types/ast.d.ts +140 -0
  136. package/dist/types/ast.d.ts.map +1 -0
  137. package/dist/types/config.d.ts +17 -0
  138. package/dist/types/config.d.ts.map +1 -0
  139. package/dist/types/diff.d.ts +43 -0
  140. package/dist/types/diff.d.ts.map +1 -0
  141. package/dist/types/errors.d.ts +21 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +6 -0
  144. package/dist/types/index.d.ts.map +1 -0
  145. package/dist/types/semantic.d.ts +45 -0
  146. package/dist/types/semantic.d.ts.map +1 -0
  147. package/package.json +31 -0
  148. package/src/ast/hash.ts +26 -0
  149. package/src/ast/index.ts +2 -0
  150. package/src/ast/traverse.ts +59 -0
  151. package/src/config/index.ts +44 -0
  152. package/src/diff/auth-differ.ts +72 -0
  153. package/src/diff/endpoint-differ.ts +144 -0
  154. package/src/diff/index.ts +13 -0
  155. package/src/diff/schema-differ.ts +70 -0
  156. package/src/diff/server-differ.ts +22 -0
  157. package/src/index.ts +92 -0
  158. package/src/loader/file-loader.ts +19 -0
  159. package/src/loader/git-loader.ts +22 -0
  160. package/src/loader/index.ts +32 -0
  161. package/src/loader/url-loader.ts +25 -0
  162. package/src/output/html.ts +177 -0
  163. package/src/output/index.ts +16 -0
  164. package/src/output/json.ts +5 -0
  165. package/src/output/markdown.ts +29 -0
  166. package/src/output/terminal.ts +37 -0
  167. package/src/parsers/base.ts +8 -0
  168. package/src/parsers/graphql/index.ts +181 -0
  169. package/src/parsers/index.ts +61 -0
  170. package/src/parsers/openapi2/index.ts +218 -0
  171. package/src/parsers/openapi3/index.ts +223 -0
  172. package/src/parsers/openapi3/ref-resolver.ts +101 -0
  173. package/src/parsers/openapi3/schema-normalizer.ts +52 -0
  174. package/src/parsers/openapi3/security-normalizer.ts +18 -0
  175. package/src/parsers/protobuf/index.ts +208 -0
  176. package/src/rules/auth/oauth-scope-removed.ts +34 -0
  177. package/src/rules/auth/security-added.ts +32 -0
  178. package/src/rules/auth/security-removed.ts +47 -0
  179. package/src/rules/auth/security-scheme-type-changed.ts +30 -0
  180. package/src/rules/base.ts +29 -0
  181. package/src/rules/endpoint/endpoint-added.ts +26 -0
  182. package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
  183. package/src/rules/endpoint/endpoint-removed.ts +26 -0
  184. package/src/rules/endpoint/http-method-changed.ts +29 -0
  185. package/src/rules/endpoint/path-changed.ts +29 -0
  186. package/src/rules/index.ts +83 -0
  187. package/src/rules/meta/server-removed.ts +26 -0
  188. package/src/rules/param/param-added.ts +52 -0
  189. package/src/rules/param/param-deprecated.ts +34 -0
  190. package/src/rules/param/param-enum-value-added.ts +32 -0
  191. package/src/rules/param/param-enum-value-removed.ts +32 -0
  192. package/src/rules/param/param-location-changed.ts +43 -0
  193. package/src/rules/param/param-removed.ts +52 -0
  194. package/src/rules/param/param-required-added.ts +34 -0
  195. package/src/rules/param/param-required-true-to-false.ts +34 -0
  196. package/src/rules/param/param-type-changed.ts +32 -0
  197. package/src/rules/request/request-body-added-required.ts +33 -0
  198. package/src/rules/request/request-body-removed.ts +30 -0
  199. package/src/rules/request/request-content-type-added.ts +31 -0
  200. package/src/rules/request/request-content-type-removed.ts +31 -0
  201. package/src/rules/request/request-field-added-required.ts +41 -0
  202. package/src/rules/request/request-field-removed.ts +35 -0
  203. package/src/rules/request/request-field-type-changed.ts +37 -0
  204. package/src/rules/request/request-required-false-to-true.ts +56 -0
  205. package/src/rules/response/response-field-added.ts +34 -0
  206. package/src/rules/response/response-field-removed.ts +34 -0
  207. package/src/rules/response/response-field-type-changed.ts +37 -0
  208. package/src/rules/response/response-header-added-required.ts +47 -0
  209. package/src/rules/response/response-header-removed.ts +32 -0
  210. package/src/rules/response/response-media-type-added.ts +32 -0
  211. package/src/rules/response/response-media-type-removed.ts +32 -0
  212. package/src/rules/response/response-status-added.ts +31 -0
  213. package/src/rules/response/response-status-removed.ts +31 -0
  214. package/src/types/ast.ts +164 -0
  215. package/src/types/config.ts +31 -0
  216. package/src/types/diff.ts +49 -0
  217. package/src/types/errors.ts +26 -0
  218. package/src/types/index.ts +5 -0
  219. package/src/types/semantic.ts +60 -0
  220. package/test/integration/stripe-v1.yaml +36 -0
  221. package/test/integration/stripe-v2.yaml +35 -0
  222. package/tests/ast.test.ts +60 -0
  223. package/tests/config.test.ts +43 -0
  224. package/tests/diff/auth-differ.test.ts +55 -0
  225. package/tests/diff/endpoint-differ.test.ts +42 -0
  226. package/tests/diff/schema-differ.test.ts +46 -0
  227. package/tests/diff/server-differ.test.ts +23 -0
  228. package/tests/diff.test.ts +116 -0
  229. package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
  230. package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
  231. package/tests/integration.test.ts +21 -0
  232. package/tests/loader-more.test.ts +75 -0
  233. package/tests/loader.test.ts +61 -0
  234. package/tests/output.test.ts +58 -0
  235. package/tests/parsers/openapi3-coverage.test.ts +77 -0
  236. package/tests/parsers/openapi3.test.ts +41 -0
  237. package/tests/parsers/parsers-other.test.ts +135 -0
  238. package/tests/parsers/ref-resolver.test.ts +78 -0
  239. package/tests/parsers/schema-normalizer.test.ts +30 -0
  240. package/tests/rules/auth-meta.test.ts +87 -0
  241. package/tests/rules/base.test.ts +44 -0
  242. package/tests/rules/endpoint.test.ts +122 -0
  243. package/tests/rules/param.test.ts +242 -0
  244. package/tests/rules/request.test.ts +147 -0
  245. package/tests/rules/response.test.ts +161 -0
  246. package/tests/rules/rules-coverage.test.ts +64 -0
  247. package/tests/types-errors.test.ts +22 -0
  248. package/tsconfig.json +8 -0
  249. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,52 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange, Parameter } from '../../types/index.js';
3
+
4
+ export class ParamAddedRule extends BaseRule {
5
+ id = 'PARAM_ADDED';
6
+ description = 'A new parameter 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 !== 'changed') continue;
13
+ if (this.isIgnored(ed.path, context)) continue;
14
+
15
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath.length === 2 && fc.changeType === 'added') {
17
+ const paramId = fc.fieldPath[1];
18
+ const [paramName, inLoc] = paramId.split(':');
19
+
20
+ // Check if it's a location change
21
+ const isMoved = ed.fieldChanges.some(c => c.fieldPath[0] === 'parameters' && c.fieldPath.length === 2 && c.changeType === 'removed' && c.fieldPath[1].startsWith(`${paramName}:`));
22
+ if (isMoved) continue;
23
+
24
+ const newParam = fc.newValue as Parameter;
25
+
26
+ if (newParam.required) {
27
+ changes.push(this.makeChange({
28
+ severity: 'breaking',
29
+ ruleId: 'PARAM_ADDED_REQUIRED',
30
+ category: 'parameter',
31
+ message: `Required parameter '${paramName}' in ${inLoc} was added.`,
32
+ consequence: 'Existing clients failing to send this new required parameter will receive 400 Bad Request errors.',
33
+ migration: `Update clients to include the '${paramName}' parameter.`,
34
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
35
+ }, context));
36
+ } else {
37
+ changes.push(this.makeChange({
38
+ severity: 'info',
39
+ ruleId: 'PARAM_ADDED',
40
+ category: 'parameter',
41
+ message: `Optional parameter '${paramName}' in ${inLoc} was added.`,
42
+ consequence: 'Clients can optionally provide this new parameter for extended functionality.',
43
+ migration: 'No immediate action required.',
44
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
45
+ }, context));
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return changes;
51
+ }
52
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class ParamDeprecatedRule extends BaseRule {
5
+ id = 'PARAM_DEPRECATED';
6
+ description = 'A parameter 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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'deprecated' && fc.changeType === 'changed') {
17
+ if (fc.oldValue === false && fc.newValue === true) {
18
+ const paramId = fc.fieldPath[1];
19
+ const [paramName, inLoc] = paramId.split(':');
20
+ changes.push(this.makeChange({
21
+ severity: 'warning',
22
+ category: 'parameter',
23
+ message: `Parameter '${paramName}' in ${inLoc} was deprecated.`,
24
+ consequence: 'This parameter is slated for future removal.',
25
+ migration: `Plan to migrate away from using '${paramName}'.`,
26
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
27
+ }, context));
28
+ }
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 ParamEnumValueAddedRule extends BaseRule {
5
+ id = 'PARAM_ENUM_VALUE_ADDED';
6
+ description = 'A new allowed enum value was added to a parameter';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'schema' && fc.fieldPath.includes('enum') && fc.changeType === 'added') {
17
+ const paramId = fc.fieldPath[1];
18
+ const [paramName, inLoc] = paramId.split(':');
19
+ changes.push(this.makeChange({
20
+ severity: 'info',
21
+ category: 'parameter',
22
+ message: `Enum value '${fc.newValue}' was added to parameter '${paramName}' in ${inLoc}.`,
23
+ consequence: `Clients can now optionally send '${fc.newValue}' for this parameter.`,
24
+ migration: 'No immediate action required.',
25
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
26
+ }, context));
27
+ }
28
+ }
29
+ }
30
+ return changes;
31
+ }
32
+ }
@@ -0,0 +1,32 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class ParamEnumValueRemovedRule extends BaseRule {
5
+ id = 'PARAM_ENUM_VALUE_REMOVED';
6
+ description = 'An allowed enum value was removed from a parameter';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'schema' && fc.fieldPath.includes('enum') && fc.changeType === 'removed') {
17
+ const paramId = fc.fieldPath[1];
18
+ const [paramName, inLoc] = paramId.split(':');
19
+ changes.push(this.makeChange({
20
+ severity: 'breaking',
21
+ category: 'parameter',
22
+ message: `Enum value '${fc.oldValue}' was removed from parameter '${paramName}' in ${inLoc}.`,
23
+ consequence: `Clients sending '${fc.oldValue}' will now receive validation errors.`,
24
+ migration: `Update clients to use one of the remaining allowed enum values for '${paramName}'.`,
25
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
26
+ }, context));
27
+ }
28
+ }
29
+ }
30
+ return changes;
31
+ }
32
+ }
@@ -0,0 +1,43 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class ParamLocationChangedRule extends BaseRule {
5
+ id = 'PARAM_LOCATION_CHANGED';
6
+ description = 'A parameter was moved to a different location (e.g. query to header)';
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 removedParams = new Map<string, string>(); // name -> in
16
+ const addedParams = new Map<string, string>(); // name -> in
17
+
18
+ for (const fc of ed.fieldChanges) {
19
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath.length === 2) {
20
+ const paramId = fc.fieldPath[1];
21
+ const [paramName, inLoc] = paramId.split(':');
22
+ if (fc.changeType === 'removed') removedParams.set(paramName, inLoc);
23
+ if (fc.changeType === 'added') addedParams.set(paramName, inLoc);
24
+ }
25
+ }
26
+
27
+ for (const [name, oldLoc] of removedParams.entries()) {
28
+ const newLoc = addedParams.get(name);
29
+ if (newLoc) {
30
+ changes.push(this.makeChange({
31
+ severity: 'breaking',
32
+ category: 'parameter',
33
+ message: `Parameter '${name}' moved from ${oldLoc} to ${newLoc}.`,
34
+ consequence: `Clients sending '${name}' in the ${oldLoc} will fail.`,
35
+ migration: `Update clients to send '${name}' in the ${newLoc} instead.`,
36
+ location: { path: ed.path, method: ed.method, paramName: name, field: oldLoc }
37
+ }, context));
38
+ }
39
+ }
40
+ }
41
+ return changes;
42
+ }
43
+ }
@@ -0,0 +1,52 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange, Parameter } from '../../types/index.js';
3
+
4
+ export class ParamRemovedRule extends BaseRule {
5
+ id = 'PARAM_REMOVED';
6
+ description = 'A parameter was removed';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath.length === 2 && fc.changeType === 'removed') {
17
+ const paramId = fc.fieldPath[1];
18
+ const [paramName, inLoc] = paramId.split(':');
19
+
20
+ // Check if it's a location change
21
+ const isMoved = ed.fieldChanges.some(c => c.fieldPath[0] === 'parameters' && c.fieldPath.length === 2 && c.changeType === 'added' && c.fieldPath[1].startsWith(`${paramName}:`));
22
+ if (isMoved) continue;
23
+
24
+ const oldParam = fc.oldValue as Parameter;
25
+
26
+ if (oldParam.required) {
27
+ changes.push(this.makeChange({
28
+ severity: 'breaking',
29
+ ruleId: 'PARAM_REMOVED',
30
+ category: 'parameter',
31
+ message: `Required parameter '${paramName}' in ${inLoc} was removed.`,
32
+ consequence: 'Clients sending this parameter may experience errors or unexpected behavior if the server rejects unknown parameters.',
33
+ migration: `Update clients to stop sending the '${paramName}' parameter.`,
34
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
35
+ }, context));
36
+ } else {
37
+ changes.push(this.makeChange({
38
+ severity: 'warning',
39
+ ruleId: 'PARAM_OPTIONAL_REMOVED',
40
+ category: 'parameter',
41
+ message: `Optional parameter '${paramName}' in ${inLoc} was removed.`,
42
+ consequence: 'Clients sending this parameter will have it ignored, or may receive errors if the server strictly validates inputs.',
43
+ migration: `Update clients to stop sending the '${paramName}' parameter.`,
44
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
45
+ }, context));
46
+ }
47
+ }
48
+ }
49
+ }
50
+ return changes;
51
+ }
52
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class ParamRequiredAddedRule extends BaseRule {
5
+ id = 'PARAM_REQUIRED_FALSE_TO_TRUE';
6
+ description = 'An optional parameter was made required';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'required' && fc.changeType === 'changed') {
17
+ if (fc.oldValue === false && fc.newValue === true) {
18
+ const paramId = fc.fieldPath[1];
19
+ const [paramName, inLoc] = paramId.split(':');
20
+ changes.push(this.makeChange({
21
+ severity: 'breaking',
22
+ category: 'parameter',
23
+ message: `Optional parameter '${paramName}' in ${inLoc} is now required.`,
24
+ consequence: `Requests missing '${paramName}' will be rejected with a 400 Bad Request error.`,
25
+ migration: `Update all clients to include '${paramName}' in their requests.`,
26
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
27
+ }, context));
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return changes;
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class ParamRequiredTrueToFalseRule extends BaseRule {
5
+ id = 'PARAM_REQUIRED_TRUE_TO_FALSE';
6
+ description = 'A required parameter was made optional';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'required' && fc.changeType === 'changed') {
17
+ if (fc.oldValue === true && fc.newValue === false) {
18
+ const paramId = fc.fieldPath[1];
19
+ const [paramName, inLoc] = paramId.split(':');
20
+ changes.push(this.makeChange({
21
+ severity: 'info',
22
+ category: 'parameter',
23
+ message: `Required parameter '${paramName}' in ${inLoc} is now optional.`,
24
+ consequence: 'Clients can omit this parameter in requests.',
25
+ migration: 'Clients may stop sending this parameter if desired.',
26
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
27
+ }, context));
28
+ }
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 ParamTypeChangedRule extends BaseRule {
5
+ id = 'PARAM_TYPE_CHANGED';
6
+ description = 'The data type of a parameter 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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'parameters' && fc.fieldPath[2] === 'schema' && fc.fieldPath[3] === 'type' && fc.changeType === 'changed') {
17
+ const paramId = fc.fieldPath[1];
18
+ const [paramName, inLoc] = paramId.split(':');
19
+ changes.push(this.makeChange({
20
+ severity: 'breaking',
21
+ category: 'parameter',
22
+ message: `Parameter '${paramName}' in ${inLoc} changed type from ${fc.oldValue} to ${fc.newValue}.`,
23
+ consequence: 'Clients sending the old type will receive validation errors.',
24
+ migration: `Update clients to send the new type (${fc.newValue}) for '${paramName}'.`,
25
+ location: { path: ed.path, method: ed.method, paramName: paramName, field: inLoc }
26
+ }, context));
27
+ }
28
+ }
29
+ }
30
+ return changes;
31
+ }
32
+ }
@@ -0,0 +1,33 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange, RequestBody } from '../../types/index.js';
3
+
4
+ export class RequestBodyAddedRequiredRule extends BaseRule {
5
+ id = 'REQUEST_BODY_ADDED_REQUIRED';
6
+ description = 'A required request body was added';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.fieldPath.length === 1 && fc.changeType === 'added') {
17
+ const body = fc.newValue as RequestBody;
18
+ if (body.required) {
19
+ changes.push(this.makeChange({
20
+ severity: 'breaking',
21
+ category: 'request-body',
22
+ message: 'A required request body was added.',
23
+ consequence: 'Clients not sending a request body will now receive 400 Bad Request errors.',
24
+ migration: 'Update clients to send the required request body.',
25
+ location: { path: ed.path, method: ed.method, field: 'requestBody' }
26
+ }, context));
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return changes;
32
+ }
33
+ }
@@ -0,0 +1,30 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestBodyRemovedRule extends BaseRule {
5
+ id = 'REQUEST_BODY_REMOVED';
6
+ description = 'A request body was removed';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.fieldPath.length === 1 && fc.changeType === 'removed') {
17
+ changes.push(this.makeChange({
18
+ severity: 'breaking',
19
+ category: 'request-body',
20
+ message: 'The request body was removed.',
21
+ consequence: 'Clients sending a request body may experience errors if the server strictly validates requests.',
22
+ migration: 'Update clients to stop sending a request body.',
23
+ location: { path: ed.path, method: ed.method, field: 'requestBody' }
24
+ }, context));
25
+ }
26
+ }
27
+ }
28
+ return changes;
29
+ }
30
+ }
@@ -0,0 +1,31 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestContentTypeAddedRule extends BaseRule {
5
+ id = 'REQUEST_CONTENT_TYPE_ADDED';
6
+ description = 'A new content type was added to the request body';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.fieldPath[1] === 'content' && fc.fieldPath.length === 3 && fc.changeType === 'added') {
17
+ const mimeType = fc.fieldPath[2];
18
+ changes.push(this.makeChange({
19
+ severity: 'info',
20
+ category: 'request-body',
21
+ message: `Request content type '${mimeType}' was added.`,
22
+ consequence: 'Clients can now use this new content type when sending requests.',
23
+ migration: 'No immediate action required.',
24
+ location: { path: ed.path, method: ed.method, field: `requestBody.content['${mimeType}']` }
25
+ }, context));
26
+ }
27
+ }
28
+ }
29
+ return changes;
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestContentTypeRemovedRule extends BaseRule {
5
+ id = 'REQUEST_CONTENT_TYPE_REMOVED';
6
+ description = 'A content type was removed from the request body';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.fieldPath[1] === 'content' && fc.fieldPath.length === 3 && fc.changeType === 'removed') {
17
+ const mimeType = fc.fieldPath[2];
18
+ changes.push(this.makeChange({
19
+ severity: 'breaking',
20
+ category: 'request-body',
21
+ message: `Request content type '${mimeType}' was removed.`,
22
+ consequence: `Clients sending requests with Content-Type '${mimeType}' will receive 415 Unsupported Media Type errors.`,
23
+ migration: `Update clients to use a supported content type instead of '${mimeType}'.`,
24
+ location: { path: ed.path, method: ed.method, field: `requestBody.content['${mimeType}']` }
25
+ }, context));
26
+ }
27
+ }
28
+ }
29
+ return changes;
30
+ }
31
+ }
@@ -0,0 +1,41 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange, Schema } from '../../types/index.js';
3
+
4
+ export class RequestFieldAddedRequiredRule extends BaseRule {
5
+ id = 'REQUEST_FIELD_ADDED_REQUIRED';
6
+ description = 'A required field was added to the request body';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.changeType === 'added') {
17
+ const requiredIdx = fc.fieldPath.lastIndexOf('required');
18
+ if (requiredIdx !== -1 && fc.fieldPath.length === requiredIdx + 2) {
19
+ const fieldName = fc.fieldPath[requiredIdx + 1];
20
+
21
+ // Check if this property was ALSO added
22
+ const propsPath = fc.fieldPath.slice(0, requiredIdx).concat(['properties', fieldName]).join('.');
23
+ const propAdded = ed.fieldChanges.some(c => c.changeType === 'added' && c.fieldPath.join('.') === propsPath);
24
+
25
+ if (propAdded) {
26
+ changes.push(this.makeChange({
27
+ severity: 'breaking',
28
+ category: 'request-body',
29
+ message: `Required request body field '${fieldName}' was added.`,
30
+ consequence: `Clients not sending the new '${fieldName}' field will receive validation errors.`,
31
+ migration: `Update clients to include the '${fieldName}' field in requests.`,
32
+ location: { path: ed.path, method: ed.method, field: propsPath }
33
+ }, context));
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ return changes;
40
+ }
41
+ }
@@ -0,0 +1,35 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestFieldRemovedRule extends BaseRule {
5
+ id = 'REQUEST_FIELD_REMOVED';
6
+ description = 'A field was removed from the request body';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.changeType === 'removed') {
17
+ // Look for: ['requestBody', 'content', mimeType, 'schema', 'properties', fieldName, ...]
18
+ const propsIdx = fc.fieldPath.lastIndexOf('properties');
19
+ if (propsIdx !== -1 && fc.fieldPath.length === propsIdx + 2) {
20
+ const fieldName = fc.fieldPath[propsIdx + 1];
21
+ changes.push(this.makeChange({
22
+ severity: 'breaking',
23
+ category: 'request-body',
24
+ message: `Request body field '${fieldName}' was removed.`,
25
+ consequence: 'Clients sending this field may experience errors if the server strictly validates requests.',
26
+ migration: `Update clients to stop sending the '${fieldName}' field.`,
27
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.join('.') }
28
+ }, context));
29
+ }
30
+ }
31
+ }
32
+ }
33
+ return changes;
34
+ }
35
+ }
@@ -0,0 +1,37 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestFieldTypeChangedRule extends BaseRule {
5
+ id = 'REQUEST_FIELD_TYPE_CHANGED';
6
+ description = 'The data type of a request body field 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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody' && fc.changeType === 'changed') {
17
+ const typeIdx = fc.fieldPath.lastIndexOf('type');
18
+ if (typeIdx !== -1 && typeIdx === fc.fieldPath.length - 1) {
19
+ const propsIdx = fc.fieldPath.lastIndexOf('properties');
20
+ if (propsIdx !== -1 && typeIdx > propsIdx + 1) {
21
+ const fieldName = fc.fieldPath[propsIdx + 1];
22
+ changes.push(this.makeChange({
23
+ severity: 'breaking',
24
+ category: 'request-body',
25
+ message: `Request body field '${fieldName}' changed type from ${fc.oldValue} to ${fc.newValue}.`,
26
+ consequence: 'Clients sending the old type will receive validation errors.',
27
+ migration: `Update clients to send the new type (${fc.newValue}) for '${fieldName}'.`,
28
+ location: { path: ed.path, method: ed.method, field: fc.fieldPath.slice(0, -1).join('.') }
29
+ }, context));
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return changes;
36
+ }
37
+ }
@@ -0,0 +1,56 @@
1
+ import { BaseRule } from '../base.js';
2
+ import type { DiffSet, RuleContext, SemanticChange } from '../../types/index.js';
3
+
4
+ export class RequestRequiredFalseToTrueRule extends BaseRule {
5
+ id = 'REQUEST_REQUIRED_FALSE_TO_TRUE';
6
+ description = 'An optional request body field was made required';
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
+ for (const fc of ed.fieldChanges) {
16
+ if (fc.fieldPath[0] === 'requestBody') {
17
+ // Check for requestBody.required changing from false to true
18
+ if (fc.fieldPath.length === 2 && fc.fieldPath[1] === 'required' && fc.changeType === 'changed' && fc.oldValue === false && fc.newValue === true) {
19
+ changes.push(this.makeChange({
20
+ severity: 'breaking',
21
+ category: 'request-body',
22
+ message: 'The request body was made required.',
23
+ consequence: 'Clients not sending a request body will now receive validation errors.',
24
+ migration: 'Update clients to always send a request body.',
25
+ location: { path: ed.path, method: ed.method, field: 'requestBody.required' }
26
+ }, context));
27
+ }
28
+
29
+ // Check for schema field required changing from false to true
30
+ if (fc.changeType === 'added') {
31
+ const requiredIdx = fc.fieldPath.lastIndexOf('required');
32
+ if (requiredIdx !== -1 && fc.fieldPath.length === requiredIdx + 2) {
33
+ const fieldName = fc.fieldPath[requiredIdx + 1];
34
+
35
+ // Check if this property was ALSO added (if it was, it's covered by REQUEST_FIELD_ADDED_REQUIRED)
36
+ const propsPath = fc.fieldPath.slice(0, requiredIdx).concat(['properties', fieldName]).join('.');
37
+ const propAdded = ed.fieldChanges.some(c => c.changeType === 'added' && c.fieldPath.join('.') === propsPath);
38
+
39
+ if (!propAdded) {
40
+ changes.push(this.makeChange({
41
+ severity: 'breaking',
42
+ category: 'request-body',
43
+ message: `Optional request body field '${fieldName}' is now required.`,
44
+ consequence: `Requests missing '${fieldName}' will now be rejected.`,
45
+ migration: `Update clients to always include the '${fieldName}' field.`,
46
+ location: { path: ed.path, method: ed.method, field: propsPath }
47
+ }, context));
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return changes;
55
+ }
56
+ }