|
| 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 | + |
| 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