Skip to content

Commit 9006334

Browse files
openAPI proof of concept
1 parent 55c4cc5 commit 9006334

5 files changed

Lines changed: 411619 additions & 1 deletion

File tree

.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",

bin/generateOpenAPI.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
self.metajson = None
24+
self.metapath = f'{self.outdir}/sldn_metadata.json'
25+
self.openapi = {
26+
"openapi": '3.0.3',
27+
"info": {
28+
"title": "SoftLayer API - OpenAPI 3.0",
29+
"description": "SoftLayer API Definitions in a swagger format",
30+
"termsOfService": "https://cloud.ibm.com/docs/overview?topic=overview-terms",
31+
"version": "1.0.0"
32+
},
33+
"externalDocs": {
34+
"description": "SLDN",
35+
"url": "https://sldn.softlayer.com"
36+
},
37+
"servers": [
38+
{"url": "https://api.softlayer.com"},
39+
{"url": "https://api.service.softlayer.com"}
40+
],
41+
"paths": {},
42+
"components": {
43+
"schemas": {},
44+
"requestBodies": {},
45+
"securitySchemes": {
46+
"api_key": {
47+
"type": "apiKey",
48+
"name": "api_key",
49+
"in": "header"
50+
}
51+
}
52+
}
53+
}
54+
55+
def getMetadata(self, url: str) -> dict:
56+
"""Downloads metadata from SLDN"""
57+
response = requests.get(url)
58+
if response.status_code != 200:
59+
raise Exception(f"{url} returned \n{response.text}\nHTTP CODE: {response.status_code}")
60+
61+
self.metajson = response.json()
62+
return self.metajson
63+
64+
def saveMetadata(self) -> None:
65+
"""Saves metadata to a file"""
66+
print(f"Writing SLDN Metadata to {self.metapath}")
67+
with open(self.metapath, 'w') as f:
68+
json.dump(self.metajson, f, indent=4)
69+
70+
def getLocalMetadata(self) -> dict:
71+
"""Loads metadata from local data folder"""
72+
with open(self.metapath, "r", encoding="utf-8") as f:
73+
metadata = f.read()
74+
self.metajson = json.loads(metadata)
75+
return self.metajson
76+
77+
def addInORMMethods(self):
78+
for serviceName, service in self.metajson.items():
79+
# noservice means datatype only.
80+
if service.get('noservice', False) == False:
81+
for propName, prop in service.get('properties', {}).items():
82+
if prop.get('form', '') == 'relational':
83+
# capitlize() sadly lowercases the other letters in the string
84+
ormName = f"get{propName[0].upper()}{propName[1:]}"
85+
ormMethod = {
86+
'doc': prop.get('doc', ''),
87+
'docOverview': "",
88+
'name': ormName,
89+
'type': prop.get('type'),
90+
'typeArray': prop.get('typeArray', None),
91+
'ormMethod': True,
92+
'maskable': True,
93+
'filterable': True,
94+
'deprecated': prop.get('deprecated', False)
95+
}
96+
if ormMethod['typeArray']:
97+
ormMethod['limitable'] = True
98+
self.metajson[serviceName]['methods'][ormName] = ormMethod
99+
return self.metajson
100+
101+
def addInChildMethods(self):
102+
for serviceName, service in self.metajson.items():
103+
self.metajson[serviceName]['methods'] = self.getBaseMethods(serviceName, 'methods')
104+
self.metajson[serviceName]['properties'] = self.getBaseMethods(serviceName, 'properties')
105+
106+
107+
def getBaseMethods(self, serviceName, objectType):
108+
"""Responsible for pulling in properties or methods from the base class of the service requested"""
109+
service = self.metajson[serviceName]
110+
methods = service.get(objectType, {})
111+
if service.get('base', "SoftLayer_Entity") != "SoftLayer_Entity":
112+
113+
baseMethods = self.getBaseMethods(service.get('base'), objectType)
114+
for bName, bMethod in baseMethods.items():
115+
if not methods.get(bName, False):
116+
methods[bName] = bMethod
117+
return methods
118+
119+
def generate(self) -> None:
120+
print("OK")
121+
for serviceName, service in self.metajson.items():
122+
print(f"Working on {serviceName}")
123+
# Writing the check this way to be more clear to myself when reading it
124+
# This service has methods
125+
if service.get('noservice', False) == False:
126+
for methodName, method in service.get('methods', {}).items():
127+
self.openapi['paths'].update(self.genPath(serviceName, methodName, method))
128+
129+
130+
with open(f"{self.outdir}/sl_openapi.json", "w") as outfile:
131+
json.dump(self.openapi, outfile, indent=4)
132+
133+
def genPath(self, serviceName: str, methodName: str, method: dict) -> dict:
134+
http_method = "get"
135+
if method.get('parameters', False):
136+
http_method = "post"
137+
init_param = ''
138+
if not method.get('static', False):
139+
init_param = f"{{{serviceName}ID}}/"
140+
141+
schema = method.get('type')
142+
new_path = {
143+
f"{serviceName}/{init_param}{methodName}": {
144+
http_method: {
145+
"description": method.get('doc'),
146+
"summary": method.get('docOverview', ''),
147+
"externalDocs": f"https://sldn.softlayer.com/reference/services/{serviceName}/{methodName}/",
148+
"operationId": f"{serviceName}::{methodName}",
149+
"responses": {
150+
"200": {
151+
"description": "Successful operation",
152+
"content": {
153+
"application/json": {
154+
"schema": self.getSchema(method)
155+
}
156+
}
157+
}
158+
},
159+
"security": [
160+
{"api_key": []}
161+
]
162+
}
163+
}
164+
}
165+
return new_path
166+
167+
def getSchema(self, method: dict) -> dict:
168+
"""Gets a formatted schema object from a method"""
169+
is_array = method.get('typeArray', False)
170+
sl_type = method.get('type', "null")
171+
ref = {}
172+
if sl_type.startswith("SoftLayer_"):
173+
ref = {"$ref": f"#/components/schemas/{sl_type}"}
174+
else:
175+
ref = {"type": sl_type}
176+
if is_array:
177+
schema = {"type": "array", "items": ref}
178+
else:
179+
schema = ref
180+
return schema
181+
182+
183+
184+
@click.command()
185+
@click.option('--download', default=False, is_flag=True)
186+
@click.option('--clean', default=False, is_flag=True, help="Removes the services and datatypes directories so they can be built from scratch")
187+
def main(download: bool, clean: bool):
188+
cwd = os.getcwd()
189+
outdir = f'{cwd}/openapi'
190+
if not cwd.endswith('githubio_source'):
191+
raise Exception(f"Working Directory should be githubio_source, is currently {cwd}")
192+
193+
if clean:
194+
print(f"Removing {outdir}")
195+
try:
196+
shutil.rmtree(f'{outdir}')
197+
except FileNotFoundError:
198+
print("Directory doesnt exist...")
199+
200+
generator = OpenAPIGen(outdir)
201+
if download:
202+
try:
203+
metajson = generator.getMetadata(url = METAURL)
204+
generator.addInChildMethods()
205+
generator.addInORMMethods()
206+
generator.saveMetadata()
207+
except Exception as e:
208+
print("========== ERROR ==========")
209+
print(f"{e}")
210+
print("========== ERROR ==========")
211+
else:
212+
metajson = generator.getLocalMetadata()
213+
214+
print("Generating OpenAPI....")
215+
generator.generate()
216+
217+
218+
if __name__ == "__main__":
219+
main()

0 commit comments

Comments
 (0)