diff options
| author | Bobby <[email protected]> | 2026-01-15 15:53:17 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-01-15 15:53:17 +0530 |
| commit | c8d0bbb5b54f5cec3ebb245f9a21d8a94b3bd944 (patch) | |
| tree | 6a5c2500da90253ad07a0d5192071bb77f093d36 | |
| download | cafe-c8d0bbb5b54f5cec3ebb245f9a21d8a94b3bd944.tar.xz cafe-c8d0bbb5b54f5cec3ebb245f9a21d8a94b3bd944.zip | |
Add initial project structure with Go Fiber framework and environment configuration
| -rw-r--r-- | .gitignore | 44 | ||||
| -rw-r--r-- | Makefile | 44 | ||||
| -rw-r--r-- | cafe/main.go | 33 | ||||
| -rw-r--r-- | config/config.go | 22 | ||||
| -rw-r--r-- | config/env.go | 8 | ||||
| -rw-r--r-- | go.mod | 22 | ||||
| -rw-r--r-- | go.sum | 29 | ||||
| -rw-r--r-- | router/base.go | 16 | ||||
| -rw-r--r-- | router/router.go | 22 | ||||
| -rw-r--r-- | types/http.go | 13 | ||||
| -rw-r--r-- | utils/env/functions.go | 97 | ||||
| -rw-r--r-- | utils/env/parser.go | 28 | ||||
| -rw-r--r-- | utils/env/setters.go | 107 | ||||
| -rw-r--r-- | utils/env/validators.go | 15 | ||||
| -rw-r--r-- | utils/urls/attach.go | 38 | ||||
| -rw-r--r-- | utils/urls/namespace.go | 7 | ||||
| -rw-r--r-- | utils/urls/path.go | 51 | ||||
| -rw-r--r-- | utils/urls/registery.go | 28 |
18 files changed, 624 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1cc003b --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Binaries +bin/ +tmp/ +*.exe +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories +vendor/ + +# Go workspace file +go.work + +# Environment variables +.env +.env.local +.env.dev + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Database +*.db +*.sqlite +*.sqlite3 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9a00c39 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +BINARY_NAME=cafe +BUILD_PATH=bin/$(BINARY_NAME) +MAIN_PATH=$(BINARY_NAME)/main.go + +.PHONY: setup clean tidy build run dev test all + +setup: + @echo "Setting up environment..." + @go mod download + @go mod tidy + @echo "Environment setup complete." + +clean: + @echo "Cleaning up..." + @rm -rf bin + @rm -rf tmp + @echo "Cleanup complete." + +tidy: + @echo "Tidying modules..." + @go mod tidy + @echo "Modules tidied." + +build: + @echo "Building..." + @go build -o $(BUILD_PATH) $(MAIN_PATH) || true + @echo "Build complete." + +run: + @if [ ! -f $(BUILD_PATH) ]; then echo "Binary not found. Building binary..."; $(MAKE) -s build; fi + @echo "Running..." + @$(BUILD_PATH) || true + +dev: + @echo "Running in development mode..." + @go run $(MAIN_PATH) || true + +test: + @echo "Running tests..." + @go test -v ./... || true + +all: setup clean build run + +.SILENT:
\ No newline at end of file diff --git a/cafe/main.go b/cafe/main.go new file mode 100644 index 0000000..66b1860 --- /dev/null +++ b/cafe/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "cafe/config" + "cafe/router" + "fmt" + "log" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/helmet" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/recover" +) + +func main() { + app := fiber.New(fiber.Config{ + ErrorHandler: router.ErrorHandler, + }) + + app.Use(logger.New()) + app.Use(recover.New()) + app.Use(helmet.New(helmet.Config{ + CrossOriginEmbedderPolicy: "unsafe-none", + })) + app.Use(cors.New()) + + router.Initialize(app) + + address := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port) + log.Printf("Starting server at %s\n", address) + log.Fatal(app.Listen(address)) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..85f947c --- /dev/null +++ b/config/config.go @@ -0,0 +1,22 @@ +package config + +import ( + "cafe/utils/env" + "log" + + "github.com/joho/godotenv" +) + +var ( + Server server +) + +func init() { + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using environment variables") + } + + if err := env.Parse(&Server); err != nil { + log.Fatalf("Failed to parse ServerConfig: %v", err) + } +} diff --git a/config/env.go b/config/env.go new file mode 100644 index 0000000..bdac2b4 --- /dev/null +++ b/config/env.go @@ -0,0 +1,8 @@ +package config + +type server struct { + Host string `env:"SERVER_HOST" default:"localhost"` + Port int `env:"SERVER_PORT" default:"8080"` + AppSecret string `env:"APP_SECRET" default:"mysecret"` + DevMode bool `env:"DEV_MODE" default:"true"` +} @@ -0,0 +1,22 @@ +module cafe + +go 1.25.5 + +require ( + github.com/gofiber/fiber/v2 v2.52.10 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/x/sys v0.28.0 // indirect +) @@ -0,0 +1,29 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/gofiber/fiber/v2 v2.52.10 h1:jRHROi2BuNti6NYXmZ6gbNSfT3zj/8c0xy94GOU5elY= +github.com/gofiber/fiber/v2 v2.52.10/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/router/base.go b/router/base.go new file mode 100644 index 0000000..f453ba2 --- /dev/null +++ b/router/base.go @@ -0,0 +1,16 @@ +package router + +import ( + "cafe/types" + "cafe/utils/urls" + + "github.com/gofiber/fiber/v2" +) + +func init() { + urls.SetNamespace("") + + urls.Path(types.GET, "/", func(c *fiber.Ctx) error { + return c.SendString("Welcome to Shifoo's Cafe") + }, "home") +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..c44d6d3 --- /dev/null +++ b/router/router.go @@ -0,0 +1,22 @@ +package router + +import ( + "cafe/utils/urls" + + "github.com/gofiber/fiber/v2" +) + +func Initialize(router *fiber.App) { + router.Static("/static", "./static") + + urls.Attach(router) +} + +func ErrorHandler(ctx *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + if e, ok := err.(*fiber.Error); ok { + code = e.Code + } + + return ctx.Status(code).SendString(err.Error()) +} diff --git a/types/http.go b/types/http.go new file mode 100644 index 0000000..bc85d99 --- /dev/null +++ b/types/http.go @@ -0,0 +1,13 @@ +package types + +type HTTPMethod string + +const ( + GET HTTPMethod = "GET" + POST HTTPMethod = "POST" + PUT HTTPMethod = "PUT" + PATCH HTTPMethod = "PATCH" + DELETE HTTPMethod = "DELETE" + OPTIONS HTTPMethod = "OPTIONS" + HEAD HTTPMethod = "HEAD" +) diff --git a/utils/env/functions.go b/utils/env/functions.go new file mode 100644 index 0000000..de03793 --- /dev/null +++ b/utils/env/functions.go @@ -0,0 +1,97 @@ +package env + +import ( + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func getEnv(key, defaultVal string) string { + if value := os.Getenv(key); value != "" { + return value + } + return defaultVal +} + +func getEnvBool(key string, defaultVal bool) bool { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseBool(value); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvDuration(key string, defaultVal time.Duration) time.Duration { + if value := os.Getenv(key); value != "" { + if parsed, err := time.ParseDuration(value); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvInt(key string, defaultVal int64) int64 { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseInt(value, 10, 64); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvFloat(key string, defaultVal float64) float64 { + if value := os.Getenv(key); value != "" { + if parsed, err := strconv.ParseFloat(value, 64); err == nil { + return parsed + } + } + return defaultVal +} + +func getEnvStringSlice(key string, defaultVal []string) []string { + if value := os.Getenv(key); value != "" { + parts := strings.Split(value, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result + } + return defaultVal +} + +func Defaults[T any](config *T) *T { + v := reflect.ValueOf(config) + if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { + return config + } + + elem := v.Elem() + t := elem.Type() + newStruct := reflect.New(t) + newElem := newStruct.Elem() + + for i := range elem.NumField() { + field := newElem.Field(i) + fieldType := t.Field(i) + + if !field.CanSet() { + continue + } + + defaultVal := fieldType.Tag.Get("default") + if defaultVal == "" { + continue + } + + setFieldDefault(field, defaultVal) + } + + return newStruct.Interface().(*T) +} diff --git a/utils/env/parser.go b/utils/env/parser.go new file mode 100644 index 0000000..c1fdb53 --- /dev/null +++ b/utils/env/parser.go @@ -0,0 +1,28 @@ +package env + +func Parse(config any) error { + elem, t, err := validateConfigInput(config) + if err != nil { + return err + } + + for i := range elem.NumField() { + field := elem.Field(i) + fieldType := t.Field(i) + + if !field.CanSet() { + continue + } + + envKey := fieldType.Tag.Get("env") + defaultVal := fieldType.Tag.Get("default") + + if envKey == "" { + continue + } + + setFieldFromEnv(field, envKey, defaultVal) + } + + return nil +} diff --git a/utils/env/setters.go b/utils/env/setters.go new file mode 100644 index 0000000..7e42274 --- /dev/null +++ b/utils/env/setters.go @@ -0,0 +1,107 @@ +package env + +import ( + "os" + "reflect" + "strconv" + "strings" + "time" +) + +func setFieldFromEnv(field reflect.Value, envKey, defaultVal string) { + switch field.Kind() { + case reflect.String: + field.SetString(getEnv(envKey, defaultVal)) + case reflect.Bool: + defaultBool, _ := strconv.ParseBool(defaultVal) + field.SetBool(getEnvBool(envKey, defaultBool)) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + defaultInt, _ := strconv.ParseInt(defaultVal, 10, 64) + field.SetInt(getEnvInt(envKey, defaultInt)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + defaultUint, _ := strconv.ParseUint(defaultVal, 10, 64) + setUintField(field, envKey, defaultUint) + case reflect.Float32, reflect.Float64: + defaultFloat, _ := strconv.ParseFloat(defaultVal, 64) + field.SetFloat(getEnvFloat(envKey, defaultFloat)) + case reflect.Slice: + setSliceField(field, envKey, defaultVal) + default: + setDurationField(field, envKey, defaultVal) + } +} + +func setUintField(field reflect.Value, envKey string, defaultVal uint64) { + if value := os.Getenv(envKey); value != "" { + if parsed, err := strconv.ParseUint(value, 10, 64); err == nil { + field.SetUint(parsed) + return + } + } + field.SetUint(defaultVal) +} + +func setDurationField(field reflect.Value, envKey, defaultVal string) { + if field.Type() == reflect.TypeFor[time.Duration]() { + defaultDuration, _ := time.ParseDuration(defaultVal) + field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration))) + } +} + +func setSliceField(field reflect.Value, envKey, defaultVal string) { + if field.Type().Elem().Kind() == reflect.String { + var defaultSlice []string + if defaultVal != "" { + parts := strings.Split(defaultVal, ",") + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + defaultSlice = append(defaultSlice, trimmed) + } + } + } + result := getEnvStringSlice(envKey, defaultSlice) + field.Set(reflect.ValueOf(result)) + } +} + +func setFieldDefault(field reflect.Value, defaultVal string) { + switch field.Kind() { + case reflect.String: + field.SetString(defaultVal) + case reflect.Bool: + if defaultBool, err := strconv.ParseBool(defaultVal); err == nil { + field.SetBool(defaultBool) + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if defaultInt, err := strconv.ParseInt(defaultVal, 10, 64); err == nil { + field.SetInt(defaultInt) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if defaultUint, err := strconv.ParseUint(defaultVal, 10, 64); err == nil { + field.SetUint(defaultUint) + } + case reflect.Float32, reflect.Float64: + if defaultFloat, err := strconv.ParseFloat(defaultVal, 64); err == nil { + field.SetFloat(defaultFloat) + } + case reflect.Slice: + if field.Type().Elem().Kind() == reflect.String && defaultVal != "" { + parts := strings.Split(defaultVal, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + field.Set(reflect.ValueOf(result)) + } + default: + if field.Type() == reflect.TypeFor[time.Duration]() { + if defaultDuration, err := time.ParseDuration(defaultVal); err == nil { + field.Set(reflect.ValueOf(defaultDuration)) + } + } + } +} diff --git a/utils/env/validators.go b/utils/env/validators.go new file mode 100644 index 0000000..fa9b17f --- /dev/null +++ b/utils/env/validators.go @@ -0,0 +1,15 @@ +package env + +import ( + "fmt" + "reflect" +) + +func validateConfigInput(config any) (reflect.Value, reflect.Type, error) { + v := reflect.ValueOf(config) + if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct { + return reflect.Value{}, nil, fmt.Errorf("config must be a pointer to struct") + } + elem := v.Elem() + return elem, elem.Type(), nil +} diff --git a/utils/urls/attach.go b/utils/urls/attach.go new file mode 100644 index 0000000..7d5d732 --- /dev/null +++ b/utils/urls/attach.go @@ -0,0 +1,38 @@ +package urls + +import ( + "cafe/types" + "log" + + "github.com/gofiber/fiber/v2" +) + +var methodBinders = map[types.HTTPMethod]func(fiber.Router, string, fiber.Handler) fiber.Router{ + types.GET: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Get(path, h) }, + types.POST: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Post(path, h) }, + types.PUT: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Put(path, h) }, + types.PATCH: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Patch(path, h) }, + types.DELETE: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Delete(path, h) }, + types.OPTIONS: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Options(path, h) }, + types.HEAD: func(r fiber.Router, path string, h fiber.Handler) fiber.Router { return r.Head(path, h) }, +} + +func Attach(app *fiber.App) { + namespaceGroups := make(map[string]fiber.Router) + + for fullName, route := range registry.routes { + group, exists := namespaceGroups[route.namespace] + if !exists { + group = app.Group("/" + route.namespace) + namespaceGroups[route.namespace] = group + } + + binder, ok := methodBinders[route.method] + if !ok { + log.Fatalf("%s", "unsupported HTTP method: "+string(route.method)) + } + + fiberRoute := binder(group, route.path, route.handler) + fiberRoute.Name(fullName) + } +} diff --git a/utils/urls/namespace.go b/utils/urls/namespace.go new file mode 100644 index 0000000..7bb5311 --- /dev/null +++ b/utils/urls/namespace.go @@ -0,0 +1,7 @@ +package urls + +func SetNamespace(namespace string) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + registry.currentNamespace = namespace +} diff --git a/utils/urls/path.go b/utils/urls/path.go new file mode 100644 index 0000000..e24ed58 --- /dev/null +++ b/utils/urls/path.go @@ -0,0 +1,51 @@ +package urls + +import ( + "cafe/types" + "strings" + + "github.com/gofiber/fiber/v2" +) + +func Path(method types.HTTPMethod, path string, handler fiber.Handler, name string) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + namespace := registry.currentNamespace + fullName := name + fullPath := path + + if namespace != "" { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + fullName = namespace + "." + name + fullPath = "/" + namespace + path + } else { + if !strings.HasPrefix(fullPath, "/") { + fullPath = "/" + fullPath + } + } + + registry.routes[fullName] = registeredRoute{ + method: method, + path: path, + handler: handler, + namespace: namespace, + name: name, + fullPath: fullPath, + } +} + +func GetFullPath(routeName string) (string, bool) { + registry.mutex.Lock() + defer registry.mutex.Unlock() + + route, ok := registry.routes[routeName] + if !ok { + return "", false + } + + return route.fullPath, true +} diff --git a/utils/urls/registery.go b/utils/urls/registery.go new file mode 100644 index 0000000..b4e6a36 --- /dev/null +++ b/utils/urls/registery.go @@ -0,0 +1,28 @@ +package urls + +import ( + "sync" + + "cafe/types" + + "github.com/gofiber/fiber/v2" +) + +type registeredRoute struct { + method types.HTTPMethod + path string + handler fiber.Handler + namespace string + name string + fullPath string +} + +type routeRegistry struct { + mutex sync.Mutex + currentNamespace string + routes map[string]registeredRoute +} + +var registry = &routeRegistry{ + routes: make(map[string]registeredRoute), +} |
