Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Configuration as code

Mocks can be declared in a rustybin.yaml file next to your specs, committed with your demo or test setup, loaded at startup, reloaded without a restart, and exported from mocks you built in the console.

Loading mocks at startup

SourceHow
A config filerustybin --config path/to/rustybin.yaml, else RUSTYBIN_CONFIG_FILE=path/to/rustybin.yaml, else a rustybin.yaml in the working directory when there is one
A directory of specsRUSTYBIN_MOCKS_DIR=path/to/specs: every *.yaml, *.yml and *.json file (at most 200, not recursive) becomes a mock with default settings, named after the file (Orders API.yaml becomes orders-api)
The built-in sampleRUSTYBIN_MOCKS_SAMPLE (default true, false in public mode)

Both can be used together; a mock in the config file wins over a spec file of the same name in the directory. Nothing stops the server from starting: a file that cannot be read or parsed, an unset environment variable or a spec that does not compile is logged as a warning and listed in load_errors of GET /_rustybin/mocks (and in the console), and the other mocks load normally. File-defined mocks are deployed even when their deploy checks report errors (the report shows them) and never expire. Their origin is file.

Here is the file the documentation's example server runs with (via RUSTYBIN_CONFIG_FILE), next to specs/inventory.yaml, plus RUSTYBIN_MOCKS_DIR pointing at a directory holding weather.yaml:

# yaml-language-server: $schema=http://localhost:8080/_rustybin/mocks/config.schema.json
#
# Loaded by the docs example server (docs/examples/run.sh) through
# RUSTYBIN_CONFIG_FILE, and shown in docs/src/mocks/config.md.
version: 1
mocks:
  - name: inventory
    spec: specs/inventory.yaml            # relative to this file
    strip_server_prefix: true             # /mock/inventory/items, not /mock/inventory/v2/items
    responses:
      mode: generated
      seed: 42
      validate_requests: enforce
    security:
      enabled: true
      mode: api_key
      api_key:
        header: x-api-key
        keys: ["${INVENTORY_API_KEY:-inventory-key}"]
    operations:
      health:
        public: true
      getItem:
        rules:
          - name: sold out
            when: { path.sku: { equals: SKU-0 } }
            respond: { status: 404 }
GET {{base_url}}/_rustybin/mocks/inventory
HTTP 200
[Asserts]
jsonpath "$.origin" == "file"
jsonpath "$.state" == "live"
jsonpath "$.url" == "{{base_url}}/mock/inventory"
jsonpath "$.settings.responses.mode" == "generated"
jsonpath "$.settings.security.mode" == "api_key"

GET {{base_url}}/_rustybin/mocks/weather
HTTP 200
[Asserts]
jsonpath "$.origin" == "file"
jsonpath "$.title" == "Weather"
# The key comes from ${INVENTORY_API_KEY:-inventory-key}.
GET {{base_url}}/mock/inventory/items
x-api-key: docs-inventory-key
HTTP 200
[Asserts]
jsonpath "$" isCollection
jsonpath "$[0].sku" matches /^SKU-[0-9]+$/

GET {{base_url}}/mock/inventory/items/SKU-0
x-api-key: docs-inventory-key
HTTP 404
[Asserts]
header "X-Rustybin-Mock-Source" startsWith "rule:sold out"

# validate_requests: enforce
GET {{base_url}}/mock/inventory/items/42
x-api-key: docs-inventory-key
HTTP 400
[Asserts]
jsonpath "$.errors[0].pointer" == "/path/sku"

# health is public
GET {{base_url}}/mock/inventory/health
HTTP 200

GET {{base_url}}/mock/weather/forecast/oslo
HTTP 200
[Asserts]
jsonpath "$.city" == "oslo"

Reference

