diff --git a/.gitignore b/.gitignore
index eae46b4c55..91b5b56e4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/doc/api.html b/doc/api.html
new file mode 100644
index 0000000000..502d1247f8
--- /dev/null
+++ b/doc/api.html
@@ -0,0 +1,42 @@
+
+
+
+
+
+ Swagger UI
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/generate_api_doc.py b/doc/generate_api_doc.py
new file mode 100644
index 0000000000..939dd90bd5
--- /dev/null
+++ b/doc/generate_api_doc.py
@@ -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())