Skip to content

Commit

Permalink
feat(web): add new parameters with different semantics to the `torren…
Browse files Browse the repository at this point in the history
…tstotal` api

A frontend interface was created using AI, and I initially implemented both "previous" and "next" pagination. However, I was unable to achieve the specific page navigation functionality I intended.

After modifying the API, it now provides an approximate count of results (e.g., "Approximately xxx items found"). Pagination has been implemented using cursor-based pagination, which also improves the experience on mobile devices.

At the time, I mistakenly thought it was possible to paginate by specifying a page number, without realizing that pagination could only be achieved using cursors.

The functionality of this API has now been modified to return an approximate count of results that can be fetched.

Two new parameters, **NewLogic** and **QueryType**, have been added.

- **NewLogic** controls compatibility: when set to `true`, the response will use the new JSON format; when set to `false`, it will use the old response format.
- **QueryType** enhances the API’s functionality to avoid issues arising from potential misunderstandings. Currently, it only supports total count queries by keyword, and it requires **NewLogic=true** to be effective.

Compatibility with the old behaviour has been implemented.
  • Loading branch information
lazyoop authored Dec 28, 2024
1 parent afe0ba2 commit fc60049
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 6 deletions.
75 changes: 71 additions & 4 deletions web/torrents.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ func apiTorrentsTotal(w http.ResponseWriter, r *http.Request) {
var tq struct {
Epoch int64 `schema:"epoch"`
Query string `schema:"query"`
// Controls compatibility. If this parameter is not provided or is set to false, the old logic is executed.
// If set to true, the new logic is enabled.
// The old logic returns a single number, while the new logic returns a map[string]any JSON object.
NewLogic bool `schema:"newLogic"`
// Due to potential ambiguity in the function name apiTorrentsTotal, the QueryType parameter was introduced.
// To use this parameter, `NewLogic=true` is required. This parameter specifies the type of query we are performing.
// For example, `byAll` indicates querying the total count from the database,
// while `byKeyword` indicates querying the total count that matches the given query.
QueryType string `schema:"queryType"`
}

err := r.ParseForm()
Expand All @@ -229,16 +238,56 @@ func apiTorrentsTotal(w http.ResponseWriter, r *http.Request) {
return
}

if r.Form.Has("newLogic") {
tq.NewLogic, err = strconv.ParseBool(r.Form.Get("newLogic"))
if err != nil {
http.Error(w, "error while parsing the URL: "+err.Error(), http.StatusBadRequest)
return
}
}

tq.Query = r.Form.Get("query")
tq.QueryType = r.Form.Get("queryType")

w.Header().Set(ContentType, ContentTypeJson)

if !tq.NewLogic {

torrentsTotal, err := database.GetNumberOfQueryTorrents(tq.Query, tq.Epoch)
torrentsTotal, err := database.GetNumberOfQueryTorrents(tq.Query, tq.Epoch)
if err != nil {
http.Error(w, "GetNumberOfQueryTorrents: "+err.Error(), http.StatusInternalServerError)
return
}

if err = json.NewEncoder(w).Encode(torrentsTotal); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
}

queryCountType, err := parseQueryCountType(tq.QueryType)
if err != nil {
http.Error(w, "GetNumberOfQueryTorrents: "+err.Error(), http.StatusInternalServerError)
http.Error(w, "error while parsing the URL: "+err.Error(), http.StatusBadRequest)
return
}

w.Header().Set(ContentType, ContentTypeJson)
if err = json.NewEncoder(w).Encode(torrentsTotal); err != nil {
var results map[string]any

switch queryCountType {
case CountQueryTorrentsByKeyword:
total, err := database.GetNumberOfQueryTorrents(tq.Query, tq.Epoch)
if err != nil {
http.Error(w, "GetNumberOfQueryTorrents: "+err.Error(), http.StatusInternalServerError)
return
}
results = map[string]any{"queryType": "byKeyword", "data": total}

default:
http.Error(w, "no suitable queryType query was matched", http.StatusBadRequest)
return
}

if err = json.NewEncoder(w).Encode(results); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
}
Expand Down Expand Up @@ -270,3 +319,21 @@ func parseOrderBy(s string) (persistence.OrderingCriteria, error) {
return persistence.ByDiscoveredOn, fmt.Errorf("unknown orderBy string: %s", s)
}
}

type CountQueryTorrentsType uint8

const (
CountQueryTorrentsByAll CountQueryTorrentsType = iota
CountQueryTorrentsByKeyword
)

func parseQueryCountType(s string) (CountQueryTorrentsType, error) {
switch s {
case "byKeyword":
return CountQueryTorrentsByKeyword, nil
case "byAll":
return CountQueryTorrentsByAll, nil
default:
return CountQueryTorrentsByKeyword, fmt.Errorf("unknown queryType string: %s", s)
}
}
68 changes: 66 additions & 2 deletions web/torrents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func TestApiTorrentsTotal(t *testing.T) {
},
{
name: "valid request with epoch",
queryParams: "epoch=1234567890",
queryParams: "epoch=1234567890&query=testQuery",
expectedStatus: http.StatusOK,
},
{
Expand All @@ -200,11 +200,29 @@ func TestApiTorrentsTotal(t *testing.T) {
queryParams: "epoch=1234567890&lastOrderedValue=123.45&lastID=123",
expectedStatus: http.StatusOK,
},
{
name: "valid request with newLogic=true",
queryParams: "epoch=1234567890&newLogic=true&queryType=byKeyword",
expectedStatus: http.StatusOK,
expectedError: `{"data":0,"queryType":"byKeyword"}`,
},
{
name: "invalid queryType",
queryParams: "epoch=1234567890&newLogic=true&queryType=invalidType",
expectedStatus: http.StatusBadRequest,
expectedError: "error while parsing the URL: unknown queryType string: invalidType",
},
{
name: "invalid newLogic parameter",
queryParams: "epoch=1234567890&newLogic=bool",
expectedStatus: http.StatusBadRequest,
expectedError: "error while parsing the URL: strconv.ParseBool: parsing \"bool\": invalid syntax",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "/api/torrents/total?"+tt.queryParams, nil)
req, err := http.NewRequest("GET", "/api/torrentstotal?"+tt.queryParams, nil)
if err != nil {
t.Fatalf("could not create request: %v", err)
}
Expand All @@ -228,3 +246,49 @@ func TestApiTorrentsTotal(t *testing.T) {
})
}
}

func TestParseQueryCountType(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
expectedOutput CountQueryTorrentsType
expectedError string
}{
{
name: "Valid byKeyword",
input: "byKeyword",
expectedOutput: CountQueryTorrentsByKeyword,
expectedError: "",
},
{
name: "Valid byAll",
input: "byAll",
expectedOutput: CountQueryTorrentsByAll,
expectedError: "",
},
{
name: "Invalid queryType",
input: "invalidType",
expectedOutput: CountQueryTorrentsByKeyword,
expectedError: "unknown queryType string: invalidType",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, err := parseQueryCountType(tt.input)

if err != nil && err.Error() != tt.expectedError {
t.Errorf("expected error %v, got %v", tt.expectedError, err.Error())
} else if err == nil && tt.expectedError != "" {
t.Errorf("expected error %v, got nil", tt.expectedError)
}

if output != tt.expectedOutput {
t.Errorf("expected output %v, got %v", tt.expectedOutput, output)
}
})
}
}

0 comments on commit fc60049

Please sign in to comment.