diff --git a/internal/domain/order.go b/internal/domain/order.go new file mode 100644 index 0000000..23fc535 --- /dev/null +++ b/internal/domain/order.go @@ -0,0 +1,32 @@ +package domain + +import ( + "errors" + "fmt" + "strings" +) + +var ( + ErrOrderAlreadyExist = errors.New("order already exist") + ErrOrderNotFound = errors.New("order not found") + ErrInvalidOrderData = errors.New("invalid order data") +) + +type Order struct { + ID string + Item string + Quantity int32 +} + +func (o *Order) Validate() error { + if strings.TrimSpace(o.Item) == "" { + return fmt.Errorf("%w: item cannot be empty", ErrInvalidOrderData) + } + if o.Quantity <= 0 { + return fmt.Errorf("%w: quantity must be positive", ErrInvalidOrderData) + } + if o.ID == "" { + return fmt.Errorf("%w: ID cannot be empty", ErrInvalidOrderData) + } + return nil +} diff --git a/internal/handler/grpc/errors.go b/internal/handler/grpc/errors.go new file mode 100644 index 0000000..aa7ae1e --- /dev/null +++ b/internal/handler/grpc/errors.go @@ -0,0 +1,24 @@ +package handler + +import ( + "errors" + + "orderservice/internal/domain" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func mapError(err error) error { + if errors.Is(err, domain.ErrOrderNotFound) { + return status.Error(codes.NotFound, err.Error()) + } + if errors.Is(err, domain.ErrOrderAlreadyExist) { + return status.Error(codes.AlreadyExists, err.Error()) + } + if errors.Is(err, domain.ErrInvalidOrderData) { + return status.Error(codes.InvalidArgument, err.Error()) + } + + return status.Error(codes.Internal, "internal server error") +} diff --git a/internal/handler/grpc/order.go b/internal/handler/grpc/order.go new file mode 100644 index 0000000..53e3fb0 --- /dev/null +++ b/internal/handler/grpc/order.go @@ -0,0 +1,91 @@ +package handler + +import ( + "context" + + "orderservice/internal/domain" + "orderservice/internal/service" + pb "orderservice/pkg/api/order" +) + +type OrderHandler struct { + pb.UnimplementedOrderServiceServer + + service *service.OrderService +} + +func NewOrderHandler(service *service.OrderService) *OrderHandler { + return &OrderHandler{ + service: service, + } +} + +func mapDomainStructToHandler(order *domain.Order) *pb.Order { + return &pb.Order{ + Id: order.ID, + Item: order.Item, + Quantity: order.Quantity, + } +} + +func (h *OrderHandler) CreateOrder( + ctx context.Context, + req *pb.CreateOrderRequest, +) (*pb.CreateOrderResponse, error) { + order, err := h.service.Create(ctx, req.GetItem(), req.GetQuantity()) + if err != nil { + return nil, mapError(err) + } + + return &pb.CreateOrderResponse{Id: order.ID}, nil +} + +func (h *OrderHandler) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) { + order, err := h.service.Get(ctx, req.GetId()) + if err != nil { + return nil, mapError(err) + } + + return &pb.GetOrderResponse{Order: mapDomainStructToHandler(order)}, nil +} + +func (h *OrderHandler) UpdateOrder( + ctx context.Context, + req *pb.UpdateOrderRequest, +) (*pb.UpdateOrderResponse, error) { + order, err := h.service.Update(ctx, req.GetId(), req.GetItem(), req.GetQuantity()) + if err != nil { + return nil, mapError(err) + } + + return &pb.UpdateOrderResponse{Order: mapDomainStructToHandler(order)}, nil +} + +func (h *OrderHandler) DeleteOrder( + ctx context.Context, + req *pb.DeleteOrderRequest, +) (*pb.DeleteOrderResponse, error) { + err := h.service.Delete(ctx, req.GetId()) + if err != nil { + return nil, mapError(err) + } + + return &pb.DeleteOrderResponse{Success: true}, nil +} + +func (h *OrderHandler) ListOrders( + ctx context.Context, + _ *pb.ListOrdersRequest, +) (*pb.ListOrdersResponse, error) { + domainOrders, err := h.service.List(ctx) + if err != nil { + return nil, mapError(err) + } + + orders := make([]*pb.Order, 0, len(domainOrders)) + for _, o := range domainOrders { + orders = append(orders, mapDomainStructToHandler(o)) + } + + return &pb.ListOrdersResponse{Orders: orders}, nil +} diff --git a/internal/repository/inmemory/order.go b/internal/repository/inmemory/order.go new file mode 100644 index 0000000..f3da9e2 --- /dev/null +++ b/internal/repository/inmemory/order.go @@ -0,0 +1,100 @@ +package inmemory + +import ( + "context" + "sync" + + "orderservice/internal/domain" +) + +type OrderRepository struct { + mu sync.RWMutex + orders map[string]*domain.Order +} + +func NewOrderRepository() *OrderRepository { + return &OrderRepository{ + orders: make(map[string]*domain.Order), + } +} + +func (r *OrderRepository) Create(ctx context.Context, order *domain.Order) error { + if err := ctx.Err(); err != nil { + return err + } + + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.orders[order.ID]; ok { + return domain.ErrOrderAlreadyExist + } + r.orders[order.ID] = order + + return nil +} + +func (r *OrderRepository) Get(ctx context.Context, id string) (*domain.Order, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + r.mu.RLock() + defer r.mu.RUnlock() + + order, ok := r.orders[id] + if !ok { + return nil, domain.ErrOrderNotFound + } + + return order, nil +} + +func (r *OrderRepository) Update(ctx context.Context, order *domain.Order) error { + if err := ctx.Err(); err != nil { + return err + } + + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.orders[order.ID]; !ok { + return domain.ErrOrderNotFound + } + r.orders[order.ID] = order + + return nil +} + +func (r *OrderRepository) Delete(ctx context.Context, id string) error { + if err := ctx.Err(); err != nil { + return err + } + + r.mu.Lock() + defer r.mu.Unlock() + + if _, ok := r.orders[id]; !ok { + return domain.ErrOrderNotFound + } + + delete(r.orders, id) + + return nil +} + +func (r *OrderRepository) List(ctx context.Context) ([]*domain.Order, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + + r.mu.RLock() + defer r.mu.RUnlock() + + orders := make([]*domain.Order, 0, len(r.orders)) + for _, order := range r.orders { + orders = append(orders, order) + } + + return orders, nil +} diff --git a/internal/repository/order.go b/internal/repository/order.go new file mode 100644 index 0000000..48f0459 --- /dev/null +++ b/internal/repository/order.go @@ -0,0 +1,15 @@ +package repository + +import ( + "context" + + "orderservice/internal/domain" +) + +type OrderRepository interface { + Create(ctx context.Context, order *domain.Order) error + Get(ctx context.Context, id string) (*domain.Order, error) + Update(ctx context.Context, order *domain.Order) error + Delete(ctx context.Context, id string) error + List(ctx context.Context) ([]*domain.Order, error) +} diff --git a/internal/server/server.go b/internal/server/server.go index 1a57181..904597b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,6 +7,9 @@ import ( "orderservice/internal/config" "orderservice/internal/interceptor" + + orderGrpcHandler "orderservice/internal/handler/grpc" + orderInMemory "orderservice/internal/repository/inmemory" "orderservice/internal/service" pb "orderservice/pkg/api/order" @@ -35,8 +38,11 @@ func New(cfg *config.Config) *Server { } func (s *Server) RegisterServices() { - orderService := service.NewOrderServiceServer() - pb.RegisterOrderServiceServer(s.grpcServer, orderService) + repo := orderInMemory.NewOrderRepository() + orderService := service.NewOrderService(repo) + orderHandler := orderGrpcHandler.NewOrderHandler(orderService) + + pb.RegisterOrderServiceServer(s.grpcServer, orderHandler) if s.config.GRPCEnableReflection { reflection.Register(s.grpcServer) diff --git a/internal/service/order.go b/internal/service/order.go new file mode 100644 index 0000000..10dede4 --- /dev/null +++ b/internal/service/order.go @@ -0,0 +1,70 @@ +package service + +import ( + "context" + + "orderservice/internal/domain" + "orderservice/internal/repository" + + "github.com/google/uuid" +) + +type OrderService struct { + repo repository.OrderRepository +} + +func NewOrderService(repo repository.OrderRepository) *OrderService { + return &OrderService{ + repo: repo, + } +} + +func (s *OrderService) Create(ctx context.Context, item string, quantity int32) (*domain.Order, error) { + order := &domain.Order{ + ID: uuid.NewString(), + Item: item, + Quantity: quantity, + } + + err := order.Validate() + if err != nil { + return nil, err + } + + if err := s.repo.Create(ctx, order); err != nil { + return nil, err + } + return order, nil +} + +func (s *OrderService) Get(ctx context.Context, id string) (*domain.Order, error) { + return s.repo.Get(ctx, id) +} + +func (s *OrderService) Update(ctx context.Context, id string, item string, quantity int32) (*domain.Order, error) { + order, err := s.repo.Get(ctx, id) + if err != nil { + return nil, err + } + + order.Item = item + order.Quantity = quantity + + err = order.Validate() + if err != nil { + return nil, err + } + + if err := s.repo.Update(ctx, order); err != nil { + return nil, err + } + return order, nil +} + +func (s *OrderService) Delete(ctx context.Context, id string) error { + return s.repo.Delete(ctx, id) +} + +func (s *OrderService) List(ctx context.Context) ([]*domain.Order, error) { + return s.repo.List(ctx) +} diff --git a/internal/service/order_service.go b/internal/service/order_service.go deleted file mode 100644 index 37d49eb..0000000 --- a/internal/service/order_service.go +++ /dev/null @@ -1,110 +0,0 @@ -package service - -import ( - "context" - "errors" - "sync" - - pb "orderservice/pkg/api/order" - - "github.com/google/uuid" -) - -var ErrOrderNotFound = errors.New("order not found") - -func generateOrderID() string { - return uuid.NewString() -} - -type OrderServiceServer struct { - pb.UnimplementedOrderServiceServer - - mu sync.RWMutex - orders map[string]*pb.Order -} - -func NewOrderServiceServer() *OrderServiceServer { - return &OrderServiceServer{ - orders: make(map[string]*pb.Order), - } -} - -func (s *OrderServiceServer) CreateOrder( - _ context.Context, - req *pb.CreateOrderRequest, -) (*pb.CreateOrderResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - id := generateOrderID() - order := &pb.Order{ - Id: id, - Item: req.GetItem(), - Quantity: req.GetQuantity(), - } - s.orders[id] = order - - return &pb.CreateOrderResponse{Id: id}, nil -} - -func (s *OrderServiceServer) GetOrder(_ context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - order, ok := s.orders[req.GetId()] - if !ok { - return nil, ErrOrderNotFound - } - - return &pb.GetOrderResponse{Order: order}, nil -} - -func (s *OrderServiceServer) UpdateOrder( - _ context.Context, - req *pb.UpdateOrderRequest, -) (*pb.UpdateOrderResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - order, ok := s.orders[req.GetId()] - if !ok { - return nil, ErrOrderNotFound - } - - order.Item = req.GetItem() - order.Quantity = req.GetQuantity() - - return &pb.UpdateOrderResponse{Order: order}, nil -} - -func (s *OrderServiceServer) DeleteOrder( - _ context.Context, - req *pb.DeleteOrderRequest, -) (*pb.DeleteOrderResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - - _, ok := s.orders[req.GetId()] - if !ok { - return nil, ErrOrderNotFound - } - - delete(s.orders, req.GetId()) - - return &pb.DeleteOrderResponse{Success: true}, nil -} - -func (s *OrderServiceServer) ListOrders( - _ context.Context, - _ *pb.ListOrdersRequest, -) (*pb.ListOrdersResponse, error) { - s.mu.RLock() - defer s.mu.RUnlock() - - orders := make([]*pb.Order, 0, len(s.orders)) - for _, o := range s.orders { - orders = append(orders, o) - } - - return &pb.ListOrdersResponse{Orders: orders}, nil -}