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

Implement Dummy API for Project Understanding #445

Merged
merged 7 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
712 changes: 579 additions & 133 deletions api/backend-openapi-spec.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions backend-go/api/codepair/v1/models/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package models

import (
"errors"
"fmt"
)

func RequiredFieldError(field string) error {
return errors.New(field + " is required")
}

func MinLengthError(field string, min int) error {
return errors.New(fmt.Sprintf("%s must be at least %d characters", field, min))
}
7 changes: 7 additions & 0 deletions backend-go/api/codepair/v1/models/model_hello_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package models

type HelloRequest struct {

// New nickname to say hello
Nickname string `json:"nickname"`
}
Comment on lines +1 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about writing a Request, Response, and Validator of the same model (Hello) in the same file? It seems unnecessary to write the Request and Response in different files.

Maybe it would be better to write them together in the router file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally agree with @blurfx's suggestion, as this style of micro-separating so-called DTOs is not a common approach in Golang and may lead to an unnecessary proliferation of files for DTO management.

Copy link
Member Author

@devleejb devleejb Feb 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One important thing to keep in mind is that all request and response types are auto-generated from the OpenAPI specification.

Validator

Since these files are auto-generated, declaring validation functions within the same file is risky, as they might be overwritten during regeneration.

Type Placement

Moving these types to their corresponding packages is quite challenging, as it is auto-generated. Additionally, the directory structure is similar to yorkie. I believe these types correspond to files like admin.pb.go and yorkie.pb.go.

DTO

I’m unsure how we should manage request and response types for REST APIs in Go. Could you provide an example or some guidance on this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn’t realize these files were auto-generated by OpenAPI generation. If that’s the case, I think it’s great!

7 changes: 7 additions & 0 deletions backend-go/api/codepair/v1/models/model_hello_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package models

