From 2e639e0feffc713597d9e9675d168c4b9e50ae68 Mon Sep 17 00:00:00 2001 From: Li Wei Date: Sun, 5 Mar 2023 11:54:39 +0800 Subject: [PATCH] feat: api result add code (#96) --- pkg/rest/api.go | 84 ++++++++++++++++---------- pkg/rest/model/result.go | 42 ++++++++++--- pkg/rest/server.go | 18 +++++- pkg/rest/server_test.go | 24 ++++---- pkg/rest/util/util.go | 29 --------- ui/flutter/lib/api/api.dart | 12 ++-- ui/flutter/lib/api/model/result.dart | 3 + ui/flutter/lib/api/model/result.g.dart | 5 +- ui/flutter/pubspec.lock | 2 +- 9 files changed, 130 insertions(+), 89 deletions(-) delete mode 100644 pkg/rest/util/util.go diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 84c33ec9..cbcfc378 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -5,7 +5,6 @@ import ( "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/rest/model" - "github.com/GopeedLab/gopeed/pkg/rest/util" "github.com/gorilla/mux" "io" "net/http" @@ -15,19 +14,19 @@ import ( func Resolve(w http.ResponseWriter, r *http.Request) { var req base.Request - if util.ReadJson(w, r, &req) { + if ReadJson(r, w, &req) { rr, err := Downloader.Resolve(&req) if err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } - util.WriteJsonOk(w, model.NewResultWithData(rr)) + WriteJson(w, model.NewOkResult(rr)) } } func CreateTask(w http.ResponseWriter, r *http.Request) { var req model.CreateTask - if util.ReadJson(w, r, &req) { + if ReadJson(r, w, &req) { var ( taskId string err error @@ -37,64 +36,85 @@ func CreateTask(w http.ResponseWriter, r *http.Request) { } else if req.Req != nil { taskId, err = Downloader.DirectCreate(req.Req, req.Opts) } else { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: rid or req")) + WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam)) return } if err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } - util.WriteJsonOk(w, model.NewResultWithData(taskId)) + WriteJson(w, model.NewOkResult(taskId)) } } func PauseTask(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskId := vars["id"] - if err := Downloader.Pause(taskId); err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + if taskId == "" { + WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)) return } - util.WriteJsonOk(w, nil) + if err := Downloader.Pause(taskId); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) } func ContinueTask(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskId := vars["id"] - if err := Downloader.Continue(taskId); err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + if taskId == "" { + WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)) return } - util.WriteJsonOk(w, nil) + if err := Downloader.Continue(taskId); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) } func DeleteTask(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskId := vars["id"] force := r.FormValue("force") - if err := Downloader.Delete(taskId, force == "true"); err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + if taskId == "" { + WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)) return } - util.WriteJsonOk(w, nil) + if err := Downloader.Delete(taskId, force == "true"); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) } func GetTask(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskId := vars["id"] - task := Downloader.GetTask(taskId) - if task == nil { - util.WriteJson(w, http.StatusNotFound, model.NewResultWithMsg("task not found")) + if taskId == "" { + WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam)) return } - util.WriteJsonOk(w, model.NewResultWithData(task)) + task := Downloader.GetTask(taskId) + if task == nil { + WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound)) + return + } + WriteJson(w, model.NewOkResult(task)) } func GetTasks(w http.ResponseWriter, r *http.Request) { status := r.FormValue("status") if status == "" { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: status")) - return + status = strings.Join([]string{ + string(base.DownloadStatusReady), + string(base.DownloadStatusRunning), + string(base.DownloadStatusPause), + string(base.DownloadStatusError), + string(base.DownloadStatusDone), + }, ",") } statusArr := strings.Split(status, ",") tasks := Downloader.GetTasks() @@ -106,33 +126,33 @@ func GetTasks(w http.ResponseWriter, r *http.Request) { } } } - util.WriteJsonOk(w, model.NewResultWithData(result)) + WriteJson(w, model.NewOkResult(result)) } func GetConfig(w http.ResponseWriter, r *http.Request) { - util.WriteJsonOk(w, model.NewResultWithData(getServerConfig())) + WriteJson(w, model.NewOkResult(getServerConfig())) } func PutConfig(w http.ResponseWriter, r *http.Request) { var cfg download.DownloaderStoreConfig - if util.ReadJson(w, r, &cfg) { + if ReadJson(r, w, &cfg) { if err := Downloader.PutConfig(&cfg); err != nil { - util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } } - util.WriteJsonOk(w, nil) + WriteJson(w, model.NewNilResult()) } func DoProxy(w http.ResponseWriter, r *http.Request) { target := r.Header.Get("X-Target-Uri") if target == "" { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("header is required: X-Target-Uri")) + WriteJson(w, model.NewErrorResult("param invalid: X-Target-Uri", model.CodeInvalidParam)) return } targetUrl, err := url.Parse(target) if err != nil { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } r.RequestURI = "" @@ -140,7 +160,7 @@ func DoProxy(w http.ResponseWriter, r *http.Request) { r.Host = targetUrl.Host resp, err := http.DefaultClient.Do(r) if err != nil { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } defer resp.Body.Close() @@ -159,7 +179,7 @@ func DoProxy(w http.ResponseWriter, r *http.Request) { reader = resp.Body } if _, err := io.Copy(w, reader); err != nil { - util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error())) + WriteJson(w, model.NewErrorResult(err.Error())) return } } diff --git a/pkg/rest/model/result.go b/pkg/rest/model/result.go index 6807e383..e3e35d98 100644 --- a/pkg/rest/model/result.go +++ b/pkg/rest/model/result.go @@ -1,21 +1,47 @@ package model +type RespCode int + +const ( + CodeOk RespCode = 0 + // CodeError is the common error code + CodeError RespCode = 1000 + // CodeUnauthorized is the error code for unauthorized + CodeUnauthorized RespCode = 1002 + // CodeInvalidParam is the error code for invalid parameter + CodeInvalidParam RespCode = 1003 + // CodeTaskNotFound is the error code for task not found + CodeTaskNotFound RespCode = 2001 +) + type Result[T any] struct { - Msg string `json:"msg"` - Data T `json:"data"` + Code RespCode `json:"code"` + Msg string `json:"msg"` + Data T `json:"data"` } -func NewResult[T any](msg string, data T) *Result[T] { +func NewOkResult[T any](data T) *Result[T] { return &Result[T]{ - Msg: msg, + Code: CodeOk, Data: data, } } -func NewResultWithMsg(msg string) *Result[any] { - return NewResult[any](msg, nil) +func NewNilResult() *Result[any] { + return &Result[any]{ + Code: CodeOk, + } } -func NewResultWithData[T any](data T) *Result[T] { - return NewResult[T]("", data) +func NewErrorResult(msg string, code ...RespCode) *Result[any] { + // if code is not provided, the default code is CodeError + c := CodeError + if len(code) > 0 { + c = code[0] + } + + return &Result[any]{ + Code: c, + Msg: msg, + } } diff --git a/pkg/rest/server.go b/pkg/rest/server.go index 1872c372..8adf32ad 100644 --- a/pkg/rest/server.go +++ b/pkg/rest/server.go @@ -2,9 +2,9 @@ package rest import ( "context" + "encoding/json" "github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/rest/model" - restUtil "github.com/GopeedLab/gopeed/pkg/rest/util" "github.com/GopeedLab/gopeed/pkg/util" "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -98,7 +98,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error r.Use(func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Api-Token") != startCfg.ApiToken { - restUtil.WriteJson(w, http.StatusUnauthorized, model.NewResultWithMsg("invalid token")) + WriteJson(w, model.NewErrorResult("invalid token", model.CodeUnauthorized)) return } h.ServeHTTP(w, r) @@ -113,3 +113,17 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error )(r)} return srv, listener, nil } + +func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool { + if err := json.NewDecoder(r.Body).Decode(v); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return false + } + return true +} + +func WriteJson(w http.ResponseWriter, v any) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(v) +} diff --git a/pkg/rest/server_test.go b/pkg/rest/server_test.go index 13d8e942..748c7e7e 100644 --- a/pkg/rest/server_test.go +++ b/pkg/rest/server_test.go @@ -150,7 +150,7 @@ func TestDeleteTask(t *testing.T) { time.Sleep(time.Millisecond * 200) httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil) code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil) - checkCode(code, http.StatusNotFound) + checkCode(code, model.CodeTaskNotFound) }) } @@ -160,7 +160,7 @@ func TestDeleteTaskForce(t *testing.T) { time.Sleep(time.Millisecond * 200) httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil) code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil) - checkCode(code, http.StatusNotFound) + checkCode(code, model.CodeTaskNotFound) if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) { t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist) } @@ -179,8 +179,7 @@ func TestGetTasks(t *testing.T) { httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusRunning), createReq) - code, _ := httpRequest[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil) - checkCode(code, http.StatusBadRequest) + httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil) wg.Wait() r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", @@ -245,12 +244,12 @@ func TestAuthorization(t *testing.T) { }() code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil) - checkCode(code, http.StatusUnauthorized) + checkCode(code, model.CodeUnauthorized) code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{ "X-Api-Token": cfg.ApiToken, }, nil) - checkCode(code, http.StatusOK) + checkCode(code, model.CodeOk) } @@ -308,17 +307,20 @@ func doHttpRequest0(method string, path string, headers map[string]string, body if err != nil { panic(err) } + if response.StatusCode != http.StatusOK { + panic(fmt.Sprintf("http request failed, status code: %d", response.StatusCode)) + } return response.StatusCode, respBody } func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) { - code, respBody := doHttpRequest0(method, path, headers, body) + _, respBody := doHttpRequest0(method, path, headers, body) var r model.Result[T] if err := json.Unmarshal(respBody, &r); err != nil { panic(err) } - return code, &r + return int(r.Code), &r } func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) { @@ -332,11 +334,11 @@ func httpRequestCheckOk[T any](method string, path string, body any) T { } func checkOk(code int) { - checkCode(code, 200) + checkCode(code, model.CodeOk) } -func checkCode(code int, exceptCode int) { - if code != exceptCode { +func checkCode(code int, exceptCode model.RespCode) { + if code != int(exceptCode) { panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode)) } } diff --git a/pkg/rest/util/util.go b/pkg/rest/util/util.go deleted file mode 100644 index d6d3433e..00000000 --- a/pkg/rest/util/util.go +++ /dev/null @@ -1,29 +0,0 @@ -package util - -import ( - "encoding/json" - "github.com/GopeedLab/gopeed/pkg/rest/model" - "net/http" -) - -func ReadJson(w http.ResponseWriter, r *http.Request, v any) bool { - if err := json.NewDecoder(r.Body).Decode(v); err != nil { - WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) - return false - } - return true -} - -func WriteJsonOk(w http.ResponseWriter, v any) { - if v == nil { - WriteJson(w, http.StatusOK, model.NewResultWithData[any](nil)) - } else { - WriteJson(w, http.StatusOK, v) - } -} - -func WriteJson(w http.ResponseWriter, code int, v any) { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(code) - json.NewEncoder(w).Encode(v) -} diff --git a/ui/flutter/lib/api/api.dart b/ui/flutter/lib/api/api.dart index 1a558c97..00f760de 100644 --- a/ui/flutter/lib/api/api.dart +++ b/ui/flutter/lib/api/api.dart @@ -70,15 +70,17 @@ Future _parse( try { var resp = await fetch(); if (fromJsonT != null) { - return Result.fromJson(resp.data, fromJsonT).data as T; + final result = Result.fromJson(resp.data, fromJsonT); + if (result.code == 0) { + return result.data as T; + } else { + throw Exception(result); + } } else { return null as T; } } on DioError catch (e) { - if (e.response == null) { - throw Exception("Server error"); - } - throw Exception(Result.fromJson(e.response?.data, (_) => null).msg); + throw Exception(Result(code: 1000, msg: e.message)); } } diff --git a/ui/flutter/lib/api/model/result.dart b/ui/flutter/lib/api/model/result.dart index aade7e9a..7c312ed8 100644 --- a/ui/flutter/lib/api/model/result.dart +++ b/ui/flutter/lib/api/model/result.dart @@ -4,10 +4,12 @@ part 'result.g.dart'; @JsonSerializable(genericArgumentFactories: true) class Result { + int code; String? msg; T? data; Result({ + required this.code, this.msg, this.data, }); @@ -18,6 +20,7 @@ class Result { ) => _$ResultFromJson(json, fromJsonT); Map toJson() => { + 'code': code, 'msg': msg, 'data': data is List ? (data as dynamic)?.map((e) => e.toJson()).toList() diff --git a/ui/flutter/lib/api/model/result.g.dart b/ui/flutter/lib/api/model/result.g.dart index 4420a502..452a7c7c 100644 --- a/ui/flutter/lib/api/model/result.g.dart +++ b/ui/flutter/lib/api/model/result.g.dart @@ -11,6 +11,7 @@ Result _$ResultFromJson( T Function(Object? json) fromJsonT, ) => Result( + code: json['code'] as int, msg: json['msg'] as String?, data: _$nullableGenericFromJson(json['data'], fromJsonT), ); @@ -19,7 +20,9 @@ Map _$ResultToJson( Result instance, Object? Function(T value) toJsonT, ) { - final val = {}; + final val = { + 'code': instance.code, + }; void writeNotNull(String key, dynamic value) { if (value != null) { diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index fe0fa714..bb64588e 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -1004,5 +1004,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.0 <4.0.0" + dart: ">=2.18.0 <3.0.0" flutter: ">=3.3.0"