From 0c66e0b7dedda5aab5a5848513ccafffdad66d6b Mon Sep 17 00:00:00 2001 From: Priyansh Date: Tue, 26 Aug 2025 14:23:25 +0530 Subject: basic boilerplate setup --- .gitignore | 4 + Makefile | 33 +++++++ config/config.go | 205 ++++++++++++++++++++++++++++++++++++++++++ config/constants.go | 7 ++ config/types.go | 12 +++ controllers/home.go | 12 +++ eda/main.go | 52 +++++++++++ go.mod | 26 ++++++ go.sum | 51 +++++++++++ processors/meta.go | 10 +++ processors/processors.go | 7 ++ router/routes.go | 12 +++ templates/home.django | 4 + templates/layouts/main.django | 30 +++++++ utils/shortcuts/render.go | 71 +++++++++++++++ 15 files changed, 536 insertions(+) create mode 100644 Makefile create mode 100644 config/config.go create mode 100644 config/constants.go create mode 100644 config/types.go create mode 100644 controllers/home.go create mode 100644 eda/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 processors/meta.go create mode 100644 processors/processors.go create mode 100644 router/routes.go create mode 100644 templates/home.django create mode 100644 templates/layouts/main.django create mode 100644 utils/shortcuts/render.go diff --git a/.gitignore b/.gitignore index aaadf73..c1e39e1 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +# OS Specific +.DS_Store +Thumbs.db diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fb20d96 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ +BINARY_NAME=eda +BUILD_PATH=bin/$(BINARY_NAME) +MAIN_PATH=$(BINARY_NAME)/main.go + +.PHONY: setup clean build run dev all + +setup: + @echo "Setting up environment..." + @go mod download + @echo "Environment setup complete." + +clean: + @echo "Cleaning up..." + @rm -rf bin + @echo "Cleanup complete." + +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 + +all: setup clean build run + +.SILENT: \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4ad3c25 --- /dev/null +++ b/config/config.go @@ -0,0 +1,205 @@ +package config + +import ( + "fmt" + "log" + "os" + "reflect" + "strconv" + "time" + + "github.com/joho/godotenv" +) + +var ( + Server ServerConfig + GitHub GitHubConfig +) + +func init() { + godotenv.Load() + + if err := parse(&Server); err != nil { + log.Fatalf("failed to parse server config: %v", err) + } + + if err := parse(&GitHub); err != nil { + log.Fatalf("failed to parse GitHub config: %v", err) + } +} + +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 getEnvInt64(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 getEnvFloat64(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 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(getEnvInt64(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(getEnvFloat64(envKey, defaultFloat)) + 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.TypeOf(time.Duration(0)) { + defaultDuration, _ := time.ParseDuration(defaultVal) + field.Set(reflect.ValueOf(getEnvDuration(envKey, defaultDuration))) + } +} + +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) + } + default: + if field.Type() == reflect.TypeOf(time.Duration(0)) { + if defaultDuration, err := time.ParseDuration(defaultVal); err == nil { + field.Set(reflect.ValueOf(defaultDuration)) + } + } + } +} + +func validateConfigInput(config any) (reflect.Value, reflect.Type, error) { + v := reflect.ValueOf(config) + if v.Kind() != reflect.Ptr || 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 +} + +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 +} + +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/config/constants.go b/config/constants.go new file mode 100644 index 0000000..8cb829d --- /dev/null +++ b/config/constants.go @@ -0,0 +1,7 @@ +package config + +const ( + PAGETITLE_HOME = "Home" + + TEMPLATE_HOME = "home" +) diff --git a/config/types.go b/config/types.go new file mode 100644 index 0000000..a57100f --- /dev/null +++ b/config/types.go @@ -0,0 +1,12 @@ +package config + +type ServerConfig struct { + Host string `env:"SERVER_HOST" default:"0.0.0.0"` + Port int `env:"SERVER_PORT" default:"8080"` + IsDevMode bool `env:"SERVER_IS_DEV_MODE" default:"true"` +} + +type GitHubConfig struct { + GitHubUsername string `env:"GITHUB_USERNAME"` + GitHubToken string `env:"GITHUB_TOKEN"` +} diff --git a/controllers/home.go b/controllers/home.go new file mode 100644 index 0000000..8cb7d06 --- /dev/null +++ b/controllers/home.go @@ -0,0 +1,12 @@ +package controllers + +import ( + "eda/config" + "eda/utils/shortcuts" + + "github.com/gofiber/fiber/v2" +) + +func HomeController(ctx *fiber.Ctx) error { + return shortcuts.Render(ctx, config.TEMPLATE_HOME, nil) +} diff --git a/eda/main.go b/eda/main.go new file mode 100644 index 0000000..56ad7fe --- /dev/null +++ b/eda/main.go @@ -0,0 +1,52 @@ +package main + +import ( + "eda/config" + "eda/processors" + "eda/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" + "github.com/gofiber/template/django/v3" +) + +func main() { + engine := django.New("./templates", ".django") + engine.Reload(config.Server.IsDevMode) + + server := fiber.New(fiber.Config{ + Views: engine, + ErrorHandler: serverErrorHandler, + }) + server.Use(recover.New()) + server.Use(logger.New()) + server.Use(helmet.New(helmet.Config{ + CrossOriginEmbedderPolicy: "unsafe-none", + })) + server.Use(cors.New(cors.Config{ + AllowOrigins: "*", + AllowHeaders: "Origin, Content-Type, Accept", + })) + + processors.Initialise(server) + router.Initialise(server) + + log.Fatalf("Server failed to start: %v", server.Listen(fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port))) +} + +func serverErrorHandler(ctx *fiber.Ctx, err error) error { + code := fiber.StatusInternalServerError + msg := "Internal Server Error" + if e, ok := err.(*fiber.Error); ok { + code = e.Code + msg = e.Message + } else if err != nil { + msg = err.Error() + } + return ctx.Status(code).SendString(msg) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b34e826 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module eda + +go 1.24.5 + +require ( + github.com/gofiber/fiber/v2 v2.52.9 + github.com/gofiber/template/django/v3 v3.1.14 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/flosch/pongo2/v6 v6.0.0 // indirect + github.com/gofiber/template v1.8.3 // indirect + github.com/gofiber/utils 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3284bdd --- /dev/null +++ b/go.sum @@ -0,0 +1,51 @@ +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= +github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= +github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= +github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= +github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= +github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= +github.com/gofiber/template/django/v3 v3.1.14 h1:SvTvs+u5vTZuu1Y2pMUD2NhaGIjBj9FmDA3XD50QBvw= +github.com/gofiber/template/django/v3 v3.1.14/go.mod h1:gP4vH+T1ajZw7yaejqG1dZVdHQkMC/jPoQbmlG812I0= +github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= +github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= +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/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/processors/meta.go b/processors/meta.go new file mode 100644 index 0000000..2c931b1 --- /dev/null +++ b/processors/meta.go @@ -0,0 +1,10 @@ +package processors + +import "github.com/gofiber/fiber/v2" + +const defaultTitle = "default" + +func MetaContextProcessor(ctx *fiber.Ctx) error { + ctx.Locals("Title", defaultTitle) + return ctx.Next() +} diff --git a/processors/processors.go b/processors/processors.go new file mode 100644 index 0000000..a07608e --- /dev/null +++ b/processors/processors.go @@ -0,0 +1,7 @@ +package processors + +import "github.com/gofiber/fiber/v2" + +func Initialise(app *fiber.App) { + app.Use(MetaContextProcessor) +} diff --git a/router/routes.go b/router/routes.go new file mode 100644 index 0000000..6915ec6 --- /dev/null +++ b/router/routes.go @@ -0,0 +1,12 @@ +package router + +import ( + "eda/controllers" + + "github.com/gofiber/fiber/v2" +) + +func Initialise(router *fiber.App) { + router.Static("/static", "./static") + router.Get("/", controllers.HomeController) +} diff --git a/templates/home.django b/templates/home.django new file mode 100644 index 0000000..ec62d17 --- /dev/null +++ b/templates/home.django @@ -0,0 +1,4 @@ +{% extends 'layouts/main.django' %} +{% block content %} +

