Add Go transcript and glossary schemas

This commit is contained in:
2026-05-10 23:52:36 +00:00
parent ea8def423e
commit 950edc01f2
20 changed files with 898 additions and 0 deletions

2
go.mod
View File

@@ -1,3 +1,5 @@
module gitea.maximumdirect.net/eric/audita
go 1.22
require gopkg.in/yaml.v3 v3.0.1

4
go.sum Normal file
View File

@@ -0,0 +1,4 @@
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,23 @@
package schema
import "fmt"
type ParseError struct {
Message string
}
func (e *ParseError) Error() string {
return e.Message
}
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
if e.Field != "" {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
return e.Message
}

View File

@@ -0,0 +1,74 @@
package schema
import (
"fmt"
"gopkg.in/yaml.v3"
)
type GlossaryEntry struct {
Name string `yaml:"name"`
Aliases []string `yaml:"aliases,omitempty"`
Plural string `yaml:"plural,omitempty"`
Category string `yaml:"category"`
Summary string `yaml:"summary"`
}
type Glossary struct {
Entries []GlossaryEntry `yaml:"glossary"`
}
func ParseGlossaryYAML(raw []byte) (*Glossary, error) {
var top map[string]any
if err := yaml.Unmarshal(raw, &top); err != nil {
return nil, &ParseError{Message: fmt.Sprintf("glossary is not valid YAML: %v", err)}
}
var g Glossary
if err := yaml.Unmarshal(raw, &g); err != nil {
return nil, &ParseError{Message: fmt.Sprintf("failed to parse glossary: %v", err)}
}
if err := validateGlossary(&g); err != nil {
return nil, err
}
return &g, nil
}
func validateGlossary(g *Glossary) error {
if len(g.Entries) == 0 {
return &ValidationError{Field: "glossary", Message: "must contain at least one entry"}
}
for i, e := range g.Entries {
entryLabel := fmt.Sprintf("glossary[%d]", i)
if e.Name == "" {
return &ValidationError{Field: fmt.Sprintf("%s.name", entryLabel), Message: "must not be empty"}
}
if e.Category == "" {
return &ValidationError{Field: fmt.Sprintf("%s.category", entryLabel), Message: "must not be empty"}
}
if e.Summary == "" {
return &ValidationError{Field: fmt.Sprintf("%s.summary", entryLabel), Message: "must not be empty"}
}
for j, alias := range e.Aliases {
if alias == "" {
return &ValidationError{
Field: fmt.Sprintf("%s.aliases[%d]", entryLabel, j),
Message: "must not be empty",
}
}
}
if e.Plural != "" && len(e.Plural) == 0 {
return &ValidationError{Field: fmt.Sprintf("%s.plural", entryLabel), Message: "must not be empty if present"}
}
}
return nil
}

View File

