A RESTful Adventure

rack.to/rest

Prerequisites

Kurt Griffiths @kgriffs
Everett Toews @everett_toews

Goal

Design and Implement

a RESTful API

+𝓡 =☁

Agenda

  1. REST Fundamentals
  2. Design the API
  3. Break
  4. Implement the Design
  5. Conclusion
  6. Q & A

REST Fundamentals

REST is not

[URL|RPC|CRUD] over HTTP

REST is

An architectural style for network-based applications

A set of constraints formalized by Roy Fielding

A counterpoint to RPC

Six Constraints

“The shallow consider liberty a release from all law, from every constraint. The wise man sees in it, on the contrary, the potent Law of Laws.”

- Walt Whitman

1. Client-Server

2. Stateless

3. Cache

4. Uniform Interface

Image credit: Nayu Kim

Image credit: dankreider

Image credit: Ωméga

5. Layered System

6. Code-on-Demand

Elements

of the

Uniform Interface

Components

Connectors

Resources & Representations

Control Data

Metadata about each message

Purpose and meaning (HTTP's GET/POST, Content-Type)

Override default behavior (HTTP's If-Modified-Since)

Hypermedia

Image credit: Jan Kronquist

Design the API

Before We Proceed

Your REST != My REST

Brainstorming

Lab

Whiteboarding

Use Cases

Example

Safely navigate a dungeon from start to finish

Early Technical Decisions

  • Media Types
  • IDs vs hypermedia
  • Pretty URLs
  • Versioning
  • Authentication
  • Resource Updates (State Transitions)

API Definition

Formats

  • API Blueprint
  • RAML
  • Swagger

Image Credit: Kin Lane

Swagger

Spec

Lab: Paths

Hint: Path Templating


swagger: "2.0"
info:
  version: 1.0.0
  title: A RESTful Adventure
host: localhost
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /characters:
  ???
          

Lab: Examples

Hint: Example Object and Resource Data Types


swagger: "2.0"
info:
  version: 1.0.0
  title: A RESTful Adventure
host: localhost
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /characters:
    get:
      responses:
        200:
          examples:
            replace-this-with-a-mime-type: |-
              {
                "?": ?
                ...
              }
    post:
      x-examples:
        replace-this-with-a-mime-type: { "?": "?" }
      responses:
        201:
          headers:
            Location:
              type: ?
              format: ?
              description: ?
          examples:
            replace-this-with-a-mime-type: |-
              {
                "?": "?"
                ...
              }
          

Lab: Models

Hint: Resource Data Types


swagger: "2.0"
info:
  version: 1.0.0
  title: A RESTful Adventure
host: localhost
schemes:
  - http
consumes:
  - application/json
produces:
  - application/json
paths:
  /characters:
  ...
definitions:
  Character:
    type: object
    properties:
      replace-this-with-name-of-property-1:
        type: ?
        format: ?
      replace-this-with-name-of-property-2:
        type: ?
        format: ?
  Characters:
    type: array
    items:
      $ref: "#/definitions/Character"
          

Mock Server/Client

Lab: curl and python ‑m json.tool

Bonus Points: Use httpie and jq instead


HOST=http://104.130.13.137

echo "Get characters"
curl ? | python -m json.tool

echo "Create a character"
curl ?
          

Example

Errors

Example


{
  "transaction_id": "71607e7c-df7c-45f3-b571-d1829de4ad9a",
  "code": "736.9",
  "title": "Teleport Denied",
  "description": "The room you tried to visit does not exist or is not accessible from your current room. Thought you could get away with it didn't you.",
  "link": {
      "rel": "help",
      "href": "http://en.wikipedia.org/wiki/No-teleportation_theorem"
    }
}
          

Documentation

Lab: API Doc

Copy: Swagger YAML

Paste: Swagger Editor

User-Centric Design

Break

Implement the Design

rack.to/rest#impl

The API Spec

/design/hypermedia-based/swagger.yaml

Lab: Hello world!

WSGI ("whiskey")

Python Web Server Gateway Interface


def application(env, start_response):
    body = 'Hello ' + env['HTTP_X_NAME'] + '!\n'

    start_response("200 OK", [('Content-Type', 'text/plain')])
    return [body.encode('utf-8')]

          

Self-hosting a WSGI App


if __name__ == '__main__':
    from wsgiref.simple_server import make_server

    host = '127.0.0.1'
    port = 8000
    server = make_server(host, port, application)

    print('Listening on {0}:{1}'.format(host, port))
    server.serve_forever()
          

$ python myapp.py

Listening on 127.0.0.1:8000
          

Falcon

falconframework.org


$ pip install falcon
          

httpie.org


$ pip install httpie
          

Ready?


$ mkdir restful-adventure && cd restful-adventure
          

$ wget http://rack.to/rest-api -O api.py
$ wget http://rack.to/rest-dal -O dal.py
          

w3


class HelloResource(object):
    def on_get(self, req, resp):
        resp.body = 'Hello ' + req.get_header('x-name') + '!\n'

        # Falcon defaults to 'application/json'
        resp.content_type = 'text/plain'

        # Falcon defaults to 200 OK
        # resp.status = falcon.HTTP_200
          

w4


# An instance of falcon.API is a WSGI application
api = falcon.API()
api.add_route('/', HelloResource())
          

Run it!


$ python api.py

Listening on 127.0.0.1:8000
          

Test it!


$ http 127.0.0.1:8000 x-name:Adventurer

          

HTTP/1.0 200 OK
Content-Length: 11
Content-Type: text/plain
Date: Wed, 18 Feb 2015 19:29:29 GMT
Server: WSGIServer/0.1 Python/2.7.9

Hello Adventurer!

          

Lab: Choose a character

Image Credit: PixelBlock


/characters:
  get:
    summary: List all Characters
    operationId: list_characters
    responses:
      200:
        description: An array of Characters
        schema:
          $ref: "#/definitions/Characters"
        examples:
          application/json: |-
            {
              "characters": [
                {
                  "name": "Knox Thunderbane",
                  "links": [
                    {
                      "rel": "self",
                      "allow": [
                        "GET", "PUT"
                      ],
                      "href": "/characters/1234"
                    },
                    {
                      "rel": "location",
                      "allow": [
                        "GET", "PUT"
                      ],
                      "href": "/characters/1234/location"
                    }
                  ]
                }
              ],
              "links": [
                {
                  "rel": "self",
                  "allow": [
                    "GET", "POST"
                  ],
                  "href": "/characters"
                }
              ]
            }
          

w4


# api.add_route('/', HelloResource())

          

a14


api.add_route('/characters', CharacterList(controller))

          

a7


def on_get(self, req, resp):
    # Ask the DAL for a list of entities
    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    characters = self._controller.list_characters()

    # Map the entities to the resource
    resource = {
        'characters': [self._entity_to_resource(c) for c in characters],
        'links': [
            {
                'rel': 'self',
                'allow': ['GET', 'POST'],
                'href': '/characters'
            }
        ]
    }

    # Create a JSON representation of the resource
    resp.body = json.dumps(resource, ensure_ascii=False)

    # Falcon defaults to the JSON media type for the content
    # resp.content_type = 'application/json'

    # Falcon defaults to 200 OK
    # resp.status = falcon.HTTP_200

          

a1


def _entity_to_resource(self, character):
    base_href = self._id_to_href(character['id'])
    links = [
        {
            'rel': 'self',
            'allow': [
                'GET', 'PUT'
            ],
            'href': base_href
        },
        {
            'rel': 'location',
            'allow': [
                'GET', 'PUT'
            ],
            'href': base_href + '/location'
        }
    ]

    return {
        'name': character['name'],
        'links': links
    }
          

a2


def _id_to_href(self, character_id):
    return '/characters/{0}'.format(character_id)

          

Checkpoint

16-1.py


$ http 127.0.0.1:8000/characters

          

HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 22:52:54 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8

{
    "characters": [
        {
            "links": [
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
                    "rel": "location"
                }
            ],
            "name": "Knox Thunderbane"
        }
    ],
    "links": [
        {
            "allow": [
                "GET",
                "POST"
            ],
            "href": "/characters",
            "rel": "self"
        }
    ]
}
          

Lab: Add a character

Image Credit: Antirest



post:
  summary: Create a Character
  operationId: create_character
  parameters:
    - name: body
      in: body
      required: true
      schema:
        properties:
          name:
            type: string
            minLength: 1
            maxLength: 256
  x-examples:
    application/json: { "name": "Knox Thunderbane" }
  responses:
    201:
      description: Character created
      headers:
        Location:
          type: string
          format: url
          description: A link to the Character
      schema:
        $ref: "#/definitions/Character"
      examples:
        application/json: |-
          {
            "name": "Knox Thunderbane",
            "links": [
              {
                "rel": "self",
                "allow": [
                  "GET", "PUT"
                ],
                "href": "/characters/1234"
              },
              {
                "rel": "location",
                "allow": [
                  "GET", "PUT"
                ],
                "href": "/characters/1234/location"
              }
            ]
          }
    default:
      description: Unexpected errors
      schema:
        $ref: "#/definitions/Error"

          

a8


def on_post(self, req, resp):
    # Parse the incoming representation. This can be factored out into
    # Falcon hooks or middleware, but we'll keep it inline for now.
    # TODO: Validate against a schema
    representation = req.stream.read().decode('utf-8')
    representation = json.loads(representation)

    # Create a new entity from the representation
    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    character = self._controller.add_character(representation['name'])

    # Map the entity to the resource. Again, this sort of thing
    # could be factored out into a Falcon hook (DRY).
    resource = self._entity_to_resource(character)

    resp.location = self._id_to_href(character['id'])
    resp.body = json.dumps(resource, ensure_ascii=False)

          

Checkpoint

api-checkpoint-2.py


$ http OPTIONS 127.0.0.1:8000/characters

          

HTTP/1.0 204 No Content
Content-Length: 0
Date: Sat, 21 Feb 2015 22:59:18 GMT
Server: WSGIServer/0.1 Python/2.7.9
allow: GET, POST
          

$ http POST 127.0.0.1:8000/characters name="Commander Keen"

          

HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 23:00:02 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 254
content-type: application/json; charset=utf-8
location: /characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7

{
    "links": [
        {
            "allow": [
                "GET",
                "PUT"
            ],
            "href": "/characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7",
            "rel": "self"
        },
        {
            "allow": [
                "GET",
                "PUT"
            ],
            "href": "/characters/8713e99b-d4d2-4855-85a2-9d0a32fec0b7/location",
            "rel": "location"
        }
    ],
    "name": "Commander Keen"
}
          

$ http 127.0.0.1:8000/characters

          

HTTP/1.0 200 OK
Date: Sat, 21 Feb 2015 23:00:58 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 514
content-type: application/json; charset=utf-8

{
    "characters": [
        {
            "links": [
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
                    "rel": "location"
                }
            ],
            "name": "Knox Thunderbane"
        },
        {
            "links": [
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/13742b78-be28-4662-b379-c9bba55b476c",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/13742b78-be28-4662-b379-c9bba55b476c/location",
                    "rel": "location"
                }
            ],
            "name": "Commander Keen"
        }
    ],
    "links": [
        {
            "allow": [
                "GET",
                "POST"
            ],
            "href": "/characters",
            "rel": "self"
        }
    ]
}
          

