Skip to content

Commit 8c49c19

Browse files
committed
Initial commit
0 parents  commit 8c49c19

File tree

28 files changed

+999
-0
lines changed

28 files changed

+999
-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+
}

app/model.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package gwm_app
2+
3+
import (
4+
"reflect"
5+
"time"
6+
7+
"go.mongodb.org/mongo-driver/v2/bson"
8+
)
9+
10+
// An Identifier wrapper for database
11+
type ID string
12+
13+
type Model struct {
14+
ID ID `json:"id" bson:"_id"`
15+
CreatedAt time.Time `json:"createdAt" bson:"created_at"`
16+
UpdatedAt time.Time `json:"updatedAt" bson:"updated_at"`
17+
DeletedAt *time.Time `json:"deletedAt,omitempty" bson:"deleted_at"`
18+
}
19+
20+
func FromObjectID(objectID bson.ObjectID) ID {
21+
return ID(objectID.Hex())
22+
}
23+
24+
func RegisterIDToBsonRegistry(registry *bson.Registry) {
25+
registry.RegisterTypeEncoder(IDType, &IDEncoder{})
26+
}
27+
28+
var IDType = reflect.TypeOf(ID(bson.NewObjectID().Hex()))
29+
30+
type IDEncoder struct{}
31+
32+
func (e *IDEncoder) EncodeValue(ectx bson.EncodeContext, vw bson.ValueWriter, val reflect.Value) error {
33+
if val.Type() != IDType {
34+
return bson.ValueDecoderError{
35+
Name: "IDEncodeValue", Types: []reflect.Type{IDType}, Received: val,
36+
}
37+
}
38+
if val.String() == "" {
39+
return vw.WriteNull()
40+
}
41+
objectID, err := bson.ObjectIDFromHex(val.String())
42+
if err != nil {
43+
return err
44+
}
45+
return vw.WriteObjectID(objectID)
46+
}

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.ConfigKeyGORMDatabaseDriver)
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.ConfigKeyGORMDatabaseHost),
45+
gwm_app.Config().GetString(internal.ConfigKeyGORMDatabasePort),
46+
gwm_app.Config().GetString(internal.ConfigKeyGORMDatabaseUsername),
47+
gwm_app.Config().GetString(internal.ConfigKeyGORMDatabaseName),
48+
gwm_app.Config().GetString(internal.ConfigKeyGORMDatabasePassword),
49+
gwm_app.Config().GetString(internal.ConfigKeyGORMDatabaseSSLMode),
50+
)
51+
db, err = gorm.Open(postgres.Open(dsn))
52+
if err != nil {
53+
return nil, err
54+
}
55+
return db, nil
56+
}

0 commit comments

Comments
 (0)