diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 886c9c7..86a8c2a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [3.9, "3.10", "3.11", "3.12"] + python-version: [3.9, "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cf86b8..285a3ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2025-03-20 + +- Fix bug in `MARKDOWN` view for `servers` (displaying twice the `description` + instead of `description` and `url`). +- Fix bug happening when a `path item` includes properties that are not of kind + `operation item` (https://github.com/Neoteroi/mkdocs-plugins/issues/5). +- Add support for handling `parameters` defined on `path items` (common + parameters for all operation under a certain path). + Refer to the [`Path Item` specification](https://swagger.io/specification/#path-item-object). +- Fix bug happening when a parameter has a non-str `name` property. +- Add Python 3.13 to the build matrix. + ## [1.1.0] - 2025-01-18 - Add additionalProperties to Schema object, by @tyzhnenko. diff --git a/openapidocs/__init__.py b/openapidocs/__init__.py index 43d7d68..2c30038 100644 --- a/openapidocs/__init__.py +++ b/openapidocs/__init__.py @@ -1,2 +1,2 @@ -__version__ = "1.1.0" +__version__ = "1.1.1" VERSION = __version__ diff --git a/openapidocs/mk/v3/__init__.py b/openapidocs/mk/v3/__init__.py index 8c5a920..72a9145 100644 --- a/openapidocs/mk/v3/__init__.py +++ b/openapidocs/mk/v3/__init__.py @@ -178,6 +178,8 @@ def get_operations(self): paths = data["paths"] for path, path_item in paths.items(): + if not isinstance(path_item, dict): + continue tag = self.get_tag(path_item) or "" for operation in path_item.values(): @@ -186,11 +188,32 @@ def get_operations(self): operation["requestBody"] = self._resolve_opt_ref( operation["requestBody"] ) - - groups[tag].append((path, path_item)) + groups[tag].append((path, self._keep_operations(path_item))) return groups + def _keep_operations(self, path_item): + # discard dictionary keys that are not of dict type + + # if the path item defines common parameters, merge them into each operation: + # https://swagger.io/specification/#path-item-object + common_parameters = path_item.get("parameters", []) + # Note: we don't need to resolve $ref here, because they are resolved in + # get_parameters + + return { + key: self._merge_common_parameters(value, common_parameters) + for key, value in path_item.items() + if isinstance(value, dict) + } + + def _merge_common_parameters(self, operation, common_parameters): + if not common_parameters: + return operation + data = copy.deepcopy(operation) + data["parameters"] = common_parameters + data.get("parameters", []) + return data + def get_schemas(self): schemas = read_dict(self.doc, "components", "schemas") @@ -215,8 +238,12 @@ def get_tag(self, path_item) -> Optional[str]: """ single_tag: Optional[str] = None - for operation in path_item.values(): - tags = operation.get("tags") + for prop in path_item.values(): + if not isinstance(prop, dict): + # This property is not an operation; in this context we ignore it. + # See Path Item Object here: https://swagger.io/specification/ + continue + tags = prop.get("tags") if not tags: continue @@ -383,6 +410,11 @@ def _resolve_opt_ref(self, obj): return self.resolve_reference(obj) return obj + def _lower(self, obj): + if isinstance(obj, str): + return obj.lower() + return str(obj) + def get_parameters(self, operation) -> List[dict]: """ Returns a list of objects describing the input parameters for a given operation. @@ -397,7 +429,7 @@ def get_parameters(self, operation) -> List[dict]: param for param in sorted( parameters, - key=lambda x: x["name"].lower() if (x and "name" in x) else "", + key=lambda x: self._lower(x["name"]) if (x and "name" in x) else "", ) if param ] diff --git a/openapidocs/mk/v3/views_markdown/partial/servers.html b/openapidocs/mk/v3/views_markdown/partial/servers.html index 6a77694..28dd45a 100644 --- a/openapidocs/mk/v3/views_markdown/partial/servers.html +++ b/openapidocs/mk/v3/views_markdown/partial/servers.html @@ -3,7 +3,7 @@ {% with rows = [[texts.description, texts.url]] %} {%- for server in servers -%} -{%- set _ = rows.append([server.description, server.description]) -%} +{%- set _ = rows.append([server.description, server.url]) -%} {%- endfor -%} {{ rows | table }} {%- endwith -%} diff --git a/requirements.txt b/requirements.txt index d99937e..af58338 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,8 +26,7 @@ pathspec==0.11.2 platformdirs==4.0.0 pluggy==1.3.0 pycodestyle==2.11.1 -pydantic==2.5.1 -pydantic_core==2.14.3 +pydantic==2.10.6 pyflakes==3.1.0 Pygments==2.17.1 pytest==7.4.3 @@ -35,5 +34,5 @@ pytest-cov==4.1.0 PyYAML==6.0.1 rich==13.7.0 sniffio==1.3.0 -typing_extensions==4.8.0 +typing_extensions>=4.8.0 Werkzeug==3.0.1 diff --git a/tests/res/example1-output-plain.md b/tests/res/example1-output-plain.md index 035351c..b239b18 100644 --- a/tests/res/example1-output-plain.md +++ b/tests/res/example1-output-plain.md @@ -20,9 +20,9 @@ Optional multiline or single-line description in ## Servers -| Description | URL | -| ----------------- | ----------------- | -| Production server | Production server | +| Description | URL | +| ----------------- | ------------------------------------------- | +| Production server | https://www.neoteroi.xyz/software-center/v1 | ## Blobs diff --git a/tests/res/example8-openapi.yaml b/tests/res/example8-openapi.yaml new file mode 100644 index 0000000..a831c2b --- /dev/null +++ b/tests/res/example8-openapi.yaml @@ -0,0 +1,128 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: http://petstore.swagger.io/v1 + description: Petstore server +paths: + /pets: + summary: Everything about pets + description: This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). + parameters: + - name: X-Country + in: header + description: Country code. + required: false + schema: + type: string + default: PL + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + default: 100 + responses: + "200": + description: A paged array of pets + headers: + x-next: + description: A link to the next page of responses + schema: + type: string + content: + application/json: + schema: + $ref: "#/components/schemas/Pets" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + "201": + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + "200": + description: Expected response to a valid request + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: "#/components/schemas/Pet" + PetsIds: + type: array + items: + type: integer + format: int64 + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string diff --git a/tests/res/example8-output-plain.md b/tests/res/example8-output-plain.md new file mode 100644 index 0000000..4bcc5b9 --- /dev/null +++ b/tests/res/example8-output-plain.md @@ -0,0 +1,277 @@ + + +# Swagger Petstore 1.0.0 + + +**License:** MIT + + +## Servers + +| Description | URL | +| --------------- | ----------------------------- | +| Petstore server | http://petstore.swagger.io/v1 | + + +## pets + + + +### GET /pets +List all pets + +**Input parameters** + +| Parameter | In | Type | Default | Nullable | Description | +| --------- | ------ | ------- | ------- | -------- | ---------------------------------------------- | +| limit | query | integer | 100 | No | How many items to return at one time (max 100) | +| X-Country | header | string | PL | No | Country code. | + +### Response 200 OK + +**application/json** + +```json +[ + { + "id": 144, + "name": "string", + "tag": "string" + } +] +``` +_This example has been generated automatically from the schema and it is not +accurate. Refer to the schema for more information._ + + + +**Schema of the response body** + +```json +{ + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } +} +``` + + + +| Name | Description | Schema | +| ------ | ------------------------------------ | ------ | +| x-next | A link to the next page of responses | string | + +### Other responses + +**application/json** + +```json +{ + "code": 235, + "message": "string" +} +``` +_This example has been generated automatically from the schema and it is not +accurate. Refer to the schema for more information._ + + + +**Schema of the response body** + +```json +{ + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } +} +``` + + + +### POST /pets +Create a pet + +**Input parameters** + +| Parameter | In | Type | Default | Nullable | Description | +| --------- | ------ | ------ | ------- | -------- | ------------- | +| X-Country | header | string | PL | No | Country code. | + +### Response 201 Created + +### Other responses + +**application/json** + +```json +{ + "code": 154, + "message": "string" +} +``` +_This example has been generated automatically from the schema and it is not +accurate. Refer to the schema for more information._ + + + +**Schema of the response body** + +```json +{ + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } +} +``` + + + +### GET /pets/{petId} +Info for a specific pet + +**Input parameters** + +| Parameter | In | Type | Default | Nullable | Description | +| --------- | ---- | ------ | ------- | -------- | ----------------------------- | +| petId | path | string | | No | The id of the pet to retrieve | + +### Response 200 OK + +**application/json** + +```json +{ + "id": 258, + "name": "string", + "tag": "string" +} +``` +_This example has been generated automatically from the schema and it is not +accurate. Refer to the schema for more information._ + + + +**Schema of the response body** + +```json +{ + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } +} +``` + + + +### Other responses + +**application/json** + +```json +{ + "code": 182, + "message": "string" +} +``` +_This example has been generated automatically from the schema and it is not +accurate. Refer to the schema for more information._ + + + +**Schema of the response body** + +```json +{ + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } +} +``` + + + +--- +## Schemas + + +### Error + +| Name | Type | +| -- | -- | +| code | integer(int32) | +| message | string | + + + +### Pet + +| Name | Type | +| -- | -- | +| id | integer(int64) | +| name | string | +| tag | string | + + + +### Pets +Type: Array<[Pet](#pet)> + + + +### PetsIds +Type: Array<integer(int64)> + + diff --git a/tests/res/example8-output.md b/tests/res/example8-output.md new file mode 100644 index 0000000..309197d --- /dev/null +++ b/tests/res/example8-output.md @@ -0,0 +1,418 @@ + + +# Swagger Petstore 1.0.0 + + +
+ License: MIT +
+ +## Servers + + + + + + + + + + + + + + +
DescriptionURL
Petstore server + http://petstore.swagger.io/v1 +
+ +## pets + + +
+ +### GET /pets +List all pets + +**Input parameters** + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeDefaultNullableDescription
limitqueryinteger100NoHow many items to return at one time (max 100)
X-CountryheaderstringPLNoCountry code.
+ +

+ Response 200 OK +

+ +=== "application/json" + + + ```json + [ + { + "id": 171, + "name": "string", + "tag": "string" + } + ] + ``` + ⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information. + + + + ??? hint "Schema of the response body" + ```json + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + ``` + + +
+

Response headers

+ + + + + + + + + + + + + + + + +
NameDescriptionSchema
x-nextA link to the next page of responsesstring
+
+ + +

+ Other responses +

+ +=== "application/json" + + + ```json + { + "code": 206, + "message": "string" + } + ``` + ⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information. + + + + ??? hint "Schema of the response body" + ```json + { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + ``` + + + +
+ +### POST /pets +Create a pet + +**Input parameters** + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeDefaultNullableDescription
X-CountryheaderstringPLNoCountry code.
+ +

+ Response 201 Created +

+ +

+ Other responses +

+ +=== "application/json" + + + ```json + { + "code": 290, + "message": "string" + } + ``` + ⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information. + + + + ??? hint "Schema of the response body" + ```json + { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + ``` + + + + +
+ +### GET /pets/{petId} +Info for a specific pet + +**Input parameters** + + + + + + + + + + + + + + + + + + + + + + +
ParameterInTypeDefaultNullableDescription
petIdpathstringNoThe id of the pet to retrieve
+ +

+ Response 200 OK +

+ +=== "application/json" + + + ```json + { + "id": 112, + "name": "string", + "tag": "string" + } + ``` + ⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information. + + + + ??? hint "Schema of the response body" + ```json + { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + } + ``` + + + +

+ Other responses +

+ +=== "application/json" + + + ```json + { + "code": 175, + "message": "string" + } + ``` + ⚠️ This example has been generated automatically from the schema and it is not accurate. Refer to the schema for more information. + + + + ??? hint "Schema of the response body" + ```json + { + "type": "object", + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + ``` + + + + + + +--- +## Schemas + + +### Error + + + + + + + + + + + + + + + + + + +
NameType
codeinteger(int32)
messagestring
+ + + +### Pet + + + + + + + + + + + + + + + + + + + + + + +
NameType
idinteger(int64)
namestring
tagstring
+ + + +### Pets +Type: Array<Pet> + + + +### PetsIds +Type: Array<integer(int64)> + + diff --git a/tests/test_mk_v3.py b/tests/test_mk_v3.py index 7404d5e..0d44fd0 100644 --- a/tests/test_mk_v3.py +++ b/tests/test_mk_v3.py @@ -292,13 +292,16 @@ def test_object_example_handler_handles_missing_pros(): assert handler.get_example({}) == {} -@pytest.mark.parametrize("example_file", ["example6", "example7"]) +@pytest.mark.parametrize("example_file", ["example6", "example7", "example8"]) def test_v3_markdown_yaml(example_file): # example6 # https://github.com/Neoteroi/essentials-openapi/issues/21 # example7 # https://github.com/Neoteroi/essentials-openapi/issues/24 + + # example8 + # https://github.com/Neoteroi/mkdocs-plugins/issues/5#issuecomment-2741388516 example_file_name = f"{example_file}-openapi.yaml" data = get_file_yaml(example_file_name) expected_result = get_resource_file_content(f"{example_file}-output.md") @@ -310,3 +313,20 @@ def test_v3_markdown_yaml(example_file): html = handler.write() compatible_str(html, expected_result) + + +@pytest.mark.parametrize("example_file", ["example8"]) +def test_v3_markdown_yaml_plain_markdown(example_file): + # example8 + # https://github.com/Neoteroi/mkdocs-plugins/issues/5#issuecomment-2741388516 + example_file_name = f"{example_file}-openapi.yaml" + data = get_file_yaml(example_file_name) + expected_result = get_resource_file_content(f"{example_file}-output-plain.md") + + handler = OpenAPIV3DocumentationHandler( + data, source=get_resource_file_path(example_file_name), style="MARKDOWN" + ) + + html = handler.write() + + compatible_str(html, expected_result)