Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
531f9b2
feat: [wip] apply hexagonal
dexfs Apr 21, 2024
0b11935
feat: [wip] apply hexagonal
dexfs Apr 22, 2024
4578481
feat: [wip] apply hexagonal
dexfs Apr 22, 2024
527f427
feat: refactor router
dexfs Apr 22, 2024
687892b
feat: config - rest validation
dexfs Apr 22, 2024
cdacceb
feat: get user feed flow
dexfs Apr 22, 2024
b9f0563
chore: remove previous structure
dexfs Apr 26, 2024
2027936
refactor: database in memory for support schemas
dexfs Apr 27, 2024
d23b4db
refactor: structure to follow hexagonal architecture
dexfs Apr 27, 2024
b8f3699
feat: new package for db in memory
dexfs Apr 28, 2024
d4cfe66
refactor: post context
dexfs Apr 28, 2024
ae24483
refactor: user context
dexfs Apr 28, 2024
3b5e86d
feat: rest validation
dexfs Apr 28, 2024
89523b2
refactor: routes
dexfs Apr 28, 2024
ba44a69
chore: update mocks
dexfs Apr 28, 2024
8994b7d
chore: go sum and go mod
dexfs Apr 28, 2024
0daee99
feat: add endlees to handle with gracefulshutdown
dexfs Apr 28, 2024
26ad853
chore: new makefile actions
dexfs Apr 28, 2024
dc394e4
chore: gitignore
dexfs Apr 28, 2024
70d71da
refactor: routes
dexfs Apr 30, 2024
17d9b35
chore: http requests
dexfs Apr 30, 2024
ed381ba
feat: routes post quote | post repost
dexfs May 6, 2024
bfd5e47
fix: unit test
dexfs May 6, 2024
fbae2fc
refactor: rename router files and remove dependencies initialization
dexfs May 11, 2024
24fb9a4
chore: rename file
dexfs May 11, 2024
5c5e896
fix: initialize users in memory
dexfs May 11, 2024
f0f038a
chore: add testify
dexfs May 11, 2024
7b4ca1b
chore: change file name
dexfs May 11, 2024
694bf55
test: user info resource
dexfs May 11, 2024
7974240
test: user resources feed
dexfs May 12, 2024
e82e70a
test: invalid username
dexfs May 12, 2024
d1453dc
chore: docker, docker compose and build script
dexfs Aug 27, 2024
a12bb9c
chore: change file name from api to main
dexfs Aug 27, 2024
d132fce
feat: add cors
dexfs Jan 28, 2025
0697c59
chore: request collection
dexfs Jan 28, 2025
b579e54
Merge pull request #4 from dexfs/feat/docker
dexfs Jan 28, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/api
35 changes: 35 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Stage 1: Build the Go binary
FROM golang:1.22-alpine AS builder

# Install necessary build tools
RUN apk add --no-cache gcc musl-dev

# Set the Current Working Directory inside the container
WORKDIR /app

# Copy go mod and sum files
COPY go.mod go.sum ./

# Download all dependencies
RUN go mod download

# Copy the entire project source into the container
COPY . .

# Build the Go app, specifying the directory containing the entry point
RUN go build -o ./main ./cmd/api

## Stage 2: A minimal image to run the Go binary
FROM alpine:latest
#
## Set the Current Working Directory inside the container
WORKDIR /root/
#
## Copy the Pre-built binary file from the builder stage
COPY --from=builder /app/main .

# Expose port 8080 to the outside world
EXPOSE 8080

# Command to run the executable
CMD ["./main"]
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
test:
@go test ./... -v

start:
@go run cmd/api/api.go
build:
@go build -o bin/api cmd/api/api.go

start: build
./bin/api
96 changes: 96 additions & 0 deletions adapter/input/adapter_http/posts_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package adapter_http

Check failure on line 1 in adapter/input/adapter_http/posts_controller.go

View workflow job for this annotation

GitHub Actions / test-coverage

File test coverage below threshold

File test coverage below threshold: coverage: 0.0% (0/30); threshold: 60%

import (
"github.com/dexfs/go-twitter-clone/adapter/input/model/request"
"github.com/dexfs/go-twitter-clone/adapter/input/model/response"
"github.com/dexfs/go-twitter-clone/config/validation"
"github.com/dexfs/go-twitter-clone/internal/core/port/input"
"github.com/gin-gonic/gin"
"net/http"
)

type postsController struct {
createPostUseCase input.CreatePostUseCase
createRepostUseCase input.CreateRepostUseCase
createQuoteUseCase input.CreateQuoteUseCase
}

func NewPostsController(createPostUseCase input.CreatePostUseCase, createRepostUseCase input.CreateRepostUseCase, createQuoteUseCase input.CreateQuoteUseCase) *postsController {
return &postsController{
createPostUseCase: createPostUseCase,
createRepostUseCase: createRepostUseCase,
createQuoteUseCase: createQuoteUseCase,
}
}

