openapi: 3.0.3
info:
  title: Bliss translator API
  version: 2.1.0
  description: |
    Translates English into Blissymbolics with BCI grammar indicators, draws
    Bliss symbols and sentence strips, and searches AAC symbol libraries
    (Blissymbolics, Mulberry, ARASAAC). Bliss drawings and glosses: parts
    herein (c) Blissymbolics Communication International (BCI), distributed
    under the Creative Commons BY-SA 4.0 licence.
  contact:
    name: Luke Steuber
    email: luke@lukesteuber.com
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
servers:
  - url: http://localhost:7778
    description: Local development
  - url: https://lukesteuber.com/bliss
    description: Production

# A key is optional everywhere; it raises the search rate limit.
security:
  - {}
  - BearerKey: []

paths:
  /api/health:
    get:
      summary: Health check
      description: Returns service status including database connectivity. No auth required.
      security: []
      responses:
        "200":
          description: Service status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [healthy, degraded]
                  timestamp:
                    type: string
                    format: date-time
                  version:
                    type: string
                  database:
                    type: string
                  symbol_count:
                    type: integer

  /api/search:
    get:
      summary: Search symbols across all libraries
      description: |
        Full-text search using SQLite FTS5 with BM25 ranking. A key
        (Authorization: Bearer) is optional and raises the rate limit; an
        invalid key returns 401. q is limited to 100 characters; language
        must be a code such as en, es or pt-BR.
      parameters:
        - name: q
          in: query
          required: true
          description: Search term
          schema:
            type: string
        - name: limit
          in: query
          description: Max results per source (default 5, max 20)
          schema:
            type: integer
            minimum: 1
            maximum: 20
            default: 5
        - name: sources
          in: query
          description: Comma-separated source list
          schema:
            type: string
            default: local,arasaac
        - name: language
          in: query
          description: Language code for ARASAAC
          schema:
            type: string
            default: en
        - name: nocache
          in: query
          description: Bypass cache (true/false)
          schema:
            type: string
            default: "false"
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SearchResponse"
        "400":
          description: Missing required parameter
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/search/batch:
    post:
      summary: Batch search for multiple terms
      description: |
        Search up to 10 terms in a single request. A key is optional.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BatchSearchRequest"
      responses:
        "200":
          description: Batch search results
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BatchSearchResponse"
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/metadata:
    get:
      summary: Symbol source metadata and counts
      description: Returns counts and metadata for each symbol source. No auth required.
      security: []
      responses:
        "200":
          description: Source metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  name:
                    type: string
                  version:
                    type: string
                  database:
                    type: string
                  total_symbols:
                    type: integer
                  total_keywords:
                    type: integer
                  sources:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        license:
                          type: string
                        license_url:
                          type: string
                        symbol_count:
                          type: integer

  /api/cache:
    get:
      summary: Cache statistics
      description: Returns cache hit/miss rates. Optionally clear cache with ?clear=all|local|arasaac. No auth required.
      security: []
      parameters:
        - name: clear
          in: query
          description: Clear cache (all, local, or arasaac)
          schema:
            type: string
            enum: [all, local, arasaac]
      responses:
        "200":
          description: Cache statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: boolean
                  local:
                    $ref: "#/components/schemas/CacheStats"
                  arasaac:
                    $ref: "#/components/schemas/CacheStats"

  /api/translate:
    get:
      summary: Translate English into Bliss
      security: []
      parameters:
        - {name: q, in: query, required: true, schema: {type: string, maxLength: 1000}}
        - {name: grammar, in: query, schema: {type: boolean, default: true}}
        - {name: punctuation, in: query, schema: {type: boolean, default: true}}
        - {name: articles, in: query, schema: {type: boolean, default: false}}
        - {name: question_style, in: query, schema: {$ref: "#/components/schemas/QuestionStyle"}}
        - {name: progressive, in: query, schema: {$ref: "#/components/schemas/Progressive"}}
        - {name: names, in: query, schema: {$ref: "#/components/schemas/NameStyle"}}
        - {name: plural_after_number, in: query, schema: {type: boolean, default: true}}
        - {name: command_with_please, in: query, schema: {type: boolean, default: false}}
        - {name: alternatives, in: query, schema: {type: integer, minimum: 0, maximum: 12, default: 6}}
      responses:
        "200": {description: Sentences with Bliss units, content: {application/json: {schema: {$ref: "#/components/schemas/TranslateResponse"}}}}
        "400": {description: Missing text or an unknown option value}
        "413": {description: Text longer than 1000 characters}
    post:
      summary: Translate English into Bliss
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: {type: string, maxLength: 1000}
                options: {$ref: "#/components/schemas/TranslateOptions"}
      responses:
        "200": {description: Sentences with Bliss units, content: {application/json: {schema: {$ref: "#/components/schemas/TranslateResponse"}}}}
        "400": {description: Missing text or an unknown option value}
        "413": {description: Text longer than 1000 characters}
  /api/bliss/render:
    get:
      summary: One Bliss symbol as SVG, with an indicator replaced, added or removed
      security: []
      parameters:
        - {name: id, in: query, description: BCI-AV number, schema: {type: integer}}
        - {name: key, in: query, description: Drawing name such as eat-(to), schema: {type: string}}
        - {name: indicator, in: query, description: "Indicator kind (past_action, plural...), or none to remove; omit to draw as published", schema: {type: string}}
      responses:
        "200": {description: SVG, content: {image/svg+xml: {schema: {type: string}}}}
        "400": {description: Unknown indicator}
        "404": {description: Unknown symbol}
  /api/bliss/strip:
    post:
      summary: A sentence strip as one SVG
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                labels: {type: boolean, default: true}
                items:
                  type: array
                  maxItems: 80
                  items:
                    type: object
                    properties:
                      id: {type: integer}
                      key: {type: string}
                      indicator: {type: string}
                      label: {type: string, description: "English under the symbol; omit for the drawing's name, send an empty string for none. Drawn as text when there is no id or key"}
                      join: {type: boolean, default: false, description: "Continue the previous item's Bliss-word (in-word spacing, one label under the word)"}
      responses:
        "200": {description: SVG, content: {image/svg+xml: {schema: {type: string}}}}
  /api/bliss/lookup:
    get:
      summary: Bliss entries for an English word, best first
      security: []
      parameters:
        - {name: q, in: query, required: true, schema: {type: string}}
        - {name: pos, in: query, description: "NOUN, VERB, ADJ, ADV, PRON...", schema: {type: string}}
      responses:
        "200": {description: Matching entries}
  /api/boards:
    post:
      summary: Create a communication board; the response includes edit_token once
      security: []
      responses:
        "201": {description: Board created}
    get:
      summary: Summaries of the boards named in ids (there is no list of all boards)
      security: []
      parameters:
        - {name: ids, in: query, required: true, schema: {type: string}, description: Comma-separated board IDs}
      responses:
        "200": {description: Board summaries}
        "400": {description: ids missing}
  /api/boards/{board_id}:
    parameters:
      - {name: board_id, in: path, required: true, schema: {type: string}}
    get:
      summary: A board with its cells (anyone with the ID can view it)
      security: []
      responses:
        "200": {description: Board}
        "404": {description: Not found}
    put:
      summary: Update a board's title or size
      security: []
      parameters:
        - {name: X-Board-Token, in: header, required: true, schema: {type: string}}
      responses:
        "200": {description: Updated}
        "403": {description: Missing or wrong edit token}
    delete:
      summary: Delete a board and its cells
      security: []
      parameters:
        - {name: X-Board-Token, in: header, required: true, schema: {type: string}}
      responses:
        "200": {description: Deleted}
        "403": {description: Missing or wrong edit token}
  /api/boards/{board_id}/cells:
    parameters:
      - {name: board_id, in: path, required: true, schema: {type: string}}
    put:
      summary: Replace a board's cells
      security: []
      parameters:
        - {name: X-Board-Token, in: header, required: true, schema: {type: string}}
      responses:
        "200": {description: Saved}
        "403": {description: Missing or wrong edit token}
  /api/openapi.yaml:
    get:
      summary: OpenAPI specification
      security: []
      responses:
        "200":
          description: YAML spec
          content:
            text/yaml:
              schema:
                type: string

  /symbols/{filename}:
    get:
      summary: Serve Blissymbolics symbol file
      security: []
      parameters:
        - name: filename
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Symbol image (SVG)
          content:
            image/svg+xml:
              schema:
                type: string
                format: binary

  /symbols/mulberry/{filename}:
    get:
      summary: Serve Mulberry symbol file
      security: []
      parameters:
        - name: filename
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Symbol image (SVG)
          content:
            image/svg+xml:
              schema:
                type: string
                format: binary

