Skip to content

Commit

Permalink
[enh] Revive the old auto documentation of API with swagger (#1483)
Browse files Browse the repository at this point in the history
* [enh] Revive the old auto documentation of API with swagger

* [fix] RequestBody versus params in auto apidoc

* [fix] Auto api doc no need of Headers on other than post

* [fix] Remove Authentication from swagger API

* Redelete bash completion

* [fix] Delete file

* Delete openapi.json

* Delete doc/swagger

* Add swagger stuff and bashcompletion to gitignore

Co-authored-by: Alexandre Aubin <[email protected]>
  • Loading branch information
zamentur and alexAubin authored Jan 6, 2023
1 parent dd33476 commit e7a0e65
Show file tree
Hide file tree
Showing 3 changed files with 361 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ src/locales

# Test
src/tests/apps

# Tmp/local doc stuff
doc/bash-completion.sh
doc/bash_completion.d
doc/openapi.js
doc/openapi.json
doc/swagger
42 changes: 42 additions & 0 deletions doc/api.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="swagger/swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="swagger/index.css" />
<link rel="icon" type="image/png" href="swagger/favicon-32x32.png" sizes="32x32" />
</head>

<body>
<div id="swagger-ui"></div>
<script src="swagger/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="swagger/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="openapi.js" type="text/javascript" language="javascript"></script>
<script>
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
spec: openapiJSON,
dom_id: '#swagger-ui',
deepLinking: true,
displayOperationId: true,
validatorUrl: null,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout"
});

//</editor-fold>
};



</script>
</body>
</html>

312 changes: 312 additions & 0 deletions doc/generate_api_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,312 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" License
Copyright (C) 2013 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""

"""
Generate JSON specification files API
"""
import os
import sys
import yaml
import json
import requests

def main():
"""
"""
with open('../share/actionsmap.yml') as f:
action_map = yaml.safe_load(f)

try:
with open('/etc/yunohost/current_host', 'r') as f:
domain = f.readline().rstrip()
except IOError:
domain = requests.get('http://ip.yunohost.org').text
with open('../debian/changelog') as f:
top_changelog = f.readline()
api_version = top_changelog[top_changelog.find("(")+1:top_changelog.find(")")]

csrf = {
'name': 'X-Requested-With',
'in': 'header',
'required': True,
'schema': {
'type': 'string',
'default': 'Swagger API'
}

}

resource_list = {
'openapi': '3.0.3',
'info': {
'title': 'YunoHost API',
'description': 'This is the YunoHost API used on all YunoHost instances. This API is essentially used by YunoHost Webadmin.',
'version': api_version,

},
'servers': [
{
'url': "https://{domain}/yunohost/api",
'variables': {
'domain': {
'default': 'demo.yunohost.org',
'description': 'Your yunohost domain'
}
}
}
],
'tags': [
{
'name': 'public',
'description': 'Public route'
}
],
'paths': {
'/login': {
'post': {
'tags': ['public'],
'summary': 'Logs in and returns the authentication cookie',
'parameters': [csrf],
'requestBody': {
'required': True,
'content': {
'multipart/form-data': {
'schema': {
'type': 'object',
'properties': {
'credentials': {
'type': 'string',
'format': 'password'
}
},
'required': [
'credentials'
]
}
}
}
},
'security': [],
'responses': {
'200': {
'description': 'Successfully login',
'headers': {
'Set-Cookie': {
'schema': {
'type': 'string'
}
}
}
}
}
}
},
'/installed': {
'get': {
'tags': ['public'],
'summary': 'Test if the API is working',
'parameters': [],
'security': [],
'responses': {
'200': {
'description': 'Successfully working',
}
}
}
}
},
}


def convert_categories(categories, parent_category=""):
for category, category_params in categories.items():
if parent_category:
category = f"{parent_category} {category}"
if 'subcategory_help' in category_params:
category_params['category_help'] = category_params['subcategory_help']

