Skip to content

Custom Module Example

This example shows how to create a custom GOE module for email functionality.

Email Module

Email Service Interface

go
package email

import "context"

type EmailService interface {
    SendEmail(to, subject, body string) error
    SendTemplateEmail(to, template string, data interface{}) error
}

type EmailConfig struct {
    SMTPHost     string
    SMTPPort     int
    SMTPUsername string
    SMTPPassword string
    FromEmail    string
    FromName     string
}

Email Service Implementation

go
package email

import (
    "fmt"
    "net/smtp"
    "go.oease.dev/goe/v2/contract"
)

type emailService struct {
    config *EmailConfig
    logger contract.Logger
}

func NewEmailService(config *EmailConfig, logger contract.Logger) EmailService {
    return &emailService{
        config: config,
        logger: logger,
    }
}

func (s *emailService) SendEmail(to, subject, body string) error {
    s.logger.Info("Sending email", "to", to, "subject", subject)
    
    // Set up authentication
    auth := smtp.PlainAuth("", s.config.SMTPUsername, s.config.SMTPPassword, s.config.SMTPHost)
    
    // Compose message
    message := fmt.Sprintf("From: %s <%s>\r\n", s.config.FromName, s.config.FromEmail)
    message += fmt.Sprintf("To: %s\r\n", to)
    message += fmt.Sprintf("Subject: %s\r\n", subject)
    message += "\r\n"
    message += body
    
    // Send email
    addr := fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort)
    err := smtp.SendMail(addr, auth, s.config.FromEmail, []string{to}, []byte(message))
    
    if err != nil {
        s.logger.Error("Failed to send email", "to", to, "error", err)
        return err
    }
    
    s.logger.Info("Email sent successfully", "to", to)
    return nil
}

func (s *emailService) SendTemplateEmail(to, template string, data interface{}) error {
    // Template rendering logic would go here
    body := fmt.Sprintf("Template: %s, Data: %v", template, data)
    return s.SendEmail(to, "Template Email", body)
}

Email Module

go
package email

import (
    "context"
    "go.oease.dev/goe/v2/contract"
)

type Module struct {
    service EmailService
    config  *EmailConfig
    logger  contract.Logger
}

func NewModule(config contract.Config, logger contract.Logger) *Module {
    emailConfig := &EmailConfig{
        SMTPHost:     config.GetString("SMTP_HOST"),
        SMTPPort:     config.GetInt("SMTP_PORT"),
        SMTPUsername: config.GetString("SMTP_USERNAME"),
        SMTPPassword: config.GetString("SMTP_PASSWORD"),
        FromEmail:    config.GetString("FROM_EMAIL"),
        FromName:     config.GetString("FROM_NAME"),
    }
    
    service := NewEmailService(emailConfig, logger)
    
    return &Module{
        service: service,
        config:  emailConfig,
        logger:  logger,
    }
}

func (m *Module) Name() string {
    return "email-module"
}

func (m *Module) OnStart(ctx context.Context) error {
    m.logger.Info("Email module starting")
    
    // Validate configuration
    if m.config.SMTPHost == "" {
        return fmt.Errorf("SMTP_HOST is required")
    }
    
    if m.config.FromEmail == "" {
        return fmt.Errorf("FROM_EMAIL is required")
    }
    
    m.logger.Info("Email module started successfully")
    return nil
}

func (m *Module) OnStop(ctx context.Context) error {
    m.logger.Info("Email module stopping")
    
    // Cleanup logic here if needed
    
    m.logger.Info("Email module stopped")
    return nil
}

func (m *Module) EmailService() EmailService {
    return m.service
}

Using the Custom Module

Main Application

go
package main

import (
    "go.oease.dev/goe/v2"
    "go.oease.dev/goe/v2/contract"
    "your-app/email"
    "your-app/handler"
    "your-app/service"
)

func main() {
    goe.New(goe.Options{
        WithHTTP: true,
        Providers: []any{
            email.NewModule,
            service.NewUserService,
            handler.NewUserHandler,
        },
        Invokers: []any{
            RegisterRoutes,
        },
    })
    
    goe.Run()
}

