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