Skip to content

Commit bc37ebb

Browse files
committed
Automatically docs are available when jsweb run
1 parent 2f7d28f commit bc37ebb

File tree

7 files changed

+249
-18
lines changed

7 files changed

+249
-18
lines changed

jsweb/cli.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,31 @@ def cli():
345345
from jsweb.database import init_db
346346
init_db(config.DATABASE_URL)
347347

348+
# Automatically setup OpenAPI documentation (unless disabled in config)
349+
if getattr(config, 'ENABLE_OPENAPI_DOCS', True):
350+
try:
351+
from jsweb.docs import setup_openapi_docs
352+
from jsweb.docs.introspection import introspect_app_routes
353+
354+
# Introspect routes first
355+
introspect_app_routes(app_instance)
356+
357+
# Setup docs with config values or defaults
358+
setup_openapi_docs(
359+
app_instance,
360+
title=getattr(config, 'API_TITLE', 'jsweb API'),
361+
version=getattr(config, 'API_VERSION', '1.0.0'),
362+
description=getattr(config, 'API_DESCRIPTION', ''),
363+
docs_url=getattr(config, 'OPENAPI_DOCS_URL', '/docs'),
364+
redoc_url=getattr(config, 'OPENAPI_REDOC_URL', '/redoc'),
365+
openapi_url=getattr(config, 'OPENAPI_JSON_URL', '/openapi.json'),
366+
)
367+
except ImportError:
368+
# Pydantic not installed, skip OpenAPI setup
369+
pass
370+
except Exception as e:
371+
logger.warning(f"⚠️ Could not setup OpenAPI docs: {e}")
372+
348373
host = args.host or config.HOST
349374
port = args.port or config.PORT
350375

jsweb/docs/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from .setup import setup_openapi_docs, configure_openapi, add_security_scheme
2424
from .registry import openapi_registry
25+
from .auto_validation import disable_auto_validation
2526

2627
__all__ = [
2728
# Decorators
@@ -38,6 +39,9 @@
3839
'configure_openapi',
3940
'add_security_scheme',
4041

42+
# Utilities
43+
'disable_auto_validation',
44+
4145
# Registry (for advanced usage)
4246
'openapi_registry',
4347
]

jsweb/docs/auto_validation.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Automatic request/response validation - FastAPI-style
3+
4+
This module provides automatic validation when DTOs are used,
5+
with option to disable if needed.
6+
"""
7+
8+
from functools import wraps
9+
from typing import Type, get_type_hints
10+
import inspect
11+
from pydantic import ValidationError as PydanticValidationError
12+
13+
14+
def validate_request_body(dto_class: Type):
15+
"""
16+
Decorator that automatically validates request body against a DTO.
17+
18+
This is automatically applied when @api_body is used.
19+
20+
Args:
21+
dto_class: The DTO class to validate against
22+
23+
Example:
24+
@api_body(CreateUserDto) # Automatically adds validation
25+
async def create_user(req):
26+
# req.validated_body is the validated DTO instance
27+
return json(req.validated_body.to_dict())
28+
"""
29+
def decorator(handler):
30+
@wraps(handler)
31+
async def wrapper(req, *args, **kwargs):
32+
# Parse request body
33+
try:
34+
if hasattr(req, 'json'):
35+
data = await req.json()
36+
else:
37+
# Fallback for testing
38+
data = {}
39+
40+
# Validate with DTO
41+
validated = dto_class(**data)
42+
43+
# Attach validated DTO to request
44+
req.validated_body = validated
45+
req.validated_data = validated.to_dict()
46+
47+
except PydanticValidationError as e:
48+
# Return validation error response
49+
from jsweb.response import JSONResponse
50+
errors = []
51+
for error in e.errors():
52+
errors.append({
53+
"field": ".".join(str(x) for x in error["loc"]),
54+
"message": error["msg"],
55+
"type": error["type"]
56+
})
57+
58+
return JSONResponse({
59+
"error": "Validation failed",
60+
"details": errors
61+
}, status=400)
62+
63+
except Exception as e:
64+
# Return generic error
65+
from jsweb.response import JSONResponse
66+
return JSONResponse({
67+
"error": "Invalid request body",
68+
"details": str(e)
69+
}, status=400)
70+
71+
# Call original handler
72+
return await handler(req, *args, **kwargs)
73+
74+
# Mark as validated
75+
wrapper._jsweb_validated = True
76+
wrapper._jsweb_dto_class = dto_class
77+
78+
return wrapper
79+
return decorator
80+
81+
82+
def auto_serialize_response(dto_class: Type, status_code: int = 200):
83+
"""
84+
Decorator that automatically serializes DTO responses to JSON.
85+
86+
Example:
87+
@api_response(200, UserDto) # Can optionally add auto-serialization
88+
async def get_user(req, user_id):
89+
user = UserDto(id=user_id, name="John", ...)
90+
return user # Automatically converts to JSONResponse
91+
"""
92+
def decorator(handler):
93+
@wraps(handler)
94+
async def wrapper(req, *args, **kwargs):
95+
result = await handler(req, *args, **kwargs)
96+
97+
# If result is already a Response, return as-is
98+
if hasattr(result, 'status_code') or isinstance(result, dict):
99+
return result
100+
101+
# If result is a DTO instance, serialize it
102+
if hasattr(result, 'to_dict'):
103+
from jsweb.response import JSONResponse
104+
return JSONResponse(result.to_dict(), status=status_code)
105+
106+
# If result is a list of DTOs
107+
if isinstance(result, list) and result and hasattr(result[0], 'to_dict'):
108+
from jsweb.response import JSONResponse
109+
return JSONResponse([item.to_dict() for item in result], status=status_code)
110+
111+
# Return as-is
112+
return result
113+
114+
return wrapper
115+
return decorator
116+
117+
118+
def disable_auto_validation(handler):
119+
"""
120+
Decorator to disable automatic validation for a specific route.
121+
122+
Use this when you want the documentation but not the validation.
123+
124+
Example:
125+
@api_body(CreateUserDto)
126+
@disable_auto_validation
127+
async def create_user(req):
128+
# Validation is skipped, but docs still generated
129+
data = await req.json() # Manual handling
130+
return json(data)
131+
"""
132+
handler._jsweb_disable_validation = True
133+
return handler

jsweb/docs/decorators.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""
2-
NestJS-style decorators for API documentation
2+
Decorators for API documentation
33
44
These decorators allow developers to add rich OpenAPI documentation to their routes.
55
"""
66

