package validation import ( "context" "encoding/json" "fmt" "meteor-compute-service/internal/models" "time" "github.com/google/uuid" ) // Validator interface defines the contract for event validation type Validator interface { Validate(ctx context.Context, rawEvent *models.RawEvent) (*models.ValidationResult, error) } // MVPValidator implements a basic pass-through validation for MVP // This will be replaced with more sophisticated algorithms in Epic 3 type MVPValidator struct { algorithmName string version string } // NewMVPValidator creates a new MVP validator instance func NewMVPValidator() *MVPValidator { return &MVPValidator{ algorithmName: "mvp_pass_through", version: "1.0.0", } } // Validate performs basic validation on a raw event // For MVP, this is a simple pass-through that marks all events as valid func (v *MVPValidator) Validate(ctx context.Context, rawEvent *models.RawEvent) (*models.ValidationResult, error) { // Basic validation details that will be stored details := ValidationDetails{ Algorithm: v.algorithmName, Version: v.version, ValidationSteps: []ValidationStep{}, Metadata: make(map[string]interface{}), } // Step 1: Basic data completeness check step1 := v.validateDataCompleteness(rawEvent) details.ValidationSteps = append(details.ValidationSteps, step1) // Step 2: Event type validation step2 := v.validateEventType(rawEvent) details.ValidationSteps = append(details.ValidationSteps, step2) // Step 3: File validation step3 := v.validateFile(rawEvent) details.ValidationSteps = append(details.ValidationSteps, step3) // Step 4: Metadata validation step4 := v.validateMetadata(rawEvent) details.ValidationSteps = append(details.ValidationSteps, step4) // For MVP, calculate a simple score based on completed validation steps totalSteps := len(details.ValidationSteps) passedSteps := 0 for _, step := range details.ValidationSteps { if step.Passed { passedSteps++ } } score := float64(passedSteps) / float64(totalSteps) isValid := score >= 0.8 // 80% threshold for MVP // Add summary to metadata details.Metadata["total_steps"] = totalSteps details.Metadata["passed_steps"] = passedSteps details.Metadata["score"] = score details.Metadata["threshold"] = 0.8 // Serialize details to JSON detailsJSON, err := json.Marshal(details) if err != nil { return nil, fmt.Errorf("failed to marshal validation details: %w", err) } return &models.ValidationResult{ IsValid: isValid, Score: score, Algorithm: v.algorithmName, Details: detailsJSON, ProcessedAt: time.Now().UTC(), Reason: v.generateReason(isValid, passedSteps, totalSteps), }, nil } // ValidationDetails represents the detailed validation information type ValidationDetails struct { Algorithm string `json:"algorithm"` Version string `json:"version"` ValidationSteps []ValidationStep `json:"validation_steps"` Metadata map[string]interface{} `json:"metadata"` } // ValidationStep represents a single validation step type ValidationStep struct { Name string `json:"name"` Description string `json:"description"` Passed bool `json:"passed"` Details map[string]interface{} `json:"details,omitempty"` Error string `json:"error,omitempty"` } // validateDataCompleteness checks if required fields are present func (v *MVPValidator) validateDataCompleteness(rawEvent *models.RawEvent) ValidationStep { step := ValidationStep{ Name: "data_completeness", Description: "Checks if required fields are present and valid", Details: make(map[string]interface{}), } issues := []string{} // Check required UUID fields if rawEvent.ID == (uuid.UUID{}) { issues = append(issues, "missing_id") } if rawEvent.DeviceID == (uuid.UUID{}) { issues = append(issues, "missing_device_id") } if rawEvent.UserProfileID == (uuid.UUID{}) { issues = append(issues, "missing_user_profile_id") } // Check required string fields if rawEvent.FilePath == "" { issues = append(issues, "missing_file_path") } if rawEvent.EventType == "" { issues = append(issues, "missing_event_type") } // Check timestamp if rawEvent.EventTimestamp.IsZero() { issues = append(issues, "missing_event_timestamp") } step.Details["issues"] = issues step.Details["issues_count"] = len(issues) step.Passed = len(issues) == 0 if len(issues) > 0 { step.Error = fmt.Sprintf("Found %d data completeness issues", len(issues)) } return step } // validateEventType checks if the event type is supported func (v *MVPValidator) validateEventType(rawEvent *models.RawEvent) ValidationStep { step := ValidationStep{ Name: "event_type_validation", Description: "Validates that the event type is supported", Details: make(map[string]interface{}), } supportedTypes := []string{ models.EventTypeMotion, models.EventTypeAlert, models.EventTypeMeteor, } step.Details["event_type"] = rawEvent.EventType step.Details["supported_types"] = supportedTypes // Check if event type is supported isSupported := false for _, supportedType := range supportedTypes { if rawEvent.EventType == supportedType { isSupported = true break } } step.Passed = isSupported step.Details["is_supported"] = isSupported if !isSupported { step.Error = fmt.Sprintf("Unsupported event type: %s", rawEvent.EventType) } return step } // validateFile checks basic file information func (v *MVPValidator) validateFile(rawEvent *models.RawEvent) ValidationStep { step := ValidationStep{ Name: "file_validation", Description: "Validates file information and properties", Details: make(map[string]interface{}), } issues := []string{} // Check file path format (basic validation) if len(rawEvent.FilePath) < 3 { issues = append(issues, "file_path_too_short") } // Check file size if provided if rawEvent.FileSize != nil { step.Details["file_size"] = *rawEvent.FileSize if *rawEvent.FileSize <= 0 { issues = append(issues, "invalid_file_size") } // Check for reasonable file size limits (e.g., not more than 100MB for video files) if *rawEvent.FileSize > 100*1024*1024 { issues = append(issues, "file_size_too_large") } } // Check file type if provided if rawEvent.FileType != nil { step.Details["file_type"] = *rawEvent.FileType // Basic MIME type validation for common formats supportedMimeTypes := []string{ "video/mp4", "video/quicktime", "video/x-msvideo", "image/jpeg", "image/png", "application/gzip", "application/x-tar", } isSupportedMime := false for _, mimeType := range supportedMimeTypes { if *rawEvent.FileType == mimeType { isSupportedMime = true break } } if !isSupportedMime { issues = append(issues, "unsupported_file_type") } step.Details["supported_mime_types"] = supportedMimeTypes } step.Details["issues"] = issues step.Details["issues_count"] = len(issues) step.Passed = len(issues) == 0 if len(issues) > 0 { step.Error = fmt.Sprintf("Found %d file validation issues", len(issues)) } return step } // validateMetadata performs basic metadata validation func (v *MVPValidator) validateMetadata(rawEvent *models.RawEvent) ValidationStep { step := ValidationStep{ Name: "metadata_validation", Description: "Validates event metadata structure and content", Details: make(map[string]interface{}), } issues := []string{} // Check if metadata is valid JSON if rawEvent.Metadata != nil { var metadata map[string]interface{} if err := json.Unmarshal(rawEvent.Metadata, &metadata); err != nil { issues = append(issues, "invalid_json_metadata") step.Details["json_error"] = err.Error() } else { step.Details["metadata_keys"] = getKeys(metadata) step.Details["metadata_size"] = len(rawEvent.Metadata) // Check for reasonable metadata size (not more than 10KB) if len(rawEvent.Metadata) > 10*1024 { issues = append(issues, "metadata_too_large") } } } else { // Metadata is optional, so this is not an error step.Details["metadata_present"] = false } step.Details["issues"] = issues step.Details["issues_count"] = len(issues) step.Passed = len(issues) == 0 if len(issues) > 0 { step.Error = fmt.Sprintf("Found %d metadata validation issues", len(issues)) } return step } // generateReason creates a human-readable reason for the validation result func (v *MVPValidator) generateReason(isValid bool, passedSteps, totalSteps int) string { if isValid { return fmt.Sprintf("Event passed validation with %d/%d steps completed successfully", passedSteps, totalSteps) } return fmt.Sprintf("Event failed validation with only %d/%d steps completed successfully (required: 80%%)", passedSteps, totalSteps) } // getKeys extracts keys from a map func getKeys(m map[string]interface{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys }