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 } \n HTTP 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