Skip to content

Commit 7c7fc02

Browse files
committed
Add array-contains-all filtering for categories and ordinal order for complexity
1 parent 8d23dde commit 7c7fc02

File tree

4 files changed

+196
-77
lines changed

4 files changed

+196
-77
lines changed

services/question-service/handlers/list.go

Lines changed: 110 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package handlers
33
import (
44
"cloud.google.com/go/firestore"
55
"encoding/json"
6-
"google.golang.org/api/iterator"
76
"net/http"
87
"question-service/models"
98
"strconv"
@@ -61,14 +60,24 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
6160
// Filtering by complexity (multi-select)
6261
complexityParam := r.URL.Query().Get("complexity")
6362
if complexityParam != "" {
64-
complexities := strings.Split(complexityParam, ",")
65-
query = query.Where("complexity", "in", complexities)
63+
var complexityInts []int
64+
complexityStrs := strings.Split(complexityParam, ",")
65+
for _, complexityStr := range complexityStrs {
66+
complexityType, err := models.ParseComplexity(complexityStr)
67+
if err != nil {
68+
http.Error(w, "Failed to filter by complexity: "+err.Error(), http.StatusInternalServerError)
69+
return
70+
}
71+
complexityInts = append(complexityInts, int(complexityType))
72+
}
73+
query = query.Where("complexity", "in", complexityInts)
6674
}
6775

68-
// Filtering by categories (multi-select)
76+
// Filtering by categories (multi-select): Partially using array-contains-any first
6977
categoriesParam := r.URL.Query().Get("categories")
78+
var categories []string
7079
if categoriesParam != "" {
71-
categories := strings.Split(categoriesParam, ",")
80+
categories = strings.Split(categoriesParam, ",")
7281
query = query.Where("categories", "array-contains-any", categories)
7382
}
7483

@@ -109,12 +118,33 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
109118
query = query.OrderBy("id", firestore.Asc)
110119
}
111120

112-
// Count total documents matching the filter
113-
totalIter, err := query.Documents(ctx).GetAll()
114-
totalCount := len(totalIter)
121+
results, err := query.Documents(ctx).GetAll()
115122
if err != nil {
116-
http.Error(w, "Failed to count questions: "+err.Error(), http.StatusInternalServerError)
117-
return
123+
http.Error(w, "Failed to to get fetch questions: "+err.Error(), http.StatusInternalServerError)
124+
}
125+
126+
// Filter the results to check if the document contains all categories (implementation of array-contains-all)
127+
var filteredResults []*firestore.DocumentSnapshot
128+
if categories != nil {
129+
for _, doc := range results {
130+
data := doc.Data()
131+
132+
// Retrieve the "categories" field from the document and convert to []string
133+
if docCategories, ok := data["categories"].([]interface{}); ok {
134+
stringCategories := make([]string, len(docCategories))
135+
for i, cat := range docCategories {
136+
if catStr, ok := cat.(string); ok {
137+
stringCategories[i] = catStr
138+
}
139+
}
140+
141+
if containsAllCategoriesSet(stringCategories, categories) {
142+
filteredResults = append(filteredResults, doc)
143+
}
144+
}
145+
}
146+
} else {
147+
filteredResults = results
118148
}
119149

120150
// Pagination
@@ -123,52 +153,51 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
123153
if limitParam != "" {
124154
limit, err = strconv.Atoi(limitParam) // convert limit to integer
125155
if err != nil || limit <= 0 {
126-
http.Error(w, "Invalid limit", http.StatusBadRequest)
156+
http.Error(w, "Invalid limit: "+strconv.Itoa(limit), http.StatusBadRequest)
127157
return
128158
}
129159
}
130-
currentPage := 1
131160
offsetParam := r.URL.Query().Get("offset")
161+
var offset int
132162
if offsetParam != "" {
133-
offset, err := strconv.Atoi(offsetParam) // convert offset to integer
163+
offset, err = strconv.Atoi(offsetParam) // convert offset to integer
134164
if err != nil {
135-
http.Error(w, "Invalid offset", http.StatusBadRequest)
165+
http.Error(w, "Invalid offset: "+strconv.Itoa(offset), http.StatusBadRequest)
136166
return
137167
}
138168
if offset%limit != 0 {
139-
http.Error(w, "Offset does not match limit. Default limit is 10 when unspecified", http.StatusBadRequest)
169+
http.Error(w, "Offset does not match limit. Default limit is 10 when offset is unspecified",
170+
http.StatusBadRequest)
140171
}
141-
currentPage = (offset / limit) + 1
142-
query = query.Offset(offset)
143172
}
144-
query = query.Limit(limit)
145173

146-
iter := query.Documents(ctx)
174+
paginatedResults := paginateResults(filteredResults, offset, limit)
147175

148176
var questions []models.Question
149-
for {
150-
// Get data
151-
doc, err := iter.Next()
152-
if err == iterator.Done {
153-
break
154-
}
155-
if err != nil {
156-
http.Error(w, "Failed to retrieve questions", http.StatusInternalServerError)
157-
return
158-
}
159-
177+
for _, doc := range paginatedResults {
160178
// Map data
161179
var question models.Question
180+
//complexityStr, ok := doc.Data()["complexity"].(string)
181+
//if ok {
182+
// complexity, err := models.ParseComplexity(complexityStr)
183+
// if err != nil {
184+
// http.Error(w, "Invalid complexity level in question: "+err.Error(), http.StatusInternalServerError)
185+
// return
186+
// }
187+
// question.Complexity = complexity
188+
//}
162189
if err := doc.DataTo(&question); err != nil {
163-
http.Error(w, "Failed to parse question", http.StatusInternalServerError)
190+
http.Error(w, "Failed to parse question: "+err.Error(), http.StatusInternalServerError)
164191
return
165192
}
166193
question.DocRefID = doc.Ref.ID
167194
questions = append(questions, question)
168195
}
169196

170197
// Calculate pagination info
198+
totalCount := len(filteredResults)
171199
totalPages := (totalCount + limit - 1) / limit
200+
currentPage := (offset / limit) + 1
172201
hasNextPage := totalPages > currentPage
173202

174203
// Construct response
@@ -185,6 +214,36 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
185214
json.NewEncoder(w).Encode(response)
186215
}
187216

217+
func containsAllCategoriesSet(docCategories []string, queryCategories []string) bool {
218+
categorySet := make(map[string]bool)
219+
220+
// Populate the set with document categories
221+
for _, cat := range docCategories {
222+
categorySet[cat] = true
223+
}
224+
225+
// Check if all query categories are present in the document categories
226+
for _, queryCat := range queryCategories {
227+
if !categorySet[queryCat] {
228+
return false
229+
}
230+
}
231+
return true
232+
}
233+
234+
func paginateResults(results []*firestore.DocumentSnapshot, offset, limit int) []*firestore.DocumentSnapshot {
235+
start := offset
236+
end := offset + limit
237+
238+
if start >= len(results) {
239+
return []*firestore.DocumentSnapshot{}
240+
}
241+
if end > len(results) {
242+
end = len(results)
243+
}
244+
return results[start:end]
245+
}
246+
188247
//Manual test cases
189248
//
190249
//curl -X GET "http://localhost:8080/questions"
@@ -194,17 +253,17 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
194253
//
195254
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String"
196255
//
197-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium"
256+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium"
198257
//
199258
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures"
200259
//
201-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium"
260+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium"
202261
//
203262
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String"
204263
//
205-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String"
264+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String"
206265
//
207-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings"
266+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings"
208267
//
209268
//
210269
//curl -X GET "http://localhost:8080/questions?sortField=title&sortValue=asc&offset=10"
@@ -222,62 +281,62 @@ func (s *Service) ListQuestions(w http.ResponseWriter, r *http.Request) {
222281
//
223282
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
224283
//
225-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=complexity&sortValue=desc"
284+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=complexity&sortValue=desc"
226285
//
227286
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=complexity&sortValue=desc"
228287
//
229-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=complexity&sortValue=desc"
288+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=complexity&sortValue=desc"
230289
//
231290
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
232291
//
233-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
292+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=complexity&sortValue=desc"
234293
//
235-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=complexity&sortValue=desc"
294+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=complexity&sortValue=desc"
236295
//
237296
//
238297
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
239298
//
240-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
299+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=createdAt&sortValue=desc"
241300
//
242301
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=createdAt&sortValue=desc"
243302
//
244-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=createdAt&sortValue=desc"
303+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=createdAt&sortValue=desc"
245304
//
246305
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
247306
//
248-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
307+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=createdAt&sortValue=desc"
249308
//
250-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=createdAt&sortValue=desc"
309+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=createdAt&sortValue=desc"
251310
//
252311
//
253312
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=id&sortValue=asc"
254313
//
255-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=id&sortValue=asc"
314+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=id&sortValue=asc"
256315
//
257316
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=id&sortValue=asc"
258317
//
259-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=id&sortValue=asc"
318+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=id&sortValue=asc"
260319
//
261320
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=id&sortValue=asc"
262321
//
263-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=id&sortValue=asc"
322+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=id&sortValue=asc"
264323
//
265-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=id&sortValue=asc"
324+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=id&sortValue=asc"
266325
//
267326
//
268327
//curl -X GET "http://localhost:8080/questions?title=Reverse%20a%20String&sortField=title&sortValue=asc"
269328
//
270-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&sortField=title&sortValue=asc"
329+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&sortField=title&sortValue=asc"
271330
//
272331
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&sortField=title&sortValue=asc"
273332
//
274-
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=Easy,Medium&sortField=title&sortValue=asc"
333+
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Data%20Structures&complexity=easy,medium&sortField=title&sortValue=asc"
275334
//
276335
//curl -X GET "http://localhost:8080/questions?categories=Algorithms,Strings&title=Reverse%20a%20String&sortField=title&sortValue=asc"
277336
//
278-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&sortField=title&sortValue=asc"
337+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&sortField=title&sortValue=asc"
279338
//
280-
//curl -X GET "http://localhost:8080/questions?complexity=Easy,Medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=title&sortValue=asc"
339+
//curl -X GET "http://localhost:8080/questions?complexity=easy,medium&title=Reverse%20a%20String&categories=Algorithms,Strings&sortField=title&sortValue=asc"
281340
//
282341
//
283342
//curl -X GET "http://localhost:8080/questions?complexity=InvalidComplexity"
Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,76 @@
11
package models
22

3-
import "time"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"time"
7+
)
8+
9+
type ComplexityType int
10+
11+
const (
12+
Easy ComplexityType = iota
13+
Medium
14+
Hard
15+
)
416

