Hypermedia walkthrough

This page goes through possible navigation steps from the point of view of an hypermedia-aware client talking to a cubicweb-jsonschema server.

Site root

We start our navigation from the root URL of the application with a GET request:

>>> r = client.get('/', headers={'Accept': 'application/json'})
>>> print(r)
Response: 204 No Content
Allow: GET
Link: </schema>; rel="describedby"; type="application/schema+json"

The application root does not contain any data, hence the 204 No Content response status. Notice the Link with a rel="describedby" which is the canonical way of indicating the JSON Schema location of a resource (here the root resource). So let’s fetch it at /schema:

>>> r = client.get('/schema',
...                headers={'Accept': 'application/schema+json'})
>>> print(r)
Response: 200 OK
Content-Type: application/json
Link: </>; rel="describes"; type="application/json"
{
   "$schema": "http://json-schema.org/draft-06/schema#",
   "title": "test app",
   "type": "null",
   "links" : [
      {
         "rel" : "collection",
         "href" : "/author/",
         "targetSchema" : {
            "$ref" : "/author/schema"
         },
         "submissionSchema" : {
            "$ref" : "/author/schema?role=creation"
         },
         "title" : "Author_plural"
      },
      {
         "rel" : "collection",
         "href" : "/book/",
         "targetSchema" : {
            "$ref" : "/book/schema"
         },
         "submissionSchema" : {
            "$ref" : "/book/schema?role=creation"
         },
         "title" : "Book_plural"
      }
   ]
}
>>> application_schema = r.json

From collection to items

The schema above has a rel=”item” link nested into the items property. This can be used to manipulate an item of the collection (notice the "auchor": "#" property of the link, indicating that the subject of the link is actually the collection # and not the item). Each item can be fetched by expanding the templated href of the link with an item of the collection as context (here it’s id property). For that we use the uritemplate Python package.

>>> from uritemplate import URITemplate
>>> item_link = books_schema['items']['links'][0]
>>> item_uritemplate = URITemplate(item_link['href'])
>>> item_uri = item_uritemplate.expand(books[0])
>>> item_response = client.get(item_uri,
...                            headers={'accept': 'application/json'})
>>> print(item_response)  
Response: 200 OK
Allow: GET, PUT, DELETE
Link: </book/>; rel="up"; title="Book_plural", </book/.../schema>; rel="describedby"; type="application/schema+json"
Content-Type: application/json
{
    "title": "The Old Man and the Sea",
    "author": [{
        "id": "..."
    }]
}

Typically the client would also retrieve the JSON Schema of this resource advertized by the rel="describedby" Link header. cubicweb-jsonschema provides a parse_links function that helps handling such headers on client side; for instance, considering the previous response:

>>> from cubicweb_jsonschema.links import parse_links
>>> item_schema_link = parse_links(item_response.headers['Link'])['describedby']
>>> sorted(item_schema_link.items())  
[('href', '/book/.../schema'), ('type', 'application/schema+json')]

Entity resource

Now if we stay on this resource and retrieve its complete hyper schema which is targetted by the rel="describedby" Link header in the resource response.

>>> r = client.get(item_schema_link['href'],
...                headers={'accept': item_schema_link['type']})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
Link: </book/.../>; rel="describes"; type="application/json"
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "Book",
  "type": "object",
  "properties": {
    "publication_date": {
      "format": "date",
      "type": "string",
      "title": "publication_date"
    },
    "title": {
      "type": "string",
      "title": "title"
    },
    "author": {
      "title": "author",
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "..."
                ],
                "title": "Ernest Hemingway"
              }
            ]
          }
        }
      }
    }
  },
  "additionalProperties": false,
  "links": [
    {
      "targetSchema": {
        "$ref": "/book/schema"
      },
      "href": "/book/",
      "rel": "collection",
      "title": "Book_plural"
    },
    {
      "title": "Book #...",
      "targetSchema": {
        "$ref": "/book/.../schema?role=view"
      },
      "href": "/book/.../",
      "rel": "self",
      "submissionSchema": {
        "$ref": "/book/.../schema?role=edition"
      }
    },
    {
      "href": "/book/.../in_library/",
      "rel": "related",
      "title": "in_library"
    },
    {
      "href": "/book/.../topics/",
      "rel": "related",
      "title": "topics"
    }
  ]
}
>>> book_schema = r.json

We get a new rel="self" link which can be used to manipulate the resource. For instance, as we have seen that we are allowed to perform a PUT request on the resource, we can update it by following the submissionSchema property of the link. So let’s fetch the schema first:

