Skip to content

Commit

Permalink
Merge pull request #693 from softlayer/openAPI
Browse files Browse the repository at this point in the history
Open api specs
  • Loading branch information
allmightyspiff authored Nov 12, 2024
2 parents 6a7b920 + fe854ed commit a02a758
Show file tree
Hide file tree
Showing 30 changed files with 602,085 additions and 4,163 deletions.
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}")
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 comments on commit a02a758

Please sign in to comment.