feat: Centralize Kobo API models #1

Merged
eleith merged 1 commits from eleith/leverage-models into main 2025-11-13 14:02:37 +00:00
3 changed files with 63 additions and 64 deletions

No files matched your search

+8 -48
View File
@@ -23,6 +23,7 @@ import (
"golang.org/x/net/html"
"readeckobo/internal/config"
"readeckobo/internal/logger"
"readeckobo/internal/models"
"readeckobo/internal/readeck"
)
@@ -74,25 +75,9 @@ func WithReadeckHTTPClient(client *http.Client) Option {
}
}
// GetRequest represents the incoming request for /api/kobo/get
type GetRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
ContentType string `json:"contentType"`
Count string `json:"count"`
DetailType string `json:"detailType"`
Offset string `json:"offset"`
State string `json:"state"`
Total string `json:"total"`
Since any `json:"since"`
}
// KoboGetResponse represents the outgoing response for /api/kobo/get
type KoboGetResponse struct {
Status int `json:"status"`
List map[string]any `json:"list"`
Total int `json:"total"`
}
// HandleKoboGet handles the /api/kobo/get endpoint.
func (a *App) HandleKoboGet(w http.ResponseWriter, r *http.Request) {
@@ -114,7 +99,7 @@ func (a *App) HandleKoboGet(w http.ResponseWriter, r *http.Request) {
return
}
var req GetRequest
var req models.KoboGetRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
a.Logger.Errorf("Error decoding /api/kobo/get request: %v, body: %s, URL: %s, Params: %v", err, string(bodyBytes), r.URL.Path, r.URL.Query())
@@ -288,7 +273,7 @@ func (a *App) HandleKoboGet(w http.ResponseWriter, r *http.Request) {
resultList[bm["item_id"].(string)] = bm
}
resp := KoboGetResponse{
resp := models.KoboGetResponse{
Status: 1,
List: resultList,
Total: totalNonArchivedBookmarks, // Total number of non-archived bookmarks
@@ -300,15 +285,7 @@ func (a *App) HandleKoboGet(w http.ResponseWriter, r *http.Request) {
a.Logger.Errorf("Error encoding response for /api/kobo/get: %v, URL: %s, Params: %v", err, r.URL.Path, r.URL.Query())
}
}
// DownloadRequest represents the incoming request for /api/kobo/download
type DownloadRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
Images int `json:"images"`
Refresh int `json:"refresh"`
Output string `json:"output"`
URL string `json:"url"`
}
// HandleKoboDownload handles the /api/kobo/download endpoint.
func (a *App) HandleKoboDownload(w http.ResponseWriter, r *http.Request) {
@@ -330,7 +307,7 @@ func (a *App) HandleKoboDownload(w http.ResponseWriter, r *http.Request) {
return
}
var req DownloadRequest
var req models.KoboDownloadRequest
// Use the restored body for decoding.
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&req); err != nil {
// If JSON decoding fails, try form parsing (Kobo devices might send form data for download)
@@ -514,24 +491,7 @@ func getSitesToTry(host string) []string {
return uniqueSites
}
// ExistingItemAction represents an action on an existing item.
type ExistingItemAction struct {
Action string `json:"action"`
ItemID string `json:"item_id"`
}
// NewItemAction represents an action to create a new item.
type NewItemAction struct {
Action string `json:"action"`
URL string `json:"url"`
}
// SendRequest represents the incoming request for /api/kobo/send
type SendRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
Actions []any `json:"actions"`
}
// HandleKoboSend handles the /api/kobo/send endpoint.
func (a *App) HandleKoboSend(w http.ResponseWriter, r *http.Request) {
@@ -553,7 +513,7 @@ func (a *App) HandleKoboSend(w http.ResponseWriter, r *http.Request) {
return
}
var req SendRequest
var req models.KoboSendRequest
// Use the restored body for decoding.
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
+17 -16
View File
@@ -16,6 +16,7 @@ import (
"readeckobo/internal/config"
"readeckobo/internal/logger"
"readeckobo/internal/models"
"readeckobo/internal/readeck"
)
@@ -162,7 +163,7 @@ func TestCompareURLs(t *testing.T) {
type koboGetTestCase struct {
name string
reqBody *GetRequest
reqBody *models.KoboGetRequest
mockBookmarksSync []readeck.BookmarkSync
mockBookmarkDetails map[string]*readeck.Bookmark
mockBookmarksSyncErr error
@@ -177,7 +178,7 @@ func TestHandleKoboGet(t *testing.T) {
testCases := []koboGetTestCase{
{
name: "successful get",
reqBody: &GetRequest{Count: "1", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: mockDeviceToken},
mockBookmarksSync: []readeck.BookmarkSync{
{ID: "1", Type: "update"},
},
@@ -260,7 +261,7 @@ func TestHandleKoboGet(t *testing.T) {
},
{
name: "delete sync type",
reqBody: &GetRequest{Count: "1", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: mockDeviceToken},
mockBookmarksSync: []readeck.BookmarkSync{
{ID: "1", Type: "delete"},
},
@@ -314,7 +315,7 @@ func TestHandleKoboGet(t *testing.T) {
},
{
name: "get bookmark details error",
reqBody: &GetRequest{Count: "1", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: mockDeviceToken},
mockBookmarksSync: []readeck.BookmarkSync{
{ID: "1", Type: "update"},
},
@@ -352,7 +353,7 @@ func TestHandleKoboGet(t *testing.T) {
},
{
name: "get bookmark details nil",
reqBody: &GetRequest{Count: "1", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: mockDeviceToken},
mockBookmarksSync: []readeck.BookmarkSync{
{ID: "1", Type: "update"},
},
@@ -432,7 +433,7 @@ func TestHandleKoboGet(t *testing.T) {
},
{
name: "get bookmarks sync error",
reqBody: &GetRequest{Count: "1", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: mockDeviceToken},
mockBookmarksSyncErr: fmt.Errorf("sync error"),
expectedStatus: http.StatusInternalServerError,
mockHTTPClientFunc: func(t *testing.T, tc *koboGetTestCase) *http.Client {
@@ -475,7 +476,7 @@ func TestHandleKoboGet(t *testing.T) {
},
{
name: "invalid access token",
reqBody: &GetRequest{Count: "1", AccessToken: "invalid-device-token"},
reqBody: &models.KoboGetRequest{Count: "1", AccessToken: "invalid-device-token"},
expectedStatus: http.StatusUnauthorized,
mockHTTPClientFunc: func(t *testing.T, tc *koboGetTestCase) *http.Client {
return &http.Client{
@@ -527,7 +528,7 @@ func TestHandleKoboGet(t *testing.T) {
}
if tc.expectedStatus == http.StatusOK {
var resp KoboGetResponse
var resp models.KoboGetResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
@@ -557,7 +558,7 @@ func TestHandleKoboDownload(t *testing.T) {
testCases := []koboDownloadTestCase{
{
name: "successful download (JSON)",
reqBody: DownloadRequest{
reqBody: models.KoboDownloadRequest{
AccessToken: mockDeviceToken,
URL: "http://example.com/article1",
},
@@ -655,7 +656,7 @@ func TestHandleKoboDownload(t *testing.T) {
},
{
name: "missing url",
reqBody: DownloadRequest{
reqBody: models.KoboDownloadRequest{
AccessToken: mockDeviceToken,
URL: "",
},
@@ -677,7 +678,7 @@ func TestHandleKoboDownload(t *testing.T) {
},
{
name: "invalid access token",
reqBody: DownloadRequest{
reqBody: models.KoboDownloadRequest{
AccessToken: "invalid-device-token",
URL: "http://example.com/article1",
},
@@ -1065,7 +1066,7 @@ func TestHandleKoboSend(t *testing.T) {
WithReadeckHTTPClient(tc.mockHTTPClientFunc(t, &tc, &updatedBookmarkID, &updatedBookmarkData, &createdBookmarkURL)),
)
reqBody := SendRequest{AccessToken: tc.accessToken, Actions: tc.actions}
reqBody := models.KoboSendRequest{AccessToken: tc.accessToken, Actions: tc.actions}
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
@@ -1125,7 +1126,7 @@ func TestHandleKoboSend(t *testing.T) {
// koboGetWithArchivedTestCase defines the structure for test cases in TestHandleKoboGetWithArchived.
type koboGetWithArchivedTestCase struct {
name string
reqBody *GetRequest
reqBody *models.KoboGetRequest
mockBookmarksSync []readeck.BookmarkSync
mockBookmarkDetails map[string]*readeck.Bookmark
expectedStatus int
@@ -1138,7 +1139,7 @@ func TestHandleKoboGetWithArchived(t *testing.T) {
testCases := []koboGetWithArchivedTestCase{
{
name: "successful get with archived",
reqBody: &GetRequest{Count: "10", AccessToken: mockDeviceToken},
reqBody: &models.KoboGetRequest{Count: "10", AccessToken: mockDeviceToken},
mockBookmarksSync: []readeck.BookmarkSync{
{ID: "1", Type: "update"},
{ID: "2", Type: "update"},
@@ -1255,7 +1256,7 @@ func TestHandleKoboGetWithArchived(t *testing.T) {
WithReadeckHTTPClient(tc.mockHTTPClientFunc(t, &tc)),
)
reqBody := GetRequest{Count: "10", AccessToken: mockDeviceToken} // Request more than the number of bookmarks
reqBody := models.KoboGetRequest{Count: "10", AccessToken: mockDeviceToken} // Request more than the number of bookmarks
body, err := json.Marshal(reqBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
@@ -1269,7 +1270,7 @@ func TestHandleKoboGetWithArchived(t *testing.T) {
t.Errorf("expected status %d, got %d", http.StatusOK, rr.Code)
}
var resp KoboGetResponse
var resp models.KoboGetResponse
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("Failed to decode response: %v", err)
}
+38
View File
@@ -0,0 +1,38 @@
package models
// KoboGetRequest is the incoming request for /api/kobo/get
type KoboGetRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
ContentType string `json:"contentType"`
Count string `json:"count"`
DetailType string `json:"detailType"`
Offset string `json:"offset"`
State string `json:"state"`
Total string `json:"total"`
Since any `json:"since"`
}
// KoboGetResponse represents the outgoing response for /api/kobo/get
type KoboGetResponse struct {
Status int `json:"status"`
List map[string]any `json:"list"`
Total int `json:"total"`
}
// KoboDownloadRequest represents the incoming request for /api/kobo/download
type KoboDownloadRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
Images int `json:"images"`
Refresh int `json:"refresh"`
Output string `json:"output"`
URL string `json:"url"`
}
// KoboSendRequest represents the incoming request for /api/kobo/send
type KoboSendRequest struct {
AccessToken string `json:"access_token"`
ConsumerKey string `json:"consumer_key"`
Actions []any `json:"actions"`
}