extends: spectral:oas
functionsDir: './functions'
functions:
  - consistent-response-body
  - error-response
  - has-header
  - lro-response-schema
  - naming-convention
  - operation-id
  - pagination-parameters
  - operation-security
  - pagination-response
  - param-names
  - param-names-unique
  - param-order
  - patch-content-type
  - path-param-schema
  - path-param-names
  - put-request-and-response-body
  - property-default-not-allowed
  - readonly-in-response-schema
  - security-definitions
  - security-requirements
  - schema-type-and-format
  - unused-definition
  - version-policy
rules:
  info-contact: off
  no-$ref-siblings: off
  oas2-api-host: off
  oas2-api-schemes: off
  oas2-unused-definition: off
  openapi-tags: off
  operation-description: off
  operation-tags: off
  operation-tag-defined: off

  # Note: The Spectral VSCode extension will not display "hint" messages, so
  # use "info" rather than "hint".

  az-additional-properties-and-properties:
    description: Don't specify additionalProperties as a sibling of properties.
    severity: warn
    formats: ['oas2', 'oas3']
    given: $..[?(@object() && @.type === 'object' && @.properties)]
    then:
      field: additionalProperties
      function: falsy

  az-additional-properties-object:
    description: additionalProperties with type object is a common error.
    severity: info
    formats: ['oas2', 'oas3']
    # This rule produces redundant errors if run on the resolved spec.
    resolved: false
    given: $..[?(@property == 'additionalProperties' && @.type == 'object' && @.properties == undefined)]
    then:
      function: falsy

  az-api-version-enum:
    description: The api-version parameter should not be an enum.
    severity: warn
    # oas3 support has broken - disable for now
    formats: ['oas2']
    given:
    - $.paths[*].parameters.[?(@.name == 'api-version')]
    - $.paths.*[get,put,post,patch,delete,options,head].parameters.[?(@.name == 'api-version')]
    then:
      field: enum
      function: falsy

  az-boolean-naming-convention:
    description: Do not use "is" prefix in names of boolean values
    severity: warn
    formats: ['oas2','oas3']
    given: $
    then:
      function: naming-convention
      functionOptions:
        type: boolean
        notMatch: '^is[A-Z]'

  az-consistent-response-body:
    description: Ensure the get, put, and patch response body schemas are consistent.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $.paths.*
    then:
      function: consistent-response-body

  az-default-response:
    description: All operations should have a default (error) response.
    message: Operation is missing a default response.
    severity: warn
    given: $.paths.*.*.responses
    then:
      field: default
      function: truthy

  az-datetime-naming-convention:
    description: Use an "At" suffix in names of date-time values.
    severity: warn
    formats: ['oas2','oas3']
    given: $
    then:
      function: naming-convention
      functionOptions:
        type: date-time
        match: 'At$'

  az-delete-response-codes:
    description: A delete operation should have a 204 response.
    message: A delete operation should have a `204` response.
    severity: warn
    formats: ['oas2','oas3']
    # The responses object of a delete operation
    given: $.paths[*].delete.responses
    then:
      function: schema
      functionOptions:
        schema:
          oneOf:
          - required: ['202']
          - required: ['204']
            not:
              required: ['200']

  az-error-code-response-header:
    description: Error response should contain a x-ms-error-code header.
    severity: warn
    formats: ['oas2']
    given:
    - $.paths[*][*].responses[?(@property >= 400)]
    - $.paths[*][*].responses["default"]
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          required: ['headers']
          properties:
            headers:
              type: object
              required: ["x-ms-error-code"]

  az-error-response:
    description: Error response body should conform to Microsoft Azure API Guidelines.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $.paths[*][*].responses
    then:
      function: error-response

  az-formdata:
    description: Check for appropriate use of formData parameters.
    severity: info
    formats: ['oas2']
    given: $.paths.*[get,put,post,patch,delete,options,head].parameters.[?(@.in == "formData")]
    then:
      function: falsy

  az-header-disallowed:
    description: Authorization, Content-type, and Accept headers should not be defined explicitly.
    message: 'Header parameter "{{value}}" should not be defined explicitly.'
    severity: warn
    # oas3 support has broken - disable for now
    formats: ['oas2']
    given:
    - $.paths[*].parameters.[?(@.in == 'header')]
    - $.paths.*[get,put,post,patch,delete,options,head].parameters.[?(@.in == 'header')]
    then:
      function: pattern
      field: name
      functionOptions:
        notMatch: '/^(authorization|content-type|accept)$/i'

  az-lro-extension:
    description: "Operations with a 202 response should specify `x-ms-long-running-operation: true`."
    message: "Operations with a 202 response should specify `x-ms-long-running-operation: true`."
    severity: warn
    formats: ['oas2']
    given: $.paths[*][*].responses[?(@property == '202')]^^
    then:
      field: x-ms-long-running-operation
      function: truthy

  az-lro-get-not-allowed:
    description: Get operations should not be long-running.
    severity: warn
    formats: ['oas2','oas3']
    given: $.paths[*].get.responses.202
    then:
      function: falsy

  az-lro-patch-not-allowed:
    description: Patch operations should not be long-running.
    severity: warn
    formats: ['oas2','oas3']
    given: $.paths[*].patch.responses.202
    then:
      function: falsy

  az-lro-put-response-codes:
    description: Long-running PUT should not return a 202 response.
    severity: warn
    formats: ['oas2','oas3']
    given: $.paths[*].put.responses.202
    then:
      function: falsy

  az-lro-response-codes:
    description: An operation that returns 202 should not return other 2XX responses.
    severity: warn
    formats: ['oas2','oas3']
    # The responses object of an operation that includes a 202 response
    # Exclude get, put and patch since the 202 for these will be flagged in other rules
    given: $.paths[*].[post,delete].responses[?(@property == '202')]^
    then:
      function: schema
      functionOptions:
        schema:
          not:
            anyOf:
              - required: ['200']
              - required: ['201']
              - required: ['204']

  az-lro-response-headers:
    description: A 202 response should include an Operation-Location response header.
    message: A 202 response should include an Operation-Location response header.
    severity: warn
    formats: ['oas2']
    given: $.paths[*][*].responses[?(@property == '202')]
    then:
      function: has-header
      functionOptions:
        name: Operation-location

  az-lro-response-schema:
    description: A 202 response should include a schema for the operation status monitor.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $.paths[*][*].responses[?(@property == '202')]
    then:
      function: lro-response-schema

  az-204-no-response-body:
    description: A 204 response should have no response body.
    severity: warn
    formats: ['oas2']
    given: $.paths[*][*].responses.204
    then:
      field: schema
      function: falsy

  az-ms-client-flatten:
    description: The use of x-ms-client-flatten extension is discouraged.
    severity: warn
    formats: ['oas2', 'oas3']
    resolved: false
    given: $..x-ms-client-flatten
    then:
      function: undefined

  az-ms-enum-descriptions:
    description: Include descriptions for all values in the x-ms-enum extension.
    severity: warn
    formats: ['oas2', 'oas3']
    given: $..x-ms-enum
    then:
      function: schema
      functionOptions:
        schema:
          type: object
          properties:
            values:
              type: array
              items:
                type: object
                required: ['value', 'description']
          required: ['values']

  az-ms-paths:
    description: Don't use x-ms-paths except where necessary to support legacy APIs.
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.x-ms-paths
    then:
      function: falsy

  az-nullable:
    description: Avoid the use of x-nullable.
    severity: warn
    formats: ['oas2', 'oas3']
    resolved: false
    given: $..x-nullable
    then:
      function: undefined

  az-operation-id:
    description: OperationId should conform to Azure API Guidelines
    message: '{{error}}'
    severity: warn
    given:
    - $.paths.*[get,put,post,patch,delete,options,head]
    then:
      function: operation-id

  az-operation-security:
    description: Operation should have a security requirement or globally.
    message: Operation should have a security requirement.
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.paths.*[get,put,post,patch,delete,options,head]
    then:
      function: operation-security

  az-operation-summary-or-description:
    description: Operation should have a summary or description.
    message: Operation should have a summary or description.
    severity: warn
    given:
    - $.paths[*][?( @property === 'get' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'put' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'post' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'patch' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'delete' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'options' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'head' && !@.summary && !@.description )]
    - $.paths[*][?( @property === 'trace' && !@.summary && !@.description )]
    then:
      function: falsy

  az-pagination-parameters:
    description: Pagination parameters must conform to Azure guidelines.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given:
    - $.paths.*[get,post]
    then:
      function: pagination-parameters

  az-pagination-response:
    description: An operation that returns a list that is potentially large should support pagination.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given:
    - $.paths.*[get,post]
    then:
      function: pagination-response

  az-pageable-post:
    description: Post operations that specify x-ms-pageable are problematic.
    severity: info
    formats: ['oas2', 'oas3']
    given:
    - $.paths.*[post]
    then:
      field: x-ms-pageable
      function: falsy

  az-parameter-default-not-allowed:
    description: A required parameter should not specify a default value.
    severity: warn
    # oas3 support has broken - restrict to oas2 for now
    formats: ['oas2']
    given:
    - $.paths[*].parameters.[?(@.required)]
    - $.paths.*[get,put,post,patch,delete,options,head].parameters.[?(@.required)]
    then:
      field: default
      function: falsy

  az-parameter-description:
    description: All parameters should have a description.
    message: Parameter should have a description.
    severity: warn
    # In oas3 the description might be within the schema -- should not flag those
    formats: ['oas2']
    given:
    - $.paths[*].parameters.*
    - $.paths.*[get,put,post,patch,delete,options,head].parameters.*
    then:
      field: description
      function: truthy

  az-parameter-names-convention:
    description: Parameter names should conform to Azure naming conventions.
    message: '{{error}}'
    severity: warn
    given:
    - $.paths[*].parameters.*
    - $.paths.*[get,put,post,patch,delete,options,head].parameters.*
    then:
      function: param-names

  az-parameter-names-unique:
    description: All parameter names for an operation should be case-insensitive unique.
    message: '{{error}}'
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.paths[*]
    then:
      function: param-names-unique

  az-parameter-order:
    description: Path parameters must be in the same order as in the path.
    message: '{{error}}'
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.paths
    then:
      function: param-order

  # Patch request body content type should be application/merge-patch+json
  az-patch-content-type:
    description: The request body content type for patch operations should be JSON merge patch.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $
    then:
      function: patch-content-type

  # Patch on a path that does not end in path parameter is uncommon.
  az-patch-path:
    description: Patch on a path that does not end with a path parameter is uncommon.
    severity: info
    formats: ['oas2', 'oas3']
    given: $.paths[?(!@path.match(/\}']$/))]
    then:
      field: patch
      function: falsy

  # Static path segments should be kebab-case
  az-path-case-convention:
    description: Static path segments should be kebab-case.
    message: Static path segments should be kebab-case.
    severity: info
    formats: ['oas2', 'oas3']
    given: $.paths.*~
    then:
      function: pattern
      functionOptions:
        # Check each path segment individually and ignore param segments
        # Note: the ':' is only allowed in the final path segment
        match: '^(\/([a-z][a-z0-9-]+|{[^}]+}))*\/([a-z][a-z0-9-]+|{[^}]*})?(:[A-Za-z0-9]+)?$'

  # DO limit your URLs characters to 0-9 A-Z a-z - . _ ~ :
  az-path-characters:
    description: Path should contain only recommended characters.
    message: Path contains non-recommended characters.
    severity: info
    formats: ['oas2', 'oas3']
    given: $.paths.*~
    then:
      function: pattern
      functionOptions:
        # Check each path segment individually and ignore param segments
        # Note: the ':' is only allowed in the final path segment
        match: '^(\/([0-9A-Za-z._~-]+|{[^}]+}))*\/([0-9A-Za-z._~-]+|{[^}]*})?(:[0-9A-Za-z._~-]+)?$'

  az-path-parameter-names:
    description: Path parameter names should be consistent across all paths.
    message: '{{error}}'
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.paths
    then:
      function: path-param-names

  az-path-parameter-schema:
    description: 'Path parameter should be type: string and specify maxLength and pattern.'
    message: '{{error}}'
    severity: info
    formats: ['oas2', 'oas3']
    given:
    - $.paths[*].parameters[?(@.in == 'path')]
    - $.paths[*][get,put,post,patch,delete,options,head].parameters[?(@.in == 'path')]
    then:
      function: path-param-schema

  az-post-201-response:
    description: Using post for a create operation is discouraged.
    message: Using post for a create operation is discouraged.
    severity: warn
    formats: ['oas2']
    given: $.paths[*].post.responses
    then:
      field: '201'
      function: falsy

  az-property-default-not-allowed:
    description: A required property should not specify a default value.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given:
    - $.paths[*].[put,post,patch].parameters.[?(@.in == 'body')].schema
    - $.paths[*].[get,put,post,patch,delete].responses[*].schema
    then:
      function: property-default-not-allowed

  az-property-description:
    description: All schema properties should have a description.
    message: Property should have a description.
    severity: warn
    resolved: false
    given: $..properties[?(@object() && @.$ref == undefined)]
    then:
      field: description
      function: truthy

  # https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#172-casing
  az-property-names-convention:
    description: Property names should be camel case.
    message: Property name should be camel case.
    severity: warn
    # This rule can report false positives if run on the resolved spec.
    # Issue: https://github.com/stoplightio/spectral/issues/1316
    resolved: false
    given: $..[?(@.type === 'object' && @.properties)].properties.*~
    then:
      function: casing
      functionOptions:
        type: camel

  az-property-type:
    description: All schema properties should have a defined type.
    message: Property should have a defined type.
    severity: warn
    resolved: false
    # Exclude properties that contains allOf or oneOf or anyOf to avoid false positives.
    given: $..properties[?(@object() && @.$ref == undefined && @.allOf == undefined && @.oneOf == undefined && @.anyOf == undefined)]
    then:
      field: type
      function: truthy

  az-put-path:
    description: Put on a path that does not end with a path parameter is uncommon.
    severity: info
    formats: ['oas2', 'oas3']
    given: $.paths[*].put^~
    then:
      function: pattern
      functionOptions:
        match: '/\}$/'

  az-put-request-and-response-body:
    description: A PUT operation should use the same schema for the request and response body.
    severity: info
    formats: ['oas2']
    # Run on the unresolved document so that we compare the $ref'ed schema
    resolved: false
    given: $.paths[*].put
    then:
      function: put-request-and-response-body

  az-readonly-in-response-schema:
    description: Properties in response-only schemas should not be marked as readOnly true
    severity: warn
    formats: ['oas2']
    given: $.definitions[*]
    then:
      function: readonly-in-response-schema

  az-request-body-not-allowed:
    description: A get or delete operation must not accept a body parameter.
    severity: error
    formats: ['oas2']
    given:
    - $.paths[*].[get,delete].parameters[*]
    then:
      field: in
      function: pattern
      functionOptions:
        notMatch: '/^body$/'

  az-request-body-optional:
    description: Flag optional request body -- common oversight.
    message: The body parameter is not marked as required.
    severity: info
    formats: ['oas2']
    given:
    # Don't flag request body if it explicitly specifies required: false
    - $.paths[*].[put,post,patch].parameters.[?(@.in == 'body' && @.required == undefined)]
    then:
      function: falsy

  az-request-body-type:
    description: Request body schema must not be a bare array.
    severity: warn
    formats: ['oas2']
    given:
    - $.paths[*].[put,post,patch].parameters.[?(@.in == 'body')].schema
    then:
      field: type
      function: pattern
      functionOptions:
        notMatch: '/^array$/'

  az-response-body-type:
    description: Response body schema must not be a bare array.
    severity: warn
    formats: ['oas2', 'oas3']
    given: $.paths[*][*][responses][*].schema
    then:
      field: type
      function: pattern
      functionOptions:
        notMatch: '/^array$/'

  az-schema-description-or-title:
    description: All schemas should have a description or title.
    message: Schema should have a description or title.
    severity: warn
    formats: ['oas2', 'oas3']
    given:
    - $.definitions[?(!@.description && !@.title)]
    - $.components.schemas[?(!@.description && !@.title)]
    then:
      function: falsy

  az-schema-names-convention:
    description: Schema names should be Pascal case.
    message: Schema name should be Pascal case.
    severity: info
    formats: ['oas2']
    given: $.definitions.*~
    then:
      # Pascal case with optional "." separator
      function: pattern
      functionOptions:
        match: '^([A-Z][a-z0-9]+\.?)*[A-Z][a-z0-9]+$'

  az-schema-type-and-format:
    description: Schema should use well-defined type and format.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given:
    - $.paths[*].[put,post,patch].parameters.[?(@.in == 'body')].schema
    - $.paths[*].[get,put,post,patch,delete].responses[*].schema
    then:
      function: schema-type-and-format

  az-security-definition-description:
    description: A security definition should have a description.
    message: Security definition should have a description.
    severity: warn
    formats: ['oas2', 'oas3']
    given:
    - $.securityDefinitions[*]
    - $.components.securitySchemes[*]
    then:
      field: description
      function: truthy

  az-security-definitions:
    description: Security definitions must follow Azure conventions.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $
    then:
      function: security-definitions

  az-security-requirements:
    description: The security requirement should reference a defined security scheme.
    message: '{{error}}'
    severity: warn
    # Drop oas3 format for now
    formats: ['oas2']
    given:
    - $.security
    - $.paths.*[get,put,post,patch,delete,options,head].security
    then:
      function: security-requirements

  az-security-min-length:
    description: Security property should specify at least one security requirement.
    severity: warn
    formats: ['oas2', 'oas3']
    given:
    - $.security
    - $.paths.*[get,put,post,patch,delete,options,head].security
    then:
      function: length
      functionOptions:
        min: 1

  # All success responses except 204 should define a response body
  # ref: https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md#1321-put
  az-success-response-body:
    description: All success responses except 204 should define a response body.
    severity: warn
    formats: ['oas2']
    # list http methods explicitly to exclude head
    # exclude 202 to avoid duplication with az-lro-response-schema rule
    given: $.paths[*][get,put,post,patch,delete].responses[?(@property >= 200 && @property < 300 && @property != '202' && @property != '204')]
    then:
      field: schema
      function: truthy

  az-unused-definition:
    description: Potentially unused definition has been detected.
    severity: warn
    formats: ['oas2']
    resolved: false
    given: $.definitions
    then:
      function: unused-definition

  az-version-convention:
    description: API version should be a date in YYYY-MM-DD format, optionally suffixed with '-preview'.
    severity: error
    formats: ['oas2', 'oas3']
    given: $.info.version
    then:
      function: pattern
      functionOptions:
        match: '^\d\d\d\d-\d\d-\d\d(-preview)?$'

  az-version-policy:
    description: Specify API version using `api-version` query parameter, not in path.
    message: '{{error}}'
    severity: warn
    formats: ['oas2']
    given: $
    then:
      function: version-policy