>>> r = client.get(book_schema['links'][1]['submissionSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "Book",
  "type": "object",
  "properties": {
    "publication_date": {
      "format": "date",
      "type": "string",
      "title": "publication_date"
    },
    "title": {
      "type": "string",
      "title": "title"
    },
    "author": {
      "title": "author",
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["id"],
        "properties": {
          "id": {
            "oneOf": [
              {
                "type": "string",
                "enum": [
                  "..."
                ],
                "title": "Ernest Hemingway"
              }
            ]
          }
        }
      },
      "minItems": 1,
      "maxItems": 1
    }
  },
  "required": [
    "title",
    "author"
  ],
  "additionalProperties": false
}

then retrieve the resource data:

>>> r = client.get(book_schema['links'][1]['href'],
...                headers={'Accept': 'application/json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
    "title": "The Old Man and the Sea",
    "author": [{
        "id": "..."
    }]
}
>>> book = r.json

and then we perform the PUT:

>>> book['publication_date'] = '1952-08-25'
>>> r = client.put_json(book_schema['links'][1]['href'],
...                     book,
...                     headers={'Accept': 'application/json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
Location: https://localhost:80/book/.../
{
    "title": "The Old Man and the Sea",
    "publication_date": "1952-08-25",
    "author": [{
        "id": "..."
    }]
}

Entity relationships

Another kind of interesting links are rel="related" links which advertized relationships between the current resource and related ones.

>>> topics_link = book_schema['links'][-1]
>>> r = client.get(topics_link['href'],
...                headers={'Accept': 'application/json'})
>>> print(r)  
Response: 200 OK
Allow: GET, POST
Content-Type: application/json
Link: </book/.../topics/schema>; rel="describedby"; type="application/schema+json"
[]

No data yet here, let’s follow the rel="describedby" Link to see what can be done there.

>>> topics_schema_link = parse_links(r.headers['Link'])['describedby']
>>> sorted(topics_schema_link.items())  
[('href', '/book/.../topics/schema'), ('type', 'application/schema+json')]
>>> r = client.get(topics_schema_link['href'],
...                headers={'Accept': topics_schema_link['type']})
>>> print(r) 
Response: 200 OK
Content-Type: application/json
Link: </book/.../topics/>; rel="describes"; type="application/json"
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "topics" ,
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "id": {
        "oneOf": [
          {
            "enum": [
              "..."
            ],
            "title": "sword fish",
            "type": "string"
          },
          {
            "enum": [
              "..."
            ],
            "title": "gardening",
            "type": "string"
          },
          {
            "enum": [
              "..."
            ],
            "title": "fishing",
            "type": "string"
          }
        ]
      }
    },
    "additionalProperties": false,
    "links": [
      {
        "href": "/book/.../topics/{id}",
        "anchor": "#",
        "rel": "item"
      }
    ]
  },
  "links": [
    {
      "title": "topics",
      "rel": "self",
      "href": "/book/.../topics/",
      "targetSchema": {
        "$ref": "/book/.../topics/schema?role=view"
      },
      "submissionSchema": {
        "$ref": "/book/.../topics/schema?role=creation"
      }
    }
  ]
}
>>> topics_schema = r.json

So in order to add a topic relation, we need to POST at URL specified in rel="self" link of this schema. Payload should also conform to the submissionSchema of the link, let’s retrieve it first:

>>> r = client.get(topics_schema['links'][0]['submissionSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "topics",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "oneOf": [
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "sword fish"
        },
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "gardening"
        },
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "fishing"
        }
      ]
    }
  }
}
>>> possible_topics = r.json['properties']['id']['oneOf']

We can create relationships with the Book entity by POST-ing to the relationship route:

>>> fishing_topic = [{'id': t['enum'][0]} for t in possible_topics
...                  if t['title'] == 'fishing'][0]
>>> r = client.post_json(topics_link['href'], fishing_topic,
...                      headers={'Accept': 'application/json'})
>>> print(r)  
Response: 201 Created
Content-Type: application/json
Location: https://localhost:80/book/.../topics/.../
{
  "name": "fishing"
}
>>> swordfish_topic = [{'id': t['enum'][0]} for t in possible_topics
...                    if t['title'] == 'sword fish'][0]
>>> r = client.post_json(topics_link['href'], swordfish_topic,
...                      headers={'Accept': 'application/json'})
>>> print(r)  
Response: 201 Created
Content-Type: application/json
Location: https://localhost:80/book/.../topics/.../
{
  "name": "sword fish"
}

Now if we retrieve back the relation URL:

