Skip to content

Commit f06f15a

Browse files
committed
Mutiple changes to REST code.
Requesting an invalid attribut via rest/data/class/id/attrib used to return a 405, it now returns a 400 and a better error message. /rest/ response scans the registered endpoints rather than using a hard coded description. So new endpoints added in interfaces.py are listed. Fix a number of Allow headers that were listing invalid methods. Also when invalid method is used, report valid methods in response. Extract methods from Route list. Fix Access-Control-Allow-Methods. Add X-Requested-With to Access-Control-Allow-Headers. Add decorator openapi_doc to add openapi annotations for the rest endpoints. Added a couple of examples. Returning this info to a client is still a work in progress.
1 parent 2dc2076 commit f06f15a

File tree

4 files changed

+251
-50
lines changed

4 files changed

+251
-50
lines changed

CHANGES.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ Fixed:
3838
- issue2551167 - pip install in containerized environments puts
3939
template and locale files under site-packages where roundup can't find
4040
them. Change code to find them under site-packages.
41+
- REST replace hard coded list of child endpoints for /rest/ with list
42+
pulled from registered endpoints. So newly added endpoints are
43+
shown. (John Rouillard)
4144

4245
Features:
4346

@@ -61,6 +64,15 @@ Features:
6164
"-V HTTP/1.0" can turn it off. (John Rouillard)
6265
- issue2551163 - add scripts/Dockerfile to provide basic support for
6366
containerization. See installation.txt for details. (John Rouillard)
67+
- REST add openapi_doc decorator to add openapi_doc to
68+
endpoints. Decorate a couple of examples. (John Rouillard)
69+
- REST when incorrect method is used, report allowed methods in error
70+
message as well as in an Allow header. (John Rouillard)
71+
- REST change response to invalid attribute specified in path. Return
72+
400 code not 405 code for this case and improve error. (John
73+
Rouillard)
74+
- REST correct values for some Access-Control-Allow-Methods and
75+
Access-Control-Allow-Headers headers. (John Rouillard)
6476

6577
2021-07-13 2.1.0
6678

roundup/rest.py

Lines changed: 175 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,51 @@ def format_object(self, *args, **kwargs):
123123
'data': data
124124
}
125125
return result
126+
127+
format_object.wrapped_func = func
126128
return format_object
127129

130+
def openapi_doc(d):
131+
"""Annotate rest routes with openapi data. Takes a dict
132+
for the openapi spec. It can be used standalone
133+
as the openapi spec paths.<path>.<method> =
134+
135+
{
136+
"summary": "this path gets a value",
137+
"description": "a longer description",
138+
"responses": {
139+
"200": {
140+
"description": "normal response",
141+
"content": {
142+
"application/json": {},
143+
"application/xml": {}
144+
}
145+
},
146+
"406": {
147+
"description": "Unable to provide requested content type",
148+
"content": {
149+
"application/json": {}
150+
}
151+
}
152+
},
153+
"parameters": [
154+
{
155+
"$ref": "#components/parameters/generic_.stats"
156+
},
157+
{
158+
"$ref": "#components/parameters/generic_.apiver"
159+
},
160+
{
161+
"$ref": "#components/parameters/generic_.verbose"
162+
}
163+
]
164+
}
165+
"""
166+
167+
def wrapper(f):
168+
f.openapi_doc = d
169+
return f
170+
return wrapper
128171

129172
def calculate_etag(node, key, classname="Missing", id="0",
130173
repr_format="json"):
@@ -347,7 +390,13 @@ def execute(cls, instance, path, method, input):
347390
try:
348391
func_obj = funcs[method]
349392
except KeyError:
350-
raise Reject('Method %s not allowed' % method)
393+
valid_methods = ', '.join(sorted(funcs.keys()))
394+
raise Reject(_('Method %(m)s not allowed. '
395+
'Allowed: %(a)s')% {
396+
'm': method,
397+
'a': valid_methods
398+
},
399+
valid_methods)
351400

352401
# retrieve the vars list and the function caller
353402
list_vars = func_obj['vars']
@@ -891,6 +940,7 @@ def get_collection(self, class_name, input):
891940

892941
result['@total_size'] = result_len
893942
self.client.setHeader("X-Count-Total", str(result_len))
943+
self.client.setHeader("Allow", "OPTIONS, GET, POST")
894944
return 200, result
895945

896946
@Routing.route("/data/<:class_name>/<:item_id>", 'GET')
@@ -1028,7 +1078,10 @@ def get_attribute(self, class_name, item_id, attr_name, input):
10281078
node = class_obj.getnode(item_id)
10291079
etag = calculate_etag(node, self.db.config.WEB_SECRET_KEY,
10301080
class_name, item_id, repr_format="json")
1031-
data = node.__getattr__(attr_name)
1081+
try:
1082+
data = node.__getattr__(attr_name)
1083+
except AttributeError as e:
1084+
raise UsageError(_("Invalid attribute %s"%attr_name))
10321085
result = {
10331086
'id': item_id,
10341087
'type': str(type(data)),
@@ -1189,6 +1242,15 @@ def post_collection_inner(self, class_name, input):
11891242
link = '%s/%s/%s' % (self.data_path, class_name, item_id)
11901243
self.client.setHeader("Location", link)
11911244

1245+
self.client.setHeader(
1246+
"Allow",
1247+
None
1248+
)
1249+
self.client.setHeader(
1250+
"Access-Control-Allow-Methods",
1251+
None
1252+
)
1253+
11921254
# set the response body
11931255
result = {
11941256
'id': item_id,
@@ -1643,6 +1705,11 @@ def options_collection(self, class_name, input):
16431705
"Allow",
16441706
"OPTIONS, GET, POST"
16451707
)
1708+
1709+
self.client.setHeader(
1710+
"Access-Control-Allow-Methods",
1711+
"OPTIONS, GET, POST"
1712+
)
16461713
return 204, ""
16471714

16481715
@Routing.route("/data/<:class_name>/<:item_id>", 'OPTIONS')
@@ -1698,19 +1765,114 @@ def option_attribute(self, class_name, item_id, attr_name, input):
16981765
attr_name, class_name))
16991766
return 204, ""
17001767

