From 2fdc32ce9f24471b3942e9634b8d18f13871d2e0 Mon Sep 17 00:00:00 2001 From: alex Date: Sat, 5 Jul 2025 16:05:03 +0300 Subject: [PATCH] Refactor error handling and utility functions; remove deprecated code and improve logging --- cmd/node/node.go | 1 - com/exec.lua | 16 -------- core/config/consts.go | 6 ++- core/general_server/handle_multi.go | 24 ++---------- core/logs/logger.go | 9 ++++- core/logs/mock.go | 24 ++++-------- core/sv1/handle_com.go | 60 ++++++++++------------------- core/sv1/handle_list.go | 15 ++++++-- core/sv1/server.go | 32 +++++---------- core/sv1/utils.go | 41 ++------------------ core/utils/http_errors.go | 22 +++++++++++ core/utils/internal_lua.go | 26 +++++++++++++ core/utils/uuid.go | 18 +++++++++ 13 files changed, 132 insertions(+), 162 deletions(-) delete mode 100644 com/exec.lua create mode 100644 core/utils/http_errors.go create mode 100644 core/utils/internal_lua.go create mode 100644 core/utils/uuid.go diff --git a/cmd/node/node.go b/cmd/node/node.go index a2488e3..5cbd8ba 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -63,7 +63,6 @@ func main() { w.WriteHeader(http.StatusNoContent) }) }) - r.NotFound(serverv1.ErrNotFound) address := cfg.Address if cfg.TlsEnabled { diff --git a/com/exec.lua b/com/exec.lua deleted file mode 100644 index 67b2090..0000000 --- a/com/exec.lua +++ /dev/null @@ -1,16 +0,0 @@ -if not Params.f then - Result.status = "error" - Result.error = "Missing parameter: f" - return -end - -local code = os.execute("touch " .. Params.f) -if code ~= 0 then - Result.status = "error" - Result.message = "Failed to execute command" - return -end - - -Result.status = "ok" -Result.message = "Command executed successfully" \ No newline at end of file diff --git a/core/config/consts.go b/core/config/consts.go index 601ff95..8685719 100644 --- a/core/config/consts.go +++ b/core/config/consts.go @@ -5,13 +5,15 @@ var UUIDLength byte = 4 // ApiRoute setting for go-chi for main route for api requests var ApiRoute string = "/api/{ver}" + // ComDirRoute setting for go-chi for main route for commands var ComDirRoute string = "/com" // NodeVersion is the version of the node. It can be set by the build system or manually. // If not set, it will return "version0.0.0-none" by default var NodeVersion string -// ActualFileName is a feature of the GoSally update system. + +// ActualFileName is a feature of the GoSally update system. // In the repository, the file specified in the variable contains the current information about updates var ActualFileName string = "actual.txt" @@ -28,7 +30,7 @@ func (_ _updateConsts) GetNodeVersion() string { } func (_ _updateConsts) GetActualFileName() string { return ActualFileName } -func GetInternalConsts() _internalConsts { return _internalConsts{} } +func GetInternalConsts() _internalConsts { return _internalConsts{} } func (_ _internalConsts) GetUUIDLength() byte { return UUIDLength } func GetServerConsts() _serverConsts { return _serverConsts{} } diff --git a/core/general_server/handle_multi.go b/core/general_server/handle_multi.go index ddf3381..f0c1e7a 100644 --- a/core/general_server/handle_multi.go +++ b/core/general_server/handle_multi.go @@ -14,13 +14,13 @@ package general_server import ( - "encoding/json" "errors" "log/slog" "net/http" "slices" "github.com/akyaiy/GoSally-mvp/core/config" + "github.com/akyaiy/GoSally-mvp/core/utils" "github.com/go-chi/chi/v5" ) @@ -140,7 +140,7 @@ func (s *GeneralServer) Handle(w http.ResponseWriter, r *http.Request) { log.Error("HTTP request error: unsupported API version", slog.Int("status", http.StatusBadRequest)) - s.writeJSONError(http.StatusBadRequest, "unsupported API version") + utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version") } // HandleList processes incoming HTTP requests for listing commands, routing them to the appropriate server based on the API version. @@ -182,23 +182,5 @@ func (s *GeneralServer) HandleList(w http.ResponseWriter, r *http.Request) { log.Error("HTTP request error: unsupported API version", slog.Int("status", http.StatusBadRequest)) - s.writeJSONError(http.StatusBadRequest, "unsupported API version") -} - -// writeJSONError writes a JSON error response to the HTTP response writer. -// It sets the Content-Type to application/json, writes the specified HTTP status code, -func (s *GeneralServer) writeJSONError(status int, msg string) { - s.w.Header().Set("Content-Type", "application/json") - s.w.WriteHeader(status) - resp := map[string]interface{}{ - "status": "error", - "error": msg, - "code": status, - } - if err := json.NewEncoder(s.w).Encode(resp); err != nil { - s.log.Error("Failed to write JSON error response", - slog.String("error", err.Error()), - slog.Int("status", status)) - return - } + utils.WriteJSONError(s.w, http.StatusBadRequest, "unsupported API version") } diff --git a/core/logs/logger.go b/core/logs/logger.go index 7b20f06..deeec53 100644 --- a/core/logs/logger.go +++ b/core/logs/logger.go @@ -1,3 +1,6 @@ +// Package logs provides a logger setup function that configures the logger based on the environment. +// It supports different logging levels for development and production environments. +// It uses the standard library's slog package for structured logging. package logs import ( @@ -5,11 +8,15 @@ import ( "os" ) +// Environment constants for logger setup const ( - envDev = "dev" + // envDev enables development logging with debug level + envDev = "dev" + // envProd enables production logging with info level envProd = "prod" ) +// SetupLogger initializes and returns a logger based on the provided environment. func SetupLogger(env string) *slog.Logger { var log *slog.Logger switch env { diff --git a/core/logs/mock.go b/core/logs/mock.go index f0cd8a2..f8f8334 100644 --- a/core/logs/mock.go +++ b/core/logs/mock.go @@ -6,30 +6,20 @@ import ( "sync" ) +// MockHandler is a mock implementation of slog.Handler for testing purposes. type MockHandler struct { - mu sync.Mutex + mu sync.Mutex + // Logs stores the log records captured by the handler. Logs []slog.Record } -func NewMockHandler() *MockHandler { - return &MockHandler{} -} - -func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { - return true -} - +func NewMockHandler() *MockHandler { return &MockHandler{} } +func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { return true } +func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h } +func (h *MockHandler) WithGroup(_ string) slog.Handler { return h } func (h *MockHandler) Handle(_ context.Context, r slog.Record) error { h.mu.Lock() defer h.mu.Unlock() h.Logs = append(h.Logs, r.Clone()) return nil } - -func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { - return h -} - -func (h *MockHandler) WithGroup(_ string) slog.Handler { - return h -} diff --git a/core/sv1/handle_com.go b/core/sv1/handle_com.go index 3b8a4dc..4eb68ae 100644 --- a/core/sv1/handle_com.go +++ b/core/sv1/handle_com.go @@ -7,12 +7,22 @@ import ( "os" "path/filepath" + "github.com/akyaiy/GoSally-mvp/core/utils" "github.com/go-chi/chi/v5" lua "github.com/yuin/gopher-lua" ) -func (h *HandlerV1) _handle() { - uuid16 := h.newUUID() +// HandlerV1 is the main handler for version 1 of the API. +// The function processes the HTTP request and runs Lua scripts, +// preparing the environment and subsequently transmitting the execution result +func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { + uuid16, err := utils.NewUUID() + if err != nil { + h.log.Error("Failed to generate UUID", + slog.String("error", err.Error())) + utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()) + return + } log := h.log.With( slog.Group("request", slog.String("version", h.GetVersion()), @@ -32,7 +42,7 @@ func (h *HandlerV1) _handle() { slog.String("error", "invalid command"), slog.String("cmd", cmd), slog.Int("status", http.StatusBadRequest)) - h.writeJSONError(http.StatusBadRequest, "invalid command") + utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid command") return } @@ -42,7 +52,7 @@ func (h *HandlerV1) _handle() { slog.String("error", "command not found"), slog.String("cmd", cmd), slog.Int("status", http.StatusNotFound)) - h.writeJSONError(http.StatusNotFound, "command not found") + utils.WriteJSONError(h.w, http.StatusNotFound, "command not found") return } @@ -52,15 +62,13 @@ func (h *HandlerV1) _handle() { slog.String("error", "command not found"), slog.String("cmd", cmd), slog.Int("status", http.StatusNotFound)) - h.writeJSONError(http.StatusNotFound, "command not found") + utils.WriteJSONError(h.w, http.StatusNotFound, "command not found") return } L := lua.NewState() defer L.Close() - // Создаем таблицу Params - // Создаем таблицу In с Params paramsTable := L.NewTable() qt := h.r.URL.Query() for k, v := range qt { @@ -78,49 +86,44 @@ func (h *HandlerV1) _handle() { L.SetField(outTable, "Result", resultTable) L.SetGlobal("Out", outTable) - // Скрипт подготовки окружения prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua") if _, err := os.Stat(prepareLuaEnv); err == nil { if err := L.DoFile(prepareLuaEnv); err != nil { log.Error("Failed to prepare lua environment", slog.String("error", err.Error())) - h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) + utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error()) return } } else { log.Warn("No environment preparation script found, skipping preparation") } - // Основной Lua скрипт if err := L.DoFile(scriptPath); err != nil { log.Error("Failed to execute lua script", slog.String("error", err.Error())) - h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) + utils.WriteJSONError(h.w, http.StatusInternalServerError, "lua error: "+err.Error()) return } - // Получаем Out lv := L.GetGlobal("Out") tbl, ok := lv.(*lua.LTable) if !ok { log.Error("Lua global 'Out' is not a table") - h.writeJSONError(http.StatusInternalServerError, "'Out' is not a table") + utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Out' is not a table") return } - // Получаем Result из Out resultVal := tbl.RawGetString("Result") resultTbl, ok := resultVal.(*lua.LTable) if !ok { log.Error("Lua global 'Result' is not a table") - h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table") + utils.WriteJSONError(h.w, http.StatusInternalServerError, "'Result' is not a table") return } - // Перебираем таблицу Result out := make(map[string]interface{}) resultTbl.ForEach(func(key lua.LValue, value lua.LValue) { - out[key.String()] = convertTypes(value) + out[key.String()] = utils.ConvertLuaTypesToGolang(value) }) h.w.Header().Set("Content-Type", "application/json") @@ -147,26 +150,3 @@ func (h *HandlerV1) _handle() { log.Info("Session completed") } - -func convertTypes(value lua.LValue) any { - switch value.Type() { - case lua.LTString: - return value.String() - case lua.LTNumber: - return float64(value.(lua.LNumber)) - case lua.LTBool: - return bool(value.(lua.LBool)) - case lua.LTTable: - result := make(map[string]interface{}) - if tbl, ok := value.(*lua.LTable); ok { - tbl.ForEach(func(key lua.LValue, value lua.LValue) { - result[key.String()] = convertTypes(value) - }) - } - return result - case lua.LTNil: - return nil - default: - return value.String() - } -} diff --git a/core/sv1/handle_list.go b/core/sv1/handle_list.go index 1008e35..355e684 100644 --- a/core/sv1/handle_list.go +++ b/core/sv1/handle_list.go @@ -8,11 +8,19 @@ import ( "path/filepath" "strings" + "github.com/akyaiy/GoSally-mvp/core/utils" "github.com/go-chi/chi/v5" ) -func (h *HandlerV1) _handleList() { - uuid16 := h.newUUID() +// The function processes the HTTP request and returns a list of available commands. +func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) { + uuid16, err := utils.NewUUID() + if err != nil { + h.log.Error("Failed to generate UUID", + slog.String("error", err.Error())) + utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to generate UUID: "+err.Error()) + return + } log := h.log.With( slog.Group("request", slog.String("version", h.GetVersion()), @@ -31,7 +39,6 @@ func (h *HandlerV1) _handleList() { } var ( files []os.DirEntry - err error commands = make(map[string]ComMeta) cmdsProcessed = make(map[string]bool) ) @@ -39,7 +46,7 @@ func (h *HandlerV1) _handleList() { if files, err = os.ReadDir(h.cfg.ComDir); err != nil { log.Error("Failed to read commands directory", slog.String("error", err.Error())) - h.writeJSONError(http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) + utils.WriteJSONError(h.w, http.StatusInternalServerError, "failed to read commands directory: "+err.Error()) return } diff --git a/core/sv1/server.go b/core/sv1/server.go index 605bb47..2e7e1fb 100644 --- a/core/sv1/server.go +++ b/core/sv1/server.go @@ -1,3 +1,5 @@ +// Package sv1 provides the implementation of the Server V1 API handler. +// It includes utilities for handling API requests, extracting descriptions, and managing UUIDs. package sv1 import ( @@ -8,16 +10,7 @@ import ( "github.com/akyaiy/GoSally-mvp/core/config" ) -type ServerV1UtilsContract interface { - extractDescriptionStatic(path string) (string, error) - writeJSONError(status int, msg string) - newUUID() string - - _errNotFound() - ErrNotFound(w http.ResponseWriter, r *http.Request) -} - -// structure only for initialization +// HandlerV1InitStruct structure is only for initialization type HandlerV1InitStruct struct { Ver string Log slog.Logger @@ -26,6 +19,7 @@ type HandlerV1InitStruct struct { ListAllowedCmd *regexp.Regexp } +// HandlerV1 implements the ServerV1UtilsContract and serves as the main handler for API requests. type HandlerV1 struct { w http.ResponseWriter r *http.Request @@ -34,12 +28,16 @@ type HandlerV1 struct { cfg *config.ConfigConf + // allowedCmd and listAllowedCmd are regular expressions used to validate command names. allowedCmd *regexp.Regexp listAllowedCmd *regexp.Regexp ver string } +// InitV1Server initializes a new HandlerV1 with the provided configuration and returns it. +// Should be carefull with giving to this function invalid parameters, +// because there is no validation of parameters in this function. func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { return &HandlerV1{ log: o.Log, @@ -50,18 +48,8 @@ func InitV1Server(o *HandlerV1InitStruct) *HandlerV1 { } } -func (h *HandlerV1) Handle(w http.ResponseWriter, r *http.Request) { - h.w = w - h.r = r - h._handle() -} - -func (h *HandlerV1) HandleList(w http.ResponseWriter, r *http.Request) { - h.w = w - h.r = r - h._handleList() -} - +// GetVersion returns the API version of the HandlerV1, which is set during initialization. +// This version is used to identify the API version in the request routing. func (h *HandlerV1) GetVersion() string { return h.ver } diff --git a/core/sv1/utils.go b/core/sv1/utils.go index 043732c..ff37a2b 100644 --- a/core/sv1/utils.go +++ b/core/sv1/utils.go @@ -1,35 +1,16 @@ package sv1 import ( - "crypto/rand" - "encoding/hex" - "encoding/json" "log/slog" "net/http" "os" "regexp" - "github.com/akyaiy/GoSally-mvp/core/config" + "github.com/akyaiy/GoSally-mvp/core/utils" ) -func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) { - h.w = w - h.r = r - h._errNotFound() -} - -func (h *HandlerV1) newUUID() string { - bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2)) - _, 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") +func (h *HandlerV1) errNotFound(w http.ResponseWriter, r *http.Request) { + utils.WriteJSONError(h.w, http.StatusBadRequest, "invalid request") h.log.Error("HTTP request error", slog.String("remote", h.r.RemoteAddr), slog.String("method", h.r.Method), @@ -37,22 +18,6 @@ func (h *HandlerV1) _errNotFound() { 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, - } - if err := json.NewEncoder(h.w).Encode(resp); err != nil { - h.log.Error("Failed to write JSON error response", - slog.String("error", err.Error()), - slog.Int("status", status)) - return - } -} - func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) { data, err := os.ReadFile(path) if err != nil { diff --git a/core/utils/http_errors.go b/core/utils/http_errors.go new file mode 100644 index 0000000..7e52cd1 --- /dev/null +++ b/core/utils/http_errors.go @@ -0,0 +1,22 @@ +package utils + +import ( + "encoding/json" + "net/http" +) + +// writeJSONError writes a JSON error response to the HTTP response writer. +// It sets the Content-Type to application/json, writes the specified HTTP status code +func WriteJSONError(w http.ResponseWriter, status int, msg string) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + resp := map[string]any{ + "status": "error", + "error": msg, + "code": status, + } + if err := json.NewEncoder(w).Encode(resp); err != nil { + return err + } + return nil +} diff --git a/core/utils/internal_lua.go b/core/utils/internal_lua.go new file mode 100644 index 0000000..16b0889 --- /dev/null +++ b/core/utils/internal_lua.go @@ -0,0 +1,26 @@ +package utils + +import lua "github.com/yuin/gopher-lua" + +func ConvertLuaTypesToGolang(value lua.LValue) any { + switch value.Type() { + case lua.LTString: + return value.String() + case lua.LTNumber: + return float64(value.(lua.LNumber)) + case lua.LTBool: + return bool(value.(lua.LBool)) + case lua.LTTable: + result := make(map[string]interface{}) + if tbl, ok := value.(*lua.LTable); ok { + tbl.ForEach(func(key lua.LValue, value lua.LValue) { + result[key.String()] = ConvertLuaTypesToGolang(value) + }) + } + return result + case lua.LTNil: + return nil + default: + return value.String() + } +} diff --git a/core/utils/uuid.go b/core/utils/uuid.go new file mode 100644 index 0000000..23719ae --- /dev/null +++ b/core/utils/uuid.go @@ -0,0 +1,18 @@ +package utils + +import ( + "crypto/rand" + "encoding/hex" + "errors" + + "github.com/akyaiy/GoSally-mvp/core/config" +) + +func NewUUID() (string, error) { + bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2)) + _, err := rand.Read(bytes) + if err != nil { + return "", errors.New("failed to generate UUID: " + err.Error()) + } + return hex.EncodeToString(bytes), nil +}