// Package validate provides WebSocket handlers for validation-related operations package validate import ( "errors" "net/http" "time" "github.com/gorilla/websocket" "github.com/jfraeys/fetch_ml/internal/api/helpers" "github.com/jfraeys/fetch_ml/internal/auth" "github.com/jfraeys/fetch_ml/internal/experiment" "github.com/jfraeys/fetch_ml/internal/logging" ) // Handler provides validation-related WebSocket handlers type Handler struct { expManager *experiment.Manager logger *logging.Logger authConfig *auth.Config } // NewHandler creates a new validate handler func NewHandler( expManager *experiment.Manager, logger *logging.Logger, authConfig *auth.Config, ) *Handler { return &Handler{ expManager: expManager, logger: logger, authConfig: authConfig, } } // Error codes const ( ErrorCodeUnknownError = 0x00 ErrorCodeInvalidRequest = 0x01 ErrorCodeAuthenticationFailed = 0x02 ErrorCodePermissionDenied = 0x03 ErrorCodeResourceNotFound = 0x04 ErrorCodeValidationFailed = 0x40 ) // Permissions const ( PermJobsRead = "jobs:read" ) // sendErrorPacket sends an error response packet to the client func (h *Handler) sendErrorPacket(conn *websocket.Conn, code byte, message, details string) error { err := map[string]interface{}{ "error": true, "code": code, "message": message, "details": details, } return conn.WriteJSON(err) } // sendSuccessPacket sends a success response packet func (h *Handler) sendSuccessPacket(conn *websocket.Conn, data map[string]interface{}) error { return conn.WriteJSON(data) } // ValidateRequest represents a validation request // Protocol: [api_key_hash:16][validate_id_len:1][validate_id:var][commit_id:20] type ValidateRequest struct { ValidateID string CommitID string } // ParseValidateRequest parses a validation request from the payload func ParseValidateRequest(payload []byte) (*ValidateRequest, error) { if len(payload) < 16+1+20 { return nil, errors.New("validate request payload too short") } offset := 16 validateIDLen := int(payload[offset]) offset += 1 if validateIDLen <= 0 || len(payload) < offset+validateIDLen+20 { return nil, errors.New("invalid validate id length") } validateID := string(payload[offset : offset+validateIDLen]) offset += validateIDLen commitID := string(payload[offset : offset+20]) return &ValidateRequest{ ValidateID: validateID, CommitID: commitID, }, nil } // HandleValidate handles the validate WebSocket operation func (h *Handler) HandleValidate(conn *websocket.Conn, payload []byte, user *auth.User) error { req, err := ParseValidateRequest(payload) if err != nil { return h.sendErrorPacket(conn, ErrorCodeInvalidRequest, "invalid validate request", err.Error()) } h.logger.Info("validation requested", "validate_id", req.ValidateID, "commit_id", req.CommitID, "user", user.Name) // Validate commit ID format if ok, errMsg := helpers.ValidateCommitIDFormat(req.CommitID); !ok { return h.sendErrorPacket(conn, ErrorCodeInvalidRequest, "invalid commit_id format", errMsg) } // Validate experiment manifest if ok, details := helpers.ValidateExperimentManifest(h.expManager, req.CommitID); !ok { return h.sendErrorPacket(conn, ErrorCodeResourceNotFound, "experiment manifest validation failed", details) } // Create validation report report := helpers.NewValidateReport() report.CommitID = req.CommitID report.TS = time.Now().UTC().Format(time.RFC3339) // Add basic checks report.Checks["commit_id_format"] = helpers.ValidateCheck{OK: true, Expected: "40 hex chars", Actual: req.CommitID} report.Checks["manifest_exists"] = helpers.ValidateCheck{OK: true, Expected: "present", Actual: "found"} return h.sendSuccessPacket(conn, map[string]interface{}{ "success": true, "validate_id": req.ValidateID, "commit_id": req.CommitID, "report": report, "timestamp": time.Now().UTC(), }) } // HandleGetValidateStatus handles getting the status of a validation func (h *Handler) HandleGetValidateStatus(conn *websocket.Conn, validateID string, user *auth.User) error { h.logger.Info("getting validation status", "validate_id", validateID, "user", user.Name) // Stub implementation - in production, would query validation status from database return h.sendSuccessPacket(conn, map[string]interface{}{ "success": true, "validate_id": validateID, "status": "completed", "timestamp": time.Now().UTC(), }) } // HandleListValidations handles listing all validations for a commit func (h *Handler) HandleListValidations(conn *websocket.Conn, commitID string, user *auth.User) error { h.logger.Info("listing validations", "commit_id", commitID, "user", user.Name) // Stub implementation - in production, would query validations from database return h.sendSuccessPacket(conn, map[string]interface{}{ "success": true, "commit_id": commitID, "validations": []map[string]interface{}{ { "validate_id": "val-001", "status": "completed", "timestamp": time.Now().UTC(), }, }, "count": 1, }) } // HTTP Handlers for REST API // ValidateHTTP handles HTTP requests for validation func (h *Handler) ValidateHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not implemented", http.StatusNotImplemented) } // GetValidationStatusHTTP handles HTTP requests for validation status func (h *Handler) GetValidationStatusHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Not implemented", http.StatusNotImplemented) }