func (pc *postsController) CreatePost(c *gin.Context) {
createPostRequest := request.CreatePostRequest{}

if err := c.ShouldBindJSON(&createPostRequest); err != nil {
errRest := validation.RestError(err)
c.JSON(errRest.Code, errRest)
return
}

postDomain, err := pc.createPostUseCase.Execute(input.CreatePostUseCaseInput{
UserID: createPostRequest.UserID,
Content: createPostRequest.Content,
})

if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}

c.JSON(http.StatusCreated, response.CreatePostResponse{
PostID: postDomain.ID,
})
}

func (pc *postsController) CreateRepost(c *gin.Context) {
createRequest := request.RepostRequest{}

if err := c.ShouldBindJSON(&createRequest); err != nil {
errRest := validation.RestError(err)
c.JSON(errRest.Code, errRest)
return
}

postDomain, err := pc.createRepostUseCase.Execute(input.CreateRepostUseCaseInput{
PostID: createRequest.PostID,
UserID: createRequest.UserID,
})

if err != nil {
c.JSON(http.StatusInternalServerError, err)
return
}

c.JSON(http.StatusCreated, response.CreatePostResponse{
PostID: postDomain.ID,
})
}

func (pc *postsController) CreateQuote(c *gin.Context) {
createRequest := request.QuoteRequest{}

if err := c.ShouldBindJSON(&createRequest); err != nil {
errRest := validation.RestError(err)
c.JSON(errRest.Code, errRest)
return
}

postDomain, err := pc.createQuoteUseCase.Execute(input.CreateQuoteUseCaseInput{
PostID: createRequest.PostID,
UserID: createRequest.UserID,
Quote: createRequest.Quote,
})

if err != nil {
c.JSON(http.StatusInternalServerError, err)
}

c.JSON(http.StatusCreated, response.CreatePostResponse{
PostID: postDomain.ID,
})
}
70 changes: 70 additions & 0 deletions adapter/input/adapter_http/users_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package adapter_http

import "C"
import (
"github.com/dexfs/go-twitter-clone/adapter/input/model/request"
"github.com/dexfs/go-twitter-clone/adapter/input/model/response"
"github.com/dexfs/go-twitter-clone/config/validation"
"github.com/dexfs/go-twitter-clone/internal/core/domain"
"github.com/dexfs/go-twitter-clone/internal/core/port/input"
"github.com/gin-gonic/gin"
"net/http"
)

type usersController struct {
getUserInfoUseCase input.GetUserInfoUseCase
getUserFeedUseCase input.GetUserFeedUseCase
}

func NewUsersController(getUserInfoUseCase input.GetUserInfoUseCase, getUserFeedUseCase input.GetUserFeedUseCase) *usersController {
return &usersController{
getUserInfoUseCase,
getUserFeedUseCase,
}
}

func (uc *usersController) GetInfo(c *gin.Context) {
userInfoRequest := request.UserInfoRequest{}

if err := c.ShouldBindUri(&userInfoRequest); err != nil {
errRest := validation.RestError(err)
c.JSON(errRest.Code, errRest)
return
}
userFeedDomain, err := uc.getUserInfoUseCase.Execute(userInfoRequest.Username)
if err != nil {
c.JSON(err.Code, err)
return
}

c.JSON(http.StatusOK, response.GetUserInfoResponse{
ID: userFeedDomain.ID,
Username: userFeedDomain.Username,
CreatedAt: userFeedDomain.CreatedAt.Format("2006-01-02"),
})
}

func (uc *usersController) GetFeed(c *gin.Context) {
userRequest := request.UserFeedRequest{}

if err := c.ShouldBindUri(&userRequest); err != nil {
errRest := validation.RestError(err)
c.JSON(errRest.Code, errRest)
return
}

userFeedDomain, err := uc.getUserFeedUseCase.Execute(userRequest.Username)
if err != nil {
c.JSON(err.Code, err)
return
}
var items []*domain.Post
if len(userFeedDomain) == 0 {
items = make([]*domain.Post, 0)
} else {
items = userFeedDomain
}
c.JSON(http.StatusOK, response.GetUserFeedResponse{
Items: items,
})
}
6 changes: 6 additions & 0 deletions adapter/input/model/request/post_create_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package request

type CreatePostRequest struct {
UserID string `json:"user_id" binding:"required,uuid4"`
Content string `json:"content" binding:"required,min=10,max=255"`
}
7 changes: 7 additions & 0 deletions adapter/input/model/request/quote_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package request

type QuoteRequest struct {
UserID string `json:"user_id" binding:"required,uuid4"`
PostID string `json:"post_id" binding:"required,uuid4"`
Quote string `json:"quote" binding:"required,min=10,max=100"`
}
6 changes: 6 additions & 0 deletions adapter/input/model/request/repost_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package request

