1package syntax
2
3import (
4 "errors"
5 "regexp"
6 "strings"
7)
8
9var nsidRegex = regexp.MustCompile(`^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z0-9]{0,62})?)$`)
10
11// String type which represents a syntaxtually valid Namespace Identifier (NSID), as would pass Lexicon syntax validation.
12//
13// Always use [ParseNSID] instead of wrapping strings directly, especially when working with input.
14//
15// Syntax specification: https://atproto.com/specs/nsid
16type NSID string
17
18func ParseNSID(raw string) (NSID, error) {
19 if raw == "" {
20 return "", errors.New("expected NSID, got empty string")
21 }
22 if len(raw) > 317 {
23 return "", errors.New("NSID is too long (317 chars max)")
24 }
25 if !nsidRegex.MatchString(raw) {
26 return "", errors.New("NSID syntax didn't validate via regex")
27 }
28 return NSID(raw), nil
29}
30
31// Authority domain name, in regular DNS order, not reversed order, normalized to lower-case.
32func (n NSID) Authority() string {
33 parts := strings.Split(string(n), ".")
34 if len(parts) < 2 {
35 // something has gone wrong (would not validate); return empty string instead
36 return ""
37 }
38 // NSID must have at least two parts, verified by ParseNSID
39 parts = parts[:len(parts)-1]
40 // reverse
41 for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
42 parts[i], parts[j] = parts[j], parts[i]
43 }
44 return strings.ToLower(strings.Join(parts, "."))
45}
46
47func (n NSID) Name() string {
48 parts := strings.Split(string(n), ".")
49 return parts[len(parts)-1]
50}
51
52func (n NSID) String() string {
53 return string(n)
54}
55
56func (n NSID) Normalize() NSID {
57 parts := strings.Split(string(n), ".")
58 if len(parts) < 2 {
59 // something has gone wrong (would not validate); just return the whole identifier
60 return n
61 }
62 name := parts[len(parts)-1]
63 prefix := strings.ToLower(strings.Join(parts[:len(parts)-1], "."))
64 return NSID(prefix + "." + name)
65}
66
67func (n NSID) MarshalText() ([]byte, error) {
68 return []byte(n.String()), nil
69}
70
71func (n *NSID) UnmarshalText(text []byte) error {
72 nsid, err := ParseNSID(string(text))
73 if err != nil {
74 return err
75 }
76 *n = nsid
77 return nil
78}