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