From 622f66e376c76db55a9121857a0fc5521b438a2e Mon Sep 17 00:00:00 2001 From: Jay Date: Fri, 8 May 2026 09:58:04 -0400 Subject: [PATCH] restructure package. Change label extraction strategy. --- envelope/enclose.go => enclose.go | 0 envelope/enclose_test.go => enclose_test.go | 0 envelope.go | 77 +++++++++++++++++++ envelope/envelope.go | 55 ------------- envelope/envelope_test.go => envelope_test.go | 23 +++--- errors.go | 33 ++++++++ errors/errors.go | 33 -------- envelope/find.go => find.go | 27 ++++--- envelope/find_test.go => find_test.go | 63 ++++++++------- roots-ws.go | 18 ----- roots-ws_test.go | 21 ----- 11 files changed, 164 insertions(+), 186 deletions(-) rename envelope/enclose.go => enclose.go (100%) rename envelope/enclose_test.go => enclose_test.go (100%) create mode 100644 envelope.go delete mode 100644 envelope/envelope.go rename envelope/envelope_test.go => envelope_test.go (81%) create mode 100644 errors.go delete mode 100644 errors/errors.go rename envelope/find.go => find.go (88%) rename envelope/find_test.go => find_test.go (91%) delete mode 100644 roots-ws.go delete mode 100644 roots-ws_test.go diff --git a/envelope/enclose.go b/enclose.go similarity index 100% rename from envelope/enclose.go rename to enclose.go diff --git a/envelope/enclose_test.go b/enclose_test.go similarity index 100% rename from envelope/enclose_test.go rename to enclose_test.go diff --git a/envelope.go b/envelope.go new file mode 100644 index 0000000..088cbc2 --- /dev/null +++ b/envelope.go @@ -0,0 +1,77 @@ +// Package envelope provides types and functions for working with Nostr protocol +// websocket messages. It defines the Envelope type representing a Nostr message +// and offers utilities for creating, parsing, and validating standardized message +// formats. +package envelope + +// Envelope represents a Nostr websocket message. +type Envelope []byte + +// GetLabel extracts the message label from an envelope. +// Returns the label as a byte slice or an error if the envelope is malformed. +func GetLabel(env Envelope) ([]byte, error) { + // begin walking byte slice + i, n := 0, len(env) + + // skip whitespace before '[' + for i < n && isSpace(env[i]) { + i++ + } + + // expect '[' + if i >= n || env[i] != '[' { + return nil, ErrInvalidEnvelope + } + i++ + + // skip whitespace before '"' + for i < n && isSpace(env[i]) { + i++ + } + + // expect '"' + if i >= n || env[i] != '"' { + return nil, ErrInvalidEnvelope + } + i++ + + // scan label: [A-Z]+ terminated by '"' + start := i + for i < n && env[i] != '"' { // do until end or '"' is reached + if env[i] < 'A' || env[i] > 'Z' { // character is not [A-Z] + return nil, ErrInvalidEnvelope + } + i++ + } + + if i == n || i == start { // a label was not found in the previous loop + return nil, ErrInvalidEnvelope + } + + return env[start:i], nil +} + +func isSpace(b byte) bool { + return b == ' ' || b == '\t' || b == '\n' || b == '\r' +} + +// GetStandardLabels returns a set of standard Nostr websocket message labels +func GetStandardLabels() map[string]struct{} { + return map[string]struct{}{ + "EVENT": {}, + "REQ": {}, + "CLOSE": {}, + "CLOSED": {}, + "EOSE": {}, + "NOTICE": {}, + "OK": {}, + "AUTH": {}, + } +} + +// IsStandardLabel checks if the given label is a standard Nostr websocket message label +func IsStandardLabel(label string) bool { + labels := GetStandardLabels() + _, ok := labels[label] + return ok +} diff --git a/envelope/envelope.go b/envelope/envelope.go deleted file mode 100644 index 3da3ed2..0000000 --- a/envelope/envelope.go +++ /dev/null @@ -1,55 +0,0 @@ -// Package envelope provides types and functions for working with Nostr protocol -// websocket messages. It defines the Envelope type representing a Nostr message -// and offers utilities for creating, parsing, and validating standardized message -// formats. -package envelope - -import ( - "encoding/json" - "fmt" - "git.wisehodl.dev/jay/go-roots-ws/errors" -) - -// Envelope represents a Nostr websocket message. -type Envelope []byte - -// GetLabel extracts the message label from an envelope. -// Returns the label as a string or an error if the envelope is malformed. -func GetLabel(env Envelope) (string, error) { - var arr []json.RawMessage - if err := json.Unmarshal(env, &arr); err != nil { - return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) - } - - if len(arr) < 1 { - return "", fmt.Errorf("%w: empty envelope", errors.InvalidEnvelope) - } - - var label string - if err := json.Unmarshal(arr[0], &label); err != nil { - return "", fmt.Errorf("%w: label is not a string", errors.WrongFieldType) - } - - return label, nil -} - -// GetStandardLabels returns a set of standard Nostr websocket message labels -func GetStandardLabels() map[string]struct{} { - return map[string]struct{}{ - "EVENT": {}, - "REQ": {}, - "CLOSE": {}, - "CLOSED": {}, - "EOSE": {}, - "NOTICE": {}, - "OK": {}, - "AUTH": {}, - } -} - -// IsStandardLabel checks if the given label is a standard Nostr websocket message label -func IsStandardLabel(label string) bool { - labels := GetStandardLabels() - _, ok := labels[label] - return ok -} diff --git a/envelope/envelope_test.go b/envelope_test.go similarity index 81% rename from envelope/envelope_test.go rename to envelope_test.go index 2e52c56..9216c53 100644 --- a/envelope/envelope_test.go +++ b/envelope_test.go @@ -1,7 +1,6 @@ package envelope import ( - "git.wisehodl.dev/jay/go-roots-ws/errors" "github.com/stretchr/testify/assert" "testing" ) @@ -10,36 +9,34 @@ func TestGetLabel(t *testing.T) { cases := []struct { name string env Envelope - wantLabel string + wantLabel []byte wantErr error wantErrText string }{ { name: "valid envelope with EVENT label", env: []byte(`["EVENT",{"id":"abc123"}]`), - wantLabel: "EVENT", + wantLabel: []byte("EVENT"), }, { name: "valid envelope with custom label", env: []byte(`["TEST",{"data":"value"}]`), - wantLabel: "TEST", + wantLabel: []byte("TEST"), }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidEnvelope, }, { - name: "empty array", - env: []byte(`[]`), - wantErr: errors.InvalidEnvelope, - wantErrText: "empty envelope", + name: "empty array", + env: []byte(`[]`), + wantErr: ErrInvalidEnvelope, }, { - name: "label not a string", - env: []byte(`[123,{"id":"abc123"}]`), - wantErr: errors.WrongFieldType, - wantErrText: "label is not a string", + name: "label not a string", + env: []byte(`[123,{"id":"abc123"}]`), + wantErr: ErrInvalidEnvelope, }, } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..29b7837 --- /dev/null +++ b/errors.go @@ -0,0 +1,33 @@ +// Package errors defines standard error types used throughout the roots-ws library. +package envelope + +import ( + "errors" +) + +var ( + // Data Structure Errors + + // ErrInvalidJSON indicates that a byte sequence could not be parsed as valid JSON. + // This is typically returned when unmarshaling fails during envelope processing. + ErrInvalidJSON = errors.New("invalid JSON") + + // ErrMissingField indicates that a required field is absent from a data structure. + // This is returned when validating that all mandatory components are present. + ErrMissingField = errors.New("missing required field") + + // ErrWrongFieldType indicates that a field's type does not match the expected type. + // This is returned when unmarshaling a specific value fails due to type mismatch. + ErrWrongFieldType = errors.New("wrong field type") + + // Envelope Errors + + // ErrInvalidEnvelope indicates that a message does not conform to the Nostr envelope structure. + // This typically occurs when an array has incorrect number of elements for its message type. + ErrInvalidEnvelope = errors.New("invalid envelope format") + + // ErrWrongEnvelopeLabel indicates that an envelope's label does not match the expected type. + // This is returned when attempting to parse an envelope using a Find function that + // expects a different label than what was provided. + ErrWrongEnvelopeLabel = errors.New("wrong envelope label") +) diff --git a/errors/errors.go b/errors/errors.go deleted file mode 100644 index 16783c5..0000000 --- a/errors/errors.go +++ /dev/null @@ -1,33 +0,0 @@ -// Package errors defines standard error types used throughout the roots-ws library. -package errors - -import ( - "errors" -) - -var ( - // Data Structure Errors - - // InvalidJSON indicates that a byte sequence could not be parsed as valid JSON. - // This is typically returned when unmarshaling fails during envelope processing. - InvalidJSON = errors.New("invalid JSON") - - // MissingField indicates that a required field is absent from a data structure. - // This is returned when validating that all mandatory components are present. - MissingField = errors.New("missing required field") - - // WrongFieldType indicates that a field's type does not match the expected type. - // This is returned when unmarshaling a specific value fails due to type mismatch. - WrongFieldType = errors.New("wrong field type") - - // Envelope Errors - - // InvalidEnvelope indicates that a message does not conform to the Nostr envelope structure. - // This typically occurs when an array has incorrect number of elements for its message type. - InvalidEnvelope = errors.New("invalid envelope format") - - // WrongEnvelopeLabel indicates that an envelope's label does not match the expected type. - // This is returned when attempting to parse an envelope using a Find function that - // expects a different label than what was provided. - WrongEnvelopeLabel = errors.New("wrong envelope label") -) diff --git a/envelope/find.go b/find.go similarity index 88% rename from envelope/find.go rename to find.go index 2333383..41aa67b 100644 --- a/envelope/find.go +++ b/find.go @@ -3,14 +3,13 @@ package envelope import ( "encoding/json" "fmt" - "git.wisehodl.dev/jay/go-roots-ws/errors" ) // CheckArrayLength is a helper function that ensures the JSON array has at // least the minimum length required func CheckArrayLength(arr []json.RawMessage, minLen int) error { if len(arr) < minLen { - return fmt.Errorf("%w: expected %d elements, got %d", errors.InvalidEnvelope, minLen, len(arr)) + return fmt.Errorf("%w: expected %d elements, got %d", ErrInvalidEnvelope, minLen, len(arr)) } return nil } @@ -19,7 +18,7 @@ func CheckArrayLength(arr []json.RawMessage, minLen int) error { // matches the expected one func CheckLabel(got, want string) error { if got != want { - return fmt.Errorf("%w: expected %s, got %s", errors.WrongEnvelopeLabel, want, got) + return fmt.Errorf("%w: expected %s, got %s", ErrWrongEnvelopeLabel, want, got) } return nil } @@ -28,7 +27,7 @@ func CheckLabel(got, want string) error { // provided value func ParseElement(element json.RawMessage, value interface{}, position string) error { if err := json.Unmarshal(element, value); err != nil { - return fmt.Errorf("%w: %s is not the expected type", errors.WrongFieldType, position) + return fmt.Errorf("%w: %s is not the expected type", ErrWrongFieldType, position) } return nil } @@ -38,7 +37,7 @@ func ParseElement(element json.RawMessage, value interface{}, position string) e func FindEvent(env Envelope) ([]byte, error) { var arr []json.RawMessage if err := json.Unmarshal(env, &arr); err != nil { - return nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err := CheckArrayLength(arr, 2); err != nil { @@ -62,7 +61,7 @@ func FindEvent(env Envelope) ([]byte, error) { func FindSubscriptionEvent(env Envelope) (subID string, event []byte, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 3); err != nil { @@ -90,7 +89,7 @@ func FindSubscriptionEvent(env Envelope) (subID string, event []byte, err error) func FindOK(env Envelope) (eventID string, status bool, message string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", false, "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", false, "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 4); err != nil { @@ -126,7 +125,7 @@ func FindOK(env Envelope) (eventID string, status bool, message string, err erro func FindReq(env Envelope) (subID string, filters [][]byte, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { @@ -159,7 +158,7 @@ func FindReq(env Envelope) (subID string, filters [][]byte, err error) { func FindEOSE(env Envelope) (subID string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { @@ -187,7 +186,7 @@ func FindEOSE(env Envelope) (subID string, err error) { func FindClose(env Envelope) (subID string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { @@ -215,7 +214,7 @@ func FindClose(env Envelope) (subID string, err error) { func FindClosed(env Envelope) (subID string, message string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 3); err != nil { @@ -247,7 +246,7 @@ func FindClosed(env Envelope) (subID string, message string, err error) { func FindNotice(env Envelope) (message string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { @@ -275,7 +274,7 @@ func FindNotice(env Envelope) (message string, err error) { func FindAuthChallenge(env Envelope) (challenge string, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return "", fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return "", fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { @@ -304,7 +303,7 @@ func FindAuthChallenge(env Envelope) (challenge string, err error) { func FindAuthResponse(env Envelope) (event []byte, err error) { var arr []json.RawMessage if err = json.Unmarshal(env, &arr); err != nil { - return nil, fmt.Errorf("%w: %v", errors.InvalidJSON, err) + return nil, fmt.Errorf("%w: %v", ErrInvalidJSON, err) } if err = CheckArrayLength(arr, 2); err != nil { diff --git a/envelope/find_test.go b/find_test.go similarity index 91% rename from envelope/find_test.go rename to find_test.go index 2160bd3..ec7fa47 100644 --- a/envelope/find_test.go +++ b/find_test.go @@ -3,7 +3,6 @@ package envelope import ( "testing" - "git.wisehodl.dev/jay/go-roots-ws/errors" "github.com/stretchr/testify/assert" ) @@ -23,18 +22,18 @@ func TestFindEvent(t *testing.T) { { name: "wrong label", env: []byte(`["REQ",{"id":"abc123","kind":1}]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected EVENT, got REQ", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["EVENT"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { @@ -83,18 +82,18 @@ func TestFindSubscriptionEvent(t *testing.T) { { name: "wrong label", env: []byte(`["REQ","sub1",{"id":"abc123","kind":1}]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected EVENT, got REQ", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["EVENT","sub1"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 3 elements, got 2", }, { @@ -154,24 +153,24 @@ func TestFindOK(t *testing.T) { { name: "wrong status type", env: []byte(`["OK","abc123","ok","Event accepted"]`), - wantErr: errors.WrongFieldType, + wantErr: ErrWrongFieldType, wantErrText: "status is not the expected type", }, { name: "wrong label", env: []byte(`["EVENT","abc123",true,"Event accepted"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected OK, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["OK","abc123",true]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 4 elements, got 3", }, { @@ -239,18 +238,18 @@ func TestFindReq(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","sub1",{"kinds":[1],"limit":10}]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected REQ, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["REQ"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, } @@ -293,18 +292,18 @@ func TestFindEOSE(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","sub1"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected EOSE, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["EOSE"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { @@ -351,18 +350,18 @@ func TestFindClose(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","sub1"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected CLOSE, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["CLOSE"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { @@ -411,18 +410,18 @@ func TestFindClosed(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","sub1","Subscription complete"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected CLOSED, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["CLOSED","sub1"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 3 elements, got 2", }, { @@ -471,18 +470,18 @@ func TestFindNotice(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","This is a notice"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected NOTICE, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["NOTICE"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { @@ -529,18 +528,18 @@ func TestFindAuthChallenge(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT","random-challenge-string"]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected AUTH, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["AUTH"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { @@ -587,18 +586,18 @@ func TestFindAuthResponse(t *testing.T) { { name: "wrong label", env: []byte(`["EVENT",{"id":"abc123","kind":22242}]`), - wantErr: errors.WrongEnvelopeLabel, + wantErr: ErrWrongEnvelopeLabel, wantErrText: "expected AUTH, got EVENT", }, { name: "invalid json", env: []byte(`invalid`), - wantErr: errors.InvalidJSON, + wantErr: ErrInvalidJSON, }, { name: "missing elements", env: []byte(`["AUTH"]`), - wantErr: errors.InvalidEnvelope, + wantErr: ErrInvalidEnvelope, wantErrText: "expected 2 elements, got 1", }, { diff --git a/roots-ws.go b/roots-ws.go deleted file mode 100644 index 4b201e7..0000000 --- a/roots-ws.go +++ /dev/null @@ -1,18 +0,0 @@ -package roots_ws - -// ConnectionStatus represents the current state of a WebSocket connection. -type ConnectionStatus int - -const ( - // StatusDisconnected indicates the connection is not active and no connection attempt is in progress. - StatusDisconnected ConnectionStatus = iota - - // StatusConnecting indicates a connection attempt is currently in progress but not yet established. - StatusConnecting - - // StatusConnected indicates the connection is active and ready for message exchange. - StatusConnected - - // StatusClosing indicates the connection is in the process of shutting down gracefully. - StatusClosing -) diff --git a/roots-ws_test.go b/roots-ws_test.go deleted file mode 100644 index c79d8f6..0000000 --- a/roots-ws_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package roots_ws - -import "testing" - -func TestConnectionStatusConstants(t *testing.T) { - seen := make(map[ConnectionStatus]bool) - - constants := []ConnectionStatus{ - StatusDisconnected, - StatusConnecting, - StatusConnected, - StatusClosing, - } - - for i, status := range constants { - if seen[status] { - t.Errorf("Duplicate value found for constant at index %d: %d", i, status) - } - seen[status] = true - } -}