Lab: Choose a dungeon

Image Credit: Chester Bolingbroke


/dungeons:
  get:
    summary: List all Dungeons
    operationId: list_dungeons
    responses:
      200:
        description: An array of Dungeons
        schema:
          $ref: "#/definitions/Dungeons"
        examples:
          application/json: |-
            {
              "dungeons": [
                {
                  "name": "Dungeon of Doom",
                  "links": [
                    {
                      "rel": "self",
                      "allow": [
                          "GET"
                      ],
                      "href": "/dungeons/1234"
                    },
                    {
                      "rel": "room first",
                      "allow": [
                          "GET"
                      ],
                      "href": "/dungeons/1234/rooms/1002",
                      "description": "entrance"
                    }
                  ]
                }
              ],
              "links": [
                {
                  "rel": "self",
                  "allow": [
                      "GET"
                  ],
                  "href": "/dungeons"
                }
              ]
            }
          

a13


def on_get(self, req, resp):
    # Ask the DAL for a list of entities
    dungeons = self._controller.list_dungeons()

    # Map the entities to the resource
    resource = {
        'dungeons': [self._entity_to_resource(d) for d in dungeons],
        'links': [
            {
                'rel': 'self',
                'allow': ['GET'],
                'href': '/dungeons'
            }
        ]
    }

    # Create a JSON representation of the resource
    resp.body = json.dumps(resource, ensure_ascii=False)

          

