Refactor handler methods to use instance methods for logging and error handling; add utility functions for UUID generation and JSON error responses.

This commit is contained in:
alex
2025-06-22 11:48:33 +03:00
parent 07e1d3ecdd
commit eaff815270
8 changed files with 124 additions and 86 deletions

View File

@@ -1,14 +1,13 @@
package main package main
import ( import (
"github.com/akyaiy/GoSally-mvp/internal/config"
"github.com/akyaiy/GoSally-mvp/internal/logs"
"crypto/rand"
"encoding/hex"
"log/slog" "log/slog"
"net/http" "net/http"
"regexp" "regexp"
"github.com/akyaiy/GoSally-mvp/internal/config"
"github.com/akyaiy/GoSally-mvp/internal/logs"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -42,13 +41,3 @@ func main() {
http.ListenAndServe(cfg.Address, r) http.ListenAndServe(cfg.Address, r)
} }
func newUUID() string {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
log.Error("Failed to generate UUID", slog.String("error", err.Error()))
return ""
}
return hex.EncodeToString(bytes)
}

12
internal/logs/go.mod Normal file
View File

@@ -0,0 +1,12 @@
module github.com/akyaiy/GoSally-mvp/internal/logs
go 1.24.4
require github.com/ilyakaznacheev/cleanenv v1.5.0
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)

View File

@@ -3,12 +3,15 @@ module github.com/akyaiy/GoSally-mvp/internal/server_v1
go 1.24.4 go 1.24.4
require ( require (
github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4
github.com/go-chi/chi/v5 v5.2.2 github.com/go-chi/chi/v5 v5.2.2
github.com/yuin/gopher-lua v1.1.1 github.com/yuin/gopher-lua v1.1.1
) )
require ( require (
github.com/gorilla/websocket v1.5.3 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/ilyakaznacheev/cleanenv v1.5.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
) )

View File

@@ -1,11 +1,17 @@
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4 h1:pRcNlKaQUEvAjC9ideN8YZc2vCryTI6q045qf+NkfpI=
github.com/akyaiy/GoSally-mvp/internal/config v0.0.0-20250622083853-07e1d3ecddf4/go.mod h1:VCJJWOEkisTU5IBIuNVEc2ahosMRnoVy/I/Dnf79KVM=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -12,24 +12,24 @@ import (
) )
func (h *HandlerV1) _handle() { func (h *HandlerV1) _handle() {
uuid16 := newUUID() uuid16 := h.newUUID()
_log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) h.log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String()))
cmd := chi.URLParam(h.r, "cmd") cmd := chi.URLParam(h.r, "cmd")
if !allowedCmd.MatchString(string([]rune(cmd)[0])) { if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) {
writeJSONError(h.w, http.StatusBadRequest, "invalid command") h.writeJSONError(http.StatusBadRequest, "invalid command")
_log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest))
return return
} }
if !listAllowedCmd.MatchString(cmd) { if !h.listAllowedCmd.MatchString(cmd) {
writeJSONError(h.w, http.StatusBadRequest, "invalid command") h.writeJSONError(http.StatusBadRequest, "invalid command")
_log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest))
return return
} }
scriptPath := filepath.Join(cfg.ComDir, cmd+".lua") scriptPath := filepath.Join(h.cfg.ComDir, cmd+".lua")
if _, err := os.Stat(scriptPath); err != nil { if _, err := os.Stat(scriptPath); err != nil {
writeJSONError(h.w, http.StatusNotFound, "command not found") h.writeJSONError(http.StatusNotFound, "command not found")
_log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "command not found"), slog.String("cmd", cmd), slog.Int("status", http.StatusNotFound)) h.log.Error("HTTP request error", slog.String("connection-uuid", uuid16), slog.String("error", "command not found"), slog.String("cmd", cmd), slog.Int("status", http.StatusNotFound))
return return
} }
@@ -57,8 +57,8 @@ func (h *HandlerV1) _handle() {
`) `)
if err := L.DoFile(scriptPath); err != nil { if err := L.DoFile(scriptPath); err != nil {
writeJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error()) h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error())
_log.Error("Failed to execute lua script", slog.String("connection-uuid", uuid16), slog.String("error", err.Error())) h.log.Error("Failed to execute lua script", slog.String("connection-uuid", uuid16), slog.String("error", err.Error()))
return return
} }
@@ -78,15 +78,15 @@ func (h *HandlerV1) _handle() {
}) })
} }
w.Header().Set("Content-Type", "application/json") h.w.Header().Set("Content-Type", "application/json")
json.NewEncoder(h.w).Encode(out) json.NewEncoder(h.w).Encode(out)
switch out["status"] { switch out["status"] {
case "error": case "error":
_log.Info("Command executed with error", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) h.log.Info("Command executed with error", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out))
case "ok": case "ok":
_log.Info("Command executed successfully", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) h.log.Info("Command executed successfully", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out))
default: default:
_log.Info("Command executed and returned an unknown status", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out)) h.log.Info("Command executed and returned an unknown status", slog.String("connection-uuid", uuid16), slog.String("cmd", cmd), slog.Any("result", out))
} }
_log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String())) h.log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String()))
} }

View File

