Commits

Kirill Simonov committed 3e3fd30

Added drafts of HTML, text, JSON and CSV renderers.

The request interface was updated, but in no way is in the final form.

Comments (0)

Files changed (24)

src/htsql/application.py

         """
         with self:
             wsgi = WSGI()
-            return wsgi(environ, start_response)
+            body = wsgi(environ, start_response)
+            for chunk in body:
+                yield chunk
 
 

src/htsql/ctl/request.py

         assert isinstance(remote_user, maybe(str))
         assert isinstance(content_type, maybe(str))
         assert isinstance(content_body, maybe(oneof(str, filelike())))
-        assert isinstance(extra_headers, maybe(listof(tupleof(str, str))))
+        assert isinstance(extra_headers, maybe(dictof(str, str)))
         if method == 'GET':
             assert content_type is None
             assert content_body is None

src/htsql/export.py

 from connect import connect_adapters
 from split_sql import split_sql_adapters
 from introspect import introspect_adapters
-from translate import translate_adapters
-from produce import produce_adapters
-from render import render_adapters
+from request import request_adapters
 from tr.binder import bind_adapters
 from tr.lookup import lookup_adapters
 from tr.encoder import encode_adapters
 from tr.outliner import outline_adapters
 from tr.compiler import compile_adapters
 from tr.serializer import serialize_adapters
+from fmt import fmt_adapters
+from fmt.format import format_adapters
+from fmt.json import json_adapters
+from fmt.spreadsheet import spreadsheet_adapters
+from fmt.text import text_adapters
+from fmt.html import html_adapters
 
 
 class HTSQL_CORE(Addon):
                 outline_adapters +
                 compile_adapters +
                 serialize_adapters +
-                translate_adapters +
-                produce_adapters +
-                render_adapters)
+                request_adapters +
+                fmt_adapters +
+                format_adapters +
+                json_adapters +
+                spreadsheet_adapters +
+                text_adapters +
+                html_adapters)
 
 

src/htsql/fmt/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt`
+================
+
+This package implements product formatters.
+"""
+
+
+from ..adapter import find_adapters
+from .format import FindRenderer
+from .json import JSONRenderer
+from .spreadsheet import CSVRenderer
+from .html import HTMLRenderer
+from .text import TextRenderer
+
+
+class FindStandardRenderer(FindRenderer):
+
+    def get_renderers(self):
+        return ([CSVRenderer, JSONRenderer, HTMLRenderer, TextRenderer]
+                + super(FindStandardRenderer, self).get_renderers())
+
+
+fmt_adapters = find_adapters()
+
+

