@@ -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
129172def 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" :
0 commit comments