summaryrefslogtreecommitdiff
path: root/docs/jsonschema_role.py
blob: 12091450ece212577bf9ae9da273a0705ff750f2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
from datetime import datetime
from docutils import nodes
import errno
import os

try:
    import urllib2 as urllib
except ImportError:
    import urllib.request as urllib

from lxml import html


VALIDATION_SPEC = "http://json-schema.org/latest/json-schema-validation.html"


def setup(app):
    """
    Install the plugin.

    :argument sphinx.application.Sphinx app: the Sphinx application context

    """

    app.add_config_value("cache_path", "_cache", "")

    try:
        os.makedirs(app.config.cache_path)
    except OSError as error:
        if error.errno != errno.EEXIST:
            raise

    path = os.path.join(app.config.cache_path, "spec.html")
    spec = fetch_or_load(path)
    app.add_role("validator", docutils_sucks(spec))


def fetch_or_load(spec_path):
    """
    Fetch a new specification or use the cache if it's current.

    :argument cache_path: the path to a cached specification

    """

    headers = {}

    try:
        modified = datetime.utcfromtimestamp(os.path.getmtime(spec_path))
        date = modified.strftime("%a, %d %b %Y %I:%M:%S UTC")
        headers["If-Modified-Since"] = date
    except OSError as error:
        if error.errno != errno.ENOENT:
            raise

    request = urllib.Request(VALIDATION_SPEC, headers=headers)
    response = urllib.urlopen(request)

    if response.code == 200:
        with open(spec_path, "w+b") as spec:
            spec.writelines(response)
            spec.seek(0)
            return html.parse(spec)

    with open(spec_path) as spec:
        return html.parse(spec)


def docutils_sucks(spec):
    """
    Yeah.

    It doesn't allow using a class because it does stupid stuff like try to set
    attributes on the callable object rather than just keeping a dict.

    """

    base_url = VALIDATION_SPEC
    ref_url = "http://json-schema.org/latest/json-schema-core.html#anchor25"
    schema_url = "http://json-schema.org/latest/json-schema-core.html#anchor22"

    def validator(name, raw_text, text, lineno, inliner):
        """
        Link to the JSON Schema documentation for a validator.

        :argument str name: the name of the role in the document
        :argument str raw_source: the raw text (role with argument)
        :argument str text: the argument given to the role
        :argument int lineno: the line number
        :argument docutils.parsers.rst.states.Inliner inliner: the inliner

        :returns: 2-tuple of nodes to insert into the document and an iterable
            of system messages, both possibly empty

        """

        if text == "$ref":
            return [nodes.reference(raw_text, text, refuri=ref_url)], []
        elif text == "$schema":
            return [nodes.reference(raw_text, text, refuri=schema_url)], []

        xpath = "//h3[re:match(text(), '(^|\W)\"?{0}\"?($|\W,)', 'i')]"
        header = spec.xpath(
            xpath.format(text),
            namespaces={"re": "http://exslt.org/regular-expressions"},
        )

        if len(header) == 0:
            inliner.reporter.warning(
                "Didn't find a target for {0}".format(text),
            )
            uri = base_url
        else:
            if len(header) > 1:
                inliner.reporter.info(
                    "Found multiple targets for {0}".format(text),
                )
            uri = base_url + "#" + header[0].getprevious().attrib["name"]

        reference = nodes.reference(raw_text, text, refuri=uri)
        return [reference], []

    return validator