Skip to content

Commit 55782ef

Browse files
authored
Merge pull request #13 from kasimlyee/lyee/docs
Added OpenAPI Documentation System
2 parents 995ccc0 + cb25a95 commit 55782ef

21 files changed

+4252
-6
lines changed

JSWEB_OPENAPI_GUIDE.md

Lines changed: 983 additions & 0 deletions
Large diffs are not rendered by default.

docs/JSWEB_OPENAPI_GUIDE.md

Lines changed: 812 additions & 0 deletions
Large diffs are not rendered by default.

examples/example_api.py

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
"""
2+
Example jsweb API with OpenAPI Documentation
3+
4+
This example demonstrates:
5+
- DTO definitions with validation
6+
- NestJS-style route documentation
7+
- Automatic OpenAPI generation
8+
- Swagger UI and ReDoc interfaces
9+
"""
10+
11+
from jsweb import JsWebApp, Blueprint
12+
try:
13+
from jsweb.response import JSONResponse as json
14+
except ImportError:
15+
# Fallback for this example
16+
def json(data, status=200):
17+
import json as json_module
18+
return {
19+
'status': status,
20+
'headers': {'Content-Type': 'application/json'},
21+
'body': json_module.dumps(data)
22+
}
23+
24+
# Import DTO system
25+
from jsweb.dto import JswebBaseModel, Field, validator
26+
27+
# Import documentation decorators
28+
from jsweb.docs import (
29+
setup_openapi_docs,
30+
api_operation,
31+
api_response,
32+
api_body,
33+
api_query,
34+
api_tags,
35+
api_security,
36+
add_security_scheme
37+
)
38+
39+
# ============================================================================
40+
# DTOs (Data Transfer Objects)
41+
# ============================================================================
42+
43+
class UserDto(JswebBaseModel):
44+
"""User data model."""
45+
id: int = Field(description="User ID", example=1)
46+
name: str = Field(
47+
description="User's full name",
48+
min_length=1,
49+
max_length=100,
50+
example="John Doe"
51+
)
52+
email: str = Field(
53+
description="Email address",
54+
pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$',
55+
56+
)
57+
age: int = Field(
58+
ge=0,
59+
le=150,
60+
description="User's age in years",
61+
example=30
62+
)
63+
role: str = Field(
64+
description="User role",
65+
example="user"
66+
)
67+
68+
69+
class CreateUserDto(JswebBaseModel):
70+
"""DTO for creating a new user."""
71+
name: str = Field(
72+
min_length=1,
73+
max_length=100,
74+
description="User name"
75+
)
76+
email: str = Field(
77+
pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$',
78+
description="Valid email address"
79+
)
80+
age: int = Field(ge=0, le=150, description="User age")
81+
82+
@validator('name')
83+
@classmethod
84+
def validate_name(cls, value):
85+
"""Validate and normalize name."""
86+
if value.strip() == '':
87+
raise ValueError('Name cannot be empty')
88+
return value.strip()
89+
90+
@validator('email')
91+
@classmethod
92+
def validate_email(cls, value):
93+
"""Normalize email to lowercase."""
94+
return value.lower()
95+
96+
97+
class UpdateUserDto(JswebBaseModel):
98+
"""DTO for updating a user (all fields optional)."""
99+
name: str = Field(None, min_length=1, max_length=100)
100+
email: str = Field(None, pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$')
101+
age: int = Field(None, ge=0, le=150)
102+
103+
104+
class ErrorDto(JswebBaseModel):
105+
"""Error response model."""
106+
error: str = Field(description="Error message")
107+
code: int = Field(description="Error code", example=400)
108+
details: dict = Field(default_factory=dict, description="Additional error details")
109+
110+
111+
# ============================================================================
112+
# API Blueprint
113+
# ============================================================================
114+
115+
api = Blueprint('api', url_prefix='/api')
116+
117+
# Mock database
118+
USERS_DB = [
119+
{"id": 1, "name": "John Doe", "email": "[email protected]", "age": 30, "role": "admin"},
120+
{"id": 2, "name": "Jane Smith", "email": "[email protected]", "age": 25, "role": "user"},
121+
{"id": 3, "name": "Bob Johnson", "email": "[email protected]", "age": 35, "role": "user"},
122+
]
123+
124+
125+
@api.route("/users", methods=["GET"])
126+
@api_tags("Users")
127+
@api_operation(
128+
summary="List all users",
129+
description="Returns a paginated list of all users in the system. "
130+
"You can filter by role and search by name."
131+
)
132+
@api_query('page', type=int, required=False, description="Page number (starts at 1)", example=1)
133+
@api_query('limit', type=int, required=False, description="Items per page (max 100)", example=10)
134+
@api_query('role', type=str, required=False, description="Filter by role (admin, user)")
135+
@api_query('search', type=str, required=False, description="Search in user names")
136+
@api_response(200, UserDto, description="List of users")
137+
async def list_users(req):
138+
"""Get all users with optional filtering."""
139+
page = int(req.query_params.get('page', 1)) if hasattr(req, 'query_params') else 1
140+
limit = int(req.query_params.get('limit', 10)) if hasattr(req, 'query_params') else 10
141+
142+
users = USERS_DB.copy()
143+
return json({
144+
"users": users,
145+
"page": page,
146+
"limit": limit,
147+
"total": len(users)
148+
})
149+
150+
151+
@api.route("/users/<int:user_id>", methods=["GET"])
152+
@api_tags("Users")
153+
@api_operation(
154+
summary="Get user by ID",
155+
description="Retrieves a single user by their unique ID"
156+
)
157+
@api_response(200, UserDto, description="User found")
158+
@api_response(404, ErrorDto, description="User not found")
159+
async def get_user(req, user_id):
160+
"""Get a specific user by ID."""
161+
user = next((u for u in USERS_DB if u["id"] == user_id), None)
162+
163+
if not user:
164+
return json({
165+
"error": "User not found",
166+
"code": 404
167+
}, status=404)
168+
169+
return json(user)
170+
171+
172+
@api.route("/users", methods=["POST"])
173+
@api_tags("Users")
174+
@api_operation(
175+
summary="Create new user",
176+
description="Creates a new user in the system. "
177+
"Email must be unique."
178+
)
179+
@api_body(CreateUserDto, description="User data")
180+
@api_response(201, UserDto, description="User created successfully")
181+
@api_response(400, ErrorDto, description="Validation error")
182+
@api_response(409, ErrorDto, description="Email already exists")
183+
async def create_user(req):
184+
"""Create a new user."""
185+
try:
186+
data = await req.json() if hasattr(req, 'json') else {}
187+
188+
# Validate with DTO (in production)
189+
# user_dto = CreateUserDto(**data)
190+
191+
# Check email uniqueness
192+
if any(u["email"] == data.get("email") for u in USERS_DB):
193+
return json({
194+
"error": "Email already exists",
195+
"code": 409
196+
}, status=409)
197+
198+
# Create user
199+
new_user = {
200+
"id": len(USERS_DB) + 1,
201+
"role": "user",
202+
**data
203+
}
204+
USERS_DB.append(new_user)
205+
206+
return json(new_user, status=201)
207+
208+
except Exception as e:
209+
return json({
210+
"error": str(e),
211+
"code": 400
212+
}, status=400)
213+
214+
215+
@api.route("/users/<int:user_id>", methods=["PATCH"])
216+
@api_tags("Users")
217+
@api_operation(
218+
summary="Update user",
219+
description="Updates an existing user. All fields are optional."
220+
)
221+
@api_body(UpdateUserDto, description="Fields to update")
222+
@api_response(200, UserDto, description="User updated successfully")
223+
@api_response(404, ErrorDto, description="User not found")
224+
@api_response(400, ErrorDto, description="Validation error")
225+
async def update_user(req, user_id):
226+
"""Update an existing user."""
227+
user = next((u for u in USERS_DB if u["id"] == user_id), None)
228+
229+
if not user:
230+
return json({
231+
"error": "User not found",
232+
"code": 404
233+
}, status=404)
234+
235+
try:
236+
data = await req.json() if hasattr(req, 'json') else {}
237+
user.update(data)
238+
return json(user)
239+
except Exception as e:
240+
return json({
241+
"error": str(e),
242+
"code": 400
243+
}, status=400)
244+
245+
246+
@api.route("/users/<int:user_id>", methods=["DELETE"])
247+
@api_tags("Users")
248+
@api_operation(
249+
summary="Delete user",
250+
description="Permanently deletes a user from the system"
251+
)
252+
@api_response(204, description="User deleted successfully")
253+
@api_response(404, ErrorDto, description="User not found")
254+
@api_security("bearer_auth")
255+
async def delete_user(req, user_id):
256+
"""Delete a user (requires authentication)."""
257+
global USERS_DB
258+
user = next((u for u in USERS_DB if u["id"] == user_id), None)
259+
260+
if not user:
261+
return json({
262+
"error": "User not found",
263+
"code": 404
264+
}, status=404)
265+
266+
USERS_DB = [u for u in USERS_DB if u["id"] != user_id]
267+
return json(None, status=204)
268+
269+
270+
@api.route("/health", methods=["GET"])
271+
@api_tags("System")
272+
@api_operation(
273+
summary="Health check",
274+
description="Check if the API is running"
275+
)
276+
@api_response(200, description="API is healthy")
277+
async def health_check(req):
278+
"""Check API health."""
279+
return json({"status": "ok", "version": "1.0.0"})
280+
281+
282+
# ============================================================================
283+
# App Setup
284+
# ============================================================================
285+
286+
def create_app():
287+
"""Create and configure the jsweb application."""
288+
app = JsWebApp(__name__)
289+
290+
# Register blueprints FIRST
291+
app.register_blueprint(api)
292+
293+
# Setup OpenAPI documentation (MUST be after blueprint registration!)
294+
setup_openapi_docs(
295+
app,
296+
title="User Management API",
297+
version="1.0.0",
298+
description="""
299+
# User Management API
300+
301+
A simple RESTful API for managing users, built with jsweb framework.
302+
303+
## Features
304+
305+
- ✅ CRUD operations for users
306+
- ✅ Input validation with Pydantic
307+
- ✅ Automatic OpenAPI documentation
308+
- ✅ JWT authentication support
309+
- ✅ Search and filtering
310+
311+
## Authentication
312+
313+
Some endpoints require a Bearer token. Use the **Authorize** button above to add your token.
314+
""",
315+
contact={
316+
"name": "API Support",
317+
"email": "[email protected]",
318+
"url": "https://example.com/support"
319+
},
320+
license_info={
321+
"name": "MIT",
322+
"url": "https://opensource.org/licenses/MIT"
323+
},
324+
tags=[
325+
{
326+
"name": "Users",
327+
"description": "User management operations"
328+
},
329+
{
330+
"name": "System",
331+
"description": "System health and status"
332+
}
333+
],
334+
security_schemes={
335+
"bearer_auth": {
336+
"type": "http",
337+
"scheme": "bearer",
338+
"bearerFormat": "JWT",
339+
"description": "Enter your JWT token"
340+
}
341+
}
342+
)
343+
344+
return app
345+
346+
347+
# ============================================================================
348+
# Run
349+
# ============================================================================
350+
351+
if __name__ == "__main__":
352+
from jsweb.server import run
353+
354+
app = create_app()
355+
356+
print("\n" + "="*60)
357+
print("[*] Example API with OpenAPI Documentation")
358+
print("="*60)
359+
PORT = 8001
360+
print(f"\nStarting server on http://localhost:{PORT}")
361+
print("\nAvailable endpoints:")
362+
print(f" > Swagger UI: http://localhost:{PORT}/docs")
363+
print(f" > ReDoc: http://localhost:{PORT}/redoc")
364+
print(f" > OpenAPI JSON: http://localhost:{PORT}/openapi.json")
365+
print("\nPress Ctrl+C to stop")
366+
print("="*60 + "\n")
367+
368+
# Note: reload doesn't work when passing app directly
369+
run(app, host="127.0.0.1", port=PORT, reload=False)

0 commit comments

Comments
 (0)