diff --git a/events/event.go b/events/event.go index bae500d..4d534a8 100644 --- a/events/event.go +++ b/events/event.go @@ -3,6 +3,10 @@ // serialization, cryptographic signatures, and subscription filters. package events +import ( + "time" +) + // Tag represents a single tag within an event as an array of strings. // The first element identifies the tag name, the second contains the value, // and subsequent elements are optional. @@ -13,9 +17,67 @@ type Tag []string type Event struct { ID string `json:"id"` PubKey string `json:"pubkey"` - CreatedAt int `json:"created_at"` + CreatedAt int64 `json:"created_at"` Kind int `json:"kind"` Tags []Tag `json:"tags"` Content string `json:"content"` Sig string `json:"sig"` } + +func NewEvent(opts ...EventOption) Event { + e := Event{Tags: make([]Tag, 0)} + for _, opt := range opts { + opt(&e) + } + return e +} + +type EventOption func(*Event) + +func WithID(id string) EventOption { + return func(e *Event) { + e.ID = id + } +} + +func WithPubKey(pk string) EventOption { + return func(e *Event) { + e.PubKey = pk + } +} + +func WithCreatedAt(t int64) EventOption { + return func(e *Event) { + e.CreatedAt = t + } +} + +func WithCreatedAtTime(t time.Time) EventOption { + return func(e *Event) { + e.CreatedAt = t.Unix() + } +} + +func WithKind(k int) EventOption { + return func(e *Event) { + e.Kind = k + } +} + +func WithTag(t Tag) EventOption { + return func(e *Event) { + e.Tags = append(e.Tags, t) + } +} + +func WithContent(c string) EventOption { + return func(e *Event) { + e.Content = c + } +} + +func WithSig(s string) EventOption { + return func(e *Event) { + e.Sig = s + } +} diff --git a/events/event_json_test.go b/events/event_json_test.go index 016bbca..702aff1 100644 --- a/events/event_json_test.go +++ b/events/event_json_test.go @@ -7,7 +7,7 @@ import ( ) func TestUnmarshalEventJSON(t *testing.T) { - event := Event{} + event := NewEvent() json.Unmarshal(testEventJSONBytes, &event) if err := Validate(event); err != nil { t.Error("unmarshalled event is invalid") @@ -22,19 +22,17 @@ func TestMarshalEventJSON(t *testing.T) { } func TestEventJSONRoundTrip(t *testing.T) { - event := Event{ - ID: "86e856d0527dd08527498cd8afd8a7d296bde37e4757a8921f034f0b344df3ad", - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: []Tag{ - {"a", "value"}, - {"b", "value", "optional"}, - {"name", "value", "optional", "optional"}, - }, - Content: testEvent.Content, - Sig: "c05fe02a9c082ff56aad2b16b5347498a21665f02f050ba086dbe6bd593c8cd448505d2831d1c0340acc1793eaf89b7c0cb21bb696c71da6b8d6b857702bb557", - } + event := NewEvent( + WithID("86e856d0527dd08527498cd8afd8a7d296bde37e4757a8921f034f0b344df3ad"), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithTag(Tag{"a", "value"}), + WithTag(Tag{"b", "value", "optional"}), + WithTag(Tag{"name", "value", "optional", "optional"}), + WithContent(testEvent.Content), + WithSig("c05fe02a9c082ff56aad2b16b5347498a21665f02f050ba086dbe6bd593c8cd448505d2831d1c0340acc1793eaf89b7c0cb21bb696c71da6b8d6b857702bb557"), + ) expectedJSON := `{"id":"86e856d0527dd08527498cd8afd8a7d296bde37e4757a8921f034f0b344df3ad","pubkey":"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef","created_at":1760740551,"kind":1,"tags":[["a","value"],["b","value","optional"],["name","value","optional","optional"]],"content":"hello world","sig":"c05fe02a9c082ff56aad2b16b5347498a21665f02f050ba086dbe6bd593c8cd448505d2831d1c0340acc1793eaf89b7c0cb21bb696c71da6b8d6b857702bb557"}` if err := Validate(event); err != nil { diff --git a/events/event_test.go b/events/event_test.go index 5be72c5..71a77df 100644 --- a/events/event_test.go +++ b/events/event_test.go @@ -8,15 +8,14 @@ import ( const testSK = "f43a0435f69529f310bbd1d6263d2fbf0977f54bfe2310cc37ae5904b83bb167" const testPK = "cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef" -var testEvent = Event{ - ID: "c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad", - PubKey: testPK, - CreatedAt: 1760740551, - Kind: 1, - Tags: []Tag{}, - Content: "hello world", - Sig: "83b71e15649c9e9da362c175f988c36404cabf357a976d869102a74451cfb8af486f6088b5631033b4927bd46cad7a0d90d7f624aefc0ac260364aa65c36071a", -} +var testEvent = NewEvent( + WithID("c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad"), + WithPubKey(testPK), + WithCreatedAt(1760740551), + WithKind(1), + WithContent("hello world"), + WithSig("83b71e15649c9e9da362c175f988c36404cabf357a976d869102a74451cfb8af486f6088b5631033b4927bd46cad7a0d90d7f624aefc0ac260364aa65c36071a"), +) var testEventJSON = `{"id":"c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad","pubkey":"cfa87f35acbde29ba1ab3ee42de527b2cad33ac487e80cf2d6405ea0042c8fef","created_at":1760740551,"kind":1,"tags":[],"content":"hello world","sig":"83b71e15649c9e9da362c175f988c36404cabf357a976d869102a74451cfb8af486f6088b5631033b4927bd46cad7a0d90d7f624aefc0ac260364aa65c36071a"}` var testEventJSONBytes = []byte(testEventJSON) diff --git a/events/id_test.go b/events/id_test.go index 7b7be3d..4fd6ef0 100644 --- a/events/id_test.go +++ b/events/id_test.go @@ -14,181 +14,161 @@ type IDTestCase struct { var idTestCases = []IDTestCase{ { name: "minimal event", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{}, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + ), expected: "13a55672a600398894592f4cb338652d4936caffe5d3718d11597582bb030c39", }, { name: "alphanumeric content", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{}, - Content: "hello world", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithContent("hello world"), + ), expected: "c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad", }, { name: "unicode content", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{}, - Content: "hello world 😀", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithContent("hello world 😀"), + ), expected: "e42083fafbf9a39f97914fd9a27cedb38c429ac3ca8814288414eaad1f472fe8", }, { name: "escaped content", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{}, - Content: "\"You say yes.\"\\n\\t\"I say no.\"", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithContent("\"You say yes.\"\\n\\t\"I say no.\""), + ), expected: "343de133996a766bf00561945b6f2b2717d4905275976ca75c1d7096b7d1900c", }, { name: "json content", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{}, - Content: "{\"field\": [\"value\",\"value\"],\"numeral\": 123}", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithContent("{\"field\": [\"value\",\"value\"],\"numeral\": 123}"), + ), expected: "c6140190453ee947efb790e70541a9d37c41604d1f29e4185da4325621ed5270", }, { name: "empty tag", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{ - {"a", ""}, - }, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithTag(Tag{"a", ""}), + WithContent(""), + ), expected: "7d3e394c75916362436f11c603b1a89b40b50817550cfe522a90d769655007a4", }, { name: "single tag", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{ - {"a", "value"}, - }, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithTag(Tag{"a", "value"}), + WithContent(""), + ), expected: "7db394e274fb893edbd9f4aa9ff189d4f3264bf1a29cef8f614e83ebf6fa19fe", }, { name: "optional tag values", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{ - {"a", "value", "optional"}, - }, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithTag(Tag{"a", "value", "optional"}), + WithContent(""), + ), expected: "656b47884200959e0c03054292c453cfc4beea00b592d92c0f557bff765e9d34", }, { name: "multiple tags", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{ - {"a", "value", "optional"}, - {"b", "another"}, - {"c", "data"}, - }, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithTag(Tag{"a", "value", "optional"}), + WithTag(Tag{"b", "another"}), + WithTag(Tag{"c", "data"}), + WithContent(""), + ), expected: "f7c27f2eacda7ece5123a4f82db56145ba59f7c9e6c5eeb88552763664506b06", }, { name: "unicode tag", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 1, - Tags: []Tag{ - {"a", "😀"}, - }, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(1), + WithTag(Tag{"a", "😀"}), + WithContent(""), + ), expected: "fd2798d165d9bf46acbe817735dc8cedacd4c42dfd9380792487d4902539e986", }, { name: "zero timestamp", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: 0, - Kind: 1, - Tags: []Tag{}, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(0), + WithKind(1), + WithContent(""), + ), expected: "9ca742f2e2eea72ad6e0277a6287e2bb16a3e47d64b8468bc98474e266cf0ec2", }, { name: "negative timestamp", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: -1760740551, - Kind: 1, - Tags: []Tag{}, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(-1760740551), + WithKind(1), + WithContent(""), + ), expected: "4740b027040bb4d0ee8e885f567a80277097da70cddd143d8a6dadf97f6faaa3", }, { name: "max int64 timestamp", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: 9223372036854775807, - Kind: 1, - Tags: []Tag{}, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(9223372036854775807), + WithKind(1), + WithContent(""), + ), expected: "b28cdd44496acb49e36c25859f0f819122829a12dc57c07612d5f44cb121d2a7", }, { name: "different kind", - event: Event{ - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: 20021, - Tags: []Tag{}, - Content: "", - }, + event: NewEvent( + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(20021), + WithContent(""), + ), expected: "995c4894c264e6b9558cb94b7b34008768d53801b99960b47298d4e3e23fadd3", }, } diff --git a/events/util_test.go b/events/util_test.go deleted file mode 100644 index bf464d9..0000000 --- a/events/util_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package events - -func intPtr(i int) *int { - return &i -} diff --git a/events/validate_test.go b/events/validate_test.go index 7097983..984123a 100644 --- a/events/validate_test.go +++ b/events/validate_test.go @@ -14,169 +14,161 @@ type ValidateEventTestCase struct { var structureTestCases = []ValidateEventTestCase{ { name: "empty pubkey", - event: Event{ - ID: testEvent.ID, - PubKey: "", - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(""), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "public key must be 64 lowercase hex characters", }, { name: "short pubkey", - event: Event{ - ID: testEvent.ID, - PubKey: "abc123", - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey("abc123"), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "public key must be 64 lowercase hex characters", }, { name: "long pubkey", - event: Event{ - ID: testEvent.ID, - PubKey: "c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48adabc", - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey("c7a702e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48adabc"), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "public key must be 64 lowercase hex characters", }, { name: "non-hex pubkey", - event: Event{ - ID: testEvent.ID, - PubKey: "zyx-!2e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad", - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey("zyx-!2e6158744ca03508bbb4c90f9dbb0d6e88fefbfaa511d5ab24b4e3c48ad"), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "public key must be 64 lowercase hex characters", }, { name: "uppercase pubkey", - event: Event{ - ID: testEvent.ID, - PubKey: "C7A702E6158744CA03508BBB4C90F9DBB0D6E88FEFBFAA511D5AB24B4E3C48AD", - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey("C7A702E6158744CA03508BBB4C90F9DBB0D6E88FEFBFAA511D5AB24B4E3C48AD"), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "public key must be 64 lowercase hex characters", }, { name: "empty id", - event: Event{ - ID: "", - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(""), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "id must be 64 hex characters", }, { name: "short id", - event: Event{ - ID: "abc123", - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID("abc123"), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "id must be 64 hex characters", }, { name: "empty signature", - event: Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: "", - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(""), + ), expectedError: "signature must be 128 hex characters", }, { name: "short signature", - event: Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: "abc123", - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig("abc123"), + ), expectedError: "signature must be 128 hex characters", }, { name: "empty tag", - event: Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: []Tag{{}}, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithTag(Tag{}), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "tags must contain at least two elements", }, { name: "single element tag", - event: Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: []Tag{{"a"}}, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithTag(Tag{"a"}), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "tags must contain at least two elements", }, { name: "one good tag, one single element tag", - event: Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: []Tag{{"a", "value"}, {"b"}}, - Content: testEvent.Content, - Sig: testEvent.Sig, - }, + event: NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithTag(Tag{"a", "value"}), + WithTag(Tag{"b"}), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ), expectedError: "tags must contain at least two elements", }, } @@ -191,37 +183,36 @@ func TestValidateEventStructure(t *testing.T) { } func TestValidateEventIDFailure(t *testing.T) { - event := Event{ - ID: "7f661c2a3c1ed67dc959d6cd968d743d5e6e334313df44724bca939e2aa42c9e", - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: testEvent.Tags, - Content: testEvent.Content, - Sig: testEvent.Sig, - } + event := NewEvent( + WithID("7f661c2a3c1ed67dc959d6cd968d743d5e6e334313df44724bca939e2aa42c9e"), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithContent(testEvent.Content), + WithSig(testEvent.Sig), + ) err := ValidateID(event) assert.ErrorContains(t, err, "does not match computed id") } func TestValidateSignature(t *testing.T) { - event := Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - Sig: testEvent.Sig, - } + event := NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithSig(testEvent.Sig), + ) err := ValidateSignature(event) assert.NoError(t, err) } func TestValidateInvalidSignature(t *testing.T) { - event := Event{ - ID: testEvent.ID, - PubKey: testEvent.PubKey, - Sig: "9e43cbcf7e828a21c53fa35371ee79bffbfd7a3063ae46fc05ec623dd3186667c57e3d006488015e19247df35eb41c61013e051aa87860e23fa5ffbd44120482", - } + event := NewEvent( + WithID(testEvent.ID), + WithPubKey(testEvent.PubKey), + WithSig("9e43cbcf7e828a21c53fa35371ee79bffbfd7a3063ae46fc05ec623dd3186667c57e3d006488015e19247df35eb41c61013e051aa87860e23fa5ffbd44120482"), + ) err := ValidateSignature(event) assert.ErrorContains(t, err, "event signature is invalid") @@ -280,7 +271,11 @@ var validateSignatureTestCases = []ValidateSignatureTestCase{ func TestValidateSignatureInvalidEventSignature(t *testing.T) { for _, tc := range validateSignatureTestCases { t.Run(tc.name, func(t *testing.T) { - event := Event{ID: tc.id, PubKey: tc.pubkey, Sig: tc.sig} + event := NewEvent( + WithID(tc.id), + WithPubKey(tc.pubkey), + WithSig(tc.sig), + ) err := ValidateSignature(event) assert.ErrorContains(t, err, tc.expectedError) }) @@ -288,18 +283,16 @@ func TestValidateSignatureInvalidEventSignature(t *testing.T) { } func TestValidateEvent(t *testing.T) { - event := Event{ - ID: "c9a0f84fcaa889654da8992105eb122eb210c8cbd58210609a5ef7e170b51400", - PubKey: testEvent.PubKey, - CreatedAt: testEvent.CreatedAt, - Kind: testEvent.Kind, - Tags: []Tag{ - {"a", "value"}, - {"b", "value", "optional"}, - }, - Content: "valid event", - Sig: "668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14", - } + event := NewEvent( + WithID("c9a0f84fcaa889654da8992105eb122eb210c8cbd58210609a5ef7e170b51400"), + WithPubKey(testEvent.PubKey), + WithCreatedAt(testEvent.CreatedAt), + WithKind(testEvent.Kind), + WithTag(Tag{"a", "value"}), + WithTag(Tag{"b", "value", "optional"}), + WithContent("valid event"), + WithSig("668a715f1eb983172acf230d17bd283daedb2598adf8de4290bcc7eb0b802fdb60669d1e7d1104ac70393f4dbccd07e8abf897152af6ce6c0a75499874e27f14"), + ) err := Validate(event) assert.NoError(t, err) diff --git a/filters/filter.go b/filters/filter.go index e5384e2..7bad6ca 100644 --- a/filters/filter.go +++ b/filters/filter.go @@ -4,6 +4,7 @@ import ( "encoding/json" "git.wisehodl.dev/jay/go-roots/events" "strings" + "time" ) // TagFilters maps tag names to arrays of values for tag-based filtering @@ -20,13 +21,94 @@ type Filter struct { IDs []string Authors []string Kinds []int - Since *int - Until *int + Since *int64 + Until *int64 Limit *int Tags TagFilters Extensions FilterExtensions } +func NewFilter(opts ...FilterOption) Filter { + f := Filter{} + for _, opt := range opts { + opt(&f) + } + return f +} + +type FilterOption func(*Filter) + +func WithIDs(ids []string) FilterOption { + return func(f *Filter) { + f.IDs = ids + } +} + +func WithAuthors(authors []string) FilterOption { + return func(f *Filter) { + f.Authors = authors + } +} + +func WithKinds(kinds []int) FilterOption { + return func(f *Filter) { + f.Kinds = kinds + } +} + +func WithSince(since int64) FilterOption { + return func(f *Filter) { + ptr := since + f.Since = &ptr + } +} + +func WithUntil(until int64) FilterOption { + return func(f *Filter) { + ptr := until + f.Until = &ptr + } +} + +func WithUntilTime(until time.Time) FilterOption { + return func(f *Filter) { + untilInt := until.Unix() + f.Until = &untilInt + } +} + +func WithSinceTime(since time.Time) FilterOption { + return func(f *Filter) { + sinceInt := since.Unix() + f.Since = &sinceInt + } +} + +func WithLimit(limit int) FilterOption { + return func(f *Filter) { + ptr := limit + f.Limit = &ptr + } +} + +func WithTag(l string, v []string) FilterOption { + return func(f *Filter) { + if f.Tags == nil { + f.Tags = make(TagFilters) + } + f.Tags[l] = v + } +} + +func WithExtension(l string, e json.RawMessage) FilterOption { + return func(f *Filter) { + if f.Extensions == nil { + f.Extensions = make(FilterExtensions) + } + f.Extensions[l] = e + } +} + // MarshalJSON converts the filter to JSON with standard fields, tag filters // (prefixed with "#"), and extensions merged into a single object. func MarshalJSON(f Filter) ([]byte, error) { @@ -119,7 +201,7 @@ func UnmarshalJSON(data []byte, f *Filter) error { if len(v) == 4 && string(v) == "null" { f.Since = nil } else { - var val int + var val int64 if err := json.Unmarshal(v, &val); err != nil { return err } @@ -132,7 +214,7 @@ func UnmarshalJSON(data []byte, f *Filter) error { if len(v) == 4 && string(v) == "null" { f.Until = nil } else { - var val int + var val int64 if err := json.Unmarshal(v, &val); err != nil { return err } @@ -237,7 +319,7 @@ func matchesKinds(candidate int, kinds []int) bool { return false } -func matchesTimeRange(timestamp int, since *int, until *int) bool { +func matchesTimeRange(timestamp int64, since *int64, until *int64) bool { if since != nil && timestamp < *since { return false } diff --git a/filters/filter_json_test.go b/filters/filter_json_test.go index 1976e11..9857b8c 100644 --- a/filters/filter_json_test.go +++ b/filters/filter_json_test.go @@ -37,57 +37,57 @@ var marshalTestCases = []FilterMarshalTestCase{ // ID cases { name: "nil IDs", - filter: Filter{IDs: nil}, + filter: NewFilter(WithIDs(nil)), expected: `{}`, }, { name: "empty IDs", - filter: Filter{IDs: []string{}}, + filter: NewFilter(WithIDs([]string{})), expected: `{"ids":[]}`, }, { name: "populated IDs", - filter: Filter{IDs: []string{"abc", "123"}}, + filter: NewFilter(WithIDs([]string{"abc", "123"})), expected: `{"ids":["abc","123"]}`, }, // Author cases { name: "nil Authors", - filter: Filter{Authors: nil}, + filter: NewFilter(WithAuthors(nil)), expected: `{}`, }, { name: "empty Authors", - filter: Filter{Authors: []string{}}, + filter: NewFilter(WithAuthors([]string{})), expected: `{"authors":[]}`, }, { name: "populated Authors", - filter: Filter{Authors: []string{"abc", "123"}}, + filter: NewFilter(WithAuthors([]string{"abc", "123"})), expected: `{"authors":["abc","123"]}`, }, // Kind cases { name: "nil Kinds", - filter: Filter{Kinds: nil}, + filter: NewFilter(WithKinds(nil)), expected: `{}`, }, { name: "empty Kinds", - filter: Filter{Kinds: []int{}}, + filter: NewFilter(WithKinds([]int{})), expected: `{"kinds":[]}`, }, { name: "populated Kinds", - filter: Filter{Kinds: []int{1, 20001}}, + filter: NewFilter(WithKinds([]int{1, 20001})), expected: `{"kinds":[1,20001]}`, }, @@ -100,7 +100,7 @@ var marshalTestCases = []FilterMarshalTestCase{ { name: "populated Since", - filter: Filter{Since: intPtr(1000)}, + filter: NewFilter(WithSince(1000)), expected: `{"since":1000}`, }, @@ -113,7 +113,7 @@ var marshalTestCases = []FilterMarshalTestCase{ { name: "populated Until", - filter: Filter{Until: intPtr(1000)}, + filter: NewFilter(WithUntil(1000)), expected: `{"until":1000}`, }, @@ -126,27 +126,31 @@ var marshalTestCases = []FilterMarshalTestCase{ { name: "populated Limit", - filter: Filter{Limit: intPtr(100)}, + filter: NewFilter(WithLimit(100)), expected: `{"limit":100}`, }, // All standard fields { name: "all standard fields", - filter: Filter{ - IDs: []string{"abc", "123"}, - Authors: []string{"def", "456"}, - Kinds: []int{1, 200, 3000}, - Since: intPtr(1000), - Until: intPtr(2000), - Limit: intPtr(100), - }, + filter: NewFilter( + WithIDs([]string{"abc", "123"}), + WithAuthors([]string{"def", "456"}), + WithKinds([]int{1, 200, 3000}), + WithSince(1000), + WithUntil(2000), + WithLimit(100), + ), expected: `{"ids":["abc","123"],"authors":["def","456"],"kinds":[1,200,3000],"since":1000,"until":2000,"limit":100}`, }, { - name: "mixed fields", - filter: Filter{IDs: nil, Authors: []string{}, Kinds: []int{1}}, + name: "mixed fields", + filter: NewFilter( + WithIDs(nil), + WithAuthors([]string{}), + WithKinds([]int{1}), + ), expected: `{"authors":[],"kinds":[1]}`, }, @@ -159,164 +163,138 @@ var marshalTestCases = []FilterMarshalTestCase{ { name: "single-letter tag", - filter: Filter{Tags: map[string][]string{ - "e": {"event1"}, - }}, + filter: NewFilter( + WithTag("e", []string{"event1"}), + ), expected: `{"#e":["event1"]}`, }, { name: "multi-letter tag", - filter: Filter{Tags: map[string][]string{ - "emoji": {"🔥", "💧"}, - }}, + filter: NewFilter( + WithTag("emoji", []string{"🔥", "💧"}), + ), expected: `{"#emoji":["🔥","💧"]}`, }, { name: "empty tag array", - filter: Filter{Tags: map[string][]string{ - "p": {}, - }}, + filter: NewFilter( + WithTag("p", []string{}), + ), expected: `{"#p":[]}`, }, { name: "multiple tags", - filter: Filter{Tags: map[string][]string{ - "e": {"event1", "event2"}, - "p": {"pubkey1", "pubkey2"}, - }}, + filter: NewFilter( + WithTag("e", []string{"event1", "event2"}), + WithTag("p", []string{"pubkey1", "pubkey2"}), + ), expected: `{"#e":["event1","event2"],"#p":["pubkey1","pubkey2"]}`, }, // Extensions { name: "simple extension", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"query"`), - }, - }, + filter: NewFilter( + WithExtension("search", json.RawMessage(`"query"`)), + ), expected: `{"search":"query"}`, }, { name: "extension with nested object", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "meta": json.RawMessage(`{"author":"alice","score":99}`), - }, - }, + filter: NewFilter( + WithExtension("meta", json.RawMessage(`{"author":"alice","score":99}`)), + ), expected: `{"meta":{"author":"alice","score":99}}`, }, { name: "extension with nested array", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "items": json.RawMessage(`[1,2,3]`), - }, - }, + filter: NewFilter( + WithExtension("items", json.RawMessage(`[1,2,3]`)), + ), expected: `{"items":[1,2,3]}`, }, { name: "extension with complex nested structure", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "data": json.RawMessage(`{"users":[{"id":1}],"count":5}`), - }, - }, + filter: NewFilter( + WithExtension("data", json.RawMessage(`{"users":[{"id":1}],"count":5}`)), + ), expected: `{"data":{"users":[{"id":1}],"count":5}}`, }, { name: "multiple extensions", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"x"`), - "depth": json.RawMessage(`3`), - }, - }, + filter: NewFilter( + WithExtension("search", json.RawMessage(`"x"`)), + WithExtension("depth", json.RawMessage(`3`)), + ), expected: `{"search":"x","depth":3}`, }, // Extension Collisions { name: "extension collides with standard field - IDs", - filter: Filter{ - IDs: []string{"real"}, - Extensions: map[string]json.RawMessage{ - "ids": json.RawMessage(`["fake"]`), - }, - }, + filter: NewFilter( + WithIDs([]string{"real"}), + WithExtension("ids", json.RawMessage(`["fake"]`)), + ), expected: `{"ids":["real"]}`, }, { name: "extension collides with standard field - Since", - filter: Filter{ - Since: intPtr(100), - Extensions: map[string]json.RawMessage{ - "since": json.RawMessage(`999`), - }, - }, + filter: NewFilter( + WithSince(100), + WithExtension("since", json.RawMessage(`999`)), + ), expected: `{"since":100}`, }, { name: "extension collides with multiple standard fields", - filter: Filter{ - Authors: []string{"a"}, - Kinds: []int{1}, - Extensions: map[string]json.RawMessage{ - "authors": json.RawMessage(`["b"]`), - "kinds": json.RawMessage(`[2]`), - }, - }, + filter: NewFilter( + WithAuthors([]string{"a"}), + WithKinds([]int{1}), + WithExtension("authors", json.RawMessage(`["b"]`)), + WithExtension("kinds", json.RawMessage(`[2]`)), + ), expected: `{"authors":["a"],"kinds":[1]}`, }, { name: "extension collides with tag field - #e", - filter: Filter{ - Extensions: map[string]json.RawMessage{ - "#e": json.RawMessage(`["fakeevent"]`), - }, - }, + filter: NewFilter( + WithExtension("#e", json.RawMessage(`["fakeevent"]`)), + ), expected: `{}`, }, { name: "extension collides with standard and tag fields", - filter: Filter{ - Authors: []string{"realauthor"}, - Tags: map[string][]string{ - "e": {"realevent"}, - }, - Extensions: map[string]json.RawMessage{ - "authors": json.RawMessage(`["fakeauthor"]`), - "#e": json.RawMessage(`["fakeevent"]`), - }, - }, + filter: NewFilter( + WithAuthors([]string{"realauthor"}), + WithTag("e", []string{"realevent"}), + WithExtension("authors", json.RawMessage(`["fakeauthor"]`)), + WithExtension("#e", json.RawMessage(`["fakeevent"]`)), + ), expected: `{"authors":["realauthor"],"#e":["realevent"]}`, }, // Kitchen Sink { name: "filter with all field types", - filter: Filter{ - IDs: []string{"x"}, - Since: intPtr(100), - Tags: map[string][]string{ - "e": {"y"}, - }, - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"z"`), - "ids": json.RawMessage(`["fakeid"]`), - }, - }, + filter: NewFilter( + WithIDs([]string{"x"}), + WithSince(100), + WithTag("e", []string{"y"}), + WithExtension("search", json.RawMessage(`"z"`)), + WithExtension("ids", json.RawMessage(`["fakeid"]`)), + ), expected: `{"ids":["x"],"since":100,"#e":["y"],"search":"z"}`, }, } @@ -325,64 +303,64 @@ var unmarshalTestCases = []FilterUnmarshalTestCase{ { name: "empty object", input: `{}`, - expected: Filter{}, + expected: NewFilter(), }, // ID cases { name: "null IDs", input: `{"ids": null}`, - expected: Filter{IDs: nil}, + expected: NewFilter(WithIDs(nil)), }, { name: "empty IDs", input: `{"ids": []}`, - expected: Filter{IDs: []string{}}, + expected: NewFilter(WithIDs([]string{})), }, { name: "populated IDs", input: `{"ids": ["abc","123"]}`, - expected: Filter{IDs: []string{"abc", "123"}}, + expected: NewFilter(WithIDs([]string{"abc", "123"})), }, // Author cases { name: "null Authors", input: `{"authors": null}`, - expected: Filter{Authors: nil}, + expected: NewFilter(WithAuthors(nil)), }, { name: "empty Authors", input: `{"authors": []}`, - expected: Filter{Authors: []string{}}, + expected: NewFilter(WithAuthors([]string{})), }, { name: "populated Authors", input: `{"authors": ["abc","123"]}`, - expected: Filter{Authors: []string{"abc", "123"}}, + expected: NewFilter(WithAuthors([]string{"abc", "123"})), }, // Kind cases { name: "null Kinds", input: `{"kinds": null}`, - expected: Filter{Kinds: nil}, + expected: NewFilter(WithKinds(nil)), }, { name: "empty Kinds", input: `{"kinds": []}`, - expected: Filter{Kinds: []int{}}, + expected: NewFilter(WithKinds([]int{})), }, { name: "populated Kinds", input: `{"kinds": [1,2,3]}`, - expected: Filter{Kinds: []int{1, 2, 3}}, + expected: NewFilter(WithKinds([]int{1, 2, 3})), }, // Since cases @@ -395,7 +373,7 @@ var unmarshalTestCases = []FilterUnmarshalTestCase{ { name: "populated Since", input: `{"since": 1000}`, - expected: Filter{Since: intPtr(1000)}, + expected: NewFilter(WithSince(1000)), }, // Until cases @@ -408,7 +386,7 @@ var unmarshalTestCases = []FilterUnmarshalTestCase{ { name: "populated Until", input: `{"until": 1000}`, - expected: Filter{Until: intPtr(1000)}, + expected: NewFilter(WithUntil(1000)), }, // Limit cases @@ -421,161 +399,146 @@ var unmarshalTestCases = []FilterUnmarshalTestCase{ { name: "populated Limit", input: `{"limit": 1000}`, - expected: Filter{Limit: intPtr(1000)}, + expected: NewFilter(WithLimit(1000)), }, // All standard fields { name: "all standard fields", input: `{"ids":["abc","123"],"authors":["def","456"],"kinds":[1,200,3000],"since":1000,"until":2000,"limit":100}`, - expected: Filter{ - IDs: []string{"abc", "123"}, - Authors: []string{"def", "456"}, - Kinds: []int{1, 200, 3000}, - Since: intPtr(1000), - Until: intPtr(2000), - Limit: intPtr(100), - }, + expected: NewFilter( + WithIDs([]string{"abc", "123"}), + WithAuthors([]string{"def", "456"}), + WithKinds([]int{1, 200, 3000}), + WithSince(1000), + WithUntil(2000), + WithLimit(100), + ), }, { - name: "mixed fields", - input: `{"ids": null, "authors": [], "kinds": [1]}`, - expected: Filter{IDs: nil, Authors: []string{}, Kinds: []int{1}}, + name: "mixed fields", + input: `{"ids": null, "authors": [], "kinds": [1]}`, + expected: NewFilter( + WithIDs(nil), + WithAuthors([]string{}), + WithKinds([]int{1}), + ), }, { name: "zero int pointers", input: `{"since": 0, "until": 0, "limit": 0}`, - expected: Filter{Since: intPtr(0), Until: intPtr(0), Limit: intPtr(0)}, + expected: NewFilter(WithSince(0), WithUntil(0), WithLimit(0)), }, // Tags { name: "single-letter tag", input: `{"#e":["event1"]}`, - expected: Filter{Tags: map[string][]string{"e": {"event1"}}}, + expected: NewFilter(WithTag("e", []string{"event1"})), }, { name: "multi-letter tag", input: `{"#emoji":["🔥","💧"]}`, - expected: Filter{Tags: map[string][]string{"emoji": {"🔥", "💧"}}}, + expected: NewFilter(WithTag("emoji", []string{"🔥", "💧"})), }, { name: "empty tag array", input: `{"#p":[]}`, - expected: Filter{Tags: map[string][]string{"p": {}}}, + expected: NewFilter(WithTag("p", []string{})), }, { name: "multiple tags", input: `{"#p":["pubkey1","pubkey2"],"#e":["event1","event2"]}`, - expected: Filter{Tags: map[string][]string{ - "p": {"pubkey1", "pubkey2"}, - "e": {"event1", "event2"}, - }}, + expected: NewFilter( + WithTag("p", []string{"pubkey1", "pubkey2"}), + WithTag("e", []string{"event1", "event2"}), + ), }, { name: "null tag", input: `{"#p":null}`, - expected: Filter{Tags: map[string][]string{"p": nil}}, + expected: NewFilter(WithTag("p", nil)), }, // Extensions { name: "simple extension", input: `{"search":"query"}`, - expected: Filter{Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"query"`), - }, - }, + expected: NewFilter( + WithExtension("search", json.RawMessage(`"query"`)), + ), }, { name: "extension with nested object", input: `{"meta":{"author":"alice","score":99}}`, - expected: Filter{ - Extensions: map[string]json.RawMessage{ - "meta": json.RawMessage(`{"author":"alice","score":99}`), - }, - }, + expected: NewFilter( + WithExtension("meta", json.RawMessage(`{"author":"alice","score":99}`)), + ), }, { name: "extension with nested array", input: `{"items":[1,2,3]}`, - expected: Filter{ - Extensions: map[string]json.RawMessage{ - "items": json.RawMessage(`[1,2,3]`), - }, - }, + expected: NewFilter( + WithExtension("items", json.RawMessage(`[1,2,3]`)), + ), }, { name: "extension with complex nested structure", input: `{"data":{"level1":{"level2":[{"id":1}]}}}`, - expected: Filter{ - Extensions: map[string]json.RawMessage{ - "data": json.RawMessage(`{"level1":{"level2":[{"id":1}]}}`), - }, - }, + expected: NewFilter( + WithExtension("data", json.RawMessage(`{"level1":{"level2":[{"id":1}]}}`)), + ), }, { name: "multiple extensions", input: `{"search":"x","custom":true,"depth":3}`, - expected: Filter{ - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"x"`), - "custom": json.RawMessage(`true`), - "depth": json.RawMessage(`3`), - }, - }, + expected: NewFilter( + WithExtension("search", json.RawMessage(`"x"`)), + WithExtension("custom", json.RawMessage(`true`)), + WithExtension("depth", json.RawMessage(`3`)), + ), }, { name: "extension with null value", input: `{"optional":null}`, - expected: Filter{ - Extensions: map[string]json.RawMessage{ - "optional": json.RawMessage(`null`), - }, - }, + expected: NewFilter( + WithExtension("optional", json.RawMessage(`null`)), + ), }, // Kitchen Sink { name: "extension with null value", input: `{"ids":["x"],"since":100,"#e":["y"],"search":"z"}`, - expected: Filter{ - IDs: []string{"x"}, - Since: intPtr(100), - Tags: map[string][]string{ - "e": {"y"}, - }, - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"z"`), - }, - }, + expected: NewFilter( + WithIDs([]string{"x"}), + WithSince(100), + WithTag("e", []string{"y"}), + WithExtension("search", json.RawMessage(`"z"`)), + ), }, } var roundTripTestCases = []FilterRoundTripTestCase{ { name: "fully populated filter", - filter: Filter{ - IDs: []string{"x"}, - Since: intPtr(100), - Tags: map[string][]string{ - "e": {"y"}, - }, - Extensions: map[string]json.RawMessage{ - "search": json.RawMessage(`"z"`), - }, - }, + filter: NewFilter( + WithIDs([]string{"x"}), + WithSince(100), + WithTag("e", []string{"y"}), + WithExtension("search", json.RawMessage(`"z"`)), + ), }, } diff --git a/filters/filter_match_test.go b/filters/filter_match_test.go index bb8d23a..ea9e2e5 100644 --- a/filters/filter_match_test.go +++ b/filters/filter_match_test.go @@ -21,7 +21,7 @@ func init() { } // Test keypairs corresponding to test events, for reference. -var ( +const ( nayru_sk = "1784be782585dfa97712afe12585d13ee608b624cf564116fa143c31a124d31e" nayru_pk = "d877e187934bd942a71221b50ff2b426bd0777991b41b6c749119805dc40bcbe" farore_sk = "03d0611c41048a9108a75bf5d023180b5cf2d2d24e2e6b83def29de977315bb3" @@ -39,7 +39,7 @@ type FilterTestCase struct { var filterTestCases = []FilterTestCase{ { name: "empty filter", - filter: Filter{}, + filter: NewFilter(), expectedIDs: []string{ "e751d41f", "562bc378", @@ -55,7 +55,7 @@ var filterTestCases = []FilterTestCase{ { name: "empty id", - filter: Filter{IDs: []string{}}, + filter: NewFilter(WithIDs([]string{})), expectedIDs: []string{ "e751d41f", "562bc378", @@ -71,31 +71,34 @@ var filterTestCases = []FilterTestCase{ { name: "single id prefix", - filter: Filter{IDs: []string{"e751d41f"}}, + filter: NewFilter(WithIDs([]string{"e751d41f"})), expectedIDs: []string{"e751d41f"}, }, { - name: "single full id", - filter: Filter{IDs: []string{"e67fa7b84df6b0bb4c57f8719149de77f58955d7849da1be10b2267c72daad8b"}}, + name: "single full id", + filter: NewFilter( + WithIDs([]string{ + "e67fa7b84df6b0bb4c57f8719149de77f58955d7849da1be10b2267c72daad8b"}), + ), expectedIDs: []string{"e67fa7b8"}, }, { name: "multiple id prefixes", - filter: Filter{IDs: []string{"562bc378", "5e4c64f1"}}, + filter: NewFilter(WithIDs([]string{"562bc378", "5e4c64f1"})), expectedIDs: []string{"562bc378", "5e4c64f1"}, }, { name: "no id match", - filter: Filter{IDs: []string{"ffff"}}, + filter: NewFilter(WithIDs([]string{"ffff"})), expectedIDs: []string{}, }, { name: "empty author", - filter: Filter{Authors: []string{}}, + filter: NewFilter(WithAuthors([]string{})), expectedIDs: []string{ "e751d41f", "562bc378", @@ -111,13 +114,13 @@ var filterTestCases = []FilterTestCase{ { name: "single author prefix", - filter: Filter{Authors: []string{"d877e187"}}, + filter: NewFilter(WithAuthors([]string{"d877e187"})), expectedIDs: []string{"e751d41f", "562bc378", "e67fa7b8"}, }, { name: "multiple author prefixex", - filter: Filter{Authors: []string{"d877e187", "9e4b726a"}}, + filter: NewFilter(WithAuthors([]string{"d877e187", "9e4b726a"})), expectedIDs: []string{ "e751d41f", "562bc378", @@ -129,20 +132,23 @@ var filterTestCases = []FilterTestCase{ }, { - name: "single author full", - filter: Filter{Authors: []string{"d877e187934bd942a71221b50ff2b426bd0777991b41b6c749119805dc40bcbe"}}, + name: "single author full", + filter: NewFilter( + WithAuthors([]string{ + "d877e187934bd942a71221b50ff2b426bd0777991b41b6c749119805dc40bcbe"}), + ), expectedIDs: []string{"e751d41f", "562bc378", "e67fa7b8"}, }, { name: "no author match", - filter: Filter{Authors: []string{"ffff"}}, + filter: NewFilter(WithAuthors([]string{"ffff"})), expectedIDs: []string{}, }, { name: "empty kind", - filter: Filter{Kinds: []int{}}, + filter: NewFilter(WithKinds([]int{})), expectedIDs: []string{ "e751d41f", "562bc378", @@ -158,13 +164,13 @@ var filterTestCases = []FilterTestCase{ { name: "single kind", - filter: Filter{Kinds: []int{1}}, + filter: NewFilter(WithKinds([]int{1})), expectedIDs: []string{"562bc378", "7a5d83d4", "4b03b69a"}, }, { name: "multiple kinds", - filter: Filter{Kinds: []int{0, 2}}, + filter: NewFilter(WithKinds([]int{0, 2})), expectedIDs: []string{ "e751d41f", "e67fa7b8", @@ -177,13 +183,13 @@ var filterTestCases = []FilterTestCase{ { name: "no kind match", - filter: Filter{Kinds: []int{99}}, + filter: NewFilter(WithKinds([]int{99})), expectedIDs: []string{}, }, { name: "since only", - filter: Filter{Since: intPtr(5000)}, + filter: NewFilter(WithSince(5000)), expectedIDs: []string{ "7a5d83d4", "3a122100", @@ -195,7 +201,7 @@ var filterTestCases = []FilterTestCase{ { name: "until only", - filter: Filter{Until: intPtr(3000)}, + filter: NewFilter(WithUntil(3000)), expectedIDs: []string{ "e751d41f", "562bc378", @@ -204,11 +210,8 @@ var filterTestCases = []FilterTestCase{ }, { - name: "time range", - filter: Filter{ - Since: intPtr(4000), - Until: intPtr(6000), - }, + name: "time range", + filter: NewFilter(WithSince(4000), WithUntil(6000)), expectedIDs: []string{ "5e4c64f1", "7a5d83d4", @@ -217,20 +220,14 @@ var filterTestCases = []FilterTestCase{ }, { - name: "outside time range", - filter: Filter{ - Since: intPtr(10000), - }, + name: "outside time range", + filter: NewFilter(WithSince(10000)), expectedIDs: []string{}, }, { - name: "empty tag filter", - filter: Filter{ - Tags: TagFilters{ - "e": {}, - }, - }, + name: "empty tag filter", + filter: NewFilter(WithTag("e", []string{})), expectedIDs: []string{ "e751d41f", "562bc378", @@ -246,97 +243,85 @@ var filterTestCases = []FilterTestCase{ { name: "single letter tag filter: e", - filter: Filter{ - Tags: TagFilters{ - "e": {"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}, - }, - }, + filter: NewFilter( + WithTag("e", []string{ + "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36"}), + ), expectedIDs: []string{"562bc378"}, }, { name: "multiple tag matches", - filter: Filter{ - Tags: TagFilters{ - "e": { - "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", - "ae3f2a91b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7", - }, - }, - }, + filter: NewFilter( + WithTag("e", []string{ + "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", + "ae3f2a91b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7", + }), + ), expectedIDs: []string{"562bc378", "3a122100"}, }, { name: "multiple tag matches - single event match", - filter: Filter{ - Tags: TagFilters{ - "e": { - "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", - "cb7787c460a79187d6a13e75a0f19240e05fafca8ea42288f5765773ea69cf2f", - }, - }, - }, + filter: NewFilter( + WithTag("e", []string{ + "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", + "cb7787c460a79187d6a13e75a0f19240e05fafca8ea42288f5765773ea69cf2f", + }), + ), expectedIDs: []string{"562bc378"}, }, { name: "single letter tag filter: p", - filter: Filter{ - Tags: TagFilters{ - "p": {"91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"}, - }, - }, + filter: NewFilter( + WithTag("p", []string{ + "91cf9b32f3735070f46c0a86a820a47efa08a5be6c9f4f8cf68e5b5b75c92d60"}), + ), expectedIDs: []string{"e67fa7b8"}, }, { name: "multi letter tag filter", - filter: Filter{ - Tags: TagFilters{ - "emoji": {"🌊"}, - }, - }, + filter: NewFilter( + WithTag("emoji", []string{"🌊"}), + ), expectedIDs: []string{"e67fa7b8"}, }, { name: "multiple tag filters", - filter: Filter{ - Tags: TagFilters{ - "e": {"ae3f2a91b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7"}, - "p": {"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}, - }, - }, + filter: NewFilter( + WithTag("e", []string{ + "ae3f2a91b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7e9a1c5b4d8f2e7a9b6c3d8f7"}), + WithTag("p", []string{ + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"}), + ), expectedIDs: []string{"3a122100"}, }, { name: "prefix tag filter", - filter: Filter{ - Tags: TagFilters{ - "p": {"ae3f2a91"}, - }, - }, + filter: NewFilter( + WithTag("p", []string{"ae3f2a91"}), + ), expectedIDs: []string{}, }, { name: "unknown tag filter", - filter: Filter{ - Tags: TagFilters{ - "z": {"anything"}, - }, - }, + filter: NewFilter( + WithTag("z", []string{"anything"}), + ), expectedIDs: []string{}, }, { name: "combined author+kind tag filter", - filter: Filter{ - Authors: []string{"d877e187"}, - Kinds: []int{1, 2}, - }, + filter: NewFilter( + WithAuthors([]string{"d877e187"}), + WithKinds([]int{1, 2}), + ), expectedIDs: []string{ "562bc378", "e67fa7b8", @@ -345,11 +330,11 @@ var filterTestCases = []FilterTestCase{ { name: "combined kind+time range tag filter", - filter: Filter{ - Kinds: []int{0}, - Since: intPtr(2000), - Until: intPtr(7000), - }, + filter: NewFilter( + WithKinds([]int{0}), + WithSince(2000), + WithUntil(7000), + ), expectedIDs: []string{ "5e4c64f1", "4a15d963", @@ -358,12 +343,10 @@ var filterTestCases = []FilterTestCase{ { name: "combined author+tag tag filter", - filter: Filter{ - Authors: []string{"e719e8f8"}, - Tags: TagFilters{ - "power": {"fire"}, - }, - }, + filter: NewFilter( + WithAuthors([]string{"e719e8f8"}), + WithTag("power", []string{"fire"}), + ), expectedIDs: []string{ "4a15d963", }, @@ -371,15 +354,13 @@ var filterTestCases = []FilterTestCase{ { name: "combined tag filter", - filter: Filter{ - Authors: []string{"e719e8f8"}, - Kinds: []int{0}, - Since: intPtr(5000), - Until: intPtr(10000), - Tags: TagFilters{ - "power": {"fire"}, - }, - }, + filter: NewFilter( + WithAuthors([]string{"e719e8f8"}), + WithKinds([]int{0}), + WithSince(5000), + WithUntil(10000), + WithTag("power", []string{"fire"}), + ), expectedIDs: []string{ "4a15d963", }, @@ -404,17 +385,13 @@ func TestEventFilterMatching(t *testing.T) { // TestEventFilterMatchingSkipMalformedTags documents that filter.Matches() // skips malformed tags during tag matching func TestEventFilterMatchingSkipMalformedTags(t *testing.T) { - event := events.Event{ - Tags: []events.Tag{ - {"malformed"}, - {"valid", "value"}, - }, - } - filter := Filter{ - Tags: TagFilters{ - "valid": {"value"}, - }, - } + event := events.NewEvent( + events.WithTag(events.Tag{"malformed"}), + events.WithTag(events.Tag{"valid", "value"}), + ) + filter := NewFilter( + WithTag("valid", []string{"value"}), + ) assert.True(t, Matches(filter, event)) } diff --git a/filters/util_test.go b/filters/util_test.go deleted file mode 100644 index 0d406d8..0000000 --- a/filters/util_test.go +++ /dev/null @@ -1,5 +0,0 @@ -package filters - -func intPtr(i int) *int { - return &i -}