version: 1                       # optional, must be 1
mocks:
  - name: orders                 # required: a-z, 0-9 and '-', 1 to 40 characters
    spec: specs/orders.yaml      # a spec file, relative to this file (or absolute)
    # spec_inline: {...}         # or the spec itself (object or text), not both
    enabled: true                # the on/off switch
    strip_server_prefix: false   # serve /orders, not /api/orders
    responses:
      mode: examples             # examples | generated | static
      seed: 1                    # generated data
      delay_ms: 0                # before every response (max 30000, 10000 in public mode)
      validate_requests: warn    # off | warn | enforce
      disabled_status: 503       # status while switched off (400 to 599)
    security:
      enabled: false
      mode: spec                 # spec | api_key | basic | bearer | jwt
      api_key:
        header: x-api-key        # the header of the fixed api_key mode
        keys: [demo-key]
      basic:
        users: { demo: demo }
      bearer:
        tokens: [demo-token]
      jwt:
        issuer: http://localhost:8080   # optional iss check
        audience: rustybin              # optional aud check
    operations:                  # keyed by operationId
      getOrder:
        mode: generated
        delay_ms: 150
        public: false
        respond: { status: 200, example: big, headers: { X-Cache: miss } }
        rules:
          - name: missing order
            when: { path.orderId: { equals: 0 } }
            respond: { status: 404 }
          - when: { header.x-tier: { equals: gold }, query.expand: { exists: true } }
            respond: { status: 200, body: { orderId: 1, total: 0 } }

Every field except name (and one of spec / spec_inline) is optional, with the defaults shown. Unknown fields are errors, so a typo does not go unnoticed. The response fields are described in Responses, rules and validation, the security fields in Mocked security.

Editor completion

