Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/swagger #32

Merged
merged 9 commits into from
Feb 25, 2025
5 changes: 5 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ HOST=0.0.0.0
setup:
uv sync
uv run src/manage.py makemigrations

setup-dev: setup
uv run pre-commit install

migration:
Expand All @@ -23,3 +25,6 @@ e2e-test:

test:
uv run src/manage.py test

export-swagger:
uv run src/manage.py spectacular --file swagger/api.yaml
6 changes: 4 additions & 2 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
LOCAL_APPS = [
"config",
"faq",
"core",
"merchandise",
]

Expand All @@ -54,10 +55,11 @@
# rest_framework settings
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"DEFAULT_PAGINATION_CLASS": "core.paginations.PageNumberPagination",
"EXCEPTION_HANDLER": "rest_framework.views.exception_handler",
"PAGE_SIZE_QUERY_PARAM": "page_size",
"PAGE_SIZE": 10,
"MAX_PAGE_SIZE": 100,
"PAGE_SIZE_QUERY_PARAM": "page_size",
}
# spectacular settings
SPECTACULAR_SETTINGS = {
Expand Down
Empty file added src/core/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions src/core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CoreConfig(AppConfig):
name = "core"
14 changes: 14 additions & 0 deletions src/core/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from rest_framework import status
from rest_framework.exceptions import APIException


class BaseException(APIException):
status_code = status.HTTP_200_OK
default_detail = "Internal Server Error"
default_code = "ERROR"


class NotExistException(BaseException):
status_code = status.HTTP_200_OK
default_detail = "Not Exist."
default_code = "Not Exist"
Empty file.
27 changes: 27 additions & 0 deletions src/core/middlewares/exception_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
from django.http import Http404
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
from rest_framework.views import set_rollback

from core.responses.base import BaseResponse


def custom_exception_handler(exc, context):
if isinstance(exc, Http404):
exc = NotFound(*(exc.args))
elif isinstance(exc, DjangoPermissionDenied):
exc = PermissionDenied(*(exc.args))

if isinstance(exc, APIException):
headers = {}
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["Retry-After"] = "%d" % exc.wait

error = dict(code=exc.default_code, message=exc.detail)

set_rollback()
return BaseResponse(error=error, headers=headers, status="ERROR")

return None
20 changes: 20 additions & 0 deletions src/core/paginations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.pagination import PageNumberPagination as DRFPageNumberPagination

from .responses.base import BaseResponse


class PageNumberPagination(DRFPageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100 # page λ‹¨μœ„μ˜ μš”μ²­ μ΅œλŒ€ size

def get_paginated_response(self, data):
pagination = {
"count": self.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
}
return BaseResponse(data=data, pagination=pagination)

def get_paginated_response_schema(self, schema: dict) -> dict:
return schema
Empty file added src/core/responses/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions src/core/responses/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Literal

from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.status import HTTP_200_OK

from core.responses.serializer import SuccessResponseSerializer


class BaseResponse(Response):
def __init__(
self,
data: dict | list | None = None,
error: dict | None = None,
pagination: dict | None = None,
template_name: str | None = None,
headers: dict | None = None,
exception: bool = False,
content_type: str | None = None,
status: Literal["SUCCESS", "ERROR"] = "SUCCESS",
):
response_format = {
"status": status,
"error": error,
"data": data,
"pagination": pagination,
}
super().__init__(
response_format, HTTP_200_OK, template_name, headers, exception, content_type
)

def __class_getitem__(cls, *args, **kwargs):
return cls

def get_response_schema(self) -> Serializer:
return SuccessResponseSerializer()
66 changes: 66 additions & 0 deletions src/core/responses/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from rest_framework import serializers


class ErrorSerializer(serializers.Serializer):
code = serializers.CharField()
message = serializers.CharField()

def to_representation(self, instance):
return {
"code": instance.code,
"message": instance.message,
}


class PageNumberPaginationSerializer(serializers.Serializer):
count = serializers.IntegerField()
next = serializers.CharField()
previous = serializers.CharField()

def to_representation(self, instance):
return {
"count": instance.count,
"next": instance.next,
"previous": instance.previous,
}


class SuccessResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="SUCCESS")
data = serializers.DictField(required=False)
pagination = PageNumberPaginationSerializer(required=False, allow_null=True)

def to_representation(self, instance):
return {
"status": instance.status,
"data": instance.data,
"error": None,
"pagination": None,
}


class ListSuccessResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="SUCCESS")
data = serializers.ListField(required=False, child=serializers.DictField())
pagination = PageNumberPaginationSerializer(required=False, allow_null=True)

