Compare commits

...

4 Commits

Author SHA1 Message Date
Muyao CHEN
8e14ccd12a feat: add the core project design 2024-10-04 00:01:16 +02:00
Muyao CHEN
dd49035c8e feat: add errno 2024-10-03 21:54:16 +02:00
Muyao CHEN
51474ce04d feat: gracefully shutdown impl 2024-10-03 21:18:07 +02:00
Muyao CHEN
ffaec01809 feat: add cors middleware 2024-10-03 20:53:56 +02:00
18 changed files with 323 additions and 7 deletions

View File

@ -1,5 +1,20 @@
# howmuch
<!--toc:start-->
- [howmuch](#howmuch)
- [Project Diary](#project-diary)
- [2024/09/30](#20240930)
- [2024/10/01](#20241001)
- [Config](#config)
- [Business logic](#business-logic)
- [Startup framework](#startup-framework)
- [2024/10/02](#20241002)
- [Logging](#logging)
- [Version](#version)
- [2024/10/03](#20241003)
- [2024/10/04](#20241004)
<!--toc:end-->
A tricount like expense-sharing system written in Go
---
@ -99,7 +114,7 @@ consumer queue, etc.
#### Startup framework
When business logic becomes complicated, we cannot spread them into a simple
`main` function. We need something to handle all thoses task, sync or async.
`main` function. We need something to handle all those task, sync or async.
That is why we use `cobra`.
So for this project, we will use the combination of `pflag`, `viper` and
@ -124,3 +139,59 @@ Set up the web server with some necessary/nice to have middlewares.
- Recovery, Logger (already included in Default mode)
- CORS
- RequestId
Using channel and signal to gracefully shutdown the server.
A more comprehensible error code design :
- Classical HTTP code.
- Service error code composed by "PlatformError.ServiceError", e.g. "ResourceNotFound.PageNotFound"
- error message.
The service error code helps to identify the problem more precisely.
### 2024/10/04
Application architecture design follows [Clean Architecture](https://manakuro.medium.com/clean-architecture-with-go-bce409427d31)
that has several layers:
- Entities: the models of the product
- Use cases: the core business rule
- Interface Adapters: convert data-in to entities and convert data-out to
output ports.
- Frameworks and drivers: Web server, DB.
Based on this logic, we create the following directories:
- `model`: entities
- `infra`: Provides the necessary functions to setup the infrastructure,
especially the DB (output-port), but also the router (input-port). Once
setup, we don't touch them anymore.
- `registry`: Provides a register function for the main to register a service.
It takes the pass to the output-port (ex.DBs) and gives back a pass
(controller) to the input-port
- `adapter`: Controllers are one of the adapters, when they are called,
they parse the user input and parse them into models and run the usecase
rules. Then they send back the response(input-port). For the output-port
part, the `repo` is the implementation of interfaces defined in `usecase/repo`.
- `usecase`: with the input of adapter, do what have to be done, and answer
with the result. In the meantime, we may have to store things into DBs.
Here we use the Repository model to decouple the implementation of the repo
with the interface. Thus in `usecase/repo` we only define interfaces.
Then it comes the real design for the app.
Following the Agile method, I don't try to define the entire project at the
beginning but step by step, starting at the user part.
```go
type User struct {
CreatedAt time.Time
UpdatedAt time.Time
FirstName string
LastName string
Email string
Password string
ID int
}
```

View File

@ -22,7 +22,9 @@
dev-mode: true
web:
addr: :8080
shutdown-timeout: 10
db:
# DB host

3
go.mod
View File

@ -4,6 +4,7 @@ go 1.23.1
require (
github.com/fsnotify/fsnotify v1.7.0
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/gosuri/uitable v0.0.4
@ -12,6 +13,7 @@ require (
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
go.uber.org/zap v1.27.0
golang.org/x/net v0.25.0
)
require (
@ -54,7 +56,6 @@ require (
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect

6
go.sum
View File

@ -19,6 +19,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
@ -141,8 +143,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

View File

@ -23,14 +23,24 @@
package howmuch
import (
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/core"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/log"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/middleware"
"git.vinchent.xyz/vinchent/howmuch/pkg/version/verflag"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/net/context"
)
var cfgFile string
@ -96,13 +106,56 @@ func run() error {
r := gin.Default()
// Middlewares
corsCfg := cors.DefaultConfig()
corsCfg.AllowAllOrigins = true
corsCfg.AllowHeaders = append(corsCfg.AllowHeaders, "Authorization", "Accept", "X-CSRF-Token")
r.Use(cors.New(corsCfg))
r.Use(middleware.RequestID())
r.NoRoute(func(ctx *gin.Context) {
core.WriteResponse(ctx, errno.PageNotFoundErr, nil)
})
r.GET("/", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
// time.Sleep(10 * time.Second) // Test shutdown
core.WriteResponse(ctx, nil, gin.H{
"message": "how much?",
})
})
r.Run(viper.GetString("addr"))
server := http.Server{
Addr: viper.GetString("web.addr"),
Handler: r,
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.FatalLog(err.Error())
}
}()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM, syscall.SIGINT)
<-signalChan
shutdownTimeout := viper.GetDuration("web.shutdown-timeout")
log.DebugLog("Shutdown", "timeout", shutdownTimeout)
ctx, cancel := context.WithTimeout(
context.Background(),
shutdownTimeout*time.Second,
)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.ErrorLog("Server forced shutdown", "err", err)
return err
}
log.InfoLog("Ciao!")
return nil
}

View File

View File

View File

View File

View File

View File

51
internal/pkg/core/core.go Normal file
View File

@ -0,0 +1,51 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package core
import (
"net/http"
"git.vinchent.xyz/vinchent/howmuch/internal/pkg/errno"
"github.com/gin-gonic/gin"
)
type ErrResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}
// WriteResponse writes the response to the HTTP response writer with HTTP code
// and potential errors.
func WriteResponse(c *gin.Context, err error, data any) {
// No error, write json response directly
if err == nil {
c.JSON(http.StatusOK, data)
return
}
error := errno.Decode(err)
c.JSON(error.HTTP, ErrResponse{
Code: error.Code,
Message: error.Message,
})
}

View File

@ -0,0 +1,41 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package errno
import "net/http"
var (
OK = &Errno{HTTP: http.StatusOK, Code: "", Message: ""}
InternalServerErr = &Errno{
HTTP: http.StatusInternalServerError,
Code: "InternalError",
Message: "Internal server error",
}
PageNotFoundErr = &Errno{
HTTP: http.StatusNotFound,
Code: "ResourceNotFound.PageNotFound",
Message: "Page not found",
}
)

View File

@ -0,0 +1,51 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package errno
type Errno struct {
HTTP int
Code string
Message string
}
// Error implements Error() method in error interface
func (err *Errno) Error() string {
return err.Message
}
// Decode tries to get the Errno from the error
func Decode(err error) *Errno {
if err == nil {
return OK
}
switch typed := err.(type) {
case *Errno:
return typed
default:
}
internErr := InternalServerErr
internErr.Message = err.Error()
return internErr
}

View File

@ -1,3 +1,25 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package middleware
import (

View File

@ -1,3 +1,25 @@
// MIT License
//
// Copyright (c) 2024 vinchent <vinchent@vinchent.xyz>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package middleware
import (