89 lines
2.4 KiB
Go
89 lines
2.4 KiB
Go
package results
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"denotesrv/pkg/metadata"
|
|
)
|
|
|
|
// Marshal serializes Results to a pipe-delimited byte format.
|
|
// Format: identifier | title | tags (comma-separated)
|
|
func Marshal(rs metadata.Results) []byte {
|
|
var buf strings.Builder
|
|
for _, e := range rs {
|
|
title := e.Title
|
|
if title == "" {
|
|
title = "(untitled)"
|
|
}
|
|
|
|
tags := strings.Join(e.Tags, ",")
|
|
fmt.Fprintf(&buf, "%s | %s | %s\n", e.Identifier, title, tags)
|
|
}
|
|
return []byte(buf.String())
|
|
}
|
|
|
|
// Unmarshal parses pipe-delimited byte data into Results.
|
|
// Format: identifier | title | tags (comma-separated)
|
|
// Invalid tags produce warnings but parsing continues.
|
|
func Unmarshal(data []byte) (metadata.Results, error) {
|
|
return unmarshal(data, false)
|
|
}
|
|
|
|
// UnmarshalStrict parses pipe-delimited byte data into Results with strict tag validation.
|
|
// Returns an error if any tags are invalid.
|
|
func UnmarshalStrict(data []byte) (metadata.Results, error) {
|
|
return unmarshal(data, true)
|
|
}
|
|
|
|
func unmarshal(data []byte, strict bool) (metadata.Results, error) {
|
|
var results metadata.Results
|
|
lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))
|
|
// Allow lowercase Latin letters, other letters (CJK, etc.), and digits, no spaces
|
|
tagPattern := regexp.MustCompile(`^([\p{Ll}\p{Lo}\p{Nd}]+,)*[\p{Ll}\p{Lo}\p{Nd}]+$`)
|
|
|
|
for lineNum, line := range lines {
|
|
line = bytes.TrimSpace(line)
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
parts := bytes.Split(line, []byte("|"))
|
|
if len(parts) != 3 {
|
|
return nil, fmt.Errorf("line %d: expected 3 columns, got %d (line: %q)", lineNum+1, len(parts), line)
|
|
}
|
|
|
|
identifier := string(bytes.TrimSpace(parts[0]))
|
|
title := string(bytes.TrimSpace(parts[1]))
|
|
tagsStr := string(bytes.TrimSpace(parts[2]))
|
|
|
|
if identifier == "" {
|
|
return nil, fmt.Errorf("line %d: identifier cannot be empty", lineNum+1)
|
|
}
|
|
|
|
var tags []string
|
|
if tagsStr != "" {
|
|
if !tagPattern.MatchString(tagsStr) {
|
|
if strict {
|
|
return nil, fmt.Errorf("line %d: tags must be comma-delimited lowercase unicode words (no spaces): got '%s'", lineNum+1, tagsStr)
|
|
}
|
|
log.Printf("warning: line %d: invalid tags '%s' (must be lowercase alphanumeric)", lineNum+1, tagsStr)
|
|
}
|
|
tags = strings.Split(tagsStr, ",")
|
|
} else {
|
|
tags = []string{}
|
|
}
|
|
|
|
results = append(results, &metadata.Metadata{
|
|
Identifier: identifier,
|
|
Title: title,
|
|
Tags: tags,
|
|
})
|
|
}
|
|
|
|
return results, nil
|
|
}
|