a12


def _entity_to_resource(self, dungeon):
    base_href = '/dungeons/{0}'.format(dungeon['id'])
    links = [
        {
            'rel': 'self',
            'allow': ['GET'],
            'href': base_href
        },
        {
            'rel': 'room first',
            'allow': ['GET'],
            'href': '{0}/rooms/{1}'.format(base_href, dungeon['entry_id'])
        }
    ]

    return {
        'name': dungeon['name'],
        'links': links
    }
          

a17


api.add_route('/dungeons', DungeonList(controller))

          

Checkpoint

api-checkpoint-3.py


$ http 127.0.0.1:8000/dungeons

          

HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 02:13:32 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 564
content-type: application/json; charset=utf-8

{
    "dungeons": [
        {
            "links": [
                {
                    "allow": [
                        "GET"
                    ],
                    "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET"
                    ],
                    "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
                    "rel": "room first"
                }
            ],
            "name": "Dungeon of Doom"
        }
    ],
    "links": [
        {
            "allow": [
                "GET"
            ],
            "href": "/dungeons",
            "rel": "self"
        }
    ]
}
          

Lab: Enter the dungeon


$ http 127.0.0.1:8000/characters

          

HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 02:32:23 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8

{
    "characters": [
        {
            "links": [
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location",
                    "rel": "location"
                }
            ],
            "name": "Knox Thunderbane"
        }
    ],
    "links": [
        {
            "allow": [
                "GET",
                "POST"
            ],
            "href": "/characters",
            "rel": "self"
        }
    ]
}
          

