package mapping
import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"
)
type SpeciesMapping struct {
Species string `json:"species"`
Calltypes map[string]string `json:"calltypes,omitempty"`
}
type File map[string]SpeciesMapping
func Load(path string) (File, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read mapping file: %w", err)
}
var m File
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("failed to parse mapping JSON: %w", err)
}
if len(m) == 0 {
return nil, fmt.Errorf("mapping file is empty")
}
for dataSpecies, sm := range m {
if sm.Species == "" {
return nil, fmt.Errorf("mapping entry '%s' has empty species field", dataSpecies)
}
}
return m, nil
}
const (
Negative = "__NEGATIVE__"
Ignore = "__IGNORE__"
)
type Kind int
const (
Real Kind = iota
Neg
Ign
)
func (m File) Classify(dataSpecies string) (canonical string, kind Kind, ok bool) {
sm, exists := m[dataSpecies]
if !exists {
return "", Real, false
}
switch sm.Species {
case Negative:
return "", Neg, true
case Ignore:
return "", Ign, true
default:
return sm.Species, Real, true
}
}
func (m File) ValidateCoversSpecies(speciesSet map[string]bool) []string {
missing := make([]string, 0)
for s := range speciesSet {
if _, exists := m[s]; !exists {
missing = append(missing, s)
}
}
sort.Strings(missing)
return missing
}
func (m File) Classes() []string {
set := make(map[string]bool)
for _, sm := range m {
switch sm.Species {
case Negative, Ignore, "":
continue
default:
set[sm.Species] = true
}
}
out := make([]string, 0, len(set))
for s := range set {
out = append(out, s)
}
sort.Strings(out)
return out
}
func (m File) GetDBSpecies(dataSpecies string) (string, bool) {
sm, exists := m[dataSpecies]
if !exists {
return "", false
}
return sm.Species, true
}
func (m File) GetDBCalltype(dataSpecies, dataCalltype string) string {
sm, exists := m[dataSpecies]
if !exists || sm.Calltypes == nil {
return dataCalltype
}
if dbCT, ok := sm.Calltypes[dataCalltype]; ok {
return dbCT
}
return dataCalltype
}
type ValidationResult struct {
MissingSpecies []string MissingDBSpecies []string MissingCalltypes map[string]string }
func (r ValidationResult) HasErrors() bool {
return len(r.MissingSpecies) > 0 ||
len(r.MissingDBSpecies) > 0 ||
len(r.MissingCalltypes) > 0
}
func (r ValidationResult) Error() string {
var parts []string
if len(r.MissingSpecies) > 0 {
parts = append(parts, fmt.Sprintf("species in .data but not in mapping: [%s]",
strings.Join(r.MissingSpecies, ", ")))
}
if len(r.MissingDBSpecies) > 0 {
parts = append(parts, fmt.Sprintf("mapped species not found in DB: [%s]",
strings.Join(r.MissingDBSpecies, ", ")))
}
if len(r.MissingCalltypes) > 0 {
var ctErrors []string
for k, v := range r.MissingCalltypes {
ctErrors = append(ctErrors, fmt.Sprintf("%s->%s", k, v))
}
sort.Strings(ctErrors)
parts = append(parts, fmt.Sprintf("calltypes not found in DB: [%s]",
strings.Join(ctErrors, ", ")))
}
return strings.Join(parts, "; ")
}