517
// TODO: currently the Question model is a simplified model
618
type Question struct {
7-
Title string `json:"title"`
8-
Description string `json:"description"`
9-
Categories []string `json:"categories"`
10-
Complexity string `json:"complexity"`
19+
Title string `json:"title"`
20+
Description string `json:"description"`
21+
Categories []string `json:"categories"`
22+
Complexity ComplexityType `json:"complexity"` // Now an enum
1123

1224
// Special DB fields
1325
ID int64 `json:"id"`
1426
DocRefID string `json:"docRefId"` // The firestore document reference ID
1527
CreatedAt time.Time `json:"createdAt"`
1628
}
29+
30+
func (c ComplexityType) String() string {
31+
return [...]string{"easy", "medium", "hard"}[c]
32+
}
33+
34+
func ParseComplexity(complexityStr string) (ComplexityType, error) {
35+
switch complexityStr {
36+
case "easy":
37+
return Easy, nil
38+
case "medium":
39+
return Medium, nil
40+
case "hard":
41+
return Hard, nil
42+
default:
43+
return Easy, fmt.Errorf("invalid complexity level: %s", complexityStr)
44+
}
45+
}
46+
47+
func (c ComplexityType) MarshalJSON() ([]byte, error) {
48+
return json.Marshal(c.String())
49+
}
50+
51+
func (c *ComplexityType) UnmarshalJSON(data []byte) error {
52+
var complexityStr string
53+
if err := json.Unmarshal(data, &complexityStr); err != nil {
54+
return err
55+
}
56+
57+
complexity, err := ParseComplexity(complexityStr)
58+
if err != nil {
59+
return err
60+
}
61+
62+
*c = complexity
63+
return nil
64+
}
65+
66+
func (c ComplexityType) MarshalFirestore() (interface{}, error) {
67+
return int(c), nil
68+
}
69+
70+
func (c *ComplexityType) UnmarshalFirestore(data interface{}) error {
71+
if complexityInt, ok := data.(int); ok {
72+
*c = ComplexityType(complexityInt)
73+
return nil
74+
}
75+
return fmt.Errorf("invalid type for ComplexityType in Firestore: %T", data)
76+
}

0 commit comments

Comments
 (0)