# Spectral ruleset for Noun Project API # # Enforces conventions observed in the Noun Project API V2: # - paths use kebab-case under /v2 # - operation summaries are Title Case prefixed with "Noun Project" # - schema and parameter properties use snake_case # - OAuth 1.0a is the documented auth scheme # - pagination via prev_page / next_page cursor tokens # - thumbnail_size enum [42, 84, 200] extends: - spectral:oas rules: # ── INFO / METADATA ──────────────────────────────────────────────────── info-title-noun-project: description: API title must begin with "Noun Project". severity: error given: $.info then: field: title function: pattern functionOptions: match: '^Noun Project' info-description-required: description: API info description is required and must be substantive. severity: error given: $.info then: field: description function: truthy info-description-length: description: API info description should be at least 100 characters. severity: warn given: $.info.description then: function: length functionOptions: min: 100 info-version-required: description: API info version is required. severity: error given: $.info then: field: version function: truthy info-contact-required: description: API info contact block is required. severity: warn given: $.info then: field: contact function: truthy info-license-required: description: API info license block is required. severity: warn given: $.info then: field: license function: truthy # ── OPENAPI VERSION ─────────────────────────────────────────────────── openapi-version-3: description: OpenAPI version must be 3.0.x or higher. severity: error given: $.openapi then: function: pattern functionOptions: match: '^3\.' # ── SERVERS ─────────────────────────────────────────────────────────── servers-required: description: At least one server must be defined. severity: error given: $ then: field: servers function: truthy servers-https-only: description: Servers must use HTTPS. severity: error given: $.servers[*].url then: function: pattern functionOptions: match: '^https://' servers-noun-project-host: description: Servers should point to api.thenounproject.com. severity: warn given: $.servers[*].url then: function: pattern functionOptions: match: 'api\.thenounproject\.com' # ── PATHS — NAMING CONVENTIONS ──────────────────────────────────────── paths-v2-prefix: description: All paths must be under the /v2 version prefix. severity: error given: $.paths then: field: '@key' function: pattern functionOptions: match: '^/v2(/|$)' paths-kebab-case: description: Path segments (excluding parameters) must be kebab-case. severity: warn given: $.paths then: field: '@key' function: pattern functionOptions: match: '^(/[a-z0-9-]+|/\{[a-z_][a-z0-9_]*\})+$' paths-no-trailing-slash: description: Paths must not end with a trailing slash. severity: error given: $.paths then: field: '@key' function: pattern functionOptions: notMatch: '/$' paths-no-query-string: description: Paths must not include query strings. severity: error given: $.paths then: field: '@key' function: pattern functionOptions: notMatch: '\?' # ── OPERATIONS ──────────────────────────────────────────────────────── operation-operationid-required: description: Each operation must declare an operationId. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: operationId function: truthy operation-operationid-camel-case: description: operationId must be camelCase. severity: warn given: $.paths[*][get,post,put,patch,delete].operationId then: function: pattern functionOptions: match: '^[a-z][a-zA-Z0-9]*$' operation-operationid-verb-prefix: description: operationId should start with a recognized verb (get/list/search/add/update/delete/download/autocomplete). severity: info given: $.paths[*][get,post,put,patch,delete].operationId then: function: pattern functionOptions: match: '^(get|list|search|add|update|delete|download|autocomplete|create)' operation-summary-required: description: Each operation must declare a summary. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: summary function: truthy operation-summary-title-case-prefix: description: Operation summaries should start with a capital letter. severity: warn given: $.paths[*][get,post,put,patch,delete].summary then: function: pattern functionOptions: match: '^[A-Z]' operation-description-required: description: Each operation must declare a description. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: description function: truthy operation-tags-required: description: Each operation must declare at least one tag. severity: error given: $.paths[*][get,post,put,patch,delete] then: field: tags function: truthy operation-security-required: description: Each operation must declare a security block (OAuth 1.0a). severity: warn given: $.paths[*][get,post,put,patch,delete] then: field: security function: truthy # ── TAGS ────────────────────────────────────────────────────────────── tags-global-defined: description: Global tags array must be defined. severity: warn given: $ then: field: tags function: truthy tag-description-required: description: Each global tag must have a description. severity: warn given: $.tags[*] then: field: description function: truthy tag-title-case: description: Tag names must be Title Case (e.g., "Icon", "Collection", "Autocomplete"). severity: warn given: $.tags[*].name then: function: pattern functionOptions: match: '^[A-Z][a-zA-Z0-9]*$' # ── PARAMETERS ──────────────────────────────────────────────────────── parameter-description-required: description: Each parameter must have a description. severity: warn given: $.paths[*][get,post,put,patch,delete].parameters[*] then: field: description function: truthy parameter-snake-case: description: Query and path parameter names must be snake_case. severity: warn given: $.paths[*][get,post,put,patch,delete].parameters[?(@.in == 'query' || @.in == 'path')].name then: function: pattern functionOptions: match: '^[a-z][a-z0-9_]*$' parameter-schema-required: description: Each parameter must declare a schema with a type. severity: error given: $.paths[*][get,post,put,patch,delete].parameters[*] then: field: schema function: truthy parameter-pagination-cursor: description: Pagination parameters must use prev_page / next_page cursor tokens. severity: info given: $.paths[*][get].parameters[?(@.in == 'query' && (@.name == 'page' || @.name == 'offset'))] then: function: falsy parameter-thumbnail-size-enum: description: thumbnail_size parameter must constrain to enum [42, 84, 200]. severity: warn given: $.paths[*][get,post,put,patch,delete].parameters[?(@.name == 'thumbnail_size')].schema then: field: enum function: truthy # ── REQUEST BODIES ──────────────────────────────────────────────────── request-body-json: description: Request bodies should use application/json content. severity: warn given: $.paths[*][post,put,patch].requestBody.content then: field: application/json function: truthy request-body-description: description: Request bodies should have a description. severity: info given: $.paths[*][post,put,patch].requestBody then: field: description function: truthy # ── RESPONSES ───────────────────────────────────────────────────────── response-success-required: description: Each operation must declare a 2xx response. severity: error given: $.paths[*][get,post,put,patch,delete].responses then: function: schema functionOptions: schema: type: object patternProperties: '^2\d{2}$': type: object minProperties: 1 response-401-required: description: Each authenticated operation must declare a 401 response. severity: warn given: $.paths[*][get,post,put,patch,delete].responses then: field: '401' function: truthy response-429-required: description: Operations subject to rate limits must declare a 429 response. severity: warn given: $.paths[*][get,post,put,patch,delete].responses then: field: '429' function: truthy response-description-required: description: Each response must have a description. severity: error given: $.paths[*][get,post,put,patch,delete].responses[*] then: field: description function: truthy response-json-content: description: 2xx JSON responses must use application/json content type. severity: warn given: $.paths[*][get,post,put,patch,delete].responses[?(@property.match(/^2\\d{2}$/))].content then: function: schema functionOptions: schema: type: object minProperties: 1 # ── SCHEMAS — PROPERTY NAMING ──────────────────────────────────────── schema-property-snake-case: description: Schema property names must be snake_case. severity: warn given: $.components.schemas[*].properties then: field: '@key' function: pattern functionOptions: match: '^[a-z_][a-z0-9_]*$' schema-description-required: description: Each component schema must have a description. severity: warn given: $.components.schemas[*] then: field: description function: truthy schema-type-required: description: Each component schema must declare a type. severity: error given: $.components.schemas[*] then: field: type function: truthy schema-id-integer: description: id properties should be typed as integer for Noun Project resources. severity: info given: $.components.schemas[*].properties.id then: field: type function: pattern functionOptions: match: '^integer$' schema-permalink-uri: description: permalink properties should use format uri. severity: info given: $.components.schemas[*].properties.permalink then: field: format function: pattern functionOptions: match: '^uri$' # ── SECURITY ────────────────────────────────────────────────────────── security-global-defined: description: Global security must be declared. severity: warn given: $ then: field: security function: truthy security-scheme-oauth1: description: Security schemes must include the oauth1 scheme. severity: warn given: $.components.securitySchemes then: field: oauth1 function: truthy security-scheme-description: description: Each security scheme must have a description. severity: warn given: $.components.securitySchemes[*] then: field: description function: truthy # ── HTTP METHOD CONVENTIONS ────────────────────────────────────────── no-get-request-body: description: GET operations must not declare a request body. severity: error given: $.paths[*][get] then: field: requestBody function: falsy post-request-body-required: description: POST operations should declare a request body. severity: warn given: $.paths[*][post] then: field: requestBody function: truthy # ── GENERAL QUALITY ────────────────────────────────────────────────── example-encouraged: description: Schema properties should include example values. severity: info given: $.components.schemas[*].properties[*] then: field: example function: truthy external-docs-encouraged: description: The spec should reference external documentation. severity: info given: $ then: field: externalDocs function: truthy