swaggerizer 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- swaggerizer/__init__.py +3 -0
- swaggerizer/cli.py +121 -0
- swaggerizer/files/customizer.css +70 -0
- swaggerizer/files/makefile +17 -0
- swaggerizer/files/openapi.yaml +802 -0
- swaggerizer/helpers/__init__.py +0 -0
- swaggerizer/helpers/create_project.py +392 -0
- swaggerizer/helpers/html_exporter.py +112 -0
- swaggerizer/helpers/json2yaml_converter.py +44 -0
- swaggerizer/helpers/json_utils.py +11 -0
- swaggerizer/helpers/validator.py +67 -0
- swaggerizer/helpers/yaml2json_converter.py +45 -0
- swaggerizer-0.1.1.dist-info/METADATA +167 -0
- swaggerizer-0.1.1.dist-info/RECORD +18 -0
- swaggerizer-0.1.1.dist-info/WHEEL +5 -0
- swaggerizer-0.1.1.dist-info/entry_points.txt +2 -0
- swaggerizer-0.1.1.dist-info/licenses/LICENSE +201 -0
- swaggerizer-0.1.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import os, re, shutil, yaml
|
|
2
|
+
import sys, threading, time
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from importlib import resources
|
|
5
|
+
from ruamel.yaml import YAML
|
|
6
|
+
from ruamel.yaml.error import YAMLError
|
|
7
|
+
from .validator import validator
|
|
8
|
+
|
|
9
|
+
class Swaggerizer:
|
|
10
|
+
|
|
11
|
+
def __init__(self, input_file):
|
|
12
|
+
self.target_file_path = os.path.abspath(input_file)
|
|
13
|
+
self.target_file_name = os.path.basename(self.target_file_path)
|
|
14
|
+
self.target_dir = os.path.dirname(self.target_file_path)
|
|
15
|
+
self.show_progress = False
|
|
16
|
+
self.version = None
|
|
17
|
+
self.src_sub_dirs = dict.fromkeys(["intro", "endpoints", "components"])
|
|
18
|
+
self.project_sections = {
|
|
19
|
+
# Correlate project sections to top-level OpenAPI sections...
|
|
20
|
+
"intro": {
|
|
21
|
+
"basics": {
|
|
22
|
+
"v2": ["swagger", "info", "schemes", "host", "basePath", "externalDocs"],
|
|
23
|
+
"v3": ["openapi", "info", "servers", "externalDocs"]
|
|
24
|
+
},
|
|
25
|
+
"other": ["tags"]
|
|
26
|
+
},
|
|
27
|
+
"endpoints": "paths",
|
|
28
|
+
"components": {
|
|
29
|
+
"v2": ["definitions", "parameters", "responses", "securityDefinitions"],
|
|
30
|
+
"v3": "components"
|
|
31
|
+
}
|
|
32
|
+
# ... anything else will get added to "misc".
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def create(self):
|
|
37
|
+
if os.path.exists(self.target_file_path):
|
|
38
|
+
print(f"Found {self.target_file_path}.")
|
|
39
|
+
proceed = input("Create modularized OpenAPI project for this file? (Y/n) ").strip().lower()
|
|
40
|
+
else:
|
|
41
|
+
print(f"Could not find {self.target_file_path}.")
|
|
42
|
+
proceed = input("Create default modularized OpenAPI project in current directory? (Y/n) ").strip().lower()
|
|
43
|
+
|
|
44
|
+
if proceed in ["y", ""]:
|
|
45
|
+
print("\nCreating the project...")
|
|
46
|
+
self._create_project()
|
|
47
|
+
else:
|
|
48
|
+
print("\nCreate operation canceled.")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _create_project(self):
|
|
52
|
+
# Check for existing "openapi_src" directory
|
|
53
|
+
openapi_src_dir = os.path.join(self.target_dir, "openapi_src")
|
|
54
|
+
if os.path.exists(openapi_src_dir):
|
|
55
|
+
print(f"\nError: Source directory {openapi_src_dir} already exists.")
|
|
56
|
+
print(" To avoid overwrite, rename this directory or create the new project somewhere else.\n")
|
|
57
|
+
print("Aborting project creation.")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
# Validate the src file
|
|
61
|
+
if os.path.exists(self.target_file_path):
|
|
62
|
+
print("\n- Make sure input file is valid YAML", end=" ")
|
|
63
|
+
self.show_progress = True
|
|
64
|
+
progress_thread = threading.Thread(target=self._progress_tracker)
|
|
65
|
+
progress_thread.start()
|
|
66
|
+
try:
|
|
67
|
+
validator.validate_yaml(self.target_file_path)
|
|
68
|
+
finally:
|
|
69
|
+
self.show_progress = False
|
|
70
|
+
progress_thread.join()
|
|
71
|
+
print(" done")
|
|
72
|
+
|
|
73
|
+
# Create the project dir structure
|
|
74
|
+
print(f"- Create OpenAPI source directory: {openapi_src_dir}")
|
|
75
|
+
os.makedirs(openapi_src_dir)
|
|
76
|
+
self.src_sub_dirs.update({
|
|
77
|
+
"intro": os.path.join(openapi_src_dir, "0_intro"),
|
|
78
|
+
"endpoints": os.path.join(openapi_src_dir, "1_endpoints"),
|
|
79
|
+
"components": os.path.join(openapi_src_dir, "2_components")
|
|
80
|
+
})
|
|
81
|
+
for src_sub_dir in self.src_sub_dirs.values():
|
|
82
|
+
os.makedirs(src_sub_dir)
|
|
83
|
+
|
|
84
|
+
# Select the target file
|
|
85
|
+
if os.path.exists(self.target_file_path):
|
|
86
|
+
print(f"- Modularize OpenAPI content from file: {self.target_file_name}", end=" ")
|
|
87
|
+
else:
|
|
88
|
+
print("- Modularize OpenAPI content from the default 'pet store' file", end=" ")
|
|
89
|
+
self._copy_default_file(os.path.dirname(self.target_file_path), "openapi.yaml")
|
|
90
|
+
|
|
91
|
+
# Modularize the content...
|
|
92
|
+
self.show_progress = True
|
|
93
|
+
progress_thread = threading.Thread(target=self._progress_tracker)
|
|
94
|
+
progress_thread.start()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
self._extract_intro(self.src_sub_dirs["intro"])
|
|
98
|
+
self._extract_endpoints(self.src_sub_dirs["endpoints"])
|
|
99
|
+
if self.version == "v2":
|
|
100
|
+
self._extract_components_v2(self.src_sub_dirs["components"])
|
|
101
|
+
elif self.version == "v3":
|
|
102
|
+
self._extract_components_v3(self.src_sub_dirs["components"])
|
|
103
|
+
self._extract_misc(openapi_src_dir)
|
|
104
|
+
finally:
|
|
105
|
+
self.show_progress = False
|
|
106
|
+
progress_thread.join()
|
|
107
|
+
print(f" done (OpenAPI{self.version})")
|
|
108
|
+
|
|
109
|
+
# Add and update the makefile
|
|
110
|
+
print("- Add makefile to the source directory")
|
|
111
|
+
self._copy_default_file(openapi_src_dir, "makefile")
|
|
112
|
+
self._update_makefile(openapi_src_dir, self.src_sub_dirs["components"])
|
|
113
|
+
|
|
114
|
+
# Implement project structure creation and file organization here
|
|
115
|
+
print("\nProject creation complete!")
|
|
116
|
+
print("Read the docs @ https://swaggerizer.com")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _extract_intro(self, output_dir):
|
|
120
|
+
"""
|
|
121
|
+
Extracts OpenAPI intro sections as defined in self.project_sections["intro"]
|
|
122
|
+
and writes them to a series of files in src/intro.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
# Use ruamel to preserve order and spacing
|
|
126
|
+
yaml = YAML()
|
|
127
|
+
yaml.preserve_quotes = True
|
|
128
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
129
|
+
yaml.width = 4096
|
|
130
|
+
|
|
131
|
+
# Read the OpenAPI YAML input file
|
|
132
|
+
with open(self.target_file_path, 'r', encoding='utf-8') as input_file:
|
|
133
|
+
openapi_content = yaml.load(input_file)
|
|
134
|
+
|
|
135
|
+
# Determine the Swagger/OpenAPI version
|
|
136
|
+
if "swagger" in openapi_content:
|
|
137
|
+
self.version = "v2"
|
|
138
|
+
elif "openapi" in openapi_content:
|
|
139
|
+
self.version = "v3"
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError("Error: Missing 'openapi' or 'swagger' tag in OpenAPI input file!")
|
|
142
|
+
|
|
143
|
+
# Get the project sections
|
|
144
|
+
basics_sections = self.project_sections["intro"]["basics"][self.version]
|
|
145
|
+
other_sections = self.project_sections["intro"]["other"]
|
|
146
|
+
|
|
147
|
+
# Write intro/basics.yaml
|
|
148
|
+
basics = {k: openapi_content[k] for k in basics_sections if k in openapi_content}
|
|
149
|
+
if basics:
|
|
150
|
+
output_file = os.path.join(output_dir, "basics.yaml")
|
|
151
|
+
with open(output_file, "w", encoding="utf-8") as file:
|
|
152
|
+
yaml.dump(basics, file)
|
|
153
|
+
|
|
154
|
+
# Write other sections to intro/<name>.yaml
|
|
155
|
+
for section in other_sections:
|
|
156
|
+
if section in openapi_content:
|
|
157
|
+
output_data = {section: openapi_content[section]}
|
|
158
|
+
output_file = os.path.join(output_dir, f"{section}.yaml")
|
|
159
|
+
with open(output_file, "w", encoding="utf-8") as file:
|
|
160
|
+
yaml.dump(output_data, file)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _extract_endpoints(self, output_dir):
|
|
164
|
+
"""
|
|
165
|
+
Extracts the endpoints from the OpenAPI "paths" section and writes them to
|
|
166
|
+
a series of files in src/endpoints named by the tag group.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
# Use ruamel to preserve order and indentation
|
|
170
|
+
yaml = YAML()
|
|
171
|
+
yaml.preserve_quotes = True
|
|
172
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
173
|
+
|
|
174
|
+
# Read the OpenAPI YAML input file
|
|
175
|
+
with open(self.target_file_path, 'r', encoding='utf-8') as file:
|
|
176
|
+
openapi_content = yaml.load(file)
|
|
177
|
+
|
|
178
|
+
# Make sure the "paths" section exists
|
|
179
|
+
if 'paths' not in openapi_content:
|
|
180
|
+
raise ValueError("Error: Missing 'paths' section in OpenAPI input file!")
|
|
181
|
+
|
|
182
|
+
# Group endpoints by tag
|
|
183
|
+
tag_endpoints = defaultdict(dict)
|
|
184
|
+
for path, methods in openapi_content['paths'].items():
|
|
185
|
+
for method, details in methods.items():
|
|
186
|
+
if 'tags' in details:
|
|
187
|
+
# store tagged endpoints by tag name
|
|
188
|
+
for tag in details['tags']:
|
|
189
|
+
tag = tag.replace(" ", "_").lower()
|
|
190
|
+
if path not in tag_endpoints[tag]:
|
|
191
|
+
tag_endpoints[tag][path] = {}
|
|
192
|
+
tag_endpoints[tag][path][method] = details
|
|
193
|
+
else:
|
|
194
|
+
# store untagged endpoints as "untagged"
|
|
195
|
+
tag_endpoints['untagged'][path] = tag_endpoints['untagged'].get(path, {})
|
|
196
|
+
tag_endpoints['untagged'][path][method] = details
|
|
197
|
+
|
|
198
|
+
# Write endpoints to the 'src/endpoints' dir
|
|
199
|
+
for tag, endpoints in tag_endpoints.items():
|
|
200
|
+
output_file_path = os.path.join(output_dir, f"{tag}.yaml")
|
|
201
|
+
|
|
202
|
+
# write to the output file
|
|
203
|
+
with open(output_file_path, 'w', encoding='utf-8') as output_file:
|
|
204
|
+
yaml.dump({'paths': endpoints}, output_file)
|
|
205
|
+
|
|
206
|
+
# remove 'paths:' label from output
|
|
207
|
+
self._chop_first_line(output_file_path)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _extract_components_v2(self, output_dir):
|
|
211
|
+
"""
|
|
212
|
+
Extracts the sections from the OpenAPI v2 top-level components sections and writes
|
|
213
|
+
them to a series of files in src/components named by the section.
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
# Use ruamel to preserve order and indentation
|
|
217
|
+
yaml = YAML()
|
|
218
|
+
yaml.preserve_quotes = True
|
|
219
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
220
|
+
|
|
221
|
+
# Read the OpenAPI YAML input file
|
|
222
|
+
with open(self.target_file_path, 'r', encoding='utf-8') as file:
|
|
223
|
+
openapi_content = yaml.load(file)
|
|
224
|
+
|
|
225
|
+
# Get the list of v2 components
|
|
226
|
+
components_sections = self.project_sections["components"]["v2"]
|
|
227
|
+
|
|
228
|
+
# Write sections to the 'src/components' dir
|
|
229
|
+
for section_name in components_sections:
|
|
230
|
+
if section_name in openapi_content:
|
|
231
|
+
output_file = os.path.join(output_dir, f"{section_name}.yaml")
|
|
232
|
+
|
|
233
|
+
# include both section name and content
|
|
234
|
+
section_data = {section_name: openapi_content[section_name]}
|
|
235
|
+
|
|
236
|
+
# write to the output file
|
|
237
|
+
with open(output_file, 'w', encoding='utf-8') as out_file:
|
|
238
|
+
yaml.dump(section_data, out_file)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _extract_components_v3(self, output_dir):
|
|
242
|
+
"""
|
|
243
|
+
Extracts the sections from the OpenAPI v3 "components" section and writes them
|
|
244
|
+
to a series of files in src/components named by the section.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
# Use ruamel to preserve order and indentation
|
|
248
|
+
yaml = YAML()
|
|
249
|
+
yaml.preserve_quotes = True
|
|
250
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
251
|
+
|
|
252
|
+
# Read the OpenAPI YAML input file
|
|
253
|
+
with open(self.target_file_path, 'r', encoding='utf-8') as file:
|
|
254
|
+
openapi_content = yaml.load(file)
|
|
255
|
+
|
|
256
|
+
# Make sure the "components" section exists
|
|
257
|
+
if 'components' not in openapi_content:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# Write components to the 'src/components' dir
|
|
261
|
+
components = openapi_content['components']
|
|
262
|
+
for section_name, section_content in components.items():
|
|
263
|
+
output_file = os.path.join(output_dir, f"{section_name}.yaml")
|
|
264
|
+
yaml.indent(mapping=4, sequence=4, offset=2)
|
|
265
|
+
|
|
266
|
+
# write to the output file
|
|
267
|
+
with open(output_file, 'w', encoding='utf-8') as out_file:
|
|
268
|
+
yaml.dump({'components': {section_name: section_content}}, out_file)
|
|
269
|
+
|
|
270
|
+
# remove 'components:' label from output
|
|
271
|
+
self._chop_first_line(output_file)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _extract_misc(self, openapi_src_dir):
|
|
275
|
+
"""
|
|
276
|
+
Extracts any other top-level OpenAPI sections besides the ones defined in
|
|
277
|
+
self.project_sections. If present, writes them to src/misc.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
# Use ruamel to preserve order and spacing
|
|
281
|
+
yaml = YAML()
|
|
282
|
+
yaml.preserve_quotes = True
|
|
283
|
+
yaml.indent(mapping=2, sequence=4, offset=2)
|
|
284
|
+
yaml.width = 4096
|
|
285
|
+
|
|
286
|
+
# Read the OpenAPI YAML input file
|
|
287
|
+
with open(self.target_file_path, 'r', encoding='utf-8') as input_file:
|
|
288
|
+
openapi_content = yaml.load(input_file)
|
|
289
|
+
|
|
290
|
+
# Assign all the main sections to a set
|
|
291
|
+
assigned_keys = self._collect_keys(self.project_sections)
|
|
292
|
+
|
|
293
|
+
# Find any unassigned keys in the OpenAPI file
|
|
294
|
+
misc_keys = sorted(set(openapi_content.keys()) - assigned_keys)
|
|
295
|
+
|
|
296
|
+
# If there misc keys, save and write them to src/misc dir
|
|
297
|
+
if misc_keys:
|
|
298
|
+
# save keys and create 'src/misc' dir
|
|
299
|
+
self.src_sub_dirs["misc"] = os.path.join(openapi_src_dir, "3_misc")
|
|
300
|
+
os.makedirs(self.src_sub_dirs["misc"])
|
|
301
|
+
self.project_sections["misc"] = misc_keys
|
|
302
|
+
|
|
303
|
+
# Write each section to 'misc/<name>.yaml'
|
|
304
|
+
for name in misc_keys:
|
|
305
|
+
output_data = {name: openapi_content[name]}
|
|
306
|
+
output_file = os.path.join(self.src_sub_dirs["misc"], f"{name}.yaml")
|
|
307
|
+
with open(output_file, "w", encoding="utf-8") as file:
|
|
308
|
+
yaml.dump(output_data, file)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _copy_default_file(self, target_dir, filename):
|
|
312
|
+
"""
|
|
313
|
+
Copy default template files from swaggerizer/files into target_dir.
|
|
314
|
+
"""
|
|
315
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
316
|
+
src = resources.files("swaggerizer.files").joinpath(filename)
|
|
317
|
+
dest = os.path.join(target_dir, filename)
|
|
318
|
+
shutil.copy(src, dest)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _update_makefile(self, src_dir, components_dir):
|
|
322
|
+
"""
|
|
323
|
+
Add target to makefile to build final OpenAPI yaml file.
|
|
324
|
+
If no misc sections present, remove that part.
|
|
325
|
+
"""
|
|
326
|
+
|
|
327
|
+
# Read default makefile content
|
|
328
|
+
make_file = os.path.join(src_dir, 'makefile')
|
|
329
|
+
with open(make_file, 'r', encoding='utf-8') as file:
|
|
330
|
+
content = file.read()
|
|
331
|
+
|
|
332
|
+
# Replace placeholder build target
|
|
333
|
+
content = re.sub(r'FINAL_BUILD_TARGET', self.target_file_name, content)
|
|
334
|
+
|
|
335
|
+
# If no components files, remove components build
|
|
336
|
+
if not os.listdir(components_dir):
|
|
337
|
+
content = re.sub(r'^.+components[:/].+\n', '', content, flags=re.MULTILINE)
|
|
338
|
+
elif self.version == "v2":
|
|
339
|
+
content = re.sub(r'^.+components[:].+\n', '', content, flags=re.MULTILINE)
|
|
340
|
+
|
|
341
|
+
# If no misc sections, remove misc build
|
|
342
|
+
if not self.project_sections.get("misc"):
|
|
343
|
+
content = re.sub(r'^.+_misc[/].+\n', '', content, flags=re.MULTILINE)
|
|
344
|
+
|
|
345
|
+
# Write final version of makefile
|
|
346
|
+
with open(make_file, 'w', encoding='utf-8') as file:
|
|
347
|
+
file.write(content)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _collect_keys(self, data):
|
|
351
|
+
"""
|
|
352
|
+
Iterate nested data structure to get all the keys.
|
|
353
|
+
"""
|
|
354
|
+
keys = set()
|
|
355
|
+
if isinstance(data, str):
|
|
356
|
+
keys.add(data)
|
|
357
|
+
elif isinstance(data, list):
|
|
358
|
+
keys.update(data)
|
|
359
|
+
elif isinstance(data, dict):
|
|
360
|
+
for val in data.values():
|
|
361
|
+
if isinstance(val, dict) and self.version in val:
|
|
362
|
+
keys.update(self._collect_keys(val[self.version]))
|
|
363
|
+
else:
|
|
364
|
+
keys.update(self._collect_keys(val))
|
|
365
|
+
return keys
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _chop_first_line(self, file_path):
|
|
369
|
+
"""
|
|
370
|
+
Remove 1st line of output file during OpenAPI content modularization.
|
|
371
|
+
"""
|
|
372
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
373
|
+
lines = file.readlines()
|
|
374
|
+
|
|
375
|
+
if len(lines) > 1:
|
|
376
|
+
# ouput everything after line 1
|
|
377
|
+
with open(file_path, 'w', encoding='utf-8') as file:
|
|
378
|
+
file.writelines(lines[1:])
|
|
379
|
+
else:
|
|
380
|
+
# output empty file
|
|
381
|
+
open(file_path, 'w').close()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _progress_tracker(self):
|
|
385
|
+
"""
|
|
386
|
+
Progress bar for long modularize operations.
|
|
387
|
+
"""
|
|
388
|
+
while self.show_progress:
|
|
389
|
+
sys.stdout.write(".")
|
|
390
|
+
sys.stdout.flush()
|
|
391
|
+
time.sleep(2)
|
|
392
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os, yaml, json
|
|
2
|
+
from .json_utils import json_serializer
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Helper class for converting Swagger YAML files into HTML format.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
exporter = HTMLExporter("input.yaml", "output.html")
|
|
9
|
+
exporter.export()
|
|
10
|
+
|
|
11
|
+
# Copyright 2024 JR Halterlein
|
|
12
|
+
# Licensed under the Apache License, Version 2.0.
|
|
13
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for the full text.
|
|
14
|
+
#
|
|
15
|
+
# This file is based on
|
|
16
|
+
# https://github.com/swagger-api/swagger-ui/blob/4f1772f6544699bc748299bd65f7ae2112777abc/dist/index.html
|
|
17
|
+
# (Copyright 2017 SmartBear Software, Licensed under Apache 2.0)
|
|
18
|
+
# https://github.com/yousan/swagger-yaml-to-html/blob/master/swagger-yaml-to-html.py
|
|
19
|
+
# (Copyright 2017 Otto Seiskari, Licensed under Apache 2.0)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class HTMLExporter:
|
|
24
|
+
TEMPLATE = """
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head>
|
|
28
|
+
<meta charset="UTF-8">
|
|
29
|
+
<title>Swagger UI</title>
|
|
30
|
+
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
|
|
31
|
+
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.28.0/swagger-ui.css" >
|
|
32
|
+
<link rel="stylesheet" type="text/css" href="customizer.css" >
|
|
33
|
+
<style>
|
|
34
|
+
html
|
|
35
|
+
{
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
overflow: -moz-scrollbars-vertical;
|
|
38
|
+
overflow-y: scroll;
|
|
39
|
+
}
|
|
40
|
+
*,
|
|
41
|
+
*:before,
|
|
42
|
+
*:after
|
|
43
|
+
{
|
|
44
|
+
box-sizing: inherit;
|
|
45
|
+
}
|
|
46
|
+
body {
|
|
47
|
+
margin:0;
|
|
48
|
+
background: #fafafa;
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div id="swagger-ui"></div>
|
|
54
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.28.0/swagger-ui-bundle.js"> </script>
|
|
55
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.28.0/swagger-ui-standalone-preset.js"> </script>
|
|
56
|
+
<script src="customizer.js"> </script>
|
|
57
|
+
<script>
|
|
58
|
+
window.onload = function() {
|
|
59
|
+
var spec = %s;
|
|
60
|
+
// Build a system
|
|
61
|
+
const ui = SwaggerUIBundle({
|
|
62
|
+
spec: spec,
|
|
63
|
+
dom_id: '#swagger-ui',
|
|
64
|
+
deepLinking: true,
|
|
65
|
+
presets: [
|
|
66
|
+
SwaggerUIBundle.presets.apis,
|
|
67
|
+
SwaggerUIStandalonePreset
|
|
68
|
+
],
|
|
69
|
+
plugins: [
|
|
70
|
+
SwaggerUIBundle.plugins.DownloadUrl
|
|
71
|
+
],
|
|
72
|
+
layout: "StandaloneLayout"
|
|
73
|
+
})
|
|
74
|
+
window.ui = ui
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def __init__(self, input_file, output_file):
|
|
83
|
+
self.input_file = input_file
|
|
84
|
+
if output_file:
|
|
85
|
+
self.output_file = output_file
|
|
86
|
+
else:
|
|
87
|
+
# Derive from input filename
|
|
88
|
+
input_dir = os.path.dirname(input_file)
|
|
89
|
+
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
|
90
|
+
self.output_file = os.path.join(input_dir, f"{base_name}.html")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def export(self):
|
|
94
|
+
try:
|
|
95
|
+
# Read the YAML file
|
|
96
|
+
with open(self.input_file, 'r', encoding='utf-8') as infile:
|
|
97
|
+
spec = yaml.load(infile, Loader=yaml.FullLoader)
|
|
98
|
+
|
|
99
|
+
# Generate the HTML content
|
|
100
|
+
html_content = self.TEMPLATE % json.dumps(spec, default=json_serializer)
|
|
101
|
+
|
|
102
|
+
# Write the HTML content to the output file
|
|
103
|
+
with open(self.output_file, 'w', encoding='utf-8') as outfile:
|
|
104
|
+
outfile.write(html_content)
|
|
105
|
+
|
|
106
|
+
print(f"Exported HTML to {self.output_file}")
|
|
107
|
+
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
print(f"Error: Input file '{self.input_file}' not found.")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"Error during export: {e}")
|
|
112
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os, json, yaml
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Helper class for converting JSON files into YAML format.
|
|
5
|
+
|
|
6
|
+
Example usage:
|
|
7
|
+
converter = json2YamlConverter(args.input, args.output)
|
|
8
|
+
converter.convert()
|
|
9
|
+
|
|
10
|
+
# Copyright 2024 JR Halterlein
|
|
11
|
+
# Licensed under the Apache License, Version 2.0.
|
|
12
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for the full text.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class json2YamlConverter:
|
|
17
|
+
def __init__(self, input_file, output_file):
|
|
18
|
+
self.input_file = input_file
|
|
19
|
+
if output_file:
|
|
20
|
+
self.output_file = output_file
|
|
21
|
+
else:
|
|
22
|
+
# Derive from input filename
|
|
23
|
+
input_dir = os.path.dirname(input_file)
|
|
24
|
+
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
|
25
|
+
self.output_file = os.path.join(input_dir, f"{base_name}.yaml")
|
|
26
|
+
|
|
27
|
+
def convert(self):
|
|
28
|
+
try:
|
|
29
|
+
with open(self.input_file, 'r') as json_file:
|
|
30
|
+
data = json.load(json_file)
|
|
31
|
+
with open(self.output_file, 'w') as yaml_file:
|
|
32
|
+
yaml.dump(data, yaml_file, default_flow_style=False)
|
|
33
|
+
|
|
34
|
+
print(f"Converted JSON file {self.input_file} to YAML file {self.output_file}.")
|
|
35
|
+
|
|
36
|
+
except FileNotFoundError:
|
|
37
|
+
print(f"Error: Input file '{self.input_file}' not found.")
|
|
38
|
+
except json.JSONDecodeError as e:
|
|
39
|
+
print(f"Error: Failed to parse JSON file. Details: {e}")
|
|
40
|
+
except PermissionError:
|
|
41
|
+
print(f"Error: Permission denied when accessing '{self.output_file}'.")
|
|
42
|
+
except Exception as e:
|
|
43
|
+
print(f"An unexpected error occurred: {e}")
|
|
44
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
|
|
3
|
+
def json_serializer(obj):
|
|
4
|
+
"""
|
|
5
|
+
Custom JSON serializer for non-serializable objects.
|
|
6
|
+
Converts datetime.date and datetime.datetime to ISO 8601 strings.
|
|
7
|
+
"""
|
|
8
|
+
if isinstance(obj, (date, datetime)):
|
|
9
|
+
return obj.isoformat()
|
|
10
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
|
11
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import os, sys, json
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
from ruamel.yaml import YAML
|
|
4
|
+
from ruamel.yaml.error import YAMLError
|
|
5
|
+
|
|
6
|
+
class validator:
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def validate(file_path, file_type):
|
|
10
|
+
"""
|
|
11
|
+
Validate a file as YAML or JSON, depending on file_type ('yaml'/'yml' or 'json').
|
|
12
|
+
Confirms the file exists before validation.
|
|
13
|
+
"""
|
|
14
|
+
if not os.path.isfile(file_path):
|
|
15
|
+
print(f"\nInput file does not exist: {file_path}\n")
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
type = (file_type or "").strip().lower()
|
|
19
|
+
if not type:
|
|
20
|
+
file, file_type = os.path.splitext(file_path)
|
|
21
|
+
type = file_type.lstrip(".").lower()
|
|
22
|
+
|
|
23
|
+
if type in ("yaml", "yml"):
|
|
24
|
+
validator.validate_yaml(file_path)
|
|
25
|
+
print(f"\n{file_path} is valid YAML.\n")
|
|
26
|
+
elif type == "json":
|
|
27
|
+
validator.validate_json(file_path)
|
|
28
|
+
print(f"\n{file_path} is valid JSON.\n")
|
|
29
|
+
else:
|
|
30
|
+
print(f"\nUnsupported file type: {type!r}. Expected 'yaml'/'yml' or 'json'.\n")
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def validate_yaml(file_path):
|
|
36
|
+
"""
|
|
37
|
+
Validate structural YAML in src file.
|
|
38
|
+
Note: Does not validate OpenAPI spec.
|
|
39
|
+
"""
|
|
40
|
+
yaml = YAML()
|
|
41
|
+
|
|
42
|
+
# Validate YAML structure
|
|
43
|
+
try:
|
|
44
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
45
|
+
file_content = yaml.load(file)
|
|
46
|
+
except YAMLError as e:
|
|
47
|
+
print(f"\nCould not parse YAML for input file: {file_path}")
|
|
48
|
+
print(f"Error details:\n\n{e}")
|
|
49
|
+
print("Fix YAML structural issues and try again.")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def validate_json(file_path):
|
|
55
|
+
"""
|
|
56
|
+
Validate structural JSON in src file.
|
|
57
|
+
Note: Does not validate OpenAPI spec.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
61
|
+
file_content = json.load(file)
|
|
62
|
+
except JSONDecodeError as e:
|
|
63
|
+
print(f"\nCould not parse JSON for input file: {file_path}")
|
|
64
|
+
print(f"Error details:\n\n{e}")
|
|
65
|
+
print("Fix JSON structural issues and try again.")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import os, json, yaml
|
|
2
|
+
from .json_utils import json_serializer
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Helper class for converting YAML files into JSON format.
|
|
6
|
+
|
|
7
|
+
Example usage:
|
|
8
|
+
converter = yaml2JsonConverter(args.input, args.output)
|
|
9
|
+
converter.convert()
|
|
10
|
+
|
|
11
|
+
# Copyright 2024 JR Halterlein
|
|
12
|
+
# Licensed under the Apache License, Version 2.0.
|
|
13
|
+
# See http://www.apache.org/licenses/LICENSE-2.0 for the full text.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class yaml2JsonConverter:
|
|
18
|
+
def __init__(self, input_file, output_file):
|
|
19
|
+
self.input_file = input_file
|
|
20
|
+
if output_file:
|
|
21
|
+
self.output_file = output_file
|
|
22
|
+
else:
|
|
23
|
+
# Derive from input filename
|
|
24
|
+
input_dir = os.path.dirname(input_file)
|
|
25
|
+
base_name = os.path.splitext(os.path.basename(input_file))[0]
|
|
26
|
+
self.output_file = os.path.join(input_dir, f"{base_name}.json")
|
|
27
|
+
|
|
28
|
+
def convert(self):
|
|
29
|
+
try:
|
|
30
|
+
with open(self.input_file, 'r') as yaml_file:
|
|
31
|
+
data = yaml.safe_load(yaml_file)
|
|
32
|
+
with open(self.output_file, 'w') as json_file:
|
|
33
|
+
json.dump(data, json_file, indent=4, default=json_serializer)
|
|
34
|
+
|
|
35
|
+
print(f"Converted YAML file {self.input_file} to JSON file {self.output_file}")
|
|
36
|
+
|
|
37
|
+
except FileNotFoundError:
|
|
38
|
+
print(f"Error: Input file '{self.input_file}' not found.")
|
|
39
|
+
except yaml.YAMLError as e:
|
|
40
|
+
print(f"Error: Failed to parse YAML file. Details: {e}")
|
|
41
|
+
except PermissionError:
|
|
42
|
+
print(f"Error: Permission denied when accessing '{self.output_file}'.")
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"An unexpected error occurred: {e}")
|
|
45
|
+
|