def to_representation(self, instance):
return {
"status": instance.status,
"data": instance.data,
"error": None,
"pagination": instance.pagination,
}


class ErrorResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="ERROR")
error = ErrorSerializer()

def to_representation(self, instance):
return {
"status": instance.status,
"error": instance.error,
"data": None,
"pagination": None,
}
9 changes: 9 additions & 0 deletions src/core/swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Callable

from drf_spectacular.utils import extend_schema


class SwaggerSchema:
@classmethod
def generate_schema(cls, *args, **kwargs) -> Callable:
return extend_schema(*args, **kwargs)
1 change: 1 addition & 0 deletions src/core/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your tests here.
104 changes: 104 additions & 0 deletions src/faq/swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from drf_spectacular.utils import OpenApiExample, OpenApiResponse

from core.responses.serializer import (
ErrorResponseSerializer,
ListSuccessResponseSerializer,
SuccessResponseSerializer,
)
from core.swagger import SwaggerSchema


class FAQAPIDocs(SwaggerSchema):
@classmethod
def retrieve(cls):
responses = {
"성곡": OpenApiResponse(
response=SuccessResponseSerializer,
description="단일 응닡 성곡",
examples=[
OpenApiExample(
name="FAQ 쑰회",
value={
"status": "SUCCESS",
"data": {
"id": 1,
"question": "질문",
"answer": "λ‹΅λ³€",
"created_at": "2021-01-01",
},
},
),
],
),
"μ—λŸ¬": OpenApiResponse(
response=ErrorResponseSerializer,
description="응닡 μ—λŸ¬",
examples=[
OpenApiExample(
name="데이터 μ—†μŒ",
value={
"status": "ERROR",
"error": {"code": "NOT_EXIST", "message": "데이터가 μ—†μŠ΅λ‹ˆλ‹€."},
},
),
],
),
}
return cls.generate_schema(
operation_id="faq_retrieve", description="FAQ 쑰회", responses=responses
)

@classmethod
def list(cls):
responses = {
"성곡": OpenApiResponse(
response=ListSuccessResponseSerializer,
description="닀쀑 응닡 성곡",
examples=[
OpenApiExample(
name="FAQ λͺ©λ‘ 쑰회 (νŽ˜μ΄μ§€λ„€μ΄μ…˜ 있음)",
value={
"status": "SUCCESS",
"data": [
{
"id": 1,
"question": "질문1",
"answer": "λ‹΅λ³€1",
},
],
"pagination": {
"count": 20,
"next": "http://localhost:8000/api/v1/faqs/?page=2",
"previous": "http://localhost:8000/api/v1/faqs/?page=1",
},
},
),
OpenApiExample(
name="FAQ λͺ©λ‘ 쑰회 (데이터 있음)",
value={
"status": "SUCCESS",
"data": [
{
"id": 1,
"question": "질문1",
"answer": "λ‹΅λ³€1",
},
],
"pagination": {"count": 1, "next": None, "previous": None},
},
),
OpenApiExample(
name="FAQ λͺ©λ‘ 쑰회 (데이터 μ—†μŒ)",
value={
"status": "SUCCESS",
"data": [],
"pagination": {"count": 0, "next": None, "previous": None},
},
),
],
),
"μ—λŸ¬": OpenApiResponse(response=ErrorResponseSerializer, description="응닡 μ—λŸ¬"),
}
return cls.generate_schema(
operation_id="faq_list", description="λͺ¨λ“  FAQ λͺ©λ‘ 쑰회", responses=responses
)
Loading