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())