Skip to content

Commit 606b638

Browse files
committed
Initial commit
0 parents  commit 606b638

File tree

25 files changed

+719
-0
lines changed

25 files changed

+719
-0
lines changed

.github/workflows/deploy-mdbook.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Deploy mdBook
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
permissions:
9+
pages: write
10+
id-token: write
11+
12+
jobs:
13+
build-and-deploy:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
- name: Install Rust (rustup)
19+
run: rustup update stable --no-self-update && rustup default stable
20+
- name: Install mdBook
21+
run: cargo install mdbook
22+
- name: Build doc
23+
run: mdbook build doc
24+
- name: Configure GitHub Pages
25+
uses: actions/configure-pages@v5
26+
- name: Upload Pages artifact
27+
uses: actions/upload-pages-artifact@v3
28+
with:
29+
path: doc/book
30+
- name: Deploy to GitHub Pages
31+
uses: actions/deploy-pages@v4

.github/workflows/lint.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Lint
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@v4
15+
- name: Run golangci-lint
16+
uses: golangci/golangci-lint-action@v8

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"go.testFlags": ["-v", "-count=1"],
3+
"[markdown]": {
4+
"editor.formatOnSave": true,
5+
"editor.formatOnPaste": true
6+
}
7+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 slhmy
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# GWM (Golang Web Modules)
2+
3+
Solution collection for quickly setting up
4+
Golang web applications (Currently especially for Gin framework).
5+
6+
## Why GWM?
7+
8+
GWM maintains the best practices and patterns for building web applications in Go.
9+
If painlessly building a web application in Go is your goal, GWM is the solution.
10+
It provides a solid foundation for your application,
11+
allowing you to focus on building features rather than boilerplate code.
12+
13+
### Application Core
14+
15+
- **Configuration Store** depends on [viper](https://github.com/spf13/viper) for managing application configuration.
16+
- **Slog Logger** with contextual logging and support (using [tint](github.com/lmittmann/tint) for colorized output).
17+
18+
### Clients
19+
20+
- **MongoDB Driver** for MongoDB database operations.
21+
- **Redis Clients** for Redis operations and other Redis-based services (For example, distributed locks & caching).
22+
23+
### Modules
24+
25+
- **Gin** for Gin framework, including middleware and helpers.
26+
27+
See more in [documentation](https://slhmy.github.io/go-webmods).

app/config.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package gwm_app
2+
3+
import (
4+
"os"
5+
"path"
6+
"strings"
7+
8+
"github.com/spf13/viper"
9+
)
10+
11+
const (
12+
envRelativeConfigPath = "GWM_RELATIVE_CONFIG_PATH"
13+
envOverrideConfigName = "GWM_OVERRIDE_CONFIG_NAME"
14+
15+
defaultConfigName = "config"
16+
defaultOverrideConfigName = "override"
17+
)
18+
19+
var (
20+
config *viper.Viper
21+
)
22+
23+
func Config() *viper.Viper {
24+
return config
25+
}
26+
27+
func init() {
28+
cwd, _ := os.Getwd()
29+
relativeConfigPath := os.Getenv(envRelativeConfigPath)
30+
viper.AddConfigPath(path.Join(cwd, relativeConfigPath))
31+
viper.SetConfigName(defaultConfigName)
32+
err := viper.ReadInConfig()
33+
if err != nil {
34+
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
35+
panic(err)
36+
}
37+
}
38+
overrideConfigName := os.Getenv(envOverrideConfigName)
39+
if len(overrideConfigName) == 0 {
40+
overrideConfigName = defaultOverrideConfigName
41+
}
42+
viper.SetConfigName(overrideConfigName)
43+
err = viper.MergeInConfig()
44+
if err != nil {
45+
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
46+
panic(err)
47+
}
48+
}
49+
viper.AutomaticEnv()
50+
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
51+
config = viper.GetViper()
52+
}

app/error.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package gwm_app
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
7+
"github.com/gin-gonic/gin"
8+
)
9+
10+
const ErrorMetaFieldHttpStatus = "httpStatus"
11+
12+
type Error struct {
13+
Err error
14+
Meta map[string]any
15+
}
16+
17+
func (e Error) Error() string {
18+
return e.Err.Error()
19+
}
20+
21+
func (e Error) GinError() *gin.Error {
22+
return &gin.Error{
23+
Err: e.Err,
24+
Type: gin.ErrorTypePublic,
25+
Meta: e.Meta,
26+
}
27+
}
28+
29+
func NewError(err error, httpStatus int, meta map[string]any) Error {
30+
if meta == nil {
31+
meta = make(map[string]any)
32+
}
33+
meta[ErrorMetaFieldHttpStatus] = httpStatus
34+
return Error{
35+
Err: err,
36+
Meta: meta,
37+
}
38+
}
39+
40+
var ErrUnauthorized = NewError(errors.New("unauthorized"), http.StatusUnauthorized, nil)
41+
42+
func NewNotFoundError(target, by string) Error {
43+
meta := map[string]any{}
44+
if target != "" {
45+
meta["target"] = target
46+
}
47+
if by != "" {
48+
meta["by"] = by
49+
}
50+
return NewError(errors.New("not found"), http.StatusNotFound, meta)
51+
}
52+
53+
func NewForbiddenError(reason string) Error {
54+
meta := map[string]any{
55+
"reason": reason,
56+
}
57+
return NewError(errors.New("forbidden"), http.StatusForbidden, meta)
58+
}
59+
60+
func NewBadRequestError(reason string) Error {
61+
meta := map[string]any{
62+
"reason": reason,
63+
}
64+
return NewError(errors.New("bad request"), http.StatusBadRequest, meta)
65+
}

