From 018af454f286120452e33d2568dd40908474a8a8 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Thu, 7 Jun 2018 08:26:05 -0700 Subject: [PATCH] Initial sketch of custom URL routing, refs #306 --- datasette/routes.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_routes.py | 65 ++++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 datasette/routes.py create mode 100644 tests/test_routes.py diff --git a/datasette/routes.py b/datasette/routes.py new file mode 100644 index 00000000..a404a3c0 --- /dev/null +++ b/datasette/routes.py @@ -0,0 +1,78 @@ +from collections import namedtuple +from datasette.views.database import DatabaseView +from datasette.views.special import JsonDataView +from datasette.views.table import TableView + +RouteResult = namedtuple('RouteResult', ('view', 'kwargs', 'redirect')) + + +def redirect(path): + return RouteResult(None, None, path) + + +def resolve(path, database_exists, table_exists, database_hash): + bits = path.split('/') + # Kill the leading /: + del bits[0] + # /-/... + if bits[0] == '-': + return resolve_special(bits[1:]) + # /databasename + bit = bits[0] + rest = '' + if bits[1:]: + rest = '/'.join([''] + bits[1:]) + # Might be database-databasehash + if '-' in bit: + database, databasehash = bit.rsplit('-', 1) + if database_exists(database): + # Is the hash correct? + expected_hash = database_hash(database) + if expected_hash == databasehash: + if not rest: + return RouteResult( + DatabaseView, {'database': database}, None + ) + else: + # Pass on to table logic + return resolve_table(rest, database, table_exists) + else: + # Bad hash, redirect + return redirect('/{}-{}{}'.format( + database, expected_hash, rest) + ) + # If we get here, maybe the full string is a DB name? + if database_exists(bit): + database = bit + databasehash = database_hash(bit) + return redirect('/{}-{}{}'.format(database, databasehash, rest)) + return None + + +def resolve_table(rest, database, table_exists): + # TODO: Rows, views, canned queries + table = rest.lstrip('/') + if not table_exists(database, table): + return None + return RouteResult(TableView, {'database': database, 'table': table}, None) + + +specials = {'inspect', 'metadata', 'versions', 'plugins', 'config'} + + +def resolve_special(path_bits): + if len(path_bits) != 1: + return None + filename = path_bits[0] + as_json = False + if filename.endswith('.json'): + as_json = True + filename = filename.replace('.json', '') + if filename not in specials: + return None + kwargs = { + 'filename': filename, + } + if as_json: + kwargs['format'] = 'json' + return RouteResult(JsonDataView, kwargs, None) diff --git a/tests/test_routes.py b/tests/test_routes.py new file mode 100644 index 00000000..de682cbc --- /dev/null +++ b/tests/test_routes.py @@ -0,0 +1,65 @@ +from datasette import routes +from datasette.views.database import DatabaseView +from datasette.views.special import JsonDataView +from datasette.views.table import TableView +import pytest + +MOCK_DATABASES = { + # database: set-of-tables + 'foo': {'bar'}, + 'foo-bar': {'baz'} +} +MOCK_DATABASE_HASHES = { + 'foo': 'foohash', + 'foo-bar': 'foobarhash', +} + + +def database_exists(database): + return database in MOCK_DATABASES + + +def table_exists(database, table): + print('table_exists: ', database, table) + return table in MOCK_DATABASES.get(database, set()) + + +def database_hash(database): + return MOCK_DATABASE_HASHES[database] + + +@pytest.mark.parametrize('path,expected', [ + ('/does-not-exist', None), + # This should redirect + ('/foo', routes.RouteResult( + None, None, '/foo-foohash' + )), + ('/foo-bar-badhash', routes.RouteResult( + None, None, '/foo-bar-foobarhash' + )), + ('/foo-foohash', routes.RouteResult( + DatabaseView, {'database': 'foo'}, None + )), + # Table views + ('/foo/bar', routes.RouteResult( + None, None, '/foo-foohash/bar' + )), + ('/foo/bad', routes.RouteResult( + None, None, '/foo-foohash/bad' + )), + ('/foo-foohash/bad', None), + ('/foo-foohash/bar', routes.RouteResult( + TableView, {'database': 'foo', 'table': 'bar'}, None + )), +] + [ + ('/-/{}'.format(filename), routes.RouteResult( + JsonDataView, {'filename': filename}, None + )) for filename in ('inspect', 'metadata', 'versions', 'plugins', 'config') +] + [ + ('/-/{}.json'.format(filename), routes.RouteResult( + JsonDataView, {'filename': filename, 'format': 'json'}, None + )) for filename in ('inspect', 'metadata', 'versions', 'plugins', 'config') +]) +def test_routes(path, expected): + actual = routes.resolve(path, database_exists, table_exists, database_hash) + assert actual == expected