Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [
},
errors: [],
},
{
name: 'valid paths - IPA-132 Operations endpoints',
document: {
paths: {
// Collection-scoped Operations endpoints, nested directly under the parent collection
'/api/atlas/v2/resourceName/operations': {},
'/api/atlas/v2/resourceName/operations/{operationId}': {},
'/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations': {},
'/api/atlas/v2/resourceName1/{pathParam}/resourceName2/operations/{operationId}': {},
'/api/atlas/v2/unauth/resourceName/operations': {},
'/api/atlas/v2/unauth/resourceName/operations/{operationId}': {},
// Instance-scoped Operations endpoints, nested under the parent resource instance
'/api/atlas/v2/resourceName/{pathParam}/operations': {},
'/api/atlas/v2/resourceName/{pathParam}/operations/{operationId}': {},
},
},
errors: [],
},
{
name: 'invalid paths - api/atlas/v2',
document: {
Expand Down Expand Up @@ -134,6 +152,69 @@ testRule('xgen-IPA-102-path-alternate-resource-name-path-param', [
},
],
},
{
name: 'invalid paths - only a trailing Operations suffix is exempt',
document: {
paths: {
// `operations` is only exempt as the final segment, optionally followed by its path param
'/api/atlas/v2/resourceName/operations/foo': {},
'/api/atlas/v2/resourceName/operations/{operationId}/details': {},
},
},
errors: [
{
code: 'xgen-IPA-102-path-alternate-resource-name-path-param',
message: 'API paths must alternate between resource name and path params.',
path: ['paths', '/api/atlas/v2/resourceName/operations/foo'],
severity: DiagnosticSeverity.Error,
},
{
code: 'xgen-IPA-102-path-alternate-resource-name-path-param',
message: 'API paths must alternate between resource name and path params.',
path: ['paths', '/api/atlas/v2/resourceName/operations/{operationId}/details'],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'Operations endpoints no longer need an exception',
document: {
paths: {
'/api/atlas/v2/resourceName/operations': {
'x-xgen-IPA-exception': {
'xgen-IPA-102-path-alternate-resource-name-path-param': 'reason',
},
},
},
},
errors: [
{
code: 'xgen-IPA-102-path-alternate-resource-name-path-param',
message: 'This component adopts the rule and does not need an exception. Please remove the exception.',
path: [
'paths',
'/api/atlas/v2/resourceName/operations',
'x-xgen-IPA-exception',
'xgen-IPA-102-path-alternate-resource-name-path-param',
],
severity: DiagnosticSeverity.Error,
},
],
},
{
name: 'Operations paths which are still violations may keep an exception',
document: {
paths: {
// `operations` is not the final segment, so the path is a violation the exception suppresses
'/api/atlas/v2/resourceName/operations/foo': {
'x-xgen-IPA-exception': {
'xgen-IPA-102-path-alternate-resource-name-path-param': 'reason',
},
},
},
},
errors: [],
},
{
name: 'invalid paths with exceptions',
document: {
Expand Down
2 changes: 2 additions & 0 deletions tools/spectral/ipa/rulesets/IPA-102.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ rules:
- Paths must follow a pattern where resource names and path parameters strictly alternate
- Even-indexed path segments should be resource names (not path parameters)
- Odd-indexed path segments should be path parameters
- A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations
endpoints defined by IPA-132
- Paths with `x-xgen-IPA-exception` for this rule are excluded from validation
- If any parent path has an exception for this rule, the exception will be inherited.

Expand Down
2 changes: 2 additions & 0 deletions tools/spectral/ipa/rulesets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Rule checks for the following conditions:
- Paths must follow a pattern where resource names and path parameters strictly alternate
- Even-indexed path segments should be resource names (not path parameters)
- Odd-indexed path segments should be path parameters
- A trailing `operations` or `operations/{operationId}` suffix is exempt, for the Operations
endpoints defined by IPA-132
- Paths with `x-xgen-IPA-exception` for this rule are excluded from validation
- If any parent path has an exception for this rule, the exception will be inherited.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,35 @@ const getPrefix = (path) => {
return null;
};

const OPERATIONS_SEGMENT = 'operations';

/**
* Removes a trailing Operations suffix (`operations` or `operations/{operationId}`) so the rest of
* the path can be checked for strict alternation. The Operations collection defined by IPA-132 is
* nested directly under its parent resource and intentionally does not alternate.
*
* @param {string[]} elements - The path segments
* @returns {string[]} The path segments without a trailing Operations suffix
*/
const stripOperationsSuffix = (elements) => {
const last = elements[elements.length - 1];
const secondToLast = elements[elements.length - 2];

let suffixLength;
if (secondToLast === OPERATIONS_SEGMENT && isPathParam(last)) {
suffixLength = 2; // `.../operations/{operationId}`
} else if (last === OPERATIONS_SEGMENT) {
suffixLength = 1; // `.../operations`
} else {
return elements;
}

// An unscoped `/api/atlas/v2/operations` strips to nothing and passes; rejecting it is IPA-132's job.
return elements.slice(0, elements.length - suffixLength);
};

const validatePathStructure = (elements) => {
return elements.every((element, index) => {
return stripOperationsSuffix(elements).every((element, index) => {
const isEvenIndex = index % 2 === 0;
return isEvenIndex ? !isPathParam(element) : isPathParam(element);
});
Expand Down
Loading