diff --git a/.gitignore b/.gitignore index c061dd8..b44d593 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ go.work.sum # Custom /_output /deployment/db_data +/tmp/** diff --git a/api/openapi/openapi.yaml b/api/openapi/openapi.yaml index f4eb877..4b0b222 100644 --- a/api/openapi/openapi.yaml +++ b/api/openapi/openapi.yaml @@ -39,7 +39,7 @@ tags: - name: user paths: - /signup: + /user/signup: post: tags: - user diff --git a/docs/.keep b/docs/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/error_code.md b/docs/error_code.md new file mode 100644 index 0000000..12a3bbc --- /dev/null +++ b/docs/error_code.md @@ -0,0 +1,7 @@ +# Platform level error code design + +- InternalError +- InvalidParameter +- AuthFailure +- ResourceNotFound +- FailedOperation diff --git a/internal/howmuch/adapter/controller/user.go b/internal/howmuch/adapter/controller/user.go index ccb80bf..4d650a9 100644 --- a/internal/howmuch/adapter/controller/user.go +++ b/internal/howmuch/adapter/controller/user.go @@ -25,33 +25,66 @@ package controller import ( "net/http" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/model" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase" "git.vinchent.xyz/vinchent/howmuch/internal/pkg/core" + "git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno" + "github.com/gin-gonic/gin" ) // User is the user controller interface, it describes all the handlers // that need to be implemented for the /user endpoint type User interface { Signup(core.Context) - UpdateInfo(core.Context) - Login(core.Context) - Logout(core.Context) - ChangePassword(core.Context) + UpdateInfo(*gin.Context) + Login(*gin.Context) + Logout(*gin.Context) + ChangePassword(*gin.Context) } -type UserController struct{} +type UserController struct { + userUsecase usecase.User +} + +var UserParamsErr = &errno.Errno{ + HTTP: http.StatusBadRequest, + Code: errno.ErrorCode(errno.InvalidParameterCode, "UserParamsErr"), + Message: "user info is not correct", +} + +func NewUserController(us usecase.User) User { + return &UserController{ + userUsecase: us, + } +} func (uc *UserController) Signup(ctx core.Context) { - ctx.JSON(http.StatusOK, "hello") + var params model.User + + if err := ctx.Bind(¶ms); err != nil { + core.WriteResponse(ctx, UserParamsErr, nil) + return + } + + // TODO: check params validity (govalidator) + + _, err := uc.userUsecase.Create(ctx, ¶ms) + if err != nil { + core.WriteResponse(ctx, err, nil) + return + } + + core.WriteResponse(ctx, errno.OK, nil) } -func (uc *UserController) UpdateInfo(ctx core.Context) { +func (uc *UserController) UpdateInfo(ctx *gin.Context) { } -func (uc *UserController) Login(ctx core.Context) { +func (uc *UserController) Login(ctx *gin.Context) { } -func (uc *UserController) Logout(ctx core.Context) { +func (uc *UserController) Logout(ctx *gin.Context) { } -func (uc *UserController) ChangePassword(ctx core.Context) { +func (uc *UserController) ChangePassword(ctx *gin.Context) { } diff --git a/internal/howmuch/adapter/repo/user.go b/internal/howmuch/adapter/repo/user.go index df23842..80a48bc 100644 --- a/internal/howmuch/adapter/repo/user.go +++ b/internal/howmuch/adapter/repo/user.go @@ -34,9 +34,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -// Get business service model, convert to the DB model (generated by sqlc) -// To test repo's methods, I have to mock a pgx.Conn - type userRepository struct { db *pgx.Conn } @@ -67,9 +64,9 @@ func (ur *userRepository) Create( UpdatedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, } - tx, ok := transaction.(*pgx.Conn) + tx, ok := transaction.(pgx.Tx) if !ok { - return nil, errors.New("transaction is not a *pgx.Conn") + return nil, errors.New("transaction is not a pgx.Tx") } queries := sqlc.New(tx) diff --git a/internal/howmuch/infra/router/router.go b/internal/howmuch/infra/router/router.go index 540dc1d..bfcecf5 100644 --- a/internal/howmuch/infra/router/router.go +++ b/internal/howmuch/infra/router/router.go @@ -48,7 +48,13 @@ func Routes(engine *gin.Engine, c controller.AppController) *gin.Engine { core.WriteResponse(ctx, errno.PageNotFoundErr, nil) }) - engine.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) }) + v1 := engine.Group("/v1") + { + userV1 := v1.Group("/user") + { + userV1.POST("/signup", func(ctx *gin.Context) { c.User.Signup(ctx) }) + } + } return engine } diff --git a/internal/howmuch/registry/user.go b/internal/howmuch/registry/user.go index 036e1d6..776a03f 100644 --- a/internal/howmuch/registry/user.go +++ b/internal/howmuch/registry/user.go @@ -22,9 +22,14 @@ package registry -import "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller" +import ( + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/controller" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/adapter/repo" + "git.vinchent.xyz/vinchent/howmuch/internal/howmuch/usecase/usecase" +) // NewUserController returns a user controller's implementation func (r *registry) NewUserController() controller.User { - return &controller.UserController{} + u := usecase.NewUserUsecase(repo.NewUserRepository(r.db), repo.NewDBRepository(r.db)) + return controller.NewUserController(u) } diff --git a/internal/howmuch/usecase/usecase/user.go b/internal/howmuch/usecase/usecase/user.go index ca5c46f..76bc9f9 100644 --- a/internal/howmuch/usecase/usecase/user.go +++ b/internal/howmuch/usecase/usecase/user.go @@ -51,6 +51,10 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.User) (*model.User, data, err := uuc.dbRepo.Transaction( ctx, func(txCtx context.Context, tx interface{}) (interface{}, error) { + // TODO: should check if the user exists + // DB will return an error since we have set email to UNIQUE. + // But we may not want to expose the exact db error. + u, err := uuc.userRepo.Create(txCtx, tx, u) if err != nil { return nil, err @@ -68,11 +72,12 @@ func (uuc *userUsecase) Create(ctx context.Context, u *model.User) (*model.User, return u, err }, ) - user := data.(*model.User) - if err != nil { + // TODO: We should wrap the error at service level return nil, err } + user := data.(*model.User) + return user, nil } diff --git a/internal/pkg/core/context.go b/internal/pkg/core/context.go index 832d46e..3d15dde 100644 --- a/internal/pkg/core/context.go +++ b/internal/pkg/core/context.go @@ -22,6 +22,18 @@ package core +import "time" + type Context interface { + // Context + Deadline() (deadline time.Time, ok bool) + Done() <-chan struct{} + Err() error + Value(key any) any + + // Request + Bind(obj any) error + + // Response JSON(code int, obj any) } diff --git a/internal/pkg/errno/code.go b/internal/pkg/errno/code.go index 23b0a7c..e332767 100644 --- a/internal/pkg/errno/code.go +++ b/internal/pkg/errno/code.go @@ -24,18 +24,28 @@ package errno import "net/http" +type PlatformLevelErrCode string + +const ( + InternalErrorCode = "InternalError" + InvalidParameterCode = "InvalidParameter" + AuthFailureCode = "AuthFailure" + ResourceNotFoundCode = "ResourceNotFound" + FailedOperationCode = "FailedOperation" +) + var ( OK = &Errno{HTTP: http.StatusOK, Code: "", Message: ""} InternalServerErr = &Errno{ HTTP: http.StatusInternalServerError, - Code: "InternalError", + Code: InternalErrorCode, Message: "Internal server error", } PageNotFoundErr = &Errno{ HTTP: http.StatusNotFound, - Code: "ResourceNotFound.PageNotFound", + Code: ErrorCode(ResourceNotFoundCode, "PageNotFound"), Message: "Page not found", } ) diff --git a/internal/pkg/errno/errno.go b/internal/pkg/errno/errno.go index 25fa3c8..ddc4afe 100644 --- a/internal/pkg/errno/errno.go +++ b/internal/pkg/errno/errno.go @@ -28,6 +28,10 @@ type Errno struct { Message string } +func ErrorCode(platformErrCode string, resourceErrCode string) string { + return platformErrCode + "." + resourceErrCode +} + // Error implements Error() method in error interface func (err *Errno) Error() string { return err.Message diff --git a/tmp/build-errors.log b/tmp/build-errors.log deleted file mode 100644 index 0e06a41..0000000 --- a/tmp/build-errors.log +++ /dev/null @@ -1 +0,0 @@ -exit status 2exit status 2exit status 2exit status 2exit status 2 \ No newline at end of file