Collection Reuse with $ref

Slumber supports a $ref anywhere in any YAML file that allows referencing any other part of a YAML document (including other files). It uses the JSON Reference and JSON Pointer notation used by OpenAPI.

The format of the $ref is a URI with an optional base/path. The base can be:

  • Empty, indicating a reference within the same file
  • A file path, indicating a reference to another file
    • Path is always relative to the importing file
requests:
  list_fish:
    method: GET
    url: "{{ host }}/fishes"

  get_fish:
    method:
      $ref: "#/requests/list_fish/method"
    url: "{{ host }}/fishes/{{ fish_id }}"

The reference source is everything before the #; the pointer is everything after.

The Problem

Let's start with an example of something that sucks. Let's say you're making requests to a fish-themed JSON API, and it requires authentication. Gotta protect your fish! Your request collection might look like so:

profiles:
  local:
    data:
      host: http://localhost:3000
      fish_id: 6
  production:
    data:
      host: https://myfishes.fish
      fish_id: 6

requests:
  list_fish:
    method: GET
    url: "{{ host }}/fishes"
    query:
      big: true
    headers:
      Accept: application/json
    authentication:
      type: bearer
      token: "{{ file('./api_token.txt') | trim() }}"

  get_fish:
    method: GET
    url: "{{ host }}/fishes/{{ fish_id }}"
    headers:
      Accept: application/json
    authentication:
      type: bearer
      token: "{{ file('./api_token.txt') | trim() }}"

The Solution

You've heard of DRY, so you know this is bad. Every profile has to include the fish ID, and every new request recipe has to copy-paste the authentication and headers.

You can easily reuse components of your collection using $ref:

# The name here is arbitrary, pick any name you like. Make sure it starts with
# . to avoid errors about an unknown field
.base_profile_data:
  fish_id: 6
.base_request:
  headers:
    Accept: application/json
  authentication:
    type: bearer
    token: "{{ file('./api_token.txt') | trim() }}"

profiles:
  local:
    data:
      $ref: "#/.base_profile_data"
      host: http://localhost:3000
  production:
    data:
      $ref: "#/.base_profile_data"
      host: https://myfishes.fish

requests:
  list_fish:
    $ref: "#/.base_request"
    method: GET
    url: "{{ host }}/fishes"
    query:
      big: true

  get_fish:
    $ref: "#/.base_request"
    method: GET
    url: "{{ host }}/fishes/{{ fish_id }}"

Great! That's so much cleaner. Now each recipe can inherit whatever base properties you want just by including $ref: "#/.base_request". This is still a bit repetitive, but it has the advantage of being explicit. You may have some requests that don't want to include those values.

Recursive Composition

But wait! What if you have a new request that needs an additional header? Unfortunately, $ref does not support recursive merging. If you need to extend the headers map from the base request, you'll need to pull the parent headers in manually:

.base_request:
  headers:
    Accept: application/json
  authentication:
    type: bearer
    token: "{{ file('./api_token.txt') | trim() }}"

requests:
  create_fish:
    $ref: "#/.base_request"
    method: GET
    url: "{{ host }}/fishes/{{ fish_id }}"
    headers:
      $ref: "#/.base_request/headers"
      Host: myfishes.fish
    body:
      type: json
      data: { "kind": "barracuda", "name": "Barry" }

Cross-File Composition

Reusing components within a single file is great and all, but $ref also supports importing components from other files:

base.yml

requests:
  login:
    method: POST
    url: "{{ host }}/login"
    body:
      type: json
      data:
        {
          "username": "{{ prompt(message='Username') }}",
          "password": "{{ prompt(message='Password', sensitive=true) }}",
        }

slumber.yml

requests:
  login:
    $ref: "./base.yml#/requests/login"

Referenced files do not need to be valid Slumber collections; any valid YAML file can be referenced

Replacement vs Extension

Depending on how $ref is used, the referenced value will either replace or extend the reference.

  • If $ref is the only field in its mapping, the entire mapping will be replaced
  • If there are other fields in $ref, just the $ref field will be replaced by the fields in the referenced mapping
    • In this case, the referenced value must be a mapping; any other value type will trigger an error
refs:
  string: "hello!"
  mapping:
    a: 1
    b: 2

# `string`'s mapping value is replaced by the referenced string
# string: "hello!"
string:
  $ref: "#/refs/string"

# `mapping`'s mapping value is replaced by another mapping. This is functionally
# equivalent to extending `mapping` with `refs/mapping`.
#
# mapping:
#   a: 1
#   b: 2
mapping:
  $ref: "#/refs/mapping"

# The values of `refs/mapping` are replaced exactly where $ref is. `mapping/a`
# is overridden by `refs/mapping/a`, but `mapping/b` overrides `refs/mapping/b`
#
# mapping_extend:
#   a: 1
#   b: 3
mapping_extend:
  a: 0
  $ref: "#/refs/mapping"
  b: 3

# Error! Can't extend a mapping with a string
mapping_error:
  $ref: "#/refs/string"
  b: 3