@@ -0,0 +1,142 @@
package schema
import (
"os"
"testing"
)
func TestParseGlossaryYAML_Valid(t *testing.T) {
raw, err := os.ReadFile("testdata/glossary_valid.yaml")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
glossary, err := ParseGlossaryYAML(raw)
if err != nil {
t.Fatalf("ParseGlossaryYAML failed: %v", err)
}
if len(glossary.Entries) != 2 {
t.Fatalf("expected 2 entries, got %d", len(glossary.Entries))
}
e1 := glossary.Entries[0]
if e1.Name != "Jesters" {
t.Errorf("expected entries[0].name = Jesters, got %q", e1.Name)
}
if e1.Category != "faction" {
t.Errorf("expected entries[0].category = faction, got %q", e1.Category)
}
if e1.Summary != "A faction name." {
t.Errorf("expected entries[0].summary = A faction name., got %q", e1.Summary)
}
if len(e1.Aliases) != 0 {
t.Errorf("expected entries[0].aliases = [], got %v", e1.Aliases)
}
e2 := glossary.Entries[1]
if e2.Name != "Popov" {
t.Errorf("expected entries[1].name = Popov, got %q", e2.Name)
}
if len(e2.Aliases) != 1 || e2.Aliases[0] != "Hrank" {
t.Errorf("expected entries[1].aliases = [Hrank], got %v", e2.Aliases)
}
}
func TestParseGlossaryYAML_MalformedYAML(t *testing.T) {
raw, err := os.ReadFile("testdata/glossary_malformed.yaml")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseGlossaryYAML(raw)
if err == nil {
t.Fatal("expected error for malformed YAML, got nil")
}
var parseErr *ParseError
if parseErr, ok := err.(*ParseError); !ok {
t.Errorf("expected *ParseError, got %T", err)
} else if parseErr.Message == "" {
t.Error("expected non-empty parse error message")
}
}
func TestParseGlossaryYAML_MissingRequiredFields(t *testing.T) {
raw, err := os.ReadFile("testdata/glossary_missing_fields.yaml")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseGlossaryYAML(raw)
if err == nil {
t.Fatal("expected error for missing required fields, got nil")
}
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
} else if valErr.Field == "" {
t.Error("expected non-empty validation error field")
}
}
func TestParseGlossaryYAML_EmptyGlossary(t *testing.T) {
raw := []byte(`glossary: []`)
_, err := ParseGlossaryYAML(raw)
if err == nil {
t.Fatal("expected error for empty glossary, got nil")
}
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
} else if valErr.Message != "must contain at least one entry" {
t.Errorf("expected 'must contain at least one entry', got %q", valErr.Message)
}
}
func TestParseGlossaryYAML_EmptyName(t *testing.T) {
raw := []byte(`
glossary:
- name: ""
category: faction
summary: A faction.
`)
_, err := ParseGlossaryYAML(raw)
if err == nil {
t.Fatal("expected error for empty name, got nil")
}
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
} else if valErr.Message != "must not be empty" {
t.Errorf("expected 'must not be empty', got %q", valErr.Message)
}
}
func TestParseGlossaryYAML_EmptyAlias(t *testing.T) {
raw := []byte(`
glossary:
- name: Test
aliases:
- ""
category: faction
summary: A test entry.
`)
_, err := ParseGlossaryYAML(raw)
if err == nil {
t.Fatal("expected error for empty alias, got nil")
}
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
} else if valErr.Message != "must not be empty" {
t.Errorf("expected 'must not be empty', got %q", valErr.Message)
}
}

View File