type RepostRequest struct {
UserID string `json:"user_id" binding:"required,uuid4"`
PostID string `json:"post_id" binding:"required,uuid4"`
}
9 changes: 9 additions & 0 deletions adapter/input/model/request/user_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package request

type UserInfoRequest struct {
Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"`
}

type UserFeedRequest struct {
Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"`
}
5 changes: 5 additions & 0 deletions adapter/input/model/response/create_post_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package response

type CreatePostResponse struct {
PostID string `json:"post_id"`
}
13 changes: 13 additions & 0 deletions adapter/input/model/response/user_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package response

import "github.com/dexfs/go-twitter-clone/internal/core/domain"

type GetUserInfoResponse struct {
ID string `json:"id"`
Username string `json:"username"`
CreatedAt string `json:"created_at"`
}

type GetUserFeedResponse struct {
Items []*domain.Post `json:"items"`
}
61 changes: 61 additions & 0 deletions adapter/input/model/rest_errors/rest_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package rest_errors

import "net/http"

type RestError struct {
Message string `json:"message"`
Err string `json:"error"`
Code int `json:"code"`
Causes []Cause `json:"causes"`
}

type Cause struct {
Field string `json:"field"`
Message string `json:"message"`
}

func (r *RestError) Error() string {
return r.Message
}

func NewRestError(message string, err string, code int, causes []Cause) *RestError {
return &RestError{
Message: message,
Err: err,
Code: code,
Causes: causes,
}
}

func NewBadRequestError(message string) *RestError {
return &RestError{
Message: message,
Err: http.StatusText(http.StatusBadRequest),
Code: http.StatusBadRequest,
}
}

func NewBadRequestValidationError(message string, causes []Cause) *RestError {
return &RestError{
Message: message,
Err: http.StatusText(http.StatusBadRequest),
Code: http.StatusBadRequest,
Causes: causes,
}
}

func NewInternalServerError(message string) *RestError {
return &RestError{
Message: message,
Err: http.StatusText(http.StatusInternalServerError),
Code: http.StatusInternalServerError,
}
}

func NewNotFoundError(message string) *RestError {
return &RestError{
Message: message,
Err: http.StatusText(http.StatusNotFound),
Code: http.StatusNotFound,
}
}
62 changes: 62 additions & 0 deletions adapter/input/routes/gin_router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package routes

Check failure on line 1 in adapter/input/routes/gin_router.go

View workflow job for this annotation

GitHub Actions / test-coverage

File test coverage below threshold

File test coverage below threshold: coverage: 17.4% (4/23); threshold: 60%

import (
"github.com/dexfs/go-twitter-clone/adapter/input/adapter_http"
"github.com/dexfs/go-twitter-clone/internal/core/port/output"
"github.com/dexfs/go-twitter-clone/internal/core/usecase"
"github.com/fvbock/endless"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)

type AppServer struct {
Router *gin.Engine
addr string
}

func NewRouter(addr string) *AppServer {
//gin.SetMode(gin.DebugMode)
//gin.ForceConsoleColor()
r := gin.Default()
r.Use(cors.Default())
r.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})

return &AppServer{
Router: r,
addr: addr,
}
}

func (s *AppServer) Run(userRepo output.UserPort, postRepo output.PostPort) error {
err := s.Router.SetTrustedProxies(nil)
if err != nil {
return err
}

s.InitUserResources(userRepo, postRepo)
s.InitPostResources(userRepo, postRepo)
return endless.ListenAndServe(s.addr, s.Router)
}
func (s *AppServer) InitUserResources(userRepo output.UserPort, postRepo output.PostPort) {
getUserInfoService, _ := usecase.NewGetUserInfoUseCase(userRepo)
getUserFeedUseCase, _ := usecase.NewGetUserFeedUseCase(userRepo, postRepo)

usersController := adapter_http.NewUsersController(getUserInfoService, getUserFeedUseCase)

s.Router.GET("/users/:username/info", usersController.GetInfo)
s.Router.GET("/users/:username/feed", usersController.GetFeed)
}

func (s *AppServer) InitPostResources(userRepo output.UserPort, postRepo output.PostPort) {
createPostUseCase, _ := usecase.NewCreatePostUseCase(postRepo, userRepo)
createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, userRepo)
createQuoteUseCase, _ := usecase.NewCreateQuoteUseCase(postRepo, userRepo)

postsController := adapter_http.NewPostsController(createPostUseCase, createRepostUseCase, createQuoteUseCase)

s.Router.POST("/posts", postsController.CreatePost)
s.Router.POST("/posts/repost", postsController.CreateRepost)
s.Router.POST("/posts/quote", postsController.CreateQuote)
}
Loading
Loading