# ClickZetta 开放平台 OpenAPI 设计规范（Spectral 规则集）
# 由云器自行编写并发布（草稿）。适用于 /openapi/v2 下的全部管理接口。
# 级别约定：error = 发布前必须修复；warn = 新接口必须遵守，存量接口逐步整改；info = 推荐做法。
# 本地检查：npx @stoplight/spectral-cli lint openapi/*.yml --ruleset rules/yunqi-rules.yml
x-method: published
x-publisher: Yunqi (云器科技)
x-source: https://www.yunqi.tech/apis/rules/yunqi-rules.yml
formats:
  - oas3
extends:
  - ["spectral:oas", recommended]
documentationUrl: https://www.yunqi.tech/documents

aliases:
  Operation:
    - "$.paths[*][get,put,post,delete,patch]"
  JsonMediaType:
    - "$.paths[*][*].requestBody.content['application/json']"
    - "$.paths[*][*].responses[*].content['application/json']"

rules:
  # ---------- info 与 servers ----------
  yunqi-info-contact:
    description: info.contact 需要写明团队名称和对外邮箱。
    severity: warn
    given: "$.info"
    then:
      field: contact
      function: schema
      functionOptions:
        schema:
          type: object
          required: [name, email]

  yunqi-info-description-length:
    description: info.description 至少 50 个字符，说明接口用途和认证方式。
    severity: warn
    given: "$.info.description"
    then:
      function: length
      functionOptions:
        min: 50

  yunqi-servers-official-host:
    description: servers 只能使用官方生产域名（api.clickzetta.com、<region>.api.clickzetta.com、<region>.api.singdata.com），不能写 localhost、内网地址、模板变量或无法解析的 api.singdata.com。
    message: "{{value}} 不是官方 API 域名"
    severity: error
    given: "$.servers[*].url"
    then:
      function: pattern
      functionOptions:
        match: '^https://(api\.clickzetta\.com|[a-z0-9-]+\.api\.clickzetta\.com|[a-z0-9-]+\.api\.singdata\.com)(/[a-z0-9-]+)?$'

  yunqi-server-description:
    description: 每个 server 需要说明对应的区域或环境。
    severity: warn
    given: "$.servers[*]"
    then:
      field: description
      function: truthy

  # ---------- 路径 ----------
  yunqi-path-prefix:
    description: 开放平台接口路径必须以 /openapi/v2/ 开头。
    message: "{{property}} 不在 /openapi/v2/ 下"
    severity: error
    given: "$.paths"
    then:
      field: "@key"
      function: pattern
      functionOptions:
        match: "^/openapi/v2/"

  yunqi-path-segment-camel-case:
    description: 路径段使用 lowerCamelCase（如 getPublicKey、adhoc），不使用下划线或连字符。
    severity: warn
    given: "$.paths"
    then:
      field: "@key"
      function: pattern
      functionOptions:
        match: "^(/([a-z][a-zA-Z0-9]*|\\{[a-zA-Z][a-zA-Z0-9]*\\}))+$"

  yunqi-path-no-trailing-slash:
    description: 路径不能以 / 结尾。
    severity: error
    given: "$.paths"
    then:
      field: "@key"
      function: pattern
      functionOptions:
        notMatch: ".+/$"

  # ---------- 操作 ----------
  yunqi-operation-summary:
    description: 每个操作都要有 summary（建议用中文）。本规则只检查字段是否存在。
    severity: error
    given: "#Operation"
    then:
      field: summary
      function: truthy

  yunqi-operation-description:
    description: 每个操作都要有 description，说明行为、前置条件和副作用。
    severity: warn
    given: "#Operation"
    then:
      field: description
      function: truthy

  yunqi-operation-single-tag:
    description: 每个操作有且只有一个 tag，对应所属的 API。
    severity: error
    given: "#Operation"
    then:
      field: tags
      function: length
      functionOptions:
        min: 1
        max: 1

  yunqi-operation-id-camel-case:
    description: operationId 使用 lowerCamelCase 动宾结构，如 createVirtualCluster。
    severity: warn
    given: "#Operation.operationId"
    then:
      function: casing
      functionOptions:
        type: camel

  yunqi-operation-success-response:
    description: 每个操作必须定义 2xx 响应。
    severity: error
    given: "#Operation.responses"
    then:
      function: schema
      functionOptions:
        schema:
          anyOf:
            - required: ["200"]
            - required: ["201"]
            - required: ["202"]
            - required: ["204"]

  yunqi-operation-error-response:
    description: 每个操作至少定义一个 4xx 响应（签名错误、参数错误、资源不存在等）。
    severity: warn
    given: "#Operation.responses"
    then:
      function: schema
      functionOptions:
        schema:
          anyOf:
            - required: ["400"]
            - required: ["401"]
            - required: ["403"]
            - required: ["404"]
            - required: ["409"]
            - required: ["429"]

  yunqi-operation-rate-limit-response:
    description: 推荐为每个操作声明 429 响应。本规则只检查是否声明了 429，不检查 Retry-After 响应头。
    severity: info
    given: "#Operation.responses"
    then:
      field: "429"
      function: truthy

  yunqi-delete-not-get:
    description: 删除、停止、下线等有副作用的操作不能用 GET。
    severity: error
    given: "$.paths[?(@property.match(/\\/(delete|remove|stop|kill|offline|disable|suspend)[A-Za-z]*$/))]"
    then:
      field: get
      function: falsy

  # ---------- 认证 ----------
  yunqi-security-aksk:
    description: 根级 security 必须同时引用 AccessKeyID 和 RequestSignature（AK/SK 请求签名）。
    severity: error
    given: "$"
    then:
      field: security
      function: schema
      functionOptions:
        schema:
          type: array
          contains:
            type: object
            required: [AccessKeyID, RequestSignature]

  yunqi-security-scheme-header-only:
    description: 认证信息只能放在请求头，不能放在 query string。
    severity: error
    given: "$.components.securitySchemes[?(@.type == 'apiKey')]"
    then:
      field: in
      function: enumeration
      functionOptions:
        values: [header]

  yunqi-security-scheme-description:
    description: 每个 securityScheme 都要说明取值方式和签名算法。
    severity: warn
    given: "$.components.securitySchemes[*]"
    then:
      field: description
      function: truthy

  # ---------- 请求与响应 ----------
  yunqi-json-only:
    description: 请求和响应统一使用 application/json。
    severity: warn
    given:
      - "#Operation.requestBody.content"
      - "#Operation.responses[?(@property.match(/^2/))].content"
    then:
      field: "@key"
      function: enumeration
      functionOptions:
        values: [application/json, application/problem+json]

  yunqi-request-body-ref:
    description: 请求体 schema 使用 components/schemas 中的命名模型（$ref），便于复用和生成 SDK。
    resolved: false
    severity: warn
    given: "#Operation.requestBody.content['application/json'].schema"
    then:
      field: "$ref"
      function: truthy

  yunqi-media-type-example:
    description: 推荐为 JSON 请求和响应提供 example 或 examples。
    severity: info
    given: "#JsonMediaType"
    then:
      function: schema
      functionOptions:
        schema:
          anyOf:
            - required: [example]
            - required: [examples]

  yunqi-error-problem-json:
    description: 推荐 4xx/5xx 响应使用 RFC 9457 application/problem+json。
    severity: info
    given: "#Operation.responses[?(@property.match(/^[45]/))].content"
    then:
      field: application/problem+json
      function: truthy

  # ---------- 数据模型 ----------
  yunqi-property-camel-case:
    description: 字段名使用 lowerCamelCase，例如 workspaceId、vclusterName。
    severity: warn
    given: "$.components.schemas[*].properties"
    then:
      field: "@key"
      function: casing
      functionOptions:
        type: camel

  yunqi-schema-description:
    description: 推荐为 components/schemas 中的每个模型写 description。
    severity: info
    given: "$.components.schemas[*]"
    then:
      field: description
      function: truthy

  yunqi-parameter-description:
    description: 每个参数都要有 description。
    severity: warn
    given: "#Operation.parameters[?(@.in)]"
    then:
      field: description
      function: truthy

  yunqi-tag-description:
    description: 根级 tags 需要有 description。
    severity: warn
    given: "$.tags[*]"
    then:
      field: description
      function: truthy

