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:
Till JS 2026-03-28 10:24:01 +01:00
parent 4c5e9456dc
commit b60877e367
5 changed files with 214 additions and 0 deletions

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

View 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)
}
}

View file

@ -0,0 +1,3 @@
module github.com/manacore/shared-go
go 1.25.0

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

View 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)
}
}