$ CHAR_LOCATION_URL=/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/location

          

$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27

          

$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL

          

HTTP/1.0 404 Not Found
Date: Fri, 20 Feb 2015 18:45:24 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 0

          

a6


def on_put(self, req, resp, character_id):
    # TODO: Validate against a schema
    representation = req.stream.read().decode('utf-8')
    representation = json.loads(representation)

    # TODO: Raise falcon.HTTPError if ID is not a UUID
    character_id = uuid.UUID(character_id)

    # TODO: Raise falcon.HTTPError if ID is not a UUID
    room_href = representation['href']
    room_id = self._room_href_to_id(room_href)

    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    self._controller.move_character(character_id, room_id)

    # Success!
    resp.status = falcon.HTTP_204
          

a5


def on_get(self, req, resp, character_id):
    # TODO: Handle the case that character_id is not a valid UUID
    character_id = uuid.UUID(character_id)

    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    room_id, dungeon_id = self._controller.get_location(character_id)

    # Define the resource. We have to translate the DAL's notion
    # of a "location" to the API's concept of a "location".
    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    resource = self._room_id_to_location(room_id, dungeon_id)

    # Create a JSON representation of the resource
    # TODO: Use functools.partial to create a version of json.dumps that
    #       defaults to ensure_ascii=False
    resp.body = json.dumps(resource, ensure_ascii=False)

          

a3


def _room_href_to_id(self, href):
    # ID will be the last part of the URL.
    return uuid.UUID(href.split('/')[-1])

          

a4


def _room_id_to_location(self, room_id, dungeon_id):
    return {
        'rel': 'room',
        'allow': ['GET'],
        'href': 'dungeons/{0}/rooms/{1}'.format(dungeon_id, room_id)
    }

          

a15


api.add_route('/characters/{character_id}/location', CharacterLocation(controller))
          

Checkpoint

api-checkpoint-4.py


$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL

          

HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:35:06 GMT
Server: WSGIServer/0.1 Python/2.7.9
          

$ http 127.0.0.1:8000$CHAR_LOCATION_URL

          

HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 03:36:20 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 133
content-type: application/json; charset=utf-8