GET /_rustybin/mocks/config.schema.json is the JSON Schema of the file. With the YAML language server (VS Code's YAML extension, Neovim, JetBrains IDEs), a comment on the first line gives completion, hover documentation and validation:

# yaml-language-server: $schema=http://localhost:8080/_rustybin/mocks/config.schema.json
version: 1
mocks: []
GET {{base_url}}/_rustybin/mocks/config.schema.json
HTTP 200
[Asserts]
jsonpath "$.title" == "Rustybin mocks file (rustybin.yaml)"
jsonpath "$.properties.mocks.items.required" contains "name"

Environment variables

In files read from disk (the config file, and rustybin mock push / check-config), ${VAR} is replaced by the environment variable VAR and ${VAR:-default} by default when VAR is unset. An unset variable without a default becomes an empty string and is reported as a load problem. Use it for credentials:

security:
  enabled: true
  api_key:
    keys: ["${ORDERS_API_KEY}", "${ORDERS_API_KEY_2:-backup-key}"]

Documents sent to the API (POST /_rustybin/mocks/apply) are never expanded.

Reloading

POST /_rustybin/mocks/reload (admin) and the SIGHUP signal re-read the config file and the mocks directory and apply the difference: new mocks are created, changed ones are redeployed (their traffic history is kept), unchanged ones are left alone, and file-defined mocks that are no longer declared are deleted. Mocks created through the console or the API are never deleted by a reload.

POST {{base_url}}/_rustybin/mocks/reload
Authorization: Bearer {{admin_token}}
HTTP 200
[Asserts]
jsonpath "$.report.unchanged" contains "inventory"
jsonpath "$.report.unchanged" contains "weather"
jsonpath "$.problems" isEmpty
kill -HUP "$(pidof rustybin)"            # or: docker kill --signal HUP rustybin

Export and apply

GET /_rustybin/mocks/{name}/export returns one mock, and GET /_rustybin/mocks/export every mock except the built-in sample, as a rustybin.yaml document with the spec inlined (spec_inline) and every setting spelled out. It describes the draft, so it includes changes that are not deployed yet. The console offers the same export, so "build it in the console, export it, commit the file" is a supported workflow.

GET {{base_url}}/_rustybin/mocks/docs-hello/export
HTTP 200
[Asserts]
header "Content-Type" == "application/yaml; charset=utf-8"
header "Content-Disposition" == "inline; filename=\"docs-hello.rustybin.yaml\""
body startsWith "# Rustybin mocks (rustybin.yaml)"
body contains "- name: docs-hello"
body contains "spec_inline:"
body contains "validate_requests: warn"

GET {{base_url}}/_rustybin/mocks/export
HTTP 200
[Asserts]
body contains "- name: docs-hello"
body contains "- name: inventory"
body not contains "- name: petstore"

POST /_rustybin/mocks/apply (admin) applies such a document (YAML or JSON) to a running instance: listed mocks are created, or updated and redeployed when their spec or settings differ, and reported as created, updated, unchanged; problems are listed in errors (a changed mock is deployed even with check errors, which are named there). With ?prune=true, mocks created through the console or the API that the document does not list are deleted (deleted); file-defined mocks and the sample are never pruned. This is what rustybin mock push calls.

POST {{base_url}}/_rustybin/mocks/apply
Authorization: Bearer {{admin_token}}
Content-Type: application/yaml

version: 1 mocks:

  • name: docs-hello responses: { mode: examples } spec_inline: openapi: 3.1.0 info: { title: Hello, version: "1" } paths: /hello: get: responses: "200": description: A greeting content: application/json: example: { message: hello }
  • name: docs-goodbye enabled: false spec_inline: | openapi: 3.0.3 info: { title: Goodbye, version: "1" } paths: {}
HTTP 200
[Asserts]
jsonpath "$.created" contains "docs-hello"
jsonpath "$.created" contains "docs-goodbye"
jsonpath "$.errors" isEmpty

GET {{base_url}}/mock/docs-hello/hello
HTTP 200
[Asserts]
header "X-Rustybin-Mock-Operation" == "get_hello"
jsonpath "$.message" == "hello"

GET {{base_url}}/mock/docs-goodbye/anything
HTTP 503
# Only docs-hello is listed: prune deletes the other mocks created through
# the API (file-defined mocks and the sample are never pruned).
POST {{base_url}}/_rustybin/mocks/apply?prune=true
Authorization: Bearer {{admin_token}}
Content-Type: application/json
{"version": 1, "mocks": [{"name": "docs-hello", "spec_inline": {"openapi": "3.1.0", "info": {"title": "Hello", "version": "1"}, "paths": {}}}]}
HTTP 200
[Asserts]
jsonpath "$.updated" contains "docs-hello"
jsonpath "$.deleted" contains "docs-goodbye"
jsonpath "$.deleted" not contains "inventory"

The API only accepts spec_inline: a server never reads files named by a request. rustybin mock push reads the spec files locally and inlines them before sending the document.

# Documents sent to the API cannot reference files on the server.
POST {{base_url}}/_rustybin/mocks/apply
Authorization: Bearer {{admin_token}}
Content-Type: application/yaml

mocks:

  • name: docs-nope spec: /etc/passwd
HTTP 200
[Asserts]
jsonpath "$.created" isEmpty
jsonpath "$.errors[0].name" == "docs-nope"
jsonpath "$.errors[0].error" contains "send spec_inline"

Docker

The image's working directory is /app and it runs as uid 10001, so the simplest setup mounts the file as /app/rustybin.yaml (found without any setting) and the specs next to it, read-only:

demo/
  rustybin.yaml        # spec: specs/orders.yaml
  specs/
    orders.yaml
docker run -d --name rustybin -p 8080:80 \
  -v "$PWD/demo/rustybin.yaml:/app/rustybin.yaml:ro" \
  -v "$PWD/demo/specs:/app/specs:ro" \
  rustybin

With only a folder of specs and default settings:

docker run -d --name rustybin -p 8080:80 \
  -e RUSTYBIN_MOCKS_DIR=/app/specs \
  -v "$PWD/specs:/app/specs:ro" \
  rustybin

The files must be readable by uid 10001 (world-readable files are). After editing them, docker kill --signal HUP rustybin reloads the mocks without a restart.

With Docker Compose, the repository's docker-compose.yml has both variants commented out; enabled, the service looks like this:

services:
  rustybin:
    build: .
    container_name: rustybin
    ports:
      - "80:80"
    environment:
      - ORDERS_API_KEY=change-me          # used as ${ORDERS_API_KEY} in rustybin.yaml
      # - RUSTYBIN_MOCKS_DIR=/app/specs   # or: every spec in /app/specs, default settings
    volumes:
      - rustybin-certs:/app/certs
      - ./rustybin.yaml:/app/rustybin.yaml:ro
      - ./specs:/app/specs:ro
volumes:
  rustybin-certs:

docker compose kill -s HUP rustybin reloads. To manage mocks from inside the container, see the CLI.