Files
notifier/REFACTORING_QUICKREF.md
2025-10-26 02:25:24 -07:00

7.6 KiB

Code Duplication Refactoring - Quick Reference Guide

Top 3 Quick Wins (Do These First!)

1. Remove Duplicate Type Conversion Function (15 minutes)

File: /Users/igodwin/Workspace/notifier/api/grpc/handler.go

Problem: Function convertDomainToProtoType (lines 331-344) is an exact duplicate of convertDomainTypeToProto (lines 294-307).

Action:

  1. Delete lines 331-344
  2. Replace call at line 368:
    - Type:       convertDomainToProtoType(notif.Type),
    + Type:       convertDomainTypeToProto(notif.Type),
    

Result: Removes 14 lines of duplicate code


2. Extract Notifier Validation Pattern (30 minutes)

Files:

  • /Users/igodwin/Workspace/notifier/internal/notifier/smtp.go (lines 63-70)
  • /Users/igodwin/Workspace/notifier/internal/notifier/slack.go (lines 78-85)
  • /Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go (lines 185-192)
  • /Users/igodwin/Workspace/notifier/internal/notifier/stdout.go (lines 26-33)

Problem: All 4 notifiers have identical validation at start of Send():

if err := ValidateContext(ctx); err != nil {
    return nil, err
}
if err := s.Validate(notification); err != nil {
    return nil, err
}

Action:

  1. Add to /Users/igodwin/Workspace/notifier/internal/notifier/notifier.go:
// ValidateNotification validates context and notification for sending
func ValidateNotification(ctx context.Context, n *domain.Notification, validator domain.Notifier) error {
    if err := ValidateContext(ctx); err != nil {
        return err
    }
    return validator.Validate(n)
}
  1. Replace validation blocks in all 4 files with:
if err := ValidateNotification(ctx, notification, s); err != nil {
    return nil, err
}

Result: Reduces duplication from 4 places to 1, easier maintenance


3. Add NotificationResult Error Helper (30 minutes)

Files:

  • /Users/igodwin/Workspace/notifier/internal/notifier/smtp.go (multiple locations)
  • /Users/igodwin/Workspace/notifier/internal/notifier/slack.go (multiple locations)
  • /Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go (multiple locations)

Problem: Repeated error result creation pattern in all notifiers

Action:

  1. Add to BaseNotifier struct:
func (b *BaseNotifier) ErrorResult(notificationID, msg string) *domain.NotificationResult {
    return &domain.NotificationResult{
        NotificationID: notificationID,
        Success:        false,
        Error:          msg,
        SentAt:         time.Now(),
    }
}
  1. Replace error creation calls with:
return b.ErrorResult(notification.ID, err.Error()), err

Result: Consistent error handling across all notifiers


Medium-Effort Improvements

4. Refactor Filter Matching (40 minutes)

File: /Users/igodwin/Workspace/notifier/internal/service/service.go (lines 481-540)

Problem: 60+ lines of repetitive filter matching logic

Solution: Extract helper functions:

func contains[T comparable](items []T, target T) bool {
    for _, item := range items {
        if item == target {
            return true
        }
    }
    return false
}

func notificationHasRecipient(n *domain.Notification, recipients []string) bool {
    for _, fr := range recipients {
        if contains(n.Recipients, fr) {
            return true
        }
    }
    return false
}

Then simplify matchesFilter to use these helpers.

Result: Reduces 60+ lines to ~30, easier to extend


5. Extract Auth Validation (50 minutes)

Files:

  • /Users/igodwin/Workspace/notifier/internal/auth/rest_middleware.go (lines 35-50)
  • /Users/igodwin/Workspace/notifier/internal/auth/grpc_middleware.go (lines 38-50, 82-94)

Problem: Same validation logic in 3 places

Solution: Add to APIKeyStore:

type ValidatedKey struct {
    APIKey   *APIKey
    ClientID string
    Roles    []string
}

func (s *APIKeyStore) ValidateAndAuthorize(keyStr string) (*ValidatedKey, error) {
    key, err := s.ValidateKey(keyStr)
    if err != nil {
        return nil, err
    }
    
    allowed, err := s.CheckRateLimit(keyStr)
    if err != nil || !allowed {
        return nil, fmt.Errorf("rate limit exceeded")
    }
    
    if err := s.UpdateLastUsed(keyStr); err != nil {
        // Log warning but continue
    }
    
    return &ValidatedKey{
        APIKey:   key,
        ClientID: key.ClientID,
        Roles:    key.Roles,
    }, nil
}

Use in both middleware implementations.

Result: Consistent auth logic, easier to update validation rules


6. Create HTTP Request Helper (45 minutes)

Files:

  • /Users/igodwin/Workspace/notifier/internal/notifier/slack.go (sendToSlack)
  • /Users/igodwin/Workspace/notifier/internal/notifier/ntfy.go (sendToTopic)

Problem: 25+ lines of identical HTTP handling

Solution: Add to notifier package:

func sendJSONPostRequest(ctx context.Context, client *http.Client, 
    url string, payload interface{}, headers map[string]string) error {
    
    jsonData, err := json.Marshal(payload)
    if err != nil {
        return fmt.Errorf("failed to marshal: %w", err)
    }
    
    req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("failed to create request: %w", err)
    }
    
    req.Header.Set("Content-Type", "application/json")
    for k, v := range headers {
        req.Header.Set(k, v)
    }
    
    resp, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("failed to send request: %w", err)
    }
    defer resp.Body.Close()
    
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("API returned status: %d", resp.StatusCode)
    }
    return nil
}

Result: Single source of truth for HTTP logic


More Involved Refactorings

7. Generic Notifier Registration (60 minutes)

File: /Users/igodwin/Workspace/notifier/cmd/server/main.go (lines 193-241)

Creates generic registration helper to reduce 50+ lines of repetitive code.

8. Generic Default Account Resolution (45 minutes)

File: /Users/igodwin/Workspace/notifier/internal/config/config.go (lines 346-380)

Extract generic helper to eliminate 35+ lines of repetitive default account lookup.


Implementation Checklist

  • Remove duplicate convertDomainToProtoType (15 min)
  • Extract validation helper (30 min)
  • Add error result helper (30 min)
  • Total: 75 minutes
  • Refactor filter matching (40 min)
  • Extract auth validation (50 min)
  • Create HTTP helper (45 min)
  • Total: 135 minutes

Phase 3 (Nice to Have: Later)

  • Generic registration function (60 min)
  • Generic default resolution (45 min)
  • Total: 105 minutes

Testing Strategy

  1. Phase 1 refactorings: Run existing unit tests - no behavior changes
  2. Phase 2 refactorings: Add unit tests for new helper functions
  3. Phase 3 refactorings: Comprehensive integration tests for registration flow

All changes maintain backward compatibility.


Expected Benefits

Metric Phase 1 Phase 1+2 All Phases
Lines of duplication removed ~60 ~250 ~380
Number of duplicate patterns 3 6 8
Estimated maintenance effort reduction 15% 40% 60%

Files to Review After Refactoring

  1. All notifier implementations (internal/notifier/)
  2. Service filtering logic (internal/service/service.go)
  3. Auth middleware files (internal/auth/)
  4. Main server initialization (cmd/server/main.go)
  5. Config file (internal/config/config.go)