From 6cf2b6eb0af726b1ba6fdf24372182faae1edea3 Mon Sep 17 00:00:00 2001 From: bryan newbold Date: Wed, 16 Aug 2023 19:22:46 -0700 Subject: [PATCH] syntax: add RecordKey type --- atproto/syntax/recordkey.go | 31 ++++++++++ atproto/syntax/recordkey_test.go | 59 +++++++++++++++++++ .../testdata/recordkey_syntax_invalid.txt | 14 +++++ .../testdata/recordkey_syntax_valid.txt | 5 ++ 4 files changed, 109 insertions(+) create mode 100644 atproto/syntax/recordkey.go create mode 100644 atproto/syntax/recordkey_test.go create mode 100644 atproto/syntax/testdata/recordkey_syntax_invalid.txt create mode 100644 atproto/syntax/testdata/recordkey_syntax_valid.txt diff --git a/atproto/syntax/recordkey.go b/atproto/syntax/recordkey.go new file mode 100644 index 00000000..5de4724a --- /dev/null +++ b/atproto/syntax/recordkey.go @@ -0,0 +1,31 @@ +package syntax + +import ( + "fmt" + "regexp" +) + +// String type which represents a syntaxtually valid RecordKey identifier, as could be included in an AT URI +// +// Always use [ParseRecordKey] instead of wrapping strings directly, especially when working with input. +// +// Syntax specification: https://atproto.com/specs/record-key +type RecordKey string + +func ParseRecordKey(raw string) (RecordKey, error) { + if len(raw) > 253 { + return "", fmt.Errorf("recordkey is too long (512 chars max)") + } + if raw == "" || raw == "." || raw == ".." { + return "", fmt.Errorf("recordkey can not be empty, '.', or '..'") + } + var recordkeyRegex = regexp.MustCompile(`^[a-zA-Z0-9_~.-]{1,512}$`) + if !recordkeyRegex.MatchString(raw) { + return "", fmt.Errorf("recordkey syntax didn't validate via regex") + } + return RecordKey(raw), nil +} + +func (h RecordKey) String() string { + return string(h) +} diff --git a/atproto/syntax/recordkey_test.go b/atproto/syntax/recordkey_test.go new file mode 100644 index 00000000..f25cb36c --- /dev/null +++ b/atproto/syntax/recordkey_test.go @@ -0,0 +1,59 @@ +package syntax + +import ( + "bufio" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInteropRecordKeysValid(t *testing.T) { + assert := assert.New(t) + file, err := os.Open("testdata/recordkey_syntax_valid.txt") + assert.NoError(err) + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || line[0] == '#' { + continue + } + _, err := ParseRecordKey(line) + if err != nil { + fmt.Println("GOOD: " + line) + } else { + fmt.Println("FAIL: " + line) + } + assert.NoError(err) + } + assert.NoError(scanner.Err()) +} + +func TestInteropRecordKeysInvalid(t *testing.T) { + assert := assert.New(t) + file, err := os.Open("testdata/recordkey_syntax_invalid.txt") + assert.NoError(err) + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + if len(line) == 0 || line[0] == '#' { + continue + } + _, err := ParseRecordKey(line) + if err == nil { + fmt.Println("BAD: " + line) + } + assert.Error(err) + } + assert.NoError(scanner.Err()) +} + +func TestRecordKeyNoPanic(t *testing.T) { + for _, s := range []string{"", "a", ".", ".."} { + bad := RecordKey(s) + _ = bad.String() + } +} diff --git a/atproto/syntax/testdata/recordkey_syntax_invalid.txt b/atproto/syntax/testdata/recordkey_syntax_invalid.txt new file mode 100644 index 00000000..1da3d1e7 --- /dev/null +++ b/atproto/syntax/testdata/recordkey_syntax_invalid.txt @@ -0,0 +1,14 @@ +# specs +literal:self +alpha/beta +. +.. +#extra +@handle +any space +any+space +number[3] +number(3) +"quote" +pre:fix +dHJ1ZQ== diff --git a/atproto/syntax/testdata/recordkey_syntax_valid.txt b/atproto/syntax/testdata/recordkey_syntax_valid.txt new file mode 100644 index 00000000..4ba39689 --- /dev/null +++ b/atproto/syntax/testdata/recordkey_syntax_valid.txt @@ -0,0 +1,5 @@ +# specs +self +example.com +~1.2-3_ +dHJ1ZQ -- 2.51.2