Basic impl added

This commit is contained in:
2025-10-16 21:22:51 -07:00
parent 097ca99788
commit 9087a710e5
43 changed files with 7616 additions and 107 deletions
+196
View File
@@ -0,0 +1,196 @@
syntax = "proto3";
package notifier.v1;
option go_package = "github.com/igodwin/notifier/api/grpc/pb";
import "google/protobuf/timestamp.proto";
// NotifierService handles notification operations
service NotifierService {
// SendNotification sends a single notification
rpc SendNotification(SendNotificationRequest) returns (SendNotificationResponse);
// SendBatchNotifications sends multiple notifications
rpc SendBatchNotifications(SendBatchNotificationsRequest) returns (SendBatchNotificationsResponse);
// GetNotification retrieves a notification by ID
rpc GetNotification(GetNotificationRequest) returns (GetNotificationResponse);
// ListNotifications retrieves notifications matching a filter
rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse);
// CancelNotification cancels a pending notification
rpc CancelNotification(CancelNotificationRequest) returns (CancelNotificationResponse);
// RetryNotification retries a failed notification
rpc RetryNotification(RetryNotificationRequest) returns (RetryNotificationResponse);
// GetStats returns notification statistics
rpc GetStats(GetStatsRequest) returns (GetStatsResponse);
// HealthCheck verifies the service is operational
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
}
// NotificationType defines the channel for notification delivery
enum NotificationType {
NOTIFICATION_TYPE_UNSPECIFIED = 0;
NOTIFICATION_TYPE_EMAIL = 1;
NOTIFICATION_TYPE_SLACK = 2;
NOTIFICATION_TYPE_NTFY = 3;
NOTIFICATION_TYPE_STDOUT = 4;
}
// Priority defines the urgency level
enum Priority {
PRIORITY_UNSPECIFIED = 0;
PRIORITY_LOW = 1;
PRIORITY_NORMAL = 2;
PRIORITY_HIGH = 3;
PRIORITY_CRITICAL = 4;
}
// NotificationStatus represents the state of a notification
enum NotificationStatus {
NOTIFICATION_STATUS_UNSPECIFIED = 0;
NOTIFICATION_STATUS_PENDING = 1;
NOTIFICATION_STATUS_QUEUED = 2;
NOTIFICATION_STATUS_PROCESSING = 3;
NOTIFICATION_STATUS_SENT = 4;
NOTIFICATION_STATUS_FAILED = 5;
NOTIFICATION_STATUS_RETRYING = 6;
}
// Notification represents a notification message
message Notification {
string id = 1;
NotificationType type = 2;
Priority priority = 3;
NotificationStatus status = 4;
string subject = 5;
string body = 6;
repeated string recipients = 7;
map<string, string> metadata = 8;
google.protobuf.Timestamp created_at = 9;
google.protobuf.Timestamp scheduled_for = 10;
google.protobuf.Timestamp sent_at = 11;
int32 retry_count = 12;
int32 max_retries = 13;
string last_error = 14;
}
// NotificationResult represents the outcome of sending a notification
message NotificationResult {
string notification_id = 1;
bool success = 2;
string message = 3;
string error = 4;
google.protobuf.Timestamp sent_at = 5;
map<string, string> provider_response = 6;
}
// SendNotificationRequest sends a single notification
message SendNotificationRequest {
NotificationType type = 1;
Priority priority = 2;
string subject = 3;
string body = 4;
repeated string recipients = 5;
map<string, string> metadata = 6;
google.protobuf.Timestamp scheduled_for = 7;
int32 max_retries = 8;
}
// SendNotificationResponse returns the result of sending a notification
message SendNotificationResponse {
NotificationResult result = 1;
}
// SendBatchNotificationsRequest sends multiple notifications
message SendBatchNotificationsRequest {
repeated SendNotificationRequest notifications = 1;
}
// SendBatchNotificationsResponse returns the results of sending multiple notifications
message SendBatchNotificationsResponse {
repeated NotificationResult results = 1;
}
// GetNotificationRequest retrieves a notification by ID
message GetNotificationRequest {
string id = 1;
}
// GetNotificationResponse returns a notification
message GetNotificationResponse {
Notification notification = 1;
}
// NotificationFilter is used for querying notifications
message NotificationFilter {
repeated string ids = 1;
repeated NotificationType types = 2;
repeated NotificationStatus statuses = 3;
repeated string recipients = 4;
google.protobuf.Timestamp created_after = 5;
google.protobuf.Timestamp created_before = 6;
int32 limit = 7;
int32 offset = 8;
}
// ListNotificationsRequest retrieves notifications matching a filter
message ListNotificationsRequest {
NotificationFilter filter = 1;
}
// ListNotificationsResponse returns a list of notifications
message ListNotificationsResponse {
repeated Notification notifications = 1;
int64 total = 2;
}
// CancelNotificationRequest cancels a pending notification
message CancelNotificationRequest {
string id = 1;
}
// CancelNotificationResponse returns the result of canceling a notification
message CancelNotificationResponse {
bool success = 1;
string message = 2;
}
// RetryNotificationRequest retries a failed notification
message RetryNotificationRequest {
string id = 1;
}
// RetryNotificationResponse returns the result of retrying a notification
message RetryNotificationResponse {
NotificationResult result = 1;
}
// GetStatsRequest requests notification statistics
message GetStatsRequest {}
// GetStatsResponse returns notification statistics
message GetStatsResponse {
int64 total_sent = 1;
int64 total_failed = 2;
int64 total_pending = 3;
int64 total_queued = 4;
map<string, int64> by_type = 5;
map<string, int64> by_status = 6;
double average_latency_ms = 7;
}
// HealthCheckRequest requests health status
message HealthCheckRequest {}
// HealthCheckResponse returns health status
message HealthCheckResponse {
bool healthy = 1;
string status = 2;
map<string, string> components = 3;
}
File diff suppressed because it is too large Load Diff
+407
View File
@@ -0,0 +1,407 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.33.0
// source: api/grpc/notifier.proto
package pb
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
NotifierService_SendNotification_FullMethodName = "/notifier.v1.NotifierService/SendNotification"
NotifierService_SendBatchNotifications_FullMethodName = "/notifier.v1.NotifierService/SendBatchNotifications"
NotifierService_GetNotification_FullMethodName = "/notifier.v1.NotifierService/GetNotification"
NotifierService_ListNotifications_FullMethodName = "/notifier.v1.NotifierService/ListNotifications"
NotifierService_CancelNotification_FullMethodName = "/notifier.v1.NotifierService/CancelNotification"
NotifierService_RetryNotification_FullMethodName = "/notifier.v1.NotifierService/RetryNotification"
NotifierService_GetStats_FullMethodName = "/notifier.v1.NotifierService/GetStats"
NotifierService_HealthCheck_FullMethodName = "/notifier.v1.NotifierService/HealthCheck"
)
// NotifierServiceClient is the client API for NotifierService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// NotifierService handles notification operations
type NotifierServiceClient interface {
// SendNotification sends a single notification
SendNotification(ctx context.Context, in *SendNotificationRequest, opts ...grpc.CallOption) (*SendNotificationResponse, error)
// SendBatchNotifications sends multiple notifications
SendBatchNotifications(ctx context.Context, in *SendBatchNotificationsRequest, opts ...grpc.CallOption) (*SendBatchNotificationsResponse, error)
// GetNotification retrieves a notification by ID
GetNotification(ctx context.Context, in *GetNotificationRequest, opts ...grpc.CallOption) (*GetNotificationResponse, error)
// ListNotifications retrieves notifications matching a filter
ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error)
// CancelNotification cancels a pending notification
CancelNotification(ctx context.Context, in *CancelNotificationRequest, opts ...grpc.CallOption) (*CancelNotificationResponse, error)
// RetryNotification retries a failed notification
RetryNotification(ctx context.Context, in *RetryNotificationRequest, opts ...grpc.CallOption) (*RetryNotificationResponse, error)
// GetStats returns notification statistics
GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
// HealthCheck verifies the service is operational
HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
}
type notifierServiceClient struct {
cc grpc.ClientConnInterface
}
func NewNotifierServiceClient(cc grpc.ClientConnInterface) NotifierServiceClient {
return &notifierServiceClient{cc}
}
func (c *notifierServiceClient) SendNotification(ctx context.Context, in *SendNotificationRequest, opts ...grpc.CallOption) (*SendNotificationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SendNotificationResponse)
err := c.cc.Invoke(ctx, NotifierService_SendNotification_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) SendBatchNotifications(ctx context.Context, in *SendBatchNotificationsRequest, opts ...grpc.CallOption) (*SendBatchNotificationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SendBatchNotificationsResponse)
err := c.cc.Invoke(ctx, NotifierService_SendBatchNotifications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) GetNotification(ctx context.Context, in *GetNotificationRequest, opts ...grpc.CallOption) (*GetNotificationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetNotificationResponse)
err := c.cc.Invoke(ctx, NotifierService_GetNotification_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListNotificationsResponse)
err := c.cc.Invoke(ctx, NotifierService_ListNotifications_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) CancelNotification(ctx context.Context, in *CancelNotificationRequest, opts ...grpc.CallOption) (*CancelNotificationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CancelNotificationResponse)
err := c.cc.Invoke(ctx, NotifierService_CancelNotification_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) RetryNotification(ctx context.Context, in *RetryNotificationRequest, opts ...grpc.CallOption) (*RetryNotificationResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RetryNotificationResponse)
err := c.cc.Invoke(ctx, NotifierService_RetryNotification_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetStatsResponse)
err := c.cc.Invoke(ctx, NotifierService_GetStats_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *notifierServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthCheckResponse)
err := c.cc.Invoke(ctx, NotifierService_HealthCheck_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// NotifierServiceServer is the server API for NotifierService service.
// All implementations must embed UnimplementedNotifierServiceServer
// for forward compatibility.
//
// NotifierService handles notification operations
type NotifierServiceServer interface {
// SendNotification sends a single notification
SendNotification(context.Context, *SendNotificationRequest) (*SendNotificationResponse, error)
// SendBatchNotifications sends multiple notifications
SendBatchNotifications(context.Context, *SendBatchNotificationsRequest) (*SendBatchNotificationsResponse, error)
// GetNotification retrieves a notification by ID
GetNotification(context.Context, *GetNotificationRequest) (*GetNotificationResponse, error)
// ListNotifications retrieves notifications matching a filter
ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error)
// CancelNotification cancels a pending notification
CancelNotification(context.Context, *CancelNotificationRequest) (*CancelNotificationResponse, error)
// RetryNotification retries a failed notification
RetryNotification(context.Context, *RetryNotificationRequest) (*RetryNotificationResponse, error)
// GetStats returns notification statistics
GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
// HealthCheck verifies the service is operational
HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
mustEmbedUnimplementedNotifierServiceServer()
}
// UnimplementedNotifierServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedNotifierServiceServer struct{}
func (UnimplementedNotifierServiceServer) SendNotification(context.Context, *SendNotificationRequest) (*SendNotificationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendNotification not implemented")
}
func (UnimplementedNotifierServiceServer) SendBatchNotifications(context.Context, *SendBatchNotificationsRequest) (*SendBatchNotificationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendBatchNotifications not implemented")
}
func (UnimplementedNotifierServiceServer) GetNotification(context.Context, *GetNotificationRequest) (*GetNotificationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetNotification not implemented")
}
func (UnimplementedNotifierServiceServer) ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListNotifications not implemented")
}
func (UnimplementedNotifierServiceServer) CancelNotification(context.Context, *CancelNotificationRequest) (*CancelNotificationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CancelNotification not implemented")
}
func (UnimplementedNotifierServiceServer) RetryNotification(context.Context, *RetryNotificationRequest) (*RetryNotificationResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RetryNotification not implemented")
}
func (UnimplementedNotifierServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
}
func (UnimplementedNotifierServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method HealthCheck not implemented")
}
func (UnimplementedNotifierServiceServer) mustEmbedUnimplementedNotifierServiceServer() {}
func (UnimplementedNotifierServiceServer) testEmbeddedByValue() {}
// UnsafeNotifierServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to NotifierServiceServer will
// result in compilation errors.
type UnsafeNotifierServiceServer interface {
mustEmbedUnimplementedNotifierServiceServer()
}
func RegisterNotifierServiceServer(s grpc.ServiceRegistrar, srv NotifierServiceServer) {
// If the following call pancis, it indicates UnimplementedNotifierServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&NotifierService_ServiceDesc, srv)
}
func _NotifierService_SendNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SendNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).SendNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_SendNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).SendNotification(ctx, req.(*SendNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_SendBatchNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SendBatchNotificationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).SendBatchNotifications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_SendBatchNotifications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).SendBatchNotifications(ctx, req.(*SendBatchNotificationsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_GetNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).GetNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_GetNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).GetNotification(ctx, req.(*GetNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_ListNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListNotificationsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).ListNotifications(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_ListNotifications_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).ListNotifications(ctx, req.(*ListNotificationsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_CancelNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CancelNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).CancelNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_CancelNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).CancelNotification(ctx, req.(*CancelNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_RetryNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RetryNotificationRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).RetryNotification(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_RetryNotification_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).RetryNotification(ctx, req.(*RetryNotificationRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_GetStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetStatsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).GetStats(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_GetStats_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).GetStats(ctx, req.(*GetStatsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _NotifierService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HealthCheckRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(NotifierServiceServer).HealthCheck(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: NotifierService_HealthCheck_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(NotifierServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
}
return interceptor(ctx, in, info, handler)
}
// NotifierService_ServiceDesc is the grpc.ServiceDesc for NotifierService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var NotifierService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "notifier.v1.NotifierService",
HandlerType: (*NotifierServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SendNotification",
Handler: _NotifierService_SendNotification_Handler,
},
{
MethodName: "SendBatchNotifications",
Handler: _NotifierService_SendBatchNotifications_Handler,
},
{
MethodName: "GetNotification",
Handler: _NotifierService_GetNotification_Handler,
},
{
MethodName: "ListNotifications",
Handler: _NotifierService_ListNotifications_Handler,
},
{
MethodName: "CancelNotification",
Handler: _NotifierService_CancelNotification_Handler,
},
{
MethodName: "RetryNotification",
Handler: _NotifierService_RetryNotification_Handler,
},
{
MethodName: "GetStats",
Handler: _NotifierService_GetStats_Handler,
},
{
MethodName: "HealthCheck",
Handler: _NotifierService_HealthCheck_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/grpc/notifier.proto",
}
+239
View File
@@ -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,
})
}
+61
View File
@@ -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)
})
}
+147
View File
@@ -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"`
}