type HelloResponse struct {

// Welcome message
Message string `json:"message"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package models
type HttpExceptionResponse struct {

// HTTP status code
StatusCode float32 `json:"statusCode"`
StatusCode int `json:"statusCode"`

// Description of the error
Message string `json:"message"`
Expand Down
13 changes: 13 additions & 0 deletions backend-go/api/codepair/v1/models/validators.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

func (r *HelloRequest) Validate() error {
if r.Nickname == "" {
return RequiredFieldError("nickname")
}

if len(r.Nickname) < 2 {
return MinLengthError("nickname", 2)
}

return nil
}
26 changes: 26 additions & 0 deletions backend-go/internal/core/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package core

import (
"github.com/yorkie-team/codepair/backend/internal/core/hello"
"github.com/yorkie-team/codepair/backend/internal/infra/database/mongo"
)

type Handlers struct {
Hello *hello.Handler
}

// NewHandlers creates a new handlers.
func NewHandlers() *Handlers {
// Repositories
helloRepository := mongo.NewHelloRepository()

kokodak marked this conversation as resolved.
Show resolved Hide resolved
// Services
helloService := hello.NewService(helloRepository)

// Handlers
helloHandler := hello.NewHandler(helloService)

return &Handlers{
Hello: helloHandler,
}
}
41 changes: 41 additions & 0 deletions backend-go/internal/core/hello/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package hello

import (
"fmt"

"github.com/labstack/echo/v4"

"github.com/yorkie-team/codepair/backend/api/codepair/v1/models"
"github.com/yorkie-team/codepair/backend/internal/transport/http"
)

type Handler struct {
helloService *Service
}

// NewHandler creates a new handler for hello.
func NewHandler(service *Service) *Handler {
return &Handler{
helloService: service,
}
}

// HelloCodePair returns a hello message for a given CodePairVisitor.
func (h *Handler) HelloCodePair(e echo.Context) error {
req := new(models.HelloRequest)

if err := http.BindAndValidateRequest(e, req); err != nil {
return fmt.Errorf("%w", err)
}

helloMessage, err := h.helloService.HelloCodePair(e, CodePairVisitor{
Nickname: req.Nickname,
})
if err != nil {
return fmt.Errorf("%w", http.NewErrorResponse(e, err))
}

return fmt.Errorf("%w", http.NewOkResponse(e, models.HelloResponse{
Message: helloMessage,
}))
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions backend-go/internal/core/hello/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package hello

// CodePairVisitor is a visitor for CodePair
type CodePairVisitor struct {
// Nickname is the nickname of the visitor
Nickname string
}
6 changes: 6 additions & 0 deletions backend-go/internal/core/hello/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package hello

type Repository interface {
// ReadHelloMessageFor reads a hello message for a given CodePairVisitor
ReadHelloMessageFor(codePairVisitor CodePairVisitor) (string, error)
}
28 changes: 28 additions & 0 deletions backend-go/internal/core/hello/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package hello

import (
"github.com/labstack/echo/v4"
"github.com/yorkie-team/codepair/backend/internal/transport/http"
)

type Service struct {
helloRepository Repository
}

// NewService creates a new service for hello.
func NewService(repository Repository) *Service {
return &Service{
helloRepository: repository,
}
}

// HelloCodePair returns a hello message for a given CodePairVisitor
func (s *Service) HelloCodePair(e echo.Context, codePairVisitor CodePairVisitor) (string, error) {
helloMessage, err := s.helloRepository.ReadHelloMessageFor(codePairVisitor)
if err != nil {
e.Logger().Fatal(err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Replace Fatal logging with Error level logging.

Using Fatal logging here is problematic as it terminates the program. For recoverable errors, use Error level logging instead to maintain service availability.

Apply this diff:

-		e.Logger().Fatal(err)
+		e.Logger().Error(err)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
e.Logger().Fatal(err)
e.Logger().Error(err)

return "", http.ErrInternalServerError
}

return helloMessage, nil
devleejb marked this conversation as resolved.
Show resolved Hide resolved
}
14 changes: 14 additions & 0 deletions backend-go/internal/infra/database/mongo/hello.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mongo

import "github.com/yorkie-team/codepair/backend/internal/core/hello"

type HelloRepository struct{}

// NewHelloRepository creates a new HelloRepository.
func NewHelloRepository() HelloRepository {
return HelloRepository{}
}

func (h HelloRepository) ReadHelloMessageFor(codePairVisitor hello.CodePairVisitor) (string, error) {
return "Hello, " + codePairVisitor.Nickname + "!", nil
}
Comment on lines +12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation and error handling.

The current implementation could be improved with:

  1. Input validation to prevent nil pointer dereference
  2. Error handling for edge cases

Consider this safer implementation:

 func (h HelloRepository) ReadHelloMessageFor(codePairVisitor hello.CodePairVisitor) (string, error) {
+	if codePairVisitor.Nickname == "" {
+		return "", errors.New("invalid visitor: nickname is required")
+	}
 	return "Hello, " + codePairVisitor.Nickname + "!", nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (h HelloRepository) ReadHelloMessageFor(codePairVisitor hello.CodePairVisitor) (string, error) {
return "Hello, " + codePairVisitor.Nickname + "!", nil
}
func (h HelloRepository) ReadHelloMessageFor(codePairVisitor hello.CodePairVisitor) (string, error) {
if codePairVisitor.Nickname == "" {
return "", errors.New("invalid visitor: nickname is required")
}
return "Hello, " + codePairVisitor.Nickname + "!", nil
}

12 changes: 4 additions & 8 deletions backend-go/internal/server/routes.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package server

import (
"fmt"
"net/http"

"github.com/labstack/echo/v4"
"github.com/yorkie-team/codepair/backend/internal/core"
)

func RegisterRoutes(e *echo.Echo) {
e.GET("/", func(c echo.Context) error {
err := c.String(http.StatusOK, "Hello, World!")
return fmt.Errorf("error: %w", err)
})
// RegisterRoutes registers routes for the server.
func RegisterRoutes(e *echo.Echo, handlers *core.Handlers) {
e.POST("/hello", handlers.Hello.HelloCodePair)
}
6 changes: 5 additions & 1 deletion backend-go/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import (
"github.com/labstack/echo/v4"

"github.com/yorkie-team/codepair/backend/internal/config"
"github.com/yorkie-team/codepair/backend/internal/core"
)

type CodePair struct {
config *config.Config
echo *echo.Echo
}

// New creates a new CodePair server.
func New(e *echo.Echo, conf *config.Config) *CodePair {
RegisterRoutes(e)
handlers := core.NewHandlers()
RegisterRoutes(e, handlers)
kokodak marked this conversation as resolved.
Show resolved Hide resolved

cp := &CodePair{
config: conf,
Expand All @@ -25,6 +28,7 @@ func New(e *echo.Echo, conf *config.Config) *CodePair {
return cp
}

// Start starts the server.
func (c *CodePair) Start() error {
addr := fmt.Sprintf(":%d", c.config.Server.Port)
if err := c.echo.Start(addr); !errors.Is(err, http.ErrServerClosed) {
Expand Down
17 changes: 17 additions & 0 deletions backend-go/internal/transport/http/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package http

import (
"errors"

"github.com/yorkie-team/codepair/backend/api/codepair/v1/models"
)

// ConvertErrorToResponse converts an error to a response.
func ConvertErrorToResponse(err error) models.HttpExceptionResponse {
switch {
case errors.Is(err, ErrInternalServerError):
return NewInternalServerErrorResponse()
default:
return NewInvalidJSONErrorResponse()
}
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved
29 changes: 29 additions & 0 deletions backend-go/internal/transport/http/error_responses.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package http

import (
nethttp "net/http"

"github.com/yorkie-team/codepair/backend/api/codepair/v1/models"
)

func newHTTPExceptionResponse(statusCode int, message string) models.HttpExceptionResponse {
return models.HttpExceptionResponse{
StatusCode: statusCode,
Message: message,
}
}

// NewInvalidJSONErrorResponse creates a HttpExceptionResponse that represents an invalid JSON error.
func NewInvalidJSONErrorResponse() models.HttpExceptionResponse {
return newHTTPExceptionResponse(nethttp.StatusBadRequest, "Invalid JSON")
}

// NewValidationErrorResponse creates a HttpExceptionResponse that represents a validation error.
func NewValidationErrorResponse(reason string) models.HttpExceptionResponse {
return newHTTPExceptionResponse(nethttp.StatusBadRequest, reason)
}

// NewInternalServerErrorResponse creates a HttpExceptionResponse that represents a internal server error.
func NewInternalServerErrorResponse() models.HttpExceptionResponse {
return newHTTPExceptionResponse(nethttp.StatusInternalServerError, "Internal Server Error")
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved
8 changes: 8 additions & 0 deletions backend-go/internal/transport/http/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package http

import "errors"

var (
// ErrInternalServerError is an internal server error
ErrInternalServerError = errors.New("internal server error")
)
37 changes: 37 additions & 0 deletions backend-go/internal/transport/http/handler_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package http

import (
"fmt"
nethttp "net/http"

"github.com/labstack/echo/v4"
)

type request interface {
Validate() error
}

// BindAndValidateRequest binds and validates the request.
// If the request is invalid, it returns an error response.
func BindAndValidateRequest(e echo.Context, req request) error {
if err := e.Bind(req); err != nil {
return fmt.Errorf("failed to bind request: %w", e.JSON(nethttp.StatusBadRequest, NewInvalidJSONErrorResponse()))
}
if err := req.Validate(); err != nil {
return fmt.Errorf("validation failed: %w", e.JSON(nethttp.StatusBadRequest, NewValidationErrorResponse(err.Error())))
}
return nil
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved

// NewErrorResponse handles the creation and response of an error.
// It converts the provided error into a response structure and sends it as a JSON response.
// The response status code is determined by the error's associated status.
func NewErrorResponse(e echo.Context, err error) error {
resp := ConvertErrorToResponse(err)
return fmt.Errorf("returning error response as JSON: %w", e.JSON(resp.StatusCode, ConvertErrorToResponse(err)))
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved

// NewOkResponse sends a JSON response with a status code of 200.
func NewOkResponse(e echo.Context, resp interface{}) error {
return fmt.Errorf("returning success response as JSON: %w", e.JSON(nethttp.StatusOK, resp))
}
kokodak marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"lint:check": "pnpm run --parallel lint:check",
"format": "pnpm run --parallel format",
"format:check": "pnpm run --parallel format:check",
"generate:backend": "openapi-generator-cli generate -i ./api/backend-openapi-spec.json -g go-echo-server -o ./backend-go/api/codepair/v1 --global-property models",
"generate:backend": "openapi-generator-cli generate -i ./api/backend-openapi-spec.json -g go-echo-server -o ./backend-go/api/codepair/v1 --global-property models --type-mappings int32=int",
"generate:frontend": "openapi-generator-cli generate -i ./api/backend-openapi-spec.json -g typescript-axios -o ./frontend/src/api"
},
"devDependencies": {
Expand Down
Loading