CI / Build Pip package (push) Successful in 7m57s
Operations could only inline schemas: the generated document had no components section, so #/components/... references dangled. OpenAPIMixin and generate_spec now accept a components mapping (merged verbatim into the document) and kaya.openapi exports schema_ref() to build #/components/schemas/<name> references. Component references are validated at generation time and unknown ones raise ValueError.
137 lines
4.6 KiB
Markdown
137 lines
4.6 KiB
Markdown
# kaya-openapi
|
|
|
|
Automatic [OpenAPI](https://www.openapis.org/) specification generation for the
|
|
[Kaya](https://github.com/woggioni/kaya) lightweight ASGI web framework.
|
|
|
|
The package provides an `OpenAPIMixin` that inspects a `KayaApp`'s routing
|
|
tree and serves:
|
|
|
|
- an OpenAPI 3.1 JSON document (default: `GET /openapi.json`)
|
|
- a Swagger UI page to browse it interactively (default: `GET /docs`)
|
|
|
|
## Usage
|
|
|
|
```python
|
|
from kaya.core import HttpContext, KayaApp
|
|
from kaya.openapi import OpenAPIMixin, operation
|
|
|
|
app = KayaApp(mixins=[OpenAPIMixin(title='My API', version='1.0.0')])
|
|
|
|
|
|
@app.GET('/users/${user_id:int}')
|
|
@operation(summary='Get a user',
|
|
tags=['users'],
|
|
responses={
|
|
200: {'description': 'The user'},
|
|
404: {'description': 'User not found'},
|
|
})
|
|
async def get_user(ctx: HttpContext, user_id: int) -> None:
|
|
...
|
|
```
|
|
|
|
Run the app with any ASGI/RSGI server and open `http://localhost:8000/docs`.
|
|
|
|
## How routes are mapped
|
|
|
|
- Static segments and parameters are converted to OpenAPI path templating:
|
|
- `/users/${user_id}` → `/users/{user_id}` (string path parameter)
|
|
- `/users/${user_id:int}` → `/users/{user_id}` (integer path parameter)
|
|
- Wildcard routes (`*`) are **skipped**: they cannot be expressed in OpenAPI
|
|
path syntax.
|
|
- Websocket routes are **skipped**: OpenAPI does not model websockets.
|
|
- Method-agnostic routes (registered with `app.route(path)` without methods)
|
|
are documented under **all** standard HTTP methods, since they respond to
|
|
all of them.
|
|
- The mixin's own endpoints are excluded from the document unless
|
|
`include_self=True`.
|
|
|
|
The document is generated on every request to the spec endpoint, so routes
|
|
registered after the mixin is applied are always included.
|
|
|
|
## Operation metadata
|
|
|
|
The `@operation` decorator attaches OpenAPI metadata to a route handler.
|
|
All fragments are plain dicts merged verbatim into the generated operation
|
|
object, so any valid OpenAPI 3.1 construct can be used:
|
|
|
|
```python
|
|
@operation(summary='...', # operation summary
|
|
description='...', # defaults to the handler docstring
|
|
tags=['users'],
|
|
operation_id='getUser',
|
|
request_body={...}, # OpenAPI requestBody object
|
|
responses={200: {...}}, # per-status-code response objects
|
|
parameters=[...], # extra/overriding parameter objects
|
|
deprecated=False,
|
|
hidden=False) # exclude from the document
|
|
```
|
|
|
|
`parameters` entries whose `name` and `in` match an auto-generated path
|
|
parameter override it; all others are appended.
|
|
|
|
## Reusable schemas (`components`)
|
|
|
|
Schemas can be defined once and referenced from operation fragments: pass a
|
|
`components` mapping to the mixin and use `schema_ref` in `@operation`.
|
|
|
|
```python
|
|
from kaya.openapi import OpenAPIMixin, operation, schema_ref
|
|
|
|
USER = {
|
|
'type': 'object',
|
|
'required': ['id'],
|
|
'properties': {'id': {'type': 'integer'}, 'name': {'type': 'string'}},
|
|
}
|
|
USER_PAGE = {
|
|
'type': 'object',
|
|
'properties': {'results': {'type': 'array', 'items': schema_ref('User')}},
|
|
}
|
|
|
|
app = KayaApp(mixins=[OpenAPIMixin(
|
|
title='My API',
|
|
version='1.0.0',
|
|
components={'schemas': {'User': USER, 'UserPage': USER_PAGE}},
|
|
)])
|
|
|
|
|
|
@app.GET('/users')
|
|
@operation(summary='List users',
|
|
responses={200: {
|
|
'description': 'A page of users',
|
|
'content': {'application/json': {'schema': schema_ref('UserPage')}},
|
|
}})
|
|
async def list_users(ctx: HttpContext) -> None:
|
|
...
|
|
```
|
|
|
|
`components` is merged verbatim (as a copy) into the generated document under
|
|
`components`, so Swagger UI lists the named schemas. Every
|
|
`#/components/<section>/<name>` reference — in operations or between
|
|
components — is validated when the document is generated: a missing section or
|
|
entry raises `ValueError`. Reflected references (`./other.json#/...`, absolute
|
|
URLs) are left alone.
|
|
|
|
## Configuration
|
|
|
|
```python
|
|
OpenAPIMixin(
|
|
title='My API', # info.title (required)
|
|
version='1.0.0', # info.version (required)
|
|
description='', # info.description
|
|
spec_path='/openapi.json', # where the JSON document is served
|
|
docs_path='/docs', # where Swagger UI is served
|
|
servers=[{'url': 'https://api.example.com'}],
|
|
openapi_version='3.1.0',
|
|
include_self=False, # include spec/docs endpoints in the document
|
|
components=None, # reusable OpenAPI components (e.g. schemas)
|
|
)
|
|
```
|
|
|
|
The document can also be generated programmatically without serving it:
|
|
|
|
```python
|
|
from kaya.openapi import generate_spec
|
|
|
|
spec = generate_spec(app, title='My API', version='1.0.0')
|
|
```
|