clients/gorm/db.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package gwm_gorm
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
gwm_app "github.com/slhmy/go-webmods/app"
8+
"github.com/slhmy/go-webmods/internal"
9+
"gorm.io/driver/postgres"
10+
"gorm.io/gorm"
11+
)
12+
13+
var (
14+
initMutx sync.Mutex
15+
db *gorm.DB
16+
)
17+
18+
func GetDB() *gorm.DB {
19+
if db == nil {
20+
initMutx.Lock()
21+
defer initMutx.Unlock()
22+
if db != nil {
23+
return db
24+
}
25+
26+
driver := gwm_app.Config().GetString(internal.ConfigKeyDatabaseDriver)
27+
switch driver {
28+
case "postgres":
29+
db, err := openPostgres()
30+
if err != nil {
31+
panic(err)
32+
}
33+
return db
34+
default:
35+
panic(fmt.Sprintf("unsupported database driver: %s", driver))
36+
}
37+
}
38+
return db
39+
}
40+
41+
func openPostgres() (db *gorm.DB, err error) {
42+
dsn := fmt.Sprintf(
43+
"host=%s port=%s user=%s dbname=%s password=%s sslmode=%s",
44+
gwm_app.Config().GetString(internal.ConfigKeyDatabaseHost),
45+
gwm_app.Config().GetString(internal.ConfigKeyDatabasePort),
46+
gwm_app.Config().GetString(internal.ConfigKeyDatabaseUsername),
47+
gwm_app.Config().GetString(internal.ConfigKeyDatabaseName),
48+
gwm_app.Config().GetString(internal.ConfigKeyDatabasePassword),
49+
gwm_app.Config().GetString(internal.ConfigKeyDatabaseSSLMode),
50+
)
51+
db, err = gorm.Open(postgres.Open(dsn))
52+
if err != nil {
53+
return nil, err
54+
}
55+
return db, nil
56+
}

clients/redis/rdb.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package gwm_redis
2+
3+
import (
4+
"sync"
5+
6+
"github.com/redis/go-redis/v9"
7+
gwm_app "github.com/slhmy/go-webmods/app"
8+
"github.com/slhmy/go-webmods/internal"
9+
)
10+
11+
var (
12+
rdbInitMutex sync.Mutex
13+
rdbClient redis.UniversalClient
14+
)
15+
16+
func GetRDB() redis.UniversalClient {
17+
if rdbClient == nil {
18+
rdbInitMutex.Lock()
19+
defer rdbInitMutex.Unlock()
20+
if rdbClient != nil {
21+
return rdbClient
22+
}
23+
24+
addrs := gwm_app.Config().GetStringSlice(internal.ConfigKeyRedisAddrs)
25+
password := gwm_app.Config().GetString(internal.ConfigKeyRedisPassword)
26+
if len(addrs) == 0 {
27+
panic("No redis hosts configured")
28+
}
29+
if len(addrs) == 1 {
30+
rdbClient = redis.NewClient(&redis.Options{
31+
Addr: addrs[0],
32+
Password: password,
33+
})
34+
}
35+
if len(addrs) > 1 {
36+
rdbClient = redis.NewClusterClient(&redis.ClusterOptions{
37+
Addrs: addrs,
38+
Password: password,
39+
})
40+
}
41+
}
42+
return rdbClient
43+
}

doc/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
book

doc/book.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[book]
2+
authors = ["slhmy"]
3+
language = "en"
4+
src = "src"
5+
title = "Golang Web Modules"

doc/src/SUMMARY.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!-- markdownlint-enable MD025 -->
2+
3+
# Summary
4+
5+
# Application Core
6+
7+
- [Config](app/config.md)
8+
- [Service Error](app/service_error.md)
9+
10+
# Utilities
11+
12+
- [Gin Error Handler](utils/gin/error_handler.md)

doc/src/app/config.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Config
2+
3+
Read configuration from config files or environment variables.
4+
5+
Config namings are recommended to be in snake_case and separated by dots.
6+
7+
You can set configs in `conifg` file and override them in `override` file
8+
(multiple file extensions are supported, such as `.yaml`, `.toml`, `.json`, etc.).
9+
10+
In some cases, using environment variables is more convenient.
11+
You can override the final config by making every character uppercase and replacing `.` with `_`
12+
(e.g. `server.port` becomes `SERVER_PORT`),
13+
environment variables have the highest priority.
14+
15+
## Example
16+
17+
```go
18+
os.Setenv("SERVER_PORT", "443")
19+
port = gwm_app.Config().GetInt("server.port")
20+
// port = 443
21+
```
22+
23+
## Customize
24+
25+
Customize the config file path by setting the `GWM_RELATIVE_CONFIG_PATH` environment variable.
26+
Set the override config file name by setting the `GWM_OVERRIDE_CONFIG_NAME` environment variable.
27+
28+
The following environment variables should be set before running the application.

doc/src/app/service_error.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Service Error

doc/src/utils/gin/error_handler.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Gin Error Handler

0 commit comments

Comments
 (0)