openapi: 3.1.0

info:
  title: queno.es
  version: '1.0.0'
  summary: Una negativa que quepa donde tiene que caber.
  description: |
    API pública de queno.es. Sin registro, sin clave y con CORS abierto.

    Sirve **negativas** por defecto y otros tipos de frase con `tipo` — hoy también **excusas**.

    El contrato entero cabe en una frase: *«dame una frase de como mucho este tamaño»*. Nunca
    devuelve algo más largo de lo pedido — si no hay ninguna que quepa, responde 404, que es
    preferible a devolver una que no cabe.

    **Límite de uso:** una petición cada dos segundos por dirección IP, sin ráfaga. La segunda
    llamada seguida recibe un `429` con `Retry-After`. Cada negativa trae su enlace permanente,
    así que lo razonable es pedirla una vez y guardarla.

    Uso libre citando queno.es.
  contact:
    url: https://queno.es/api/docs

servers:
  - url: https://queno.es
    description: El servidor que ha servido este documento.

security: []

paths:
  /api:
    get:
      operationId: negativa
      summary: Devuelve una frase
      description: |
        De entre las que caben devuelve **la más larga**, que es la que mejor aprovecha el hueco.

        Nunca devuelve una negativa de folio: los párrafos largos son para la web, donde tienen
        su propia composición. Aquí siempre se sirven frases cortas.
      parameters:
        - name: c
          in: query
          required: false
          description: |
            Caracteres como máximo. El mínimo es 2 porque «no» es la
            negativa más corta que existe: por debajo no hay nada que devolver.
          schema:
            type: integer
            minimum: 2
            maximum: 256
          example: 40
        - name: p
          in: query
          required: false
          description: |
            Palabras como máximo. No se combina con `c`: pedir las dos cosas a la vez es un
            error, no una intersección.
          schema:
            type: integer
            minimum: 1
            maximum: 64
          example: 3
        - name: tipo
          in: query
          required: false
          description: |
            Qué clase de frase se quiere. Sin este parámetro se devuelve una **negativa**, que
            es lo que esta API devolvía antes de que existieran los tipos.
          schema:
            type: string
            enum: [negativa, excusa, insulto]
            default: negativa
        - name: lang
          in: query
          required: false
          description: Idioma de la frase.
          schema:
            type: string
            enum: [en, es]
            default: es
        - name: nsfw
          in: query
          required: false
          description: Deja entrar las groseras. Ausente o `0`, no salen.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Una negativa que cabe en lo pedido.
          headers:
            Cache-Control:
              description: |
                Siempre `no-store`. El resultado es aleatorio: cachearlo lo convertiría en una
                constante. La página del enlace permanente sí se cachea.
              schema:
                type: string
            Access-Control-Allow-Origin:
              description: Siempre `*`. Está pensada para llamarse desde el navegador.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Frase'
        '400':
          description: |
            Petición mal formada: tipo o idioma que no existen, `c` o `p` fuera de rango, o las
            dos medidas a la vez.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            No hay ninguna frase de ese tipo que quepa en ese tamaño. Es una respuesta
            legítima, no un fallo: quien pidió 20 caracteres los pidió por algo.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Demasiadas peticiones. Una cada dos segundos por IP.
          headers:
            Retry-After:
              description: Segundos que faltan para la siguiente.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    Frase:
      type: object
      required: [texto, tipo, idioma, caracteres, palabras, nsfw, codigo, url]
      properties:
        texto:
          type: string
          description: La frase. Es lo único que hace falta para mostrarla.
          example: 'Ni de coña.'
        tipo:
          type: string
          enum: [negativa, excusa, insulto]
          description: Qué clase de frase es. Coincide siempre con el `tipo` pedido.
          example: negativa
        idioma:
          type: string
          enum: [en, es]
          example: es
        caracteres:
          type: integer
          description: Longitud real. Nunca mayor que la `c` pedida.
          example: 11
        palabras:
          type: integer
          description: Palabras reales. Nunca mayor que la `p` pedida.
          example: 3
        nsfw:
          type: boolean
          description: Si es grosera. Solo puede ser `true` si se pidió con `nsfw=1`.
          example: false
        codigo:
          type: string
          description: Identificador permanente de esta frase.
          pattern: '^[0-9A-Za-z]{7}$'
          example: aB3xK9p
        url:
          type: string
          format: uri
          description: |
            Su página propia, para poder enlazarla. Esa sí es estable y cacheable. Cada tipo
            vive bajo su propio prefijo: las negativas en la raíz, las excusas en `/e/`.
          example: 'https://queno.es/aB3xK9p'
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: |
            Qué ha pasado, en lenguaje llano y con el rango correcto cuando el fallo es de rango.
          example: 'Pide por caracteres (c) o por palabras (p), no por las dos cosas.'