>>> r = client.get(topics_link['href'],
...                headers={'Accept': 'application/json'})
>>> print(r)  
Response: 200 OK
Allow: GET, POST
Content-Type: application/json
Link: </book/.../topics/schema>; rel="describedby"; type="application/schema+json"
[
  {
    "id": "..."
  },
  {
    "id": "..."
  }
]
>>> topics = r.json

we have items in the topics collection.

If we now come back to the /book/…/topics/schema response we got earlier, we can now use the rel="item" link to fetch an item of the collection given the URI template /book/.../topics/{id} and the above response.

>>> from uritemplate import URITemplate
>>> item_link = topics_schema['items']['links'][0]
>>> item_uritemplate = URITemplate(item_link['href'])
>>> item_uri = item_uritemplate.expand(topics[1])
>>> item_response = client.get(item_uri,
...                            headers={'accept': 'application/json'})
>>> print(item_response)  
Response: 200 OK
Allow: GET, PUT, DELETE
Link: </book/.../topics/>; rel="up"; title="topics", </book/.../topics/.../schema>; rel="describedby"; type="application/schema+json"
Content-Type: application/json
{
  "name": "fishing"
}

along with its JSON Schema as advertized by the rel="describedby" Link header:

>>> related_topic_schema_link = parse_links(item_response.headers['Link'])['describedby']
>>> sorted(related_topic_schema_link.items())  
[('href', '/book/.../topics/.../schema'), ('type', 'application/schema+json')]
>>> r = client.get(related_topic_schema_link['href'],
...                headers={'Accept': related_topic_schema_link['type']})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
Link: </book/.../topics/.../>; rel="describes"; type="application/json"
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "Topic",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "title": "name"
    }
  },
  "additionalProperties": false,
  "links": [
    {
      "href": "/book/.../topics/",
      "rel": "collection",
      "title": "Topic_plural",
      "targetSchema": {
        "$ref": "/book/.../topics/schema"
      }
    },
    {
      "href": "/book/.../topics/.../",
      "rel": "self",
      "title": "Topic #...",
      "targetSchema": {
        "$ref": "/book/.../topics/.../schema?role=view"
      },
      "submissionSchema": {
        "$ref": "/book/.../topics/.../schema?role=edition"
      }
    }
  ]
}
>>> fishing_topic_schema = r.json

Notice the rel="self" link which can (as for any resource) be used to manipulate the related entity. In particular, should we want to update the related topic, we’d need to conform the the submissionSchema:

>>> r = client.get(fishing_topic_schema['links'][-1]['submissionSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "Topic",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "title": "name"
    }
  },
  "required": [
    "name"
  ],
  "additionalProperties": false
}

So let’s update the “fishing” topic and change it’s name:

>>> r = client.put_json(item_uri, {'name': 'fish hunting'},
...                     headers={'Accept': 'application/json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
Location: https://localhost:80/book/.../topics/.../
{
  "name": "fish hunting"
}

Let’s now fetch back the relation schema:

>>> r = client.get(topics_schema['links'][0]['targetSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "topics",
  "type": "array",
  "items": {
    "additionalProperties": false,
    "type": "object",
    "properties": {
      "id": {
        "oneOf": [
          {
            "enum": [
              "..."
            ],
            "type": "string",
            "title": "fish hunting"
          },
          {
            "enum": [
              "..."
            ],
            "type": "string",
            "title": "sword fish"
          }
        ]
      }
    }
  }
}

we notice that the items of the array contains a oneOf constraint which lists schemas for existing relations.

Another request on topics link’s submissionSchema:

>>> r = client.get(topics_schema['links'][0]['submissionSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "topics",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "oneOf": [
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "gardening"
        }
      ]
    }
  }
}

We can see that only unrelated targets are listed in the oneOf property of submissionSchema.

Finally, if we issue a DELETE on a “topics” relation URI we delete the relation (not necessarily the target entity):

>>> r = client.delete(item_uri)
>>> print(r)
Response: 204 No Content

and then fetch back the submissionSchema of topics link:

>>> r = client.get(topics_schema['links'][0]['submissionSchema']['$ref'],
...                headers={'Accept': 'application/schema+json'})
>>> print(r)  
Response: 200 OK
Content-Type: application/json
{
  "$schema": "http://json-schema.org/draft-06/schema#",
  "title": "topics",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "id"
  ],
  "properties": {
    "id": {
      "oneOf": [
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "fish hunting"
        },
        {
          "enum": [
            "..."
          ],
          "type": "string",
          "title": "gardening"
        }
      ]
    }
  }
}

we notice that “fish hunting” topic appears back as a possible target of topics relation for our book.