@@ -0,0 +1 @@
glossary: [

View File

@@ -0,0 +1,2 @@
glossary:
- name: Jesters

View File

@@ -0,0 +1,9 @@
glossary:
- name: Jesters
category: faction
summary: A faction name.
- name: Popov
aliases:
- Hrank
category: character
summary: A character name.

View File

@@ -0,0 +1,17 @@
[
{
"id": 1,
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
},
{
"id": 2,
"speaker": "Bob",
"start": 2.0,
"end": 3.5,
"text": "Hi there.",
"categories": ["greeting"]
}
]

View File

@@ -0,0 +1,16 @@
[
{
"id": 1,
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
},
{
"id": 1,
"speaker": "Bob",
"start": 2.0,
"end": 3.5,
"text": "Hi there."
}
]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,9 @@
[
{
"id": 1,
"speaker": "",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
}
]

View File

@@ -0,0 +1,9 @@
[
{
"id": 1,
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": ""
}
]

View File

@@ -0,0 +1,9 @@
[
{
"id": 1,
"speaker": "Alice",
"start": 5.0,
"end": 1.5,
"text": "Hello world."
}
]

View File

@@ -0,0 +1 @@
{"segments":[{"id":1,"text":"oops"}

View File

@@ -0,0 +1,14 @@
[
{
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
},
{
"speaker": "Bob",
"start": 2.0,
"end": 3.5,
"text": "Hi there."
}
]

View File

@@ -0,0 +1,9 @@
[
{
"id": 2,
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
}
]

View File

@@ -0,0 +1,19 @@
{
"segments": [
{
"id": 1,
"speaker": "Alice",
"start": 0.0,
"end": 1.5,
"text": "Hello world."
},
{
"id": 2,
"speaker": "Bob",
"start": 2.0,
"end": 3.5,
"text": "Hi there.",
"categories": ["greeting"]
}
]
}

View File

@@ -0,0 +1,240 @@
package schema
import (
"encoding/json"
"fmt"
"math"
)
type SourceSegment struct {
ID *int `json:"id,omitempty"`
Speaker string `json:"speaker"`
Start float64 `json:"start"`
End float64 `json:"end"`
Text string `json:"text"`
Categories []string `json:"categories,omitempty"`
}
type Segment struct {
ID int `json:"id"`
Speaker string `json:"speaker"`
Start float64 `json:"start"`
End float64 `json:"end"`
Text string `json:"text"`
Categories []string `json:"categories,omitempty"`
}
type Transcript struct {
Segments []Segment `json:"segments"`
}
type SourceTranscript struct {
Segments []SourceSegment `json:"segments"`
}
func ParseSourceTranscriptJSON(raw []byte) (*SourceTranscript, error) {
if !json.Valid(raw) {
return nil, &ParseError{Message: "transcript is not valid JSON"}
}
var top any
if err := json.Unmarshal(raw, &top); err != nil {
return nil, &ParseError{Message: fmt.Sprintf("failed to parse transcript JSON: %v", err)}
}
segmentsRaw, err := extractSegments(top)
if err != nil {
return nil, err
}
if len(segmentsRaw) == 0 {
return nil, &ParseError{Message: "transcript must contain at least one segment"}
}
var segments []SourceSegment
if err := json.Unmarshal(segmentsRaw, &segments); err != nil {
return nil, &ParseError{Message: fmt.Sprintf("failed to parse segments: %v", err)}
}
if err := validateSourceSegments(segments); err != nil {
return nil, err
}
return &SourceTranscript{Segments: segments}, nil
}
func ParseTranscriptJSON(raw []byte) (*Transcript, error) {
source, err := ParseSourceTranscriptJSON(raw)
if err != nil {
return nil, err
}
segments := make([]Segment, len(source.Segments))
for i, s := range source.Segments {
segments[i] = Segment{
ID: *s.ID,
Speaker: s.Speaker,
Start: s.Start,
End: s.End,
Text: s.Text,
Categories: s.Categories,
}
}
if err := validateSequentialIDs(segments); err != nil {
return nil, err
}
return &Transcript{Segments: segments}, nil
}
func ParseTranscriptJSONLenient(raw []byte) (*Transcript, error) {
source, err := ParseSourceTranscriptJSON(raw)
if err != nil {
return nil, err
}
segments := make([]Segment, len(source.Segments))
for i, s := range source.Segments {
id := i + 1
if s.ID != nil {
id = *s.ID
}
segments[i] = Segment{
ID: id,
Speaker: s.Speaker,
Start: s.Start,
End: s.End,
Text: s.Text,
Categories: s.Categories,
}
}
return &Transcript{Segments: segments}, nil
}
func extractSegments(top any) (json.RawMessage, error) {
switch v := top.(type) {
case []any:
raw, err := json.Marshal(v)
if err != nil {
return nil, &ParseError{Message: "failed to re-encode segment array"}
}
return raw, nil
case map[string]any:
segs, ok := v["segments"]
if !ok {
return nil, &ParseError{Message: "transcript object must contain a segments array"}
}
segsArray, ok := segs.([]any)
if !ok {
return nil, &ParseError{Message: "segments field must be an array"}
}
raw, err := json.Marshal(segsArray)
if err != nil {
return nil, &ParseError{Message: "failed to re-encode segments array"}
}
return raw, nil
default:
return nil, &ParseError{Message: "transcript must be a JSON array or an object with a segments array"}
}
}
func validateSourceSegments(segments []SourceSegment) error {
for i, s := range segments {
segLabel := segmentLabel(i, s.ID)
if s.Speaker == "" {
return &ValidationError{Field: fmt.Sprintf("%s.speaker", segLabel), Message: "must not be empty"}
}
if s.Text == "" {
return &ValidationError{Field: fmt.Sprintf("%s.text", segLabel), Message: "must not be empty"}
}
if err := validateTime(s.Start, fmt.Sprintf("%s.start", segLabel)); err != nil {
return err
}
if err := validateTime(s.End, fmt.Sprintf("%s.end", segLabel)); err != nil {
return err
}
if s.End < s.Start {
return &ValidationError{
Field: fmt.Sprintf("%s.end", segLabel),
Message: fmt.Sprintf("end (%g) must be greater than or equal to start (%g)", s.End, s.Start),
}
}
for j, cat := range s.Categories {
if cat == "" {
return &ValidationError{
Field: fmt.Sprintf("%s.categories[%d]", segLabel, j),
Message: "must not be empty",
}
}
}
}
seenIDs := make(map[int]int)
for i, s := range segments {
if s.ID != nil {
if firstIdx, exists := seenIDs[*s.ID]; exists {
return &ValidationError{
Field: fmt.Sprintf("segment[%d].id", i),
Message: fmt.Sprintf("duplicate id %d (first used at segment[%d])", *s.ID, firstIdx),
}
}
seenIDs[*s.ID] = i
}
}
return nil
}
func validateTime(t float64, field string) error {
if math.IsNaN(t) || math.IsInf(t, 0) {
return &ValidationError{Field: field, Message: "must be a finite number"}
}
if t < 0 {
return &ValidationError{Field: field, Message: "must be non-negative"}
}
return nil
}
func validateSequentialIDs(segments []Segment) error {
for i, s := range segments {
expected := i + 1
if s.ID != expected {
return &ValidationError{
Field: fmt.Sprintf("segment[%d].id", i),
Message: fmt.Sprintf("must be sequential starting at 1 (got %d, expected %d)", s.ID, expected),
}
}
}
return nil
}
func segmentLabel(index int, id *int) string {
if id != nil {
return fmt.Sprintf("segment[%d] (id=%d)", index, *id)
}
return fmt.Sprintf("segment[%d]", index)
}
func TranscriptToJSON(t *Transcript) ([]byte, error) {
payload := make([]map[string]any, len(t.Segments))
for i, s := range t.Segments {
payload[i] = map[string]any{
"id": s.ID,
"speaker": s.Speaker,
"start": s.Start,
"end": s.End,
"text": s.Text,
}
if len(s.Categories) > 0 {
payload[i]["categories"] = s.Categories
}
}
return json.MarshalIndent(payload, "", " ")
}

View File

@@ -0,0 +1,297 @@
package schema
import (
"os"
"testing"
)
func TestParseSourceTranscriptJSON_BareArray(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_bare_array.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
transcript, err := ParseSourceTranscriptJSON(raw)
if err != nil {
t.Fatalf("ParseSourceTranscriptJSON failed: %v", err)
}
if len(transcript.Segments) != 2 {
t.Fatalf("expected 2 segments, got %d", len(transcript.Segments))
}
s1 := transcript.Segments[0]
if *s1.ID != 1 {
t.Errorf("expected segment[0].id = 1, got %d", *s1.ID)
}
if s1.Speaker != "Alice" {
t.Errorf("expected segment[0].speaker = Alice, got %q", s1.Speaker)
}
if s1.Start != 0.0 {
t.Errorf("expected segment[0].start = 0.0, got %g", s1.Start)
}
if s1.End != 1.5 {
t.Errorf("expected segment[0].end = 1.5, got %g", s1.End)
}
if s1.Text != "Hello world." {
t.Errorf("expected segment[0].text = Hello world., got %q", s1.Text)
}
if s1.Categories != nil {
t.Errorf("expected segment[0].categories = nil, got %v", s1.Categories)
}
s2 := transcript.Segments[1]
if *s2.ID != 2 {
t.Errorf("expected segment[1].id = 2, got %d", *s2.ID)
}
if len(s2.Categories) != 1 || s2.Categories[0] != "greeting" {
t.Errorf("expected segment[1].categories = [greeting], got %v", s2.Categories)
}
}
func TestParseSourceTranscriptJSON_ObjectWithSegments(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_object.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
transcript, err := ParseSourceTranscriptJSON(raw)
if err != nil {
t.Fatalf("ParseSourceTranscriptJSON failed: %v", err)
}
if len(transcript.Segments) != 2 {
t.Fatalf("expected 2 segments, got %d", len(transcript.Segments))
}
if *transcript.Segments[0].ID != 1 {
t.Errorf("expected segment[0].id = 1, got %d", *transcript.Segments[0].ID)
}
}
func TestParseSourceTranscriptJSON_MalformedJSON(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_malformed.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for malformed JSON, got nil")
}
var parseErr *ParseError
if parseErr, ok := err.(*ParseError); !ok {
t.Errorf("expected *ParseError, got %T", err)
} else if parseErr.Message != "transcript is not valid JSON" {
t.Errorf("expected 'transcript is not valid JSON', got %q", parseErr.Message)
}
}
func TestParseSourceTranscriptJSON_EmptySpeaker(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_empty_speaker.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for empty speaker, got nil")
}
assertValidationError(t, err, "speaker", "must not be empty")
}
func TestParseSourceTranscriptJSON_EmptyText(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_empty_text.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for empty text, got nil")
}
assertValidationError(t, err, "text", "must not be empty")
}
func TestParseSourceTranscriptJSON_InvalidTimes(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_invalid_times.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for invalid times, got nil")
}
assertValidationErrorContains(t, err, "end", "must be greater than or equal to start")
}
func TestParseSourceTranscriptJSON_DuplicateIDs(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_duplicate_ids.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for duplicate IDs, got nil")
}
assertValidationErrorContains(t, err, "id", "duplicate id")
}
func TestParseSourceTranscriptJSON_EmptyArray(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_empty_array.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for empty array, got nil")
}
var parseErr *ParseError
if parseErr, ok := err.(*ParseError); !ok {
t.Errorf("expected *ParseError, got %T", err)
} else if parseErr.Message != "transcript must contain at least one segment" {
t.Errorf("expected 'transcript must contain at least one segment', got %q", parseErr.Message)
}
}
func TestParseTranscriptJSON_NonSequentialID(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_non_sequential_id.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
_, err = ParseTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for non-sequential ID, got nil")
}
assertValidationErrorContains(t, err, "id", "must be sequential")
}
func TestParseTranscriptJSONLenient_AssignsSequentialIDs(t *testing.T) {
raw, err := os.ReadFile("testdata/transcript_no_ids.json")
if err != nil {
t.Fatalf("failed to read test fixture: %v", err)
}
transcript, err := ParseTranscriptJSONLenient(raw)
if err != nil {
t.Fatalf("ParseTranscriptJSONLenient failed: %v", err)
}
if len(transcript.Segments) != 2 {
t.Fatalf("expected 2 segments, got %d", len(transcript.Segments))
}
if transcript.Segments[0].ID != 1 {
t.Errorf("expected segment[0].id = 1, got %d", transcript.Segments[0].ID)
}
if transcript.Segments[1].ID != 2 {
t.Errorf("expected segment[1].id = 2, got %d", transcript.Segments[1].ID)
}
}
func TestParseSourceTranscriptJSON_UnsupportedTopLevelShape(t *testing.T) {
raw := []byte(`"just a string"`)
_, err := ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for unsupported top-level shape, got nil")
}
var parseErr *ParseError
if parseErr, ok := err.(*ParseError); !ok {
t.Errorf("expected *ParseError, got %T", err)
} else if parseErr.Message != "transcript must be a JSON array or an object with a segments array" {
t.Errorf("expected unsupported shape message, got %q", parseErr.Message)
}
}
func TestParseSourceTranscriptJSON_ObjectWithoutSegments(t *testing.T) {
raw := []byte(`{"metadata": {"version": "1.0"}}`)
_, err := ParseSourceTranscriptJSON(raw)
if err == nil {
t.Fatal("expected error for object without segments, got nil")
}
var parseErr *ParseError
if parseErr, ok := err.(*ParseError); !ok {
t.Errorf("expected *ParseError, got %T", err)
} else if parseErr.Message != "transcript object must contain a segments array" {
t.Errorf("expected 'transcript object must contain a segments array', got %q", parseErr.Message)
}
}
func TestTranscriptToJSON(t *testing.T) {
transcript := &Transcript{
Segments: []Segment{
{ID: 1, Speaker: "Alice", Start: 0.0, End: 1.5, Text: "Hello world."},
{ID: 2, Speaker: "Bob", Start: 2.0, End: 3.5, Text: "Hi there.", Categories: []string{"greeting"}},
},
}
raw, err := TranscriptToJSON(transcript)
if err != nil {
t.Fatalf("TranscriptToJSON failed: %v", err)
}
parsed, err := ParseTranscriptJSON(raw)
if err != nil {
t.Fatalf("failed to parse round-trip JSON: %v", err)
}
if len(parsed.Segments) != 2 {
t.Fatalf("expected 2 segments after round-trip, got %d", len(parsed.Segments))
}
if parsed.Segments[0].Speaker != "Alice" {
t.Errorf("expected segment[0].speaker = Alice, got %q", parsed.Segments[0].Speaker)
}
if len(parsed.Segments[1].Categories) != 1 || parsed.Segments[1].Categories[0] != "greeting" {
t.Errorf("expected segment[1].categories = [greeting], got %v", parsed.Segments[1].Categories)
}
}
func assertValidationError(t *testing.T, err error, fieldContains, messageContains string) {
t.Helper()
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
return
}
if fieldContains != "" && valErr.Field == "" {
t.Errorf("expected field to contain %q, got empty field", fieldContains)
}
if messageContains != "" && valErr.Message == "" {
t.Errorf("expected message to contain %q, got empty message", messageContains)
}
}
func assertValidationErrorContains(t *testing.T, err error, fieldContains, messageContains string) {
t.Helper()
var valErr *ValidationError
if valErr, ok := err.(*ValidationError); !ok {
t.Errorf("expected *ValidationError, got %T", err)
return
}
if fieldContains != "" && valErr.Field == "" {
t.Errorf("expected field to contain %q, got empty field", fieldContains)
}
if messageContains != "" && valErr.Message == "" {
t.Errorf("expected message to contain %q, got empty message", messageContains)
}
}