Skip to content

Commit 3fa6c7a

Browse files
committed
Add HALE support
1 parent b21b6a7 commit 3fa6c7a

File tree

7 files changed

+226
-9
lines changed

7 files changed

+226
-9
lines changed

hypermedia_resource/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__title__ = 'hypermedia_resource'
2-
__version__ = '0.1.10'
2+
__version__ = '0.1.11'
33
__author__ = 'Stephen Mizell'
44
__license__ = 'MIT'
55

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import json
2+
from hypermedia_resource.base import HypermediaResource
3+
4+
RESERVED_ATTRIBUTES = ["_links", "_embedded"]
5+
6+
def parse_attributes(hal_rep, resource):
7+
attrs = [[key, value] for key, value in hal_rep.items()
8+
if key not in RESERVED_ATTRIBUTES]
9+
for attr in attrs:
10+
resource.attributes.add(attr[0], attr[1])
11+
12+
def parse_link(rel, link, resource):
13+
if "templated" in link and link["templated"]:
14+
return
15+
if (link.has_key("method") and link["method"] != "GET"):
16+
resource = resource.actions.add(rel, link["href"], link["method"])
17+
else:
18+
resource = resource.links.add(rel=rel, href=link["href"])
19+
if link.has_key("request_encoding"):
20+
resource.response_types.add(link["request_encoding"])
21+
if link.has_key("data"):
22+
# Right now, it only supports name and values
23+
for name, value in link["data"].items():
24+
attr = resource.attributes.add(name)
25+
if value.has_key("options"):
26+
for option in value["options"]:
27+
attr.options.add(option, option)
28+
return resource
29+
30+
def parse_links(hal_rep, resource):
31+
if not "_links" in hal_rep or not hal_rep["_links"]:
32+
return
33+
for rel, link in hal_rep["_links"].items():
34+
if type(link) is dict:
35+
parse_link(rel, link, resource)
36+
else:
37+
for item in link:
38+
parse_link(rel, item, resource)
39+
40+
def parse_embedded(rel, embedded, resource):
41+
href = embedded["_links"]["self"]
42+
item = resource.embedded_resources.add(rel, href)
43+
parse_attributes(embedded, item)
44+
parse_links(embedded, item)
45+
parse_embeddeds(embedded, item)
46+
return item
47+
48+
def parse_embeddeds(hal_rep, resource):
49+
if not "_embedded" in hal_rep or not hal_rep["_embedded"]:
50+
return
51+
for rel, link in hal_rep["_embedded"].items():
52+
if type(link) is dict:
53+
parse_embedded(rel, link, resource)
54+
else:
55+
for item in link:
56+
parse_embedded(rel, item, resource)
57+
58+
class HaleJSONAdapter(object):
59+
60+
media_type = "application/vnd.hale+json"
61+
62+
@classmethod
63+
def parse(self, raw_json):
64+
hal_rep = json.loads(raw_json)
65+
resource = HypermediaResource()
66+
parse_attributes(hal_rep, resource)
67+
parse_links(hal_rep, resource)
68+
parse_embeddeds(hal_rep, resource)
69+
return resource

hypermedia_resource/base.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,8 @@ class BaseTransitionItem(object):
5555
def __init__(self, rel, method='GET', **kwargs):
5656
self.rel = rel
5757
self.method = method
58-
self.hreflang = kwargs.get('embed_as', None)
58+
self.hreflang = kwargs.get('hreflang', None)
5959
self.embed_as = kwargs.get('embed_as', None)
60-
self.language = kwargs.get('language', None)
6160
self.response_types = MediaTypeCollection()
6261
self.label = kwargs.get('label', None)
6362

hypermedia_resource/media_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
class MediaTypeCollection(ItemCollection):
44

55
def __init__(self):
6+
super(MediaTypeCollection, self).__init__()
67
self.item = MediaTypeItem
78

89
class MediaTypeItem(object):

hypermedia_resource/wrappers.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ def build_response(self, resource, accepts):
5858

5959
class FlaskAPIResource(APIResource):
6060

61+
def flask_response(self, resource, request, *args, **kwargs):
62+
response = self.build_response(resource, request.headers.get('Accept'))
63+
return Response(response.body, mimetype=response.media_type, *args, **kwargs)
64+
6165
def get_method(self, request):
6266
if request.method != "POST":
6367
return request.method
@@ -70,6 +74,4 @@ def response_for(self, request):
7074
method = self.get_method(request)
7175
action_name = self.actions()[method]
7276
action = getattr(self, action_name)
73-
resource = action(request)
74-
response = self.build_response(resource, request.headers.get('Accept'))
75-
return Response(response.body, mimetype=response.media_type)
77+
return action(request)

