package cv import ( "fmt" "net/mail" "net/url" "strings" ) // ValidationError represents a validation error with context type ValidationError struct { Field string Message string } func (e ValidationError) Error() string { return fmt.Sprintf("%s: %s", e.Field, e.Message) } // ValidationErrors is a collection of validation errors type ValidationErrors []ValidationError func (e ValidationErrors) Error() string { if len(e) == 0 { return "" } var sb strings.Builder sb.WriteString("validation failed:\n") for _, err := range e { sb.WriteString(" - ") sb.WriteString(err.Error()) sb.WriteString("\n") } return sb.String() } // Validate performs comprehensive validation on the CV func (cv *CV) Validate() error { var errors ValidationErrors // Validate Personal section if err := cv.Personal.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { errors = append(errors, verrs...) } else { errors = append(errors, ValidationError{Field: "personal", Message: err.Error()}) } } // Validate Experience entries for i, exp := range cv.Experience { if err := exp.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { for _, verr := range verrs { errors = append(errors, ValidationError{ Field: fmt.Sprintf("experience[%d].%s", i, verr.Field), Message: verr.Message, }) } } else { errors = append(errors, ValidationError{ Field: fmt.Sprintf("experience[%d]", i), Message: err.Error(), }) } } } // Validate Education entries for i, edu := range cv.Education { if err := edu.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { for _, verr := range verrs { errors = append(errors, ValidationError{ Field: fmt.Sprintf("education[%d].%s", i, verr.Field), Message: verr.Message, }) } } else { errors = append(errors, ValidationError{ Field: fmt.Sprintf("education[%d]", i), Message: err.Error(), }) } } } // Validate Skills if err := cv.Skills.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { errors = append(errors, verrs...) } else { errors = append(errors, ValidationError{Field: "skills", Message: err.Error()}) } } // Validate Languages for i, lang := range cv.Languages { if err := lang.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { for _, verr := range verrs { errors = append(errors, ValidationError{ Field: fmt.Sprintf("languages[%d].%s", i, verr.Field), Message: verr.Message, }) } } else { errors = append(errors, ValidationError{ Field: fmt.Sprintf("languages[%d]", i), Message: err.Error(), }) } } } // Validate Projects for i, proj := range cv.Projects { if err := proj.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { for _, verr := range verrs { errors = append(errors, ValidationError{ Field: fmt.Sprintf("projects[%d].%s", i, verr.Field), Message: verr.Message, }) } } else { errors = append(errors, ValidationError{ Field: fmt.Sprintf("projects[%d]", i), Message: err.Error(), }) } } } // Validate Meta (essential CV metadata) if err := cv.Meta.Validate(); err != nil { if verrs, ok := err.(ValidationErrors); ok { errors = append(errors, verrs...) } else { errors = append(errors, ValidationError{Field: "meta", Message: err.Error()}) } } if len(errors) > 0 { return errors } return nil } // Validate checks Personal information func (p *Personal) Validate() error { var errors ValidationErrors if strings.TrimSpace(p.Name) == "" { errors = append(errors, ValidationError{Field: "name", Message: "name is required"}) } if strings.TrimSpace(p.Email) == "" { errors = append(errors, ValidationError{Field: "email", Message: "email is required"}) } else if !isValidEmail(p.Email) { errors = append(errors, ValidationError{Field: "email", Message: "invalid email format"}) } // Validate URLs if present if p.LinkedIn != "" && !isValidURL(p.LinkedIn) { errors = append(errors, ValidationError{Field: "linkedin", Message: "invalid URL format"}) } if p.GitHub != "" && !isValidURL(p.GitHub) { errors = append(errors, ValidationError{Field: "github", Message: "invalid URL format"}) } if p.Website != "" && !isValidURL(p.Website) { errors = append(errors, ValidationError{Field: "website", Message: "invalid URL format"}) } if p.Domestika != "" && !isValidURL(p.Domestika) { errors = append(errors, ValidationError{Field: "domestika", Message: "invalid URL format"}) } if len(errors) > 0 { return errors } return nil } // Validate checks Experience entry func (e *Experience) Validate() error { var errors ValidationErrors if strings.TrimSpace(e.Position) == "" { errors = append(errors, ValidationError{Field: "position", Message: "position is required"}) } if strings.TrimSpace(e.Company) == "" { errors = append(errors, ValidationError{Field: "company", Message: "company is required"}) } if strings.TrimSpace(e.StartDate) == "" { errors = append(errors, ValidationError{Field: "startDate", Message: "start date is required"}) } // EndDate is only required if not current if !e.Current && strings.TrimSpace(e.EndDate) == "" { errors = append(errors, ValidationError{Field: "endDate", Message: "end date is required when current is false"}) } // Validate company URL if present if e.CompanyURL != "" && !isValidURL(e.CompanyURL) { errors = append(errors, ValidationError{Field: "companyURL", Message: "invalid URL format"}) } if len(errors) > 0 { return errors } return nil } // Validate checks Education entry func (e *Education) Validate() error { var errors ValidationErrors if strings.TrimSpace(e.Degree) == "" { errors = append(errors, ValidationError{Field: "degree", Message: "degree is required"}) } if strings.TrimSpace(e.Institution) == "" { errors = append(errors, ValidationError{Field: "institution", Message: "institution is required"}) } if strings.TrimSpace(e.StartDate) == "" { errors = append(errors, ValidationError{Field: "startDate", Message: "start date is required"}) } if strings.TrimSpace(e.EndDate) == "" { errors = append(errors, ValidationError{Field: "endDate", Message: "end date is required"}) } if len(errors) > 0 { return errors } return nil } // Validate checks Skills section func (s *Skills) Validate() error { var errors ValidationErrors for i, cat := range s.Technical { if strings.TrimSpace(cat.Category) == "" { errors = append(errors, ValidationError{ Field: fmt.Sprintf("technical[%d].category", i), Message: "category name is required", }) } // Proficiency should be between 1-10 (half-star increments over 5 stars) if cat.Proficiency < 1 || cat.Proficiency > 10 { errors = append(errors, ValidationError{ Field: fmt.Sprintf("technical[%d].proficiency", i), Message: "proficiency must be between 1 and 10", }) } if len(cat.Items) == 0 { errors = append(errors, ValidationError{ Field: fmt.Sprintf("technical[%d].items", i), Message: "at least one skill item is required", }) } // Validate sidebar value if cat.Sidebar != "" && cat.Sidebar != "left" && cat.Sidebar != "right" { errors = append(errors, ValidationError{ Field: fmt.Sprintf("technical[%d].sidebar", i), Message: "sidebar must be 'left', 'right', or empty", }) } } if len(errors) > 0 { return errors } return nil } // Validate checks Language entry func (l *Language) Validate() error { var errors ValidationErrors if strings.TrimSpace(l.Language) == "" { errors = append(errors, ValidationError{Field: "language", Message: "language name is required"}) } if strings.TrimSpace(l.Proficiency) == "" { errors = append(errors, ValidationError{Field: "proficiency", Message: "proficiency is required"}) } // Level should be between 1-5 (typical language proficiency scale) if l.Level < 1 || l.Level > 5 { errors = append(errors, ValidationError{Field: "level", Message: "level must be between 1 and 5"}) } if len(errors) > 0 { return errors } return nil } // Validate checks Project entry func (p *Project) Validate() error { var errors ValidationErrors if strings.TrimSpace(p.Title) == "" { errors = append(errors, ValidationError{Field: "title", Message: "title is required"}) } // Validate URL if present if p.URL != "" && !isValidURL(p.URL) { errors = append(errors, ValidationError{Field: "url", Message: "invalid URL format"}) } // GitRepoUrl can be either a URL or a local filesystem path // We don't validate it strictly since it supports both formats if len(errors) > 0 { return errors } return nil } // Validate checks Meta information func (m *Meta) Validate() error { var errors ValidationErrors if strings.TrimSpace(m.Version) == "" { errors = append(errors, ValidationError{Field: "version", Message: "version is required"}) } if strings.TrimSpace(m.Language) == "" { errors = append(errors, ValidationError{Field: "language", Message: "language is required"}) } if len(errors) > 0 { return errors } return nil } // Helper functions // isValidEmail checks if an email address is valid func isValidEmail(email string) bool { email = strings.TrimSpace(email) if email == "" { return false } _, err := mail.ParseAddress(email) return err == nil } // isValidURL checks if a URL is valid func isValidURL(urlStr string) bool { urlStr = strings.TrimSpace(urlStr) if urlStr == "" { return false } // Parse the URL u, err := url.Parse(urlStr) if err != nil { return false } // Check that it has a scheme (http, https, etc.) if u.Scheme == "" { return false } // Check that it has a host if u.Host == "" { return false } return true }