functions/pagination-response.js (73 lines of code) (raw):

// Check conformance to Azure guidelines for paginated responses: // - The operation should have the `x-ms-pageable` annotation [R2029] // - The response should contain a top-level `value` property of type array and required // - The response should contain a top-level `nextLink` property of type string and optional [R4012] module.exports = (operation, _opts, paths) => { // operation should be a get or post operation if (operation === null || typeof operation !== 'object') { return []; } const path = paths.path || paths.target || []; // responses is required property of an operation in OpenAPI 2.0, so if // isn't present this will be flagged elsewhere -- just return; if (!operation.responses || typeof operation.responses !== 'object') { return []; } // Find success response code const resp = Object.keys(operation.responses) .find((code) => code.startsWith('2')); // No success response will be flagged elsewhere, just return if (!resp) { return []; } // Get the schema of the success response const responseSchema = operation.responses[resp].schema || {}; const errors = []; if (operation['x-ms-pageable']) { // Check value property if (responseSchema.properties && 'value' in responseSchema.properties) { if (responseSchema.properties.value.type !== 'array') { errors.push({ message: '`value` property in pageable response should be type: array', path: [...path, 'responses', resp, 'schema', 'properties', 'value', 'type'], }); } if (!(responseSchema.required?.includes('value'))) { errors.push({ message: '`value` property in pageable response should be required', path: [...path, 'responses', resp, 'schema', 'required'], }); } } else if (!responseSchema.allOf) { // skip error for missing value -- it might be in allOf errors.push({ message: 'Response body schema of pageable response should contain top-level array property `value`', path: [...path, 'responses', resp, 'schema', 'properties'], }); } // Check nextLink property const nextLinkName = operation['x-ms-pageable'].nextLinkName || 'nextLink'; if (responseSchema.properties && nextLinkName in responseSchema.properties) { const nextLinkProperty = responseSchema.properties[nextLinkName]; if (nextLinkProperty.type !== 'string') { errors.push({ message: `\`${nextLinkName}\` property in pageable response should be type: string`, path: [...path, 'responses', resp, 'schema', 'properties', nextLinkName, 'type'], }); } else if (nextLinkProperty.format !== 'uri' && nextLinkProperty.format !== 'url') { // Allow "uri" or "url", but prefer "uri" errors.push({ message: `\`${nextLinkName}\` property in pageable response should be format: uri`, path: [...path, 'responses', resp, 'schema', 'properties', nextLinkName, 'format'], }); } if (responseSchema.required?.includes(nextLinkName)) { errors.push({ message: `\`${nextLinkName}\` property in pageable response should be optional.`, path: [...path, 'responses', resp, 'schema', 'required'], }); } } else if (!responseSchema.allOf) { // skip error for missing nextLink -- it might be in allOf errors.push({ message: `Response body schema of pageable response should contain top-level property \`${nextLinkName}\``, path: [...path, 'responses', resp, 'schema', 'properties'], }); } } else { const responseHasArray = Object.values(responseSchema.properties || {}) .some((prop) => prop.type === 'array'); // Why 3? [value, nextLink, count] if (responseHasArray && Object.keys(responseSchema.properties).length <= 3) { errors.push({ message: 'Operation might be pageable. Consider adding the x-ms-pageable extension.', path, }); } } return errors; };