From 71b9bda06893514de61abc1354e4835cbcf4cfdf Mon Sep 17 00:00:00 2001 From: eleith Date: Thu, 13 Nov 2025 06:01:15 -0800 Subject: [PATCH] feat: Centralize Kobo API models Moved Kobo API request and response structs from internal/app/app.go to internal/models/kobo.go to improve separation of concerns and prepare for further refactoring. Updated app.go and app_test.go to use the new models package. --- internal/app/app.go | 56 ++++++---------------------------------- internal/app/app_test.go | 33 +++++++++++------------ internal/models/kobo.go | 38 +++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 64 deletions(-) create mode 100644 internal/models/kobo.go diff --git a/internal/app/app.go b/internal/app/app.go index 84e5ea2..7840c26 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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) diff --git a/internal/app/app_test.go b/internal/app/app_test.go index b8de7ad..a990b56 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -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) } diff --git a/internal/models/kobo.go b/internal/models/kobo.go new file mode 100644 index 0000000..e679b85 --- /dev/null +++ b/internal/models/kobo.go @@ -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"` +} -- 2.54.0