Skip to content

Commit a02a758

Browse files
Merge pull request #693 from softlayer/openAPI
Open api specs
2 parents 6a7b920 + fe854ed commit a02a758

30 files changed

+602085
-4163
lines changed

.secrets.baseline

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"exclude": {
3-
"files": "static/*|content/reference/*|data/sldn_metadata\\.json|^.secrets.baseline$",
3+
"files": "static/*|content/reference/*|data/sldn_metadata\\.json|^.secrets.baseline$|openapi/*",
44
"lines": null
55
},
66
"generated_at": "2024-08-16T19:42:32Z",

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,22 @@ Any main classes your examples uses should be included. Helpful in searching.
120120

121121
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!
122122

123+
124+
125+
# OpenAPI Spec Generation
126+
127+
You should be in the githubio_source directory first before running these commands.
128+
129+
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.
130+
```
131+
$> python bin/generateOpenAPI.py --download
132+
```
133+
134+
[OpenAPI CLI](https://openapi-generator.tech/docs/installation) Can be used to generate HTML or whatever from this document.
135+
```
136+
$> java -jar openapi-generator-cli.jar generate -g html -i static/openapi/sl_openapi.json -o static/openapi/generated/ --skip-operation-example
137+
```
138+
139+
`--skip-operation-example` is needed otherwise the generator will run out of memory trying to build examples.
140+
141+
`./bin/generateOpenAPI-multiFile.py` can be used for a multi-file output if one file is too much to deal with.

bin/generateOpenAPI-multiFile.py

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
#!python
2+
3+
import click
4+
# from prettytable import PrettyTable
5+
import json
6+
import requests
7+
import os
8+
import shutil
9+
from string import Template
10+
import re
11+
12+
13+
METAURL = 'https://api.softlayer.com/metadata/v3.1'
14+
15+
16+
class OpenAPIGen():
17+
18+
def __init__(self, outdir: str) -> None:
19+
self.outdir = outdir
20+
if not os.path.isdir(self.outdir):
21+
print(f"Creating directory {self.outdir}")
22+
os.mkdir(self.outdir)
23+
os.mkdir(f"{self.outdir}/paths")
24+
self.metajson = None
25+
self.metapath = f'{self.outdir}/sldn_metadata.json'
26+
self.openapi = {
27+
"openapi": '3.0.3',
28+
"info": {
29+
"title": "SoftLayer API - OpenAPI 3.0",
30+
"description": "SoftLayer API Definitions in a swagger format",
31+
"termsOfService": "https://cloud.ibm.com/docs/overview?topic=overview-terms",
32+
"version": "1.0.0"
33+
},
34+
"externalDocs": {
35+
"description": "SLDN",
36+
"url": "https://sldn.softlayer.com"
37+
},
38+
"servers": [
39+
{"url": "https://api.softlayer.com/rest/v3.1"},
40+
{"url": "https://api.service.softlayer.com/rest/v3.1"}
41+
],
42+
"paths": {},
43+
"components": {
44+
"schemas": {},
45+
"requestBodies": {},
46+
"securitySchemes": { # https://swagger.io/specification/#security-scheme-object
47+
"api_key": {
48+
"type": "http",
49+
"scheme": "basic"
50+
}
51+
}
52+
},
53+
"security": [{"api_key": []}]
54+
}
55+
56+
def getMetadata(self, url: str) -> dict:
57+
"""Downloads metadata from SLDN"""
58+
response = requests.get(url)
59+
if response.status_code != 200:
60+
raise Exception(f"{url} returned \n{response.text}\nHTTP CODE: {response.status_code}")
61+
62+
self.metajson = response.json()
63+
return self.metajson
64+
65+
def saveMetadata(self) -> None:
66+
"""Saves metadata to a file"""
67+
print(f"Writing SLDN Metadata to {self.metapath}")
68+
with open(self.metapath, 'w') as f:
69+
json.dump(self.metajson, f, indent=4)
70+
71+
def getLocalMetadata(self) -> dict:
72+
"""Loads metadata from local data folder"""
73+
with open(self.metapath, "r", encoding="utf-8") as f:
74+
metadata = f.read()
75+
self.metajson = json.loads(metadata)
76+
return self.metajson
77+
78+
def addInORMMethods(self):
79+
for serviceName, service in self.metajson.items():
80+
# noservice means datatype only.
81+
if service.get('noservice', False) == False:
82+
for propName, prop in service.get('properties', {}).items():
83+
if prop.get('form', '') == 'relational':
84+
# capitlize() sadly lowercases the other letters in the string
85+
ormName = f"get{propName[0].upper()}{propName[1:]}"
86+
ormMethod = {
87+
'doc': prop.get('doc', ''),
88+
'docOverview': "",
89+
'name': ormName,
90+
'type': prop.get('type'),
91+
'typeArray': prop.get('typeArray', None),
92+
'ormMethod': True,
93+
'maskable': True,
94+
'filterable': True,
95+
'deprecated': prop.get('deprecated', False)
96+
}
97+
if ormMethod['typeArray']:
98+
ormMethod['limitable'] = True
99+
self.metajson[serviceName]['methods'][ormName] = ormMethod
100+
return self.metajson
101+
102+
def addInChildMethods(self):
103+
for serviceName, service in self.metajson.items():
104+
self.metajson[serviceName]['methods'] = self.getBaseMethods(serviceName, 'methods')
105+
self.metajson[serviceName]['properties'] = self.getBaseMethods(serviceName, 'properties')
106+
107+
108+
def getBaseMethods(self, serviceName, objectType):
109+
"""Responsible for pulling in properties or methods from the base class of the service requested"""
110+
service = self.metajson[serviceName]
111+
methods = service.get(objectType, {})
112+
if service.get('base', "SoftLayer_Entity") != "SoftLayer_Entity":
113+
114+
baseMethods = self.getBaseMethods(service.get('base'), objectType)
115+
for bName, bMethod in baseMethods.items():
116+
if not methods.get(bName, False):
117+
methods[bName] = bMethod
118+
return methods
119+
120+
def testDirectories(self) -> None:
121+
"""Makes sure all the directories exist that are supposed to"""
122+
for serviceName, service in self.metajson.items():
123+
if service.get('noservice', False) == False:
124+
this_path = f"{self.outdir}/paths/{serviceName}"
125+
if not os.path.isdir(this_path):
126+
print(f"Creating directory: {this_path}")
127+
os.mkdir(this_path)
128+
129+
if not os.path.isdir(f"{self.outdir}/components"):
130+
os.mkdir(f"{self.outdir}/components")
131+
if not os.path.isdir(f"{self.outdir}/generated"):
132+
os.mkdir(f"{self.outdir}/generated")
133+
134+
def generate(self) -> None:
135+
print("OK")
136+
self.testDirectories()
137+
for serviceName, service in self.metajson.items():
138+
print(f"Working on {serviceName}")
139+
# Writing the check this way to be more clear to myself when reading it
140+
# This service has methods
141+
if service.get('noservice', False) == False:
142+
# if serviceName in ["SoftLayer_Account", "SoftLayer_User_Customer"]:
143+
for methodName, method in service.get('methods', {}).items():
144+
path_name, new_path = self.genPath(serviceName, methodName, method)
145+
with open(f"{self.outdir}/paths/{serviceName}/{methodName}.json", "w") as newfile:
146+
json.dump(new_path, newfile, indent=4)
147+
self.openapi['paths'][path_name] = {"$ref": f"./paths/{serviceName}/{methodName}.json"}
148+
149+
component = self.genComponent(serviceName, service)
150+
with open(f"{self.outdir}/components/{serviceName}.json", "w") as newfile:
151+
json.dump(component, newfile, indent=4)
152+
# self.openapi['components']['schemas'][serviceName] = {"$ref": f"./components/{serviceName}.json"}
153+
154+
155+
# WRITE OUTPUT HERE
156+
with open(f"{self.outdir}/sl_openapi.json", "w") as outfile:
157+
json.dump(self.openapi, outfile, indent=4)
158+
159+
def getPathName(self, serviceName: str, methodName: str, static: bool) -> str:
160+
init_param = ''
161+
if not static and not serviceName == "SoftLayer_Account":
162+
init_param = f"{{{serviceName}ID}}/"
163+
return f"/{serviceName}/{init_param}{methodName}"
164+
165+
def genPath(self, serviceName: str, methodName: str, method: dict) -> (str, dict):
166+
http_method = "get"
167+
if method.get('parameters', False):
168+
http_method = "post"
169+
path_name = self.getPathName(serviceName, methodName, method.get('static', False))
170+
new_path = {
171+
http_method: {
172+
"description": method.get('doc'),
173+
"summary": method.get('docOverview', ''),
174+
"externalDocs": {
175+
"description": "SLDN Documentation",
176+
"url": f"https://sldn.softlayer.com/reference/services/{serviceName}/{methodName}/"
177+
},
178+
"operationId": f"{serviceName}::{methodName}",
179+
"responses": {
180+
"200": {
181+
"description": "Successful operation",
182+
"content": {
183+
"application/json": {
184+
"schema": self.getSchema(method, True)
185+
}
186+
}
187+
}
188+
},
189+
"security": [
190+
{"api_key": []}
191+
]
192+
}
193+
}
194+
195+
if not method.get('static', False) and not serviceName == "SoftLayer_Account":
196+
this_param = {
197+
"name": f"{serviceName}ID",
198+
"in": "path",
199+
"description": f"ID for a {serviceName} object",
200+
"required": True,
201+
"schema": {"type": "integer"}
202+
}
203+
new_path[http_method]['parameters'] = [this_param]
204+
205+
request_body = {
206+
"description": "POST parameters",
207+
"content": {
208+
"application/json": {
209+
"schema": {
210+
"type": "object",
211+
"properties": {
212+
"parameters": {}
213+
}
214+
}
215+
}
216+
}
217+
}
218+
request_parameters = {
219+
"parameters": {
220+
"type": "object",
221+
"properties": {}
222+
}
223+
}
224+
for parameter in method.get('parameters', []):
225+
request_parameters['parameters']['properties'][parameter.get('name')] = self.getSchema(parameter, True)
226+
227+
if len(method.get('parameters', [])) > 0:
228+
request_body['content']['application/json']['schema']['properties'] = request_parameters
229+
new_path[http_method]['requestBody'] = request_body
230+
231+
return (path_name, new_path)
232+
233+
def getSchema(self, method: dict, fromMethod: bool = False) -> dict:
234+
"""Gets a formatted schema object from a method"""
235+
is_array = method.get('typeArray', False)
236+
sl_type = method.get('type', "null")
237+
ref = {}
238+
239+
if sl_type in ["int", "decimal", "unsignedLong", "float", "unsignedInt"]:
240+
ref = {"type": "number"}
241+
elif sl_type in ["dateTime", "enum", "base64Binary", "string", "json"]:
242+
ref = {"type": "string"}
243+
elif sl_type == "void":
244+
ref = {"type": "null"}
245+
elif sl_type == "boolean":
246+
ref = {"type": "boolean"}
247+
# This is last because SOME properties are marked relational when they are not really.
248+
elif sl_type.startswith("SoftLayer_") or method.get('form') == 'relational':
249+
# ref = {"$ref": f"#/components/schemas/{sl_type}"}
250+
if fromMethod:
251+
ref = {"$ref": f"../../components/{sl_type}.json"}
252+
else:
253+
ref = {"$ref": f"./{sl_type}.json"}
254+
else:
255+
ref = {"type": sl_type}
256+
257+
if is_array:
258+
schema = {"type": "array", "items": ref}
259+
else:
260+
schema = ref
261+
return schema
262+
263+
def genComponent(self, serviceName: str, service: dict) -> dict:
264+
"""Generates return component for a datatype"""
265+
schema = {
266+
"type": "object",
267+
"properties": {}
268+
}
269+
for propName, prop in service.get('properties').items():
270+
schema['properties'][propName] = self.getSchema(prop)
271+
272+
return schema
273+
274+
275+
@click.command()
276+
@click.option('--download', default=False, is_flag=True)
277+
@click.option('--clean', default=False, is_flag=True, help="Removes the services and datatypes directories so they can be built from scratch")
278+
def main(download: bool, clean: bool):
279+
cwd = os.getcwd()
280+
outdir = f'{cwd}/openapi'
281+
if not cwd.endswith('githubio_source'):
282+
raise Exception(f"Working Directory should be githubio_source, is currently {cwd}")
283+
284+
if clean:
285+
print(f"Removing {outdir}")
286+
try:
287+
shutil.rmtree(f'{outdir}')
288+
except FileNotFoundError:
289+
print("Directory doesnt exist...")
290+
291+
generator = OpenAPIGen(outdir)
292+
if download:
293+
try:
294+
metajson = generator.getMetadata(url = METAURL)
295+
generator.addInChildMethods()
296+
generator.addInORMMethods()
297+
generator.saveMetadata()
298+
except Exception as e:
299+
print("========== ERROR ==========")
300+
print(f"{e}")
301+
print("========== ERROR ==========")
302+
else:
303+
metajson = generator.getLocalMetadata()
304+
305+
print("Generating OpenAPI....")
306+
generator.generate()
307+
308+
309+
if __name__ == "__main__":
310+
main()

0 commit comments

Comments
 (0)