package criteria import ( "bytes" "encoding/json" "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) var _ = Describe("Criteria", func() { var goObj Criteria var jsonObj string Context("with a complex criteria", func() { BeforeEach(func() { goObj = Criteria{ Expression: All{ Contains{"title": "love"}, NotContains{"title": "hate"}, Any{ IsNot{"artist": "u2"}, Is{"album": "best of"}, }, All{ StartsWith{"comment": "this"}, InTheRange{"year": []int{1980, 1990}}, IsNot{"genre": "Rock"}, Gt{"albumrating": 3}, }, }, Sort: "title", Order: "asc", Limit: 20, Offset: 10, } var b bytes.Buffer err := json.Compact(&b, []byte(` { "all": [ { "contains": {"title": "love"} }, { "notContains": {"title": "hate"} }, { "any": [ { "isNot": {"artist": "u2"} }, { "is": {"album": "best of"} } ] }, { "all": [ { "startsWith": {"comment": "this"} }, { "inTheRange": {"year":[1980,1990]} }, { "isNot": { "genre": "Rock" }}, { "gt": { "albumrating": 3 } } ] } ], "sort": "title", "order": "asc", "limit": 20, "offset": 10 } `)) if err != nil { panic(err) } jsonObj = b.String() }) It("marshals to JSON", func() { j, err := json.Marshal(goObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(string(j)).To(gomega.Equal(jsonObj)) }) It("is reversible to/from JSON", func() { var newObj Criteria err := json.Unmarshal([]byte(jsonObj), &newObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) j, err := json.Marshal(newObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(string(j)).To(gomega.Equal(jsonObj)) }) }) Describe("LimitPercent", func() { Describe("JSON round-trip", func() { It("marshals and unmarshals limitPercent", func() { goObj := Criteria{ Expression: All{Contains{"title": "love"}}, Sort: "title", Order: "asc", LimitPercent: 10, } j, err := json.Marshal(goObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(string(j)).To(gomega.ContainSubstring(`"limitPercent":10`)) gomega.Expect(string(j)).ToNot(gomega.ContainSubstring(`"limit"`)) var newObj Criteria err = json.Unmarshal(j, &newObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(newObj.LimitPercent).To(gomega.Equal(10)) gomega.Expect(newObj.Limit).To(gomega.Equal(0)) }) It("does not include limitPercent when zero", func() { goObj := Criteria{ Expression: All{Contains{"title": "love"}}, Limit: 50, } j, err := json.Marshal(goObj) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(string(j)).To(gomega.ContainSubstring(`"limit":50`)) gomega.Expect(string(j)).ToNot(gomega.ContainSubstring(`limitPercent`)) }) It("backward compatible: JSON with only limit still works", func() { jsonStr := `{"all":[{"contains":{"title":"love"}}],"limit":20}` var c Criteria err := json.Unmarshal([]byte(jsonStr), &c) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(c.Limit).To(gomega.Equal(20)) gomega.Expect(c.LimitPercent).To(gomega.Equal(0)) }) }) Describe("UnmarshalJSON clamping", func() { It("clamps values above 100 to 100", func() { jsonStr := `{"all":[{"contains":{"title":"love"}}],"limitPercent":150}` var c Criteria err := json.Unmarshal([]byte(jsonStr), &c) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(c.LimitPercent).To(gomega.Equal(100)) }) It("clamps negative values to 0", func() { jsonStr := `{"all":[{"contains":{"title":"love"}}],"limitPercent":-5}` var c Criteria err := json.Unmarshal([]byte(jsonStr), &c) gomega.Expect(err).ToNot(gomega.HaveOccurred()) gomega.Expect(c.LimitPercent).To(gomega.Equal(0)) }) }) Describe("EffectiveLimit", func() { It("returns fixed limit when Limit is set", func() { c := Criteria{Limit: 50, LimitPercent: 10} gomega.Expect(c.EffectiveLimit(1000)).To(gomega.Equal(50)) }) It("returns percentage-based limit", func() { c := Criteria{LimitPercent: 10} gomega.Expect(c.EffectiveLimit(450)).To(gomega.Equal(45)) }) It("returns minimum 1 when totalCount > 0 and percentage rounds to 0", func() { c := Criteria{LimitPercent: 1} gomega.Expect(c.EffectiveLimit(5)).To(gomega.Equal(1)) }) It("returns 0 when totalCount is 0", func() { c := Criteria{LimitPercent: 10} gomega.Expect(c.EffectiveLimit(0)).To(gomega.Equal(0)) }) It("returns 0 when no limit is set", func() { c := Criteria{} gomega.Expect(c.EffectiveLimit(1000)).To(gomega.Equal(0)) }) It("returns full count for 100%", func() { c := Criteria{LimitPercent: 100} gomega.Expect(c.EffectiveLimit(450)).To(gomega.Equal(450)) }) It("returns 1 for 1% of 50 items", func() { c := Criteria{LimitPercent: 1} gomega.Expect(c.EffectiveLimit(50)).To(gomega.Equal(1)) }) }) Describe("ResolveLimit", func() { It("resolves percentage to absolute limit preserving LimitPercent", func() { c := Criteria{LimitPercent: 10} c.ResolveLimit(450) gomega.Expect(c.Limit).To(gomega.Equal(45)) }) It("does nothing when Limit is already set", func() { c := Criteria{Limit: 50, LimitPercent: 10} c.ResolveLimit(1000) gomega.Expect(c.Limit).To(gomega.Equal(50)) }) It("does nothing when no limit is configured", func() { c := Criteria{} c.ResolveLimit(1000) gomega.Expect(c.Limit).To(gomega.Equal(0)) }) It("sets minimum 1 when percentage rounds to 0 and totalCount > 0", func() { c := Criteria{LimitPercent: 1} c.ResolveLimit(5) gomega.Expect(c.Limit).To(gomega.Equal(1)) }) It("is idempotent when called twice", func() { c := Criteria{LimitPercent: 10} c.ResolveLimit(450) c.ResolveLimit(450) gomega.Expect(c.Limit).To(gomega.Equal(45)) }) }) Describe("IsPercentageLimit", func() { It("returns true when LimitPercent is set and Limit is 0", func() { c := Criteria{LimitPercent: 10} gomega.Expect(c.IsPercentageLimit()).To(gomega.BeTrue()) }) It("returns false when Limit is set", func() { c := Criteria{Limit: 50, LimitPercent: 10} gomega.Expect(c.IsPercentageLimit()).To(gomega.BeFalse()) }) It("returns false when neither is set", func() { c := Criteria{} gomega.Expect(c.IsPercentageLimit()).To(gomega.BeFalse()) }) It("returns false when LimitPercent is out of range", func() { c := Criteria{LimitPercent: 150} gomega.Expect(c.IsPercentageLimit()).To(gomega.BeFalse()) }) }) }) Context("with child playlists", func() { var ( topLevelInPlaylistID string topLevelNotInPlaylistID string nestedAnyInPlaylistID string nestedAnyNotInPlaylistID string nestedAllInPlaylistID string nestedAllNotInPlaylistID string ) BeforeEach(func() { topLevelInPlaylistID = uuid.NewString() topLevelNotInPlaylistID = uuid.NewString() nestedAnyInPlaylistID = uuid.NewString() nestedAnyNotInPlaylistID = uuid.NewString() nestedAllInPlaylistID = uuid.NewString() nestedAllNotInPlaylistID = uuid.NewString() goObj = Criteria{ Expression: All{ InPlaylist{"id": topLevelInPlaylistID}, NotInPlaylist{"id": topLevelNotInPlaylistID}, Any{ InPlaylist{"id": nestedAnyInPlaylistID}, NotInPlaylist{"id": nestedAnyNotInPlaylistID}, }, All{ InPlaylist{"id": nestedAllInPlaylistID}, NotInPlaylist{"id": nestedAllNotInPlaylistID}, }, }, } }) It("extracts all child smart playlist IDs from expression criteria", func() { ids := goObj.ChildPlaylistIds() gomega.Expect(ids).To(gomega.ConsistOf(topLevelInPlaylistID, topLevelNotInPlaylistID, nestedAnyInPlaylistID, nestedAnyNotInPlaylistID, nestedAllInPlaylistID, nestedAllNotInPlaylistID)) }) It("extracts child smart playlist IDs from deeply nested expression", func() { goObj = Criteria{ Expression: Any{ Any{ All{ Any{ InPlaylist{"id": nestedAnyInPlaylistID}, NotInPlaylist{"id": nestedAnyNotInPlaylistID}, Any{ All{ InPlaylist{"id": nestedAllInPlaylistID}, NotInPlaylist{"id": nestedAllNotInPlaylistID}, }, }, }, }, }, }, } ids := goObj.ChildPlaylistIds() gomega.Expect(ids).To(gomega.ConsistOf(nestedAnyInPlaylistID, nestedAnyNotInPlaylistID, nestedAllInPlaylistID, nestedAllNotInPlaylistID)) }) It("returns empty list when no child playlist IDs are present", func() { ids := Criteria{}.ChildPlaylistIds() gomega.Expect(ids).To(gomega.BeEmpty()) }) It("returns empty list for leaf expressions", func() { ids := Criteria{Expression: Is{"title": "Low Rider"}}.ChildPlaylistIds() gomega.Expect(ids).To(gomega.BeEmpty()) }) It("deduplicates repeated playlist IDs", func() { sharedID := uuid.NewString() goObj = Criteria{ Expression: All{ InPlaylist{"id": sharedID}, Any{ InPlaylist{"id": sharedID}, NotInPlaylist{"id": sharedID}, }, }, } ids := goObj.ChildPlaylistIds() gomega.Expect(ids).To(gomega.Equal([]string{sharedID})) }) }) })