# ---------- 按 API 例外 ----------
# Analytics Agent 开放 API 是独立产品线：路径在 /open/ 下、用 loginToken 认证，不适用 /openapi/v2 的路径和 AK/SK 规则。
# loginToken 目前只能放在 query string，降为 warn 保留提示，后端支持请求头传递后恢复为 error。
overrides:
  - files:
      - "../openapi/yunqi-analytics-agent-api-openapi.yml#"
    rules:
      yunqi-path-prefix: "off"
      yunqi-path-segment-camel-case: "off"
      yunqi-security-aksk: "off"
      yunqi-security-scheme-header-only: warn
      # 该 API 以 HTTP 200 加 success=false 表示业务错误，字段名 model_name 由接口定义，均属已知例外。
      yunqi-operation-error-response: "off"
      yunqi-property-camel-case: "off"
  - files:
      - "../openapi/yunqi-ai-gateway-api-openapi.yml#"
    rules:
      # AI Gateway 兼容 OpenAI、Anthropic 和火山引擎协议，路径、字段名（snake_case）和流式响应都由这些协议决定。
      yunqi-path-prefix: "off"
      yunqi-path-segment-camel-case: "off"
      yunqi-security-aksk: "off"
      yunqi-property-camel-case: "off"
      yunqi-json-only: "off"
      # AI Gateway 的错误格式是 { error: { code, message, source } }，不使用 RFC 9457。
      yunqi-error-problem-json: "off"
