8000 Open api specs by allmightyspiff · Pull Request #693 · softlayer/githubio_source · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Open api specs #693

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"exclude": {
"files": "static/*|content/reference/*|data/sldn_metadata\\.json|^.secrets.baseline$",
"files": "static/*|content/reference/*|data/sldn_metadata\\.json|^.secrets.baseline$|openapi/*",
"lines": null
},
"generated_at": "2024-08-16T19:42:32Z",
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,22 @@ Any main classes your examples uses should be included. Helpful in searching.

If you ever find yourself wishing there was an example of how to do something in the SoftLayer API, please make a github issue on the [githubio_source](https://github.com/softlayer/githubio_source/issues) repository. We are always on the look out for more content ideas!



# OpenAPI Spec Generation

You should be in the githubio_source directory first before running these commands.

First download the metadata. You can use `--clean` option to remove any other files in the directories as well. Sub directories should be created automatically.
```
$> python bin/generateOpenAPI.py --download
```

[OpenAPI CLI](https://openapi-generator.tech/docs/installation) Can be used to generate HTML or whatever from this document.
```
$> java -jar openapi-generator-cli.jar generate -g html -i static/openapi/sl_openapi.json -o static/openapi/generated/ --skip-operation-example
```

`--skip-operation-example` is needed otherwise the generator will run out of memory trying to build examples.

`./bin/generateOpenAPI-multiFile.py` can be used for a multi-file output if one file is too much to deal with.
310 changes: 310 additions & 0 deletions bin/generateOpenAPI-multiFile.py
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}")
1E79 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()
Loading
0