From a63749d9dac741dc296d5f23ec5f327356105a5a Mon Sep 17 00:00:00 2001 From: fanjindong Date: Mon, 30 Aug 2021 11:26:14 +0800 Subject: [PATCH] feat: refactor --- README.md | 65 +++----- client.go | 120 +++++++++++++ client_test.go | 109 ++++++++++++ error.go | 4 +- option.go | 278 ++++++++++++++----------------- option_test.go | 184 ++++++++++++++------ request.go | 20 +++ requests.go | 52 ------ requests_test.go | 87 ---------- response.go | 72 ++------ response_test.go | 33 ---- common_test.go => server_test.go | 36 ++-- session.go | 99 ----------- 13 files changed, 575 insertions(+), 584 deletions(-) create mode 100644 client.go create mode 100644 client_test.go create mode 100644 request.go delete mode 100644 requests.go delete mode 100644 requests_test.go delete mode 100644 response_test.go rename common_test.go => server_test.go (72%) delete mode 100644 session.go diff --git a/README.md b/README.md index 5a11b3a..f95af1c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Requests + ![](./images/TrakaiLithuania_ZH-CN0447602818_1920x1080.jpg) Requests is an elegant and simple HTTP library for Go. @@ -13,15 +14,16 @@ go get github.com/fanjindong/go-requests ### Make a Request -Making a request with Requests is very simple. -For this example: +Making a request with Requests is very simple. For this example: ```go resp, err := requests.Get("https://example.com/ping", requests.Params{"key": "value"}) ``` + Now, we have a Response object called resp. We can get all the information we need from this object. -Requests’ simple API means that all forms of HTTP request are as obvious. For example, this is how you make an HTTP POST request: +Requests’ simple API means that all forms of HTTP request are as obvious. For example, this is how you make an HTTP POST +request: ```go resp, err := requests.Post("https://example.com/ping", requests.Params{"k": "v"}, requests.Json{"key": "value"}) @@ -30,25 +32,27 @@ resp, err := requests.Post("https://example.com/ping", requests.Params{"k": "v"} What about the other HTTP request types: PUT, DELETE, HEAD and OPTIONS? These are all just as simple: ```go -resp, err := requests.Put("https://example.com/ping", requests.Data{"key": "value"}) +resp, err := requests.Put("https://example.com/ping", requests.Form{"key": "value"}) resp, err := requests.Delete("https://example.com/ping") resp, err := requests.Head("https://example.com/ping") resp, err := requests.Options("https://example.com/ping") ``` + That’s all well and good, but it’s also only the start of what Requests can do. ### Passing Parameters In URLs -You often want to send some sort of data in the URL’s query string. -If you were constructing the URL by hand, this data would be given as key/value pairs in the URL after a question mark, -e.g. example.com/get?key=val. Requests allows you to provide these arguments as a dictionary of strings, -using the params keyword argument. As an example, if you wanted to pass key1=value1 and key2=value2 to example.com/get, -you would use the following code: +You often want to send some sort of data in the URL’s query string. If you were constructing the URL by hand, this data +would be given as key/value pairs in the URL after a question mark, e.g. example.com/get?key=val. Requests allows you to +provide these arguments as a dictionary of strings, using the params keyword argument. As an example, if you wanted to +pass key1=value1 and key2=value2 to example.com/get, you would use the following code: ```go resp, err := requests.Get("https://example.com/get", requests.Params{"key1": "value1", "key2": "value2"}) ``` + You can see that the URL has been correctly encoded by printing the URL: + ```go fmt.Println(resp.Request.URL) //https://example.com/get?key2=value2&key1=value1 @@ -70,8 +74,8 @@ There’s also a builtin JSON decoder, in case you’re dealing with JSON data: ```go var rStruct struct{ - Code int `json:"code"` - Message string `json:"message"` +Code int `json:"code"` +Message string `json:"message"` } err := resp.Json(&rStruct) @@ -91,18 +95,17 @@ r, err := requests.Get("https://api.github.com/some/endpoint", requests.Headers{ ### More complicated POST requests -Typically, you want to send some form-encoded data — much like an HTML form. To do this, -simply pass a `requests.Data` to the data argument. -Your data will automatically be form-encoded when the request is made: +Typically, you want to send some form-encoded data — much like an HTML form. To do this, simply pass a `requests.Form` +to the data argument. Your data will automatically be form-encoded when the request is made: ```go -r, err := requests.Post("https://httpbin.org/post", requests.Data{"key1": "value1", "key2": "value2"}) -fmt.Println(r.Text) +r, err := requests.Post("https://httpbin.org/post", requests.Form{"key1": "value1", "key2": "value2"}) +fmt.Println(r.Text()) //{"code":0,"message":"pong"} ``` -For example, the GitHub API v3 accepts JSON-Encoded POST/PATCH data, -you can also pass it `requests.Json` using the json parameter and it will be encoded automatically: +For example, the GitHub API v3 accepts JSON-Encoded POST/PATCH data, you can also pass it `requests.Json` using the json +parameter and it will be encoded automatically: ```go r, err := requests.Post("https://api.github.com/some/endpoint", requests.Json{"key1": "value1", "key2": "value2"}) @@ -110,14 +113,6 @@ r, err := requests.Post("https://api.github.com/some/endpoint", requests.Json{"k Using the `requests.Json` in the request will change the Content-Type in the header to application/json. -### POST a Multipart-Encoded File - -```go -file, err := requests.FileFromPath("demo.text") - -r, err := requests.Post("https://httpbin.org/post", requests.Files{"key": "value", "file": file}) -``` - ### Response Status Codes We can check the response status code: @@ -128,29 +123,19 @@ fmt.Println(r.StatusCode) // 200 ``` -### Response Headers +### Response Header -We can view the server’s response headers: +We can view the server’s response header: ```go -fmt.Println(r.Headers) +fmt.Println(r.Header) //map[Cache-Control:[private] Content-Type:[application/json] Set-Cookie:[QINGCLOUDELB=d9a2454c187d2875afb6701eb80e9c8761ebcf3b54797eae61b25b90f71273ea; path=/; HttpOnly]] ``` + We can access the headers using Get method: ```go r.Headers.Get("Content-Type") //"application/json" -``` - -### Timeouts - -You can tell Requests to stop waiting for a response after a given number of seconds with the timeout parameter. -Nearly all production code should use this parameter in nearly all requests. -Failure to do so can cause your program to hang indefinitely: - - -```go -r, err := requests.Get("https://github.com/", requests.Params{"key": "value"}, requests.Timeout(3*time.Secend)) ``` \ No newline at end of file diff --git a/client.go b/client.go new file mode 100644 index 0000000..5725075 --- /dev/null +++ b/client.go @@ -0,0 +1,120 @@ +package requests + +import ( + "net/http" + "strings" +) + +const ( + version = "0.0.1" + userAgent = "go-requests/" + version + author = "fanjindong" +) + +const ( + GET = "GET" + POST = "POST" + PUT = "PUT" + DELETE = "DELETE" + OPTIONS = "OPTIONS" + PATCH = "PATCH" + HEAD = "HEAD" +) + +func Get(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(GET, url, opts...) +} + +func Post(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(POST, url, opts...) +} + +func Put(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(PUT, url, opts...) +} + +func Delete(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(DELETE, url, opts...) +} + +func ReqOptions(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(OPTIONS, url, opts...) +} + +func Patch(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(PATCH, url, opts...) +} + +func Head(url string, opts ...ReqOption) (*Response, error) { + return DefaultClient.Request(HEAD, url, opts...) +} + +type Client struct { + *http.Client +} + +var DefaultClient = &Client{http.DefaultClient} + +func NewClient(opts ...ClientOption) *Client { + c := &Client{&http.Client{}} + for _, opt := range opts { + opt(c) + } + return c +} + +func (s *Client) Request(method, url string, opts ...ReqOption) (*Response, error) { + method = strings.ToUpper(method) + switch method { + case HEAD, GET, POST, DELETE, OPTIONS, PUT, PATCH: + default: + return nil, ErrInvalidMethod + } + + req, err := NewRequest(method, url) + if err != nil { + return nil, err + } + + for _, opt := range opts { + err = opt.Do(req) + if err != nil { + return nil, err + } + } + + resp, err := s.Do(req.Request) + if err != nil { + return nil, err + } + + return NewResponse(resp) +} + +func (s *Client) Get(url string, opts ...ReqOption) (*Response, error) { + return s.Request(GET, url, opts...) +} + +func (s *Client) Post(url string, opts ...ReqOption) (*Response, error) { + return s.Request(POST, url, opts...) +} + +func (s *Client) Put(url string, opts ...ReqOption) (*Response, error) { + return s.Request(PUT, url, opts...) +} + +func (s *Client) Delete(url string, opts ...ReqOption) (*Response, error) { + return s.Request(DELETE, url, opts...) +} + +func (s *Client) ReqOptions(url string, opts ...ReqOption) (*Response, error) { + return s.Request(OPTIONS, url, opts...) +} + +func (s *Client) Patch(url string, opts ...ReqOption) (*Response, error) { + return s.Request(PATCH, url, opts...) +} + +func (s *Client) Head(url string, opts ...ReqOption) (*Response, error) { + return s.Request(HEAD, url, opts...) +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..c35ebb6 --- /dev/null +++ b/client_test.go @@ -0,0 +1,109 @@ +package requests + +import ( + "fmt" + "reflect" + "testing" + "time" +) + +var testUrl = fmt.Sprintf("http://127.0.0.1:%d", port) + +func TestGet(t *testing.T) { + type args struct { + url string + option []ReqOption + } + tests := []struct { + name string + args args + want map[string]string + wantErr bool + }{ + {name: "1", args: args{url: testUrl + "/get", option: []ReqOption{}}, want: map[string]string{}}, + {name: "2", args: args{url: testUrl + "/get", option: []ReqOption{Params{"a": "1"}}}, want: map[string]string{"a": "1"}}, + {name: "3", args: args{url: testUrl + "/get", option: []ReqOption{Params{"a": "1", "b": "x"}}}, want: map[string]string{"a": "1", "b": "x"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Get(tt.args.url, tt.args.option...) + if (err != nil) != tt.wantErr { + t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr) + return + } + got := map[string]string{} + if err := resp.Json(&got); err != nil { + t.Errorf("resp.Json() error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Get() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPost(t *testing.T) { + type args struct { + url string + option []ReqOption + } + tests := []struct { + name string + args args + want map[string]interface{} + wantErr bool + }{ + {name: "1", args: args{url: testUrl + "/post", option: []ReqOption{}}, want: map[string]interface{}{}}, + {name: "2", args: args{url: testUrl + "/post", option: []ReqOption{Json{"a": "1"}}}, want: map[string]interface{}{"a": "1"}}, + {name: "3", args: args{url: testUrl + "/post", option: []ReqOption{Json{"a": "x", "b": 1.2}}}, want: map[string]interface{}{"a": "x", "b": 1.2}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, err := Post(tt.args.url, tt.args.option...) + if (err != nil) != tt.wantErr { + t.Errorf("Post() error = %v, wantErr %v", err, tt.wantErr) + return + } + got := map[string]interface{}{} + if err := resp.Json(&got); err != nil { + t.Errorf("resp.Json() error = %v", err) + return + } + //for k := range got { + // if got[k] != tt.want[k] { + // t.Errorf("Post() k = %v, got = %+v, want %+v", k, got[k].(int), tt.want[k].(int)) + // } + //} + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Post() got = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestReuseConnection(t *testing.T) { + for i := 0; i < 10; i++ { + resp, err := Get(testUrl) + //resp, err := http.Get(testUrl) + if err != nil { + t.Log(err) + return + } + //data, _ := ioutil.ReadAll(resp.Body) + t.Log(resp.Status, resp.Text()) + //resp.Body.Close() + time.Sleep(1000 * time.Millisecond) + } +} + +//BenchmarkGetRequest-8 27895 43212 ns/op +func BenchmarkGetRequest(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := Get(testUrl) + if err != nil { + panic(err) + } + //resp.Text() + } +} diff --git a/error.go b/error.go index 6ac00c8..16e58ba 100644 --- a/error.go +++ b/error.go @@ -3,7 +3,7 @@ package requests import "github.com/pkg/errors" var ( - // ErrInvalidJson will be throw out when request form body data can not be Marshal + // ErrInvalidForm will be throw out when request form body data can not be Marshal ErrInvalidForm = errors.New("go-requests: Invalid Form value") // ErrInvalidJson will be throw out when request json body data can not be Marshal @@ -11,7 +11,7 @@ var ( // ErrUnrecognizedEncoding will be throw out while changing response encoding // if encoding is not recognized - ErrUnrecognizedEncoding = errors.New("go-requests: Unrecognized encoding") + //ErrUnrecognizedEncoding = errors.New("go-requests: Unrecognized encoding") // ErrInvalidMethod will be throw out when method not in // [HEAD, GET, POST, DELETE, OPTIONS, PUT, PATCH, CONNECT, TRACE] diff --git a/option.go b/option.go index 3f574aa..2415b20 100644 --- a/option.go +++ b/option.go @@ -3,50 +3,49 @@ package requests import ( "bytes" "encoding/json" - "fmt" - "io" + "github.com/ajg/form" + "github.com/pkg/errors" "io/ioutil" - "mime/multipart" "net/http" - "net/textproto" "net/url" - "os" - "path/filepath" "strings" "time" - - "github.com/ajg/form" - "github.com/pkg/errors" ) -type Option interface { - ApplyClient(client *http.Client) - ApplyRequest(req *http.Request) error +type ClientOption func(client *Client) + +func WithTimeout(timeout time.Duration) ClientOption { + return func(client *Client) { client.Timeout = timeout } } -//TODO 增加开关 来校验 Json, Form, File 只能用一个 +func WithTransport(transport http.RoundTripper) ClientOption { + return func(client *Client) { client.Transport = transport } +} + +func WithCheckRedirect(checkRedirect func(req *http.Request, via []*http.Request) error) ClientOption { + return func(client *Client) { client.CheckRedirect = checkRedirect } +} -type Headers map[string]string +func WithJar(jar http.CookieJar) ClientOption { + return func(client *Client) { client.Jar = jar } +} + +type ReqOption interface { + Do(req *Request) error +} -func (h Headers) ApplyClient(_ *http.Client) {} -func (h Headers) ApplyRequest(req *http.Request) error { +type Header map[string]string + +func (h Header) Do(req *Request) error { for key, value := range h { req.Header.Set(key, value) } return nil } -type Timeout time.Duration - -func (t Timeout) ApplyClient(client *http.Client) { - client.Timeout = time.Duration(t) -} -func (t Timeout) ApplyRequest(_ *http.Request) error { return nil } - type Params map[string]string -func (p Params) ApplyClient(_ *http.Client) {} -func (p Params) ApplyRequest(req *http.Request) error { +func (p Params) Do(req *Request) error { if len(p) == 0 { return nil } @@ -63,8 +62,7 @@ func (p Params) ApplyRequest(req *http.Request) error { type Json map[string]interface{} -func (j Json) ApplyClient(_ *http.Client) {} -func (j Json) ApplyRequest(req *http.Request) error { +func (j Json) Do(req *Request) error { req.Header.Set("Content-Type", "application/json") if len(j) == 0 { return nil @@ -78,10 +76,9 @@ func (j Json) ApplyRequest(req *http.Request) error { return nil } -type JsonArray []map[string]interface{} +type Jsons []Json -func (j JsonArray) ApplyClient(_ *http.Client) {} -func (j JsonArray) ApplyRequest(req *http.Request) error { +func (j Jsons) Do(req *Request) error { req.Header.Set("Content-Type", "application/json") if len(j) == 0 { return nil @@ -95,32 +92,14 @@ func (j JsonArray) ApplyRequest(req *http.Request) error { return nil } -type Data map[string]interface{} - -func (d Data) ApplyClient(_ *http.Client) {} -func (d Data) ApplyRequest(req *http.Request) error { - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if len(d) == 0 { - return nil - } - data, err := form.EncodeToString(d) - if err != nil { - return errors.Wrap(ErrInvalidForm, err.Error()) - } - dataReader := strings.NewReader(data) - req.Body = ioutil.NopCloser(dataReader) - return nil -} - -type DataArray []map[string]interface{} +type Form map[string]string -func (d DataArray) ApplyClient(_ *http.Client) {} -func (d DataArray) ApplyRequest(req *http.Request) error { +func (f Form) Do(req *Request) error { req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if len(d) == 0 { + if len(f) == 0 { return nil } - data, err := form.EncodeToString(d) + data, err := form.EncodeToString(f) if err != nil { return errors.Wrap(ErrInvalidForm, err.Error()) } @@ -129,106 +108,105 @@ func (d DataArray) ApplyRequest(req *http.Request) error { return nil } -// File is a struct that is used to specify the file that a User wishes to upload. -type File struct { - // Filename is the name of the file that you wish to upload. We use this to guess the mimetype as well as pass it onto the server - FileName string - // FileContent is happy as long as you pass it a io.ReadCloser (which most file use anyways) - FileContent io.ReadCloser - // MimeType represents which mimetime should be sent along with the file. - // When empty, defaults to application/octet-stream - MimeType string -} - -// FName changes file's filename in multipart form -// invoke it in a chain -func (f *File) FName(filename string) *File { - f.FileName = filename - return f -} - -// MIME changes file's mime type in multipart form -// invoke it in a chain -func (f *File) MIME(mimeType string) *File { - f.MimeType = mimeType - return f -} - -// FileContents returns a new file struct -func FileContents(filename string, content string) *File { - return &File{FileContent: ioutil.NopCloser(strings.NewReader(content)), FileName: filename} -} - -// FilePath returns a file struct from file path -func FilePath(filePath string) (*File, error) { - fd, err := os.Open(filePath) - if err != nil { - return nil, errors.Wrap(ErrInvalidFile, err.Error()) - } - return &File{FileContent: fd, FileName: filepath.Base(filePath)}, nil -} - -type Files map[string]interface{} - -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } - -func (f Files) ApplyClient(_ *http.Client) {} -func (f Files) ApplyRequest(req *http.Request) error { - buffer := &bytes.Buffer{} - multipartWriter := multipart.NewWriter(buffer) - - for key, value := range f { - switch value := value.(type) { - case *File: - if value.FileContent == nil || value.FileName == "" { - return ErrInvalidFile - } - var writer io.Writer - var err error - - if value.MimeType == "" { - writer, err = multipartWriter.CreateFormFile(key, value.FileName) - } else { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(value.FileName))) - h.Set("Content-Type", value.MimeType) - writer, err = multipartWriter.CreatePart(h) - } - if err != nil { - return errors.Wrap(ErrInvalidFile, err.Error()) - } - - if _, err = io.Copy(writer, value.FileContent); err != nil && err != io.EOF { - return errors.Wrap(ErrInvalidFile, err.Error()) - } - - if err := value.FileContent.Close(); err != nil { - return errors.Wrap(ErrInvalidFile, err.Error()) - } - case string: - err := multipartWriter.WriteField(key, value) - if err != nil { - return errors.Wrap(ErrInvalidFile, err.Error()) - } - default: - return errors.Wrap(ErrInvalidFile, fmt.Sprintf("invalid value: %+v", value)) - } - } - - if err := multipartWriter.Close(); err != nil { - return err - } - req.Body = ioutil.NopCloser(buffer) - req.Header.Add("Content-Type", multipartWriter.FormDataContentType()) - return nil -} - +// +//// File is a struct that is used to specify the file that a User wishes to upload. +//type File struct { +// // Filename is the name of the file that you wish to upload. We use this to guess the mimetype as well as pass it onto the server +// FileName string +// // FileContent is happy as long as you pass it a io.ReadCloser (which most file use anyways) +// FileContent io.ReadCloser +// // MimeType represents which mimetime should be sent along with the file. +// // When empty, defaults to application/octet-stream +// MimeType string +//} +// +//// FName changes file's filename in multipart form +//// invoke it in a chain +//func (f *File) FName(filename string) *File { +// f.FileName = filename +// return f +//} +// +//// MIME changes file's mime type in multipart form +//// invoke it in a chain +//func (f *File) MIME(mimeType string) *File { +// f.MimeType = mimeType +// return f +//} +// +//// FileContents returns a new file struct +//func FileContents(filename string, content string) *File { +// return &File{FileContent: ioutil.NopCloser(strings.NewReader(content)), FileName: filename} +//} +// +//// FilePath returns a file struct from file path +//func FilePath(filePath string) (*File, error) { +// fd, err := os.Open(filePath) +// if err != nil { +// return nil, errors.Wrap(ErrInvalidFile, err.Error()) +// } +// return &File{FileContent: fd, FileName: filepath.Base(filePath)}, nil +//} +// +//type Files map[string]interface{} +// +//var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") +// +//func escapeQuotes(s string) string { return quoteEscaper.Replace(s) } +// +//func (f Files) Do(req *Request) error { +// buffer := &bytes.Buffer{} +// multipartWriter := multipart.NewWriter(buffer) +// +// for key, value := range f { +// switch value := value.(type) { +// case *File: +// if value.FileContent == nil || value.FileName == "" { +// return ErrInvalidFile +// } +// var writer io.Writer +// var err error +// +// if value.MimeType == "" { +// writer, err = multipartWriter.CreateFormFile(key, value.FileName) +// } else { +// h := make(textproto.MIMEHeader) +// h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(key), escapeQuotes(value.FileName))) +// h.Set("Content-Type", value.MimeType) +// writer, err = multipartWriter.CreatePart(h) +// } +// if err != nil { +// return errors.Wrap(ErrInvalidFile, err.Error()) +// } +// +// if _, err = io.Copy(writer, value.FileContent); err != nil && err != io.EOF { +// return errors.Wrap(ErrInvalidFile, err.Error()) +// } +// +// if err := value.FileContent.Close(); err != nil { +// return errors.Wrap(ErrInvalidFile, err.Error()) +// } +// case string: +// err := multipartWriter.WriteField(key, value) +// if err != nil { +// return errors.Wrap(ErrInvalidFile, err.Error()) +// } +// default: +// return errors.Wrap(ErrInvalidFile, fmt.Sprintf("invalid value: %+v", value)) +// } +// } +// +// if err := multipartWriter.Close(); err != nil { +// return err +// } +// req.Body = ioutil.NopCloser(buffer) +// req.Header.Add("Content-Type", multipartWriter.FormDataContentType()) +// return nil +//} +// type Cookies map[string]string -func (c Cookies) ApplyClient(_ *http.Client) {} -func (c Cookies) ApplyRequest(req *http.Request) error { +func (c Cookies) Do(req *Request) error { for name, value := range c { req.AddCookie(&http.Cookie{Name: name, Value: value}) } diff --git a/option_test.go b/option_test.go index 66f75bd..0249780 100644 --- a/option_test.go +++ b/option_test.go @@ -6,73 +6,163 @@ import ( "time" ) -func TestTimeout(t *testing.T) { - url := baseUrl + "/timeout" +func TestWithTimeout(t *testing.T) { + url := testUrl + "/timeout" + type args struct { + timeout time.Duration + } tests := []struct { - input []Option + name string + args args wantError bool }{ - {input: []Option{Timeout(4 * time.Second)}, wantError: false}, - {input: []Option{Timeout(3100 * time.Millisecond)}, wantError: false}, - {input: []Option{Timeout(3000 * time.Millisecond)}, wantError: true}, + {args: args{}}, + {args: args{timeout: 2 * time.Second}}, + {args: args{timeout: 1100 * time.Millisecond}}, + {args: args{timeout: 1000 * time.Millisecond}, wantError: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := NewClient(WithTimeout(tt.args.timeout)) + _, err := client.Get(url) + if !reflect.DeepEqual(err != nil, tt.wantError) { + t.Errorf("WithTimeout() err = %v, wantError %v", err, tt.wantError) + } + }) } +} +func TestHeaders(t *testing.T) { + url := testUrl + "/header" + type args struct { + headers []ReqOption + } + tests := []struct { + name string + args args + want map[string][]string + }{ + {args: args{}, want: map[string][]string{"Accept-Encoding": {"gzip"}, "User-Agent": {userAgent}}}, + {args: args{headers: []ReqOption{Header{"a": "1"}}}, want: map[string][]string{"Accept-Encoding": {"gzip"}, "User-Agent": {userAgent}, "A": {"1"}}}, + {args: args{headers: []ReqOption{Header{"a": "1", "b": "2"}}}, want: map[string][]string{"Accept-Encoding": {"gzip"}, "User-Agent": {userAgent}, "A": {"1"}, "B": {"2"}}}, + {args: args{headers: []ReqOption{Header{"a": "1"}, Header{"b": "2"}}}, want: map[string][]string{"Accept-Encoding": {"gzip"}, "User-Agent": {userAgent}, "A": {"1"}, "B": {"2"}}}, + } for _, tt := range tests { - _, err := Get(url, tt.input...) - if !reflect.DeepEqual(err != nil, tt.wantError) { - t.Errorf("Get() err = %v, wantError %v", err, tt.wantError) - } + t.Run(tt.name, func(t *testing.T) { + resp, _ := Get(url, tt.args.headers...) + got := make(map[string][]string) + _ = resp.Json(&got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Header() got = %v, want %v", got, tt.want) + } + }) } } -//func TestCookies(t *testing.T) { -// url := baseUrl -// tests := []struct { -// input []Option -// want map[string]interface{} -// }{ -// {input: []Option{Cookies{"name": "fjd"}}, want: map[string]interface{}{"name": "fjd"}}, -// {input: []Option{Cookies{"name": "fjd"}, Cookies{"age": "18"}}, want: map[string]interface{}{"name": "fjd", "age": "18"}}, -// {input: []Option{Json{"a": 2.1}, Cookies{"name": "fjd"}, Cookies{"age": "18"}}, want: map[string]interface{}{"a": 2.1, "name": "fjd", "age": "18"}}, -// } -// -// for _, tt := range tests { -// resp, err := Post(url, tt.input...) -// assert.NoError(t, err) -// respData := make(map[string]interface{}) -// err = resp.Json(&respData) -// assert.NoError(t, err) -// got := respData["data"] -// assert.EqualValues(t, tt.want, got) -// } -//} +func TestParams(t *testing.T) { + url := testUrl + "/get" + type args struct { + opts []ReqOption + } + tests := []struct { + name string + args args + want map[string]string + }{ + {args: args{}, want: map[string]string{}}, + {args: args{opts: []ReqOption{Params{"a": "1"}}}, want: map[string]string{"a": "1"}}, + {args: args{opts: []ReqOption{Params{"a": "1", "b": "2"}}}, want: map[string]string{"a": "1", "b": "2"}}, + {args: args{opts: []ReqOption{Params{"a": "1"}, Params{"b": "2"}}}, want: map[string]string{"a": "1", "b": "2"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, _ := Get(url, tt.args.opts...) + got := make(map[string]string) + _ = resp.Json(&got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Params() got = %v, want %v", got, tt.want) + } + }) + } +} -func TestOptions(t *testing.T) { +func TestJson(t *testing.T) { + url := testUrl + "/post" type args struct { - method string - url string - options []Option + opts []ReqOption } tests := []struct { - name string - args args - wantErr bool - want map[string]interface{} + name string + args args + want map[string]interface{} }{ - {name: "1", args: args{method: "GET", url: baseUrl + "/get", options: []Option{Params{"a": "1"}}}, wantErr: false, want: map[string]interface{}{"a": "1"}}, + {args: args{}, want: map[string]interface{}{}}, + {args: args{opts: []ReqOption{Json{"a": "1"}}}, want: map[string]interface{}{"a": "1"}}, + {args: args{opts: []ReqOption{Json{"a": "1", "b": 2}}}, want: map[string]interface{}{"a": "1", "b": float64(2)}}, + {args: args{opts: []ReqOption{Json{"a": "1"}, Json{"b": "2"}}}, want: map[string]interface{}{"b": "2"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - response, err := session.Request(tt.args.method, tt.args.url, tt.args.options...) - if (err != nil) != tt.wantErr { - t.Errorf("Request() error = %v, wantErr %v", err, tt.wantErr) + resp, _ := Post(url, tt.args.opts...) + got := make(map[string]interface{}) + _ = resp.Json(&got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Json() got = %v, want %v", got, tt.want) } - got := map[string]interface{}{} - if err = response.Json(&got); (err != nil) != tt.wantErr { - t.Errorf("response.Json() error = %v, wantErr %v", err, tt.wantErr) + }) + } +} + +func TestJsons(t *testing.T) { + url := testUrl + "/post" + type args struct { + opts []ReqOption + } + tests := []struct { + name string + args args + want []map[string]interface{} + }{ + {args: args{}, want: []map[string]interface{}{}}, + {args: args{opts: []ReqOption{Jsons{{"a": "1"}}}}, want: []map[string]interface{}{{"a": "1"}}}, + {args: args{opts: []ReqOption{Jsons{{"a": "1"}, {"b": 2}}}}, want: []map[string]interface{}{{"a": "1"}, {"b": float64(2)}}}, + {args: args{opts: []ReqOption{Jsons{{"a": "1", "b": 2}}}}, want: []map[string]interface{}{{"a": "1", "b": float64(2)}}}, + {args: args{opts: []ReqOption{Jsons{{"a": "1"}}, Jsons{{"b": "2"}}}}, want: []map[string]interface{}{{"b": "2"}}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, _ := Post(url, tt.args.opts...) + got := make([]map[string]interface{}, 0) + _ = resp.Json(&got) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Jsons() got = %v, want %v", got, tt.want) } + }) + } +} + +func TestForm(t *testing.T) { + url := testUrl + "/post" + type args struct { + opts []ReqOption + } + tests := []struct { + name string + args args + want map[string]interface{} + }{ + {args: args{}, want: map[string]interface{}{}}, + {args: args{opts: []ReqOption{Form{"a": "1"}}}, want: map[string]interface{}{"a": "1"}}, + {args: args{opts: []ReqOption{Form{"a": "1", "b": "2"}}}, want: map[string]interface{}{"a": "1", "b": "2"}}, + {args: args{opts: []ReqOption{Form{"a": "1"}, Form{"b": "2"}}}, want: map[string]interface{}{"b": "2"}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resp, _ := Post(url, tt.args.opts...) + got := make(map[string]interface{}) + _ = resp.Json(&got) if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Get() got = %v, want %v", got, tt.want) + t.Errorf("Form() got = %v, want %v", got["b"].(int), tt.want) } }) } diff --git a/request.go b/request.go new file mode 100644 index 0000000..45b9950 --- /dev/null +++ b/request.go @@ -0,0 +1,20 @@ +package requests + +import ( + "context" + "net/http" +) + +type Request struct { + *http.Request +} + +// NewRequest wraps NewRequestWithContext using the background context. +func NewRequest(method, url string) (*Request, error) { + r, err := http.NewRequestWithContext(context.Background(), method, url, nil) + if err != nil { + return nil, err + } + r.Header.Set("User-Agent", userAgent) + return &Request{r}, nil +} diff --git a/requests.go b/requests.go deleted file mode 100644 index ca634c9..0000000 --- a/requests.go +++ /dev/null @@ -1,52 +0,0 @@ -package requests - -const ( - version = "0.0.1" - userAgent = "go-requests/" + version - author = "fanjindong" -) - -const ( - GET = "GET" - POST = "POST" - PUT = "PUT" - DELETE = "DELETE" - OPTIONS = "OPTIONS" - PATCH = "PATCH" - HEAD = "HEAD" -) - -func Get(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(GET, url, option...) -} - -func Post(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(POST, url, option...) -} - -func Put(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(PUT, url, option...) -} - -func Delete(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(DELETE, url, option...) -} - -func Options(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(OPTIONS, url, option...) -} - -func Patch(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(PATCH, url, option...) -} - -func Head(url string, option ...Option) (*Response, error) { - s := NewSession() - return s.Request(HEAD, url, option...) -} diff --git a/requests_test.go b/requests_test.go deleted file mode 100644 index fca5b50..0000000 --- a/requests_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package requests - -import ( - "github.com/stretchr/testify/assert" - "reflect" - "testing" -) - -var ( - baseUrl = "http://127.0.0.1:8080" - session *Session -) - -func TestGet(t *testing.T) { - type input struct { - url string - params Params - } - - tests := []struct { - input input - want map[string]string - }{ - {input: input{url: baseUrl + "/get", params: Params{"a": "1"}}, want: map[string]string{"a": "1"}}, - {input: input{url: baseUrl + "/get", params: Params{"a": "1", "b": "2"}}, want: map[string]string{"a": "1", "b": "2"}}, - {input: input{url: baseUrl + "/get?"}, want: map[string]string{}}, - {input: input{url: baseUrl + "/get?a=1", params: Params{}}, want: map[string]string{"a": "1"}}, - {input: input{url: baseUrl + "/get?a=1", params: Params{"b": "2"}}, want: map[string]string{"a": "1", "b": "2"}}, - } - - for _, tt := range tests { - r, err := Get(tt.input.url, tt.input.params) - if err != nil { - panic(err) - } - got := map[string]string{} - if err = r.Json(&got); err != nil { - t.Log(r.Text()) - panic(err) - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Get() got = %v, want %v", got, tt.want) - } - } -} - -func TestPost(t *testing.T) { - url := baseUrl + "/post" - tests := []struct { - input []Option - want map[string]interface{} - }{ - {input: []Option{Json{"a": 1.1}}, want: map[string]interface{}{"a": 1.1}}, - {input: []Option{Json{"a": 1.1, "b": 2.2}}, want: map[string]interface{}{"a": 1.1, "b": 2.2}}, - {input: []Option{Data{"a": 1.1, "b": 2.2}}, want: map[string]interface{}{"a": "1.1", "b": "2.2"}}, - } - - for _, tt := range tests { - resp, err := Post(url, tt.input...) - if err != nil { - panic(err) - } - got := make(map[string]interface{}) - if err = resp.Json(&got); err != nil { - panic(err) - } - assert.EqualValues(t, tt.want, got) - } -} - -func TestFiles(t *testing.T) { - _, err := FilePath("./go.mod") - if err != nil { - panic(err) - } - _ = FileContents("demo.text", "123 \n") -} - -func BenchmarkGetRequest(b *testing.B) { - for i := 0; i < b.N; i++ { - _, err := Get(baseUrl) - if err != nil { - panic(err) - } - //resp.Text() - } -} diff --git a/response.go b/response.go index cd100f7..b68c508 100644 --- a/response.go +++ b/response.go @@ -5,36 +5,30 @@ import ( "io/ioutil" "net/http" "os" - "strings" - - "github.com/axgle/mahonia" ) +var unmarshal = json.Unmarshal + +//SetUnmarshal Set custom Unmarshal functions, default is json.Unmarshal +func SetUnmarshal(f func(data []byte, v interface{}) error) { + unmarshal = f +} + // Response is the wrapper for http.Response type Response struct { *http.Response - encoding string - bytes []byte - Headers *http.Header + bytes []byte } func NewResponse(r *http.Response) (*Response, error) { - resp := &Response{ - Response: r, - encoding: "utf-8", - Headers: &r.Header, - } + resp := &Response{Response: r} _, err := resp.Bytes() _ = r.Body.Close() return resp, err } -func (r *Response) Text() (string, error) { - if bt, err := r.Bytes(); err != nil { - return "", err - } else { - return string(bt), nil - } +func (r *Response) Text() string { + return string(r.bytes) } func (r *Response) Bytes() ([]byte, error) { @@ -43,56 +37,14 @@ func (r *Response) Bytes() ([]byte, error) { if err != nil { return nil, err } - - // for multiple reading - // e.g. goquery.NewDocumentFromReader - //r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) - - if r.encoding != "utf-8" { - data = []byte(mahonia.NewDecoder(r.encoding).ConvertString(string(data))) - } r.bytes = data } - return r.bytes, nil } // Json could parse http json response func (r Response) Json(s interface{}) error { - // Json response not must be `application/json` type - // maybe `text/plain`...etc. - // requests will parse it regardless of the content-type - /* - cType := r.Header.Get("Content-Type") - if !strings.Contains(cType, "json") { - return ErrNotJsonResponse - } - */ - if bt, err := r.Bytes(); err != nil { - return err - } else { - return json.Unmarshal(bt, s) - } -} - -// SetEncode changes Response.encoding -// and it changes Response.Text every times be invoked -func (r *Response) SetEncode(e string) error { - if r.encoding != e { - if mahonia.NewDecoder(e) == nil { - return ErrUnrecognizedEncoding - } - r.encoding = strings.ToLower(e) - if r.bytes != nil { - r.bytes = []byte(mahonia.NewDecoder(r.encoding).ConvertString(string(r.bytes))) - } - } - return nil -} - -// GetEncode returns Response.encoding -func (r Response) GetEncode() string { - return r.encoding + return unmarshal(r.bytes, s) } // SaveFile save bytes data to a local file diff --git a/response_test.go b/response_test.go deleted file mode 100644 index 402b75b..0000000 --- a/response_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package requests - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func TestResponse(t *testing.T) { - url := baseUrl - resp, err := Get(url) - assert.NoError(t, err) - - assert.Equal(t, "application/json; charset=utf-8", resp.Headers.Get("Content-Type")) - assert.Equal(t, "application/json; charset=utf-8", resp.Headers.Get("content-type")) -} - -func TestResponse_SetEncode(t *testing.T) { - resp := &Response{bytes: []byte("你好")} - err := resp.SetEncode("utf-8") - assert.NoError(t, err) - t.Log(resp.GetEncode()) - t.Log(resp.Text()) - - err = resp.SetEncode("GBK") - assert.NoError(t, err) - t.Log(resp.GetEncode()) - t.Log(resp.Text()) - - err = resp.SetEncode("ASCII") - assert.NoError(t, err) - t.Log(resp.GetEncode()) - t.Log(resp.Text()) -} diff --git a/common_test.go b/server_test.go similarity index 72% rename from common_test.go rename to server_test.go index b7d204f..f6353f1 100644 --- a/common_test.go +++ b/server_test.go @@ -2,6 +2,7 @@ package requests import ( "encoding/json" + "fmt" "io/ioutil" "net/http" "os" @@ -9,6 +10,8 @@ import ( "time" ) +var port = 8080 + func handler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) } @@ -16,7 +19,6 @@ func handler(w http.ResponseWriter, r *http.Request) { func getHandler(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() params := map[string]string{} - //fmt.Printf("params: %+v", query) for k, v := range query { params[k] = v[0] } @@ -26,7 +28,6 @@ func getHandler(w http.ResponseWriter, r *http.Request) { func postHandler(w http.ResponseWriter, r *http.Request) { contentType := r.Header.Get("Content-Type") - data := map[string]interface{}{} switch contentType { case "application/json": req, err := ioutil.ReadAll(r.Body) @@ -35,27 +36,36 @@ func postHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("invalid body: " + err.Error())) return } - if err = json.Unmarshal(req, &data); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("parse body err: " + err.Error())) - return - } + w.Write(req) + return case "application/x-www-form-urlencoded": if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("parse body: " + err.Error())) return } + data := map[string]interface{}{} for k, v := range r.PostForm { data[k] = v[0] } + body, _ := json.Marshal(data) + w.Write(body) + return } - body, _ := json.Marshal(data) - w.Write(body) } func timeoutHandler(w http.ResponseWriter, r *http.Request) { - time.Sleep(3 * time.Second) + time.Sleep(1 * time.Second) + w.Write([]byte("OK")) +} + +func headerHandler(w http.ResponseWriter, r *http.Request) { + bytes, _ := json.Marshal(r.Header) + w.Write(bytes) +} + +func cooclerHandler(w http.ResponseWriter, r *http.Request) { + r.Cookies() w.Write([]byte("OK")) } @@ -64,14 +74,12 @@ func TestMain(m *testing.M) { http.HandleFunc("/get", getHandler) http.HandleFunc("/post", postHandler) http.HandleFunc("/timeout", timeoutHandler) + http.HandleFunc("/header", headerHandler) go func() { - if err := http.ListenAndServe(":8080", nil); err != nil { + if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil { panic(err) } }() - session = NewSession() - //time.Sleep(1 * time.Second) - code := m.Run() os.Exit(code) } diff --git a/session.go b/session.go deleted file mode 100644 index 8a8ed41..0000000 --- a/session.go +++ /dev/null @@ -1,99 +0,0 @@ -package requests - -import ( - "errors" - "net/http" - "strings" - "sync" -) - -type Session struct { - client *http.Client - req *http.Request - sync.Mutex -} - -func NewSession() *Session { - client := &http.Client{} - return &Session{client: client} -} - -func (s *Session) Request(method, url string, option ...Option) (*Response, error) { - s.Lock() - defer s.Unlock() - - method = strings.ToUpper(method) - switch method { - case HEAD, GET, POST, DELETE, OPTIONS, PUT, PATCH: - default: - return nil, ErrInvalidMethod - } - - req, err := http.NewRequest(method, url, nil) - if err != nil { - return nil, err - } - - req.Header.Set("User-Agent", userAgent) - req.Close = true - - for _, opt := range option { - opt.ApplyClient(s.client) - err = opt.ApplyRequest(req) - if err != nil { - return nil, err - } - } - defer func() { // all client config will be restored to the default value after every request - s.client.CheckRedirect = defaultCheckRedirect - s.client.Timeout = 0 - s.client.Transport = &http.Transport{} - }() - - resp, err := s.client.Do(req) - if err != nil { - return nil, err - } - - return NewResponse(resp) -} - -// http's defaultCheckRedirect -func defaultCheckRedirect(req *http.Request, via []*http.Request) error { - if len(via) >= 10 { - return errors.New("stopped after 10 redirects") - } - return nil -} - -func (s *Session) GetRequest() *http.Request { - return s.req -} - -func (s *Session) Get(url string, option ...Option) (*Response, error) { - return s.Request(GET, url, option...) -} - -func (s *Session) Post(url string, option ...Option) (*Response, error) { - return s.Request(POST, url, option...) -} - -func (s *Session) Put(url string, option ...Option) (*Response, error) { - return s.Request(PUT, url, option...) -} - -func (s *Session) Delete(url string, option ...Option) (*Response, error) { - return s.Request(DELETE, url, option...) -} - -func (s *Session) Options(url string, option ...Option) (*Response, error) { - return s.Request(OPTIONS, url, option...) -} - -func (s *Session) Patch(url string, option ...Option) (*Response, error) { - return s.Request(PATCH, url, option...) -} - -func (s *Session) Head(url string, option ...Option) (*Response, error) { - return s.Request(HEAD, url, option...) -}