src/htsql/fmt/format.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.format`
+===================
+
+This module implements the format adapter.
+"""
+
+
+from ..util import setof
+from ..adapter import Adapter, Utility, adapts, find_adapters
+from ..domain import Domain
+
+
+class Renderer(object):
+
+    name = None
+    aliases = []
+
+    @classmethod
+    def names(cls):
+        if cls.name is not None:
+            yield cls.name
+        for name in cls.aliases:
+            yield name
+
+    def render(self, product):
+        raise NotImplementedError()
+
+
+class Formatter(Adapter):
+
+    adapts(Renderer)
+
+    def __init__(self, renderer):
+        self.renderer = renderer
+
+    def format(self, value, domain):
+        format = Format(self.renderer, domain, self)
+        return format(value)
+
+
+class Format(Adapter):
+
+    adapts(Renderer, Domain)
+
+    def __init__(self, renderer, domain, tool):
+        self.renderer = renderer
+        self.domain = domain
+        self.tool = tool
+
+    def __call__(self, value):
+        raise NotImplementedError()
+
+
+class FindRenderer(Utility):
+
+    def get_renderers(self):
+        return []
+
+    def __call__(self, names):
+        assert isinstance(names, setof(str))
+        for renderer_class in self.get_renderers():
+            for name in renderer_class.names():
+                if name in names:
+                    return renderer_class
+        return None
+
+
+format_adapters = find_adapters()
+
+

src/htsql/fmt/html.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.html`
+=====================
+
+This module implements the HTML renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import cgi
+
+
+class HTMLRenderer(Renderer):
+
+    name = 'text/html'
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = list(self.generate_body(product))
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/html; charset=UTF-8')]
+
+    def calculate_layout(self, product, formats):
+        segment = product.profile.binding.segment
+        caption = str(segment.base.syntax).decode('utf-8')
+        headers = [str(element.syntax).decode('utf-8')
+                   for element in segment.elements]
+        column_widths = [len(header) for header in headers]
+        total = 0
+        for record in product:
+            for idx, (format, value) in enumerate(zip(formats, record)):
+                width = format.measure(value)
+                column_widths[idx] = max(column_widths[idx], width)
+            total += 1
+        table_width = len(caption)
+        if total == 0:
+            total = u"(no rows)"
+        elif total == 1:
+            total = u"(1 row)"
+        else:
+            total = u"(%s rows)" % total
+        table_width = max(table_width, len(total)-2)
+        if formats:
+            columns_width = sum(column_widths)+3*(len(formats)-1)
+            table_width = max(table_width, columns_width)
+            if columns_width < table_width:
+                extra = table_width-columns_width
+                inc = extra/len(formats)
+                rem = extra - inc*len(formats)
+                for idx in range(len(formats)):
+                    column_widths[idx] += inc
+                    if idx < rem:
+                        column_widths[idx] += 1
+        caption = (u"%*s" % (-table_width, caption)).encode('utf-8')
+        headers = [(u"%*s" % (-width, header)).encode('utf-8')
+                   for width, header in zip(column_widths, headers)]
+        total = (u"%*s" % (table_width+4, total)).encode('utf-8')
+        return Layout(caption, headers, total, table_width, column_widths)
+
+    def generate_body(self, product):
+        for chunk in self.serialize_html(product):
+            yield chunk
+
+    def serialize_html(self, product):
+        yield "<!DOCTYPE HTML PUBLIC"
+        yield " \"-//W3C//DTD HTML 4.01 Transitional//EN\""
+        yield " \"http://www.w3.org/TR/html4/loose.dtd\">\n"
+        yield "<html>\n"
+        yield "<head>\n"
+        for chunk in self.serialize_head(product):
+            yield chunk
+        yield "</head>\n"
+        yield "<body>\n"
+        for chunk in self.serialize_body(product):
+            yield chunk
+        yield "</body>\n"
+        yield "</html>\n"
+
+    def serialize_head(self, product):
+        title = str(product.profile.syntax)
+        yield "<meta http-equiv=\"Content-Type\""
+        yield " content=\"text/html; charset=UTF-8\">\n"
+        yield "<title>%s</title>\n" % cgi.escape(title)
+        yield "<style type=\"text/css\">\n"
+        for chunk in self.serialize_style():
+            yield chunk
+        yield "</style>\n"
+
+    def serialize_style(self):
+        yield "body { font-family: sans-serif; font-size: 90%;"
+        yield " color: #515151; background: #ffffff }\n"
+        yield "a:link, a:visited { color: #1f4884; text-decoration: none }\n"
+        yield "a:hover { text-decoration: underline }\n"
+        yield "table { border-collapse: collapse;"
+        yield " margin: 0.5em auto; width: 100% }\n"
+        yield "table, tr { border-style: solid; border-width: 0 }\n"
+        yield "td, th { padding: 0.2em 0.5em; vertical-align: top;"
+        yield " text-align: left }\n"
+        yield "div.tab { position: relative; left: -1px; margin-right: 60%;"
+        yield " padding: 0.2em 0.5em; background: #ffffff;"
+        yield " border-style: solid; border-width: 5px 1px 0;"
+        yield " border-top-left-radius: 10px; border-top-right-radius: 10px;"
+        yield " -moz-border-radius-topleft: 10px;"
+        yield " -moz-border-radius-topright: 10px;"
+        yield " -webkit-border-top-left-radius: 10px;"
+        yield " -webkit-border-top-right-radius: 10px }\n"
+        yield "table.page { border: 0; padding: 1em; width: auto }\n"
+        yield "tr.content { padding: 1em 1em 0.5em }\n"
+        yield "tr.footer { padding: 0 1em 1em; text-align: left;"
+        yield " font-style: italic }\n"
+        yield "table.chart .number { text-align: right }\n"
+        yield "tr.caption { font-size: 105%; background: transparent }\n"
+        yield "tr.caption th { padding: 0 }\n"
+        yield "div.tab { border-color: #6f9ad3 #c3c3c3 }\n"
+        yield "tr.header { background: #dae3ea; border-color: #c3c3c3;"
+        yield " border-width: 1px 1px 0 }\n"
+        yield "tr.odd { background: #ffffff; border-color: #c3c3c3;"
+        yield " border-width: 0 1px }\n"
+        yield "tr.even { background: #f2f2f2; border-color: #c3c3c3;"
+        yield " border-width: 0 1px }\n"
+        yield "tr.odd:hover, tr.even:hover { background: #ffe3bd }\n"
+        yield "tr.total { background: transparent;"
+        yield "border-color: #c3c3c3; border-width: 1px 0 0 }\n"
+        yield "tr.total td { text-align: right; font-size: 75%;"
+        yield " font-style: italic; padding: 0.3em 0.5em 0 }\n"
+        yield "table.void { text-align: center;"
+        yield" border-color: #c3c3c3; border-width: 1px 0 }\n"
+
+    def serialize_body(self, product):
+        title = str(product.profile.syntax)
+        yield "<table class=\"page\" summary=\"%s\">\n" % cgi.escape(title)
+        yield "<tr>\n"
+        yield "<td class=\"content\">\n"
+        if product:
+            for chunk in self.serialize_content(product):
+                yield chunk
+        else:
+            for chunk in self.serialize_no_content():
+                yield chunk
+        yield "</td>\n"
+        yield "</tr>\n"
+        yield "<tr><td class=\"footer\">%s</td></tr>\n" % cgi.escape(title)
+        yield "</table>\n"
+
+    def serialize_no_content(self):
+        yield "<table class=\"void\">\n"
+        yield "<tr><td>no data</td></tr>\n"
+        yield "</table>\n"
+
+    def serialize_content(self, product):
+        caption = str(product.profile.segment.binding.base.syntax)
+        headers = [str(element.syntax)
+                   for element in product.profile.segment.elements]
+        width = len(product.profile.segment.elements)
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = HTMLFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        colspan = " colspan=\"%s\"" % width if width > 1 else ""
+        yield "<table class=\"chart\" summary=\"%s\">\n" % cgi.escape(caption)
+        yield "<tr class=\"caption\">"
+        yield ("<th%s><div class=\"tab\">%s</div></th>"
+                % (colspan, cgi.escape(caption)))
+        yield "</tr>\n"
+        if width:
+            yield "<tr class=\"header\">"
+            for (header, format) in zip(headers, formats):
+                style = (" class=\"%s\"" % format.style
+                         if format.style is not None else "")
+                yield "<th%s>%s</th>" % (style, cgi.escape(header))
+            yield "</tr>\n"
+        is_odd = False
+        total = 0
+        for record in product:
+            total += 1
+            is_odd = not is_odd
+            if width:
+                if is_odd:
+                    style = " class=\"odd\""
+                else:
+                    style = " class=\"even\""
+                yield "<tr%s>" % style
+                for value, format in zip(record, formats):
+                    style = (" class=\"%s\"" % format.style
+                             if format.style is not None else "")
+                    output = format(value)
+                    yield "<td%s>%s</td>" % (style, output)
+                yield "</tr>\n"
+        if total == 0:
+            total = "(no rows)"
+        elif total == 1:
+            total = "(1 row)"
+        else:
+            total = "(%s rows)" % total
+        yield "<tr class=\"total\"><td%s>%s</td></tr>" % (colspan, total)
+        yield "</table>"
+
+
+class HTMLFormatter(Formatter):
+
+    adapts(HTMLRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(HTMLRenderer, Domain)
+
+    style = None
+
+    def format_null(self):
+        return "<em>&mdash;</em>"
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return "<em>?</em>"
+
+
+class FormatBoolean(Format):
+
+    adapts(HTMLRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        if value is True:
+            return "<em>true</em>"
+        if value is False:
+            return "<em>false</em>"
+
+
+class FormatNumber(Format):
+
+    adapts(HTMLRenderer, NumberDomain)
+
+    style = 'number'
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return str(value)
+
+
+class FormatString(Format):
+
+    adapts(HTMLRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+class FormatEnum(Format):
+
+    adapts(HTMLRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+class FormatDate(Format):
+
+    adapts(HTMLRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return self.format_null()
+        return cgi.escape(value)
+
+
+html_adapters = find_adapters()
+
+

src/htsql/fmt/json.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.json`
+=====================
+
+This module implements the JSON renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import re
+
+
+class JSONRenderer(Renderer):
+
+    # Note: see `http://www.ietf.org/rfc/rfc4627.txt`.
+    name = 'application/json'
+    aliases = []
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = self.generate_body(product)
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'application/json')]
+
+    def generate_body(self, product):
+        if not product:
+            yield "[]\n"
+            return
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = JSONFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        record = None
+        for next_record in product:
+            if record is not None:
+                items = [format(value)
+                         for format, value in zip(formats, record)]
+                yield "  [%s],\n" % ", ".join(items)
+            else:
+                yield "[\n"
+            record = next_record
+        if record is not None:
+            items = [format(value)
+                     for format, value in zip(formats, record)]
+            yield "  [%s]\n" % ", ".join(items)
+            yield "]\n"
+        else:
+            yield "[]\n"
+
+
+class JSONFormatter(Formatter):
+
+    adapts(JSONRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(JSONRenderer, Domain)
+
+    escape_pattern = r"""[\x00-\x1F\\/"]"""
+    escape_regexp = re.compile(escape_pattern)
+    escape_table = {
+            '"': '"',
+            '\\': '\\',
+            '/': '/',
+            '\x08': 'b',
+            '\x0C': 'f',
+            '\x0A': 'n',
+            '\x0D': 'r',
+            '\x09': 't',
+    }
+
+    def replace(self, match):
+        char = match.group()
+        if char in self.escape_table:
+            return '\\'+self.escape_table[char]
+        return '\\u%04X' % ord(char)
+
+    def escape(self, value):
+        value = value.decode('utf-8')
+        value = self.escape_regexp.sub(self.replace, value)
+        value = value.encode('utf-8')
+        return '"%s"' % value
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return "\"?\""
+
+
+class FormatBoolean(Format):
+
+    adapts(JSONRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        if value is True:
+            return "true"
+        if value is False:
+            return "false"
+
+
+class FormatNumber(Format):
+
+    adapts(JSONRenderer, NumberDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return str(value)
+
+
+class FormatFloat(Format):
+
+    adapts(JSONRenderer, FloatDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return repr(value)
+
+
+class FormatString(Format):
+
+    adapts(JSONRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return self.escape(value)
+
+
+class FormatEnum(Format):
+
+    adapts(JSONRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return self.escape(value)
+
+
+class FormatDate(Format):
+
+    adapts(JSONRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return "null"
+        return str(value)
+
+
+json_adapters = find_adapters()
+
+

src/htsql/fmt/spreadsheet.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.spreadsheet`
+============================
+
+This module implements the CSV renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import csv
+import cStringIO
+
+
+class CSVRenderer(Renderer):
+
+    name = 'text/csv'
+    aliases = []
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = self.generate_body(product)
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/csv; charset=UTF-8')]
+
+    def generate_body(self, product):
+        if not product:
+            return
+        titles = [str(element.syntax)
+                  for element in product.profile.segment.elements]
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = Formatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        output = cStringIO.StringIO()
+        writer = csv.writer(output)
+        writer.writerow(titles)
+        yield output.getvalue()
+        output.seek(0)
+        output.truncate()
+        for record in product:
+            items = [format(value)
+                     for format, value in zip(formats, record)]
+            writer.writerow(items)
+            yield output.getvalue()
+            output.seek(0)
+            output.truncate()
+
+
+class CSVFormatter(Formatter):
+
+    adapts(CSVRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(CSVRenderer, Domain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return "\"?\""
+
+
+class FormatBoolean(Format):
+
+    adapts(CSVRenderer, BooleanDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        if value is True:
+            return "true"
+        if value is False:
+            return "false"
+
+
+class FormatNumber(Format):
+
+    adapts(CSVRenderer, NumberDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return str(value)
+
+
+class FormatFloat(Format):
+
+    adapts(CSVRenderer, FloatDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return repr(value)
+
+
+class FormatString(Format):
+
+    adapts(CSVRenderer, StringDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return value
+
+
+class FormatEnum(Format):
+
+    adapts(CSVRenderer, EnumDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return value
+
+
+class FormatDate(Format):
+
+    adapts(CSVRenderer, DateDomain)
+
+    def __call__(self, value):
+        if value is None:
+            return ""
+        return str(value)
+
+
+spreadsheet_adapters = find_adapters()
+
+

src/htsql/fmt/text.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.fmt.text`
+=====================
+
+This module implements the plain text renderer.
+"""
+
+
+from ..adapter import adapts, find_adapters
+from .format import Format, Formatter, Renderer
+from ..domain import (Domain, BooleanDomain, NumberDomain, FloatDomain,
+                      StringDomain, EnumDomain, DateDomain)
+import re
+
+
+class Layout(object):
+
+    def __init__(self, caption, headers, total, table_width, column_widths):
+        self.caption = caption
+        self.headers = headers
+        self.total = total
+        self.table_width = table_width
+        self.column_widths = column_widths
+
+
+class TextRenderer(Renderer):
+
+    name = 'text/plain'
+    aliases = ['']
+
+    def render(self, product):
+        status = self.generate_status(product)
+        headers = self.generate_headers(product)
+        body = list(self.generate_body(product))
+        return status, headers, body
+
+    def generate_status(self, product):
+        return "200 OK"
+
+    def generate_headers(self, product):
+        return [('Content-Type', 'text/plain; charset=UTF-8')]
+
+    def calculate_layout(self, product, formats):
+        segment = product.profile.binding.segment
+        caption = str(segment.base.syntax).decode('utf-8')
+        headers = [str(element.syntax).decode('utf-8')
+                   for element in segment.elements]
+        column_widths = [len(header) for header in headers]
+        total = 0
+        for record in product:
+            for idx, (format, value) in enumerate(zip(formats, record)):
+                width = format.measure(value)
+                column_widths[idx] = max(column_widths[idx], width)
+            total += 1
+        table_width = len(caption)
+        if total == 0:
+            total = u"(no rows)"
+        elif total == 1:
+            total = u"(1 row)"
+        else:
+            total = u"(%s rows)" % total
+        table_width = max(table_width, len(total)-2)
+        if formats:
+            columns_width = sum(column_widths)+3*(len(formats)-1)
+            table_width = max(table_width, columns_width)
+            if columns_width < table_width:
+                extra = table_width-columns_width
+                inc = extra/len(formats)
+                rem = extra - inc*len(formats)
+                for idx in range(len(formats)):
+                    column_widths[idx] += inc
+                    if idx < rem:
+                        column_widths[idx] += 1
+        caption = (u"%*s" % (-table_width, caption)).encode('utf-8')
+        headers = [(u"%*s" % (-width, header)).encode('utf-8')
+                   for width, header in zip(column_widths, headers)]
+        total = (u"%*s" % (table_width+4, total)).encode('utf-8')
+        return Layout(caption, headers, total, table_width, column_widths)
+
+    def generate_body(self, product):
+        request_title = str(product.profile.syntax)
+        if not product:
+            yield "(no data)\n"
+            yield "\n"
+            yield " ----\n"
+            yield " %s\n" % request_title
+            return
+        domains = [element.domain
+                   for element in product.profile.segment.elements]
+        tool = TextFormatter(self)
+        formats = [Format(self, domain, tool) for domain in domains]
+        layout = self.calculate_layout(product, formats)
+        yield " | " + layout.caption + " |\n"
+        yield "-+-" + "-"*layout.table_width + "-+-\n"
+        if product.profile.segment.elements:
+            yield (" | " +
+                   " | ".join(header for header in layout.headers) +
+                   " |\n")
+            yield ("-+-" +
+                   "-+-".join("-"*width for width in layout.column_widths) +
+                   "-+-\n")
+            for record in product:
+                columns = [format(value, width)
+                           for format, value, width
+                                in zip(formats, record, layout.column_widths)]
+                height = max(len(column) for column in columns)
+                for row_idx in range(height):
+                    if row_idx == 0:
+                        left, mid, right = " | ", " | ", " |\n"
+                    else:
+                        left, mid, right = " : ", " : ", " :\n"
+                    cells = []
+                    for idx, column in enumerate(columns):
+                        if row_idx < len(column):
+                            cell = column[row_idx]
+                        else:
+                            cell = " "*layout.column_widths[idx]
+                        cells.append(cell)
+                    if row_idx == 0:
+                        yield " | " + " | ".join(cells) + " |\n"
+                    else:
+                        yield " : " + " : ".join(cells) + " :\n"
+        yield " " + layout.total + "\n"
+        yield "\n"
+        yield " ----\n"
+        yield " %s\n" % request_title
+
+
+class TextFormatter(Formatter):
+
+    adapts(TextRenderer)
+
+
+class FormatDomain(Format):
+
+    adapts(TextRenderer, Domain)
+
+    unescaped_pattern = ur"""^(?=[^ "])[^\x00-\x1F]+(?<=[^ "])$"""
+    unescaped_regexp = re.compile(unescaped_pattern)
+
+    escape_pattern = ur"""[\x00-\x1F"\\]"""
+    escape_regexp = re.compile(escape_pattern)
+    escape_table = {
+            u'\\': u'\\\\',
+            u'"': u'\\"',
+            u'\b': u'\\b',
+            u'\f': u'\\f',
+            u'\n': u'\\n',
+            u'\r': u'\\r',
+            u'\t': u'\\t',
+    }
+
+    def escape_replace(self, match):
+        char = match.group()
+        if char in self.escape_table:
+            return self.escape_table[char]
+        return u"%%%02x" % ord(char)
+
+    def escape_string(self, value):
+        if self.unescaped_regexp.match(value):
+            return value
+        return u'"%s"' % self.escape_regexp.sub(self.escape_replace, value)
+
+    def format_null(self, width):
+        return [" "*width]
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return 1
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (-width, "?")]
+
+
+class FormatBoolean(Format):
+
+    adapts(TextRenderer, BooleanDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        if value is True:
+            return 4
+        if value is False:
+            return 5
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        if value is True:
+            return ["%*s" % (-width, "true")]
+        if value is False:
+            return ["%*s" % (-width, "false")]
+
+
+class FormatNumber(Format):
+
+    adapts(TextRenderer, NumberDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return len(str(value))
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (width, value)]
+
+
+class FormatString(Format):
+
+    adapts(TextRenderer, StringDomain)
+
+    threshold = 32
+
+    boundary_pattern = u"""(?<=\S) (?=\S)"""
+    boundary_regexp = re.compile(boundary_pattern)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        if len(value) <= self.threshold:
+            return len(value)
+        chunks = self.boundary_regexp.split(value)
+        max_length = max(len(chunk) for chunk in chunks)
+        if max_length >= self.threshold:
+            return max_length
+        max_length = length = 0
+        start = end = 0
+        while end < len(chunks):
+            length += len(chunks[end])
+            if end != 0:
+                length += 1
+            end += 1
+            while length > self.threshold:
+                length -= len(chunks[start])
+                if start != 0:
+                    length -= 1
+                start += 1
+            assert start < end
+            if length > max_length:
+                max_length = length
+        return max_length
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        if len(value) <= width:
+            line = u"%*s" % (-width, value)
+            return [line.encode('utf-8')]
+        chunks = self.boundary_regexp.split(value)
+        best_badnesses = []
+        best_lengths = []
+        best_sizes = []
+        for idx in range(len(chunks)):
+            chunk = chunks[idx]
+            best_badness = None
+            best_size = None
+            best_length = None
+            length = len(chunk)
+            size = 1
+            while length <= width and idx-size >= -1:
+                if size > idx:
+                    badness = 0
+                else:
+                    tail = width - best_lengths[idx-size]
+                    badness = best_badnesses[idx-size] + tail*tail
+                if best_badness is None or best_badness > badness:
+                    best_badness = badness
+                    best_size = size
+                    best_length = length
+                if idx >= size:
+                    length += len(chunks[idx-size]) + 1
+                size += 1
+            assert best_badness is not None and best_length <= width
+            best_badnesses.append(best_badness)
+            best_lengths.append(best_length)
+            best_sizes.append(best_size)
+        lines = []
+        idx = len(chunks)
+        while idx > 0:
+            size = best_sizes[idx-1]
+            group = u" ".join(chunks[idx-size:idx])
+            assert len(group) <= width
+            line = u"%*s" % (-width, group)
+            lines.insert(0, line.encode('utf-8'))
+            idx -= size
+        return lines
+
+
+class FormatEnum(Format):
+
+    adapts(TextRenderer, EnumDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        return len(value)
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        value = value.decode('utf-8')
+        value = self.escape_string(value)
+        line = u"%*s" % (-width, value)
+        return [line.encode('utf-8')]
+
+
+class FormatDate(Format):
+
+    adapts(TextRenderer, DateDomain)
+
+    def measure(self, value):
+        if value is None:
+            return 0
+        return 10
+
+    def __call__(self, value, width):
+        if value is None:
+            return self.format_null(width)
+        return ["%*s" % (-width, value)]
+
+
+text_adapters = find_adapters()
+
+

src/htsql/produce.py

-#
-# Copyright (c) 2006-2010, Prometheus Research, LLC
-# Authors: Clark C. Evans <cce@clarkevans.com>,
-#          Kirill Simonov <xi@resolvent.net>
-#
-
-
-"""
-:mod:`htsql.produce`
-====================
-
-This module implements the produce utility.
-"""
-
-
-from .adapter import Utility, find_adapters
-from .connect import DBError, Connect, Normalize
-
-
-class Profile(object):
-
-    def __init__(self, plan):
-        self.plan = plan
-        self.frame = plan.frame
-        self.sketch = plan.sketch
-        self.term = plan.term
-        self.code = plan.code
-        self.binding = plan.binding
-        self.syntax = plan.syntax
-
-
-class Product(object):
-
-    def __init__(self, profile, records=None):
-        self.profile = profile
-        self.records = records
-
-    def __iter__(self):
-        if self.records is not None:
-            return iter(self.records)
-        else:
-            return iter([])
-
-    def __nonzero__(self):
-        return (self.records is not None)
-
-
-class Produce(Utility):
-
-    def __call__(self, plan):
-        profile = Profile(plan)
-        records = None
-        if plan.sql:
-            try:
-                connect = Connect()
-                connection = connect()
-                cursor = connection.cursor()
-                cursor.execute(plan.sql)
-                rows = cursor.fetchall()
-                connection.close()
-            except DBError, exc:
-                raise EngineError(str(exc))
-            records = []
-            select = plan.frame.segment.select
-            normalizers = []
-            for phrase in plan.frame.segment.select:
-                normalize = Normalize(phrase.domain)
-                normalizers.append(normalize)
-            for row in rows:
-                values = []
-                for item, normalize in zip(row, normalizers):
-                    value = normalize(item)
-                    values.append(value)
-                records.append((values))
-        return Product(profile, records)
-
-
-produce_adapters = find_adapters()
-
-

src/htsql/render.py

-#
-# Copyright (c) 2006-2010, Prometheus Research, LLC
-# Authors: Clark C. Evans <cce@clarkevans.com>,
-#          Kirill Simonov <xi@resolvent.net>
-#
-
-
-"""
-:mod:`htsql.render`
-===================
-
-This module implements the render utility.
-"""
-
-
-from .adapter import Utility, find_adapters
-
-
-class Output(object):
-
-    def __init__(self, status, headers, body):
-        self.status = status
-        self.headers = headers
-        self.body = body
-
-
-class Render(Utility):
-
-    def __call__(self, product, environ):
-        status = "200 OK"
-        headers = [('Content-Type', 'text/plain; charset=UTF-8')]
-        body = self.render_product(product)
-        return Output(status, headers, body)
-
-    def render_product(self, product):
-        if not product:
-            yield "(no data)\n"
-        else:
-            for row in product:
-                yield ", ".join(str(value) for value in row)+"\n"
-
-
-render_adapters = find_adapters()
-
-

src/htsql/request.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql.request`
+====================
+
+This module implements the request utility.
+"""
+
+
+from .adapter import Utility, find_adapters
+from .connect import DBError, Connect, Normalize
+from .error import EngineError
+from .tr.parser import QueryParser
+from .tr.binder import Binder
+from .tr.encoder import Encoder
+from .tr.assembler import Assembler
+from .tr.outliner import Outliner
+from .tr.compiler import Compiler
+from .tr.serializer import Serializer
+#from .fmt.text import TextRenderer
+#from .fmt.spreadsheet import CSVRenderer
+from .fmt.format import FindRenderer
+import urllib
+
+
+class ElementProfile(object):
+
+    def __init__(self, binding):
+        self.binding = binding
+        self.domain = binding.domain
+        self.syntax = binding.syntax
+        self.mark = binding.mark
+
+
+class SegmentProfile(object):
+
+    def __init__(self, binding):
+        self.binding = binding
+        self.syntax = binding.syntax
+        self.mark = binding.mark
+        self.elements = [ElementProfile(element)
+                         for element in binding.elements]
+
+
+class RequestProfile(object):
+
+    def __init__(self, plan):
+        self.plan = plan
+        self.binding = plan.binding
+        self.syntax = plan.syntax
+        self.mark = plan.mark
+        self.segment = None
+        if plan.frame.segment is not None:
+            self.segment = SegmentProfile(plan.binding.segment)
+
+
+class Product(Utility):
+
+    def __init__(self, profile, records=None):
+        self.profile = profile
+        self.records = records
+
+    def __iter__(self):
+        if self.records is not None:
+            return iter(self.records)
+        else:
+            return iter([])
+
+    def __nonzero__(self):
+        return (self.records is not None)
+
+
+class Request(Utility):
+
+    @classmethod
+    def build(cls, environ):
+        # FIXME: override `classmethod` in `htsql.adapter`?
+        if not cls.is_realized:
+            cls = cls.realize()
+            return cls.build(environ)
+        path_info = environ['PATH_INFO']
+        query_string = environ.get('QUERY_STRING')
+        uri = urllib.quote(path_info)
+        if query_string:
+            uri += '?'+query_string
+        return cls(uri)
+
+    def __init__(self, uri):
+        self.uri = uri
+
+    def translate(self):
+        parser = QueryParser(self.uri)
+        syntax = parser.parse()
+        binder = Binder()
+        binding = binder.bind_one(syntax)
+        encoder = Encoder()
+        code = encoder.encode(binding)
+        assembler = Assembler()
+        term = assembler.assemble(code)
+        outliner = Outliner()
+        sketch = outliner.outline(term)
+        compiler = Compiler()
+        frame = compiler.compile(sketch)
+        serializer = Serializer()
+        plan = serializer.serialize(frame)
+        return plan
+
+    def produce(self):
+        plan = self.translate()
+        profile = RequestProfile(plan)
+        records = None
+        if plan.sql:
+            try:
+                connect = Connect()
+                connection = connect()
+                cursor = connection.cursor()
+                cursor.execute(plan.sql)
+                rows = cursor.fetchall()
+                connection.close()
+            except DBError, exc:
+                raise EngineError(str(exc), plan.mark)
+            records = []
+            select = plan.frame.segment.select
+            normalizers = []
+            for phrase in plan.frame.segment.select:
+                normalize = Normalize(phrase.domain)
+                normalizers.append(normalize)
+            for row in rows:
+                values = []
+                for item, normalize in zip(row, normalizers):
+                    value = normalize(item)
+                    values.append(value)
+                records.append((values))
+        return Product(profile, records)
+
+    def render(self, environ):
+        accept = set([''])
+        if 'HTTP_ACCEPT' in environ:
+            for name in environ['HTTP_ACCEPT'].split(','):
+                if ';' in name:
+                    name = name.split(';', 1)[0]
+                name = name.strip()
+                accept.add(name)
+        find_renderer = FindRenderer()
+        renderer_class = find_renderer(accept)
+        assert renderer_class is not None
+        renderer = renderer_class()
+        product = self.produce()
+        return renderer.render(product)
+
+    def __call__(self, environ):
+        return self.render(environ)
+
+
+request_adapters = find_adapters()
+
+

src/htsql/tr/serializer.py

     adapts(LeafFrame, Serializer)
 
     def serialize(self):
-        return self.format.name(self.frame.table.name)
+        parent = self.format.name(self.frame.table.schema_name)
+        child = self.format.name(self.frame.table.name)
+        return self.format.attr(parent, child)
 
     def call(self):
         return self.frame.table.name

src/htsql/translate.py

 from .tr.serializer import Serializer
 
 
-
 class Translate(Utility):
 
     def __call__(self, uri):

src/htsql/util.py

                 all(isinstance(item, self.item_type) for item in value))
 
 
+class setof(object):
+    """
+    Checks if a value is a set containing elements of the specified type.
+
+    Usage::
+    
+        isinstance(value, setof(T))
+    """
+
+    # For Python 2.5, we can't use `__instancecheck__`; in this case,
+    # we let ``isinstance(setof(...)) == isinstance(list)``.
+    if sys.version_info < (2, 6):
+        def __new__(cls, *args, **kwds):
+            return set
+
+    def __init__(self, item_type):
+        self.item_type = item_type
+
+    def __instancecheck__(self, value):
+        return (isinstance(value, set) and
+                all(isinstance(item, self.item_type) for item in value))
+
+
 class tupleof(object):
     """
     Checks if a value is a tuple with the fixed number of elements

src/htsql/wsgi.py

 """
 
 from .adapter import Utility, weights, find_adapters
+from .request import Request
 from .error import HTTPError
-from .translate import Translate
-from .produce import Produce
-from .render import Render
-import urllib
 
 
 class WSGI(Utility):
         Implements the WSGI entry point.
         """
         # Process the query.
+        request = Request.build(environ)
+        try:
+            status, headers, body = request.render(environ)
+        except HTTPError, exc:
+            return exc(environ, start_response)
+        start_response(status, headers)
+        return body
+
+    def request(self, environ):
         path_info = environ['PATH_INFO']
         query_string = environ.get('QUERY_STRING')
         uri = urllib.quote(path_info)
         if query_string:
             uri += '?'+query_string
-        try:
-            translate = Translate()
-            plan = translate(uri)
-            produce = Produce()
-            product = produce(plan)
-            render = Render()
-            output = render(product, environ)
-        except HTTPError, exc:
-            return exc(environ, start_response)
-        start_response(output.status, output.headers)
-        return output.body
+        return uri
 
 
 wsgi_adapters = find_adapters()

src/htsql_sqlite/connect.py

 """
 
 
-from htsql.connect import Connect, DBError
-from htsql.adapter import find_adapters
+from htsql.connect import Connect, Normalize, DBError
+from htsql.adapter import adapts, find_adapters
 from htsql.context import context
+from htsql.domain import StringDomain
 # In Python 2.6, the `sqlite3` module is built-in, but
 # for Python 2.5, we need to import a third-party module.
 try:
         return super(SQLiteConnect, self).normalize_error(exception)
 
 
+class NormalizeSQLiteString(Normalize):
+
+    adapts(StringDomain)
+
+    def __call__(self, value):
+        if isinstance(value, unicode):
+            value = value.encode('utf-8')
+        return value
+
+
 connect_adapters = find_adapters()
 
 

src/htsql_sqlite/export.py

 from .connect import connect_adapters
 from .split_sql import split_sql_adapters
 from .introspect import introspect_adapters
+from .tr.serializer import serializer_adapters
 
 
 class ENGINE_SQLITE(Addon):
     # List of adapters exported by the addon.
     adapters = (connect_adapters +
                 split_sql_adapters +
-                introspect_adapters)
+                introspect_adapters +
+                serializer_adapters)
 
 

src/htsql_sqlite/tr/__init__.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_sqlite.tr`
+======================
+
+This package adapts the HTSQL-to-SQL translator for SQLite.
+"""
+
+

src/htsql_sqlite/tr/serializer.py

+#
+# Copyright (c) 2006-2010, Prometheus Research, LLC
+# Authors: Clark C. Evans <cce@clarkevans.com>,
+#          Kirill Simonov <xi@resolvent.net>
+#
+
+
+"""
+:mod:`htsql_sqlite.tr.serializer`
+=================================
+
+This module adapts the SQL serializer for SQLite.
+"""
+
+
+from htsql.adapter import adapts, find_adapters
+from htsql.tr.frame import LeafFrame
+from htsql.tr.serializer import Serializer, SerializeLeaf
+
+
+class SQLiteSerializeLeaf(SerializeLeaf):
+
+    adapts(LeafFrame, Serializer)
+
+    def serialize(self):
+        return self.format.name(self.frame.table.name)
+
+
+serializer_adapters = find_adapters()
+
+

test/input/pgsql.yaml

   - uri: /test{id}
   - uri: /test{data}?id=1
     skip: true
+  - uri: /test
+    headers:
+      Accept: application/json
+  - uri: /test
+    headers:
+      Accept: text/csv
+  - uri: /test
+    headers:
+      Accept: text/plain
+  - uri: /test
+    headers:
+      Accept: text/html
 

test/input/sqlite.yaml

   - uri: /test{id}
   - uri: /test{data}?id=1
     skip: true
+  - uri: /test
+    headers:
+      Accept: application/json
+  - uri: /test
+    headers:
+      Accept: text/csv
+  - uri: /test
+    headers:
+      Accept: text/plain
+  - uri: /test
+    headers:
+      Accept: text/html
 

test/output/pgsql.yaml

     - [Content-Type, text/plain; charset=UTF-8]
     body: |
       (no data)
+
+       ----
+       /
   - uri: /{2+2}
     status: 200 OK
     headers:
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
-    body: |
-      1, one
-      2, two
-      3, three
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
   - uri: /test{id}
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test   |
+      -+--------+-
+       | id     |
+      -+--------+-
+       |      1 |
+       |      2 |
+       |      3 |
+         (3 rows)
+
+       ----
+       /test{id}
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, application/json]
     body: |
-      1
-      2
-      3
+      [
+        [1, "one"],
+        [2, "two"],
+        [3, "three"]
+      ]
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/csv; charset=UTF-8]
+    body: "id,data\r\n1,one\r\n2,two\r\n3,three\r\n"
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/html; charset=UTF-8]
+    body: |
+      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+      <html>
+      <head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+      <title>/test</title>
+      <style type="text/css">
+      body { font-family: sans-serif; font-size: 90%; color: #515151; background: #ffffff }
+      a:link, a:visited { color: #1f4884; text-decoration: none }
+      a:hover { text-decoration: underline }
+      table { border-collapse: collapse; margin: 0.5em auto; width: 100% }
+      table, tr { border-style: solid; border-width: 0 }
+      td, th { padding: 0.2em 0.5em; vertical-align: top; text-align: left }
+      div.tab { position: relative; left: -1px; margin-right: 60%; padding: 0.2em 0.5em; background: #ffffff; border-style: solid; border-width: 5px 1px 0; border-top-left-radius: 10px; border-top-right-radius: 10px; -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; -webkit-border-top-left-radius: 10px; -webkit-border-top-right-radius: 10px }
+      table.page { border: 0; padding: 1em; width: auto }
+      tr.content { padding: 1em 1em 0.5em }
+      tr.footer { padding: 0 1em 1em; text-align: left; font-style: italic }
+      table.chart .number { text-align: right }
+      tr.caption { font-size: 105%; background: transparent }
+      tr.caption th { padding: 0 }
+      div.tab { border-color: #6f9ad3 #c3c3c3 }
+      tr.header { background: #dae3ea; border-color: #c3c3c3; border-width: 1px 1px 0 }
+      tr.odd { background: #ffffff; border-color: #c3c3c3; border-width: 0 1px }
+      tr.even { background: #f2f2f2; border-color: #c3c3c3; border-width: 0 1px }
+      tr.odd:hover, tr.even:hover { background: #ffe3bd }
+      tr.total { background: transparent;border-color: #c3c3c3; border-width: 1px 0 0 }
+      tr.total td { text-align: right; font-size: 75%; font-style: italic; padding: 0.3em 0.5em 0 }
+      table.void { text-align: center; border-color: #c3c3c3; border-width: 1px 0 }
+      </style>
+      </head>
+      <body>
+      <table class="page" summary="/test">
+      <tr>
+      <td class="content">
+      <table class="chart" summary="test">
+      <tr class="caption"><th colspan="2"><div class="tab">test</div></th></tr>
+      <tr class="header"><th class="number">id</th><th>data</th></tr>
+      <tr class="odd"><td class="number">1</td><td>one</td></tr>
+      <tr class="even"><td class="number">2</td><td>two</td></tr>
+      <tr class="odd"><td class="number">3</td><td>three</td></tr>
+      <tr class="total"><td colspan="2">(3 rows)</td></tr></table></td>
+      </tr>
+      <tr><td class="footer">/test</td></tr>
+      </table>
+      </body>
+      </html>
   - uri: /test{data}?id=1
     status: 200 OK
     headers:

test/output/sqlite.yaml

     - [Content-Type, text/plain; charset=UTF-8]
     body: |
       (no data)
+
+       ----
+       /
   - uri: /{2+2}
     status: 200 OK
     headers:
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
-    body: |
-      1, one
-      2, two
-      3, three
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
   - uri: /test{id}
     status: 200 OK
     headers:
     - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test   |
+      -+--------+-
+       | id     |
+      -+--------+-
+       |      1 |
+       |      2 |
+       |      3 |
+         (3 rows)
+
+       ----
+       /test{id}
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, application/json]
     body: |
-      1
-      2
-      3
+      [
+        [1, "one"],
+        [2, "two"],
+        [3, "three"]
+      ]
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/csv; charset=UTF-8]
+    body: "id,data\r\n1,one\r\n2,two\r\n3,three\r\n"
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/plain; charset=UTF-8]
+    body: |2
+       | test       |
+      -+------------+-
+       | id | data  |
+      -+----+-------+-
+       |  1 | one   |
+       |  2 | two   |
+       |  3 | three |
+             (3 rows)
+
+       ----
+       /test
+  - uri: /test
+    status: 200 OK
+    headers:
+    - [Content-Type, text/html; charset=UTF-8]
+    body: |
+      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+      <html>
+      <head>
+      <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+      <title>/test</title>
+      <style type="text/css">
+      body { font-family: sans-serif; font-size: 90%; color: #515151; background: #ffffff }
+      a:link, a:visited { color: #1f4884; text-decoration: none }
+      a:hover { text-decoration: underline }
+      table { border-collapse: collapse; margin: 0.5em auto; width: 100% }
+      table, tr { border-style: solid; border-width: 0 }
+      td, th { padding: 0.2em 0.5em; vertical-align: top; text-align: left }
+      div.tab { position: relative; left: -1px; margin-right: 60%; padding: 0.2em 0.5em; background: #ffffff; border-style: solid; border-width: 5px 1px 0; border-top-left-radius: 10px; border-top-right-radius: 10px; -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; -webkit-border-top-left-radius: 10px; -webkit-border-top-right-radius: 10px }
+      table.page { border: 0; padding: 1em; width: auto }
+      tr.content { padding: 1em 1em 0.5em }
+      tr.footer { padding: 0 1em 1em; text-align: left; font-style: italic }
+      table.chart .number { text-align: right }
+      tr.caption { font-size: 105%; background: transparent }
+      tr.caption th { padding: 0 }
+      div.tab { border-color: #6f9ad3 #c3c3c3 }
+      tr.header { background: #dae3ea; border-color: #c3c3c3; border-width: 1px 1px 0 }
+      tr.odd { background: #ffffff; border-color: #c3c3c3; border-width: 0 1px }
+      tr.even { background: #f2f2f2; border-color: #c3c3c3; border-width: 0 1px }
+      tr.odd:hover, tr.even:hover { background: #ffe3bd }
+      tr.total { background: transparent;border-color: #c3c3c3; border-width: 1px 0 0 }
+      tr.total td { text-align: right; font-size: 75%; font-style: italic; padding: 0.3em 0.5em 0 }
+      table.void { text-align: center; border-color: #c3c3c3; border-width: 1px 0 }
+      </style>
+      </head>
+      <body>
+      <table class="page" summary="/test">
+      <tr>
+      <td class="content">
+      <table class="chart" summary="test">
+      <tr class="caption"><th colspan="2"><div class="tab">test</div></th></tr>
+      <tr class="header"><th class="number">id</th><th>data</th></tr>
+      <tr class="odd"><td class="number">1</td><td>one</td></tr>
+      <tr class="even"><td class="number">2</td><td>two</td></tr>
+      <tr class="odd"><td class="number">3</td><td>three</td></tr>
+      <tr class="total"><td colspan="2">(3 rows)</td></tr></table></td>
+      </tr>
+      <tr><td class="footer">/test</td></tr>
+      </table>
+      </body>
+      </html>
   - uri: /test{data}?id=1
     status: 200 OK
     headers: