#!/usr/bin/env bash set -euo pipefail # ============================================================================= # setup-go-module.sh — Inicializa módulo Go funcional con bindings Python # Coherente con DevFactory (pure core / impure shell) + CGO c-shared + ctypes # ============================================================================= # --- Colores --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } log_ok() { echo -e "${GREEN}[OK]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1"; } log_step() { echo -e "${CYAN}[STEP]${NC} $1"; } # --- Parámetros --- MODULE_NAME="${1:-}" TARGET_PATH="${2:-.}" DEVFACTORY_PATH="$HOME/.local_agentes/backend" DEVFACTORY_MODULE="github.com/lucasdataproyects/devfactory" # --- Validar nombre --- if [[ -z "$MODULE_NAME" ]]; then log_error "Uso: setup-go-module.sh [path]" echo "STATUS: ERROR" exit 1 fi # Normalizar nombre a kebab-case MODULE_NAME=$(echo "$MODULE_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') PROJECT_DIR="$TARGET_PATH/$MODULE_NAME" # --- Check estado existente --- if [[ -f "$PROJECT_DIR/go.mod" ]]; then log_warn "El módulo $MODULE_NAME ya existe en $PROJECT_DIR" if [[ -f "$PROJECT_DIR/export/exports.go" ]]; then log_ok "Bindings Python ya configurados" else log_warn "Falta directorio export/ — ejecuta de nuevo para completar" fi echo "STATUS: CONFIGURED" exit 0 fi # --- Verificar dependencias --- log_step "Verificando dependencias..." if ! command -v go &>/dev/null; then log_error "Go no está instalado. Instala Go 1.22+" echo "STATUS: ERROR" exit 1 fi GO_VERSION=$(go version | grep -oP '\d+\.\d+' | head -1) log_ok "Go $GO_VERSION encontrado" if ! command -v python3 &>/dev/null; then log_warn "Python3 no encontrado — los bindings se generarán pero no se podrán testear" fi if [[ ! -d "$DEVFACTORY_PATH" ]]; then log_warn "DevFactory no encontrado en $DEVFACTORY_PATH — se creará go.mod sin go.work" fi # --- Crear estructura --- log_step "Creando estructura del módulo '$MODULE_NAME'..." mkdir -p "$PROJECT_DIR"/{core,shell,export,python/bindings,cmd,internal} # --- go.mod --- log_step "Generando go.mod..." cat > "$PROJECT_DIR/go.mod" << EOF module github.com/lucasdataproyects/$MODULE_NAME go 1.22 require $DEVFACTORY_MODULE v0.0.0 EOF # --- go.work (si DevFactory existe localmente) --- if [[ -d "$DEVFACTORY_PATH" ]]; then log_step "Generando go.work con DevFactory local..." cat > "$PROJECT_DIR/go.work" << EOF go 1.22 use ( . $DEVFACTORY_PATH ) EOF log_ok "go.work enlazado a DevFactory" fi # --- core/transform.go — Funciones puras de ejemplo --- log_step "Generando core/ (funciones puras)..." cat > "$PROJECT_DIR/core/transform.go" << 'GOEOF' // Package core contiene funciones puras sin side effects. // Todas las funciones son deterministas y composables. package core import ( "strings" "errors" df "github.com/lucasdataproyects/devfactory/core" ) // ToUpper transforma texto a mayúsculas (función pura). func ToUpper(s string) string { return strings.ToUpper(s) } // ProcessItems aplica una transformación a cada elemento usando MapSlice de DevFactory. func ProcessItems(items []string, transform func(string) string) []string { return df.MapSlice(items, transform) } // FilterNonEmpty filtra elementos vacíos usando FilterSlice de DevFactory. func FilterNonEmpty(items []string) []string { return df.FilterSlice(items, func(s string) bool { return len(strings.TrimSpace(s)) > 0 }) } // SafeDivide retorna Result[float64] para evitar panic en división por cero. func SafeDivide(a, b float64) df.Result[float64] { if b == 0 { return df.Err[float64](errors.New("division by zero")) } return df.Ok(a / b) } GOEOF # --- core/types.go — Tipos exportables a Python --- cat > "$PROJECT_DIR/core/types.go" << 'GOEOF' package core // DataPoint representa un punto de datos exportable a Python. // Los campos usan tipos C-compatible para facilitar el binding. type DataPoint struct { Label string Value float64 } // Summary es el resultado de un procesamiento, exportable a Python. type Summary struct { Count int Total float64 Items []string } GOEOF # --- shell/io.go — Operaciones I/O con Result[T] --- log_step "Generando shell/ (operaciones I/O)..." cat > "$PROJECT_DIR/shell/io.go" << 'GOEOF' // Package shell contiene operaciones con side effects, wrapeadas en Result[T]. package shell import ( df "github.com/lucasdataproyects/devfactory/core" "github.com/lucasdataproyects/devfactory/shell" ) // ReadDataFile lee un archivo y retorna su contenido como Result. func ReadDataFile(path string) df.Result[string] { return shell.ReadString(path) } // WriteResult escribe un resultado a archivo. func WriteResult(path string, content string) df.Result[struct{}] { return shell.WriteString(path, content) } GOEOF # --- export/exports.go — Funciones exportadas via CGO --- log_step "Generando export/ (bindings CGO)..." cat > "$PROJECT_DIR/export/exports.go" << GOEOF // Package main exporta funciones Go como C shared library. // Cada función con //export se expone como símbolo C callable desde Python. package main import "C" import ( "encoding/json" "unsafe" "github.com/lucasdataproyects/$MODULE_NAME/core" ) //export GoToUpper func GoToUpper(input *C.char) *C.char { result := core.ToUpper(C.GoString(input)) return C.CString(result) } //export GoProcessItems func GoProcessItems(jsonInput *C.char) *C.char { var items []string if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil { return C.CString("[]") } result := core.ProcessItems(items, core.ToUpper) out, _ := json.Marshal(result) return C.CString(string(out)) } //export GoFilterNonEmpty func GoFilterNonEmpty(jsonInput *C.char) *C.char { var items []string if err := json.Unmarshal([]byte(C.GoString(jsonInput)), &items); err != nil { return C.CString("[]") } result := core.FilterNonEmpty(items) out, _ := json.Marshal(result) return C.CString(string(out)) } //export GoSafeDivide func GoSafeDivide(a, b C.double) *C.char { result := core.SafeDivide(float64(a), float64(b)) if result.IsErr() { return C.CString(`{"error":"` + result.Error().Error() + `"}`) } out, _ := json.Marshal(map[string]float64{"value": result.Unwrap()}) return C.CString(string(out)) } //export GoFree func GoFree(ptr *C.char) { C.free(unsafe.Pointer(ptr)) } func main() {} GOEOF # --- python/bindings/__init__.py — Wrapper ctypes auto-generado --- log_step "Generando python/bindings/ (ctypes wrapper)..." # Nombre de la shared library según OS SO_NAME="lib${MODULE_NAME}.so" cat > "$PROJECT_DIR/python/bindings/__init__.py" << PYEOF """ Auto-generated Python bindings for $MODULE_NAME. Uses ctypes to call Go functions compiled as C shared library. Usage: from bindings import to_upper, process_items, filter_non_empty, safe_divide """ import ctypes import json import os from pathlib import Path # Localizar la shared library _LIB_DIR = Path(__file__).parent.parent.parent / "build" _LIB_NAME = "$SO_NAME" _LIB_PATH = _LIB_DIR / _LIB_NAME if not _LIB_PATH.exists(): raise FileNotFoundError( f"Shared library not found at {_LIB_PATH}. " f"Run 'make build' in the project root first." ) _lib = ctypes.CDLL(str(_LIB_PATH)) # --- Configurar tipos de retorno --- _lib.GoToUpper.argtypes = [ctypes.c_char_p] _lib.GoToUpper.restype = ctypes.c_char_p _lib.GoProcessItems.argtypes = [ctypes.c_char_p] _lib.GoProcessItems.restype = ctypes.c_char_p _lib.GoFilterNonEmpty.argtypes = [ctypes.c_char_p] _lib.GoFilterNonEmpty.restype = ctypes.c_char_p _lib.GoSafeDivide.argtypes = [ctypes.c_double, ctypes.c_double] _lib.GoSafeDivide.restype = ctypes.c_char_p _lib.GoFree.argtypes = [ctypes.c_char_p] _lib.GoFree.restype = None def to_upper(text: str) -> str: """Convert text to uppercase using Go core.""" result = _lib.GoToUpper(text.encode("utf-8")) return result.decode("utf-8") def process_items(items: list[str]) -> list[str]: """Process items through Go pipeline (ToUpper transformation).""" input_json = json.dumps(items).encode("utf-8") result = _lib.GoProcessItems(input_json) return json.loads(result.decode("utf-8")) def filter_non_empty(items: list[str]) -> list[str]: """Filter empty strings using Go core.""" input_json = json.dumps(items).encode("utf-8") result = _lib.GoFilterNonEmpty(input_json) return json.loads(result.decode("utf-8")) def safe_divide(a: float, b: float) -> float: """Safe division using Go Result type. Raises ValueError on division by zero.""" result = _lib.GoSafeDivide(ctypes.c_double(a), ctypes.c_double(b)) data = json.loads(result.decode("utf-8")) if "error" in data: raise ValueError(data["error"]) return data["value"] PYEOF # --- python/example.py --- cat > "$PROJECT_DIR/python/example.py" << PYEOF """Example usage of $MODULE_NAME Go bindings from Python.""" from bindings import to_upper, process_items, filter_non_empty, safe_divide # String transformation print(to_upper("hello from go")) # HELLO FROM GO # Batch processing via Go's MapSlice items = ["hello", "world", "from", "go"] print(process_items(items)) # ["HELLO", "WORLD", "FROM", "GO"] # Filtering via Go's FilterSlice mixed = ["hello", "", "world", " ", "go"] print(filter_non_empty(mixed)) # ["hello", "world", "go"] # Safe division with Result[T] error handling print(safe_divide(10.0, 3.0)) # 3.333... try: safe_divide(10.0, 0.0) except ValueError as e: print(f"Caught: {e}") # Caught: division by zero PYEOF # --- core/transform_test.go --- log_step "Generando tests..." cat > "$PROJECT_DIR/core/transform_test.go" << 'GOEOF' package core import ( "testing" ) func TestToUpper(t *testing.T) { if got := ToUpper("hello"); got != "HELLO" { t.Errorf("ToUpper(\"hello\") = %q, want %q", got, "HELLO") } } func TestFilterNonEmpty(t *testing.T) { items := []string{"hello", "", "world", " ", "go"} result := FilterNonEmpty(items) if len(result) != 3 { t.Errorf("FilterNonEmpty got %d items, want 3", len(result)) } } func TestSafeDivide(t *testing.T) { ok := SafeDivide(10, 2) if ok.IsErr() { t.Error("SafeDivide(10, 2) should not error") } if ok.Unwrap() != 5.0 { t.Errorf("SafeDivide(10, 2) = %f, want 5.0", ok.Unwrap()) } err := SafeDivide(10, 0) if !err.IsErr() { t.Error("SafeDivide(10, 0) should error") } } GOEOF # --- Makefile --- log_step "Generando Makefile..." cat > "$PROJECT_DIR/Makefile" << MKEOF .PHONY: build test clean python dev MODULE_NAME := $MODULE_NAME BUILD_DIR := build SO_NAME := $SO_NAME ## build: Compila la shared library (.so) para Python build: @mkdir -p \$(BUILD_DIR) cd export && CGO_ENABLED=1 go build -buildmode=c-shared -o ../\$(BUILD_DIR)/\$(SO_NAME) . @echo "✓ Built \$(BUILD_DIR)/\$(SO_NAME)" ## test: Ejecuta tests de Go test: go test ./core/... ./shell/... -v ## python: Compila y ejecuta ejemplo Python python: build cd python && python3 example.py ## clean: Limpia artefactos clean: rm -rf \$(BUILD_DIR) find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true ## dev: Tests + build en un paso dev: test build @echo "✓ Ready — run 'make python' to test bindings" ## tidy: go mod tidy tidy: go mod tidy ## help: Muestra esta ayuda help: @grep -E '^## ' Makefile | sed 's/## //' | column -t -s ':' MKEOF # --- .gitignore --- cat > "$PROJECT_DIR/.gitignore" << 'EOF' build/ *.so *.h *.dylib *.dll __pycache__/ *.pyc .pytest_cache/ EOF # --- go mod tidy --- log_step "Ejecutando go mod tidy..." cd "$PROJECT_DIR" if [[ -f "go.work" ]]; then go mod tidy 2>/dev/null || log_warn "go mod tidy falló — revisa el go.work" else go mod tidy 2>/dev/null || log_warn "go mod tidy falló — DevFactory no está disponible" fi # --- Resumen --- echo "" log_ok "Módulo '$MODULE_NAME' creado en $PROJECT_DIR" echo "" echo -e "${CYAN}Estructura:${NC}" echo " $MODULE_NAME/" echo " ├── core/ — Funciones puras (sin side effects)" echo " ├── shell/ — Operaciones I/O con Result[T]" echo " ├── export/ — Funciones exportadas via CGO" echo " ├── python/bindings — Wrapper ctypes auto-generado" echo " ├── Makefile — build, test, python, clean" echo " └── go.work — Enlace a DevFactory" echo "" echo -e "${CYAN}Comandos:${NC}" echo " make test — Ejecutar tests Go" echo " make build — Compilar shared library" echo " make python — Testear bindings Python" echo " make dev — Test + build en un paso" echo "" echo "STATUS: READY"