Refactor error handling and utility functions; remove deprecated code and improve logging

This commit is contained in:
alex
2025-07-05 16:05:03 +03:00
parent b70819e976
commit 2fdc32ce9f
13 changed files with 132 additions and 162 deletions

View File

@@ -63,7 +63,6 @@ func main() {
w.WriteHeader(http.StatusNoContent)
})
})
r.NotFound(serverv1.ErrNotFound)
address := cfg.Address
if cfg.TlsEnabled {

View File

@@ -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"

View File

@@ -5,12 +5,14 @@ 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.
// 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{} }

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

22
core/utils/http_errors.go Normal file
View File

@@ -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
}

View File

@@ -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()
}
}

18
core/utils/uuid.go Normal file
View File

@@ -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
}