@corbat-tech/coding-standards-mcp 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -337
- package/dist/agent.d.ts +5 -6
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +95 -217
- package/dist/agent.js.map +1 -1
- package/dist/analysis/code-analyzer.d.ts +44 -0
- package/dist/analysis/code-analyzer.d.ts.map +1 -0
- package/dist/analysis/code-analyzer.js +528 -0
- package/dist/analysis/code-analyzer.js.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +112 -0
- package/dist/errors.js.map +1 -0
- package/dist/guardrails.d.ts +35 -0
- package/dist/guardrails.d.ts.map +1 -0
- package/dist/guardrails.js +303 -0
- package/dist/guardrails.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +36 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +63 -0
- package/dist/logger.js.map +1 -0
- package/dist/metrics.d.ts +40 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +97 -0
- package/dist/metrics.js.map +1 -0
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.d.ts.map +1 -1
- package/dist/profiles.js +239 -108
- package/dist/profiles.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/tools/definitions.d.ts +143 -0
- package/dist/tools/definitions.d.ts.map +1 -0
- package/dist/tools/definitions.js +229 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/handlers/get-context.d.ts +12 -0
- package/dist/tools/handlers/get-context.d.ts.map +1 -0
- package/dist/tools/handlers/get-context.js +233 -0
- package/dist/tools/handlers/get-context.js.map +1 -0
- package/dist/tools/handlers/health.d.ts +11 -0
- package/dist/tools/handlers/health.d.ts.map +1 -0
- package/dist/tools/handlers/health.js +57 -0
- package/dist/tools/handlers/health.js.map +1 -0
- package/dist/tools/handlers/index.d.ts +12 -0
- package/dist/tools/handlers/index.d.ts.map +1 -0
- package/dist/tools/handlers/index.js +12 -0
- package/dist/tools/handlers/index.js.map +1 -0
- package/dist/tools/handlers/init.d.ts +12 -0
- package/dist/tools/handlers/init.d.ts.map +1 -0
- package/dist/tools/handlers/init.js +102 -0
- package/dist/tools/handlers/init.js.map +1 -0
- package/dist/tools/handlers/profiles.d.ts +11 -0
- package/dist/tools/handlers/profiles.d.ts.map +1 -0
- package/dist/tools/handlers/profiles.js +25 -0
- package/dist/tools/handlers/profiles.js.map +1 -0
- package/dist/tools/handlers/search.d.ts +12 -0
- package/dist/tools/handlers/search.d.ts.map +1 -0
- package/dist/tools/handlers/search.js +58 -0
- package/dist/tools/handlers/search.js.map +1 -0
- package/dist/tools/handlers/validate.d.ts +15 -0
- package/dist/tools/handlers/validate.d.ts.map +1 -0
- package/dist/tools/handlers/validate.js +71 -0
- package/dist/tools/handlers/validate.js.map +1 -0
- package/dist/tools/handlers/verify.d.ts +38 -0
- package/dist/tools/handlers/verify.d.ts.map +1 -0
- package/dist/tools/handlers/verify.js +172 -0
- package/dist/tools/handlers/verify.js.map +1 -0
- package/dist/tools/index.d.ts +22 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +75 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/schemas.d.ts +29 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +20 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools.js +2 -2
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +141 -71
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +92 -40
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/profiles/examples/microservice-kafka.yaml +122 -0
- package/profiles/examples/startup-fast.yaml +67 -0
- package/profiles/examples/strict-enterprise.yaml +62 -0
- package/profiles/templates/angular.yaml +614 -0
- package/profiles/templates/csharp-dotnet.yaml +529 -0
- package/profiles/templates/flutter.yaml +547 -0
- package/profiles/templates/go.yaml +1276 -0
- package/profiles/templates/java-spring-backend.yaml +326 -0
- package/profiles/templates/kotlin-spring.yaml +417 -0
- package/profiles/templates/nextjs.yaml +536 -0
- package/profiles/templates/nodejs.yaml +594 -0
- package/profiles/templates/python.yaml +546 -0
- package/profiles/templates/react.yaml +456 -0
- package/profiles/templates/rust.yaml +508 -0
- package/profiles/templates/vue.yaml +483 -0
|
@@ -338,3 +338,549 @@ dependencies:
|
|
|
338
338
|
# pyproject.toml
|
|
339
339
|
# alembic.ini
|
|
340
340
|
# Dockerfile
|
|
341
|
+
|
|
342
|
+
# ----------------------------------------------------------------------------
|
|
343
|
+
# CODE EXAMPLES
|
|
344
|
+
# ----------------------------------------------------------------------------
|
|
345
|
+
codeExamples:
|
|
346
|
+
entity:
|
|
347
|
+
description: "Domain entity with Pydantic"
|
|
348
|
+
code: |
|
|
349
|
+
# domain/entities/order.py
|
|
350
|
+
from datetime import datetime
|
|
351
|
+
from decimal import Decimal
|
|
352
|
+
from typing import List
|
|
353
|
+
from uuid import UUID, uuid4
|
|
354
|
+
from pydantic import BaseModel, Field
|
|
355
|
+
|
|
356
|
+
from domain.value_objects.money import Money
|
|
357
|
+
from domain.exceptions import DomainError
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class OrderItem(BaseModel):
|
|
361
|
+
product_id: UUID
|
|
362
|
+
quantity: int = Field(gt=0)
|
|
363
|
+
unit_price: Money
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def subtotal(self) -> Money:
|
|
367
|
+
return self.unit_price * self.quantity
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class Order(BaseModel):
|
|
371
|
+
id: UUID = Field(default_factory=uuid4)
|
|
372
|
+
customer_id: UUID
|
|
373
|
+
items: List[OrderItem] = Field(min_length=1)
|
|
374
|
+
status: str = "PENDING"
|
|
375
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
376
|
+
|
|
377
|
+
model_config = {"frozen": True} # Immutable
|
|
378
|
+
|
|
379
|
+
@property
|
|
380
|
+
def total(self) -> Money:
|
|
381
|
+
return sum(
|
|
382
|
+
(item.subtotal for item in self.items),
|
|
383
|
+
start=Money(amount=Decimal("0"), currency="USD")
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def confirm(self) -> "Order":
|
|
387
|
+
if self.status != "PENDING":
|
|
388
|
+
raise DomainError("Order is not pending")
|
|
389
|
+
return self.model_copy(update={"status": "CONFIRMED"})
|
|
390
|
+
|
|
391
|
+
def add_item(self, item: OrderItem) -> "Order":
|
|
392
|
+
if self.status != "PENDING":
|
|
393
|
+
raise DomainError("Cannot add items to confirmed order")
|
|
394
|
+
return self.model_copy(update={"items": [*self.items, item]})
|
|
395
|
+
|
|
396
|
+
valueObject:
|
|
397
|
+
description: "Immutable value object"
|
|
398
|
+
code: |
|
|
399
|
+
# domain/value_objects/money.py
|
|
400
|
+
from decimal import Decimal
|
|
401
|
+
from pydantic import BaseModel, Field, model_validator
|
|
402
|
+
|
|
403
|
+
from domain.exceptions import DomainError
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class Money(BaseModel):
|
|
407
|
+
amount: Decimal = Field(ge=0)
|
|
408
|
+
currency: str = Field(min_length=3, max_length=3)
|
|
409
|
+
|
|
410
|
+
model_config = {"frozen": True}
|
|
411
|
+
|
|
412
|
+
def __add__(self, other: "Money") -> "Money":
|
|
413
|
+
self._ensure_same_currency(other)
|
|
414
|
+
return Money(amount=self.amount + other.amount, currency=self.currency)
|
|
415
|
+
|
|
416
|
+
def __mul__(self, factor: int | Decimal) -> "Money":
|
|
417
|
+
return Money(amount=self.amount * Decimal(str(factor)), currency=self.currency)
|
|
418
|
+
|
|
419
|
+
def _ensure_same_currency(self, other: "Money") -> None:
|
|
420
|
+
if self.currency != other.currency:
|
|
421
|
+
raise DomainError(f"Currency mismatch: {self.currency} vs {other.currency}")
|
|
422
|
+
|
|
423
|
+
def __str__(self) -> str:
|
|
424
|
+
return f"{self.currency} {self.amount:.2f}"
|
|
425
|
+
|
|
426
|
+
useCase:
|
|
427
|
+
description: "Application use case with dependency injection"
|
|
428
|
+
code: |
|
|
429
|
+
# application/use_cases/create_order.py
|
|
430
|
+
from uuid import UUID
|
|
431
|
+
from typing import Protocol
|
|
432
|
+
|
|
433
|
+
from domain.entities.order import Order, OrderItem
|
|
434
|
+
from domain.value_objects.money import Money
|
|
435
|
+
from application.ports.order_repository import OrderRepository
|
|
436
|
+
from application.ports.event_publisher import EventPublisher
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class CreateOrderCommand(BaseModel):
|
|
440
|
+
customer_id: UUID
|
|
441
|
+
items: list[dict]
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
class CreateOrderUseCase:
|
|
445
|
+
def __init__(
|
|
446
|
+
self,
|
|
447
|
+
order_repository: OrderRepository,
|
|
448
|
+
event_publisher: EventPublisher,
|
|
449
|
+
) -> None:
|
|
450
|
+
self._order_repository = order_repository
|
|
451
|
+
self._event_publisher = event_publisher
|
|
452
|
+
|
|
453
|
+
async def execute(self, command: CreateOrderCommand) -> Order:
|
|
454
|
+
items = [
|
|
455
|
+
OrderItem(
|
|
456
|
+
product_id=item["product_id"],
|
|
457
|
+
quantity=item["quantity"],
|
|
458
|
+
unit_price=Money(
|
|
459
|
+
amount=item["unit_price"],
|
|
460
|
+
currency="USD"
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
for item in command.items
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
order = Order(customer_id=command.customer_id, items=items)
|
|
467
|
+
|
|
468
|
+
await self._order_repository.save(order)
|
|
469
|
+
|
|
470
|
+
await self._event_publisher.publish(
|
|
471
|
+
"OrderCreated",
|
|
472
|
+
{"order_id": str(order.id), "customer_id": str(order.customer_id)}
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
return order
|
|
476
|
+
|
|
477
|
+
router:
|
|
478
|
+
description: "FastAPI router with validation"
|
|
479
|
+
code: |
|
|
480
|
+
# infrastructure/api/routes/orders.py
|
|
481
|
+
from uuid import UUID
|
|
482
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
483
|
+
from pydantic import BaseModel, Field
|
|
484
|
+
|
|
485
|
+
from application.use_cases.create_order import CreateOrderUseCase, CreateOrderCommand
|
|
486
|
+
from infrastructure.api.dependencies import get_create_order_use_case
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
router = APIRouter(prefix="/orders", tags=["orders"])
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class CreateOrderRequest(BaseModel):
|
|
493
|
+
customer_id: UUID
|
|
494
|
+
items: list[dict] = Field(min_length=1)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class OrderResponse(BaseModel):
|
|
498
|
+
id: UUID
|
|
499
|
+
customer_id: UUID
|
|
500
|
+
status: str
|
|
501
|
+
total: str
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@router.post("/", response_model=OrderResponse, status_code=status.HTTP_201_CREATED)
|
|
505
|
+
async def create_order(
|
|
506
|
+
request: CreateOrderRequest,
|
|
507
|
+
use_case: CreateOrderUseCase = Depends(get_create_order_use_case),
|
|
508
|
+
) -> OrderResponse:
|
|
509
|
+
command = CreateOrderCommand(
|
|
510
|
+
customer_id=request.customer_id,
|
|
511
|
+
items=request.items
|
|
512
|
+
)
|
|
513
|
+
order = await use_case.execute(command)
|
|
514
|
+
|
|
515
|
+
return OrderResponse(
|
|
516
|
+
id=order.id,
|
|
517
|
+
customer_id=order.customer_id,
|
|
518
|
+
status=order.status,
|
|
519
|
+
total=str(order.total)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@router.get("/{order_id}", response_model=OrderResponse)
|
|
524
|
+
async def get_order(
|
|
525
|
+
order_id: UUID,
|
|
526
|
+
use_case: GetOrderUseCase = Depends(get_get_order_use_case),
|
|
527
|
+
) -> OrderResponse:
|
|
528
|
+
order = await use_case.execute(order_id)
|
|
529
|
+
if not order:
|
|
530
|
+
raise HTTPException(
|
|
531
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
532
|
+
detail=f"Order {order_id} not found"
|
|
533
|
+
)
|
|
534
|
+
return OrderResponse(...)
|
|
535
|
+
|
|
536
|
+
repository:
|
|
537
|
+
description: "Repository with SQLAlchemy 2.0"
|
|
538
|
+
code: |
|
|
539
|
+
# infrastructure/database/repositories/order_repository.py
|
|
540
|
+
from uuid import UUID
|
|
541
|
+
from typing import Optional
|
|
542
|
+
|
|
543
|
+
from sqlalchemy import select
|
|
544
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
545
|
+
from sqlalchemy.orm import selectinload
|
|
546
|
+
|
|
547
|
+
from domain.entities.order import Order
|
|
548
|
+
from application.ports.order_repository import OrderRepository
|
|
549
|
+
from infrastructure.database.models.order import OrderModel
|
|
550
|
+
from infrastructure.database.mappers.order_mapper import OrderMapper
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class SQLAlchemyOrderRepository(OrderRepository):
|
|
554
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
555
|
+
self._session = session
|
|
556
|
+
|
|
557
|
+
async def save(self, order: Order) -> None:
|
|
558
|
+
model = OrderMapper.to_model(order)
|
|
559
|
+
self._session.add(model)
|
|
560
|
+
await self._session.commit()
|
|
561
|
+
|
|
562
|
+
async def find_by_id(self, order_id: UUID) -> Optional[Order]:
|
|
563
|
+
query = (
|
|
564
|
+
select(OrderModel)
|
|
565
|
+
.options(selectinload(OrderModel.items))
|
|
566
|
+
.where(OrderModel.id == order_id)
|
|
567
|
+
)
|
|
568
|
+
result = await self._session.execute(query)
|
|
569
|
+
model = result.scalar_one_or_none()
|
|
570
|
+
|
|
571
|
+
return OrderMapper.to_entity(model) if model else None
|
|
572
|
+
|
|
573
|
+
async def find_by_customer_id(self, customer_id: UUID) -> list[Order]:
|
|
574
|
+
query = (
|
|
575
|
+
select(OrderModel)
|
|
576
|
+
.options(selectinload(OrderModel.items))
|
|
577
|
+
.where(OrderModel.customer_id == customer_id)
|
|
578
|
+
.order_by(OrderModel.created_at.desc())
|
|
579
|
+
)
|
|
580
|
+
result = await self._session.execute(query)
|
|
581
|
+
models = result.scalars().all()
|
|
582
|
+
|
|
583
|
+
return [OrderMapper.to_entity(m) for m in models]
|
|
584
|
+
|
|
585
|
+
test:
|
|
586
|
+
description: "Pytest test with fixtures"
|
|
587
|
+
code: |
|
|
588
|
+
# tests/unit/domain/test_order.py
|
|
589
|
+
from decimal import Decimal
|
|
590
|
+
from uuid import uuid4
|
|
591
|
+
|
|
592
|
+
import pytest
|
|
593
|
+
|
|
594
|
+
from domain.entities.order import Order, OrderItem
|
|
595
|
+
from domain.value_objects.money import Money
|
|
596
|
+
from domain.exceptions import DomainError
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@pytest.fixture
|
|
600
|
+
def sample_item() -> OrderItem:
|
|
601
|
+
return OrderItem(
|
|
602
|
+
product_id=uuid4(),
|
|
603
|
+
quantity=2,
|
|
604
|
+
unit_price=Money(amount=Decimal("50.00"), currency="USD")
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@pytest.fixture
|
|
609
|
+
def pending_order(sample_item: OrderItem) -> Order:
|
|
610
|
+
return Order(customer_id=uuid4(), items=[sample_item])
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
class TestOrder:
|
|
614
|
+
def test_should_create_order_with_items(self, sample_item: OrderItem) -> None:
|
|
615
|
+
order = Order(customer_id=uuid4(), items=[sample_item])
|
|
616
|
+
|
|
617
|
+
assert order.status == "PENDING"
|
|
618
|
+
assert order.total == Money(amount=Decimal("100.00"), currency="USD")
|
|
619
|
+
|
|
620
|
+
def test_should_raise_when_creating_without_items(self) -> None:
|
|
621
|
+
with pytest.raises(ValueError):
|
|
622
|
+
Order(customer_id=uuid4(), items=[])
|
|
623
|
+
|
|
624
|
+
def test_should_confirm_pending_order(self, pending_order: Order) -> None:
|
|
625
|
+
confirmed = pending_order.confirm()
|
|
626
|
+
|
|
627
|
+
assert confirmed.status == "CONFIRMED"
|
|
628
|
+
assert confirmed.id == pending_order.id
|
|
629
|
+
|
|
630
|
+
def test_should_raise_when_confirming_non_pending(
|
|
631
|
+
self, pending_order: Order
|
|
632
|
+
) -> None:
|
|
633
|
+
confirmed = pending_order.confirm()
|
|
634
|
+
|
|
635
|
+
with pytest.raises(DomainError, match="not pending"):
|
|
636
|
+
confirmed.confirm()
|
|
637
|
+
|
|
638
|
+
def test_should_add_item_to_pending_order(
|
|
639
|
+
self, pending_order: Order, sample_item: OrderItem
|
|
640
|
+
) -> None:
|
|
641
|
+
updated = pending_order.add_item(sample_item)
|
|
642
|
+
|
|
643
|
+
assert len(updated.items) == 2
|
|
644
|
+
assert updated.total.amount == Decimal("200.00")
|
|
645
|
+
|
|
646
|
+
dependencyInjection:
|
|
647
|
+
description: "FastAPI dependency injection"
|
|
648
|
+
code: |
|
|
649
|
+
# infrastructure/api/dependencies.py
|
|
650
|
+
from typing import AsyncGenerator
|
|
651
|
+
from functools import lru_cache
|
|
652
|
+
|
|
653
|
+
from fastapi import Depends
|
|
654
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
655
|
+
|
|
656
|
+
from infrastructure.database.session import get_session
|
|
657
|
+
from infrastructure.database.repositories.order_repository import SQLAlchemyOrderRepository
|
|
658
|
+
from application.use_cases.create_order import CreateOrderUseCase
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
async def get_order_repository(
|
|
662
|
+
session: AsyncSession = Depends(get_session),
|
|
663
|
+
) -> SQLAlchemyOrderRepository:
|
|
664
|
+
return SQLAlchemyOrderRepository(session)
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
async def get_create_order_use_case(
|
|
668
|
+
order_repository: SQLAlchemyOrderRepository = Depends(get_order_repository),
|
|
669
|
+
event_publisher: EventPublisher = Depends(get_event_publisher),
|
|
670
|
+
) -> CreateOrderUseCase:
|
|
671
|
+
return CreateOrderUseCase(
|
|
672
|
+
order_repository=order_repository,
|
|
673
|
+
event_publisher=event_publisher,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
# ----------------------------------------------------------------------------
|
|
677
|
+
# ANTI-PATTERNS
|
|
678
|
+
# ----------------------------------------------------------------------------
|
|
679
|
+
antiPatterns:
|
|
680
|
+
noTypeHints:
|
|
681
|
+
name: "Missing Type Hints"
|
|
682
|
+
description: "Functions without type annotations"
|
|
683
|
+
bad: |
|
|
684
|
+
# ❌ No type hints
|
|
685
|
+
def calculate_total(items):
|
|
686
|
+
total = 0
|
|
687
|
+
for item in items:
|
|
688
|
+
total += item['price'] * item['quantity']
|
|
689
|
+
return total
|
|
690
|
+
|
|
691
|
+
def get_user(user_id):
|
|
692
|
+
return db.query(User).get(user_id)
|
|
693
|
+
good: |
|
|
694
|
+
# ✅ With type hints
|
|
695
|
+
from decimal import Decimal
|
|
696
|
+
from typing import Optional
|
|
697
|
+
|
|
698
|
+
def calculate_total(items: list[OrderItem]) -> Decimal:
|
|
699
|
+
return sum(item.subtotal for item in items)
|
|
700
|
+
|
|
701
|
+
def get_user(user_id: UUID) -> Optional[User]:
|
|
702
|
+
return db.query(User).get(user_id)
|
|
703
|
+
|
|
704
|
+
bareExcept:
|
|
705
|
+
name: "Bare Except Clauses"
|
|
706
|
+
description: "Catching all exceptions without specificity"
|
|
707
|
+
bad: |
|
|
708
|
+
# ❌ Bare except catches everything
|
|
709
|
+
try:
|
|
710
|
+
process_order(order)
|
|
711
|
+
except:
|
|
712
|
+
print("Something went wrong")
|
|
713
|
+
|
|
714
|
+
# ❌ Catching generic Exception
|
|
715
|
+
try:
|
|
716
|
+
process_order(order)
|
|
717
|
+
except Exception:
|
|
718
|
+
pass
|
|
719
|
+
good: |
|
|
720
|
+
# ✅ Specific exception handling
|
|
721
|
+
from domain.exceptions import DomainError, ValidationError
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
process_order(order)
|
|
725
|
+
except ValidationError as e:
|
|
726
|
+
logger.warning("Validation failed", error=str(e))
|
|
727
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
728
|
+
except DomainError as e:
|
|
729
|
+
logger.error("Domain error", error=str(e))
|
|
730
|
+
raise HTTPException(status_code=422, detail=str(e))
|
|
731
|
+
|
|
732
|
+
printStatements:
|
|
733
|
+
name: "Using print() for Logging"
|
|
734
|
+
description: "Using print instead of structured logging"
|
|
735
|
+
bad: |
|
|
736
|
+
# ❌ print statements
|
|
737
|
+
print(f"Processing order {order_id}")
|
|
738
|
+
print(f"Error: {error}")
|
|
739
|
+
good: |
|
|
740
|
+
# ✅ Structured logging
|
|
741
|
+
import structlog
|
|
742
|
+
|
|
743
|
+
logger = structlog.get_logger()
|
|
744
|
+
|
|
745
|
+
logger.info("processing_order", order_id=str(order_id))
|
|
746
|
+
logger.error("order_failed", order_id=str(order_id), error=str(error))
|
|
747
|
+
|
|
748
|
+
syncInAsync:
|
|
749
|
+
name: "Synchronous Code in Async Functions"
|
|
750
|
+
description: "Blocking the event loop with sync operations"
|
|
751
|
+
bad: |
|
|
752
|
+
# ❌ Blocking async function
|
|
753
|
+
import requests
|
|
754
|
+
|
|
755
|
+
async def fetch_user_data(user_id: str) -> dict:
|
|
756
|
+
response = requests.get(f"/api/users/{user_id}") # Blocks!
|
|
757
|
+
return response.json()
|
|
758
|
+
|
|
759
|
+
async def read_config() -> dict:
|
|
760
|
+
with open("config.json") as f: # Blocks!
|
|
761
|
+
return json.load(f)
|
|
762
|
+
good: |
|
|
763
|
+
# ✅ Async operations
|
|
764
|
+
import httpx
|
|
765
|
+
import aiofiles
|
|
766
|
+
|
|
767
|
+
async def fetch_user_data(user_id: str) -> dict:
|
|
768
|
+
async with httpx.AsyncClient() as client:
|
|
769
|
+
response = await client.get(f"/api/users/{user_id}")
|
|
770
|
+
return response.json()
|
|
771
|
+
|
|
772
|
+
async def read_config() -> dict:
|
|
773
|
+
async with aiofiles.open("config.json") as f:
|
|
774
|
+
content = await f.read()
|
|
775
|
+
return json.loads(content)
|
|
776
|
+
|
|
777
|
+
mutableDefaults:
|
|
778
|
+
name: "Mutable Default Arguments"
|
|
779
|
+
description: "Using mutable objects as default parameters"
|
|
780
|
+
bad: |
|
|
781
|
+
# ❌ Mutable default - shared between calls!
|
|
782
|
+
def add_item(item: str, items: list = []) -> list:
|
|
783
|
+
items.append(item)
|
|
784
|
+
return items
|
|
785
|
+
|
|
786
|
+
def process_config(config: dict = {}) -> dict:
|
|
787
|
+
config["processed"] = True
|
|
788
|
+
return config
|
|
789
|
+
good: |
|
|
790
|
+
# ✅ Use None and create new objects
|
|
791
|
+
def add_item(item: str, items: list | None = None) -> list:
|
|
792
|
+
if items is None:
|
|
793
|
+
items = []
|
|
794
|
+
items.append(item)
|
|
795
|
+
return items
|
|
796
|
+
|
|
797
|
+
def process_config(config: dict | None = None) -> dict:
|
|
798
|
+
if config is None:
|
|
799
|
+
config = {}
|
|
800
|
+
return {**config, "processed": True}
|
|
801
|
+
|
|
802
|
+
businessLogicInRoutes:
|
|
803
|
+
name: "Business Logic in Routes"
|
|
804
|
+
description: "Putting domain logic in API endpoints"
|
|
805
|
+
bad: |
|
|
806
|
+
# ❌ Business logic in route
|
|
807
|
+
@router.post("/orders")
|
|
808
|
+
async def create_order(request: CreateOrderRequest, db: Session = Depends(get_db)):
|
|
809
|
+
# All logic in the route - bad!
|
|
810
|
+
if not request.items:
|
|
811
|
+
raise HTTPException(400, "No items")
|
|
812
|
+
|
|
813
|
+
total = sum(item.price * item.quantity for item in request.items)
|
|
814
|
+
|
|
815
|
+
if total > 10000:
|
|
816
|
+
await send_manager_notification(total)
|
|
817
|
+
|
|
818
|
+
order = OrderModel(customer_id=request.customer_id, total=total)
|
|
819
|
+
db.add(order)
|
|
820
|
+
db.commit()
|
|
821
|
+
return order
|
|
822
|
+
good: |
|
|
823
|
+
# ✅ Route delegates to use case
|
|
824
|
+
@router.post("/orders", response_model=OrderResponse)
|
|
825
|
+
async def create_order(
|
|
826
|
+
request: CreateOrderRequest,
|
|
827
|
+
use_case: CreateOrderUseCase = Depends(get_create_order_use_case),
|
|
828
|
+
) -> OrderResponse:
|
|
829
|
+
command = CreateOrderCommand(**request.model_dump())
|
|
830
|
+
order = await use_case.execute(command)
|
|
831
|
+
return OrderResponse.from_entity(order)
|
|
832
|
+
|
|
833
|
+
globalState:
|
|
834
|
+
name: "Global Mutable State"
|
|
835
|
+
description: "Using global variables for state management"
|
|
836
|
+
bad: |
|
|
837
|
+
# ❌ Global mutable state
|
|
838
|
+
current_user = None
|
|
839
|
+
db_connection = None
|
|
840
|
+
|
|
841
|
+
def set_user(user):
|
|
842
|
+
global current_user
|
|
843
|
+
current_user = user
|
|
844
|
+
|
|
845
|
+
def get_user():
|
|
846
|
+
return current_user
|
|
847
|
+
good: |
|
|
848
|
+
# ✅ Dependency injection
|
|
849
|
+
from fastapi import Depends, Request
|
|
850
|
+
from typing import Annotated
|
|
851
|
+
|
|
852
|
+
async def get_current_user(request: Request) -> User:
|
|
853
|
+
token = request.headers.get("Authorization")
|
|
854
|
+
return await auth_service.validate_token(token)
|
|
855
|
+
|
|
856
|
+
@router.get("/profile")
|
|
857
|
+
async def get_profile(
|
|
858
|
+
current_user: Annotated[User, Depends(get_current_user)]
|
|
859
|
+
) -> UserProfile:
|
|
860
|
+
return UserProfile.from_user(current_user)
|
|
861
|
+
|
|
862
|
+
stringConcatenation:
|
|
863
|
+
name: "String Concatenation for SQL"
|
|
864
|
+
description: "Building SQL queries with string concatenation"
|
|
865
|
+
bad: |
|
|
866
|
+
# ❌ SQL injection vulnerability!
|
|
867
|
+
def get_user_by_email(email: str):
|
|
868
|
+
query = f"SELECT * FROM users WHERE email = '{email}'"
|
|
869
|
+
return db.execute(query)
|
|
870
|
+
|
|
871
|
+
def search_products(term: str):
|
|
872
|
+
query = "SELECT * FROM products WHERE name LIKE '%" + term + "%'"
|
|
873
|
+
return db.execute(query)
|
|
874
|
+
good: |
|
|
875
|
+
# ✅ Parameterized queries with SQLAlchemy
|
|
876
|
+
from sqlalchemy import select
|
|
877
|
+
|
|
878
|
+
async def get_user_by_email(email: str) -> User | None:
|
|
879
|
+
query = select(User).where(User.email == email)
|
|
880
|
+
result = await session.execute(query)
|
|
881
|
+
return result.scalar_one_or_none()
|
|
882
|
+
|
|
883
|
+
async def search_products(term: str) -> list[Product]:
|
|
884
|
+
query = select(Product).where(Product.name.ilike(f"%{term}%"))
|
|
885
|
+
result = await session.execute(query)
|
|
886
|
+
return list(result.scalars().all())
|