From 2d29002a30c6dae5bc8351ebeb107d5908be46b2 Mon Sep 17 00:00:00 2001 From: Fernando Barcelos Rosito Date: Fri, 17 Apr 2026 11:49:36 -0300 Subject: [PATCH] chore: remove Spectral from repository Remove all Spectral-related files and configuration, including: - spectral/ directory with ruleset and custom functions - linters.yml workflow running Spectral lint on OpenAPI - Spectral sync step in sync-v4.yml workflow - Spectral usage and documentation references in README.md --- .github/workflows/linters.yml | 29 - .github/workflows/sync-v4.yml | 10 +- README.md | 8 +- spectral/README.md | 370 ----------- .../functions/checkAsyncOperationSchema.js | 37 -- .../functions/checkAuthenticationScheme.js | 34 - spectral/functions/checkContactRequired.js | 31 - .../functions/checkContentTypeResponse.js | 13 - spectral/functions/checkCreatedAtField.js | 181 ------ spectral/functions/checkDataKeysPresence.js | 19 - spectral/functions/checkDataObjectType.js | 18 - .../functions/checkDiscriminatorPresent.js | 34 - .../functions/checkEndpointStatusCodes.js | 26 - .../functions/checkErrorResponseJsonApi.js | 36 -- spectral/functions/checkExamplesRequired.js | 21 - spectral/functions/checkFieldsQueryParam.js | 28 - .../functions/checkFilterDocumentation.js | 25 - spectral/functions/checkHelpTextRequired.js | 12 - spectral/functions/checkNumberMaximumLimit.js | 13 - spectral/functions/checkNumberMinimumLimit.js | 13 - .../functions/checkOperationIdRequired.js | 11 - spectral/functions/checkOrderingParameter.js | 11 - .../checkPaginationResponseSchema.js | 37 -- .../functions/checkPathEndsWithSForList.js | 19 - .../functions/checkPathParametersComplete.js | 34 - .../functions/checkQueryStringsPagination.js | 45 -- .../functions/checkRequestBodyRequired.js | 25 - spectral/functions/checkResourceIDUsage.js | 25 - .../functions/checkSchemaNamingConvention.js | 23 - spectral/functions/checkSearchParameter.js | 11 - spectral/functions/checkServersRequired.js | 34 - .../functions/checkStateDeletedResource.js | 20 - .../functions/checkStatusCodesByMethod.js | 27 - spectral/functions/checkStringMaxLength.js | 13 - spectral/functions/checkStringMinLength.js | 12 - spectral/functions/checkStringPattern.js | 13 - .../checkSummaryDescriptionRequired.js | 23 - spectral/spectral.yaml | 604 ------------------ 38 files changed, 4 insertions(+), 1941 deletions(-) delete mode 100644 .github/workflows/linters.yml delete mode 100644 spectral/README.md delete mode 100644 spectral/functions/checkAsyncOperationSchema.js delete mode 100644 spectral/functions/checkAuthenticationScheme.js delete mode 100644 spectral/functions/checkContactRequired.js delete mode 100644 spectral/functions/checkContentTypeResponse.js delete mode 100644 spectral/functions/checkCreatedAtField.js delete mode 100644 spectral/functions/checkDataKeysPresence.js delete mode 100644 spectral/functions/checkDataObjectType.js delete mode 100644 spectral/functions/checkDiscriminatorPresent.js delete mode 100644 spectral/functions/checkEndpointStatusCodes.js delete mode 100644 spectral/functions/checkErrorResponseJsonApi.js delete mode 100644 spectral/functions/checkExamplesRequired.js delete mode 100644 spectral/functions/checkFieldsQueryParam.js delete mode 100644 spectral/functions/checkFilterDocumentation.js delete mode 100644 spectral/functions/checkHelpTextRequired.js delete mode 100644 spectral/functions/checkNumberMaximumLimit.js delete mode 100644 spectral/functions/checkNumberMinimumLimit.js delete mode 100644 spectral/functions/checkOperationIdRequired.js delete mode 100644 spectral/functions/checkOrderingParameter.js delete mode 100644 spectral/functions/checkPaginationResponseSchema.js delete mode 100644 spectral/functions/checkPathEndsWithSForList.js delete mode 100644 spectral/functions/checkPathParametersComplete.js delete mode 100644 spectral/functions/checkQueryStringsPagination.js delete mode 100644 spectral/functions/checkRequestBodyRequired.js delete mode 100644 spectral/functions/checkResourceIDUsage.js delete mode 100644 spectral/functions/checkSchemaNamingConvention.js delete mode 100644 spectral/functions/checkSearchParameter.js delete mode 100644 spectral/functions/checkServersRequired.js delete mode 100644 spectral/functions/checkStateDeletedResource.js delete mode 100644 spectral/functions/checkStatusCodesByMethod.js delete mode 100644 spectral/functions/checkStringMaxLength.js delete mode 100644 spectral/functions/checkStringMinLength.js delete mode 100644 spectral/functions/checkStringPattern.js delete mode 100644 spectral/functions/checkSummaryDescriptionRequired.js delete mode 100644 spectral/spectral.yaml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 index f1931757..00000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: OpenAPI Spectral Linter - -on: - pull_request: - branches: - - main - push: - branches: - - main - -jobs: - spectral-v4: - name: Lint OpenAPI v4 - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Install Spectral - run: npm install -g @stoplight/spectral-cli - - - name: Run Spectral Lint on OpenAPI v4 - run: spectral lint openapi.yaml --ruleset spectral/spectral.yaml --verbose diff --git a/.github/workflows/sync-v4.yml b/.github/workflows/sync-v4.yml index 30b3cdcf..bf3dac8e 100644 --- a/.github/workflows/sync-v4.yml +++ b/.github/workflows/sync-v4.yml @@ -38,12 +38,6 @@ jobs: cp v4-source/openapi.yaml openapi.yaml echo "Synced openapi.yaml from azionapi-v4-openapi" - - name: Sync spectral rules - run: | - rm -rf spectral - cp -r v4-source/spectral spectral - echo "Synced spectral rules from azionapi-v4-openapi" - - name: Cleanup run: rm -rf v4-source @@ -61,6 +55,6 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git add openapi.yaml spectral/ - git commit -m "chore: sync openapi.yaml and spectral from azionapi-v4-openapi" + git add openapi.yaml + git commit -m "chore: sync openapi.yaml from azionapi-v4-openapi" git push diff --git a/README.md b/README.md index bf0ce009..7365fdd5 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,13 @@ Legacy API specifications are available in the `v3/` directory for backward comp # View with Swagger UI npx @redocly/cli preview-docs openapi.yaml -# Validate with Spectral -npx @stoplight/spectral-cli lint openapi.yaml --ruleset spectral/spectral.yaml +# Validate OpenAPI +npx @redocly/cli lint openapi.yaml # Generate client SDKs openapi-generator-cli generate -i openapi.yaml -g python -o ./client ``` -## 📚 Documentation - -- **[Spectral Validation Rules](spectral/README.md)** - Comprehensive guide to all custom Spectral linting rules - ## 🔄 Synchronization The `openapi.yaml` file is automatically synchronized from [azionapi-v4-openapi](https://github.com/aziontech/azionapi-v4-openapi) when changes are merged to the main branch. diff --git a/spectral/README.md b/spectral/README.md deleted file mode 100644 index 924d2959..00000000 --- a/spectral/README.md +++ /dev/null @@ -1,370 +0,0 @@ -#### Basic Spectral Guide for OpenAPI - -This is a quick guide on how to use Spectral, a tool for validating and linting OpenAPI specifications. It helps ensure consistency, quality, and compliance within OpenAPI definitions. - -For a complete guide, refer to [here](https://docs.google.com/document/d/1nTX3SIufCRktpixwGXwwLuSM_IiM9467btCbQUQTNYI/edit). - -## Basic Usage - -### Installation - -If you haven't installed Spectral yet, use npm (Node Package Manager) with the following command: -``` -npm install -g @stoplight/spectral -``` -Execution -To run Spectral on an OpenAPI file (e.g., a YAML file), use the following command: -``` -spectral lint your_openapi_file.yaml -``` -This command will analyze your OpenAPI file and return any problems or errors found, including details on what's incorrect and where. - -## Customizing Rules - -Spectral allows rule customization to fit your validation needs. You can create or use custom rules by defining them in a .spectral.yaml file in your project. - -For more details and specific examples, refer to the complete document. - ---- - -## Azion Custom Rules - -This document describes all custom Spectral rules configured in `spectral/spectral.yaml`. - -**Total Rules**: 62 (26 original + 36 new) -**Custom Functions**: 17 (all implemented) - ---- - -## Rules by Category - -### 1. Operation IDs - -#### `azion-operation-id-required` -- **Severity**: error -- **Description**: Every operation must have an explicit operationId - -#### `azion-operation-id-pattern` -- **Severity**: error -- **Description**: operationId must follow snake_case convention -- **Pattern**: `^[a-z][a-z0-9]*(_[a-z0-9]+)*$` - -#### `azion-operation-id-crud-convention` -- **Severity**: warn -- **Description**: CRUD operations should follow naming conventions (list_*, get_*, create_*, update_*, patch_*, delete_*) - -### 2. Path Parameters - -#### `azion-path-parameter-type-required` -- **Severity**: error -- **Description**: Path parameters must have explicit type - -#### `azion-path-parameter-required` -- **Severity**: error -- **Description**: Path parameters must be marked as required - -#### `azion-path-parameter-description-required` -- **Severity**: error -- **Description**: Path parameters must have a description - -### 3. Query Parameters - Pagination - -#### `azion-mandatory-query-string-pagination` -- **Severity**: error -- **Description**: Validates that list endpoints implement pagination correctly with query strings -- **Function**: `checkQueryStringsPagination` - -### 4. Query Parameters - Filters - -#### `azion-query-param-fields-rule` -- **Severity**: error -- **Description**: Validates correct usage of the `fields` parameter in query strings -- **Function**: `checkFieldsQueryParam` - -#### `azion-filter-description-required` -- **Severity**: error -- **Description**: Query parameters (filters) must have a description - -### 5. Search and Ordering - -#### `azion-match-search-parameter` -- **Severity**: error -- **Description**: Validates that the `search` query parameter follows the expected pattern -- **Function**: `checkSearchParameter` - -#### `azion-match-ordering-parameter` -- **Severity**: error -- **Description**: Validates that the `ordering` query parameter follows the expected pattern -- **Function**: `checkOrderingParameter` - -#### `azion-ordering-enum-required` -- **Severity**: warn -- **Description**: Ordering parameter must have enum with valid field names - -### 6. Status Codes - -#### `azion-sps-invalid-status-code` -- **Severity**: error -- **Description**: Validates that only allowed status codes are used -- **Allowed values**: 200, 201, 202, 204, 400, 401, 403, 404, 405, 406, 409, 422, 429, 500 - -#### `azion-mandatory-status-codes` -- **Severity**: error -- **Description**: Validates that endpoints return appropriate mandatory status codes -- **Function**: `checkEndpointStatusCodes` - -#### `azion-delete-response-codes` -- **Severity**: error -- **Description**: DELETE operations must return 204 or 202, and must not return 200 -- **Validation**: Requires 204 or 202, prohibits 200 - -#### `azion-204-no-response-body` -- **Severity**: error -- **Description**: Responses with status 204 must not have a response body - -#### `azion-get-response-200` -- **Severity**: error -- **Description**: GET operations must return 200 status code - -#### `azion-post-response-codes` -- **Severity**: error -- **Description**: POST operations must return 201 (sync) or 202 (async) - -#### `azion-put-response-200` -- **Severity**: error -- **Description**: PUT operations must return 200 status code - -#### `azion-patch-response-codes` -- **Severity**: error -- **Description**: PATCH operations must return 200 (sync) or 202 (async) - -### 7. Error Responses - JSON:API Format - -#### `azion-has-key-error-response` -- **Severity**: error -- **Description**: Error responses must contain the `detail` property - -#### `azion-match-type-error-response` -- **Severity**: error -- **Description**: The `detail` property in error responses must be of type string - -#### `azion-auth-error-responses-required` -- **Severity**: error -- **Description**: Endpoints with security must document 401 and 403 error responses - -### 8. Async Operations (202 Accepted) - -#### `azion-async-operation-id-required` -- **Severity**: warn -- **Description**: Async operation response must have operation_id field - -#### `azion-async-status-url-required` -- **Severity**: warn -- **Description**: Async operation response must have status_url field - -### 9. Schemas - Naming Conventions - -#### `azion-boolean-naming-convention` -- **Severity**: error -- **Description**: Boolean property names must not start with the 'is' prefix -- **Pattern**: Must not start with `is[A-Z]` - -### 10. Constraints - -#### `azion-number-minimum-limit-rule` -- **Severity**: error -- **Description**: Validates minimum limits for numeric properties -- **Function**: `checkNumberMinimumLimit` - -#### `azion-number-maximum-limit-rule` -- **Severity**: error -- **Description**: Validates maximum limits for numeric properties -- **Function**: `checkNumberMaximumLimit` - -#### `azion-string-minlength-properties-rule` -- **Severity**: error -- **Description**: Validates minimum length for string properties -- **Function**: `checkStringMinLength` - -#### `azion-string-maxlength-properties-rule` -- **Severity**: error -- **Description**: Validates maximum length for string properties -- **Function**: `checkStringMaxLength` - -#### `azion-string-pattern-properties-rule` -- **Severity**: error -- **Description**: Validates regex patterns for string properties -- **Function**: `checkStringPattern` - -#### `azion-date-time-format-rule` -- **Severity**: error -- **Description**: Validates that `last_modified` properties follow ISO 8601 format -- **Pattern**: `^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$` - -### 11. Descriptions - -#### `azion-operation-description-required` -- **Severity**: error -- **Description**: Operations must have a detailed description - -#### `azion-operation-summary-length` -- **Severity**: warn -- **Description**: Operation summary should be concise (max 100 characters) - -#### `azion-field-description-required` -- **Severity**: error -- **Description**: Schema properties must have descriptions - -#### `azion-schema-description-required` -- **Severity**: error -- **Description**: Schemas must have descriptions - -### 12. Examples - -#### `azion-request-examples-required` -- **Severity**: warn -- **Description**: POST and PUT operations should have request examples - -### 13. Content-Type - -#### `azion-match-content-type-response` -- **Severity**: error -- **Description**: Validates that response content-type is appropriate -- **Function**: `checkContentTypeResponse` - -### 14. Path Naming - -#### `azion-sps-paths-valid-uri` -- **Severity**: warn -- **Description**: Validates that path URIs do not contain invalid patterns -- **Pattern (notMatch)**: `api|v4|^[^/]*\/[^/]*$|/$` - -#### `azion-endpoint-ddd-and-snake` -- **Severity**: error -- **Description**: Endpoints must follow DDD and snake_case convention -- **Pattern**: `^(/[a-z][a-z0-9]+(_[a-z][a-z0-9]+)*){2}($|/({[a-z][a-z0-9]+[a-zA-Z0-9]*}|[a-z][a-z0-9]+(_[a-z0-9]+)*)+)+` - -#### `azion-path-naming-convention` -- **Severity**: warn -- **Description**: Validates path naming convention (plural for list endpoints) -- **Function**: `checkPathEndsWithSForList` - -#### `azion-endpoint-uri-method-validation` -- **Severity**: error -- **Description**: Endpoint URIs must not contain HTTP method names -- **Pattern (notMatch)**: `/.*(GET|POST|PUT|PATCH|DELETE).*/i` - -#### `azion-validate-resource-id-usage` -- **Severity**: error -- **Description**: Validates correct usage of resource IDs in paths -- **Function**: `checkResourceIDUsage` - -### 15. Authorization - -#### `azion-authorization-header` -- **Severity**: error -- **Description**: Endpoints must have security configuration (security) - -#### `azion-security-scheme-bearer` -- **Severity**: error -- **Description**: Security schemes should use Bearer token with JWT - -### 16. Request Body - -#### `azion-request-body-required` -- **Severity**: error -- **Description**: POST, PUT, and PATCH operations must have requestBody - -#### `azion-request-body-required-flag` -- **Severity**: warn -- **Description**: requestBody should be marked as required - -### 17. Headers - -#### `azion-header-disallowed` -- **Severity**: error -- **Description**: Authorization, Content-Type, and Accept headers must not be explicitly defined as parameters -- **Pattern (notMatch)**: `/^(authorization|content-type|accept)$/i` - -### 18. Metadata Global - -#### `azion-service-name-pattern` -- **Severity**: error -- **Description**: Service title (info.title) must end with `-api` -- **Pattern**: `.*-api$` - -#### `azion-version-semantic` -- **Severity**: error -- **Description**: API version should follow semantic versioning -- **Pattern**: `^\\d+\\.\\d+\\.\\d+$` - -### 19. Response Bodies - -#### `azion-validate-count-and-results` -- **Severity**: error -- **Description**: Validates paginated response structure with `count` (integer) and `results` (array) - -#### `azion-data-object-type-rule` -- **Severity**: error -- **Description**: Validates the type of the `data` object in responses -- **Function**: `checkDataObjectType` - -#### `azion-data-keys-presence-rule` -- **Severity**: error -- **Description**: Validates the presence of required keys in the `data` object -- **Function**: `checkDataKeysPresence` - -### 20. Deleted Resources - -#### `azion-deleted-resource-state-validation` -- **Severity**: error -- **Description**: Validates the state of deleted resources -- **Function**: `checkStateDeletedResource` - -### 21. Business Naming - -#### `azion-no-technical-schema-names` -- **Severity**: error -- **Description**: Schema names must use business terminology, not technical implementation details -- **Examples**: Use 'Database' instead of 'OpenAPISchema', 'PaginatedDatabaseList' instead of 'PaginatedOpenAPISchemaList' -- **Pattern (notMatch)**: `^.*(OpenAPI|Schema(?!Enum$)|DTO|Entity|Model(?!$)|RESTful|Swagger).*$` - -### 22. Extensible Enums - -#### `x-extensible-enum-required` -- **Severity**: error -- **Description**: Enums must include x-extensible-enum to ensure forward compatibility -- **Purpose**: Allows clients to handle unknown enum values gracefully without breaking - -### 23. Created_at Field - -#### `check-created-at-field` -- **Severity**: error -- **Description**: Ensures that POST creation endpoints include a 'created_at' field in their response schema for proper resource tracking and auditing -- **Function**: `checkCreatedAtField` -- **Behavior**: - - Reports ERROR when the field is missing entirely - - Reports WARNING when a variation like 'created' is used instead of the preferred 'created_at' -- **Detection**: Identifies creation endpoints by: - - Response status `201` (Created) - - `operationId` containing `create` or `new` - - `summary`/`description` with creation patterns like "Create a", "Create new" -- **Exceptions**: Supports allowlisting by `operationId` or `path` in the `EXCEPTIONS` object in the function - ---- - -## Summary - -All rules are documented organized into 23 categories covering: -- Operation IDs and naming conventions -- Path and query parameters -- Status codes and error handling -- Async operations -- Schema validation and naming -- Content-type and examples -- Security and authorization -- Business domain naming -- Extensible enums for forward compatibility -- Created_at field validation for auditing - -For implementation details, see `spectral/spectral.yaml`. diff --git a/spectral/functions/checkAsyncOperationSchema.js b/spectral/functions/checkAsyncOperationSchema.js deleted file mode 100644 index ba2f226a..00000000 --- a/spectral/functions/checkAsyncOperationSchema.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = (response, _opts, paths) => { - const errors = []; - - if (!response.content) { - errors.push({ - message: "202 response must have content with AsyncOperationResponse schema" - }); - return errors; - } - - const schema = response.content['application/json']?.schema; - if (!schema || !schema.properties) { - errors.push({ - message: "202 response is missing schema" - }); - return errors; - } - - const requiredFields = ['operation_id', 'status', 'status_url']; - - requiredFields.forEach(field => { - if (!schema.properties[field]) { - errors.push({ - message: `202 response must have '${field}' field` - }); - } - }); - - // Validar enum do status - if (schema.properties.status && !schema.properties.status.enum) { - errors.push({ - message: "'status' field must have enum: ['pending', 'running', 'completed', 'failed']" - }); - } - - return errors; -}; diff --git a/spectral/functions/checkAuthenticationScheme.js b/spectral/functions/checkAuthenticationScheme.js deleted file mode 100644 index c8500c8c..00000000 --- a/spectral/functions/checkAuthenticationScheme.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = (securitySchemes, _opts, paths) => { - const errors = []; - - if (!securitySchemes || Object.keys(securitySchemes).length === 0) { - errors.push({ - message: "API must define at least one security scheme in components.securitySchemes" - }); - return errors; - } - - // Verificar se tem Bearer token - let hasBearerAuth = false; - - Object.entries(securitySchemes).forEach(([name, scheme]) => { - if (scheme.type === 'http' && scheme.scheme === 'bearer') { - hasBearerAuth = true; - - // Validar formato JWT - if (scheme.bearerFormat && scheme.bearerFormat !== 'JWT') { - errors.push({ - message: `Security scheme '${name}' should use bearerFormat: 'JWT'` - }); - } - } - }); - - if (!hasBearerAuth) { - errors.push({ - message: "API should include a Bearer token authentication scheme (type: http, scheme: bearer)" - }); - } - - return errors; -}; diff --git a/spectral/functions/checkContactRequired.js b/spectral/functions/checkContactRequired.js deleted file mode 100644 index 3f57656b..00000000 --- a/spectral/functions/checkContactRequired.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = (info, _opts, paths) => { - const errors = []; - - if (!info.contact) { - errors.push({ - message: "API must have 'info.contact' with support information" - }); - return errors; - } - - // Validar campos obrigatórios do contact - if (!info.contact.name) { - errors.push({ - message: "info.contact must have 'name' field" - }); - } - - if (!info.contact.email) { - errors.push({ - message: "info.contact must have 'email' field" - }); - } - - if (!info.contact.url) { - errors.push({ - message: "info.contact should have 'url' field pointing to documentation or support" - }); - } - - return errors; -}; diff --git a/spectral/functions/checkContentTypeResponse.js b/spectral/functions/checkContentTypeResponse.js deleted file mode 100644 index 09c39911..00000000 --- a/spectral/functions/checkContentTypeResponse.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = function (context) { - const errors = []; - const mimeTypeList = Object.getOwnPropertyNames(context); - for (const mimeType of mimeTypeList){ - if (!mimeType.match(/^(application\/(json|octet-stream))$/)) { - errors.push({ - message: `The content type of the "${mimeType}" response must be "application/json" or "application/octet-stream".`, - severity: 'error', - }); - } - } - return errors; - }; diff --git a/spectral/functions/checkCreatedAtField.js b/spectral/functions/checkCreatedAtField.js deleted file mode 100644 index d34f542b..00000000 --- a/spectral/functions/checkCreatedAtField.js +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Spectral custom function to validate that POST creation endpoints - * include a 'created_at' field in their response schema. - * - * Rule: If operationId contains 'create', 'clone', or 'request', - * the response must include 'created_at' field. - * - * EXCEPTIONS contains endpoints that match the pattern but: - * - Don't have created_at (e.g., create_bucket) - * - Have a non-standard field name that needs to be fixed (e.g., 'created') - */ - -const PREFERRED_FIELD = 'created_at'; -const CREATION_PATTERNS = ['create', 'clone', 'request']; - -// Endpoints that match creation pattern but don't have created_at -const EXCEPTIONS = [ - // Don't have created_at - 'create_application', - 'clone_application', - 'create_waf', - 'clone_waf', - 'create_favorite', - 'CreateGrant', - 'create_row', - 'create_dns_record', - 'create_dns_zone', - 'create_function', - 'create_purge_request', - 'create_database', - 'create_bucket', - 'create_object_key', - 'copy_object_key', - 'create_waf_exception', - 'create_totp_device', - 'create_descendant_account', - - // AI/LLM endpoints - don't have created_at - 'create chat thread', - 'create document', - 'create knowledge base', - 'create message', - 'create tool', - - // Has timestamp data but with non-standard name (needs fixing) - 'create_data_stream', // uses 'created' instead of 'created_at' -]; - -export default (root, options, context) => { - const errors = []; - const endpoints = root.paths || {}; - const schemas = root.components?.schemas || {}; - - for (const path in endpoints) { - const methods = endpoints[path]; - - for (const method in methods) { - if (method !== 'post') continue; - - const operation = methods[method]; - const operationId = operation.operationId || ''; - const opIdLower = operationId.toLowerCase(); - - // Check if operationId contains create, clone, or request - const isCreationEndpoint = CREATION_PATTERNS.some(pattern => - opIdLower.includes(pattern) - ); - - if (!isCreationEndpoint) continue; - - // Check if it's an exception - if (isException(operationId)) continue; - - // Get response schema (201 or 200) - const responseSchema = getResponseSchema(operation, schemas); - if (!responseSchema) continue; - - // Check created_at field - const hasField = checkCreatedAtField(responseSchema, schemas); - - if (!hasField) { - errors.push({ - message: `POST creation endpoint ${path} (${operationId}) must include '${PREFERRED_FIELD}' field in response schema for auditing purposes`, - path: ['paths', path, method], - severity: 0 // error - }); - } - } - } - - return errors; -}; - -/** - * Check if the endpoint is in the exceptions list - */ -function isException(operationId) { - const opIdLower = operationId.toLowerCase(); - return EXCEPTIONS.some(exception => - opIdLower === exception.toLowerCase() - ); -} - -/** - * Get the response schema from the endpoint (201 or 200) - */ -function getResponseSchema(operation, schemas) { - const response = operation.responses?.['201'] || operation.responses?.['200']; - if (!response) return null; - - const content = response.content; - if (!content) return null; - - const contentTypes = Object.keys(content); - for (const ct of contentTypes) { - if (ct.includes('application/json') && content[ct]?.schema) { - return resolveSchema(content[ct].schema, schemas); - } - } - - return null; -} - -/** - * Resolve $ref references and combine allOf/oneOf - */ -function resolveSchema(schema, allSchemas) { - if (!schema) return null; - - if (schema.$ref) { - const refName = schema.$ref.split('/').pop(); - return allSchemas[refName] || schema; - } - - if (schema.allOf) { - const combined = { properties: {}, required: [] }; - for (const s of schema.allOf) { - const resolved = resolveSchema(s, allSchemas); - if (resolved?.properties) Object.assign(combined.properties, resolved.properties); - if (resolved?.required) combined.required.push(...resolved.required); - } - return combined; - } - - if (schema.oneOf) { - const combined = { properties: {}, required: [] }; - for (const s of schema.oneOf) { - const resolved = resolveSchema(s, allSchemas); - if (resolved?.properties) Object.assign(combined.properties, resolved.properties); - if (resolved?.required) combined.required.push(...resolved.required); - } - return combined; - } - - return schema; -} - -/** - * Check if the schema contains created_at - */ -function checkCreatedAtField(schema, allSchemas) { - if (!schema) return false; - - const resolved = resolveSchema(schema, allSchemas); - const properties = resolved?.properties || {}; - - if (properties[PREFERRED_FIELD]) { - return true; - } - - // Check nested data structures - const nestedKeys = ['results', 'data', 'item', 'result']; - for (const key of nestedKeys) { - if (properties[key]) { - const nestedSchema = resolveSchema(properties[key], allSchemas); - if (checkCreatedAtField(nestedSchema, allSchemas)) return true; - } - } - - return false; -} diff --git a/spectral/functions/checkDataKeysPresence.js b/spectral/functions/checkDataKeysPresence.js deleted file mode 100644 index 705882a0..00000000 --- a/spectral/functions/checkDataKeysPresence.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = function checkDataKeysPresence(context) { - const schema = context.get('content.application/json.schema'); - if (!schema) { - return; - } - - const dataSchema = schema.properties.data; - if (!dataSchema) { - return; - } - - const idKey = dataSchema.properties.id; - const nameKey = dataSchema.properties.name; - - if (!idKey || !nameKey) { - context.message = 'The "data" object should contain "id" and "name" properties.'; - context.path = ['responses', '[*]', 'content', 'application/json', 'schema', 'properties', 'data']; - } - }; diff --git a/spectral/functions/checkDataObjectType.js b/spectral/functions/checkDataObjectType.js deleted file mode 100644 index 052180b5..00000000 --- a/spectral/functions/checkDataObjectType.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = function checkDataObjectType(context) { - const schema = context.get('content.application/json.schema'); - if (!schema) { - return; - } - - const dataSchema = schema.properties.data; - if (!dataSchema) { - context.message = 'The "data" object is missing from the response schema.'; - context.path = ['responses', '[*]', 'content', 'application/json', 'schema', 'properties', 'data']; - return; - } - - if (dataSchema.type !== 'object') { - context.message = 'The "data" object should have a type of "object".'; - context.path = ['responses', '[*]', 'content', 'application/json', 'schema', 'properties', 'data']; - } - }; diff --git a/spectral/functions/checkDiscriminatorPresent.js b/spectral/functions/checkDiscriminatorPresent.js deleted file mode 100644 index 064c39d1..00000000 --- a/spectral/functions/checkDiscriminatorPresent.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = (schema, _opts, paths) => { - const errors = []; - - // Verificar se é um schema polimórfico (tem oneOf, anyOf, ou allOf) - const isPolymorphic = schema.oneOf || schema.anyOf || schema.allOf; - - if (!isPolymorphic) { - return []; // Não é polimórfico - } - - // Schemas polimórficos devem ter discriminator - if (!schema.discriminator) { - const schemaName = paths.target[paths.target.length - 1]; - errors.push({ - message: `Polymorphic schema '${schemaName}' should have 'discriminator' for better SDK generation` - }); - } else { - // Validar estrutura do discriminator - if (!schema.discriminator.propertyName) { - errors.push({ - message: "Discriminator must have 'propertyName' field" - }); - } - - // Recomendar mapping - if (!schema.discriminator.mapping) { - errors.push({ - message: "Discriminator should include 'mapping' for explicit type references" - }); - } - } - - return errors; -}; diff --git a/spectral/functions/checkEndpointStatusCodes.js b/spectral/functions/checkEndpointStatusCodes.js deleted file mode 100644 index 23ea7985..00000000 --- a/spectral/functions/checkEndpointStatusCodes.js +++ /dev/null @@ -1,26 +0,0 @@ -export default path => { - const errors = []; - - const requiredStatusCodes = { - "post": ["201", "400", "401", "403", "404", "429"], - "get": ["200", "400", "401", "403", "404", "429"], - "patch": ["200", "400", "401", "403", "404", "429"], - "put": ["200", "400", "401", "403", "404", "429"], - "delete": ["202", "200", "400", "401", "403", "404", "429"] - }; - - for (const method in path) { - if (requiredStatusCodes[method] && path[method].responses) { - const statusCodes = Object.keys(path[method].responses); - const missingStatusCodes = requiredStatusCodes[method].filter(code => !statusCodes.includes(code)); - - if (missingStatusCodes.length > 0) { - errors.push({ - message: `Missing status codes ${missingStatusCodes.join(', ')} for ${method.toUpperCase()} endpoint` - }); - } - } - } - - return errors; -} diff --git a/spectral/functions/checkErrorResponseJsonApi.js b/spectral/functions/checkErrorResponseJsonApi.js deleted file mode 100644 index 5ed6980d..00000000 --- a/spectral/functions/checkErrorResponseJsonApi.js +++ /dev/null @@ -1,36 +0,0 @@ -module.exports = (response, _opts, paths) => { - const errors = []; - - if (!response.content) { - return []; // Sem content (ex: 204) - } - - const contentTypes = Object.keys(response.content); - const jsonApiContent = response.content['application/vnd.api+json'] || - response.content['application/json']; - - if (!jsonApiContent) { - return []; // Não é JSON - } - - const schema = jsonApiContent.schema; - if (!schema || !schema.properties) { - errors.push({ - message: "Error response is missing schema definition" - }); - return errors; - } - - // Verificar estrutura JSON:API - if (!schema.properties.errors) { - errors.push({ - message: "Error response must have 'errors' array following JSON:API format" - }); - } else if (schema.properties.errors.type !== 'array') { - errors.push({ - message: "'errors' must be an array" - }); - } - - return errors; -}; diff --git a/spectral/functions/checkExamplesRequired.js b/spectral/functions/checkExamplesRequired.js deleted file mode 100644 index 6b925d5b..00000000 --- a/spectral/functions/checkExamplesRequired.js +++ /dev/null @@ -1,21 +0,0 @@ -module.exports = (schema, _opts, paths) => { - const errors = []; - - // Verificar se é um schema de response - const pathParts = paths.target; - const isResponseSchema = pathParts.includes('responses'); - - if (!isResponseSchema) { - return []; // Só validar schemas de response - } - - // Verificar se tem examples ou example - if (!schema.example && !schema.examples) { - const schemaName = pathParts[pathParts.length - 1]; - errors.push({ - message: `Response schema should include 'example' or 'examples' for better documentation` - }); - } - - return errors; -}; diff --git a/spectral/functions/checkFieldsQueryParam.js b/spectral/functions/checkFieldsQueryParam.js deleted file mode 100644 index a1727361..00000000 --- a/spectral/functions/checkFieldsQueryParam.js +++ /dev/null @@ -1,28 +0,0 @@ -export default paths => { - const errors = []; - - for (const currentPath in paths) { - for (const httpVerb in paths[currentPath]) { - if (httpVerb != "get") { - continue; - } - - const endpointOpenAPISections = Object.keys(paths[currentPath][httpVerb]) - if (endpointOpenAPISections.includes('parameters') === false) { - errors.push({message: `Missing parameters section`}); - break; - } - - const parameters = paths[currentPath][httpVerb]['parameters'] - const fieldsParamenter = parameters.find(currentParameter => currentParameter.name == "fields"); - if (!fieldsParamenter) { - errors.push({ - message: `Missing query string "fields" on ${httpVerb}`, - path: ['paths', currentPath], - }); - } - } - } - - return errors; -} diff --git a/spectral/functions/checkFilterDocumentation.js b/spectral/functions/checkFilterDocumentation.js deleted file mode 100644 index c5e7be2c..00000000 --- a/spectral/functions/checkFilterDocumentation.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = (operation, _opts, paths) => { - const errors = []; - const parameters = operation.parameters || []; - - // Verificar se é um list endpoint (GET sem path params) - const pathString = paths.target.join('.'); - const hasPathParams = /\{[^}]+\}/.test(pathString); - - if (hasPathParams) { - return []; // Não é list endpoint - } - - const paramNames = parameters.map(p => p.name); - const commonFilters = ['page', 'page_size', 'ordering']; - - commonFilters.forEach(filter => { - if (!paramNames.includes(filter)) { - errors.push({ - message: `List endpoint should document '${filter}' parameter` - }); - } - }); - - return errors; -}; diff --git a/spectral/functions/checkHelpTextRequired.js b/spectral/functions/checkHelpTextRequired.js deleted file mode 100644 index bda14199..00000000 --- a/spectral/functions/checkHelpTextRequired.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = (property, _opts, paths) => { - const errors = []; - - if (!property.description || property.description.trim() === '') { - const propertyName = paths.target[paths.target.length - 1]; - errors.push({ - message: `Property '${propertyName}' is missing 'description'` - }); - } - - return errors; -}; diff --git a/spectral/functions/checkNumberMaximumLimit.js b/spectral/functions/checkNumberMaximumLimit.js deleted file mode 100644 index d43b6027..00000000 --- a/spectral/functions/checkNumberMaximumLimit.js +++ /dev/null @@ -1,13 +0,0 @@ -export default property => { - const errors = []; - - if ((property.type === 'number' || property.type === 'integer') && !property.nullable && !property.readOnly && !property.enum) { - if (property.maximum === undefined) { - errors.push({ - message: `Missing maximum limit for numeric property` - }); - } - } - - return errors; -} \ No newline at end of file diff --git a/spectral/functions/checkNumberMinimumLimit.js b/spectral/functions/checkNumberMinimumLimit.js deleted file mode 100644 index a3a26645..00000000 --- a/spectral/functions/checkNumberMinimumLimit.js +++ /dev/null @@ -1,13 +0,0 @@ -export default property => { - const errors = []; - - if ((property.type === 'number' || property.type === 'integer') && !property.nullable && !property.readOnly && !property.enum) { - if (property.minimum === undefined) { - errors.push({ - message: `Missing minimum limit for numeric property` - }); - } - } - - return errors; -} \ No newline at end of file diff --git a/spectral/functions/checkOperationIdRequired.js b/spectral/functions/checkOperationIdRequired.js deleted file mode 100644 index 1513b225..00000000 --- a/spectral/functions/checkOperationIdRequired.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = (operation, _opts, paths) => { - const errors = []; - - if (!operation.operationId || operation.operationId.trim() === '') { - errors.push({ - message: `Operation is missing required 'operationId'` - }); - } - - return errors; -}; diff --git a/spectral/functions/checkOrderingParameter.js b/spectral/functions/checkOrderingParameter.js deleted file mode 100644 index 0d80a417..00000000 --- a/spectral/functions/checkOrderingParameter.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = function checkOrderingParameter(context) { - const parameter = context.get('value'); - const pattern = /^(?:(?:-|)([a-zA-Z0-9_]+)(?:,)?)+$/; - - if (!pattern.test(parameter)) { - return { - message: `The ordering parameter must follow the pattern '-,,...'. Each field must be a valid field name.`, - path: context.path - }; - } -}; diff --git a/spectral/functions/checkPaginationResponseSchema.js b/spectral/functions/checkPaginationResponseSchema.js deleted file mode 100644 index fc2b2ea7..00000000 --- a/spectral/functions/checkPaginationResponseSchema.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = (schema, _opts, paths) => { - const errors = []; - const requiredFields = ['count', 'total_pages', 'page', 'page_size', 'next', 'previous', 'results']; - - if (!schema.properties) { - return []; // Não é um schema de objeto - } - - // Verificar se parece ser uma resposta paginada - // (se tem 'results', assume que é paginada) - if (!schema.properties.results) { - return []; // Não é paginada - } - - requiredFields.forEach(field => { - if (!schema.properties[field]) { - errors.push({ - message: `Paginated response is missing required field: '${field}'` - }); - } - }); - - // Validar tipos - if (schema.properties.count && schema.properties.count.type !== 'integer') { - errors.push({ - message: "'count' must be of type 'integer'" - }); - } - - if (schema.properties.results && schema.properties.results.type !== 'array') { - errors.push({ - message: "'results' must be of type 'array'" - }); - } - - return errors; -}; diff --git a/spectral/functions/checkPathEndsWithSForList.js b/spectral/functions/checkPathEndsWithSForList.js deleted file mode 100644 index 5f6ac6d9..00000000 --- a/spectral/functions/checkPathEndsWithSForList.js +++ /dev/null @@ -1,19 +0,0 @@ -export default paths => { - const errors = []; - for (const pathName of Object.keys(paths)) { - const path = paths[pathName]; - for (const method in path) { - if (method === 'get' && - path[method]?.responses?.['200']?.content?.['application/json']?.schema?.properties?.results?.type - ) { - if (!pathName.endsWith('s')) { - errors.push({ - message: `GET method from ${pathName} returns a list, but its name is not plural`, - path: ['paths', pathName], - }); - } - } - } - } - return errors; -}; diff --git a/spectral/functions/checkPathParametersComplete.js b/spectral/functions/checkPathParametersComplete.js deleted file mode 100644 index bd101ceb..00000000 --- a/spectral/functions/checkPathParametersComplete.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = (pathItem, _opts, paths) => { - const errors = []; - const pathString = paths.target.join('.'); - - // Extrair parâmetros da URL: /workspace/{app_id}/{conn_id} - const urlParams = (pathString.match(/\{([^}]+)\}/g) || []) - .map(param => param.slice(1, -1)); // Remove {} - - if (urlParams.length === 0) { - return []; // Sem parâmetros na URL - } - - // Verificar cada operação (get, post, put, etc.) - const operations = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head']; - - operations.forEach(method => { - if (pathItem[method]) { - const operation = pathItem[method]; - const definedParams = (operation.parameters || []) - .filter(p => p.in === 'path') - .map(p => p.name); - - urlParams.forEach(urlParam => { - if (!definedParams.includes(urlParam)) { - errors.push({ - message: `Declared path parameter '${urlParam}' needs to be defined as a parameter in ${method.toUpperCase()} operation` - }); - } - }); - } - }); - - return errors; -}; diff --git a/spectral/functions/checkQueryStringsPagination.js b/spectral/functions/checkQueryStringsPagination.js deleted file mode 100644 index f665c332..00000000 --- a/spectral/functions/checkQueryStringsPagination.js +++ /dev/null @@ -1,45 +0,0 @@ -export default paths => { - const errors = []; - for (const currentPath in paths) { - // if the path accepts a parameter at the end of the URI (Ex: /domains/{id}), then it's NOT a GET/List - const lastChar = currentPath.slice(-1) - if (lastChar === '}') - continue; - for (const httpVerb in paths[currentPath]) { - // query strings for pagination and ordering data can only exist in GET List - if (httpVerb === 'get') { - const list = Object.keys(paths[currentPath][httpVerb]) - if (list.includes('parameters') === false) { - errors.push({message: `Missing parameters section`}); - break; - } - const parameters = paths[currentPath][httpVerb]['parameters'] - const parameterNameList = []; - for (const currentParameter of parameters) { - parameterNameList.push(currentParameter.name); - } - const expectedNames = ["page_size", "page", "ordering"]; - for (const currentName of expectedNames) { - if (parameterNameList.includes(currentName) === false) { - errors.push({ - message: `Missing query string ${currentName} on GET List`, - path: ['paths', currentPath], - }); - } else { - // Check the data type of the parameter - const parameter = parameters.find(p => p.name === currentName); - if (currentName === "page_size" || currentName === "page") { - if (parameter.schema.type !== 'integer') { - errors.push({ - message: `Query string parameter "${currentName}" must have type "integer" on GET List`, - path: ['paths', currentPath, 'parameters', parameter.name], - }); - } - } - } - } - } - } - } - return errors; -} diff --git a/spectral/functions/checkRequestBodyRequired.js b/spectral/functions/checkRequestBodyRequired.js deleted file mode 100644 index 444896ba..00000000 --- a/spectral/functions/checkRequestBodyRequired.js +++ /dev/null @@ -1,25 +0,0 @@ -module.exports = (operation, _opts, paths) => { - const errors = []; - const method = paths.target[paths.target.length - 1]; // 'get', 'post', etc. - - // POST, PUT, PATCH devem ter requestBody - const methodsRequiringBody = ['post', 'put', 'patch']; - - if (methodsRequiringBody.includes(method)) { - if (!operation.requestBody) { - errors.push({ - message: `${method.toUpperCase()} operation must have requestBody defined` - }); - } else if (!operation.requestBody.content) { - errors.push({ - message: `${method.toUpperCase()} operation requestBody must have content` - }); - } else if (!operation.requestBody.content['application/json']) { - errors.push({ - message: `${method.toUpperCase()} operation requestBody must include 'application/json' content type` - }); - } - } - - return errors; -}; diff --git a/spectral/functions/checkResourceIDUsage.js b/spectral/functions/checkResourceIDUsage.js deleted file mode 100644 index b2dbe287..00000000 --- a/spectral/functions/checkResourceIDUsage.js +++ /dev/null @@ -1,25 +0,0 @@ -// Define extract_id_from_path function -function extractIdFromPath(path) { - // Extract the last parameter from path and check if it looks like an ID - // Only match parameters that contain 'id' or 'uuid' in their name (case insensitive) - const match = path.match(/\/{([^}]*(?:id|uuid)[^}]*)}$/i); - return match ? match[1] : null; -} - -module.exports = function checkResourceIDUsage(value) { - const errors = []; - const paths = Object.getOwnPropertyNames(value) - for (const path of paths) { - const methods = Object.getOwnPropertyNames(value[path]); - const extractedId = extractIdFromPath(path) - - for (const method of methods) { - if (method === 'post' && extractedId) { - errors.push({ - message: `Resource ID should not be used for POST method. ID found in path: ${path}`, - }); - } - } - }; - return errors; -} diff --git a/spectral/functions/checkSchemaNamingConvention.js b/spectral/functions/checkSchemaNamingConvention.js deleted file mode 100644 index 1133cc40..00000000 --- a/spectral/functions/checkSchemaNamingConvention.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = (schema, _opts, paths) => { - const errors = []; - const schemaName = paths.target[paths.target.length - 1]; - - // Padrões válidos - const patterns = [ - /^[A-Z][a-zA-Z0-9]+Request$/, // ApplicationRequest - /^[A-Z][a-zA-Z0-9]+Response$/, // AsyncOperationResponse - /^Paginated[A-Z][a-zA-Z0-9]+List$/, // PaginatedApplicationList - /^[A-Z][a-zA-Z0-9]+$/, // Application - /^JSONAPI[A-Z][a-zA-Z0-9]+$/, // JSONAPIErrorObject - ]; - - const isValid = patterns.some(pattern => pattern.test(schemaName)); - - if (!isValid) { - errors.push({ - message: `Schema name '${schemaName}' doesn't follow naming conventions (Resource, ResourceRequest, PaginatedResourceList, ResourceResponse)` - }); - } - - return errors; -}; diff --git a/spectral/functions/checkSearchParameter.js b/spectral/functions/checkSearchParameter.js deleted file mode 100644 index e6248db9..00000000 --- a/spectral/functions/checkSearchParameter.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = function (context) { - const parameter = context.get('value'); - - if (parameter.schema.type !== 'string') { - return { - message: 'The "search" parameter must be of type string.', - }; - } - - return null; -}; diff --git a/spectral/functions/checkServersRequired.js b/spectral/functions/checkServersRequired.js deleted file mode 100644 index 7f4a3f93..00000000 --- a/spectral/functions/checkServersRequired.js +++ /dev/null @@ -1,34 +0,0 @@ -module.exports = (openapi, _opts, paths) => { - const errors = []; - - if (!openapi.servers || openapi.servers.length === 0) { - errors.push({ - message: "API must define at least one server in 'servers' array" - }); - return errors; - } - - // Validar cada servidor - openapi.servers.forEach((server, index) => { - if (!server.url) { - errors.push({ - message: `Server at index ${index} must have 'url' field` - }); - } - - if (!server.description) { - errors.push({ - message: `Server at index ${index} should have 'description' field` - }); - } - - // Validar formato da URL - if (server.url && !server.url.startsWith('http') && !server.url.startsWith('{')) { - errors.push({ - message: `Server URL '${server.url}' should be a valid HTTP(S) URL or use variables` - }); - } - }); - - return errors; -}; diff --git a/spectral/functions/checkStateDeletedResource.js b/spectral/functions/checkStateDeletedResource.js deleted file mode 100644 index 3e009d95..00000000 --- a/spectral/functions/checkStateDeletedResource.js +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = function checkStateDeletedResource(context) { - const { path, value } = context; - if (path && path.includes("delete") && value.hasOwnProperty("content")) { - const content = value.content; - if (!content.hasOwnProperty("application/json") || !content["application/json"].schema.hasOwnProperty("properties") || !content["application/json"].schema.properties.hasOwnProperty("state")) { - return { - message: "DELETE response must contain the 'state' key in JSON schema", - path: path, - }; - } - const stateSchema = content["application/json"].schema.properties.state; - const allowedTypes = ["string", "integer", "boolean"]; - if (!allowedTypes.includes(stateSchema.type)) { - return { - message: "The data type of the 'state' field in the DELETE response must be 'string', 'integer', or 'boolean'", - path: path, - }; - } - } - }; diff --git a/spectral/functions/checkStatusCodesByMethod.js b/spectral/functions/checkStatusCodesByMethod.js deleted file mode 100644 index 926c0829..00000000 --- a/spectral/functions/checkStatusCodesByMethod.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = (operation, _opts, paths) => { - const errors = []; - const method = paths.target[paths.target.length - 1]; // 'get', 'post', etc. - const responses = operation.responses || {}; - const statusCodes = Object.keys(responses); - - const rules = { - get: ['200'], - post: ['201', '202'], - put: ['200'], - patch: ['200', '202'], - delete: ['200', '202'] - }; - - if (rules[method]) { - const requiredCodes = rules[method]; - const hasRequired = requiredCodes.some(code => statusCodes.includes(code)); - - if (!hasRequired) { - errors.push({ - message: `${method.toUpperCase()} operation must include one of: ${requiredCodes.join(', ')}` - }); - } - } - - return errors; -}; diff --git a/spectral/functions/checkStringMaxLength.js b/spectral/functions/checkStringMaxLength.js deleted file mode 100644 index a62bb631..00000000 --- a/spectral/functions/checkStringMaxLength.js +++ /dev/null @@ -1,13 +0,0 @@ -export default property => { - const errors = []; - - if (property.type === 'string' && !property.enum && !property.readOnly && !property.format) { - if (property.maxLength === undefined) { - errors.push({ - message: `Missing maxLength for string property` - }); - } - } - - return errors; -} \ No newline at end of file diff --git a/spectral/functions/checkStringMinLength.js b/spectral/functions/checkStringMinLength.js deleted file mode 100644 index 183cbfd9..00000000 --- a/spectral/functions/checkStringMinLength.js +++ /dev/null @@ -1,12 +0,0 @@ -export default property => { - const errors = []; - - if (property.type === 'string' && !property.enum && !property.nullable && !property.readOnly && !property.format) { - if (property.minLength === undefined) { - errors.push({ - message: `Missing minLength for string property` - }); - } - } - return errors; -} \ No newline at end of file diff --git a/spectral/functions/checkStringPattern.js b/spectral/functions/checkStringPattern.js deleted file mode 100644 index 22ba2775..00000000 --- a/spectral/functions/checkStringPattern.js +++ /dev/null @@ -1,13 +0,0 @@ -export default property => { - const errors = []; - - if (property.type === 'string' && !property.enum && !property.readOnly && !property.format) { - if (property.pattern === undefined) { - errors.push({ - message: `Missing pattern for string property` - }); - } - } - - return errors; -} \ No newline at end of file diff --git a/spectral/functions/checkSummaryDescriptionRequired.js b/spectral/functions/checkSummaryDescriptionRequired.js deleted file mode 100644 index 312b3767..00000000 --- a/spectral/functions/checkSummaryDescriptionRequired.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = (operation, _opts, paths) => { - const errors = []; - - if (!operation.summary || operation.summary.trim() === '') { - errors.push({ - message: "Operation is missing 'summary'" - }); - } - - if (!operation.description || operation.description.trim() === '') { - errors.push({ - message: "Operation is missing 'description'" - }); - } - - if (operation.summary && operation.summary.length > 100) { - errors.push({ - message: `Operation summary is too long (${operation.summary.length} chars). Keep it under 100 characters.` - }); - } - - return errors; -}; diff --git a/spectral/spectral.yaml b/spectral/spectral.yaml deleted file mode 100644 index 8464b07b..00000000 --- a/spectral/spectral.yaml +++ /dev/null @@ -1,604 +0,0 @@ -# Spectral OpenAPI Linting Rules - Azion Standards - -extends: spectral:oas - -# ============================================================================== -# CUSTOM FUNCTIONS -# ============================================================================== -functions: - - ./checkEndpointStatusCodes - - ./checkQueryStringsPagination - - ./checkNumberMinimumLimit - - ./checkNumberMaximumLimit - - ./checkStringMinLength - - ./checkStringMaxLength - - ./checkStringPattern - - ./checkFieldsQueryParam - - ./checkContentTypeResponse - - ./checkStateDeletedResource - - ./checkSearchParameter - - ./checkOrderingParameter - - ./checkResourceIDUsage - - ./checkPathEndsWithSForList - - ./checkDataObjectType - - ./checkDataKeysPresence - - ./checkCreatedAtField - -rules: - # ============================================================================ - # 1. OPERATION IDs - # ============================================================================ - azion-operation-id-required: - description: Every operation must have an explicit operationId - message: "Operation '{{property}}' is missing required 'operationId'" - severity: error - given: $.paths.*[get,put,post,patch,delete,options,head] - then: - field: operationId - function: truthy - - azion-operation-id-pattern: - message: "operationId must follow snake_case convention (e.g., 'list_applications', not 'listApplications')" - severity: error - given: $.paths.*[get,put,post,patch,delete,options,head] - then: - field: operationId - function: pattern - functionOptions: - match: '^[a-z][a-z0-9]*(_[a-z0-9]+)*$' - - azion-operation-id-crud-convention: - description: CRUD operations should follow naming conventions (list_*, get_*, create_*, update_*, patch_*, delete_*) - message: "operationId '{{value}}' doesn't follow CRUD convention for {{property}} method" - severity: warn - given: $.paths.*[get,post,put,patch,delete] - then: - field: operationId - function: pattern - functionOptions: - match: '^(list_|get_|create_|update_|patch_|delete_|clone_|purge_|activate_|deactivate_)[a-z][a-z0-9_]*$' - - # ============================================================================ - # 2. PATH PARAMETERS - # ============================================================================ - azion-path-parameter-type-required: - description: Path parameters must have explicit type - message: "Path parameter '{{property}}' is missing 'schema.type'" - severity: error - given: $.paths.*.*.parameters[?(@.in == 'path')] - then: - field: schema.type - function: truthy - - azion-path-parameter-required: - description: Path parameters must be marked as required - message: "Path parameter '{{property}}' must have 'required: true'" - severity: error - given: $.paths.*.*.parameters[?(@.in == 'path')] - then: - field: required - function: truthy - - azion-path-parameter-description-required: - description: Path parameters must have a description - message: "Path parameter '{{property}}' is missing 'description'" - severity: error - given: $.paths.*.*.parameters[?(@.in == 'path')] - then: - field: description - function: truthy - - # ============================================================================ - # 3. QUERY PARAMETERS - PAGINATION - # ============================================================================ - azion-mandatory-query-string-pagination: - message: "{{error}}" - severity: error - given: "$.paths[?(@property != '/api/schema')]" - then: - function: checkQueryStringsPagination - - # ============================================================================ - # 4. QUERY PARAMETERS - FILTERS - # ============================================================================ - azion-query-param-fields-rule: - message: "{{error}}" - severity: error - given: "$.paths" - then: - function: checkFieldsQueryParam - - azion-filter-description-required: - description: Query parameters (filters) must have a description - message: "Query parameter '{{property}}' is missing 'description'" - severity: error - given: "$.paths.*.*.parameters[?(@.in == 'query')]" - then: - field: description - function: truthy - - # ============================================================================ - # 5. SEARCH AND ORDERING - # ============================================================================ - azion-match-search-parameter: - message: "{{error}}" - severity: error - given: "$.paths.*.*.parameters[*][?(@.in == 'query' && @.name == 'search')]" - then: - function: checkSearchParameter - - azion-match-ordering-parameter: - message: "{{error}}" - severity: error - given: "$.paths.*.*.parameters[*][?(@.in == 'query' && @.name == 'ordering')]" - then: - function: checkOrderingParameter - - azion-ordering-enum-required: - description: Ordering parameter must have enum with valid field names - message: "Ordering parameter must define 'enum' with available fields (e.g., ['id', '-id', 'name', '-name'])" - severity: warn - given: "$.paths.*.*.parameters[*][?(@.in == 'query' && @.name == 'ordering')]" - then: - field: schema.enum - function: truthy - - # ============================================================================ - # 6. STATUS CODES - # ============================================================================ - azion-sps-invalid-status-code: - message: "Status code '{{property}}' is not in the allowed list" - severity: error - given: $.paths...responses.*~ - then: - function: enumeration - functionOptions: - values: ["200","201","202","204","400","401","403","404","405","406","409","422","429","500"] - - azion-mandatory-status-codes: - message: "{{error}}" - severity: error - given: "$.paths[?(@property != '/api/schema')].*" - then: - function: checkEndpointStatusCodes - - azion-delete-response-codes: - description: DELETE operations must return 200 or 202, not 204 - message: DELETE operations must return 200 (OK) or 202 (Accepted), not 204 - severity: error - given: $.paths[*].delete.responses - then: - function: schema - functionOptions: - schema: - anyOf: - - required: ['200'] - - required: ['202'] - not: - required: ['204'] - - azion-204-no-response-body: - description: 204 responses must not have a response body - severity: error - given: $.paths[*][*].responses.204 - then: - field: content - function: falsy - - azion-get-response-200: - description: GET operations must return 200 status code - message: GET operation must include 200 status code in responses - severity: error - given: $.paths[*].get.responses - then: - field: "200" - function: truthy - - azion-post-response-codes: - description: POST operations must return 201 (sync) or 202 (async) - message: POST operations must return 201 (Created) or 202 (Accepted) - severity: error - given: $.paths[*].post.responses - then: - function: schema - functionOptions: - schema: - anyOf: - - required: ['201'] - - required: ['202'] - - azion-put-response-200: - description: PUT operations must return 200 status code - message: PUT operation must include 200 status code in responses - severity: error - given: $.paths[*].put.responses - then: - field: "200" - function: truthy - - azion-patch-response-codes: - description: PATCH operations must return 200 (sync) or 202 (async) - message: PATCH operations must return 200 (OK) or 202 (Accepted) - severity: error - given: $.paths[*].patch.responses - then: - function: schema - functionOptions: - schema: - anyOf: - - required: ['200'] - - required: ['202'] - - # ============================================================================ - # 7. ERROR RESPONSES - JSON:API FORMAT - # ============================================================================ - - azion-has-key-error-response: - message: "Error response must have 'detail' field" - severity: error - given: "$.responses[*]" - then: - field: content.application/json.schema.properties.detail - function: truthy - - azion-match-type-error-response: - message: "Error 'detail' field must be of type string" - severity: error - given: "$.responses[*]" - then: - field: content.application/json.schema.properties.detail.type - function: schema - functionOptions: - schema: - type: string - - azion-auth-error-responses-required: - description: Endpoints with security must document 401 and 403 error responses - message: "Authenticated endpoint must document 401 (Unauthorized) and 403 (Forbidden) responses" - severity: error - given: "$.paths.*.*[?(@.security)]" - then: - function: schema - functionOptions: - schema: - properties: - responses: - required: ['401', '403'] - - # ============================================================================ - # 8. ASYNC OPERATIONS (202 Accepted) - # ============================================================================ - - azion-async-operation-id-required: - description: Async operation response must have operation_id field - message: "202 response schema must include 'operation_id' field" - severity: warn - given: "$.paths.*.*.responses.202.content.*.schema.properties" - then: - field: operation_id - function: truthy - - # ============================================================================ - # 9. SCHEMAS - NAMING CONVENTIONS - # ============================================================================ - - azion-boolean-naming-convention: - description: Boolean properties must not start with 'is' prefix - severity: error - given: $..properties[*][?(@ && @.type == 'boolean')]^ - then: - field: "@key" - function: pattern - functionOptions: - notMatch: '^is[A-Z]' - - # ============================================================================ - # 10. CONSTRAINTS - # ============================================================================ - - azion-number-minimum-limit-rule: - message: "{{error}}" - severity: error - given: "$.components.schemas.*.properties.*" - then: - function: checkNumberMinimumLimit - - azion-number-maximum-limit-rule: - message: "{{error}}" - severity: error - given: "$.components.schemas.*.properties.*" - then: - function: checkNumberMaximumLimit - - azion-string-minlength-properties-rule: - message: "{{error}}" - severity: warn - given: "$.components.schemas.*.properties.*" - then: - function: checkStringMinLength - - azion-string-maxlength-properties-rule: - message: "{{error}}" - severity: warn - given: "$.components.schemas.*.properties.*" - then: - function: checkStringMaxLength - - azion-string-pattern-properties-rule: - message: "{{error}}" - severity: warn - given: "$.components.schemas.*.properties.*" - then: - function: checkStringPattern - - azion-date-time-format-rule: - message: "DateTime fields must follow RFC 3339 format" - severity: error - given: "$.components.schemas.*.properties.last_modified" - then: - - function: checkStringPattern - pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$" - - # ============================================================================ - # 11. DESCRIPTIONS - # ============================================================================ - - azion-operation-description-required: - description: Operations must have a detailed description - message: "Operation '{{property}}' is missing 'description'" - severity: error - given: $.paths.*[get,put,post,patch,delete] - then: - field: description - function: truthy - - azion-operation-summary-length: - description: Operation summary should be concise (max 100 characters) - message: "Operation summary is too long ({{value}} chars). Keep it under 100 characters." - severity: warn - given: $.paths.*[get,put,post,patch,delete].summary - then: - function: length - functionOptions: - max: 100 - - # ============================================================================ - # 12. EXAMPLES - # ============================================================================ - - azion-request-examples-required: - description: POST and PUT operations should have request examples - message: "{{property}} operation should include request examples for better documentation" - severity: warn - given: "$.paths.*[post,put]" - then: - field: requestBody.content.*.examples - function: truthy - - # ============================================================================ - # 13. CONTENT-TYPE - # ============================================================================ - - azion-match-content-type-response: - message: "{{error}}" - severity: error - given: "$.paths[?(@property != '/api/schema')].*.responses.*.content" - then: - function: checkContentTypeResponse - - # ============================================================================ - # 14. PATH NAMING - # ============================================================================ - - azion-sps-paths-valid-uri: - message: "Path should not contain 'api', 'v4', single segment, or trailing slash" - severity: warn - given: "$.paths" - then: - function: pattern - functionOptions: - notMatch: "api|v4|^[^/]*\/[^/]*$|/$" - - azion-endpoint-ddd-and-snake: - message: "Path must follow snake_case and hierarchical structure" - severity: error - given: $.paths.*~ - then: - - function: pattern - functionOptions: - match: "^(/[a-z][a-z0-9]+(_[a-z][a-z0-9]+)*){2,}($|/({[a-z][a-zA-Z0-9_]+}|[a-z][a-z0-9]+(_[a-z0-9]+)*)+)*$" - - azion-path-naming-convention: - message: "{{error}}" - severity: warn - given: $.paths - then: - function: checkPathEndsWithSForList - - azion-endpoint-uri-method-validation: - message: "Path should not contain HTTP method names" - severity: error - given: $.paths.*~ - then: - - function: pattern - functionOptions: - notMatch: "/.*(GET|POST|PUT|PATCH|DELETE).*/i" - - azion-validate-resource-id-usage: - message: "{{error}}" - severity: error - given: "$.paths" - then: - function: checkResourceIDUsage - - # ============================================================================ - # 15. AUTHORIZATION - # ============================================================================ - - azion-authorization-header: - message: "Operation must define security requirements" - severity: error - given: "$.paths.*.*.security" - then: - function: truthy - - azion-security-scheme-bearer: - description: Security schemes should use Bearer token with JWT - message: "Security scheme must use 'bearer' scheme with bearerFormat 'JWT'" - severity: error - given: "$.components.securitySchemes.*[?(@.type == 'http')]" - then: - field: scheme - function: pattern - functionOptions: - match: "^bearer$" - - # ============================================================================ - # 16. REQUEST BODY - # ============================================================================ - - azion-request-body-required: - description: POST, PUT, and PATCH operations must have requestBody - message: "{{property}} operation is missing 'requestBody'" - severity: error - given: "$.paths.*[post,put,patch]" - then: - field: requestBody - function: truthy - - azion-request-body-required-flag: - description: requestBody should be marked as required - message: "requestBody should have 'required: true'" - severity: warn - given: "$.paths.*[post,put,patch].requestBody" - then: - field: required - function: truthy - - # ============================================================================ - # 17. HEADERS - # ============================================================================ - - azion-header-disallowed: - description: Authorization, Content-Type, and Accept headers must not be defined as header parameters - message: 'Header parameter "{{value}}" must not be defined explicitly' - severity: error - given: - - $.paths[?(!@property.match(/storage.*objects/))].parameters[?(@.in == 'header')] - - $.paths[?(!@property.match(/storage.*objects/))].*[get,put,post,patch,delete,options,head].parameters[?(@.in == 'header')] - then: - function: pattern - field: name - functionOptions: - notMatch: '/^(authorization|content-type|accept)$/i' - - # ============================================================================ - # 18. METADATA GLOBAL - # ============================================================================ - - azion-service-name-pattern: - message: "API title must end with '-api'" - severity: error - given: "$.info.title" - then: - function: pattern - functionOptions: - match: ".*-api$" - - azion-version-semantic: - description: API version should follow semantic versioning - message: "info.version should follow semantic versioning (e.g., '4.0.0')" - severity: error - given: "$.info.version" - then: - function: pattern - functionOptions: - match: "^\\d+\\.\\d+\\.\\d+$" - - # ============================================================================ - # 19. RESPONSE BODIES - # ============================================================================ - - azion-validate-count-and-results: - message: "{{error}}" - severity: error - given: "$.responses[*]" - then: - - field: content.application/json.schema.properties.detail.properties - function: schema - functionOptions: - schema: - type: object - properties: - count: - type: integer - results: - type: array - - azion-data-object-type-rule: - message: "{{error}}" - severity: error - given: "$.responses[*].content.application/json.schema" - then: - function: checkDataObjectType - - azion-data-keys-presence-rule: - message: "{{error}}" - severity: error - given: "$.responses[*].content.application/json.schema" - then: - function: checkDataKeysPresence - - # ============================================================================ - # 20. DELETED RESOURCES - # ============================================================================ - - azion-deleted-resource-state-validation: - message: "{{error}}" - severity: error - given: "$.paths.*.*.responses.*" - then: - function: checkStateDeletedResource - - # ============================================================================ - # 21. BUSINESS NAMING - # ============================================================================ - - azion-no-technical-schema-names: - description: Schema names must use business terminology, not technical implementation details - message: "Schema '{{property}}' uses technical naming. Use business domain names instead. Examples: 'Database' instead of 'OpenAPISchema', 'PaginatedDatabaseList' instead of 'PaginatedOpenAPISchemaList'" - severity: error - given: $.components.schemas.*~ - then: - function: pattern - functionOptions: - notMatch: "^.*(OpenAPI|Schema(?!Enum$)|DTO|Entity|Model(?!$)|RESTful|Swagger).*$" - - # ============================================================================ - # 22. EXTENSIBLE ENUMS - # ============================================================================ - - x-extensible-enum-required: - description: Enums must include x-extensible-enum to ensure forward compatibility - message: "Enum '{{path}}' is missing 'x-extensible-enum' property for forward compatibility" - severity: error - given: "$.components.schemas[*][?(@property === 'enum')]^" - then: - field: x-extensible-enum - function: truthy - - # ============================================================================ - # 23. CREATED_AT FIELD - # ============================================================================ - - check-created-at-field: - description: | - Ensures that POST creation endpoints include a 'created_at' field - in their response schema for proper resource tracking and auditing. - Reports WARN when a variation like 'created' is used instead of 'created_at'. - Endpoints without created_at should be added to EXCEPTIONS list. - message: "{{error}}" - severity: warn - given: $ - then: - function: checkCreatedAtField