Welcome to Eda

+{% endblock %} diff --git a/templates/layouts/main.django b/templates/layouts/main.django new file mode 100644 index 0000000..713c42a --- /dev/null +++ b/templates/layouts/main.django @@ -0,0 +1,30 @@ + + + + + {{ Title }} - Eda + + {% block head %} + + {% endblock %} + + +
+ +
+ {% block content %} + + {% endblock %} +
+
+ + + + {% block scripts %} + + {% endblock %} + diff --git a/utils/shortcuts/render.go b/utils/shortcuts/render.go new file mode 100644 index 0000000..19635e2 --- /dev/null +++ b/utils/shortcuts/render.go @@ -0,0 +1,71 @@ +package shortcuts + +import ( + "reflect" + "strings" + + "maps" + + "github.com/gofiber/fiber/v2" +) + +func Render(ctx *fiber.Ctx, name string, bind any) error { + finalData := fiber.Map{} + + ctx.Context().VisitUserValues(func(key []byte, value any) { + finalData[string(key)] = value + }) + + if bind != nil { + switch v := bind.(type) { + case fiber.Map: + maps.Copy(finalData, v) + case map[string]any: + maps.Copy(finalData, v) + default: + structData := structToMap(bind) + maps.Copy(finalData, structData) + } + } + + return ctx.Render(name, finalData) +} + +func RenderWithStatus(ctx *fiber.Ctx, name string, bind any, statusCode int) error { + ctx.Status(statusCode) + return Render(ctx, name, bind) +} + +func structToMap(obj any) map[string]any { + result := make(map[string]any) + + v := reflect.ValueOf(obj) + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + if v.Kind() != reflect.Struct { + return result + } + + t := v.Type() + for i := range v.NumField() { + field := t.Field(i) + if !field.IsExported() { + continue + } + + key := field.Name + if tag := field.Tag.Get("json"); tag != "" && tag != "-" { + if commaIdx := strings.Index(tag, ","); commaIdx > 0 { + key = tag[:commaIdx] + } else if commaIdx == -1 { + key = tag + } + } + + result[key] = v.Field(i).Interface() + } + + return result +} -- cgit v1.2.3