// Package integration provides CI helper functions for BACKBEAT contract testing package integration import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strings" ) // CIHelper provides utilities for continuous integration testing type CIHelper struct { validator *MessageValidator } // NewCIHelper creates a new CI helper with a message validator func NewCIHelper(schemaDir string) (*CIHelper, error) { validator, err := NewMessageValidator(schemaDir) if err != nil { return nil, fmt.Errorf("failed to create validator: %w", err) } return &CIHelper{ validator: validator, }, nil } // ValidateDirectory validates all JSON files in a directory against BACKBEAT schemas func (ci *CIHelper) ValidateDirectory(dir string) (*DirectoryValidationResult, error) { result := &DirectoryValidationResult{ Directory: dir, Files: make(map[string]*FileValidationResult), } err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } // Skip non-JSON files if d.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".json") { return nil } fileResult, validateErr := ci.validateFile(path) if validateErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("Failed to validate %s: %v", path, validateErr)) } else { relPath, _ := filepath.Rel(dir, path) result.Files[relPath] = fileResult result.TotalFiles++ if fileResult.AllValid { result.ValidFiles++ } else { result.InvalidFiles++ } } return nil }) if err != nil { return nil, fmt.Errorf("failed to walk directory: %w", err) } result.ValidationRate = float64(result.ValidFiles) / float64(result.TotalFiles) return result, nil } // validateFile validates a single JSON file func (ci *CIHelper) validateFile(filePath string) (*FileValidationResult, error) { data, err := os.ReadFile(filePath) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } result := &FileValidationResult{ FilePath: filePath, AllValid: true, } // Try to parse as single message first var singleMessage map[string]interface{} if err := json.Unmarshal(data, &singleMessage); err == nil { if msgType, hasType := singleMessage["type"].(string); hasType && ci.validator.IsMessageTypeSupported(msgType) { // Single BACKBEAT message validationResult, validateErr := ci.validator.ValidateMessage(data) if validateErr != nil { return nil, validateErr } result.Messages = []*ValidationResult{validationResult} result.AllValid = validationResult.Valid return result, nil } } // Try to parse as array of messages var messageArray []map[string]interface{} if err := json.Unmarshal(data, &messageArray); err == nil { for i, msg := range messageArray { msgBytes, marshalErr := json.Marshal(msg) if marshalErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("Message %d: failed to marshal: %v", i, marshalErr)) result.AllValid = false continue } validationResult, validateErr := ci.validator.ValidateMessage(msgBytes) if validateErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("Message %d: validation error: %v", i, validateErr)) result.AllValid = false continue } result.Messages = append(result.Messages, validationResult) if !validationResult.Valid { result.AllValid = false } } return result, nil } // Try to parse as examples format (array with description and message fields) var examples []ExampleMessage if err := json.Unmarshal(data, &examples); err == nil { for i, example := range examples { msgBytes, marshalErr := json.Marshal(example.Message) if marshalErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("Example %d (%s): failed to marshal: %v", i, example.Description, marshalErr)) result.AllValid = false continue } validationResult, validateErr := ci.validator.ValidateMessage(msgBytes) if validateErr != nil { result.Errors = append(result.Errors, fmt.Sprintf("Example %d (%s): validation error: %v", i, example.Description, validateErr)) result.AllValid = false continue } result.Messages = append(result.Messages, validationResult) if !validationResult.Valid { result.AllValid = false } } return result, nil } return nil, fmt.Errorf("file does not contain valid JSON message format") } // ExampleMessage represents a message example with description type ExampleMessage struct { Description string `json:"description"` Message map[string]interface{} `json:"message"` } // DirectoryValidationResult contains results for validating a directory type DirectoryValidationResult struct { Directory string `json:"directory"` TotalFiles int `json:"total_files"` ValidFiles int `json:"valid_files"` InvalidFiles int `json:"invalid_files"` ValidationRate float64 `json:"validation_rate"` Files map[string]*FileValidationResult `json:"files"` Errors []string `json:"errors,omitempty"` } // FileValidationResult contains results for validating a single file type FileValidationResult struct { FilePath string `json:"file_path"` AllValid bool `json:"all_valid"` Messages []*ValidationResult `json:"messages"` Errors []string `json:"errors,omitempty"` } // GenerateCIReport generates a formatted report suitable for CI systems func (ci *CIHelper) GenerateCIReport(result *DirectoryValidationResult) string { var sb strings.Builder sb.WriteString("BACKBEAT Contract Validation Report\n") sb.WriteString("===================================\n\n") sb.WriteString(fmt.Sprintf("Directory: %s\n", result.Directory)) sb.WriteString(fmt.Sprintf("Total Files: %d\n", result.TotalFiles)) sb.WriteString(fmt.Sprintf("Valid Files: %d\n", result.ValidFiles)) sb.WriteString(fmt.Sprintf("Invalid Files: %d\n", result.InvalidFiles)) sb.WriteString(fmt.Sprintf("Validation Rate: %.2f%%\n\n", result.ValidationRate*100)) if len(result.Errors) > 0 { sb.WriteString("Directory-level Errors:\n") for _, err := range result.Errors { sb.WriteString(fmt.Sprintf(" - %s\n", err)) } sb.WriteString("\n") } // Group files by validation status validFiles := make([]string, 0) invalidFiles := make([]string, 0) for filePath, fileResult := range result.Files { if fileResult.AllValid { validFiles = append(validFiles, filePath) } else { invalidFiles = append(invalidFiles, filePath) } } if len(validFiles) > 0 { sb.WriteString("Valid Files:\n") for _, file := range validFiles { sb.WriteString(fmt.Sprintf(" ✓ %s\n", file)) } sb.WriteString("\n") } if len(invalidFiles) > 0 { sb.WriteString("Invalid Files:\n") for _, file := range invalidFiles { fileResult := result.Files[file] sb.WriteString(fmt.Sprintf(" ✗ %s\n", file)) for _, err := range fileResult.Errors { sb.WriteString(fmt.Sprintf(" - %s\n", err)) } for i, msg := range fileResult.Messages { if !msg.Valid { sb.WriteString(fmt.Sprintf(" Message %d (%s):\n", i+1, msg.MessageType)) for _, valErr := range msg.Errors { sb.WriteString(fmt.Sprintf(" - %s: %s\n", valErr.Field, valErr.Message)) } } } sb.WriteString("\n") } } return sb.String() } // ExitWithStatus exits the program with appropriate status code for CI func (ci *CIHelper) ExitWithStatus(result *DirectoryValidationResult) { if result.InvalidFiles > 0 || len(result.Errors) > 0 { fmt.Fprint(os.Stderr, ci.GenerateCIReport(result)) os.Exit(1) } else { fmt.Print(ci.GenerateCIReport(result)) os.Exit(0) } } // ValidateExamples validates the built-in example messages func (ci *CIHelper) ValidateExamples() ([]*ValidationResult, error) { examples := ExampleMessages() results := make([]*ValidationResult, 0, len(examples)) for name, example := range examples { result, err := ci.validator.ValidateStruct(example) if err != nil { return nil, fmt.Errorf("failed to validate example %s: %w", name, err) } results = append(results, result) } return results, nil } // GetSchemaInfo returns information about loaded schemas func (ci *CIHelper) GetSchemaInfo() map[string]string { info := make(map[string]string) for _, msgType := range ci.validator.GetSupportedMessageTypes() { info[msgType] = getSchemaVersion(msgType) } return info }