coverage~bigbes/sr-ht-spec64cae3afcore/id.go

Coverage
100.0% 30/30 statements
Δ
+0.0
Blob
640296f
Uncovered nothing — every instrumented line ran
1 package core
2
3 import (
4 "fmt"
5 "strings"
6 )
7
8 const (
9 // MaxDocIDPrefixLen bounds the alphabetic part of an ID ("SPEC", "RFC").
10 MaxDocIDPrefixLen = 16
11
12 // MaxDocIDSeqLen bounds the numeric part. Eight digits is far past the
13 // volume this service will ever see; the cap exists so a pathological ID
14 // cannot become a pathological registry key.
15 MaxDocIDSeqLen = 8
16 )
17
18 // DocID is a parsed document identifier such as "SPEC-0007".
19 //
20 // Seq is kept as a string rather than an int on purpose: leading zeros are part
21 // of how these IDs are written and read, and "SPEC-7" and "SPEC-0007" are
22 // therefore different IDs in a registry that is globally unique. Normalizing
23 // them to the same integer would silently merge two documents.
24 type DocID struct {
25 Prefix string
26 Seq string
27 }
28
29 // String renders the canonical "PREFIX-SEQ" form.
30 9 func (d DocID) String() string { return d.Prefix + "-" + d.Seq }
31
32 // ParseDocID parses and validates a document ID. The grammar is deliberately
33 // tiny:
34 //
35 // ID = PREFIX "-" SEQ
36 // PREFIX = [A-Z] [A-Z0-9]* (1..MaxDocIDPrefixLen)
37 // SEQ = [0-9]+ (1..MaxDocIDSeqLen)
38 //
39 // Exactly one '-' separates the two, so the split is unambiguous and IDs sort
40 // predictably. ASCII-only and uppercase-only is the load-bearing part: IDs are
41 // the global registry key, so a Cyrillic "ะก" or a lowercase "spec" must be a
42 // different-looking ID that is rejected outright rather than a homograph that
43 // quietly registers alongside the real one.
44 //
45 // Core validates shape only. Global uniqueness is enforced by the registry
46 // table, which is the only thing that can know about other spaces.
47 51 func ParseDocID(s string) (DocID, error) {
48 51 if s == "" {
49 3 return DocID{}, fmt.Errorf("%w: empty id", ErrInvalidDocID)
50 3 }
51 48 i := strings.IndexByte(s, '-')
52 48 if i < 0 {
53 2 return DocID{}, fmt.Errorf("%w: id %q has no '-' separator", ErrInvalidDocID, s)
54 2 }
55 46 prefix, seq := s[:i], s[i+1:]
56 46 if strings.IndexByte(seq, '-') >= 0 {
57 2 return DocID{}, fmt.Errorf("%w: id %q has more than one '-'", ErrInvalidDocID, s)
58 2 }
59
60 44 if prefix == "" {
61 1 return DocID{}, fmt.Errorf("%w: id %q has an empty prefix", ErrInvalidDocID, s)
62 1 }
63 43 if len(prefix) > MaxDocIDPrefixLen {
64 1 return DocID{}, fmt.Errorf("%w: id %q prefix is too long (%d > %d)",
65 1 ErrInvalidDocID, s, len(prefix), MaxDocIDPrefixLen)
66 1 }
67 42 if !isUpperAlpha(prefix[0]) {
68 6 return DocID{}, fmt.Errorf("%w: id %q prefix must start with A-Z", ErrInvalidDocID, s)
69 6 }
70 151 for i := 0; i < len(prefix); i++ {
71 151 if c := prefix[i]; !isUpperAlpha(c) && !isDigit(c) {
72 4 return DocID{}, fmt.Errorf("%w: id %q prefix contains disallowed byte %q", ErrInvalidDocID, s, c)
73 4 }
74 }
75
76 32 if seq == "" {
77 1 return DocID{}, fmt.Errorf("%w: id %q has an empty sequence", ErrInvalidDocID, s)
78 1 }
79 31 if len(seq) > MaxDocIDSeqLen {
80 3 return DocID{}, fmt.Errorf("%w: id %q sequence is too long (%d > %d)",
81 3 ErrInvalidDocID, s, len(seq), MaxDocIDSeqLen)
82 3 }
83 69 for i := 0; i < len(seq); i++ {
84 69 if !isDigit(seq[i]) {
85 6 return DocID{}, fmt.Errorf("%w: id %q sequence contains disallowed byte %q", ErrInvalidDocID, s, seq[i])
86 6 }
87 }
88
89 22 return DocID{Prefix: prefix, Seq: seq}, nil
90 }
91
92 // ValidateDocID reports whether s is a well-formed document ID, discarding the
93 // parse. Use it where only the verdict matters (frontmatter schema checks, push
94 // validation).
95 18 func ValidateDocID(s string) error {
96 18 _, err := ParseDocID(s)
97 18 return err
98 18 }
99
100 193 func isUpperAlpha(c byte) bool { return c >= 'A' && c <= 'Z' }
101 74 func isDigit(c byte) bool { return c >= '0' && c <= '9' }