-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #693 from softlayer/openAPI
Open api specs
- Loading branch information
Showing
30 changed files
with
602,085 additions
and
4,163 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,310 @@ | ||
#!python | ||
|
||
import click | ||
# from prettytable import PrettyTable | ||
import json | ||
import requests | ||
import os | ||
import shutil | ||
from string import Template | ||
import re | ||
|
||
|
||
METAURL = 'https://api.softlayer.com/metadata/v3.1' | ||
|
||
|
||
class OpenAPIGen(): | ||
|
||
def __init__(self, outdir: str) -> None: | ||
self.outdir = outdir | ||
if not os.path.isdir(self.outdir): | ||
print(f"Creating directory {self.outdir}") | ||
os.mkdir(self.outdir) | ||
os.mkdir(f"{self.outdir}/paths") | ||
self.metajson = None | ||
self.metapath = f'{self.outdir}/sldn_metadata.json' | ||
self.openapi = { | ||
"openapi": '3.0.3', | ||
"info": { | ||
"title": "SoftLayer API - OpenAPI 3.0", | ||
"description": "SoftLayer API Definitions in a swagger format", | ||
"termsOfService": "https://cloud.ibm.com/docs/overview?topic=overview-terms", | ||
"version": "1.0.0" | ||
}, | ||
"externalDocs": { | ||
"description": "SLDN", | ||
"url": "https://sldn.softlayer.com" | ||
}, | ||
"servers": [ | ||
{"url": "https://api.softlayer.com/rest/v3.1"}, | ||
{"url": "https://api.service.softlayer.com/rest/v3.1"} | ||
], | ||
"paths": {}, | ||
"components": { | ||
"schemas": {}, | ||
"requestBodies": {}, | ||
"securitySchemes": { # https://swagger.io/specification/#security-scheme-object | ||
"api_key": { | ||
"type": "http", | ||
"scheme": "basic" | ||
} | ||
} | ||
}, | ||
"security": [{"api_key": []}] | ||
} | ||
|
||
def getMetadata(self, url: str) -> dict: | ||
"""Downloads metadata from SLDN""" | ||
response = requests.get(url) | ||
if response.status_code != 200: | ||
raise Exception(f"{url} returned \n{response.text}\nHTTP CODE: {response.status_code}") | ||
|
||
self.metajson = response.json() | ||
return self.metajson | ||
|
||
def saveMetadata(self) -> None: | ||
"""Saves metadata to a file""" | ||
print(f"Writing SLDN Metadata to {self.metapath}") | ||
with open(self.metapath, 'w') as f: | ||
json.dump(self.metajson, f, indent=4) | ||
|
||
def getLocalMetadata(self) -> dict: | ||
"""Loads metadata from local data folder""" | ||
with open(self.metapath, "r", encoding="utf-8") as f: | ||
metadata = f.read() | ||
self.metajson = json.loads(metadata) | ||
return self.metajson | ||
|
||
def addInORMMethods(self): | ||
for serviceName, service in self.metajson.items(): | ||
# noservice means datatype only. | ||
if service.get('noservice', False) == False: | ||
for propName, prop in service.get('properties', {}).items(): | ||
if prop.get('form', '') == 'relational': | ||
# capitlize() sadly lowercases the other letters in the string | ||
ormName = f"get{propName[0].upper()}{propName[1:]}" | ||
ormMethod = { | ||
'doc': prop.get('doc', ''), | ||
'docOverview': "", | ||
'name': ormName, | ||
'type': prop.get('type'), | ||
'typeArray': prop.get('typeArray', None), | ||
'ormMethod': True, | ||
'maskable': True, | ||
'filterable': True, | ||
'deprecated': prop.get('deprecated', False) | ||
} | ||
if ormMethod['typeArray']: | ||
ormMethod['limitable'] = True | ||
self.metajson[serviceName]['methods'][ormName] = ormMethod | ||
return self.metajson | ||
|
||
def addInChildMethods(self): | ||
for serviceName, service in self.metajson.items(): | ||
self.metajson[serviceName]['methods'] = self.getBaseMethods(serviceName, 'methods') | ||
self.metajson[serviceName]['properties'] = self.getBaseMethods(serviceName, 'properties') | ||
|
||
|
||
def getBaseMethods(self, serviceName, objectType): | ||
"""Responsible for pulling in properties or methods from the base class of the service requested""" | ||
service = self.metajson[serviceName] | ||
methods = service.get(objectType, {}) | ||
if service.get('base', "SoftLayer_Entity") != "SoftLayer_Entity": | ||
|
||
baseMethods = self.getBaseMethods(service.get('base'), objectType) | ||
for bName, bMethod in baseMethods.items(): | ||
if not methods.get(bName, False): | ||
methods[bName] = bMethod | ||
return methods | ||
|
||
def testDirectories(self) -> None: | ||
"""Makes sure all the directories exist that are supposed to""" | ||
for serviceName, service in self.metajson.items(): | ||
if service.get('noservice', False) == False: | ||
this_path = f"{self.outdir}/paths/{serviceName}" | ||
if not os.path.isdir(this_path): | ||
print(f"Creating directory: {this_path}") | ||
os.mkdir(this_path) | ||
|
||
if not os.path.isdir(f"{self.outdir}/components"): | ||
os.mkdir(f"{self.outdir}/components") | ||
if not os.path.isdir(f"{self.outdir}/generated"): | ||
os.mkdir(f"{self.outdir}/generated") | ||
|
||
def generate(self) -> None: | ||
print("OK") | ||
self.testDirectories() | ||
for serviceName, service in self.metajson.items(): | ||
print(f"Working on {serviceName}") | ||
# Writing the check this way to be more clear to myself when reading it | ||
# This service has methods | ||
if service.get('noservice', False) == False: | ||
# if serviceName in ["SoftLayer_Account", "SoftLayer_User_Customer"]: | ||
for methodName, method in service.get('methods', {}).items(): | ||
path_name, new_path = self.genPath(serviceName, methodName, method) | ||
with open(f"{self.outdir}/paths/{serviceName}/{methodName}.json", "w") as newfile: | ||
json.dump(new_path, newfile, indent=4) | ||
self.openapi['paths'][path_name] = {"$ref": f"./paths/{serviceName}/{methodName}.json"} | ||
|
||
component = self.genComponent(serviceName, service) | ||
with open(f"{self.outdir}/components/{serviceName}.json", "w") as newfile: | ||
json.dump(component, newfile, indent=4) | ||
# self.openapi['components']['schemas'][serviceName] = {"$ref": f"./components/{serviceName}.json"} | ||
|
||
|
||
# WRITE OUTPUT HERE | ||
with open(f"{self.outdir}/sl_openapi.json", "w") as outfile: | ||
json.dump(self.openapi, outfile, indent=4) | ||
|
||
def getPathName(self, serviceName: str, methodName: str, static: bool) -> str: | ||
init_param = '' | ||
if not static and not serviceName == "SoftLayer_Account": | ||
init_param = f"{{{serviceName}ID}}/" | ||
return f"/{serviceName}/{init_param}{methodName}" | ||
|
||
def genPath(self, serviceName: str, methodName: str, method: dict) -> (str, dict): | ||
http_method = "get" | ||
if method.get('parameters', False): | ||
http_method = "post" | ||
path_name = self.getPathName(serviceName, methodName, method.get('static', False)) | ||
new_path = { | ||
http_method: { | ||
"description": method.get('doc'), | ||
"summary": method.get('docOverview', ''), | ||
"externalDocs": { | ||
"description": "SLDN Documentation", | ||
"url": f"https://sldn.softlayer.com/reference/services/{serviceName}/{methodName}/" | ||
}, | ||
"operationId": f"{serviceName}::{methodName}", | ||
"responses": { | ||
"200": { | ||
"description": "Successful operation", | ||
"content": { | ||
"application/json": { | ||
"schema": self.getSchema(method, True) | ||
} | ||
} | ||
} | ||
}, | ||
"security": [ | ||
{"api_key": []} | ||
] | ||
} | ||
} | ||
|
||
if not method.get('static', False) and not serviceName == "SoftLayer_Account": | ||
this_param = { | ||
"name": f"{serviceName}ID", | ||
"in": "path", | ||
"description": f"ID for a {serviceName} object", | ||
"required": True, | ||
"schema": {"type": "integer"} | ||
} | ||
new_path[http_method]['parameters'] = [this_param] | ||
|
||
request_body = { | ||
"description": "POST parameters", | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"type": "object", | ||
"properties": { | ||
"parameters": {} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
request_parameters = { | ||
"parameters": { | ||
"type": "object", | ||
"properties": {} | ||
} | ||
} | ||
for parameter in method.get('parameters', []): | ||
request_parameters['parameters']['properties'][parameter.get('name')] = self.getSchema(parameter, True) | ||
|
||
if len(method.get('parameters', [])) > 0: | ||
request_body['content']['application/json']['schema']['properties'] = request_parameters | ||
new_path[http_method]['requestBody'] = request_body | ||
|
||
return (path_name, new_path) | ||
|
||
def getSchema(self, method: dict, fromMethod: bool = False) -> dict: | ||
"""Gets a formatted schema object from a method""" | ||
is_array = method.get('typeArray', False) | ||
sl_type = method.get('type', "null") | ||
ref = {} | ||
|
||
if sl_type in ["int", "decimal", "unsignedLong", "float", "unsignedInt"]: | ||
ref = {"type": "number"} | ||
elif sl_type in ["dateTime", "enum", "base64Binary", "string", "json"]: | ||
ref = {"type": "string"} | ||
elif sl_type == "void": | ||
ref = {"type": "null"} | ||
elif sl_type == "boolean": | ||
ref = {"type": "boolean"} | ||
# This is last because SOME properties are marked relational when they are not really. | ||
elif sl_type.startswith("SoftLayer_") or method.get('form') == 'relational': | ||
# ref = {"$ref": f"#/components/schemas/{sl_type}"} | ||
if fromMethod: | ||
ref = {"$ref": f"../../components/{sl_type}.json"} | ||
else: | ||
ref = {"$ref": f"./{sl_type}.json"} | ||
else: | ||
ref = {"type": sl_type} | ||
|
||
if is_array: | ||
schema = {"type": "array", "items": ref} | ||
else: | ||
schema = ref | ||
return schema | ||
|
||
def genComponent(self, serviceName: str, service: dict) -> dict: | ||
"""Generates return component for a datatype""" | ||
schema = { | ||
"type": "object", | ||
"properties": {} | ||
} | ||
for propName, prop in service.get('properties').items(): | ||
schema['properties'][propName] = self.getSchema(prop) | ||
|
||
return schema | ||
|
||
|
||
@click.command() | ||
@click.option('--download', default=False, is_flag=True) | ||
@click.option('--clean', default=False, is_flag=True, help="Removes the services and datatypes directories so they can be built from scratch") | ||
def main(download: bool, clean: bool): | ||
cwd = os.getcwd() | ||
outdir = f'{cwd}/openapi' | ||
if not cwd.endswith('githubio_source'): | ||
raise Exception(f"Working Directory should be githubio_source, is currently {cwd}") | ||
|
||
if clean: | ||
print(f"Removing {outdir}") | ||
try: | ||
shutil.rmtree(f'{outdir}') | ||
except FileNotFoundError: | ||
print("Directory doesnt exist...") | ||
|
||
generator = OpenAPIGen(outdir) | ||
if download: | ||
try: | ||
metajson = generator.getMetadata(url = METAURL) | ||
generator.addInChildMethods() | ||
generator.addInORMMethods() | ||
generator.saveMetadata() | ||
except Exception as e: | ||
print("========== ERROR ==========") | ||
print(f"{e}") | ||
print("========== ERROR ==========") | ||
else: | ||
metajson = generator.getLocalMetadata() | ||
|
||
print("Generating OpenAPI....") | ||
generator.generate() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
Oops, something went wrong.