1768+
@openapi_doc({"summary": "Describe Roundup rest endpoint.",
1769+
"description": ("Report all supported api versions "
1770+
"and default api version. "
1771+
"Also report next level of link "
1772+
"endpoints below /rest endpoint"),
1773+
"responses": {
1774+
"200": {
1775+
"description": "Successful response.",
1776+
"content": {
1777+
"application/json": {
1778+
"examples": {
1779+
"success": {
1780+
"summary": "Normal json data.",
1781+
"value": """{
1782+
"data": {
1783+
"default_version": 1,
1784+
"supported_versions": [
1785+
1
1786+
],
1787+
"links": [
1788+
{
1789+
"uri": "https://tracker.example.com/demo/rest",
1790+
"rel": "self"
1791+
},
1792+
{
1793+
"uri": "https://tracker.example.com/demo/rest/data",
1794+
"rel": "data"
1795+
},
1796+
{
1797+
"uri": "https://tracker.example.com/demo/rest/summary",
1798+
"rel": "summary"
1799+
}
1800+
]
1801+
}
1802+
}"""
1803+
}
1804+
}
1805+
},
1806+
"application/xml": {
1807+
"examples": {
1808+
"success": {
1809+
"summary": "Normal xml data",
1810+
"value": """<dataf type="dict">
1811+
<default_version type="int">1</default_version>
1812+
<supported_versions type="list">
1813+
<item type="int">1</item>
1814+
</supported_versions>
1815+
<links type="list">
1816+
<item type="dict">
1817+
<uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest</uri>
1818+
<rel type="str">self</rel>
1819+
</item>
1820+
<item type="dict">
1821+
<uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/data</uri>
1822+
<rel type="str">data</rel>
1823+
</item>
1824+
<item type="dict">
1825+
<uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary</uri>
1826+
<rel type="str">summary</rel>
1827+
</item>
1828+
<item type="dict">
1829+
<uri type="str">https://rouilj.dynamic-dns.net/sysadmin/rest/summary2</uri>
1830+
<rel type="str">summary2</rel>
1831+
</item>
1832+
</links>
1833+
</dataf>"""
1834+
}
1835+
}
1836+
}
1837+
}
1838+
}
1839+
}
1840+
}
1841+
)
17011842
@Routing.route("/")
17021843
@_data_decorator
17031844
def describe(self, input):
1704-
"""Describe the rest endpoint"""
1845+
"""Describe the rest endpoint. Return direct children in
1846+
links list.
1847+
"""
1848+
1849+
# paths looks like ['^rest/$', '^rest/summary$',
1850+
# '^rest/data/<:class>$', ...]
1851+
paths = Routing._Routing__route_map.keys()
1852+
1853+
links = []
1854+
# p[1:-1] removes ^ and $ from regexp
1855+
# if p has only 1 /, it's a child of rest/ root.
1856+
child_paths = sorted([ p[1:-1] for p in paths if
1857+
p.count('/') == 1 ])
1858+
for p in child_paths:
1859+
# p.split('/')[1] is the residual path after
1860+
# removing rest/. child_paths look like:
1861+
# ['rest/', 'rest/summary'] etc.
1862+
rel = p.split('/')[1]
1863+
if rel:
1864+
rel_path = "/" + rel
1865+
else:
1866+
rel_path = rel
1867+
rel = "self"
1868+
links.append( {"uri": self.base_path + rel_path,
1869+
"rel": rel
1870+
})
1871+
17051872
result = {
17061873
"default_version": self.__default_api_version,
17071874
"supported_versions": self.__supported_api_versions,
1708-
"links": [{"uri": self.base_path + "/summary",
1709-
"rel": "summary"},
1710-
{"uri": self.base_path,
1711-
"rel": "self"},
1712-
{"uri": self.base_path + "/data",
1713-
"rel": "data"}]
1875+
"links": links
17141876
}
17151877

17161878
return 200, result
@@ -1983,15 +2145,15 @@ def dispatch(self, method, uri, input):
19832145
self.client.setHeader("Access-Control-Allow-Origin", "*")
19842146
self.client.setHeader(
19852147
"Access-Control-Allow-Headers",
1986-
"Content-Type, Authorization, X-HTTP-Method-Override"
2148+
"Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override"
19872149
)
19882150
self.client.setHeader(
19892151
"Allow",
19902152
"OPTIONS, GET, POST, PUT, DELETE, PATCH"
19912153
)
19922154
self.client.setHeader(
19932155
"Access-Control-Allow-Methods",
1994-
"HEAD, OPTIONS, GET, PUT, DELETE, PATCH"
2156+
"HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH"
19952157
)
19962158
# Is there an input.value with format json data?
19972159
# If so turn it into an object that emulates enough
@@ -2076,7 +2238,8 @@ def dispatch(self, method, uri, input):
20762238
except NotFound as msg:
20772239
output = self.error_obj(404, msg)
20782240
except Reject as msg:
2079-
output = self.error_obj(405, msg)
2241+
output = self.error_obj(405, msg.args[0])
2242+
self.client.setHeader("Allow", msg.args[1])
20802243

20812244
# Format the content type
20822245
if data_type.lower() == "json":

test/rest_common.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2062,17 +2062,17 @@ def testDispatch(self):
20622062
],
20632063
"default_version": 1,
20642064
"links": [
2065-
{
2066-
"rel": "summary",
2067-
"uri": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/summary"
2068-
},
20692065
{
20702066
"rel": "self",
20712067
"uri": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest"
20722068
},
20732069
{
20742070
"rel": "data",
20752071
"uri": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/data"
2072+
},
2073+
{
2074+
"rel": "summary",
2075+
"uri": "http://tracker.example/cgi-bin/roundup.cgi/bugs/rest/summary"
20762076
}
20772077
]
20782078
}

0 commit comments

Comments
 (0)