Add core source model validation
This commit is contained in:
23
internal/core/source/source.go
Normal file
23
internal/core/source/source.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package source
|
||||
|
||||
type SourceDocument struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Format string `json:"format"`
|
||||
Digest string `json:"digest"`
|
||||
Units []SourceUnit `json:"units"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type SourceUnit struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Text string `json:"text"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type SourceRef struct {
|
||||
SourceID string `json:"source_id"`
|
||||
StartUnitID string `json:"start_unit_id"`
|
||||
EndUnitID string `json:"end_unit_id"`
|
||||
}
|
||||
275
internal/core/source/source_test.go
Normal file
275
internal/core/source/source_test.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateDocumentValid(t *testing.T) {
|
||||
doc := validDocument()
|
||||
|
||||
if err := ValidateDocument(doc); err != nil {
|
||||
t.Fatalf("ValidateDocument() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentNil(t *testing.T) {
|
||||
err := ValidateDocument(nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateDocument() error = nil, want error")
|
||||
}
|
||||
if err.Error() != "source document must not be nil" {
|
||||
t.Fatalf("ValidateDocument() error = %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentMissingFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*SourceDocument)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "id",
|
||||
mutate: func(doc *SourceDocument) { doc.ID = " \t" },
|
||||
wantErr: "source document id must not be empty",
|
||||
},
|
||||
{
|
||||
name: "kind",
|
||||
mutate: func(doc *SourceDocument) { doc.Kind = "" },
|
||||
wantErr: "source document kind must not be empty",
|
||||
},
|
||||
{
|
||||
name: "format",
|
||||
mutate: func(doc *SourceDocument) { doc.Format = "\n" },
|
||||
wantErr: "source document format must not be empty",
|
||||
},
|
||||
{
|
||||
name: "digest",
|
||||
mutate: func(doc *SourceDocument) { doc.Digest = "" },
|
||||
wantErr: "source document digest must not be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
doc := validDocument()
|
||||
tt.mutate(doc)
|
||||
|
||||
err := ValidateDocument(doc)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateDocument() error = nil, want error")
|
||||
}
|
||||
if err.Error() != tt.wantErr {
|
||||
t.Fatalf("ValidateDocument() error = %q, want %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentEmptyUnits(t *testing.T) {
|
||||
doc := validDocument()
|
||||
doc.Units = nil
|
||||
|
||||
err := ValidateDocument(doc)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateDocument() error = nil, want error")
|
||||
}
|
||||
if err.Error() != "source document units must not be empty" {
|
||||
t.Fatalf("ValidateDocument() error = %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentMissingUnitFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*SourceDocument)
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "id",
|
||||
mutate: func(doc *SourceDocument) { doc.Units[1].ID = "" },
|
||||
wantErr: "source unit[1].id must not be empty",
|
||||
},
|
||||
{
|
||||
name: "kind",
|
||||
mutate: func(doc *SourceDocument) { doc.Units[1].Kind = " " },
|
||||
wantErr: "source unit[1].kind must not be empty",
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
mutate: func(doc *SourceDocument) { doc.Units[1].Text = "\n\t" },
|
||||
wantErr: "source unit[1].text must not be empty",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
doc := validDocument()
|
||||
tt.mutate(doc)
|
||||
|
||||
err := ValidateDocument(doc)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateDocument() error = nil, want error")
|
||||
}
|
||||
if err.Error() != tt.wantErr {
|
||||
t.Fatalf("ValidateDocument() error = %q, want %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDocumentDuplicateUnitIDs(t *testing.T) {
|
||||
doc := validDocument()
|
||||
doc.Units[1].ID = " u1 "
|
||||
|
||||
err := ValidateDocument(doc)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateDocument() error = nil, want error")
|
||||
}
|
||||
if err.Error() != "source unit id \"u1\" is duplicated" {
|
||||
t.Fatalf("ValidateDocument() error = %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRefValid(t *testing.T) {
|
||||
doc := validDocument()
|
||||
ref := SourceRef{
|
||||
SourceID: "source-1",
|
||||
StartUnitID: "u1",
|
||||
EndUnitID: "u2",
|
||||
}
|
||||
|
||||
if err := ValidateRef(doc, ref); err != nil {
|
||||
t.Fatalf("ValidateRef() error = %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRefSourceIDMismatch(t *testing.T) {
|
||||
doc := validDocument()
|
||||
ref := SourceRef{
|
||||
SourceID: "source-2",
|
||||
StartUnitID: "u1",
|
||||
EndUnitID: "u2",
|
||||
}
|
||||
|
||||
err := ValidateRef(doc, ref)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateRef() error = nil, want error")
|
||||
}
|
||||
if err.Error() != "source ref source_id \"source-2\" does not match document id \"source-1\"" {
|
||||
t.Fatalf("ValidateRef() error = %q", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRefMissingUnitIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref SourceRef
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "missing source id",
|
||||
ref: SourceRef{StartUnitID: "u1", EndUnitID: "u2"},
|
||||
wantErr: "source ref source_id must not be empty",
|
||||
},
|
||||
{
|
||||
name: "missing start id",
|
||||
ref: SourceRef{SourceID: "source-1", EndUnitID: "u2"},
|
||||
wantErr: "source ref start_unit_id must not be empty",
|
||||
},
|
||||
{
|
||||
name: "missing end id",
|
||||
ref: SourceRef{SourceID: "source-1", StartUnitID: "u1"},
|
||||
wantErr: "source ref end_unit_id must not be empty",
|
||||
},
|
||||
{
|
||||
name: "unknown start id",
|
||||
ref: SourceRef{SourceID: "source-1", StartUnitID: "u9", EndUnitID: "u2"},
|
||||
wantErr: "source ref start_unit_id \"u9\" was not found",
|
||||
},
|
||||
{
|
||||
name: "unknown end id",
|
||||
ref: SourceRef{SourceID: "source-1", StartUnitID: "u1", EndUnitID: "u9"},
|
||||
wantErr: "source ref end_unit_id \"u9\" was not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRef(validDocument(), tt.ref)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateRef() error = nil, want error")
|
||||
}
|
||||
if err.Error() != tt.wantErr {
|
||||
t.Fatalf("ValidateRef() error = %q, want %q", err.Error(), tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRefReversedUnitOrder(t *testing.T) {
|
||||
doc := validDocument()
|
||||
ref := SourceRef{
|
||||
SourceID: "source-1",
|
||||
StartUnitID: "u2",
|
||||
EndUnitID: "u1",
|
||||
}
|
||||
|
||||
err := ValidateRef(doc, ref)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("ValidateRef() error = nil, want error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "appears after") {
|
||||
t.Fatalf("ValidateRef() error = %q, want reversed order error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnitIndex(t *testing.T) {
|
||||
doc := validDocument()
|
||||
|
||||
index, ok := UnitIndex(doc, "u2")
|
||||
if !ok {
|
||||
t.Fatal("UnitIndex() ok = false, want true")
|
||||
}
|
||||
if index != 1 {
|
||||
t.Fatalf("UnitIndex() index = %d, want 1", index)
|
||||
}
|
||||
|
||||
index, ok = UnitIndex(doc, "u9")
|
||||
if ok {
|
||||
t.Fatal("UnitIndex() ok = true, want false")
|
||||
}
|
||||
if index != 0 {
|
||||
t.Fatalf("UnitIndex() index = %d, want 0", index)
|
||||
}
|
||||
}
|
||||
|
||||
func validDocument() *SourceDocument {
|
||||
return &SourceDocument{
|
||||
ID: "source-1",
|
||||
Kind: "document",
|
||||
Format: "text/plain",
|
||||
Digest: "sha256:abc123",
|
||||
Units: []SourceUnit{
|
||||
{
|
||||
ID: "u1",
|
||||
Kind: "paragraph",
|
||||
Text: "First unit.",
|
||||
},
|
||||
{
|
||||
ID: "u2",
|
||||
Kind: "paragraph",
|
||||
Text: "Second unit.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
95
internal/core/source/validation.go
Normal file
95
internal/core/source/validation.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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 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[string]struct{}, len(doc.Units))
|
||||
for i, unit := range doc.Units {
|
||||
unitID := strings.TrimSpace(unit.ID)
|
||||
if unitID == "" {
|
||||
return fmt.Errorf("source unit[%d].id must not be empty", 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[unitID]; ok {
|
||||
return fmt.Errorf("source unit id %q is duplicated", unitID)
|
||||
}
|
||||
seenUnitIDs[unitID] = struct{}{}
|
||||
}
|
||||
|
||||
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 isBlank(ref.StartUnitID) {
|
||||
return fmt.Errorf("source ref start_unit_id must not be empty")
|
||||
}
|
||||
if isBlank(ref.EndUnitID) {
|
||||
return fmt.Errorf("source ref end_unit_id must not be empty")
|
||||
}
|
||||
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 %q was not found", ref.StartUnitID)
|
||||
}
|
||||
endIndex, ok := UnitIndex(doc, ref.EndUnitID)
|
||||
if !ok {
|
||||
return fmt.Errorf("source ref end_unit_id %q was not found", ref.EndUnitID)
|
||||
}
|
||||
if startIndex > endIndex {
|
||||
return fmt.Errorf("source ref start_unit_id %q appears after end_unit_id %q", ref.StartUnitID, ref.EndUnitID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func UnitIndex(doc *SourceDocument, unitID string) (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) == ""
|
||||
}
|
||||
Reference in New Issue
Block a user