func RegisterRoutes(httpKernel contract.HTTPKernel, userHandler *handler.UserHandler) {
    app := httpKernel.App()
    
    app.Post("/users", userHandler.CreateUser)
    app.Post("/users/:id/send-email", userHandler.SendEmailToUser)
}

User Service with Email

go
package service

import (
    "go.oease.dev/goe/v2/contract"
    "your-app/email"
    "your-app/model"
)

type UserService struct {
    repository   UserRepository
    emailService email.EmailService
    logger       contract.Logger
}

func NewUserService(repository UserRepository, emailModule *email.Module, logger contract.Logger) *UserService {
    return &UserService{
        repository:   repository,
        emailService: emailModule.EmailService(),
        logger:       logger,
    }
}

func (s *UserService) CreateUser(name, email string) (*model.User, error) {
    s.logger.Info("Creating user", "name", name, "email", email)
    
    user := &model.User{
        Name:  name,
        Email: email,
    }
    
    if err := s.repository.Create(user); err != nil {
        s.logger.Error("Failed to create user", "error", err)
        return nil, err
    }
    
    // Send welcome email
    go func() {
        err := s.emailService.SendEmail(
            user.Email,
            "Welcome to Our App",
            fmt.Sprintf("Hello %s, welcome to our application!", user.Name),
        )
        if err != nil {
            s.logger.Error("Failed to send welcome email", "user_id", user.ID, "error", err)
        }
    }()
    
    s.logger.Info("User created successfully", "user_id", user.ID)
    return user, nil
}

func (s *UserService) SendEmailToUser(userID uint, subject, body string) error {
    user, err := s.repository.GetByID(userID)
    if err != nil {
        return err
    }
    
    return s.emailService.SendEmail(user.Email, subject, body)
}

Handler with Email

go
package handler

import (
    "github.com/gofiber/fiber/v3"
    "go.oease.dev/goe/v2/contract"
    "your-app/service"
)

type UserHandler struct {
    userService *service.UserService
    logger      contract.Logger
}

func NewUserHandler(userService *service.UserService, logger contract.Logger) *UserHandler {
    return &UserHandler{
        userService: userService,
        logger:      logger,
    }
}

type CreateUserRequest struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

type SendEmailRequest struct {
    Subject string `json:"subject" validate:"required"`
    Body    string `json:"body" validate:"required"`
}

func (h *UserHandler) CreateUser(c fiber.Ctx) error {
    var req CreateUserRequest
    
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
    }
    
    if err := validate.Struct(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Validation failed"})
    }
    
    user, err := h.userService.CreateUser(req.Name, req.Email)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{"error": "Failed to create user"})
    }
    
    return c.Status(201).JSON(user)
}

func (h *UserHandler) SendEmailToUser(c fiber.Ctx) error {
    userID, err := c.ParamsInt("id")
    if err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid user ID"})
    }
    
    var req SendEmailRequest
    if err := c.BodyParser(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
    }
    
    if err := validate.Struct(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "Validation failed"})
    }
    
    if err := h.userService.SendEmailToUser(uint(userID), req.Subject, req.Body); err != nil {
        return c.Status(500).JSON(fiber.Map{"error": "Failed to send email"})
    }
    
    return c.JSON(fiber.Map{"message": "Email sent successfully"})
}

Background Task Module

Task Module

go
package task

import (
    "context"
    "time"
    "go.oease.dev/goe/v2/contract"
)

type TaskService interface {
    StartPeriodicTask(name string, interval time.Duration, task func() error)
    StopTask(name string)
}

type taskService struct {
    tasks  map[string]chan struct{}
    logger contract.Logger
}

func NewTaskService(logger contract.Logger) TaskService {
    return &taskService{
        tasks:  make(map[string]chan struct{}),
        logger: logger,
    }
}