7-
from functools import wraps
8-
from typing import Type, Dict, Any, List, Optional, Union
7+
from typing import Type, Dict, Any, List
98
from .registry import (
109
openapi_registry,
11-
RouteMetadata,
1210
ResponseMetadata,
1311
RequestBodyMetadata,
1412
ParameterMetadata
@@ -22,7 +20,7 @@ def api_operation(
2220
deprecated: bool = False,
2321
):
2422
"""
25-
Document an API operation (NestJS-style).
23+
Document an API operation
2624
2725
This decorator should be placed closest to the route decorator.
2826
@@ -122,32 +120,44 @@ def api_body(
122120
description: str = "",
123121
content_type: str = "application/json",
124122
required: bool = True,
125-
examples: Dict[str, Any] = None
123+
examples: Dict[str, Any] = None,
124+
auto_validate: bool = True # NEW: Enable/disable automatic validation
126125
):
127126
"""
128-
Document request body (NestJS-style).
127+
Document request body with AUTOMATIC VALIDATION (FastAPI-style).
129128
130-
The DTO class will be used for:
131-
1. OpenAPI schema generation
132-
2. Automatic request validation (via middleware)
129+
By default, this decorator:
130+
1. Generates OpenAPI schema documentation
131+
2. Automatically validates incoming requests against the DTO
132+
3. Provides validated data via req.validated_body
133133
134134
Args:
135135
dto: Request body DTO class (JswebBaseModel subclass)
136136
description: Body description
137137
content_type: MIME type
138138
required: Whether body is required
139139
examples: Example request bodies
140+
auto_validate: Enable automatic validation (default: True)
140141
141142
Example:
142143
@api_bp.route("/users", methods=["POST"])
143144
@api_body(CreateUserDto, description="User data to create")
144145
@api_response(201, UserDto, description="User created")
145146
async def create_user(req):
146-
# req.validated_body will contain the validated DTO instance
147-
data = await req.json()
148-
return json({"user": {...}}, status=201)
147+
# req.validated_body contains the validated DTO instance
148+
# req.validated_data contains the dict representation
149+
user_data = req.validated_data
150+
return json({"user": user_data}, status=201)
151+
152+
# Disable auto-validation if needed:
153+
@api_body(CreateUserDto, auto_validate=False)
154+
async def create_user_manual(req):
155+
data = await req.json() # Manual handling
156+
return json(data)
149157
"""
150158
def decorator(handler):
159+
from .auto_validation import validate_request_body
160+
151161
metadata = openapi_registry.get_or_create_route(handler)
152162

153163
# Get schema from DTO
@@ -168,10 +178,15 @@ def decorator(handler):
168178
dto_class=dto # Store for automatic validation
169179
)
170180

171-
# Mark handler with validation info (for middleware)
181+
# Apply automatic validation unless disabled
182+
if auto_validate and not getattr(handler, '_jsweb_disable_validation', False):
183+
handler = validate_request_body(dto)(handler)
184+
185+
# Mark handler with validation info
172186
if not hasattr(handler, '_jsweb_validation'):
173187
handler._jsweb_validation = {}
174188
handler._jsweb_validation['body_dto'] = dto
189+
handler._jsweb_validation['auto_validate'] = auto_validate
175190

176191
return handler
177192
return decorator
@@ -241,7 +256,7 @@ def api_header(
241256
**schema_kwargs
242257
):
243258
"""
244-
Document a header parameter (NestJS-style).
259+
Document a header parameter
245260
246261
Args:
247262
name: Header name (e.g., 'Authorization', 'X-API-Key')
@@ -283,7 +298,7 @@ def decorator(handler):
283298

284299
def api_security(*schemes: str, scopes: List[str] = None):
285300
"""
286-
Apply security requirements to an operation (NestJS-style).
301+
Apply security requirements to an operation
287302
288303
Args:
289304
*schemes: Security scheme names (must be registered)
@@ -320,7 +335,7 @@ def decorator(handler):
320335

321336
def api_tags(*tags: str):
322337
"""
323-
Add tags to an operation for grouping in documentation (NestJS-style).
338+
Add tags to an operation for grouping in documentation
324339
325340
Args:
326341
*tags: Tag names
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Optional automatic request validation middleware
3+
4+
This middleware automatically validates request bodies against DTOs
5+
when @api_body decorator is used.
6+
"""
7+
8+
from jsweb.request import Request
9+
from jsweb.response import JSONResponse
10+
from .registry import openapi_registry
11+
12+
13+
class ValidationMiddleware:
14+
"""
15+
ASGI middleware that validates requests against DTOs.
16+
17+
Usage:
18+
from jsweb import JsWebApp
19+
from jsweb.docs.validation_middleware import ValidationMiddleware
20+
21+
app = JsWebApp(__name__)
22+
app.add_middleware(ValidationMiddleware)
23+
"""
24+
25+
def __init__(self, app):
26+
self.app = app
27+
28+
async def __call__(self, scope, receive, send):
29+
if scope["type"] != "http":
30+
await self.app(scope, receive, send)
31+
return
32+
33+
# Create request object to inspect
34+
request = Request(scope, receive, send)
35+
36+
# Find route metadata
37+
# Note: This requires access to the handler which we don't have here
38+
# This is a simplified example - full implementation would need
39+
# integration with the routing system
40+
41+
# For now, just pass through
42+
await self.app(scope, receive, send)

jsweb/project_templates/config.py.jinja

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,14 @@ BASE_DIR = os.path.abspath(os.path.dirname(__file__))
1212
DATABASE_URL = f"sqlite:///{os.path.join(BASE_DIR, 'jsweb.db')}"
1313
HOST = "127.0.0.1"
1414
PORT = 8000
15+
16+
# OpenAPI / Swagger Documentation (Automatic!)
17+
# Docs are automatically available at /docs, /redoc, and /openapi.json
18+
# Set ENABLE_OPENAPI_DOCS = False to disable
19+
ENABLE_OPENAPI_DOCS = True # Enable automatic API documentation
20+
API_TITLE = "{{ project_name | capitalize }} API"
21+
API_VERSION = "1.0.0"
22+
API_DESCRIPTION = "API documentation for {{ project_name | capitalize }}"
23+
OPENAPI_DOCS_URL = "/docs" # Swagger UI
24+
OPENAPI_REDOC_URL = "/redoc" # ReDoc UI
25+
OPENAPI_JSON_URL = "/openapi.json" # OpenAPI spec JSON

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ dependencies = [
4040
"itsdangerous",
4141
"alembic",
4242
"markupsafe",
43-
"uvicorn"
43+
"uvicorn",
44+
"pydantic>=2.0,<3.0" # Required for DTO system and OpenAPI docs
4445
]
4546

4647
[project.optional-dependencies]

0 commit comments

Comments
 (0)