# WakaTime Spectral Ruleset # Opinionated rules derived from the WakaTime API v1 specification. # Path style: snake_case under `/api/v1/` # Property style: snake_case # OperationId style: camelCase with verb prefixes (get/list/create/update/delete/put) # Tag style: Title Case # Auth: OAuth 2.0 + API Key (HTTP Basic) + Bearer extends: [[spectral:oas, recommended]] rules: # ===================================================================== # INFO / METADATA # ===================================================================== info-title-prefix: description: Spec title should start with "WakaTime". message: "info.title should start with 'WakaTime'" severity: warn given: $.info.title then: function: pattern functionOptions: match: "^WakaTime" info-description-required: description: info.description is required and should be meaningful. severity: error given: $.info then: field: description function: truthy info-description-min-length: description: info.description should be at least 120 characters. severity: warn given: $.info.description then: function: length functionOptions: min: 120 info-contact-required: description: info.contact must be defined. severity: warn given: $.info then: field: contact function: truthy info-contact-url-required: description: info.contact.url is required. severity: warn given: $.info.contact then: field: url function: truthy info-license-required: description: info.license is required. severity: warn given: $.info then: field: license function: truthy info-version-required: description: info.version is required. severity: error given: $.info then: field: version function: truthy # ===================================================================== # OPENAPI VERSION # ===================================================================== openapi-version-3: description: WakaTime specs should target OpenAPI 3.0.x. severity: error given: $.openapi then: function: pattern functionOptions: match: "^3\\.0\\." # ===================================================================== # SERVERS # ===================================================================== servers-required: description: At least one server must be defined. severity: error given: $.servers then: function: schema functionOptions: schema: type: array minItems: 1 servers-https-only: description: All servers must use HTTPS. severity: error given: $.servers[*].url then: function: pattern functionOptions: match: "^https://" servers-wakatime-host: description: Servers should be hosted at wakatime.com (or api.wakatime.com). severity: warn given: $.servers[*].url then: function: pattern functionOptions: match: "wakatime\\.com" # ===================================================================== # PATHS — NAMING CONVENTIONS # ===================================================================== path-segments-snake-case: description: Path segments use snake_case (lowercase letters, digits, underscores, dots, optional .bulk suffix). message: "Path '{{path}}' segments must be snake_case (e.g. /users/current/heartbeats.bulk)" severity: warn given: $.paths[*]~ then: function: pattern functionOptions: match: "^(/[a-z0-9_.]+|/\\{[a-z0-9_]+\\})+$" path-no-trailing-slash: description: Paths must not end with a trailing slash. severity: error given: $.paths[*]~ then: function: pattern functionOptions: notMatch: ".+/$" path-no-query-string: description: Paths must not embed a query string. severity: error given: $.paths[*]~ then: function: pattern functionOptions: notMatch: "\\?" path-current-user-shortcut: description: User-scoped paths should use `current` for the authenticated user. severity: info given: $.paths[*]~ then: function: pattern functionOptions: match: "^/(?!users/me(/|$)).*" # ===================================================================== # OPERATIONS # ===================================================================== operation-operationid-required: description: Every operation must have an operationId. severity: error given: $.paths[*][get,post,put,delete,patch] then: field: operationId function: truthy operation-operationid-camel-case: description: operationId must be camelCase. severity: error given: $.paths[*][get,post,put,delete,patch].operationId then: function: pattern functionOptions: match: "^[a-z][a-zA-Z0-9]*$" operation-operationid-verb-prefix: description: operationId should start with a recognised verb (get/list/create/update/delete/put/abort/synchronize). severity: warn given: $.paths[*][get,post,put,delete,patch].operationId then: function: pattern functionOptions: match: "^(get|list|create|update|delete|put|post|abort|synchronize)[A-Z]" operation-summary-required: description: Each operation must have a summary. severity: error given: $.paths[*][get,post,put,delete,patch] then: field: summary function: truthy operation-summary-title-case: description: Operation summaries should be Title Case (every significant word capitalised). severity: warn given: $.paths[*][get,post,put,delete,patch].summary then: function: pattern functionOptions: match: "^[A-Z0-9][^a-z]*([A-Z0-9][A-Za-z0-9]*\\s*)+$|^([A-Z][A-Za-z0-9]+|[0-9]+|[A-Z]+)(\\s+([A-Z][A-Za-z0-9]+|[A-Z]+|[0-9]+|(by|for|of|to|in|the|and|or|a|an)))*$" operation-description-required: description: Each operation must have a description. severity: warn given: $.paths[*][get,post,put,delete,patch] then: field: description function: truthy operation-tags-required: description: Every operation must declare at least one tag. severity: error given: $.paths[*][get,post,put,delete,patch] then: field: tags function: schema functionOptions: schema: type: array minItems: 1 operation-microcks-extension: description: Operations should declare x-microcks-operation for mock-server compatibility. severity: info given: $.paths[*][get,post,put,delete,patch] then: field: x-microcks-operation function: truthy # ===================================================================== # TAGS # ===================================================================== tag-global-array: description: Spec must declare a global tags array. severity: warn given: $ then: field: tags function: truthy tag-name-title-case: description: Tag names must be Title Case (e.g. "Custom Rules", "Org Dashboards"). severity: warn given: $.tags[*].name then: function: pattern functionOptions: match: "^([A-Z][A-Za-z0-9]*)(\\s+([A-Z][A-Za-z0-9]*|[A-Z]+))*$" tag-description-required: description: Every tag should have a description. severity: warn given: $.tags[*] then: field: description function: truthy # ===================================================================== # PARAMETERS # ===================================================================== parameter-description-required: description: Every parameter must have a description. severity: warn given: $.paths[*][*].parameters[*] then: field: description function: truthy parameter-name-snake-case: description: Query and path parameters must be snake_case. severity: warn given: $.paths[*][*].parameters[?(@.in=='query' || @.in=='path')].name then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$" parameter-schema-required: description: Every parameter must declare a schema. severity: error given: $.paths[*][*].parameters[*] then: field: schema function: truthy parameter-date-format: description: Query parameter named `date` must use the date format (YYYY-MM-DD). severity: warn given: $.paths[*][*].parameters[?(@.name=='date')].schema then: field: format function: truthy parameter-range-enum: description: Path/query parameter named `range` should be enumerated with the WakaTime presets. severity: warn given: $.paths[*][*].parameters[?(@.name=='range')].schema then: field: enum function: truthy parameter-api-key-not-in-query: description: Do not pass api_key as a query parameter in OpenAPI spec definitions — use the apiKey security scheme (HTTP Basic) or Bearer instead. severity: warn given: $.paths[*][*].parameters[?(@.name=='api_key')] then: function: falsy # ===================================================================== # REQUEST BODIES # ===================================================================== request-body-json-content: description: Request bodies must declare application/json content. severity: error given: $.paths[*][post,put,patch].requestBody.content then: field: application/json function: truthy request-body-schema-required: description: Request bodies must declare a schema. severity: error given: $.paths[*][post,put,patch].requestBody.content.application/json then: field: schema function: truthy # ===================================================================== # RESPONSES # ===================================================================== response-success-required: description: Every operation must declare at least one 2xx response. severity: error given: $.paths[*][get,post,put,delete,patch].responses then: function: schema functionOptions: schema: type: object patternProperties: "^2[0-9]{2}$": {} minProperties: 1 response-401-recommended: description: Operations should document a 401 Unauthorized response. severity: info given: $.paths[*][get,post,put,delete,patch].responses then: field: "401" function: truthy response-429-recommended: description: Operations should document a 429 Too Many Requests response (WakaTime enforces rate limiting). severity: info given: $.paths[*][get,post,put,delete,patch].responses then: field: "429" function: truthy response-description-required: description: Each response must have a description. severity: error given: $.paths[*][*].responses[*] then: field: description function: truthy response-data-envelope: description: 2xx JSON responses should wrap payloads in a `data` envelope (WakaTime convention). severity: info given: $.paths[*][get].responses[?(@property.match(/2\d\d/))].content.application/json.schema then: function: schema functionOptions: schema: oneOf: - properties: data: {} required: [data] - properties: responses: {} required: [responses] - $ref: {} # ===================================================================== # SCHEMAS — PROPERTY NAMING # ===================================================================== schema-property-snake-case: description: Schema properties must be snake_case. severity: warn given: $.components.schemas[*].properties[*]~ then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$" schema-property-type-defined: description: Schema properties must declare a type (or $ref / allOf / oneOf / anyOf). severity: warn given: $.components.schemas[*].properties[*] then: function: schema functionOptions: schema: anyOf: - required: [type] - required: [$ref] - required: [allOf] - required: [oneOf] - required: [anyOf] schema-timestamps-iso: description: Properties ending with _at or _date should declare format date-time or date. severity: warn given: $.components.schemas[*].properties[?(@property.match(/_(at|date)$/))] then: field: format function: pattern functionOptions: match: "^date(-time)?$" schema-seconds-numeric: description: Properties ending with _seconds should be numeric. severity: warn given: $.components.schemas[*].properties[?(@property.match(/_seconds$/))] then: field: type function: pattern functionOptions: match: "^(number|integer)$" # ===================================================================== # SECURITY # ===================================================================== security-global: description: Spec must define a global security requirement. severity: warn given: $ then: field: security function: truthy security-schemes-defined: description: components.securitySchemes must be defined. severity: error given: $.components then: field: securitySchemes function: truthy security-oauth2-defined: description: WakaTime specs must declare the oauth2 security scheme. severity: warn given: $.components.securitySchemes then: field: oauth2 function: truthy security-api-key-defined: description: WakaTime specs should declare the apiKey (HTTP Basic) security scheme for API-key auth. severity: warn given: $.components.securitySchemes then: field: apiKey function: truthy # ===================================================================== # HTTP METHOD CONVENTIONS # ===================================================================== http-get-no-request-body: description: GET operations must not declare a requestBody. severity: error given: $.paths[*].get.requestBody then: function: falsy http-delete-no-required-body: description: DELETE operations should not require a request body (bulk-delete endpoints excepted but discouraged elsewhere). severity: warn given: $.paths[?(!@property.match(/\.bulk$/))][delete].requestBody then: function: falsy # ===================================================================== # GENERAL QUALITY # ===================================================================== schema-examples-encouraged: description: Schemas should provide examples on at least some properties. severity: info given: $.components.schemas[*] then: field: properties function: truthy no-deprecated-without-replacement: description: Deprecated operations should describe a replacement in the description. severity: info given: $.paths[*][?(@.deprecated)] then: field: description function: pattern functionOptions: match: "replac|deprecat"