# Port Rules (PORT-01..05) ## PORT-01: Handler Struct Holds Application (WARNING) HTTP and gRPC handler structs MUST hold `app.Application` and delegate to it. They are thin wrappers. ```go // ports/http.go type HttpServer struct { app app.Application } // ports/grpc.go type GrpcServer struct { app app.Application } ``` --- ## PORT-02: Error Mapping (WARNING) Ports MUST map application errors to protocol-specific responses. They must NOT leak internal error details. **HTTP — using httperr helper:** ```go func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { err = h.app.Commands.MakeHoursAvailable.Handle(r.Context(), command.MakeHoursAvailable{...}) if err != nil { httperr.RespondWithSlugError(err, w, r) return } w.WriteHeader(http.StatusNoContent) } ``` **The httperr mapper:** ```go func RespondWithSlugError(err error, w http.ResponseWriter, r *http.Request) { slugError, ok := err.(errors.SlugError) if !ok { InternalError("internal-server-error", err, w, r) return } switch slugError.ErrorType() { case errors.ErrorTypeAuthorization: Unauthorised(slugError.Slug(), slugError, w, r) // 401 case errors.ErrorTypeIncorrectInput: BadRequest(slugError.Slug(), slugError, w, r) // 400 default: InternalError(slugError.Slug(), slugError, w, r) // 500 } } ``` **gRPC — using status codes:** ```go func (g GrpcServer) ScheduleTraining(ctx context.Context, req *trainer.UpdateHourRequest) (*empty.Empty, error) { if err := g.app.Commands.ScheduleTraining.Handle(ctx, command.ScheduleTraining{...}); err != nil { return nil, status.Error(codes.Internal, err.Error()) } return &empty.Empty{}, nil } ``` --- ## PORT-03: Auth Extracted from Context (WARNING) Authentication/authorization data MUST be extracted from the request context using a shared auth package, NOT parsed directly in the handler. **Correct:** ```go func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { user, err := auth.UserFromCtx(r.Context()) if err != nil { httperr.RespondWithSlugError(err, w, r) return } if user.Role != "trainer" { httperr.Unauthorised("invalid-role", nil, w, r) return } // ... delegate to app } ``` **Wrong:** ```go func (h HttpServer) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") // VIOLATION: parsing auth in handler claims, err := jwt.Parse(token, keyFunc) // VIOLATION: JWT logic in port // ... } ``` --- ## PORT-04: No Business Logic in Ports (CRITICAL) Port handlers MUST only: 1. Parse/decode the request 2. Extract auth from context 3. Construct command/query struct 4. Call `app.Commands.X.Handle()` or `app.Queries.X.Handle()` 5. Map the result/error to a response They MUST NOT contain: - Domain validation logic - Business rule checks - Direct database calls - State manipulation **Check:** Port files should only import `app/`, `app/command/`, `app/query/`, and infrastructure packages (HTTP, gRPC, auth). They should NOT import `domain/` directly (except for response mapping types). --- ## PORT-05: Response Model Mapping (INFO) Response transformation SHOULD be in separate mapping functions, not inline in handlers. ```go // Mapping function func dateModelsToResponse(models []query.Date) []Date { var dates []Date for _, m := range models { dates = append(dates, Date{ Date: m.Date, Hours: hourModelsToResponse(m.Hours), }) } return dates } // Handler uses it cleanly func (h HttpServer) GetTrainerAvailableHours(w http.ResponseWriter, r *http.Request, params GetTrainerAvailableHoursParams) { dateModels, err := h.app.Queries.TrainerAvailableHours.Handle(r.Context(), query.AvailableHours{ From: params.DateFrom, To: params.DateTo, }) if err != nil { httperr.RespondWithSlugError(err, w, r) return } dates := dateModelsToResponse(dateModels) render.Respond(w, r, dates) } ```