components:
  securitySchemes:
    BearerKey:
      type: http
      scheme: bearer
      description: |
        Optional API key sent as Authorization: Bearer <key>. Search rate limit tiers:
        - free: 10 requests/minute
        - basic: 60 requests/minute
        - premium: 300 requests/minute

  schemas:
    QuestionStyle:
      type: string
      enum: [opener, mark, both]
      default: opener
      description: "opener: QUESTION first and a full stop last (BCI Fundamental Rules 4.3.8); mark: question mark last; both"
    Progressive:
      type: string
      enum: [action, present_action]
      default: action
    NameStyle:
      type: string
      enum: [text, letters]
      default: text
      description: "text: English letters; letters: BCI-AV letter characters"
    TranslateOptions:
      type: object
      properties:
        grammar: {type: boolean, default: true}
        punctuation: {type: boolean, default: true}
        articles: {type: boolean, default: false}
        question_style: {$ref: "#/components/schemas/QuestionStyle"}
        progressive: {$ref: "#/components/schemas/Progressive"}
        names: {$ref: "#/components/schemas/NameStyle"}
        plural_after_number: {type: boolean, default: true}
        command_with_please: {type: boolean, default: false}
        alternatives: {type: integer, minimum: 0, maximum: 12, default: 6}
        use_model: {type: boolean, default: false}
    TranslateResponse:
      type: object
      properties:
        text: {type: string}
        options: {$ref: "#/components/schemas/TranslateOptions"}
        sentences:
          type: array
          items:
            type: object
            properties:
              text: {type: string}
              type: {type: string, enum: [statement, question, wh-question, command, exclamation, word], description: "word: one word typed without punctuation; no sentence markers are added"}
              units: {type: array, items: {type: object, description: "kind, provenance, confidence, symbols, notes, alternatives"}}
              skipped: {type: array, items: {type: object, properties: {text: {type: string}, reason: {type: string}}}}
        summary: {type: object}
        attribution: {type: string}
    Error:
      type: object
      properties:
        error:
          type: string
      required: [error]

    SearchResult:
      type: object
      properties:
        keyword:
          type: string
        icon_url:
          type: string
        score:
          type: number
          minimum: 0
          maximum: 1
        source:
          type: string
        license:
          type: string
        author:
          type: string
        attribution:
          type: string

    SearchResponse:
      type: object
      properties:
        query:
          type: string
        total:
          type: integer
        results:
          type: array
          items:
            $ref: "#/components/schemas/SearchResult"

    BatchSearchRequest:
      type: object
      required: [queries]
      properties:
        queries:
          type: array
          items:
            type: string
          minItems: 1
        limit:
          type: integer
          minimum: 1
          maximum: 20
          default: 5
        sources:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
          default: ["local", "arasaac"]
        language:
          type: string
          default: en
        nocache:
          type: boolean
          default: false

    BatchSearchResponse:
      type: object
      properties:
        total:
          type: integer
        results:
          type: array
          items:
            type: object
            properties:
              query:
                type: string
              total:
                type: integer
              results:
                type: array
                items:
                  $ref: "#/components/schemas/SearchResult"

    CacheStats:
      type: object
      properties:
        enabled:
          type: boolean
        entries:
          type: integer
        ttl_seconds:
          type: integer
        max_entries:
          type: integer
        hits:
          type: integer
        misses:
          type: integer
        evictions:
          type: integer