tests/adapters/hale_json_test.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import sys
2+
import unittest
3+
import json
4+
import logging
5+
6+
from hypermedia_resource import HypermediaResource
7+
from hypermedia_resource.adapters.hale_json import HaleJSONAdapter
8+
9+
hale_example = """{
10+
"_meta": {
11+
"any": {
12+
"json": "object"
13+
}
14+
},
15+
"attribute": "value",
16+
"_links": {
17+
"self": {
18+
"href": "..."
19+
},
20+
"search": {
21+
"href": ".../{?send_info}",
22+
"templated": true,
23+
"method": "GET",
24+
"data": {
25+
"send_info": {
26+
"options": [
27+
"yes",
28+
"no",
29+
"maybe"
30+
],
31+
"in": true
32+
}
33+
}
34+
},
35+
"agent": {
36+
"href": "/agent/1",
37+
"method": "GET",
38+
"render": "embed"
39+
},
40+
"customer": [
41+
{
42+
"href": "/customer/1",
43+
"method": "GET"
44+
}
45+
],
46+
"edit-customer": {
47+
"href": ".../1",
48+
"method": "PUT",
49+
"request_encoding": "application/json",
50+
"data": {
51+
"name": {
52+
"type": "string",
53+
"required": true
54+
},
55+
"send_info": {
56+
"options": [
57+
"yes",
58+
"no",
59+
"maybe"
60+
],
61+
"in": true
62+
},
63+
"user_id": {
64+
"scope": "href",
65+
"required": true
66+
}
67+
}
68+
}
69+
},
70+
"_embedded": {
71+
"customer": [
72+
{
73+
"_links": {
74+
"self": {
75+
"href": "/customer/1",
76+
"method": "GET"
77+
},
78+
"edit": {
79+
"href": ".../{?user_id}",
80+
"method": "PUT",
81+
"request_encoding": "application/json",
82+
"render": "resource",
83+
"data": {
84+
"name": {
85+
"type": "string",
86+
"required": true
87+
},
88+
"send_info": {
89+
"options": [
90+
"yes",
91+
"no",
92+
"maybe"
93+
],
94+
"in": true
95+
},
96+
"user_id": {
97+
"scope": "href",
98+
"required": true
99+
}
100+
}
101+
}
102+
},
103+
"name": "Tom",
104+
"send_info": "yes"
105+
}
106+
]
107+
}
108+
}"""
109+
110+
class TestParse(unittest.TestCase):
111+
112+
def setUp(self):
113+
HypermediaResource.adapters.add(HaleJSONAdapter)
114+
self.resource = HypermediaResource.adapters.translate_from("application/vnd.hale+json",
115+
hale_example)
116+
117+
def tearDown(self):
118+
HypermediaResource.reset_adapters()
119+
120+
def test_attributes(self):
121+
attr = self.resource.attributes.get("attribute")
122+
self.assertEqual(attr.value, "value")
123+
124+
def test_links(self):
125+
self_links = self.resource.links.filter_by_rel("self")
126+
self.assertEqual(len(self_links), 1)
127+
self_link = self.resource.links.get("self")
128+
self.assertEqual(self_link.href, "...")
129+
self.assertEqual(len(self.resource.links.all()), 3)
130+
131+
def test_actions(self):
132+
edit_customer = self.resource.actions.get("edit-customer")
133+
self.assertEqual(edit_customer.method, "PUT")
134+
request_type = edit_customer.response_types.filter_by("media_type", "application/json")
135+
self.assertEqual(len(request_type), 1)
136+
self.assertEqual(len(edit_customer.attributes.all()), 3)
137+
send_info = edit_customer.attributes.filter_by("name", "send_info")[0]
138+
self.assertEqual(len(send_info.options.all()), 3)
139+
140+
def test_embedded(self):
141+
logging.basicConfig( stream=sys.stderr )
142+
log = logging.getLogger("TestParse.test_embedded")
143+
customer = self.resource.embedded_resources.get("customer")
144+
log.debug(customer.attributes.all())
145+
self.assertEqual(customer.attributes.get("name").value, "Tom")
146+

tests/wrappers_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ def adapter():
1111
adapter.build.return_value = "built"
1212
return adapter
1313

14-
def request(method='GET', data={}):
14+
def request(method='GET', form={}):
1515
request = Mock()
1616
request.method = method
17-
request.data = data
17+
request.form = form
1818
request.headers = Mock()
1919
request.headers.get = Mock()
2020
request.headers.get.return_value = "application/hal+json"
@@ -96,7 +96,7 @@ def test_response_for(self, mock_method):
9696
self.resource.build_response = Mock()
9797
self.resource.build_response.return_value = response
9898

99-
self.resource.response_for(request())
99+
self.resource.response_for(request('GET', {}))
100100
self.resource.build_response.assert_called_with(resource, "application/hal+json")
101101
mock_method.assert_called_with(response.body, mimetype=response.media_type)
102102

0 commit comments

Comments
 (0)