Skip to content

Commit ad7134d

Browse files
authored
fix(ibm-well-defined-dictionaries): include patternProperties in validation (#713)
Currently, the rule only considers dictionaries defined with `additionalProperties`. OpenAPI 3.1.x supports defining dictionaries with `patternProperties`, so this commit adds consideration for this field in its validation, in addition to `additionalProperties`. Signed-off-by: Dustin Popp <[email protected]>
1 parent f0dc756 commit ad7134d

File tree

4 files changed

+186
-67
lines changed

4 files changed

+186
-67
lines changed

.secrets.baseline

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "package-lock.json|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2024-12-19T16:14:03Z",
6+
"generated_at": "2025-01-09T19:49:59Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"

docs/ibm-cloud-rules.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -7328,8 +7328,8 @@ paths:
73287328
This rule validates that any dictionary schemas are well defined and that all values share a single type.
73297329
Dictionaries are defined as object type schemas that have variable key names. They are distinct from model types,
73307330
which are objects with pre-defined properties. A schema must not define both concrete properties and variable key names.
7331-
Practically, this means a schema must explicitly define a `properties` object or an `additionalProperties` schema, but not both.
7332-
If used, the `additionalProperties` schema must define a concrete type. The concrete type of the values must not be a dictionary itself. See the <a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-types">IBM Cloud API Handbook documentation on types</a> for more info.
7331+
Practically, this means a schema must explicitly define a `properties` object or an `(additional|pattern)Properties` schema, but not both.
7332+
If used, the `(additional|pattern)Properties` schema must define a concrete type. The concrete type of the values must not be a dictionary itself. See the <a href="https://cloud.ibm.com/docs/api-handbook?topic=api-handbook-types">IBM Cloud API Handbook documentation on types</a> for more info.
73337333
</td>
73347334
</tr>
73357335
<tr>

packages/ruleset/src/functions/well-defined-dictionaries.js

+51-29
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
/**
2-
* Copyright 2024 IBM Corporation.
2+
* Copyright 2024 - 2025 IBM Corporation.
33
* SPDX-License-Identifier: Apache2.0
44
*/
55

66
const {
77
isObject,
88
isObjectSchema,
99
schemaHasConstraint,
10-
schemaLooselyHasConstraint,
1110
validateNestedSchemas,
1211
} = require('@ibm-cloud/openapi-ruleset-utilities');
1312
const { LoggerFactory } = require('../utils');
1413

1514
let ruleId;
1615
let logger;
1716

17+
/**
18+
* The implementation for this rule makes assumptions that are dependent on the
19+
* presence of the following other rules:
20+
*
21+
* - ibm-pattern-properties: patternProperties isn't empty or the wrong type
22+
*/
23+
1824
module.exports = function (schema, _opts, context) {
1925
if (!logger) {
2026
ruleId = context.rule.name;
@@ -32,23 +38,23 @@ function wellDefinedDictionaries(schema, path) {
3238

3339
// We will flag dictionaries of dictionaries, so we can skip
3440
// providing guidance for directly nested dictionaries.
35-
if (path.at(-1) === 'additionalProperties') {
41+
if (isDictionaryValueSchema(path)) {
3642
return [];
3743
}
3844

3945
logger.debug(
4046
`${ruleId}: checking object schema at location: ${path.join('.')}`
4147
);
4248

43-
// Dictionaries should have additionalProperties defined on them.
44-
// If the schema doesn't, make sure it has properties and then
45-
// abandon the check.
46-
if (!schemaDefinesField(schema, 'additionalProperties')) {
49+
// Dictionaries should have additionalProperties or patternProperties
50+
// defined on them. If the schema doesn't, make sure it has properties
51+
// and then abandon the check.
52+
if (!isDictionarySchema(schema)) {
4753
if (!schemaDefinesField(schema, 'properties')) {
4854
return [
4955
{
5056
message:
51-
'Object schemas must define either properties, or additionalProperties with a concrete type',
57+
'Object schemas must define either properties, or (additional/pattern)Properties with a concrete type',
5258
path,
5359
},
5460
];
@@ -79,8 +85,7 @@ function wellDefinedDictionaries(schema, path) {
7985
// more strict in the future but this meets our current purposes.
8086
if (schemaHasConstraint(schema, isAmbiguousDictionary)) {
8187
errors.push({
82-
message:
83-
'Dictionary schemas must have a single, well-defined value type in `additionalProperties`',
88+
message: 'Dictionary schemas must have a single, well-defined value type',
8489
path,
8590
});
8691
}
@@ -89,7 +94,7 @@ function wellDefinedDictionaries(schema, path) {
8994
// should not be dictionaries themselves.
9095
if (schemaHasConstraint(schema, isDictionaryOfDictionaries)) {
9196
errors.push({
92-
message: 'Dictionaries must not have values that are also dictionaries.',
97+
message: 'Dictionaries must not have values that are also dictionaries',
9398
path,
9499
});
95100
}
@@ -102,29 +107,46 @@ function schemaDefinesField(schema, field) {
102107
}
103108

104109
function isAmbiguousDictionary(schema) {
105-
if (!schema.additionalProperties) {
106-
return false;
107-
}
108-
109-
// additionalProperties must be an object (not a boolean) value
110-
// and must define a `type` field in order to be considered an
111-
// unambiguous dictionary.
112-
return (
113-
!isObject(schema.additionalProperties) ||
114-
!schemaDefinesField(schema.additionalProperties, 'type')
110+
return dictionaryValuesHaveConstraint(
111+
schema,
112+
valueSchema =>
113+
!isObject(valueSchema) || !schemaDefinesField(valueSchema, 'type')
115114
);
116115
}
117116

118117
function isDictionaryOfDictionaries(schema) {
119-
if (!isObject(schema.additionalProperties)) {
118+
return dictionaryValuesHaveConstraint(
119+
schema,
120+
valueSchema => isObject(valueSchema) && isDictionarySchema(valueSchema)
121+
);
122+
}
123+
124+
function dictionaryValuesHaveConstraint(schema, hasConstraint) {
125+
return schemaHasConstraint(schema, s => {
126+
if (s.additionalProperties !== undefined) {
127+
return hasConstraint(s.additionalProperties);
128+
}
129+
130+
if (s.patternProperties !== undefined) {
131+
return Object.values(s.patternProperties).some(p => hasConstraint(p));
132+
}
133+
120134
return false;
121-
}
135+
});
136+
}
122137

123-
// We don't want any schema that may define the dictionary values
124-
// to also be a dictionary, so we use a looser constraint that
125-
// checks against any oneOf/anyOf schema.
126-
return schemaLooselyHasConstraint(
127-
schema.additionalProperties,
128-
s => !!s['additionalProperties']
138+
// Check, *by path*, if the current schema is a dictionary value schema.
139+
function isDictionaryValueSchema(path) {
140+
return (
141+
path.at(-1) === 'additionalProperties' ||
142+
path.at(-2) === 'patternProperties'
143+
);
144+
}
145+
146+
// Check, *by object fields* if the current schema is a dictionary or not.
147+
function isDictionarySchema(schema) {
148+
return (
149+
schemaDefinesField(schema, 'additionalProperties') ||
150+
schemaDefinesField(schema, 'patternProperties')
129151
);
130152
}

0 commit comments

Comments
 (0)