Skip to content

Commit

Permalink
Allow "anyOf" in param["schema"]["type"] in openapi31 (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
glatterf42 authored Oct 24, 2023
1 parent 1118513 commit 803da3f
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 24 deletions.
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

0 comments on commit 803da3f

Please sign in to comment.