Basic impl added
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/igodwin/notifier/internal/domain"
|
||||
)
|
||||
|
||||
// Handler handles REST API requests
|
||||
type Handler struct {
|
||||
service domain.NotificationService
|
||||
}
|
||||
|
||||
// NewHandler creates a new REST handler
|
||||
func NewHandler(service domain.NotificationService) *Handler {
|
||||
return &Handler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification handles POST /api/v1/notifications
|
||||
func (h *Handler) SendNotification(w http.ResponseWriter, r *http.Request) {
|
||||
var req SendNotificationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid request body", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := req.Validate(); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "validation failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to domain notification
|
||||
notification := req.ToNotification()
|
||||
|
||||
// Send notification
|
||||
result, err := h.service.Send(r.Context(), notification)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to send notification", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusAccepted, SendNotificationResponse{
|
||||
Result: NotificationResultFromDomain(result),
|
||||
})
|
||||
}
|
||||
|
||||
// SendBatchNotifications handles POST /api/v1/notifications/batch
|
||||
func (h *Handler) SendBatchNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
var req SendBatchNotificationsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid request body", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and convert to domain notifications
|
||||
notifications := make([]*domain.Notification, 0, len(req.Notifications))
|
||||
for _, notifReq := range req.Notifications {
|
||||
if err := notifReq.Validate(); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "validation failed", err)
|
||||
return
|
||||
}
|
||||
notifications = append(notifications, notifReq.ToNotification())
|
||||
}
|
||||
|
||||
// Send batch
|
||||
results, err := h.service.SendBatch(r.Context(), notifications)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to send batch notifications", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert results
|
||||
apiResults := make([]NotificationResult, 0, len(results))
|
||||
for _, result := range results {
|
||||
apiResults = append(apiResults, NotificationResultFromDomain(result))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusAccepted, SendBatchNotificationsResponse{
|
||||
Results: apiResults,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNotification handles GET /api/v1/notifications/{id}
|
||||
func (h *Handler) GetNotification(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
notification, err := h.service.GetNotification(r.Context(), id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusNotFound, "notification not found", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, NotificationFromDomain(notification))
|
||||
}
|
||||
|
||||
// ListNotifications handles GET /api/v1/notifications
|
||||
func (h *Handler) ListNotifications(w http.ResponseWriter, r *http.Request) {
|
||||
filter := parseNotificationFilter(r)
|
||||
|
||||
notifications, err := h.service.ListNotifications(r.Context(), filter)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to list notifications", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to API format
|
||||
apiNotifications := make([]Notification, 0, len(notifications))
|
||||
for _, notif := range notifications {
|
||||
apiNotifications = append(apiNotifications, NotificationFromDomain(notif))
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, ListNotificationsResponse{
|
||||
Notifications: apiNotifications,
|
||||
Total: int64(len(apiNotifications)),
|
||||
})
|
||||
}
|
||||
|
||||
// CancelNotification handles DELETE /api/v1/notifications/{id}
|
||||
func (h *Handler) CancelNotification(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
if err := h.service.CancelNotification(r.Context(), id); err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to cancel notification", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "notification canceled successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// RetryNotification handles POST /api/v1/notifications/{id}/retry
|
||||
func (h *Handler) RetryNotification(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
result, err := h.service.RetryNotification(r.Context(), id)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to retry notification", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, RetryNotificationResponse{
|
||||
Result: NotificationResultFromDomain(result),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats handles GET /api/v1/stats
|
||||
func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := h.service.GetStats(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to get stats", err)
|
||||
return
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// HealthCheck handles GET /health
|
||||
func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
respondJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"service": "notifier",
|
||||
"time": time.Now().UTC(),
|
||||
})
|
||||
}
|
||||
|
||||
// parseNotificationFilter parses query parameters into a NotificationFilter
|
||||
func parseNotificationFilter(r *http.Request) *domain.NotificationFilter {
|
||||
query := r.URL.Query()
|
||||
filter := &domain.NotificationFilter{}
|
||||
|
||||
// Parse limit
|
||||
if limitStr := query.Get("limit"); limitStr != "" {
|
||||
if limit, err := strconv.Atoi(limitStr); err == nil {
|
||||
filter.Limit = limit
|
||||
}
|
||||
}
|
||||
|
||||
// Parse offset
|
||||
if offsetStr := query.Get("offset"); offsetStr != "" {
|
||||
if offset, err := strconv.Atoi(offsetStr); err == nil {
|
||||
filter.Offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
// Parse types
|
||||
if types := query["type"]; len(types) > 0 {
|
||||
for _, t := range types {
|
||||
filter.Types = append(filter.Types, domain.NotificationType(t))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse statuses
|
||||
if statuses := query["status"]; len(statuses) > 0 {
|
||||
for _, s := range statuses {
|
||||
filter.Statuses = append(filter.Statuses, domain.NotificationStatus(s))
|
||||
}
|
||||
}
|
||||
|
||||
// Parse recipients
|
||||
if recipients := query["recipient"]; len(recipients) > 0 {
|
||||
filter.Recipients = recipients
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// respondJSON sends a JSON response
|
||||
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// respondError sends an error response
|
||||
func respondError(w http.ResponseWriter, status int, message string, err error) {
|
||||
errMsg := message
|
||||
if err != nil {
|
||||
errMsg = message + ": " + err.Error()
|
||||
}
|
||||
|
||||
respondJSON(w, status, map[string]interface{}{
|
||||
"error": message,
|
||||
"details": errMsg,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/igodwin/notifier/internal/domain"
|
||||
)
|
||||
|
||||
// NewRouter creates a new HTTP router with all routes configured
|
||||
func NewRouter(service domain.NotificationService) *mux.Router {
|
||||
handler := NewHandler(service)
|
||||
router := mux.NewRouter()
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.PathPrefix("/api/v1").Subrouter()
|
||||
|
||||
// Notification routes
|
||||
v1.HandleFunc("/notifications", handler.SendNotification).Methods(http.MethodPost)
|
||||
v1.HandleFunc("/notifications/batch", handler.SendBatchNotifications).Methods(http.MethodPost)
|
||||
v1.HandleFunc("/notifications", handler.ListNotifications).Methods(http.MethodGet)
|
||||
v1.HandleFunc("/notifications/{id}", handler.GetNotification).Methods(http.MethodGet)
|
||||
v1.HandleFunc("/notifications/{id}", handler.CancelNotification).Methods(http.MethodDelete)
|
||||
v1.HandleFunc("/notifications/{id}/retry", handler.RetryNotification).Methods(http.MethodPost)
|
||||
|
||||
// Stats route
|
||||
v1.HandleFunc("/stats", handler.GetStats).Methods(http.MethodGet)
|
||||
|
||||
// Health check route
|
||||
router.HandleFunc("/health", handler.HealthCheck).Methods(http.MethodGet)
|
||||
|
||||
// Middleware
|
||||
router.Use(loggingMiddleware)
|
||||
router.Use(corsMiddleware)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// loggingMiddleware logs incoming requests
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// You can add structured logging here
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// corsMiddleware adds CORS headers
|
||||
func corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/igodwin/notifier/internal/domain"
|
||||
)
|
||||
|
||||
// SendNotificationRequest is the REST API request for sending a notification
|
||||
type SendNotificationRequest struct {
|
||||
Type string `json:"type"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
|
||||
MaxRetries int `json:"max_retries,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates the request
|
||||
func (r *SendNotificationRequest) Validate() error {
|
||||
if r.Type == "" {
|
||||
return fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
if len(r.Recipients) == 0 {
|
||||
return fmt.Errorf("at least one recipient is required")
|
||||
}
|
||||
|
||||
if r.Body == "" {
|
||||
return fmt.Errorf("body is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToNotification converts the request to a domain notification
|
||||
func (r *SendNotificationRequest) ToNotification() *domain.Notification {
|
||||
maxRetries := r.MaxRetries
|
||||
if maxRetries == 0 {
|
||||
maxRetries = 3 // Default
|
||||
}
|
||||
|
||||
return &domain.Notification{
|
||||
ID: uuid.New().String(),
|
||||
Type: domain.NotificationType(r.Type),
|
||||
Priority: domain.Priority(r.Priority),
|
||||
Status: domain.StatusPending,
|
||||
Subject: r.Subject,
|
||||
Body: r.Body,
|
||||
Recipients: r.Recipients,
|
||||
Metadata: r.Metadata,
|
||||
CreatedAt: time.Now(),
|
||||
ScheduledFor: r.ScheduledFor,
|
||||
MaxRetries: maxRetries,
|
||||
RetryCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotificationResponse is the REST API response for sending a notification
|
||||
type SendNotificationResponse struct {
|
||||
Result NotificationResult `json:"result"`
|
||||
}
|
||||
|
||||
// SendBatchNotificationsRequest is the REST API request for sending multiple notifications
|
||||
type SendBatchNotificationsRequest struct {
|
||||
Notifications []SendNotificationRequest `json:"notifications"`
|
||||
}
|
||||
|
||||
// SendBatchNotificationsResponse is the REST API response for sending multiple notifications
|
||||
type SendBatchNotificationsResponse struct {
|
||||
Results []NotificationResult `json:"results"`
|
||||
}
|
||||
|
||||
// Notification represents a notification in the REST API
|
||||
type Notification struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Priority int `json:"priority"`
|
||||
Status string `json:"status"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
|
||||
SentAt *time.Time `json:"sent_at,omitempty"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
MaxRetries int `json:"max_retries"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationFromDomain converts a domain notification to API format
|
||||
func NotificationFromDomain(n *domain.Notification) Notification {
|
||||
return Notification{
|
||||
ID: n.ID,
|
||||
Type: string(n.Type),
|
||||
Priority: int(n.Priority),
|
||||
Status: string(n.Status),
|
||||
Subject: n.Subject,
|
||||
Body: n.Body,
|
||||
Recipients: n.Recipients,
|
||||
Metadata: n.Metadata,
|
||||
CreatedAt: n.CreatedAt,
|
||||
ScheduledFor: n.ScheduledFor,
|
||||
SentAt: n.SentAt,
|
||||
RetryCount: n.RetryCount,
|
||||
MaxRetries: n.MaxRetries,
|
||||
LastError: n.LastError,
|
||||
}
|
||||
}
|
||||
|
||||
// NotificationResult represents the result of a notification operation
|
||||
type NotificationResult struct {
|
||||
NotificationID string `json:"notification_id"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
SentAt time.Time `json:"sent_at"`
|
||||
ProviderResponse map[string]interface{} `json:"provider_response,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResultFromDomain converts a domain result to API format
|
||||
func NotificationResultFromDomain(r *domain.NotificationResult) NotificationResult {
|
||||
return NotificationResult{
|
||||
NotificationID: r.NotificationID,
|
||||
Success: r.Success,
|
||||
Message: r.Message,
|
||||
Error: r.Error,
|
||||
SentAt: r.SentAt,
|
||||
ProviderResponse: r.ProviderResponse,
|
||||
}
|
||||
}
|
||||
|
||||
// ListNotificationsResponse is the REST API response for listing notifications
|
||||
type ListNotificationsResponse struct {
|
||||
Notifications []Notification `json:"notifications"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// RetryNotificationResponse is the REST API response for retrying a notification
|
||||
type RetryNotificationResponse struct {
|
||||
Result NotificationResult `json:"result"`
|
||||
}
|
||||
Reference in New Issue
Block a user