if 'category_help' not in category_params:
category_params['category_help'] = ''
resource_list['tags'].append({
'name': category,
'description': category_params['category_help']
})


for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params:
action_params['action_help'] = ''
if 'api' not in action_params:
continue
if not isinstance(action_params['api'], list):
action_params['api'] = [action_params['api']]

for i, api in enumerate(action_params['api']):
print(api)
method, path = api.split(' ')
method = method.lower()
key_param = ''
if '{' in path:
key_param = path[path.find("{")+1:path.find("}")]
resource_list['paths'].setdefault(path, {})

notes = ''

operationId = f"{category}_{action}"
if i > 0:
operationId += f"_{i}"
operation = {
'tags': [category],
'operationId': operationId,
'summary': action_params['action_help'],
'description': notes,
'responses': {
'200': {
'description': 'successful operation'
}
}
}
if action_params.get('deprecated'):
operation['deprecated'] = True

operation['parameters'] = []
if method == 'post':
operation['parameters'] = [csrf]

if 'arguments' in action_params:
if method in ['put', 'post', 'patch']:
operation['requestBody'] = {
'required': True,
'content': {
'multipart/form-data': {
'schema': {
'type': 'object',
'properties': {
},
'required': []
}
}
}
}
for arg_name, arg_params in action_params['arguments'].items():
if 'help' not in arg_params:
arg_params['help'] = ''
param_type = 'query'
allow_multiple = False
required = True
allowable_values = None
name = str(arg_name).replace('-', '_')
if name[0] == '_':
required = False
if 'full' in arg_params:
name = arg_params['full'][2:]
else:
name = name[2:]
name = name.replace('-', '_')

if 'choices' in arg_params:
allowable_values = arg_params['choices']
_type = 'string'
if 'type' in arg_params:
types = {
'open': 'file',
'int': 'int'
}
_type = types[arg_params['type']]
if 'action' in arg_params and arg_params['action'] == 'store_true':
_type = 'boolean'

if 'nargs' in arg_params:
if arg_params['nargs'] == '*':
allow_multiple = True
required = False
_type = 'array'
if arg_params['nargs'] == '+':
allow_multiple = True
required = True
_type = 'array'
if arg_params['nargs'] == '?':
allow_multiple = False
required = False
else:
allow_multiple = False


if name == key_param:
param_type = 'path'
required = True
allow_multiple = False

if method in ['put', 'post', 'patch']:
schema = operation['requestBody']['content']['multipart/form-data']['schema']
schema['properties'][name] = {
'type': _type,
'description': arg_params['help']
}
if required:
schema['required'].append(name)
prop_schema = schema['properties'][name]
else:
parameters = {
'name': name,
'in': param_type,
'description': arg_params['help'],
'required': required,
'schema': {
'type': _type,
},
'explode': allow_multiple
}
prop_schema = parameters['schema']
operation['parameters'].append(parameters)

if allowable_values is not None:
prop_schema['enum'] = allowable_values
if 'default' in arg_params:
prop_schema['default'] = arg_params['default']
if arg_params.get('metavar') == 'PASSWORD':
prop_schema['format'] = 'password'
if arg_params.get('metavar') == 'MAIL':
prop_schema['format'] = 'mail'
# Those lines seems to slow swagger ui too much
#if 'pattern' in arg_params.get('extra', {}):
# prop_schema['pattern'] = arg_params['extra']['pattern'][0]



resource_list['paths'][path][method.lower()] = operation

# Includes subcategories
if 'subcategories' in category_params:
convert_categories(category_params['subcategories'], category)

del action_map['_global']
convert_categories(action_map)

openapi_json = json.dumps(resource_list)
# Save the OpenAPI json
with open(os.getcwd() + '/openapi.json', 'w') as f:
f.write(openapi_json)

openapi_js = f"var openapiJSON = {openapi_json}"
with open(os.getcwd() + '/openapi.js', 'w') as f:
f.write(openapi_js)



if __name__ == '__main__':
sys.exit(main())

0 comments on commit e7a0e65

Please sign in to comment.