{
    "allow": [
        "GET"
    ],
    "href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
    "rel": "room"
}
          

Lab: Find a way out!

(Preferably without dying...)

Use Case

  1. UA gets a list of doorways leading out from the player's location.
  2. UA presents the list of doorways to Player.
  3. Player chooses a doorway from the list.
  4. UA moves Player's character to the chosen room.

Step 1: UA gets a list of doorways


/dungeons/{dungeon_id}/rooms/{room_id}:
  get:
    summary: Get a specific Room in a specific Dungeon
    operationId: get_room
    parameters:
      - name: dungeon_id
        in: path
        description: The id of the Dungeon
        required: true
        type: string
      - name: room_id
        in: path
        description: The id of the Room
        required: true
        type: string
    responses:
      200:
        description: Expected response to a valid request
        schema:
          $ref: "#/definitions/Room"
        examples:
          application/json: |-
            {
              "name": "Entrance",
              "is_exit": false,
              "links": [
                {
                  "rel": "self",
                  "href": "/dungeons/1234/rooms/1000"
                },
                {
                  "rel": "room east",
                  "allow": [
                      "GET"
                  ],
                  "href": "/dungeons/1234/rooms/1001"
                }
              ]
            }
      400:
        description: You tried to teleport. That's just not allowed.
        schema:
          $ref: "#/definitions/Error"
        examples:
          application/json: |-
            {
              "transaction_id": "71607e7c-df7c-45f3-b571-d1829de4ad9a",
              "code": "736.9",
              "title": "Teleport Denied",
              "description": "The room you tried to visit does not exist or is not accessible from your current room. Thought you could get away with it didn't you.",
              "link": {
                  "rel": "help",
                  "href": "http://en.wikipedia.org/wiki/No-teleportation_theorem"
                }
            }
      default:
        description: Unexpected errors
        schema:
          $ref: "#/definitions/Error"

          

a11


def on_get(self, req, resp, dungeon_id, room_id):
    # TODO: Handle the case that these are not valid UUIDs
    dungeon_id = uuid.UUID(dungeon_id)
    room_id = uuid.UUID(room_id)

    # Note that we don't actually need the dungeon_id, since
    # the DAL just wants the room_id. We'll just ignore it
    # for now.

    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    room = self._controller.get_room(room_id)

    # Create a resource based on the room entity
    # TODO: If an error is raised, convert it to an instance
    #       of falcon.HTTPError
    resource = self._entity_to_resource(room)

    # Create a JSON representation of the resource
    resp.body = json.dumps(resource, ensure_ascii=False)

          

a9


def _entity_to_resource(self, room):
    dungeon_id = room['dungeon_id']
    room_id = room['id']

    base_href = 'dungeons/{0}'.format(dungeon_id)

    links = [
        {
            'rel': 'self',
            'allow': ['GET'],
            'href': '{0}/rooms/{1}'.format(base_href, room_id)
        },
        {
            'rel': 'dungeon',
            'allow': ['GET'],
            'href': base_href
        }
    ]

    # Add additional links, one per doorway to another room
    links.extend([
        {
            'rel': 'room ' + doorway['direction'],
            'allow': ['GET'],
            'href': self._id_to_href(doorway['room_id'], dungeon_id)
        }

        for doorway in room['doorways']
    ])

    return {
        'name': room['name'],
        'is_exit': room['is_exit'],
        'links': links
    }

          

a10


def _id_to_href(self, room_id, dungeon_id):
    return '/dungeons/{0}/rooms/{1}'.format(dungeon_id, room_id)

          

a16


api.add_route('/dungeons/{dungeon_id}/rooms/{room_id}', Room(controller))

          

Checkpoint

api-checkpoint-5.py


$ http 127.0.0.1:8000/dungeons

          

HTTP/1.0 200 OK
Date: Tue, 24 Feb 2015 00:44:24 GMT
Server: WSGIServer/0.2 CPython/3.3.6
content-length: 356
content-type: application/json; charset=utf-8

