Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow "anyOf" in param["schema"]["type"] in openapi31 #143

Merged
merged 5 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ repos:
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
exclude: >
(?x)(
tests/renderers/httpdomain/rendered
)
- id: check-docstring-first
- id: check-json
- id: check-yaml
Expand Down
22 changes: 16 additions & 6 deletions sphinxcontrib/openapi/openapi31.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,20 +297,30 @@ def _httpresource(
yield "{indent}{line}".format(**locals())
yield ""

def _get_type_from_schema(schema):
if "type" in schema.keys():
dtype = schema["type"]
else:
dtype = set()
for t in schema["anyOf"]:
if "format" in t.keys():
dtype.add(t["format"])
else:
dtype.add(t["type"])
return dtype

# print request's path params
for param in filter(lambda p: p["in"] == "path", parameters):
yield indent + ":param {type} {name}:".format(
type=param["schema"]["type"], name=param["name"]
)
type_ = _get_type_from_schema(param["schema"])
yield indent + ":param {type} {name}:".format(type=type_, name=param["name"])

for line in convert(param.get("description", "")).splitlines():
yield "{indent}{indent}{line}".format(**locals())

# print request's query params
for param in filter(lambda p: p["in"] == "query", parameters):
yield indent + ":query {type} {name}:".format(
type=param["schema"]["type"], name=param["name"]
)
type_ = _get_type_from_schema(param["schema"])
yield indent + ":query {type} {name}:".format(type=type_, name=param["name"])
for line in convert(param.get("description", "")).splitlines():
yield "{indent}{indent}{line}".format(**locals())
if param.get("required", False):
Expand Down
28 changes: 15 additions & 13 deletions sphinxcontrib/openapi/renderers/_httpdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@

import deepmerge
import docutils.parsers.rst.directives as directives
import sphinx_mdinclude
import requests
import sphinx.util.logging as logging
import sphinx_mdinclude

from sphinxcontrib.openapi import _lib2to3 as lib2to3
from sphinxcontrib.openapi.renderers import abc
from sphinxcontrib.openapi.schema_utils import example_from_schema


CaseInsensitiveDict = requests.structures.CaseInsensitiveDict


Expand Down Expand Up @@ -121,7 +120,10 @@ def _get_markers_from_object(oas_object, schema):
schema_type = f"{schema_type}:{schema['format']}"
elif schema.get("enum"):
schema_type = f"{schema_type}:enum"
markers.append(schema_type)
if isinstance(schema_type, list):
markers = schema_type
else:
markers.append(schema_type)
elif schema.get("enum"):
markers.append("enum")

Expand Down Expand Up @@ -274,18 +276,18 @@ def render_operation(self, endpoint, method, operation):
yield f".. http:{method}:: {endpoint}"

if operation.get("deprecated"):
yield f" :deprecated:"
yield f""
yield " :deprecated:"
yield ""

if operation.get("summary"):
yield f" **{operation['summary']}**"
yield f""
yield ""

if operation.get("description"):
yield from indented(
self._convert_markup(operation["description"]).strip().splitlines()
)
yield f""
yield ""

yield from indented(self.render_parameters(operation.get("parameters", [])))
if "requestBody" in operation:
Expand Down Expand Up @@ -370,11 +372,11 @@ def render_request_body_example(self, request_body, endpoint, method):
if not isinstance(example, str):
example = json.dumps(example, indent=2)

yield f".. sourcecode:: http"
yield f""
yield ".. sourcecode:: http"
yield ""
yield f" {method.upper()} {endpoint} HTTP/1.1"
yield f" Content-Type: {content_type}"
yield f""
yield ""
yield from indented(example.splitlines())

def render_responses(self, responses):
Expand Down Expand Up @@ -483,11 +485,11 @@ def render_response_example(self, media_type, status_code):
status_code = status_code.replace("XX", "00")
status_text = http.client.responses.get(int(status_code), "-")

yield f".. sourcecode:: http"
yield f""
yield ".. sourcecode:: http"
yield ""
yield f" HTTP/1.1 {status_code} {status_text}"
yield f" Content-Type: {content_type}"
yield f""
yield ""
yield from indented(example.splitlines())

def render_json_schema_description(self, schema, req_or_res):
Expand Down
5 changes: 0 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@


_testspecs_dir = pathlib.Path(os.path.dirname(__file__), "testspecs")
_testspecs_v2_dir = _testspecs_dir.joinpath("v2.0")
_testspecs_v3_dir = _testspecs_dir.joinpath("v3.0")

_testspecs = [str(path.relative_to(_testspecs_dir)) for path in _testspecs_dir.glob("*/*")]
_testspecs_v2 = [str(path.relative_to(_testspecs_dir)) for path in _testspecs_v2_dir.glob("*/*")]
_testspecs_v3 = [str(path.relative_to(_testspecs_dir)) for path in _testspecs_v3_dir.glob("*/*")]


def pytest_addoption(parser):
Expand Down
47 changes: 47 additions & 0 deletions tests/renderers/httpdomain/rendered/v3.1/issue-112.yaml.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.. http:get:: /users

**Get all users.**

:queryparam role:
:resjsonarr id:
The user ID.
:resjsonarrtype id: integer
:resjsonarr username:
The user name.
:resjsonarrtype username: string
:resjsonarr deleted:
Whether the user account has been deleted.
:resjsonarrtype deleted: boolean

:statuscode 200:
A list of all users.


.. http:get:: /users/{userID}

**Get a user by ID.**

:param userID:
:paramtype userID: string
:resjson id:
The user ID.
:resjsonobj id: integer
:resjson username:
The user name.
:resjsonobj username: string
:resjson bio:
A brief bio about the user.
:resjsonobj bio: string, null
:resjson deleted:
Whether the user account has been deleted.
:resjsonobj deleted: boolean
:resjson created_at:
The date the user account was created.
:resjsonobj created_at: string:date
:resjson deleted_at:
The date the user account was deleted.
:resjsonobj deleted_at: string:date

:statuscode 200:
The expected information about a user.

83 changes: 83 additions & 0 deletions tests/testspecs/v3.1/issue-112.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
openapi: "3.1.0"
info:
title: "Reproducer for issue #112"
version: 2.0.0
paths:
/users:
get:
summary: Get all users.
parameters:
- in: query
name: role
required: false
schema:
# this is one way to represent nullable types in OpenAPI
oneOf:
- type: "string"
enum: ["admin", "member", "reader"]
- type: "null"
responses:
"200":
description: A list of all users.
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
description: The user ID.
type: integer
username:
description: The user name.
type: string
deleted:
description: Whether the user account has been deleted.
type: boolean
default: false
/users/{userID}:
get:
summary: Get a user by ID.
parameters:
- in: path
name: userID
schema:
type: "string"
responses:
"200":
description: The expected information about a user.
content:
application/json:
schema:
type: object
properties:
id:
description: The user ID.
type: integer
username:
description: The user name.
type: string
bio:
description: A brief bio about the user.
# this is another way to represent nullable types in OpenAPI that also demonstrates that assertions are
# ignored for different primitive types
# https://github.com/OAI/OpenAPI-Specification/issues/3148
type: ["string", "null"]
maxLength: 255
deleted:
description: Whether the user account has been deleted.
type: boolean
default: false
created_at:
description: The date the user account was created.
type: string
format: date
deleted_at:
description: The date the user account was deleted.
# this is yet another slightly different way
anyOf:
- type: string
format: date
- type: null