func (s *taskService) StartPeriodicTask(name string, interval time.Duration, task func() error) {
    s.logger.Info("Starting periodic task", "name", name, "interval", interval)
    
    done := make(chan struct{})
    s.tasks[name] = done
    
    go func() {
        ticker := time.NewTicker(interval)
        defer ticker.Stop()
        
        for {
            select {
            case <-ticker.C:
                s.logger.Debug("Running periodic task", "name", name)
                if err := task(); err != nil {
                    s.logger.Error("Periodic task failed", "name", name, "error", err)
                }
            case <-done:
                s.logger.Info("Periodic task stopped", "name", name)
                return
            }
        }
    }()
}

func (s *taskService) StopTask(name string) {
    if done, exists := s.tasks[name]; exists {
        close(done)
        delete(s.tasks, name)
    }
}

type Module struct {
    service TaskService
    logger  contract.Logger
}

func NewModule(logger contract.Logger) *Module {
    service := NewTaskService(logger)
    
    return &Module{
        service: service,
        logger:  logger,
    }
}

func (m *Module) Name() string {
    return "task-module"
}

func (m *Module) OnStart(ctx context.Context) error {
    m.logger.Info("Task module started")
    
    // Start cleanup task
    m.service.StartPeriodicTask("cleanup", 1*time.Hour, func() error {
        m.logger.Info("Running cleanup task")
        // Cleanup logic here
        return nil
    })
    
    return nil
}

func (m *Module) OnStop(ctx context.Context) error {
    m.logger.Info("Task module stopping")
    
    // Stop all tasks
    m.service.StopTask("cleanup")
    
    m.logger.Info("Task module stopped")
    return nil
}

func (m *Module) TaskService() TaskService {
    return m.service
}

Configuration

Add configuration for the custom modules:

bash
# .env
# Email configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
FROM_EMAIL=your-email@gmail.com
FROM_NAME=Your App Name

# Other configuration
HTTP_PORT=8080
LOG_LEVEL=info

Testing the Custom Module

bash
# Create a user (will trigger welcome email)
curl -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com"}'

# Send email to user
curl -X POST http://localhost:8080/users/1/send-email \
  -H "Content-Type: application/json" \
  -d '{"subject":"Test Email","body":"This is a test email"}'

Testing Custom Modules

go
package email

import (
    "context"
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "go.oease.dev/goe/v2/contract"
)

type MockConfig struct {
    mock.Mock
}

func (m *MockConfig) GetString(key string) string {
    args := m.Called(key)
    return args.String(0)
}

func (m *MockConfig) GetInt(key string) int {
    args := m.Called(key)
    return args.Int(0)
}

type MockLogger struct {
    mock.Mock
}

func (m *MockLogger) Info(msg string, keysAndValues ...interface{}) {
    args := []interface{}{msg}
    args = append(args, keysAndValues...)
    m.Called(args...)
}

func (m *MockLogger) Error(msg string, keysAndValues ...interface{}) {
    args := []interface{}{msg}
    args = append(args, keysAndValues...)
    m.Called(args...)
}

func TestEmailModule_OnStart(t *testing.T) {
    mockConfig := new(MockConfig)
    mockLogger := new(MockLogger)
    
    mockConfig.On("GetString", "SMTP_HOST").Return("smtp.gmail.com")
    mockConfig.On("GetInt", "SMTP_PORT").Return(587)
    mockConfig.On("GetString", "SMTP_USERNAME").Return("test@gmail.com")
    mockConfig.On("GetString", "SMTP_PASSWORD").Return("password")
    mockConfig.On("GetString", "FROM_EMAIL").Return("test@gmail.com")
    mockConfig.On("GetString", "FROM_NAME").Return("Test App")
    
    mockLogger.On("Info", mock.Anything, mock.Anything).Return()
    
    module := NewModule(mockConfig, mockLogger)
    
    err := module.OnStart(context.Background())
    
    assert.NoError(t, err)
    mockConfig.AssertExpectations(t)
    mockLogger.AssertExpectations(t)
}

This example demonstrates:

  • Creating a custom module with lifecycle management
  • Implementing a service interface
  • Using configuration for module setup
  • Integration with other services
  • Background task management
  • Proper error handling and logging
  • Testing custom modules

Released under the MIT License.