@@ -6,13 +6,13 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"github.com/akyaiy/GoSally-mvp/internal/config"
_ "github.com/go-chi/chi/v5" _ "github.com/go-chi/chi/v5"
) )
func (h *HandlerV1) _handleList() { func (h *HandlerV1) _handleList() {
uuid16 := newUUID() uuid16 := h.newUUID()
_log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String())) h.log.Info("Received request", slog.String("version", "v1"), slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String()))
type ComMeta struct { type ComMeta struct {
Description string Description string
} }
@@ -23,9 +23,9 @@ func (h *HandlerV1) _handleList() {
commands = make(map[string]ComMeta) commands = make(map[string]ComMeta)
) )
if files, err = os.ReadDir(cfg.ComDir); err != nil { if files, err = os.ReadDir(h.cfg.ComDir); err != nil {
writeJSONError(w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error())
_log.Error("Failed to read commands directory", slog.String("error", err.Error())) h.log.Error("Failed to read commands directory", slog.String("error", err.Error()))
return return
} }
for _, file := range files { for _, file := range files {
@@ -33,15 +33,15 @@ func (h *HandlerV1) _handleList() {
continue continue
} }
cmdName := file.Name()[:len(file.Name())-4] // remove .lua extension cmdName := file.Name()[:len(file.Name())-4] // remove .lua extension
if !allowedCmd.MatchString(string([]rune(cmdName)[0])) { if !h.allowedCmd.MatchString(string([]rune(cmdName)[0])) {
continue continue
} }
if !listAllowedCmd.MatchString(cmdName) { if !h.listAllowedCmd.MatchString(cmdName) {
continue continue
} }
if com.Description, err = extractDescriptionStatic(filepath.Join(cfg.ComDir, file.Name())); err != nil { if com.Description, err = h.extractDescriptionStatic(filepath.Join(h.cfg.ComDir, file.Name())); err != nil {
writeJSONError(w, http.StatusInternalServerError, "failed to read command: "+err.Error()) h.writeJSONError(http.StatusInternalServerError, "failed to read command: "+err.Error())
log.Error("Failed to read command", slog.String("error", err.Error())) h.log.Error("Failed to read command", slog.String("error", err.Error()))
return return
} }
if com.Description == "" { if com.Description == "" {
@@ -49,7 +49,7 @@ func (h *HandlerV1) _handleList() {
} }
commands[cmdName] = ComMeta{Description: com.Description} commands[cmdName] = ComMeta{Description: com.Description}
} }
json.NewEncoder(w).Encode(commands) json.NewEncoder(h.w).Encode(commands)
_log.Info("Command executed successfully", slog.String("connection-uuid", uuid16)) h.log.Info("Command executed successfully", slog.String("connection-uuid", uuid16))
_log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String())) h.log.Info("Session completed", slog.String("connection-uuid", uuid16), slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String()))
} }

View File

@@ -1,16 +1,23 @@
package server_v1 package server_v1
import ( import (
"encoding/json"
"log/slog" "log/slog"
"net/http" "net/http"
"os"
"regexp" "regexp"
"GoSally-mvp/internal/config" "github.com/akyaiy/GoSally-mvp/internal/config"
) )
type ServerV1UtilsContract interface {
extractDescriptionStatic(path string) (string, error)
writeJSONError(status int, msg string)
newUUID() string
errNotFound()
}
type ServerV1Contract interface { type ServerV1Contract interface {
ServerV1UtilsContract
Handle(w http.ResponseWriter, r *http.Request) Handle(w http.ResponseWriter, r *http.Request)
HandleList(w http.ResponseWriter, r *http.Request) HandleList(w http.ResponseWriter, r *http.Request)
@@ -22,11 +29,11 @@ type HandlerV1 struct {
w http.ResponseWriter w http.ResponseWriter
r *http.Request r *http.Request
_log slog.Logger log slog.Logger
cfg *config.ConfigConf cfg *config.ConfigConf
allowedCmd *regexp.Regexp allowedCmd *regexp.Regexp
listAllowedCmd *regexp.Regexp listAllowedCmd *regexp.Regexp
} }
@@ -41,33 +48,3 @@ func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) {
h.r = r h.r = r
h._handleList() h._handleList()
} }
func errNotFound(w http.ResponseWriter, r *http.Request) {
writeJSONError(w, http.StatusBadRequest, "invalid request")
_log.Error("HTTP request error", slog.String("remote", r.RemoteAddr), slog.String("method", r.Method), slog.String("url", r.URL.String()), slog.Int("status", http.StatusBadRequest))
}
func writeJSONError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
json.NewEncoder(w).Encode(resp)
}
func extractDescriptionStatic(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
re := regexp.MustCompile(`---\s*#description\s*=\s*"([^"]+)"`)
m := re.FindStringSubmatch(string(data))
if len(m) <= 0 {
return "", nil
}
return m[1], nil
}

View File

@@ -0,0 +1,51 @@
package server_v1
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log/slog"
"net/http"
"os"
"regexp"
)
func (h *HandlerV1) newUUID() string {
bytes := make([]byte, 16)
_, err := rand.Read(bytes)
if err != nil {
h.log.Error("Failed to generate UUID", slog.String("error", err.Error()))
return ""
}
return hex.EncodeToString(bytes)
}
func (h *HandlerV1) errNotFound() {
h.writeJSONError(http.StatusBadRequest, "invalid request")
h.log.Error("HTTP request error", slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), slog.String("url", h.r.URL.String()), slog.Int("status", http.StatusBadRequest))
}
func (h *HandlerV1) writeJSONError(status int, msg string) {
h.w.Header().Set("Content-Type", "application/json")
h.w.WriteHeader(status)
resp := map[string]interface{}{
"status": "error",
"error": msg,
"code": status,
}
json.NewEncoder(h.w).Encode(resp)
}
func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
re := regexp.MustCompile(`---\s*#description\s*=\s*"([^"]+)"`)
m := re.FindStringSubmatch(string(data))
if len(m) <= 0 {
return "", nil
}
return m[1], nil
}