Skip to content

Commit 893d572

Browse files
committed
add validation and improve ref resolving
1 parent 4c9ae3c commit 893d572

File tree

4 files changed

+110
-8
lines changed

4 files changed

+110
-8
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ pip install dist/serverless_openapi_generator-1.0.0-py3-none-any.whl
3434

3535
Once installed, you can use the `openapi-gen` command-line tool.
3636

37+
### Running from GitHub
38+
39+
You can also run the tool directly from GitHub using `uvx`:
40+
```bash
41+
uvx --from git+https://github.com/tkfoss/python-serverless-openapi-documentation.git openapi-gen path/to/your/serverless.yml openapi.json
42+
```
43+
For more information on running tools with `uv`, see the [official documentation](https://docs.astral.sh/uv/guides/tools/#running-tools).
44+
3745
**To Run:**
3846
```bash
3947
openapi-gen path/to/your/serverless.yml openapi.json --openApiVersion 3.0.3
@@ -46,6 +54,21 @@ serverless_yml_path Path to the serverless.yml file. (Required)
4654
output_file_path Path to the output OpenAPI JSON file. (Required)
4755
--openApiVersion The OpenAPI version to generate for. Default: 3.0.3
4856
--pre-hook Path to a Python script to run before generation.
57+
--validate Validate the generated OpenAPI spec.
58+
```
59+
60+
### Validation
61+
62+
This tool also includes a script to validate an OpenAPI specification file against the OpenAPI 3.0.3 specification.
63+
64+
**To Run:**
65+
```bash
66+
openapi-validate path/to/your/openapi.json
67+
```
68+
69+
You can also use the `--validate` flag with the `openapi-gen` command to automatically validate the generated spec:
70+
```bash
71+
openapi-gen path/to/your/serverless.yml openapi.json --validate
4972
```
5073

5174
### Configuration

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ dependencies = [
2121
"requests",
2222
"referencing",
2323
"jsonschema-spec",
24+
"openapi-spec-validator",
2425
]
2526

2627
[tool.setuptools.packages.find]
2728
where = ["src"]
2829

2930
[project.scripts]
3031
openapi-gen = "serverless_openapi_generator.openapi_generator:main"
32+
openapi-validate = "openapi_spec_validator.__main__:main"

src/serverless_openapi_generator/openapi_generator.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,13 @@ def create_media_type_object(self, models):
274274
else:
275275
# Fallback for inline schemas, though we'll focus on named models
276276
continue
277-
278-
media_type_obj[media_type] = {
279-
'schema': {
280-
'$ref': f"#/components/schemas/{model_name}"
277+
278+
if model_name in self.open_api['components']['schemas']:
279+
media_type_obj[media_type] = {
280+
'schema': {
281+
'$ref': f"#/components/schemas/{model_name}"
282+
}
281283
}
282-
}
283284
return media_type_obj
284285

285286
def create_request_body(self, request_body_doc):
@@ -391,6 +392,7 @@ def main():
391392
parser.add_argument('output_file_path', type=str, help='Path to the output OpenAPI JSON file')
392393
parser.add_argument('--openApiVersion', type=str, default='3.0.3', help='OpenAPI version to use')
393394
parser.add_argument('--pre-hook', type=str, help='Path to a Python script to run before generation')
395+
parser.add_argument('--validate', action='store_true', help='Validate the generated OpenAPI spec')
394396
args = parser.parse_args()
395397

396398
# Execute the pre-hook script if provided
@@ -426,5 +428,14 @@ def main():
426428
except IOError as e:
427429
print(f"Error writing to output file: {e}")
428430

431+
if args.validate:
432+
from openapi_spec_validator import validate
433+
print("Validating generated spec...")
434+
try:
435+
validate(open_api_spec)
436+
print("Validation successful.")
437+
except Exception as e:
438+
print(f"Validation failed: {e}")
439+
429440
if __name__ == '__main__':
430441
main()

src/serverless_openapi_generator/schema_handler.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,67 @@ def create_schema(self, name, schema_definition):
8787
self.open_api['components']['schemas'][name] = final_schema
8888
return f"#/components/schemas/{name}"
8989

90+
def _clean_schema(self, schema):
91+
if isinstance(schema, dict):
92+
# Replace $defs with definitions and update refs
93+
if '$defs' in schema:
94+
if 'definitions' not in schema:
95+
schema['definitions'] = {}
96+
schema['definitions'].update(schema.pop('$defs'))
97+
98+
def update_refs(node):
99+
if isinstance(node, dict):
100+
if '$ref' in node and node['$ref'].startswith('#/$defs/'):
101+
node['$ref'] = node['$ref'].replace('#/$defs/', '#/definitions/')
102+
for key, value in node.items():
103+
update_refs(value)
104+
elif isinstance(node, list):
105+
for item in node:
106+
update_refs(item)
107+
update_refs(schema)
108+
109+
# Replace const with enum
110+
if 'const' in schema:
111+
schema['enum'] = [schema.pop('const')]
112+
113+
# Remove propertyNames
114+
if 'propertyNames' in schema:
115+
del schema['propertyNames']
116+
117+
# Handle nullable
118+
if 'anyOf' in schema:
119+
is_nullable = False
120+
new_any_of = []
121+
for item in schema['anyOf']:
122+
if isinstance(item, dict) and item.get('type') == 'null':
123+
is_nullable = True
124+
else:
125+
new_any_of.append(item)
126+
127+
if is_nullable:
128+
for item in new_any_of:
129+
if isinstance(item, dict):
130+
item['nullable'] = True
131+
132+
if not new_any_of:
133+
del schema['anyOf']
134+
schema['nullable'] = True
135+
elif len(new_any_of) == 1:
136+
for key, value in new_any_of[0].items():
137+
if key not in schema:
138+
schema[key] = value
139+
del schema['anyOf']
140+
if is_nullable:
141+
schema['nullable'] = True
142+
else:
143+
schema['anyOf'] = new_any_of
144+
145+
# Recurse
146+
return {k: self._clean_schema(v) for k, v in schema.items()}
147+
elif isinstance(schema, list):
148+
return [self._clean_schema(item) for item in schema]
149+
return schema
150+
90151
def _resolve_schema_references(self, schema):
91152
schema = self._resolve_file_references(schema)
92153
if not isinstance(schema, dict):
@@ -100,10 +161,12 @@ def _resolve_schema_references(self, schema):
100161
else:
101162
return schema
102163

164+
schema = self._clean_schema(schema)
165+
103166
registry = Registry()
104167
if "definitions" in schema:
105168
for name, sub_schema in schema["definitions"].items():
106-
resource = Resource.from_contents(sub_schema, default_specification=DRAFT4)
169+
resource = Resource.from_contents(self._clean_schema(sub_schema), default_specification=DRAFT4)
107170
registry = registry.with_resource(f"#/definitions/{name}", resource)
108171

109172
main_resource = Resource.from_contents(schema, default_specification=DRAFT4)
@@ -118,8 +181,11 @@ def _resolve_schema_references(self, schema):
118181
def _recursive_dereference(self, node, resolver):
119182
if isinstance(node, dict):
120183
if "$ref" in node:
121-
resolved = resolver.lookup(node["$ref"])
122-
return self._recursive_dereference(resolved.contents, resolver)
184+
try:
185+
resolved = resolver.lookup(node["$ref"])
186+
return self._recursive_dereference(resolved.contents, resolver)
187+
except Exception:
188+
return node
123189
return {k: self._recursive_dereference(v, resolver) for k, v in node.items()}
124190
elif isinstance(node, list):
125191
return [self._recursive_dereference(item, resolver) for item in node]

0 commit comments

Comments
 (0)