# SWAPI Spectral Ruleset # # Enforces conventions observed in the SWAPI (Star Wars API) OpenAPI specification. # # Convention summary (dominant patterns): # OpenAPI version : 3.0.3 # Title prefix : "SWAPI" # Path casing : lowercase, plural nouns, trailing slash (Django convention) # OperationId : camelCase (listFoo / getFoo) # Parameter names : snake_case # Schema props : snake_case # Security : none (public read-only API) # Required errors : 404 extends: - spectral:oas formats: - oas3 rules: # ─── INFO / METADATA ─────────────────────────────────────────────────────────── info-title-swapi-prefix: description: API title MUST begin with "SWAPI". message: "{{property}} must start with 'SWAPI' (e.g. 'SWAPI - Star Wars API')." severity: warn given: $.info.title then: function: pattern functionOptions: match: "^SWAPI" info-description-required: description: API description MUST be present and non-trivial. message: "{{property}} description is required (min 30 chars)." severity: warn given: $.info then: - field: description function: truthy - field: description function: length functionOptions: min: 30 info-version-required: description: API version MUST be defined. severity: error given: $.info then: field: version function: truthy info-contact-required: description: A contact block (community channel) MUST be defined. severity: info given: $.info then: field: contact function: truthy info-license-required: description: A license SHOULD be declared (SWAPI is open source). severity: warn given: $.info then: field: license function: truthy # ─── OPENAPI VERSION ─────────────────────────────────────────────────────────── openapi-version-3-0-x: description: OpenAPI version MUST be 3.0.x. severity: warn given: $.openapi then: function: pattern functionOptions: match: "^3\\.0\\." # ─── SERVERS ─────────────────────────────────────────────────────────────────── servers-defined: description: At least one server entry MUST be defined. severity: error given: $.servers then: function: truthy servers-https: description: Every server URL MUST use HTTPS. severity: error given: $.servers[*].url then: function: pattern functionOptions: match: "^https://" servers-have-description: description: Each server entry SHOULD have a description. severity: warn given: $.servers[*] then: field: description function: truthy # ─── PATHS — NAMING CONVENTIONS ──────────────────────────────────────────────── paths-lowercase: description: Path segments MUST be lowercase. severity: error given: $.paths.*~ then: function: pattern functionOptions: match: "^/[a-z0-9/{}_-]*$" paths-plural-resources: description: Top-level resource collections SHOULD use plural nouns (films, people, planets, species, starships, vehicles). severity: info given: $.paths.*~ then: function: pattern functionOptions: match: "^/(films|people|planets|species|starships|vehicles)(/|/\\{id\\}/)?$" paths-no-query-string: description: Paths MUST NOT include query strings. severity: error given: $.paths.*~ then: function: pattern functionOptions: notMatch: "\\?" # ─── OPERATIONS ──────────────────────────────────────────────────────────────── operation-operationid-required: description: Every operation MUST have an operationId. severity: error given: $.paths[*][get,post,put,patch,delete,head,options] then: field: operationId function: truthy operation-operationid-camelcase: description: operationId MUST be camelCase. severity: warn given: $.paths[*][get,post,put,patch,delete,head,options].operationId then: function: pattern functionOptions: match: "^[a-z][a-zA-Z0-9]*$" operation-summary-required: description: Every operation MUST have a summary. severity: error given: $.paths[*][get,post,put,patch,delete,head,options] then: field: summary function: truthy operation-summary-title-case: description: Operation summaries SHOULD use Title Case. severity: warn given: $.paths[*][get,post,put,patch,delete,head,options].summary then: function: pattern functionOptions: match: "^[A-Z][A-Za-z0-9 -]*$" operation-description-required: description: Every operation MUST have a description. severity: warn given: $.paths[*][get,post,put,patch,delete,head,options] then: field: description function: truthy operation-tags-required: description: Every operation MUST be tagged. severity: error given: $.paths[*][get,post,put,patch,delete,head,options] then: field: tags function: truthy # ─── TAGS ────────────────────────────────────────────────────────────────────── tag-global-defined: description: Tags MUST be defined at the document root. severity: warn given: $ then: field: tags function: truthy tag-description-required: description: Each global tag SHOULD have a description. severity: warn given: $.tags[*] then: field: description function: truthy tag-title-case: description: Tag names MUST use Title Case. severity: warn given: $.tags[*].name then: function: pattern functionOptions: match: "^[A-Z][A-Za-z ]*$" # ─── PARAMETERS ──────────────────────────────────────────────────────────────── parameter-description-required: description: Every parameter MUST have a description. severity: warn given: $.paths[*][*].parameters[*] then: field: description function: truthy parameter-snake-case: description: Parameter names SHOULD be snake_case (SWAPI convention is single-word lowercase, e.g. `search`, `page`, `id`). severity: warn given: $.paths[*][*].parameters[*].name then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$" parameter-pagination-page: description: Pagination parameter MUST be named `page` (SWAPI convention). severity: info given: "$.paths[*][*].parameters[?(@.in=='query' && (@.name=='offset' || @.name=='limit' || @.name=='cursor'))]" then: function: falsy # ─── RESPONSES ───────────────────────────────────────────────────────────────── response-success-required: description: Every operation MUST declare a 2xx response. severity: error given: $.paths[*][*].responses then: field: "200" function: truthy response-404-required: description: GET operations on resource detail paths MUST declare a 404 response. severity: warn given: "$.paths[?(@property.match(/\\{id\\}/))][get].responses" then: field: "404" function: truthy response-description-required: description: Every response MUST have a description. severity: warn given: $.paths[*][*].responses[*] then: field: description function: truthy response-content-json: description: Responses with content SHOULD use application/json. severity: warn given: $.paths[*][*].responses[*].content then: field: application/json function: truthy # ─── SCHEMAS — PROPERTY NAMING ──────────────────────────────────────────────── schema-property-snake-case: description: Schema property names MUST be snake_case (the SWAPI wire format convention). severity: warn given: $.components.schemas[*].properties.*~ then: function: pattern functionOptions: match: "^[a-z][a-z0-9_]*$|^[A-Z]+$" schema-types-defined: description: Schema properties MUST declare a `type`. severity: warn given: $.components.schemas[*].properties[*] then: field: type function: truthy schema-canonical-fields: description: Resource schemas MUST include `created`, `edited`, and `url` (SWAPI canonical envelope fields). severity: info given: "$.components.schemas[?(@property=='Film' || @property=='Person' || @property=='Planet' || @property=='Species' || @property=='Starship' || @property=='Vehicle')].properties" then: - field: created function: truthy - field: edited function: truthy - field: url function: truthy # ─── HTTP METHOD CONVENTIONS ────────────────────────────────────────────────── get-no-request-body: description: GET operations MUST NOT have a request body. severity: error given: $.paths[*].get then: field: requestBody function: falsy # ─── GENERAL QUALITY ────────────────────────────────────────────────────────── no-empty-descriptions: description: Descriptions MUST NOT be empty. severity: warn given: "$..description" then: function: truthy external-docs-encouraged: description: Top-level externalDocs SHOULD be defined. severity: info given: $ then: field: externalDocs function: truthy