{
    "dungeons": [
        {
            "links": [
                {
                    "allow": [
                        "GET"
                    ],
                    "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET"
                    ],
                    "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
                    "rel": "room first"
                }
            ],
            "name": "Dungeon of Doom"
        }
    ],
    "links": [
        {
            "allow": [
                "GET"
            ],
            "href": "/dungeons",
            "rel": "self"
        }
    ]
}
          

$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27

          

$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL

          

HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:56:54 GMT
Server: WSGIServer/0.1 Python/2.7.9

          

$ http 127.0.0.1:8000$ROOM_URL

          

HTTP/1.0 200 OK
Date: Tue, 24 Feb 2015 01:03:13 GMT
Server: WSGIServer/0.2 CPython/3.3.6
content-length: 575
content-type: application/json; charset=utf-8

{
    "is_exit": false,
    "links": [
        {
            "allow": [
                "GET"
            ],
            "href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
            "rel": "self"
        },
        {
            "allow": [
                "GET"
            ],
            "href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
            "rel": "dungeon"
        },
        {
            "allow": [
                "GET"
            ],
            "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2",
            "rel": "room north"
        },
        {
            "allow": [
                "GET"
            ],
            "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/65840050-12d3-4e29-9412-7c3b22fdd52e",
            "rel": "room east"
        }
    ],
    "name": "Super Creepy Entrance"
}

          

Step 2: Present the list of doorways

 
Thou findest thyself in yonder Super Creepy Entrance...

Looking about, thou doth see doorways leading North and East. What
is thy command?

> _

          

Step 3: Player chooses a doorway

 
Thou findest thyself in yonder Super Creepy Entrance...

Looking about, thou doth see doorways leading North and East. What
is thy command?

> go north_

          

$ ROOM_URL=/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2

          

Step 4: Move the character


$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL

          

HTTP/1.0 204 No Content
Content-Length: 0
Date: Sun, 22 Feb 2015 03:56:54 GMT
Server: WSGIServer/0.1 Python/2.7.9

          

Step 5: Did we make it out alive?


$ http 127.0.0.1:8000$ROOM_URL

          

HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 03:58:01 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 417
content-type: application/json; charset=utf-8

{
    "is_exit": false,
    "links": [
        {
            "allow": [
                "GET"
            ],
            "href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/751d5812-144d-40f8-a82d-221dbb3075e2",
            "rel": "self"
        },
        {
            "allow": [
                "GET"
            ],
            "href": "dungeons/5a024cd8-2db3-446e-b777-bdc60185a117",
            "rel": "dungeon"
        },
        {
            "allow": [
                "GET"
            ],
            "href": "/dungeons/5a024cd8-2db3-446e-b777-bdc60185a117/rooms/8f726efc-5e3e-4332-ab24-243a1d3e0b27",
            "rel": "room south"
        }
    ],
    "name": "Armory"
}
          
 
Thou findest thyself in yonder Armory...

Fortunately, thou art not dead yet. Be that at is may, thou hast
found a dead-end. Thou canst only retreat back through yonder
Southward door. What is thy command?

> _

          

Lab: Independent evolution

/location   ➟   /foo


$ http 127.0.0.1:8000/characters

          

HTTP/1.0 200 OK
Date: Sun, 22 Feb 2015 04:21:58 GMT
Server: WSGIServer/0.1 Python/2.7.9
content-length: 258
content-type: application/json; charset=utf-8

{
    "characters": [
        {
            "links": [
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1",
                    "rel": "self"
                },
                {
                    "allow": [
                        "GET",
                        "PUT"
                    ],
                    "href": "/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/foo",
                    "rel": "location"
                }
            ],
            "name": "Knox Thunderbane"
        }
    ],
    "links": [
        {
            "allow": [
                "GET",
                "POST"
            ],
            "href": "/characters",
            "rel": "self"
        }
    ]
}
          

$ CHAR_LOCATION_URL=/characters/c1a008bc-105f-4793-bfa6-a54fbc9ce6b1/foo

          

$ http PUT 127.0.0.1:8000$CHAR_LOCATION_URL rel=room href=$ROOM_URL

          

Conclusion

The Entire Repo

Further Reading

Rackspace API Working Group

Other Examples:

Thanks!

Q & A

rack.to/2015-workshops-feedback