package source import ( "fmt" "strings" ) func ValidateDocument(doc *SourceDocument) error { if doc == nil { return fmt.Errorf("source document must not be nil") } if isBlank(doc.ID) { return fmt.Errorf("source document id must not be empty") } if hasSurroundingWhitespace(doc.ID) { return fmt.Errorf("source document id %q must not contain leading or trailing whitespace", doc.ID) } if isBlank(doc.Kind) { return fmt.Errorf("source document kind must not be empty") } if isBlank(doc.Format) { return fmt.Errorf("source document format must not be empty") } if isBlank(doc.Digest) { return fmt.Errorf("source document digest must not be empty") } if len(doc.Units) == 0 { return fmt.Errorf("source document units must not be empty") } seenUnitIDs := make(map[int]struct{}, len(doc.Units)) for i, unit := range doc.Units { if unit.ID <= 0 { return fmt.Errorf("source unit[%d].id must be positive", i) } if isBlank(unit.Kind) { return fmt.Errorf("source unit[%d].kind must not be empty", i) } if isBlank(unit.Text) { return fmt.Errorf("source unit[%d].text must not be empty", i) } if _, ok := seenUnitIDs[unit.ID]; ok { return fmt.Errorf("source unit id %d is duplicated", unit.ID) } seenUnitIDs[unit.ID] = struct{}{} } for i, unit := range doc.Units { if err := ValidateRef(doc, unit.Ref); err != nil { return fmt.Errorf("source unit[%d].ref: %w", i, err) } if unit.Ref.StartUnitID != unit.ID || unit.Ref.EndUnitID != unit.ID { return fmt.Errorf("source unit[%d].ref must identify source unit id %d", i, unit.ID) } } return nil } func ValidateRef(doc *SourceDocument, ref SourceRef) error { if doc == nil { return fmt.Errorf("source document must not be nil") } if isBlank(ref.SourceID) { return fmt.Errorf("source ref source_id must not be empty") } if hasSurroundingWhitespace(ref.SourceID) { return fmt.Errorf("source ref source_id %q must not contain leading or trailing whitespace", ref.SourceID) } if ref.StartUnitID <= 0 { return fmt.Errorf("source ref start_unit_id must be positive") } if ref.EndUnitID <= 0 { return fmt.Errorf("source ref end_unit_id must be positive") } if ref.SourceID != doc.ID { return fmt.Errorf("source ref source_id %q does not match document id %q", ref.SourceID, doc.ID) } startIndex, ok := UnitIndex(doc, ref.StartUnitID) if !ok { return fmt.Errorf("source ref start_unit_id %d was not found", ref.StartUnitID) } endIndex, ok := UnitIndex(doc, ref.EndUnitID) if !ok { return fmt.Errorf("source ref end_unit_id %d was not found", ref.EndUnitID) } if startIndex > endIndex { return fmt.Errorf("source ref start_unit_id %d appears after end_unit_id %d", ref.StartUnitID, ref.EndUnitID) } return nil } func UnitIndex(doc *SourceDocument, unitID int) (int, bool) { if doc == nil { return 0, false } for i, unit := range doc.Units { if unit.ID == unitID { return i, true } } return 0, false } func isBlank(value string) bool { return strings.TrimSpace(value) == "" } func hasSurroundingWhitespace(value string) bool { return strings.TrimSpace(value) != value }