mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
feat(packages): add shared-go package with httputil and envutil
Shared Go utilities for all ManaCore Go services: - httputil: WriteJSON, WriteError, DecodeJSON - envutil: Get, GetInt, GetBool, GetSlice - 8 tests, all passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c5e9456dc
commit
b60877e367
5 changed files with 214 additions and 0 deletions
44
packages/shared-go/envutil/envutil.go
Normal file
44
packages/shared-go/envutil/envutil.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Package envutil provides shared environment variable helpers for ManaCore Go services.
|
||||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Get returns an environment variable value or a fallback default.
|
||||
func Get(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// GetInt returns an environment variable as int or a fallback default.
|
||||
func GetInt(key string, fallback int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if i, err := strconv.Atoi(v); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// GetBool returns an environment variable as bool or a fallback default.
|
||||
func GetBool(key string, fallback bool) bool {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if b, err := strconv.ParseBool(v); err == nil {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// GetSlice returns an environment variable as a comma-separated slice or a fallback default.
|
||||
func GetSlice(key string, fallback []string) []string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return strings.Split(v, ",")
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
63
packages/shared-go/envutil/envutil_test.go
Normal file
63
packages/shared-go/envutil/envutil_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package envutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
os.Setenv("TEST_GET_VAR", "hello")
|
||||
defer os.Unsetenv("TEST_GET_VAR")
|
||||
|
||||
if v := Get("TEST_GET_VAR", "default"); v != "hello" {
|
||||
t.Errorf("expected 'hello', got '%s'", v)
|
||||
}
|
||||
if v := Get("TEST_GET_MISSING", "default"); v != "default" {
|
||||
t.Errorf("expected 'default', got '%s'", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInt(t *testing.T) {
|
||||
os.Setenv("TEST_INT_VAR", "42")
|
||||
defer os.Unsetenv("TEST_INT_VAR")
|
||||
|
||||
if v := GetInt("TEST_INT_VAR", 0); v != 42 {
|
||||
t.Errorf("expected 42, got %d", v)
|
||||
}
|
||||
if v := GetInt("TEST_INT_MISSING", 99); v != 99 {
|
||||
t.Errorf("expected 99, got %d", v)
|
||||
}
|
||||
|
||||
os.Setenv("TEST_INT_INVALID", "abc")
|
||||
defer os.Unsetenv("TEST_INT_INVALID")
|
||||
if v := GetInt("TEST_INT_INVALID", 7); v != 7 {
|
||||
t.Errorf("expected fallback 7 for invalid, got %d", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBool(t *testing.T) {
|
||||
os.Setenv("TEST_BOOL_VAR", "true")
|
||||
defer os.Unsetenv("TEST_BOOL_VAR")
|
||||
|
||||
if v := GetBool("TEST_BOOL_VAR", false); !v {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if v := GetBool("TEST_BOOL_MISSING", true); !v {
|
||||
t.Error("expected default true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSlice(t *testing.T) {
|
||||
os.Setenv("TEST_SLICE_VAR", "a,b,c")
|
||||
defer os.Unsetenv("TEST_SLICE_VAR")
|
||||
|
||||
v := GetSlice("TEST_SLICE_VAR", nil)
|
||||
if len(v) != 3 || v[0] != "a" || v[2] != "c" {
|
||||
t.Errorf("expected [a,b,c], got %v", v)
|
||||
}
|
||||
|
||||
v = GetSlice("TEST_SLICE_MISSING", []string{"x"})
|
||||
if len(v) != 1 || v[0] != "x" {
|
||||
t.Errorf("expected [x], got %v", v)
|
||||
}
|
||||
}
|
||||
3
packages/shared-go/go.mod
Normal file
3
packages/shared-go/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/manacore/shared-go
|
||||
|
||||
go 1.25.0
|
||||
36
packages/shared-go/httputil/httputil.go
Normal file
36
packages/shared-go/httputil/httputil.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Package httputil provides shared HTTP handler utilities for ManaCore Go services.
|
||||
package httputil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// WriteJSON writes a JSON response with the given status code.
|
||||
func WriteJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// WriteError writes a standardized error JSON response.
|
||||
func WriteError(w http.ResponseWriter, status int, message string) {
|
||||
WriteJSON(w, status, map[string]any{
|
||||
"success": false,
|
||||
"error": map[string]any{
|
||||
"statusCode": status,
|
||||
"message": message,
|
||||
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DecodeJSON decodes a JSON request body with a size limit.
|
||||
func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any, maxBytes int64) bool {
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBytes)).Decode(dst); err != nil {
|
||||
WriteError(w, http.StatusBadRequest, "invalid request body")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
68
packages/shared-go/httputil/httputil_test.go
Normal file
68
packages/shared-go/httputil/httputil_test.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package httputil
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWriteJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
WriteJSON(w, http.StatusOK, map[string]string{"key": "value"})
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("expected application/json, got %s", ct)
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), `"key":"value"`) {
|
||||
t.Errorf("unexpected body: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteError(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
WriteError(w, http.StatusBadRequest, "test error")
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, `"message":"test error"`) {
|
||||
t.Errorf("unexpected body: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"success":false`) {
|
||||
t.Errorf("missing success:false in body: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJSON(t *testing.T) {
|
||||
var dst struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"name":"test"}`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if !DecodeJSON(w, r, &dst, 1<<20) {
|
||||
t.Error("expected decode to succeed")
|
||||
}
|
||||
if dst.Name != "test" {
|
||||
t.Errorf("expected 'test', got '%s'", dst.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeJSON_Invalid(t *testing.T) {
|
||||
var dst struct{}
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`invalid`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
if DecodeJSON(w, r, &dst, 1<<20) {
|
||||
t.Error("expected decode to fail")
|
||||
}
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue