package source import ( "fmt" "strings" ) // DocumentIndex is an immutable snapshot of a source document's identity and // unit positions for repeated source-reference operations. type DocumentIndex struct { documentID string positions map[int]int hasDocument bool } // NewDocumentIndex snapshots doc without retaining or mutating it. func NewDocumentIndex(doc *SourceDocument) DocumentIndex { if doc == nil { return DocumentIndex{} } positions := make(map[int]int, len(doc.Units)) for position, unit := range doc.Units { if _, exists := positions[unit.ID]; !exists { positions[unit.ID] = position } } return DocumentIndex{ documentID: doc.ID, positions: positions, hasDocument: true, } } // DocumentID returns the indexed document identity. func (i DocumentIndex) DocumentID() (string, bool) { if !i.hasDocument { return "", false } return i.documentID, true } // Position returns the indexed document position for unitID. func (i DocumentIndex) Position(unitID int) (int, bool) { position, ok := i.positions[unitID] return position, ok } // ValidateRef validates ref against the indexed document snapshot. func (i DocumentIndex) ValidateRef(ref SourceRef) error { if !i.hasDocument { return fmt.Errorf("source document must not be nil") } return validateRef(i.documentID, i.Position, ref) } 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") } return validateRef(doc.ID, func(unitID int) (int, bool) { return UnitIndex(doc, unitID) }, ref) } func validateRef(documentID string, position func(int) (int, bool), ref SourceRef) error { 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 != documentID { return fmt.Errorf("source ref source_id %q does not match document id %q", ref.SourceID, documentID) } startIndex, ok := position(ref.StartUnitID) if !ok { return fmt.Errorf("source ref start_unit_id %d was not found", ref.StartUnitID) } endIndex, ok := position(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 }