diff --git a/filters/filters.go b/filters/filters.go deleted file mode 100644 index ad731b3..0000000 --- a/filters/filters.go +++ /dev/null @@ -1,280 +0,0 @@ -package filters - -import ( - "encoding/json" - "fmt" - roots "git.wisehodl.dev/jay/go-roots/filters" - "strings" -) - -type HeartwoodFilter struct { - Root roots.Filter - Graph []GraphFilter -} - -type GraphFilter struct { - IDs []string - Authors []string - Kinds []json.RawMessage - Since json.RawMessage - Until json.RawMessage - Limit *int - Tags roots.TagFilters - Distance *Distance - Graph []GraphFilter - Extensions roots.FilterExtensions -} - -type Distance struct { - Min int - Max int -} - -func MarshalJSON(f HeartwoodFilter) ([]byte, error) { - rootFilter := f.Root - - if f.Graph != nil { - graphArray, err := marshalGraphArray(f.Graph) - if err != nil { - return nil, fmt.Errorf("error marshalling graph field: %w", err) - } - - graphField, err := json.Marshal(graphArray) - if err != nil { - return nil, fmt.Errorf("error marshalling graph field: %w", err) - } - - if rootFilter.Extensions == nil { - rootFilter.Extensions = make(roots.FilterExtensions) - } - - rootFilter.Extensions["graph"] = graphField - } - - return roots.MarshalJSON(rootFilter) -} - -func UnmarshalJSON(data []byte, f *HeartwoodFilter) error { - var rootsFilter roots.Filter - if err := roots.UnmarshalJSON(data, &rootsFilter); err != nil { - return err - } - - if rootsFilter.Extensions["graph"] != nil { - graphArray, err := unmarshalGraphArray(rootsFilter.Extensions["graph"]) - if err != nil { - return fmt.Errorf("error unmarshalling graph extension field: %w", err) - } - f.Graph = graphArray - delete(rootsFilter.Extensions, "graph") - } - - f.Root = rootsFilter - - return nil -} - -func MarshalGraphJSON(f GraphFilter) ([]byte, error) { - outputMap := make(map[string]interface{}) - - // Add standard fields - if f.IDs != nil { - outputMap["ids"] = f.IDs - } - if f.Authors != nil { - outputMap["authors"] = f.Authors - } - if f.Kinds != nil { - outputMap["kinds"] = f.Kinds - } - if f.Since != nil { - outputMap["since"] = f.Since - } - if f.Until != nil { - outputMap["until"] = f.Until - } - if f.Limit != nil { - outputMap["limit"] = *f.Limit - } - - // Add distance field - if f.Distance != nil { - distanceMap := make(map[string]interface{}) - distanceMap["max"] = f.Distance.Max - distanceMap["min"] = f.Distance.Min - outputMap["distance"] = distanceMap - } - - // Add nested graph field - if f.Graph != nil { - graphArray, err := marshalGraphArray(f.Graph) - if err != nil { - return nil, fmt.Errorf("error in nested graph: %w", err) - } - outputMap["graph"] = graphArray - } - - // Add tags - for key, values := range f.Tags { - outputMap["#"+key] = values - } - - // Merge extensions - for key, raw := range f.Extensions { - // Disallow standard keys in extensions - if key == "ids" || - key == "authors" || - key == "kinds" || - key == "since" || - key == "until" || - key == "limit" || - key == "distance" || - key == "graph" { - continue - } - - // Disallow tag keys in extensions - if strings.HasPrefix(key, "#") { - continue - } - - var extValue interface{} - if err := json.Unmarshal(raw, &extValue); err != nil { - return nil, err - } - outputMap[key] = extValue - } - - return json.Marshal(outputMap) -} - -func UnmarshalGraphJSON(data []byte, f *GraphFilter) error { - // Decode into raw map - raw := make(map[string]json.RawMessage) - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - // Extract standard fields - if v, ok := raw["ids"]; ok { - if err := json.Unmarshal(v, &f.IDs); err != nil { - return err - } - delete(raw, "ids") - } - - if v, ok := raw["authors"]; ok { - if err := json.Unmarshal(v, &f.Authors); err != nil { - return err - } - delete(raw, "authors") - } - - if v, ok := raw["kinds"]; ok { - if err := json.Unmarshal(v, &f.Kinds); err != nil { - return err - } - delete(raw, "kinds") - } - - if v, ok := raw["since"]; ok { - var val json.RawMessage - if err := json.Unmarshal(v, &val); err != nil { - return err - } - f.Since = val - delete(raw, "since") - } - - if v, ok := raw["until"]; ok { - var val json.RawMessage - if err := json.Unmarshal(v, &val); err != nil { - return err - } - f.Until = val - delete(raw, "until") - } - - if v, ok := raw["limit"]; ok { - if len(v) == 4 && string(v) == "null" { - f.Limit = nil - } else { - var val int - if err := json.Unmarshal(v, &val); err != nil { - return err - } - f.Limit = &val - } - delete(raw, "limit") - } - - // Extract distance field - if v, ok := raw["distance"]; ok { - if err := json.Unmarshal(v, &f.Distance); err != nil { - return err - } - delete(raw, "distance") - } - - // Extract nested graph field - if v, ok := raw["graph"]; ok { - graphArray, err := unmarshalGraphArray(v) - if err != nil { - return fmt.Errorf("error unmarshalling nested graph filter: %w", err) - } - f.Graph = graphArray - delete(raw, "graph") - } - - // Extract tag fields - for key := range raw { - if strings.HasPrefix(key, "#") { - // Leave Tags as `nil` unless tag fields exist - if f.Tags == nil { - f.Tags = make(roots.TagFilters) - } - tagKey := key[1:] - var tagValues []string - if err := json.Unmarshal(raw[key], &tagValues); err != nil { - return err - } - f.Tags[tagKey] = tagValues - delete(raw, key) - } - } - - // Place remaining fields in extensions - if len(raw) > 0 { - f.Extensions = raw - } - - return nil -} - -func marshalGraphArray(filters []GraphFilter) ([]json.RawMessage, error) { - result := make([]json.RawMessage, 0, len(filters)) - for _, f := range filters { - b, err := MarshalGraphJSON(f) - if err != nil { - return nil, err - } - result = append(result, b) - } - return result, nil -} - -func unmarshalGraphArray(raws json.RawMessage) ([]GraphFilter, error) { - var rawArray []json.RawMessage - if err := json.Unmarshal(raws, &rawArray); err != nil { - return nil, err - } - result := make([]GraphFilter, 0, len(rawArray)) - for _, raw := range rawArray { - var f GraphFilter - if err := UnmarshalGraphJSON(raw, &f); err != nil { - return nil, err - } - result = append(result, f) - } - return result, nil -} diff --git a/filters/filters_test.go b/filters/filters_test.go deleted file mode 100644 index 661a68c..0000000 --- a/filters/filters_test.go +++ /dev/null @@ -1,372 +0,0 @@ -package filters - -import ( - "encoding/json" - roots "git.wisehodl.dev/jay/go-roots/filters" - "github.com/stretchr/testify/assert" - "testing" -) - -// Helpers - -func intPtr(i int) *int { ptr := i; return &ptr } - -func expectEqualHeartwoodFilters(t *testing.T, got, want HeartwoodFilter) { - t.Helper() - assert.Equal(t, want.Root.IDs, got.Root.IDs) - assert.Equal(t, want.Root.Authors, got.Root.Authors) - assert.Equal(t, want.Root.Kinds, got.Root.Kinds) - assert.Equal(t, want.Root.Since, got.Root.Since) - assert.Equal(t, want.Root.Until, got.Root.Until) - assert.Equal(t, want.Root.Limit, got.Root.Limit) - assert.Equal(t, want.Root.Tags, got.Root.Tags) - assert.Equal(t, len(want.Graph), len(got.Graph)) - for i := range want.Graph { - expectEqualGraphFilters(t, got.Graph[i], want.Graph[i]) - } -} - -func expectEqualGraphFilters(t *testing.T, got, want GraphFilter) { - t.Helper() - assert.Equal(t, want.IDs, got.IDs) - assert.Equal(t, want.Authors, got.Authors) - assert.Equal(t, want.Kinds, got.Kinds) - assert.Equal(t, want.Since, got.Since) - assert.Equal(t, want.Until, got.Until) - assert.Equal(t, want.Limit, got.Limit) - assert.Equal(t, want.Tags, got.Tags) - assert.Equal(t, want.Distance, got.Distance) - assert.Equal(t, len(want.Graph), len(got.Graph)) - for i := range want.Graph { - expectEqualGraphFilters(t, got.Graph[i], want.Graph[i]) - } - assert.Equal(t, want.Extensions, got.Extensions) -} - -// Tests - -func TestMarshalJSON(t *testing.T) { - cases := []struct { - name string - filter HeartwoodFilter - expected string - }{ - { - name: "empty filter", - filter: HeartwoodFilter{}, - expected: `{}`, - }, - { - name: "root fields only", - filter: HeartwoodFilter{ - Root: roots.NewFilter( - roots.WithIDs([]string{"abc"}), - roots.WithKinds([]int{1}), - roots.WithSince(1000), - ), - }, - expected: `{"ids":["abc"],"kinds":[1],"since":1000}`, - }, - { - name: "empty graph field", - filter: HeartwoodFilter{ - Graph: []GraphFilter{}, - }, - expected: `{"graph":[]}`, - }, - { - name: "graph field only", - filter: HeartwoodFilter{ - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, - }, - }, - expected: `{"graph":[{"kinds":[1]}]}`, - }, - { - name: "root and graph present", - filter: HeartwoodFilter{ - Root: roots.NewFilter( - roots.WithIDs([]string{"abc"}), - ), - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, - }, - }, - expected: `{"ids":["abc"],"graph":[{"kinds":[1]}]}`, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - result, err := MarshalJSON(tc.filter) - assert.NoError(t, err) - - var expectedMap, actualMap map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(tc.expected), &expectedMap)) - assert.NoError(t, json.Unmarshal(result, &actualMap)) - assert.Equal(t, expectedMap, actualMap) - }) - } - -} - -func TestUnmarshalJSON(t *testing.T) { - cases := []struct { - name string - input string - expected HeartwoodFilter - }{ - { - name: "empty object", - input: `{}`, - expected: HeartwoodFilter{}, - }, - { - name: "root fields only", - input: `{"ids":["abc"],"kinds":[1],"since":1000}`, - expected: HeartwoodFilter{ - Root: roots.NewFilter( - roots.WithIDs([]string{"abc"}), - roots.WithKinds([]int{1}), - roots.WithSince(1000), - ), - }, - }, - { - name: "empty graph field", - input: `{"graph":[]}`, - expected: HeartwoodFilter{ - Graph: []GraphFilter{}, - }, - }, - { - name: "graph field only", - input: `{"graph":[{"kinds":[1]}]}`, - expected: HeartwoodFilter{ - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, - }, - }, - }, - { - name: "root and graph present", - input: `{"ids":["abc"],"graph":[{"kinds":[1]}]}`, - expected: HeartwoodFilter{ - Root: roots.NewFilter( - roots.WithIDs([]string{"abc"}), - ), - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, - }, - }, - }, - { - name: "graph is removed from root extensions", - input: `{"ids":["abc"],"graph":[{"kinds":[1]}],"search":"bitcoin"}`, - expected: HeartwoodFilter{ - Root: roots.NewFilter( - roots.WithIDs([]string{"abc"}), - roots.WithExtension("search", json.RawMessage(`"bitcoin"`)), - ), - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`1`)}}, - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - var result HeartwoodFilter - err := UnmarshalJSON([]byte(tc.input), &result) - assert.NoError(t, err) - expectEqualHeartwoodFilters(t, result, tc.expected) - - // Ensure graph extension was popped from root filter - assert.Nil(t, result.Root.Extensions["graph"]) - }) - } -} - -func TestMarshalGraphJSON(t *testing.T) { - cases := []struct { - name string - filter GraphFilter - expected string - }{ - { - name: "empty filter", - filter: GraphFilter{}, - expected: `{}`, - }, - { - name: "standard fields", - filter: GraphFilter{ - IDs: []string{"abc"}, - Authors: []string{"def"}, - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Since: json.RawMessage(`1000`), - Until: []byte("2000"), - Limit: intPtr(10), - }, - expected: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10}`, - }, - { - name: "tag field", - filter: GraphFilter{ - Tags: roots.TagFilters{"e": {"event1"}}, - }, - expected: `{"#e":["event1"]}`, - }, - { - name: "distance present", - filter: GraphFilter{ - Distance: &Distance{Min: 1, Max: 10}, - }, - expected: `{"distance":{"min":1,"max":10}}`, - }, - { - name: "distance absent", - filter: GraphFilter{Distance: nil}, - expected: `{}`, - }, - { - name: "empty graph", - filter: GraphFilter{ - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Graph: []GraphFilter{}, - }, - expected: `{"kinds":[1],"graph":[]}`, - }, - { - name: "nested graph", - filter: GraphFilter{ - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, - }, - }, - expected: `{"kinds":[1],"graph":[{"kinds":[7]}]}`, - }, - { - name: "extensions", - filter: GraphFilter{ - Extensions: roots.FilterExtensions{ - "search": json.RawMessage(`"abc"`), - }, - }, - expected: `{"search":"abc"}`, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - result, err := MarshalGraphJSON(tc.filter) - assert.NoError(t, err) - - var expectedMap, actualMap map[string]interface{} - assert.NoError(t, json.Unmarshal([]byte(tc.expected), &expectedMap)) - assert.NoError(t, json.Unmarshal(result, &actualMap)) - assert.Equal(t, expectedMap, actualMap) - }) - } -} - -func TestUnmarshalGraphJSON(t *testing.T) { - cases := []struct { - name string - input string - expected GraphFilter - }{ - { - name: "empty object", - input: `{}`, - expected: GraphFilter{}, - }, - { - name: "standard fields", - input: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10}`, - expected: GraphFilter{ - IDs: []string{"abc"}, - Authors: []string{"def"}, - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Since: json.RawMessage(`1000`), - Until: json.RawMessage(`2000`), - Limit: intPtr(10), - }, - }, - { - name: "tag field", - input: `{"#e":["event1"]}`, - expected: GraphFilter{ - Tags: roots.TagFilters{"e": {"event1"}}, - }, - }, - { - name: "distance present", - input: `{"distance":{"min":1,"max":10}}`, - expected: GraphFilter{ - Distance: &Distance{Min: 1, Max: 10}, - }, - }, - { - name: "distance absent", - input: `{}`, - expected: GraphFilter{Distance: nil}, - }, - { - name: "empty graph", - input: `{"kinds":[1],"graph":[]}`, - expected: GraphFilter{ - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Graph: []GraphFilter{}, - }, - }, - { - name: "nested graph", - input: `{"kinds":[1],"graph":[{"kinds":[7]}]}`, - expected: GraphFilter{ - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, - }, - }, - }, - { - name: "fully populated", - input: `{"ids":["abc"],"authors":["def"],"kinds":[1],"since":1000,"until":2000,"limit":10,"#e":["event1"],"distance":{"min":1,"max":5},"graph":[{"kinds":[7]}]}`, - expected: GraphFilter{ - IDs: []string{"abc"}, - Authors: []string{"def"}, - Kinds: []json.RawMessage{json.RawMessage(`1`)}, - Since: json.RawMessage(`1000`), - Until: json.RawMessage(`2000`), - Limit: intPtr(10), - Tags: roots.TagFilters{"e": {"event1"}}, - Distance: &Distance{Min: 1, Max: 5}, - Graph: []GraphFilter{ - {Kinds: []json.RawMessage{json.RawMessage(`7`)}}, - }, - }, - }, - { - name: "unknown fields routed to extensions", - input: `{"search":"abc"}`, - expected: GraphFilter{ - Extensions: roots.FilterExtensions{ - "search": json.RawMessage(`"abc"`), - }, - }, - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - var result GraphFilter - err := UnmarshalGraphJSON([]byte(tc.input), &result) - assert.NoError(t, err) - expectEqualGraphFilters(t, result, tc.expected) - }) - } -}