Skip to content

Commit

Permalink
feat(ibm-pattern-properties): enforce use of anchors for property pat…
Browse files Browse the repository at this point in the history
…terns (#679)

This commit adds an additional check to the rule dealing with `pattern-properties`.
It requires the use of starting and ending anchors (^ and $, respectively) for the
regular expression it defines as its single element, much like we require for the
`pattern` field of string schemas.

Signed-off-by: Dustin Popp <[email protected]>
  • Loading branch information
dpopp07 authored Aug 27, 2024
1 parent c8479f2 commit 8055f2f
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/ibm-cloud-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -4733,6 +4733,7 @@ within a schema:
<ul>
<li>The <code>patternProperties</code> field must be an object with exactly one entry.
<li>The <code>patternProperties</code> and <code>additionalProperties</code> fields are mutually exclusive within a particular schema.
<li>The <code>patternProperties</code> field must contain a regular expression anchored by <code>^</code> and <code>$</code>.
</ul>
</tr>
<tr>
Expand Down
15 changes: 15 additions & 0 deletions packages/ruleset/src/functions/pattern-properties.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ module.exports = function (schema, _opts, context) {
* 2. patternProperties must be an object
* 3. patternProperties must not be empty
* 4. patternProperties must have at most one entry
* 5. patternProperties must anchor its pattern
*
* @param {*} schema the schema to check
* @param {*} path the array of path segments indicating the location of "schema" within the API definition
Expand Down Expand Up @@ -95,6 +96,20 @@ function patternPropertiesCheck(schema, path) {
];
}

// At this point, we're guaranteed to have one pattern in the list.
// We need to make sure the regular expression is anchored.
if (!keys[0].startsWith('^') || !keys[0].endsWith('$')) {
logger.debug(
`${ruleId}: Error: patternProperties has a non-anchored pattern!`
);
return [
{
message: 'patternProperties patterns should be anchored with ^ and $',
path: [...path, 'patternProperties', 0],
},
];
}

logger.debug(`${ruleId}: PASSED!`);

return [];
Expand Down
51 changes: 51 additions & 0 deletions packages/ruleset/test/pattern-properties.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const expectedMessage1 = `patternProperties and additionalProperties are mutuall
const expectedMessage2 = `patternProperties must be an object`;
const expectedMessage3 = `patternProperties must be a non-empty object`;
const expectedMessage4 = `patternProperties must be an object with at most one entry`;
const expectedMessage5 = `patternProperties patterns should be anchored with ^ and $`;

describe(`Spectral rule: ${ruleId}`, () => {
beforeAll(() => {
Expand Down Expand Up @@ -144,5 +145,55 @@ describe(`Spectral rule: ${ruleId}`, () => {
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});

it('patternProperties entry is missing beginning anchor', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.components.schemas['Movie'].patternProperties = {
'str.*$': {
type: 'string',
},
};

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(4);

const expectedPaths = [
'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.patternProperties',
'paths./v1/movies.post.responses.201.content.application/json.schema.patternProperties',
'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.patternProperties',
'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.patternProperties',
];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessage5);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});

it('patternProperties entry is missing ending anchor', async () => {
const testDocument = makeCopy(rootDocument);
testDocument.components.schemas['Movie'].patternProperties = {
'^str.*': {
type: 'string',
},
};

const results = await testRule(ruleId, rule, testDocument);
expect(results).toHaveLength(4);

const expectedPaths = [
'paths./v1/movies.get.responses.200.content.application/json.schema.allOf.1.properties.movies.items.patternProperties',
'paths./v1/movies.post.responses.201.content.application/json.schema.patternProperties',
'paths./v1/movies/{movie_id}.get.responses.200.content.application/json.schema.patternProperties',
'paths./v1/movies/{movie_id}.put.responses.200.content.application/json.schema.patternProperties',
];
for (let i = 0; i < results.length; i++) {
expect(results[i].code).toBe(ruleId);
expect(results[i].message).toBe(expectedMessage5);
expect(results[i].severity).toBe(expectedSeverity);
expect(results[i].path.join('.')).toBe(expectedPaths[i]);
}
});
});
});

0 comments on commit 8055f2f

Please sign in to comment.