|
1 | 1 | /** |
2 | | - * Copyright 2023 - 2024 IBM Corporation. |
| 2 | + * Copyright 2023 - 2025 IBM Corporation. |
3 | 3 | * SPDX-License-Identifier: Apache2.0 |
4 | 4 | */ |
5 | 5 |
|
6 | 6 | const { |
7 | 7 | getSchemaType, |
8 | 8 | isObject, |
9 | | - isObjectSchema, |
10 | 9 | isArraySchema, |
11 | 10 | schemaHasConstraint, |
12 | 11 | schemaLooselyHasConstraint, |
@@ -89,7 +88,7 @@ function checkApiForSymmetry(apidef, nodes) { |
89 | 88 | ['Summary', 'Prototype', 'Patch'].forEach(variantType => { |
90 | 89 | const variantSchemaName = `${canonicalSchemaName}${variantType}`; |
91 | 90 | const variantSchema = apidef.components.schemas[`${variantSchemaName}`]; |
92 | | - if (variantSchema && isObjectSchema(variantSchema)) { |
| 91 | + if (variantSchema && isObject(variantSchema)) { |
93 | 92 | logger.info( |
94 | 93 | `${ruleId}: checking variant schema ${variantSchemaName} against canonical schema ${canonicalSchemaName}` |
95 | 94 | ); |
@@ -128,9 +127,11 @@ function checkApiForSymmetry(apidef, nodes) { |
128 | 127 | /** |
129 | 128 | * Determine if the variant schema is indeed a proper graph fragment of the |
130 | 129 | * canonical schema, using the following conditions: |
131 | | - * - The variant does not define additional or pattern properties |
132 | 130 | * - The variant does not define any properties that do not exist on the |
133 | 131 | * canonical schema (exists = present and has same type) |
| 132 | + * - The variant does not define any nested schemas (including those defined |
| 133 | + * by arrays or dictionaries) that aren't themselves graph fragments of |
| 134 | + * the corresponding canonical schema. |
134 | 135 | * - The variant omits at least one property from the canonical schema |
135 | 136 | * |
136 | 137 | * @param {object} variant - the variant schema to check |
@@ -163,103 +164,148 @@ function checkForGraphFragmentPattern( |
163 | 164 | ); |
164 | 165 | let result = true; |
165 | 166 |
|
166 | | - // A variant schema cannot allow extraneous properties and still be |
167 | | - // an explicit graph fragment of the canonical schema. |
168 | | - if (variant.additionalProperties) { |
| 167 | + // Check for simple type equivalency - if the types are not the same, |
| 168 | + // the graph fragment pattern is violated. |
| 169 | + if ( |
| 170 | + !fromApplicator && |
| 171 | + !canonicalSchemaMeetsConstraint( |
| 172 | + canonical, |
| 173 | + canonicalPath, |
| 174 | + schemaFinder, |
| 175 | + schema => getSchemaType(variant) === getSchemaType(schema) |
| 176 | + ) |
| 177 | + ) { |
169 | 178 | logger.info( |
170 | | - `${ruleId}: schema defines 'additionalProperties' - it is not a proper graph fragment` |
| 179 | + `${ruleId}: variant and canonical schemas are different types` |
171 | 180 | ); |
172 | 181 | result = false; |
173 | 182 | } |
174 | | - if (variant.patternProperties) { |
| 183 | + |
| 184 | + // Ensure list schemas also maintain a graph fragment structure. |
| 185 | + if ( |
| 186 | + isObject(variant.items) && |
| 187 | + isArraySchema(variant) && |
| 188 | + !canonicalSchemaMeetsConstraint( |
| 189 | + canonical, |
| 190 | + canonicalPath, |
| 191 | + schemaFinder, |
| 192 | + (schema, path) => |
| 193 | + isObject(schema.items) && |
| 194 | + isGraphFragment( |
| 195 | + variant.items, |
| 196 | + schema.items, |
| 197 | + [...path, 'items'], |
| 198 | + false |
| 199 | + ) |
| 200 | + ) |
| 201 | + ) { |
175 | 202 | logger.info( |
176 | | - `${ruleId}: schema defines 'patternProperties' - it is not a proper graph fragment` |
| 203 | + `${ruleId}: variant is array with schema that is not a graph fragment of canonical items schema` |
| 204 | + ); |
| 205 | + result = false; |
| 206 | + } |
| 207 | + |
| 208 | + // Ensure dictionary schemas also maintain a graph fragment structure |
| 209 | + // (additional properties). |
| 210 | + if ( |
| 211 | + variant.additionalProperties && |
| 212 | + !canonicalSchemaMeetsConstraint( |
| 213 | + canonical, |
| 214 | + canonicalPath, |
| 215 | + schemaFinder, |
| 216 | + (schema, path) => |
| 217 | + schema.additionalProperties && |
| 218 | + isGraphFragment( |
| 219 | + variant.additionalProperties, |
| 220 | + schema.additionalProperties, |
| 221 | + [...path, 'additionalProperties'], |
| 222 | + false |
| 223 | + ) |
| 224 | + ) |
| 225 | + ) { |
| 226 | + logger.info( |
| 227 | + `${ruleId}: variant is dictionary with an additionalProperties schema that is not a graph fragment of canonical` |
| 228 | + ); |
| 229 | + result = false; |
| 230 | + } |
| 231 | + |
| 232 | + // Ensure dictionary schemas also maintain a graph fragment structure |
| 233 | + // (pattern properties). |
| 234 | + if ( |
| 235 | + isObject(variant.patternProperties) && |
| 236 | + !canonicalSchemaMeetsConstraint( |
| 237 | + canonical, |
| 238 | + canonicalPath, |
| 239 | + schemaFinder, |
| 240 | + (schema, path) => |
| 241 | + isObject(schema.patternProperties) && |
| 242 | + // This is a little convoluted but it is enforcing that |
| 243 | + // 1) every pattern in the variant schema is also in canonical |
| 244 | + // schema, and |
| 245 | + // 2) every patterned schema in the variant must be a graph fragment |
| 246 | + // of at least one patterned schema in the canonical schema. |
| 247 | + Object.entries(variant.patternProperties).every( |
| 248 | + ([variantPattern, variantPatternSchema]) => |
| 249 | + Object.keys(schema.patternProperties).includes(variantPattern) && |
| 250 | + Object.entries(schema.patternProperties).some( |
| 251 | + ([canonPattern, canonPatternSchema]) => |
| 252 | + isGraphFragment( |
| 253 | + variantPatternSchema, |
| 254 | + canonPatternSchema, |
| 255 | + [...path, 'patternProperties', canonPattern], |
| 256 | + false |
| 257 | + ) |
| 258 | + ) |
| 259 | + ) |
| 260 | + ) |
| 261 | + ) { |
| 262 | + logger.info( |
| 263 | + `${ruleId}: variant is dictionary with a patternProperties schema that is not a graph fragment of canonical` |
177 | 264 | ); |
178 | 265 | result = false; |
179 | 266 | } |
180 | 267 |
|
181 | 268 | // If the variant schema (or sub-schema) has properties, ensure that each |
182 | 269 | // property is defined *somewhere* on the corresponding canonical schema |
183 | | - // (or sub-schema) and has the same, specific type. |
| 270 | + // (or sub-schema) and ensure it is also a valid graph fragment of the |
| 271 | + // corresponding property in the canonical schema. |
184 | 272 | // |
185 | | - // We use a custom, looser contraint-checking function here because it is |
| 273 | + // We use a looser contraint-checking function here because it is |
186 | 274 | // sufficient for "one of" or "any of" the canonical schemas to define the |
187 | 275 | // property defined on the variant schema, and we need to resolve reference |
188 | 276 | // schemas on the fly each time we check the canonical schema for a constraint. |
189 | 277 | if (isObject(variant.properties)) { |
190 | 278 | for (const [name, prop] of Object.entries(variant.properties)) { |
| 279 | + let propExistsSomewhere = false; |
| 280 | + |
191 | 281 | const valid = canonicalSchemaMeetsConstraint( |
192 | 282 | canonical, |
193 | 283 | canonicalPath, |
194 | 284 | schemaFinder, |
195 | | - c => |
196 | | - 'properties' in c && |
197 | | - isObject(c.properties[name]) && |
198 | | - getSchemaType(c.properties[name]) === getSchemaType(prop) |
199 | | - ); |
| 285 | + (schema, path) => { |
| 286 | + const exists = |
| 287 | + 'properties' in schema && isObject(schema.properties[name]); |
| 288 | + propExistsSomewhere = propExistsSomewhere || exists; |
200 | 289 |
|
201 | | - // Note: Prototype schemas are allowed to define writeOnly properties |
202 | | - // that don't exist on the canonical schema. |
203 | | - if (!valid && !(considerWriteOnly && prop.writeOnly)) { |
204 | | - logger.info( |
205 | | - `${ruleId}: property '${name}' does not exist on the canonical schema` |
206 | | - ); |
207 | | - result = false; |
208 | | - } |
209 | | - |
210 | | - // Ensure nested schemas are also graph fragments of the corresponding |
211 | | - // nested schemas in the canonical schema. |
212 | | - if ( |
213 | | - valid && |
214 | | - isObjectSchema(prop) && |
215 | | - !canonicalSchemaMeetsConstraint( |
216 | | - canonical, |
217 | | - canonicalPath, |
218 | | - schemaFinder, |
219 | | - (schema, path) => |
220 | | - // Note that these first two conditions are guaranteed to be met at |
221 | | - // least once by the first call to `canonicalSchemaMeetsConstraint` |
222 | | - 'properties' in schema && |
223 | | - isObject(schema.properties[name]) && |
| 290 | + return ( |
| 291 | + exists && |
224 | 292 | isGraphFragment( |
225 | 293 | prop, |
226 | 294 | schema.properties[name], |
227 | 295 | [...path, 'properties', name], |
228 | 296 | false |
229 | 297 | ) |
230 | | - ) |
231 | | - ) { |
232 | | - logger.info( |
233 | | - `${ruleId}: nested object property ${name} is not a graph fragment of canonical property ${name}` |
234 | | - ); |
235 | | - result = false; |
236 | | - } |
| 298 | + ); |
| 299 | + } |
| 300 | + ); |
237 | 301 |
|
238 | | - // Ensure lists of schemas also maintain a graph fragment structure. |
239 | | - if ( |
240 | | - valid && |
241 | | - isArraySchema(prop) && |
242 | | - isObjectSchema(prop.items) && |
243 | | - !canonicalSchemaMeetsConstraint( |
244 | | - canonical, |
245 | | - canonicalPath, |
246 | | - schemaFinder, |
247 | | - (schema, path) => |
248 | | - // Note that these first two conditions are guaranteed to be met at |
249 | | - // least once by the first call to `canonicalSchemaMeetsConstraint` |
250 | | - 'properties' in schema && |
251 | | - isObject(schema.properties[name]) && |
252 | | - isObject(schema.properties[name].items) && |
253 | | - isGraphFragment( |
254 | | - prop.items, |
255 | | - schema.properties[name].items, |
256 | | - [...path, 'properties', name, 'items'], |
257 | | - false |
258 | | - ) |
259 | | - ) |
260 | | - ) { |
| 302 | + // Note: Prototype schemas are allowed to define writeOnly properties |
| 303 | + // that don't exist on the canonical schema. |
| 304 | + if (!valid && !(considerWriteOnly && prop.writeOnly)) { |
261 | 305 | logger.info( |
262 | | - `${ruleId}: array property ${name} items schema is not a graph fragment of canonical property ${name} items schema` |
| 306 | + propExistsSomewhere |
| 307 | + ? `${ruleId}: nested object property ${name} is not a graph fragment of canonical property ${name}` |
| 308 | + : `${ruleId}: property '${name}' does not exist on the canonical schema` |
263 | 309 | ); |
264 | 310 | result = false; |
265 | 311 | } |
|
0 commit comments