Juggernaut1397 commited on
Commit
998c988
·
verified ·
1 Parent(s): 0d8c70b

Upload api_schema_generatorV5.py

Browse files
Files changed (1) hide show
  1. api_schema_generatorV5.py +1198 -0
api_schema_generatorV5.py ADDED
@@ -0,0 +1,1198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import yaml
4
+ import json
5
+ import re
6
+ import xml.dom.minidom
7
+ import xml.etree.ElementTree as ET
8
+ from pathlib import Path
9
+ from urllib.parse import urlparse
10
+ from typing import Dict, List, Any, Optional, Tuple, Set
11
+ from collections import defaultdict
12
+
13
+ class ApiSchemaGeneratorV5:
14
+ """
15
+ A class to dynamically process API specifications and generate appropriate output files
16
+ like datasource_plugin_meta.json and default_schema.orx files.
17
+
18
+ Version 5: Added protection against circular references in schema objects
19
+ """
20
+
21
+ def __init__(self, api_spec_url: str, api_name: str = None, selected_endpoints: List[str] = None):
22
+ """
23
+ Initialize the ApiSchemaGeneratorV5 with an API specification URL.
24
+
25
+ Args:
26
+ api_spec_url: URL or path to the API specification (OpenAPI/Swagger)
27
+ api_name: Optional name for the API (will be extracted from spec if not provided)
28
+ selected_endpoints: Optional list of endpoint paths to include in the schema
29
+ """
30
+ self.api_spec_url = api_spec_url
31
+ self.api_name = api_name
32
+ self.api_spec = None
33
+ self.api_info = None
34
+ self.auth_info = None
35
+ self.endpoints = None
36
+ self.schema_objects = None
37
+ self.common_parameters = {} # Store common parameters across endpoints
38
+ self.selected_endpoints = selected_endpoints # Store selected endpoints
39
+
40
+ def fetch_api_spec(self) -> dict:
41
+ """Fetch and parse the API specification"""
42
+ try:
43
+ if self.api_spec_url.startswith(('http://', 'https://')):
44
+ response = requests.get(self.api_spec_url)
45
+ response.raise_for_status()
46
+ content = response.text
47
+ else:
48
+ with open(self.api_spec_url, 'r') as f:
49
+ content = f.read()
50
+
51
+ # Determine if it's JSON or YAML
52
+ try:
53
+ spec = json.loads(content)
54
+ except json.JSONDecodeError:
55
+ spec = yaml.safe_load(content)
56
+
57
+ self.api_spec = spec
58
+ return spec
59
+ except Exception as e:
60
+ print(f"Error fetching API specification: {str(e)}")
61
+ return {}
62
+
63
+ def extract_api_info(self) -> dict:
64
+ """Extract essential information from the API specification"""
65
+ if not self.api_spec:
66
+ self.fetch_api_spec()
67
+
68
+ if not self.api_spec:
69
+ return {}
70
+
71
+ api_info = {
72
+ 'title': self.api_spec.get('info', {}).get('title', 'Unknown API'),
73
+ 'description': self.api_spec.get('info', {}).get('description', ''),
74
+ 'version': self.api_spec.get('info', {}).get('version', '1.0.0'),
75
+ 'endpoints': {},
76
+ 'auth': None,
77
+ 'schemas': {}
78
+ }
79
+
80
+ # Set API name if not provided
81
+ if not self.api_name:
82
+ self.api_name = self._sanitize_name(api_info['title'])
83
+
84
+ # Extract authentication info if present
85
+ if 'components' in self.api_spec and 'securitySchemes' in self.api_spec['components']:
86
+ api_info['auth'] = self._extract_auth_info(self.api_spec)
87
+
88
+ # Extract common parameters if present
89
+ if 'components' in self.api_spec and 'parameters' in self.api_spec['components']:
90
+ self.common_parameters = self.api_spec['components']['parameters']
91
+
92
+ # Extract paths and their parameters
93
+ if 'paths' in self.api_spec:
94
+ for path, methods in self.api_spec['paths'].items():
95
+ # Extract path-level parameters
96
+ path_params = []
97
+ if 'parameters' in methods:
98
+ path_params = methods['parameters']
99
+
100
+ api_info['endpoints'][path] = {}
101
+ for method, details in methods.items():
102
+ if method.lower() in ['get', 'post', 'put', 'delete', 'patch']:
103
+ # Combine path-level and method-level parameters
104
+ all_params = path_params.copy() if path_params else []
105
+ if 'parameters' in details:
106
+ all_params.extend(details['parameters'])
107
+
108
+ # Process parameters
109
+ params = []
110
+ for param in all_params:
111
+ # Handle parameter references
112
+ if '$ref' in param:
113
+ ref = param['$ref']
114
+ param_name = ref.split('/')[-1]
115
+ if param_name in self.common_parameters:
116
+ param = self.common_parameters[param_name]
117
+
118
+ # Extract parameter schema
119
+ param_schema = param.get('schema', {})
120
+ param_type = None
121
+
122
+ # Try to get the type from the schema
123
+ if param_schema:
124
+ param_type = param_schema.get('type')
125
+
126
+ # If schema has a reference, resolve it
127
+ if '$ref' in param_schema:
128
+ ref_schema = self._resolve_schema_reference(param_schema['$ref'])
129
+ if ref_schema:
130
+ param_type = ref_schema.get('type')
131
+
132
+ # Default to string if no type is found
133
+ if not param_type:
134
+ param_type = 'string'
135
+
136
+ params.append({
137
+ 'name': param.get('name'),
138
+ 'in': param.get('in'),
139
+ 'required': param.get('required', False),
140
+ 'type': param_type,
141
+ 'enum': param_schema.get('enum'),
142
+ 'description': param.get('description', '')
143
+ })
144
+
145
+ # Extract requestBody if present
146
+ request_body_schema = None
147
+ if 'requestBody' in details:
148
+ content = details['requestBody'].get('content', {})
149
+ if 'application/json' in content:
150
+ request_body_schema = content['application/json'].get('schema')
151
+
152
+ # Extract response schema
153
+ response_schema = None
154
+ if 'responses' in details:
155
+ for status_code, response in details['responses'].items():
156
+ if status_code.startswith('2'): # 2xx responses
157
+ if 'content' in response:
158
+ for content_type, content_details in response['content'].items():
159
+ if 'schema' in content_details:
160
+ response_schema = content_details['schema']
161
+ break
162
+ break
163
+
164
+ api_info['endpoints'][path][method] = {
165
+ 'summary': details.get('summary', ''),
166
+ 'description': details.get('description', ''),
167
+ 'operationId': details.get('operationId', ''),
168
+ 'parameters': params,
169
+ 'requestBody': request_body_schema,
170
+ 'responseSchema': response_schema,
171
+ 'security': details.get('security')
172
+ }
173
+
174
+ # Extract schema objects
175
+ if 'components' in self.api_spec and 'schemas' in self.api_spec['components']:
176
+ api_info['schemas'] = self.api_spec['components']['schemas']
177
+
178
+ self.api_info = api_info
179
+ self.auth_info = api_info['auth']
180
+ self.endpoints = api_info['endpoints']
181
+ self.schema_objects = api_info['schemas']
182
+
183
+ return api_info
184
+
185
+ def _extract_auth_info(self, spec: dict) -> dict:
186
+ """Extract detailed authentication information from API spec"""
187
+ auth_info = {}
188
+
189
+ if 'securitySchemes' in spec.get('components', {}):
190
+ for scheme_name, scheme_details in spec['components']['securitySchemes'].items():
191
+ auth_type = scheme_details.get('type')
192
+
193
+ # Extract common info
194
+ auth_info[scheme_name] = {
195
+ 'type': auth_type,
196
+ 'description': scheme_details.get('description', '')
197
+ }
198
+
199
+ # Extract type-specific info
200
+ if auth_type == 'apiKey':
201
+ auth_info[scheme_name].update({
202
+ 'name': scheme_details.get('name'),
203
+ 'in': scheme_details.get('in') # header, query, or cookie
204
+ })
205
+ elif auth_type == 'http':
206
+ auth_info[scheme_name].update({
207
+ 'scheme': scheme_details.get('scheme') # basic, bearer, etc.
208
+ })
209
+ elif auth_type == 'oauth2':
210
+ flows = scheme_details.get('flows', {})
211
+ auth_info[scheme_name]['flows'] = {}
212
+
213
+ for flow_type, flow_details in flows.items():
214
+ auth_info[scheme_name]['flows'][flow_type] = {
215
+ 'authorizationUrl': flow_details.get('authorizationUrl'),
216
+ 'tokenUrl': flow_details.get('tokenUrl'),
217
+ 'refreshUrl': flow_details.get('refreshUrl'),
218
+ 'scopes': flow_details.get('scopes', {})
219
+ }
220
+
221
+ return auth_info
222
+
223
+ def _sanitize_name(self, name: str) -> str:
224
+ """Sanitize a name to be used as an identifier"""
225
+ if not name:
226
+ return "api"
227
+
228
+ # Remove special characters and replace spaces with underscores
229
+ sanitized = re.sub(r'[^a-zA-Z0-9_\s]', '', name)
230
+ sanitized = re.sub(r'\s+', '_', sanitized).lower()
231
+
232
+ return sanitized
233
+
234
+ def _extract_server_url(self) -> str:
235
+ """Extract server URL from API specification"""
236
+ if not self.api_spec:
237
+ self.fetch_api_spec()
238
+
239
+ # Extract server information
240
+ servers = self.api_spec.get('servers', [])
241
+ if servers and 'url' in servers[0]:
242
+ return servers[0]['url']
243
+
244
+ # Default URL if no servers defined
245
+ return "https://api.example.com"
246
+
247
+ def _extract_maxretries(self) -> str:
248
+ """Extract maximum retries from API specification"""
249
+ if not self.api_spec:
250
+ self.fetch_api_spec()
251
+
252
+ # Look for rate limiting information in the API spec
253
+ # Check in extensions
254
+ if 'x-ratelimit-retries' in self.api_spec:
255
+ return str(self.api_spec['x-ratelimit-retries'])
256
+
257
+ # Check in info section
258
+ if 'info' in self.api_spec and 'x-ratelimit-retries' in self.api_spec['info']:
259
+ return str(self.api_spec['info']['x-ratelimit-retries'])
260
+
261
+ # Check in components section
262
+ if 'components' in self.api_spec and 'x-ratelimit-retries' in self.api_spec['components']:
263
+ return str(self.api_spec['components']['x-ratelimit-retries'])
264
+
265
+ # Default value if not found in API spec
266
+ return "3"
267
+
268
+ def _extract_timeout(self) -> str:
269
+ """Extract timeout from API specification"""
270
+ if not self.api_spec:
271
+ self.fetch_api_spec()
272
+
273
+ # Look for timeout information in the API spec
274
+ # Check in extensions
275
+ if 'x-timeout' in self.api_spec:
276
+ return str(self.api_spec['x-timeout'])
277
+
278
+ # Check in info section
279
+ if 'info' in self.api_spec and 'x-timeout' in self.api_spec['info']:
280
+ return str(self.api_spec['info']['x-timeout'])
281
+
282
+ # Check in components section
283
+ if 'components' in self.api_spec and 'x-timeout' in self.api_spec['components']:
284
+ return str(self.api_spec['components']['x-timeout'])
285
+
286
+ # Default value if not found in API spec
287
+ return "60"
288
+
289
+ def _get_auth_meta_fields(self) -> List[Dict[str, Any]]:
290
+ """Generate meta fields for authentication based on the API spec"""
291
+ meta_fields = []
292
+ added_fields = set() # Track added field names to avoid duplication
293
+
294
+ # Use the custom API name for all field descriptions
295
+ api_name_title = self.api_name.title()
296
+
297
+ if not self.auth_info:
298
+ # Default to basic auth if no auth info is found
299
+ meta_fields.extend([
300
+ {
301
+ "name": "url",
302
+ "description": f"{api_name_title} URL",
303
+ "sectionName": "Connection Info",
304
+ "defaultValue": self._extract_server_url(),
305
+ "dataType": "STRING",
306
+ "isRequired": True,
307
+ "regex": ""
308
+ },
309
+ {
310
+ "name": "username",
311
+ "description": f"{api_name_title} Username",
312
+ "sectionName": "Connection Info",
313
+ "defaultValue": "[username]",
314
+ "dataType": "STRING",
315
+ "isRequired": True,
316
+ "regex": ""
317
+ },
318
+ {
319
+ "name": "password",
320
+ "description": f"{api_name_title} Password",
321
+ "sectionName": "Connection Info",
322
+ "defaultValue": "[password]",
323
+ "dataType": "STRING",
324
+ "isRequired": True,
325
+ "regex": ""
326
+ }
327
+ ])
328
+ added_fields.update(["url", "username", "password"])
329
+ else:
330
+ # Add URL field
331
+ meta_fields.append({
332
+ "name": "url",
333
+ "description": f"{api_name_title} URL",
334
+ "sectionName": "Connection Info",
335
+ "defaultValue": self._extract_server_url(),
336
+ "dataType": "STRING",
337
+ "isRequired": True,
338
+ "regex": ""
339
+ })
340
+ added_fields.add("url")
341
+
342
+ # Process each auth scheme
343
+ for scheme_name, scheme_details in self.auth_info.items():
344
+ auth_type = scheme_details.get('type')
345
+
346
+ if auth_type == 'apiKey' and "apitoken" not in added_fields:
347
+ meta_fields.append({
348
+ "name": "apitoken",
349
+ "description": f"{api_name_title} API Token",
350
+ "sectionName": "Connection Info",
351
+ "defaultValue": "[your_api_token]",
352
+ "dataType": "STRING",
353
+ "isRequired": True,
354
+ "regex": ""
355
+ })
356
+ added_fields.add("apitoken")
357
+ elif auth_type == 'http':
358
+ scheme = scheme_details.get('scheme', '').lower()
359
+ if scheme == 'basic':
360
+ if "username" not in added_fields:
361
+ meta_fields.append({
362
+ "name": "username",
363
+ "description": f"{api_name_title} Username",
364
+ "sectionName": "Connection Info",
365
+ "defaultValue": "[username]",
366
+ "dataType": "STRING",
367
+ "isRequired": True,
368
+ "regex": ""
369
+ })
370
+ added_fields.add("username")
371
+
372
+ if "password" not in added_fields:
373
+ meta_fields.append({
374
+ "name": "password",
375
+ "description": f"{api_name_title} Password",
376
+ "sectionName": "Connection Info",
377
+ "defaultValue": "[password]",
378
+ "dataType": "STRING",
379
+ "isRequired": True,
380
+ "regex": ""
381
+ })
382
+ added_fields.add("password")
383
+ elif scheme == 'bearer' and "token" not in added_fields:
384
+ meta_fields.append({
385
+ "name": "token",
386
+ "description": f"{api_name_title} Bearer Token",
387
+ "sectionName": "Connection Info",
388
+ "defaultValue": "[your_bearer_token]",
389
+ "dataType": "STRING",
390
+ "isRequired": True,
391
+ "regex": ""
392
+ })
393
+ added_fields.add("token")
394
+ elif auth_type == 'oauth2':
395
+ flows = scheme_details.get('flows', {})
396
+
397
+ # Check for client credentials flow
398
+ if 'clientCredentials' in flows:
399
+ if "clientId" not in added_fields:
400
+ meta_fields.append({
401
+ "name": "clientId",
402
+ "description": f"{api_name_title} Client ID",
403
+ "sectionName": "Connection Info",
404
+ "defaultValue": "[your_client_id]",
405
+ "dataType": "STRING",
406
+ "isRequired": True,
407
+ "regex": ""
408
+ })
409
+ added_fields.add("clientId")
410
+
411
+ if "clientSecret" not in added_fields:
412
+ meta_fields.append({
413
+ "name": "clientSecret",
414
+ "description": f"{api_name_title} Client Secret",
415
+ "sectionName": "Connection Info",
416
+ "defaultValue": "[your_client_secret]",
417
+ "dataType": "STRING",
418
+ "isRequired": True,
419
+ "regex": ""
420
+ })
421
+ added_fields.add("clientSecret")
422
+
423
+ if "tokenUrl" not in added_fields:
424
+ meta_fields.append({
425
+ "name": "tokenUrl",
426
+ "description": f"{api_name_title} Token URL",
427
+ "sectionName": "Connection Info",
428
+ "defaultValue": flows['clientCredentials'].get('tokenUrl', ''),
429
+ "dataType": "STRING",
430
+ "isRequired": True,
431
+ "regex": ""
432
+ })
433
+ added_fields.add("tokenUrl")
434
+ # Check for password flow
435
+ elif 'password' in flows:
436
+ if "username" not in added_fields:
437
+ meta_fields.append({
438
+ "name": "username",
439
+ "description": f"{api_name_title} Username",
440
+ "sectionName": "Connection Info",
441
+ "defaultValue": "[username]",
442
+ "dataType": "STRING",
443
+ "isRequired": True,
444
+ "regex": ""
445
+ })
446
+ added_fields.add("username")
447
+
448
+ if "password" not in added_fields:
449
+ meta_fields.append({
450
+ "name": "password",
451
+ "description": f"{api_name_title} Password",
452
+ "sectionName": "Connection Info",
453
+ "defaultValue": "[password]",
454
+ "dataType": "STRING",
455
+ "isRequired": True,
456
+ "regex": ""
457
+ })
458
+ added_fields.add("password")
459
+
460
+ if "clientId" not in added_fields:
461
+ meta_fields.append({
462
+ "name": "clientId",
463
+ "description": f"{api_name_title} Client ID",
464
+ "sectionName": "Connection Info",
465
+ "defaultValue": "[your_client_id]",
466
+ "dataType": "STRING",
467
+ "isRequired": True,
468
+ "regex": ""
469
+ })
470
+ added_fields.add("clientId")
471
+
472
+ if "clientSecret" not in added_fields:
473
+ meta_fields.append({
474
+ "name": "clientSecret",
475
+ "description": f"{api_name_title} Client Secret",
476
+ "sectionName": "Connection Info",
477
+ "defaultValue": "[your_client_secret]",
478
+ "dataType": "STRING",
479
+ "isRequired": True,
480
+ "regex": ""
481
+ })
482
+ added_fields.add("clientSecret")
483
+
484
+ if "tokenUrl" not in added_fields:
485
+ meta_fields.append({
486
+ "name": "tokenUrl",
487
+ "description": f"{api_name_title} Token URL",
488
+ "sectionName": "Connection Info",
489
+ "defaultValue": flows['password'].get('tokenUrl', ''),
490
+ "dataType": "STRING",
491
+ "isRequired": True,
492
+ "regex": ""
493
+ })
494
+ added_fields.add("tokenUrl")
495
+ # Check for authorization code flow
496
+ elif 'authorizationCode' in flows:
497
+ if "clientId" not in added_fields:
498
+ meta_fields.append({
499
+ "name": "clientId",
500
+ "description": f"{api_name_title} Client ID",
501
+ "sectionName": "Connection Info",
502
+ "defaultValue": "[your_client_id]",
503
+ "dataType": "STRING",
504
+ "isRequired": True,
505
+ "regex": ""
506
+ })
507
+ added_fields.add("clientId")
508
+
509
+ if "clientSecret" not in added_fields:
510
+ meta_fields.append({
511
+ "name": "clientSecret",
512
+ "description": f"{api_name_title} Client Secret",
513
+ "sectionName": "Connection Info",
514
+ "defaultValue": "[your_client_secret]",
515
+ "dataType": "STRING",
516
+ "isRequired": True,
517
+ "regex": ""
518
+ })
519
+ added_fields.add("clientSecret")
520
+
521
+ if "authorizationUrl" not in added_fields:
522
+ meta_fields.append({
523
+ "name": "authorizationUrl",
524
+ "description": f"{api_name_title} Authorization URL",
525
+ "sectionName": "Connection Info",
526
+ "defaultValue": flows['authorizationCode'].get('authorizationUrl', ''),
527
+ "dataType": "STRING",
528
+ "isRequired": True,
529
+ "regex": ""
530
+ })
531
+ added_fields.add("authorizationUrl")
532
+
533
+ if "tokenUrl" not in added_fields:
534
+ meta_fields.append({
535
+ "name": "tokenUrl",
536
+ "description": f"{api_name_title} Token URL",
537
+ "sectionName": "Connection Info",
538
+ "defaultValue": flows['authorizationCode'].get('tokenUrl', ''),
539
+ "dataType": "STRING",
540
+ "isRequired": True,
541
+ "regex": ""
542
+ })
543
+ added_fields.add("tokenUrl")
544
+
545
+ # Add request parameters from API spec
546
+ if "maxretries" not in added_fields:
547
+ maxretries_value = self._extract_maxretries()
548
+ meta_fields.append({
549
+ "name": "maxretries",
550
+ "description": "Maximum Request Retries",
551
+ "sectionName": "Request Metering",
552
+ "defaultValue": maxretries_value,
553
+ "dataType": "NUMBER",
554
+ "isRequired": True
555
+ })
556
+
557
+ if "timeout" not in added_fields:
558
+ timeout_value = self._extract_timeout()
559
+ meta_fields.append({
560
+ "name": "timeout",
561
+ "description": "Request Timeout (seconds)",
562
+ "sectionName": "Request Metering",
563
+ "defaultValue": timeout_value,
564
+ "dataType": "NUMBER",
565
+ "isRequired": True
566
+ })
567
+
568
+ return meta_fields
569
+
570
+ def generate_datasource_plugin_meta(self) -> dict:
571
+ """Generate the datasource plugin meta JSON file"""
572
+ if not self.api_info:
573
+ self.extract_api_info()
574
+
575
+ # Handle case where API info couldn't be extracted
576
+ if not self.api_info:
577
+ self.api_info = {
578
+ 'title': 'Unknown API',
579
+ 'description': '',
580
+ 'version': '1.0.0',
581
+ 'endpoints': {},
582
+ 'auth': None,
583
+ 'schemas': {}
584
+ }
585
+
586
+ # Use the custom API name for all references in the metadata
587
+ api_name_title = self.api_name.title()
588
+ api_name_lower = self.api_name.lower()
589
+
590
+ meta_data = {
591
+ "name": api_name_title,
592
+ "description": self.api_info.get('description') or f"{api_name_title} Directory",
593
+ "backendCategory": "custom",
594
+ "userCreated": False,
595
+ "icon": f"/{api_name_lower}.svg",
596
+ "isSchemaExtractable": False,
597
+ "meta": self._get_auth_meta_fields()
598
+ }
599
+
600
+ return meta_data
601
+
602
+ def _resolve_schema_reference(self, ref: str) -> dict:
603
+ """Resolve a schema reference to the actual schema definition"""
604
+ if not ref.startswith('#/components/schemas/'):
605
+ return {}
606
+
607
+ schema_name = ref.split('/')[-1]
608
+ if self.schema_objects and schema_name in self.schema_objects:
609
+ return self.schema_objects[schema_name]
610
+
611
+ return {}
612
+
613
+ def _extract_properties_from_schema(self, schema: dict) -> dict:
614
+ """Extract properties from a schema, handling references and composition"""
615
+ if not schema or not isinstance(schema, dict):
616
+ return {}
617
+
618
+ # Handle direct reference
619
+ if '$ref' in schema:
620
+ return self._extract_properties_from_schema(self._resolve_schema_reference(schema['$ref']))
621
+
622
+ # Get direct properties
623
+ properties = schema.get('properties', {})
624
+
625
+ # Handle allOf composition
626
+ if 'allOf' in schema:
627
+ for sub_schema in schema['allOf']:
628
+ sub_properties = self._extract_properties_from_schema(sub_schema)
629
+ properties.update(sub_properties)
630
+
631
+ # Handle oneOf composition
632
+ if 'oneOf' in schema:
633
+ # For oneOf, we'll take properties from the first schema as a representative
634
+ if schema['oneOf'] and isinstance(schema['oneOf'][0], dict):
635
+ first_schema = schema['oneOf'][0]
636
+ sub_properties = self._extract_properties_from_schema(first_schema)
637
+ properties.update(sub_properties)
638
+
639
+ # Handle anyOf composition
640
+ if 'anyOf' in schema:
641
+ # For anyOf, we'll take properties from all schemas
642
+ for sub_schema in schema['anyOf']:
643
+ sub_properties = self._extract_properties_from_schema(sub_schema)
644
+ properties.update(sub_properties)
645
+
646
+ return properties
647
+
648
+ def _extract_common_parameters(self) -> Dict[str, Dict[str, Any]]:
649
+ """Extract common parameters from the API spec"""
650
+ # Return empty dict as we're not using common parameters anymore
651
+ return {}
652
+
653
+ def _extract_data_models(self) -> List[Dict[str, Any]]:
654
+ """Extract data models from schema objects that are relevant to selected endpoints"""
655
+ data_models = []
656
+ processed_schemas = set()
657
+ relevant_schemas = set()
658
+
659
+ if not self.schema_objects:
660
+ return data_models
661
+
662
+ # First, identify which schemas are referenced by the selected endpoints
663
+ if self.selected_endpoints:
664
+ # Extract endpoints based on selected paths and methods
665
+ selected_paths = {}
666
+ for path, method in self.selected_endpoints:
667
+ if path not in selected_paths:
668
+ selected_paths[path] = []
669
+ selected_paths[path].append(method.lower())
670
+
671
+ # Process endpoints to find referenced schemas
672
+ for path, methods in self.endpoints.items():
673
+ if path not in selected_paths:
674
+ continue
675
+
676
+ for method, details in methods.items():
677
+ if method.lower() not in selected_paths.get(path, []):
678
+ continue
679
+
680
+ # Check response schema
681
+ response_schema = details.get('responseSchema')
682
+ if response_schema:
683
+ self._collect_referenced_schemas(response_schema, relevant_schemas)
684
+
685
+ # Check request body schema
686
+ request_body = details.get('requestBody')
687
+ if request_body:
688
+ self._collect_referenced_schemas(request_body, relevant_schemas)
689
+
690
+ # Check parameter schemas
691
+ for param in details.get('parameters', []):
692
+ param_schema = param.get('schema', {})
693
+ if param_schema:
694
+ self._collect_referenced_schemas(param_schema, relevant_schemas)
695
+ else:
696
+ # If no endpoints are selected, include all schemas
697
+ relevant_schemas = set(self.schema_objects.keys())
698
+
699
+ # Process only the relevant schema objects
700
+ for schema_name in relevant_schemas:
701
+ if schema_name in processed_schemas:
702
+ continue
703
+
704
+ schema_def = self.schema_objects.get(schema_name)
705
+ if not schema_def:
706
+ continue
707
+
708
+ # Skip non-object schemas
709
+ if schema_def.get('type') and schema_def.get('type') != 'object':
710
+ continue
711
+
712
+ table = self._create_table_from_schema(schema_name, schema_def)
713
+ if table:
714
+ # Add source information to the table
715
+ table["Source"] = "Components/Schemas"
716
+ table["Type"] = "DATA_MODEL"
717
+ data_models.append(table)
718
+ processed_schemas.add(schema_name)
719
+
720
+ return data_models
721
+
722
+ def _collect_referenced_schemas(self, schema: dict, referenced_schemas: set, visited_refs: set = None) -> None:
723
+ """
724
+ Recursively collect all schema references from a schema object.
725
+
726
+ Args:
727
+ schema: The schema object to process
728
+ referenced_schemas: Set to collect referenced schema names
729
+ visited_refs: Set to track already visited references to prevent circular reference issues
730
+ """
731
+ if not schema or not isinstance(schema, dict):
732
+ return
733
+
734
+ # Initialize visited_refs if not provided
735
+ if visited_refs is None:
736
+ visited_refs = set()
737
+
738
+ # Handle direct reference
739
+ if '$ref' in schema:
740
+ ref = schema['$ref']
741
+ if ref.startswith('#/components/schemas/'):
742
+ schema_name = ref.split('/')[-1]
743
+
744
+ # Skip if we've already processed this reference to avoid circular references
745
+ if ref in visited_refs:
746
+ return
747
+
748
+ # Add to referenced schemas and mark as visited
749
+ referenced_schemas.add(schema_name)
750
+ visited_refs.add(ref)
751
+
752
+ # Recursively collect references from the referenced schema
753
+ if self.schema_objects and schema_name in self.schema_objects:
754
+ self._collect_referenced_schemas(self.schema_objects[schema_name], referenced_schemas, visited_refs)
755
+
756
+ # Handle array items
757
+ if schema.get('type') == 'array' and 'items' in schema:
758
+ self._collect_referenced_schemas(schema['items'], referenced_schemas, visited_refs)
759
+
760
+ # Handle object properties
761
+ if schema.get('type') == 'object' and 'properties' in schema:
762
+ for prop_name, prop_def in schema['properties'].items():
763
+ self._collect_referenced_schemas(prop_def, referenced_schemas, visited_refs)
764
+
765
+ # Handle composition schemas
766
+ for comp_key in ['allOf', 'oneOf', 'anyOf']:
767
+ if comp_key in schema:
768
+ for sub_schema in schema[comp_key]:
769
+ self._collect_referenced_schemas(sub_schema, referenced_schemas, visited_refs)
770
+
771
+ def _extract_endpoints(self) -> List[Dict[str, Any]]:
772
+ """Extract endpoint information"""
773
+ endpoints = []
774
+
775
+ # Extract common parameters
776
+ common_params = self._extract_common_parameters()
777
+
778
+ selected_paths = {}
779
+ if self.selected_endpoints:
780
+ for path, method in self.selected_endpoints:
781
+ if path not in selected_paths:
782
+ selected_paths[path] = []
783
+ selected_paths[path].append(method.lower())
784
+
785
+ # Process endpoints
786
+ for path, methods in self.endpoints.items():
787
+ # Skip endpoints that are not in the selected_endpoints list if it's provided
788
+ if self.selected_endpoints is not None and path not in selected_paths:
789
+ continue
790
+ for method, details in methods.items():
791
+ # Skip methods that are not in the selected methods for this path
792
+ if self.selected_endpoints is not None and method.lower() not in selected_paths.get(path, []):
793
+ continue
794
+ if method.lower() in ['get', 'post', 'put', 'delete', 'patch']:
795
+ endpoint = {
796
+ "ID": self._sanitize_name(f"{method}_{path}"),
797
+ "Name": details.get('operationId') or f"{method.upper()} {path}",
798
+ "Path": path,
799
+ "Method": method.upper(),
800
+ "Summary": details.get('summary', ''),
801
+ "Description": details.get('description', ''),
802
+ "Parameters": [],
803
+ "Type": "ENDPOINT"
804
+ }
805
+
806
+ # Process parameters
807
+ for param in details.get('parameters', []):
808
+ param_key = f"{param.get('name')}:{param.get('in')}"
809
+
810
+ # Extract parameter schema and type
811
+ param_schema = param.get('schema', {})
812
+ param_type = None
813
+
814
+ # Try to get the type from the schema
815
+ if param_schema:
816
+ param_type = param_schema.get('type')
817
+
818
+ # If schema has a reference, resolve it
819
+ if '$ref' in param_schema:
820
+ ref_schema = self._resolve_schema_reference(param_schema['$ref'])
821
+ if ref_schema:
822
+ param_type = ref_schema.get('type')
823
+
824
+ # Default to string if no type is found
825
+ if not param_type:
826
+ param_type = 'string'
827
+
828
+ # Check if this is a common parameter
829
+ if param_key in common_params:
830
+ endpoint["Parameters"].append({
831
+ "Name": param.get('name'),
832
+ "In": param.get('in'),
833
+ "Required": param.get('required', False),
834
+ "Type": param_type, # Include type even for common parameters
835
+ "IsCommon": True,
836
+ "CommonRef": param_key
837
+ })
838
+ else:
839
+ endpoint["Parameters"].append({
840
+ "Name": param.get('name'),
841
+ "In": param.get('in'),
842
+ "Required": param.get('required', False),
843
+ "Type": param_type,
844
+ "Enum": param_schema.get('enum'),
845
+ "Description": param.get('description', ''),
846
+ "IsCommon": False
847
+ })
848
+
849
+ # Process response schema
850
+ response_schema = details.get('responseSchema')
851
+ if response_schema:
852
+ if '$ref' in response_schema:
853
+ ref = response_schema['$ref']
854
+ schema_name = ref.split('/')[-1]
855
+ endpoint["ResponseModel"] = schema_name
856
+ endpoint["ResponseType"] = "object"
857
+ elif response_schema.get('type') == 'array' and 'items' in response_schema:
858
+ items = response_schema['items']
859
+ if '$ref' in items:
860
+ ref = items['$ref']
861
+ schema_name = ref.split('/')[-1]
862
+ endpoint["ResponseModel"] = schema_name
863
+ endpoint["ResponseType"] = "array"
864
+ else:
865
+ # Inline schema
866
+ endpoint["ResponseType"] = "array"
867
+ endpoint["ResponseInlineSchema"] = items
868
+ else:
869
+ # Inline schema
870
+ endpoint["ResponseType"] = response_schema.get('type', 'object')
871
+ endpoint["ResponseInlineSchema"] = response_schema
872
+
873
+ # Process request body
874
+ request_body = details.get('requestBody')
875
+ if request_body:
876
+ if '$ref' in request_body:
877
+ ref = request_body['$ref']
878
+ schema_name = ref.split('/')[-1]
879
+ endpoint["RequestModel"] = schema_name
880
+ else:
881
+ # Inline schema
882
+ endpoint["RequestInlineSchema"] = request_body
883
+
884
+ endpoints.append(endpoint)
885
+
886
+ return endpoints
887
+
888
+ def _create_table_from_schema(self, schema_name: str, schema_def: dict) -> Optional[Dict[str, Any]]:
889
+ """Create a table definition from a schema object"""
890
+ if not schema_def or not isinstance(schema_def, dict):
891
+ return None
892
+
893
+ # Extract properties, handling references and composition
894
+ properties = self._extract_properties_from_schema(schema_def)
895
+
896
+ if not properties:
897
+ return None
898
+
899
+ # Get required properties
900
+ required = schema_def.get('required', [])
901
+
902
+ # Create table
903
+ table = {
904
+ "ID": self._sanitize_name(schema_name),
905
+ "Name": schema_name,
906
+ "Description": schema_def.get('description', ''),
907
+ "Properties": []
908
+ }
909
+
910
+ # Add properties
911
+ for prop_name, prop_def in properties.items():
912
+ property_info = {
913
+ "Name": prop_name,
914
+ "Required": prop_name in required,
915
+ "Description": prop_def.get('description', '') if isinstance(prop_def, dict) else ''
916
+ }
917
+
918
+ # Determine property type
919
+ if isinstance(prop_def, dict):
920
+ prop_type = prop_def.get('type')
921
+ if prop_type:
922
+ property_info["Type"] = prop_type
923
+
924
+ # Handle enum values
925
+ if 'enum' in prop_def:
926
+ property_info["Enum"] = prop_def['enum']
927
+
928
+ # Handle array items
929
+ if prop_type == 'array' and 'items' in prop_def:
930
+ items = prop_def['items']
931
+ if '$ref' in items:
932
+ ref = items['$ref']
933
+ schema_name = ref.split('/')[-1]
934
+ property_info["ItemsType"] = "reference"
935
+ property_info["ItemsRef"] = schema_name
936
+ else:
937
+ property_info["ItemsType"] = items.get('type', 'object')
938
+
939
+ # Handle object properties
940
+ if prop_type == 'object' and 'properties' in prop_def:
941
+ property_info["ObjectProperties"] = []
942
+ for sub_prop_name, sub_prop_def in prop_def['properties'].items():
943
+ sub_prop_info = {
944
+ "Name": sub_prop_name,
945
+ "Type": sub_prop_def.get('type', 'string'),
946
+ "Description": sub_prop_def.get('description', '')
947
+ }
948
+ property_info["ObjectProperties"].append(sub_prop_info)
949
+
950
+ # Handle references
951
+ if '$ref' in prop_def:
952
+ ref = prop_def['$ref']
953
+ schema_name = ref.split('/')[-1]
954
+ property_info["Type"] = "reference"
955
+ property_info["Ref"] = schema_name
956
+
957
+ table["Properties"].append(property_info)
958
+
959
+ return table
960
+
961
+ def _generate_datasource_value(self) -> str:
962
+ """Generate a DataSource value based on API spec information"""
963
+ # Extract server information
964
+ servers = self.api_spec.get('servers', [])
965
+ server_url = "https://api.example.com"
966
+ if servers and 'url' in servers[0]:
967
+ server_url = servers[0]['url']
968
+
969
+ # Extract authentication information
970
+ auth_info = {}
971
+ if self.auth_info:
972
+ for scheme_name, scheme_details in self.auth_info.items():
973
+ auth_type = scheme_details.get('type')
974
+
975
+ if auth_type == 'apiKey':
976
+ auth_info['type'] = 'apiKey'
977
+ auth_info['name'] = scheme_details.get('name', '')
978
+ auth_info['in'] = scheme_details.get('in', 'header')
979
+ elif auth_type == 'http':
980
+ scheme = scheme_details.get('scheme', '').lower()
981
+ auth_info['type'] = 'http'
982
+ auth_info['scheme'] = scheme
983
+ elif auth_type == 'oauth2':
984
+ auth_info['type'] = 'oauth2'
985
+ flows = scheme_details.get('flows', {})
986
+ if 'clientCredentials' in flows:
987
+ auth_info['flow'] = 'clientCredentials'
988
+ auth_info['tokenUrl'] = flows['clientCredentials'].get('tokenUrl', '')
989
+ elif 'password' in flows:
990
+ auth_info['flow'] = 'password'
991
+ auth_info['tokenUrl'] = flows['password'].get('tokenUrl', '')
992
+ elif 'authorizationCode' in flows:
993
+ auth_info['flow'] = 'authorizationCode'
994
+ auth_info['authorizationUrl'] = flows['authorizationCode'].get('authorizationUrl', '')
995
+ auth_info['tokenUrl'] = flows['authorizationCode'].get('tokenUrl', '')
996
+
997
+ # Create a connection string based on the extracted information
998
+ connection_parts = []
999
+ connection_parts.append(f"url={server_url}")
1000
+
1001
+ if auth_info:
1002
+ connection_parts.append(f"auth_type={auth_info.get('type', 'none')}")
1003
+
1004
+ if auth_info.get('type') == 'apiKey':
1005
+ connection_parts.append(f"api_key_name={auth_info.get('name', '')}")
1006
+ connection_parts.append(f"api_key_in={auth_info.get('in', '')}")
1007
+ elif auth_info.get('type') == 'http':
1008
+ connection_parts.append(f"http_scheme={auth_info.get('scheme', '')}")
1009
+ elif auth_info.get('type') == 'oauth2':
1010
+ connection_parts.append(f"oauth2_flow={auth_info.get('flow', '')}")
1011
+ if 'tokenUrl' in auth_info:
1012
+ connection_parts.append(f"token_url={auth_info.get('tokenUrl', '')}")
1013
+ if 'authorizationUrl' in auth_info:
1014
+ connection_parts.append(f"authorization_url={auth_info.get('authorizationUrl', '')}")
1015
+
1016
+ # Add API name and version
1017
+ if self.api_info:
1018
+ connection_parts.append(f"api_name={self.api_name}")
1019
+ connection_parts.append(f"api_version={self.api_info.get('version', '1.0.0')}")
1020
+
1021
+ # Create the connection string
1022
+ connection_string = ";".join(connection_parts)
1023
+
1024
+ # Simulate AES encryption with a prefix
1025
+ # In a real implementation, this would be properly encrypted
1026
+ return f"{{AES}}{connection_string}"
1027
+
1028
+ def generate_default_schema(self) -> str:
1029
+ """Generate the default schema XML file"""
1030
+ if not self.api_info:
1031
+ self.extract_api_info()
1032
+
1033
+ # Create XML structure
1034
+ root = ET.Element("Xml")
1035
+ org = ET.SubElement(root, "ORG", Name=self.api_name.lower())
1036
+
1037
+ ET.SubElement(org, "AccessMethod").text = "PDM"
1038
+
1039
+ pdm = ET.SubElement(org, "PDM")
1040
+ ET.SubElement(pdm, "Name").text = self.api_name.lower()
1041
+
1042
+ # Generate DataSource value from API spec information
1043
+ datasource_value = self._generate_datasource_value()
1044
+ ET.SubElement(pdm, "DataSource").text = datasource_value
1045
+ ET.SubElement(pdm, "DataSourceType").text = "XML"
1046
+
1047
+ # Add tables
1048
+ tables = ET.SubElement(pdm, "Tables")
1049
+
1050
+ # We no longer include common parameters section
1051
+
1052
+ # Extract data models
1053
+ data_models = self._extract_data_models()
1054
+
1055
+ # Add a section for data models
1056
+ if data_models:
1057
+ tables.append(ET.Comment(" DATA MODELS "))
1058
+ for model in data_models:
1059
+ model_class = ET.SubElement(tables, "Class", type=model["ID"])
1060
+ ET.SubElement(model_class, "Name").text = model["Name"]
1061
+ ET.SubElement(model_class, "Type").text = "DATA_MODEL"
1062
+ ET.SubElement(model_class, "Description").text = model.get("Description", "")
1063
+
1064
+ properties = ET.SubElement(model_class, "Properties")
1065
+ for prop in model.get("Properties", []):
1066
+ prop_elem = ET.SubElement(properties, "Property", ID=self._sanitize_name(prop["Name"]))
1067
+ ET.SubElement(prop_elem, "Name").text = prop["Name"]
1068
+ ET.SubElement(prop_elem, "Type").text = prop.get("Type", "string")
1069
+ ET.SubElement(prop_elem, "Required").text = str(prop.get("Required", False)).lower()
1070
+ ET.SubElement(prop_elem, "Description").text = prop.get("Description", "")
1071
+
1072
+ # Handle references
1073
+ if "Ref" in prop:
1074
+ ET.SubElement(prop_elem, "ClassType").text = prop["Ref"]
1075
+
1076
+ # Handle arrays
1077
+ if prop.get("Type") == "array" and "ItemsType" in prop:
1078
+ items = ET.SubElement(prop_elem, "Items")
1079
+ ET.SubElement(items, "Type").text = prop["ItemsType"]
1080
+ if "ItemsRef" in prop:
1081
+ ET.SubElement(items, "ClassType").text = prop["ItemsRef"]
1082
+
1083
+ # Handle object properties
1084
+ if prop.get("Type") == "object" and "ObjectProperties" in prop:
1085
+ obj_props = ET.SubElement(prop_elem, "ObjectProperties")
1086
+ for obj_prop in prop["ObjectProperties"]:
1087
+ obj_prop_elem = ET.SubElement(obj_props, "Property", ID=self._sanitize_name(obj_prop["Name"]))
1088
+ ET.SubElement(obj_prop_elem, "Name").text = obj_prop["Name"]
1089
+ ET.SubElement(obj_prop_elem, "Type").text = obj_prop.get("Type", "string")
1090
+ ET.SubElement(obj_prop_elem, "Description").text = obj_prop.get("Description", "")
1091
+
1092
+ # Extract endpoints
1093
+ endpoints_list = self._extract_endpoints()
1094
+
1095
+ # Add a section for endpoints
1096
+ if endpoints_list:
1097
+ tables.append(ET.Comment(" API ENDPOINTS "))
1098
+ for endpoint in endpoints_list:
1099
+ endpoint_elem = ET.SubElement(tables, "Endpoint", ID=endpoint["ID"])
1100
+ ET.SubElement(endpoint_elem, "Name").text = endpoint["Name"]
1101
+ ET.SubElement(endpoint_elem, "Type").text = "ENDPOINT"
1102
+ ET.SubElement(endpoint_elem, "Path").text = endpoint["Path"]
1103
+ ET.SubElement(endpoint_elem, "Method").text = endpoint["Method"]
1104
+ ET.SubElement(endpoint_elem, "Summary").text = endpoint.get("Summary", "")
1105
+ ET.SubElement(endpoint_elem, "Description").text = endpoint.get("Description", "")
1106
+
1107
+ # Add parameters
1108
+ if endpoint.get("Parameters"):
1109
+ params = ET.SubElement(endpoint_elem, "Parameters")
1110
+ for param in endpoint["Parameters"]:
1111
+ param_elem = ET.SubElement(params, "Parameter", ID=self._sanitize_name(param["Name"]))
1112
+ ET.SubElement(param_elem, "Name").text = param["Name"]
1113
+ ET.SubElement(param_elem, "In").text = param["In"]
1114
+ ET.SubElement(param_elem, "Required").text = str(param.get("Required", False)).lower()
1115
+
1116
+ # Always include Type for all parameters
1117
+ ET.SubElement(param_elem, "Type").text = param.get("Type", "string")
1118
+
1119
+ # Handle common parameters
1120
+ if param.get("IsCommon", False):
1121
+ ET.SubElement(param_elem, "IsCommon").text = "true"
1122
+ ET.SubElement(param_elem, "CommonRef").text = param["CommonRef"]
1123
+ else:
1124
+ ET.SubElement(param_elem, "IsCommon").text = "false"
1125
+ ET.SubElement(param_elem, "Description").text = param.get("Description", "")
1126
+
1127
+ # Add response information
1128
+ if "ResponseModel" in endpoint:
1129
+ response = ET.SubElement(endpoint_elem, "Response")
1130
+ ET.SubElement(response, "Type").text = endpoint.get("ResponseType", "object")
1131
+ ET.SubElement(response, "ClassType").text = endpoint["ResponseModel"]
1132
+ elif "ResponseInlineSchema" in endpoint:
1133
+ response = ET.SubElement(endpoint_elem, "Response")
1134
+ ET.SubElement(response, "Type").text = endpoint.get("ResponseType", "object")
1135
+ ET.SubElement(response, "InlineSchema").text = json.dumps(endpoint["ResponseInlineSchema"])
1136
+
1137
+ # Add request body information
1138
+ if "RequestModel" in endpoint:
1139
+ request = ET.SubElement(endpoint_elem, "Request")
1140
+ ET.SubElement(request, "ClassType").text = endpoint["RequestModel"]
1141
+ elif "RequestInlineSchema" in endpoint:
1142
+ request = ET.SubElement(endpoint_elem, "Request")
1143
+ ET.SubElement(request, "InlineSchema").text = json.dumps(endpoint["RequestInlineSchema"])
1144
+
1145
+ # Convert to pretty XML string
1146
+ rough_string = ET.tostring(root, 'utf-8')
1147
+ reparsed = xml.dom.minidom.parseString(rough_string)
1148
+ return reparsed.toprettyxml(indent=" ")
1149
+
1150
+ def save_datasource_plugin_meta(self, output_path: str) -> None:
1151
+ """Save the datasource plugin meta JSON file"""
1152
+ meta_data = self.generate_datasource_plugin_meta()
1153
+
1154
+ with open(output_path, 'w') as f:
1155
+ json.dump(meta_data, f, indent=2)
1156
+
1157
+ print(f"Datasource plugin meta saved to: {output_path}")
1158
+
1159
+ def save_default_schema(self, output_path: str) -> None:
1160
+ """Save the default schema XML file"""
1161
+ schema_xml = self.generate_default_schema()
1162
+
1163
+ with open(output_path, 'w', encoding='utf-8') as f:
1164
+ f.write(schema_xml)
1165
+
1166
+ print(f"Default schema saved to: {output_path}")
1167
+
1168
+ def generate_files(self, output_dir: str = None) -> Tuple[str, str]:
1169
+ """Generate both the datasource plugin meta JSON and default schema XML files.
1170
+
1171
+ Args:
1172
+ output_dir: Optional directory to save the files (defaults to current directory)
1173
+
1174
+ Returns:
1175
+ Tuple of (meta_json_path, schema_xml_path)
1176
+ """
1177
+ if not self.api_info:
1178
+ self.extract_api_info()
1179
+
1180
+ # Set default output directory if not provided
1181
+ if not output_dir:
1182
+ output_dir = os.getcwd()
1183
+ else:
1184
+ # Create directory if it doesn't exist
1185
+ os.makedirs(output_dir, exist_ok=True)
1186
+
1187
+ # Generate filenames
1188
+ meta_filename = f"{self.api_name.lower()}_datasource_plugin_meta.json"
1189
+ schema_filename = f"{self.api_name.lower()}_default_schema.orx"
1190
+
1191
+ meta_path = os.path.join(output_dir, meta_filename)
1192
+ schema_path = os.path.join(output_dir, schema_filename)
1193
+
1194
+ # Save files
1195
+ self.save_datasource_plugin_meta(meta_path)
1196
+ self.save_default_schema(schema_path)
1197
+
1198
+ return (meta_path, schema_path)