Skip to content

Add RFC6570 Uri Template Support #3

Merged
merged 14 commits into from Apr 10, 2012
View
203 tests/test_template.py
@@ -0,0 +1,203 @@
+# encoding: utf-8
+from uricore import URI, IRI
+from nose.tools import eq_
+from uricore.template import uri_template
+from collections import OrderedDict
+
+# http://tools.ietf.org/html/rfc6570#section-3.2
+params = {
+ 'count': ("one", "two", "three"),
+ 'dom': ("example", "com"),
+ 'dub': "me/too",
+ 'hello': "Hello World!",
+ 'half': "50%",
+ 'var': "value",
+ 'who': "fred",
+ 'base': "http://example.com/home/",
+ 'path': "/foo/bar",
+ 'list': ("red", "green", "blue"),
+ 'year': ("1965", "2000", "2012"),
+ 'semi': ';',
+ 'v': 6,
+ 'x': 1024,
+ 'y': 768,
+ 'empty': "",
+ 'empty_keys': [],
+ 'undef': None,
+ 'list': ["red", "green", "blue"],
+ 'keys': OrderedDict([('semi', ";"), ('dot', "."), ('comma', ",")]),
+}
+
+def check_template(template, expansion):
+ eq_(uri_template(template, **params), expansion)
+
+
+def test_composite_values():
+ yield check_template, "find{?year*}", "find?year=1965&year=2000&year=2012"
+ yield check_template, "www{.dom*}", "www.example.com"
+
+
+def test_form_continuation_expansion():
+ yield check_template, "{&who}", "&who=fred"
+ yield check_template, "{&half}", "&half=50%25"
+ yield check_template, "?fixed=yes{&x}", "?fixed=yes&x=1024"
+ yield check_template, "{&x,y,empty}", "&x=1024&y=768&empty="
+ yield check_template, "{&x,y,undef}", "&x=1024&y=768"
+ yield check_template, "{&var:3}", "&var=val"
+ yield check_template, "{&list}", "&list=red,green,blue"
+ yield check_template, "{&list*}", "&list=red&list=green&list=blue"
+ yield check_template, "{&keys}", "&keys=semi,%3B,dot,.,comma,%2C"
+ yield check_template, "{&keys*}", "&semi=%3B&dot=.&comma=%2C"
+
+
+def test_form_style_expansion():
+ yield check_template, "{?who}", "?who=fred"
+ yield check_template, "{?half}", "?half=50%25"
+ yield check_template, "{?x,y}", "?x=1024&y=768"
+ yield check_template, "{?x,y,empty}", "?x=1024&y=768&empty="
+ yield check_template, "{?x,y,undef}", "?x=1024&y=768"
+ yield check_template, "{?var:3}", "?var=val"
+ yield check_template, "{?list}", "?list=red,green,blue"
+ yield check_template, "{?list*}", "?list=red&list=green&list=blue"
+ yield check_template, "{?keys}", "?keys=semi,%3B,dot,.,comma,%2C"
+ yield check_template, "{?keys*}", "?semi=%3B&dot=.&comma=%2C"
+
+
+def test_fragment_expansion():
+ yield check_template, "{#var}", "#value"
+ yield check_template, "{#hello}", "#Hello%20World!"
+ yield check_template, "{#half}", "#50%25"
+ yield check_template, "foo{#empty}", "foo#"
+ yield check_template, "foo{#undef}", "foo"
+ yield check_template, "{#x,hello,y}", "#1024,Hello%20World!,768"
+ yield check_template, "{#path,x}/here", "#/foo/bar,1024/here"
+ yield check_template, "{#path:6}/here", "#/foo/b/here"
+ yield check_template, "{#list}", "#red,green,blue"
+ yield check_template, "{#list*}", "#red,green,blue"
+ yield check_template, "{#keys}", "#semi,;,dot,.,comma,,"
+ yield check_template, "{#keys*}", "#semi=;,dot=.,comma=,"
+
+
+def test_label_expansion():
+ yield check_template, "{.who}", ".fred"
+ yield check_template, "{.who,who}", ".fred.fred"
+ yield check_template, "{.half,who}", ".50%25.fred"
+ yield check_template, "www{.dom*}", "www.example.com"
+ yield check_template, "X{.var}", "X.value"
+ yield check_template, "X{.empty}", "X."
+ yield check_template, "X{.undef}", "X"
+ yield check_template, "X{.var:3}", "X.val"
+ yield check_template, "X{.list}", "X.red,green,blue"
+ yield check_template, "X{.list*}", "X.red.green.blue"
+ yield check_template, "X{.keys}", "X.semi,%3B,dot,.,comma,%2C"
+ yield check_template, "X{.keys*}", "X.semi=%3B.dot=..comma=%2C"
+ yield check_template, "X{.empty_keys}", "X"
+ yield check_template, "X{.empty_keys*}", "X"
+
+
+def test_path_expansion():
+ yield check_template, "{/who}", "/fred"
+ yield check_template, "{/who,who}", "/fred/fred"
+ yield check_template, "{/half,who}", "/50%25/fred"
+ yield check_template, "{/who,dub}", "/fred/me%2Ftoo"
+ yield check_template, "{/var}", "/value"
+ yield check_template, "{/var,empty}", "/value/"
+ yield check_template, "{/var,undef}", "/value"
+ yield check_template, "{/var,x}/here", "/value/1024/here"
+ yield check_template, "{/var:1,var}", "/v/value"
+ yield check_template, "{/list}", "/red,green,blue"
+ yield check_template, "{/list*}", "/red/green/blue"
+ yield check_template, "{/list*,path:4}", "/red/green/blue/%2Ffoo"
+ yield check_template, "{/keys}", "/semi,%3B,dot,.,comma,%2C"
+ yield check_template, "{/keys*}", "/semi=%3B/dot=./comma=%2C"
+
+
+def test_path_style_expansion():
+ yield check_template, "{;who}", ";who=fred"
+ yield check_template, "{;half}", ";half=50%25"
+ yield check_template, "{;empty}", ";empty"
+ yield check_template, "{;v,empty,who}", ";v=6;empty;who=fred"
+ yield check_template, "{;v,bar,who}", ";v=6;who=fred"
+ yield check_template, "{;x,y}", ";x=1024;y=768"
+ yield check_template, "{;x,y,empty}", ";x=1024;y=768;empty"
+ yield check_template, "{;x,y,undef}", ";x=1024;y=768"
+ yield check_template, "{;hello:5}", ";hello=Hello"
+ yield check_template, "{;list}", ";list=red,green,blue"
+ yield check_template, "{;list*}", ";list=red;list=green;list=blue"
+ yield check_template, "{;keys}", ";keys=semi,%3B,dot,.,comma,%2C"
+ yield check_template, "{;keys*}", ";semi=%3B;dot=.;comma=%2C"
+
+
+def test_reserved_expansion():
+ yield check_template, "{+var}", "value"
+ yield check_template, "{+hello}", "Hello%20World!"
+ yield check_template, "{+half}", "50%25"
+ yield check_template, "{base}index", "http%3A%2F%2Fexample.com%2Fhome%2Findex"
+ yield check_template, "{+base}index", "http://example.com/home/index"
+ yield check_template, "O{+empty}X", "OX"
+ yield check_template, "O{+undef}X", "OX"
+ yield check_template, "{+path}/here", "/foo/bar/here"
+ yield check_template, "here?ref={+path}", "here?ref=/foo/bar"
+ yield check_template, "up{+path}{var}/here", "up/foo/barvalue/here"
+ yield check_template, "{+x,hello,y}", "1024,Hello%20World!,768"
+ yield check_template, "{+path,x}/here", "/foo/bar,1024/here"
+ yield check_template, "{+path:6}/here", "/foo/b/here"
+ yield check_template, "{+list}", "red,green,blue"
+ yield check_template, "{+list*}", "red,green,blue"
+ yield check_template, "{+keys}", "semi,;,dot,.,comma,,"
+ yield check_template, "{+keys*}", "semi=;,dot=.,comma=,"
+
+
+def test_simple_string_expansion():
+ yield check_template, "{var}", "value"
+ yield check_template, "{hello}", "Hello%20World%21"
+ yield check_template, "{half}", "50%25"
+ yield check_template, "O{empty}X", "OX"
+ yield check_template, "O{undef}X", "OX"
+ yield check_template, "{x,y}", "1024,768"
+ yield check_template, "{x,hello,y}", "1024,Hello%20World%21,768"
+ yield check_template, "?{x,empty}", "?1024,"
+ yield check_template, "?{x,undef}", "?1024"
+ yield check_template, "?{undef,y}", "?768"
+ yield check_template, "{var:3}", "val"
+ yield check_template, "{var:30}", "value"
+ yield check_template, "{list}", "red,green,blue"
+ yield check_template, "{list*}", "red,green,blue"
+ yield check_template, "{keys}", "semi,%3B,dot,.,comma,%2C"
+ yield check_template, "{keys*}", "semi=%3B,dot=.,comma=%2C"
+
+
+def test_test_prefix_values():
+ yield check_template, "{var}", "value"
+ yield check_template, "{var:20}", "value"
+ yield check_template, "{var:3}", "val"
+ yield check_template, "{semi}", "%3B"
+ yield check_template, "{semi:2}", "%3B"
+
+
+def test_variable_expansion():
+ yield check_template, "{count}", "one,two,three"
+ yield check_template, "{count*}", "one,two,three"
+ yield check_template, "{/count}", "/one,two,three"
+ yield check_template, "{/count*}", "/one/two/three"
+ yield check_template, "{;count}", ";count=one,two,three"
+ yield check_template, "{;count*}", ";count=one;count=two;count=three"
+ yield check_template, "{?count}", "?count=one,two,three"
+ yield check_template, "{?count*}", "?count=one&count=two&count=three"
+ yield check_template, "{&count*}", "&count=one&count=two&count=three"
+
+
+def test_composite_values():
+ yield check_template, "find{?year*}", "find?year=1965&year=2000&year=2012"
+ yield check_template, "www{.dom*}", "www.example.com"
+
+
+def test_uri_template():
+ eq_(URI("http://example.com/value"),
+ URI.from_template("http://example.com/{var}", var="value"))
+
+
+def test_iri_template():
+ eq_(IRI(u'http://\u2603/value'),
+ IRI.from_template(u'http://\N{SNOWMAN}/{var}', var='value'))
+
View
6 uricore/core.py
@@ -7,6 +7,7 @@
import urllib.parse as urlparse
from collections import defaultdict
+from template import uri_template
# TODO: import these from httpcore someday
from . import wkz_urls as urls
@@ -194,6 +195,11 @@ def join(self, other):
return type(self)(unsplit(**vals), query_cls=self.query_cls)
+ @classmethod
+ def from_template(cls, template, **kwargs):
+ return cls(uri_template(template, **kwargs))
+
+
class IRI(ResourceIdentifier):
View
118 uricore/template.py
@@ -0,0 +1,118 @@
+import re
+from . import wkz_urls as urls
+
+
+def _format_mapping(operator, item):
+ try:
+ k, v, mapped = item
+ except ValueError:
+ k, v = item
+ mapped = False
+
+ if operator in ['#', '+']:
+ # From http://tools.ietf.org/html/rfc6570#section-1.5
+ safe = ':/?#[]@!$&\'\"()*/+,;='
+ else:
+ safe = ''
+
+ if isinstance(v, (list, tuple)):
+ v = ','.join(urls.url_quote(x, safe=safe) for x in v)
+ else:
+ v = urls.url_quote(v, safe=safe)
+
+ if operator in [';', '?', '&'] or mapped:
+ if not v:
+ mid = '' if operator == ';' else '='
+ else:
+ mid = '='
+
+ return "{}{}{}".format(k, mid, v)
+ else:
+ return "{}".format(v)
+
+
+def _template_joiner(operator):
+ if operator in ['#', '+', '']:
+ return ','
+ elif operator == '?':
+ return '&'
+ elif operator == '.':
+ return'.'
+ return operator
+
+
+def _varspec_expansion(operator, varspec, data):
+ portion = None
+ explode = False
+
+ if ':' in varspec:
+ varspec, portion = varspec.split(':', 1)
+ portion = int(portion)
+
+ if varspec.endswith('*'):
+ varspec = varspec[:-1]
+ explode = True
+
+ value = data.get(varspec)
+
+ if value == None:
+ return []
+
+ try:
+ if len(value) == 0 and value != "":
+ return []
+ except TypeError:
+ pass
+
+ try:
+ if explode:
+ return [(k, v, True) for k,v in value.items()]
+ else:
+ parts = []
+ for k, v in value.items():
+ parts += [k, v]
+ return [(varspec, parts)]
+ except AttributeError:
+ pass
+
+ if isinstance(value, (list, tuple)):
+ if explode:
+ return [(varspec, v) for v in value]
+ else:
+ return [(varspec, value)]
+
+ value = unicode(value)
+
+ if portion is not None:
+ value = value[:portion]
+
+ return [(varspec, value)]
+
+
+def uri_template(template, **kwargs):
+
+ def template_expansion(matchobj):
+ varlist = matchobj.group(1)
+ operator = ''
+
+ if re.match(r"\+|#|\.|/|;|\?|&", varlist):
+ operator = varlist[0]
+ varlist = varlist[1:]
+
+ prefix = '' if operator == '+' else operator
+ joiner = _template_joiner(operator)
+
+ params = []
+ for varspec in varlist.split(','):
+ params += _varspec_expansion(operator, varspec, kwargs)
+
+ uri = [_format_mapping(operator, item) for item in params]
+
+ if not uri:
+ return ""
+
+ return prefix + joiner.join(uri)
+
+ return re.sub(r"{(.*?)}", template_expansion, template)
+
+
Something went wrong with that request. Please try again.