REST Scope Tutorial

Note

Do not edit this document, the source is reStructured Text elsewhere.

Contents

Introduction

This tutorial is a companion piece to the client side scope tutorial describing how to implement the same scope but to run on a server instead of on the device.

We'll implement it using Python to make it easy to read, but there's nothing language-specific about this code, you could implement it just as easily using Go, PHP, or Java.

All you need to implement are two methods, search and preview and expose them. To make them available via HTTP I'll use bottle which is a very small web framework, but you can use the one you prefer with minimal changes.

The example is a server that searches the Open Clipart Library to find graphics matching the query terms. The API is described at https://openclipart.org/developers

The reference documentation for REST Scopes, which describes the required API is here

Preview

The preview method takes a result (exactly like the one returned by search) plus optional parameters and returns some JSON describing how that result should be presented to the user.

@route('/preview')
def preview():
    result = json.loads(request.query.get('result'))

    # We have to create JSON describing the desired preview for this result
    widgets = [{
        'widget' : {
            'id': 'image',
            'type': 'image',
            'source': result['mascot'],
        }},
        {'widget' : {
            'id': 'name',
            'type': 'header',
            'title': result['title'],
            'subtitle': result['subtitle'],
        }},
        {'widget' : {
            'id': 'description',
            'type': 'text',
            'text': result['description'],
        }},
        {'widget' : {
            'id': 'buttons',
            'type': 'actions',
            'actions': [{
                'label': 'view on OpenClipart',
                'uri': result['detail_link'],
                'id': 'action1',
            }],
        }},
    ]

    response.content_type = 'application/json'
    result_data = '\r\n'.join(json.dumps(w) for w in widgets)
    return result_data

Testing Your Scope

EXPLAIN AGAIN ONCE ALL THIS TOWER OF THINGS IS IN DISTRO

To test a REST scope, you need a scope server that points to your scope. Luckily, this is very easy to do, and we can even make the scope itself do it. Here's the code for that:

@route('/remote-scopes')
def remote():
    """This is just for testing purposes, doesn't belong in production."""
    response.content_type = 'application/json'
    return json.dumps([{
        'id': 'com.canonical.scopes.openclipart',
        'name': 'OpenClipart',
        'description': 'Graphics from the Open Clipart Library',
        'base_url': 'http://localhost:8000',
        'art': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg',
        'author': 'Canonical',
    }])

Remember to comment or remove that code before you put this scope in production!

Finally, to test the scope:

Navigate to your scope sliding the top bar, and test as needed.

http://ubuntuone.com/32yStxGfAE593g6bnx4A5M

The OpenClipart scope in action!

Configuration

The JSON returned in the remote call when testing, in the last section, is the configuration for the remote scope.

There are several parameters that can be put in place there:

Several examples of this configuration can be seen in the configs/scopes-info.yaml of this project.


And finally, here's the full source, including bottle boilerplate, ready to try:

import json

from bottle import (
    response,
    request,
    route,
    run,
)
import requests

# Handler for searches

@route('/search')
def search():
    # We are ignoring all optional parameters other than limit
    q = request.query.get('q')
    limit = request.query.get('limit', 20)

    if not q:  #empty query
        return

    params={
        'query': q,
        'amount': limit,
    }
    r = requests.get('https://openclipart.org/search/json/', params=params)
    # Check for errors
    if r.status_code != 200:
        raise HTTPError(status=r.status_code)
    if r.headers['content-type'] != 'application/json; charset=utf-8':
        raise HTTPError(status=502)

    # To see what data looks like, check
    # https://openclipart.org/search/json/?query=christmas&page=1&amount=20
    data = r.json()['payload']

    # We need to return at least one category object, as described in the docs
    category = {
        'category': {
            'id': 'graphics',
            'title': 'Graphics',
            'icon': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg',
            'render_template': json.dumps({  # See https://docs.google.com/a/canonical.com/document/d/1NmiM4UCnJgf6IEawmfyTOHRNAA5ZGrqpyrPqPOibwc8/edit#
                "schema-version": 1,
                "template": {
                    "category-layout": "grid",
                    "card-layout": "horizontal",
                    "card-size": "small",
                },
                "components": {
                    "title": "title",
                    "subtitle": "subtitle",
                    "mascot": "mascot",
                },
            }),
        },
    }

    # And the results, of course
    results = [category]
    for item in data:
        results.append({
            'result':{
                'cat_id': 'graphics',
                'uri': item['detail_link'],
                'title': item['title'],
                'subtitle': item['uploader'],
                'mascot': item['svg']['url'],
                'description': item['description'],
                'detail_link': item['detail_link'],
            }
        })

    # Now, we convert that into the format defined in the REST Scope docs
    # one JSON object per row, first category, then results, with
    # application/json content_type

    response.content_type = 'application/json'
    result_data = '\r\n'.join(json.dumps(r) for r in results)
    return result_data

# Handler for previews

@route('/preview')
def preview():
    result = json.loads(request.query.get('result'))

    # We have to create JSON describing the desired preview for this result
    widgets = [{
        'widget' : {
            'id': 'image',
            'type': 'image',
            'source': result['mascot'],
        }},
        {'widget' : {
            'id': 'name',
            'type': 'header',
            'title': result['title'],
            'subtitle': result['subtitle'],
        }},
        {'widget' : {
            'id': 'description',
            'type': 'text',
            'text': result['description'],
        }},
        {'widget' : {
            'id': 'buttons',
            'type': 'actions',
            'actions': [{
                'label': 'view on OpenClipart',
                'uri': result['detail_link'],
                'id': 'action1',
            }],
        }},
    ]

    response.content_type = 'application/json'
    result_data = '\r\n'.join(json.dumps(w) for w in widgets)
    return result_data

# Handler for scope listing

@route('/remote-scopes')
def remote():
    """This is just for testing purposes, doesn't belong in production."""
    response.content_type = 'application/json'
    return json.dumps([{
        'id': 'com.canonical.scopes.openclipart',
        'name': 'OpenClipart',
        'description': 'Graphics from the Open Clipart Library',
        'base_url': 'http://localhost:8000',
        'art': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg',
        'author': 'Canonical',
    }])


if __name__ == '__main__':
    run(host='localhost', port=8000)