From 15df864ca1a2b7dfa68f93e1a625e3a602028b06 Mon Sep 17 00:00:00 2001 From: fanjindong <765912710@qq.com> Date: Thu, 2 Jan 2020 19:20:50 +0800 Subject: [PATCH] feat: init requests base on go --- .gitignore | 2 + error.go | 15 +++++++ go.mod | 11 +++++ go.sum | 50 +++++++++++++++++++++++ main_test.go | 49 +++++++++++++++++++++++ option.go | 62 ++++++++++++++++++++++++++++ requests.go | 52 ++++++++++++++++++++++++ requests_test.go | 79 ++++++++++++++++++++++++++++++++++++ response.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++ session.go | 99 +++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 521 insertions(+) create mode 100644 error.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main_test.go create mode 100644 option.go create mode 100644 requests.go create mode 100644 requests_test.go create mode 100644 response.go create mode 100644 session.go diff --git a/.gitignore b/.gitignore index 66fd13c..bbfd2cb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +.idea \ No newline at end of file diff --git a/error.go b/error.go new file mode 100644 index 0000000..7333ddf --- /dev/null +++ b/error.go @@ -0,0 +1,15 @@ +package requests + +import "github.com/pkg/errors" + +var ( + ErrInvalidJson = errors.New("invalid Json value") + + // ErrUnrecognizedEncoding will be throwed while changing response encoding + // if encoding is not recognized + ErrUnrecognizedEncoding = errors.New("Unrecognized encoding") + + // ErrInvalidMethod will be throwed when method not in + // [HEAD, GET, POST, DELETE, OPTIONS, PUT, PATCH, CONNECT, TRACE] + ErrInvalidMethod = errors.New("Method is invalid") +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec4c962 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/fanjindong/go-requests + +go 1.12 + +require ( + github.com/ajg/form v1.5.1 + github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 + github.com/gin-gonic/gin v1.5.0 + github.com/pkg/errors v0.8.1 + github.com/stretchr/testify v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..36136c7 --- /dev/null +++ b/go.sum @@ -0,0 +1,50 @@ +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ= +github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..a359dba --- /dev/null +++ b/main_test.go @@ -0,0 +1,49 @@ +package requests + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "os" + "testing" +) + +func getHandler(c *gin.Context) { + c.JSON(200, gin.H{ + "url": c.Request.URL.String(), + }) +} + +func postHandler(c *gin.Context) { + reqJson := make(map[string]interface{}) + + if p := c.Query("params"); p != "" { + reqJson["params"] = p + } + + type form struct { + Data string `form:"form"` + } + f := form{} + _ = c.ShouldBindWith(&f, binding.FormPost) + if f.Data != "" { + reqJson["form"] = f.Data + } + + _ = c.ShouldBindJSON(&reqJson) + c.JSON(200, gin.H{ + "data": reqJson, + }) +} + +func TestMain(m *testing.M) { + r := gin.Default() + + r.GET("/get", getHandler) + r.POST("/post", postHandler) + r.PUT("/put", postHandler) + + go r.Run() // 监听并在 0.0.0.0:8080 上启动服务 + + code := m.Run() + os.Exit(code) +} diff --git a/option.go b/option.go new file mode 100644 index 0000000..fe06b60 --- /dev/null +++ b/option.go @@ -0,0 +1,62 @@ +package requests + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/ajg/form" + "github.com/pkg/errors" + "io/ioutil" + "net/http" + "strings" +) + +type Option interface { + ApplyClient(client *http.Client) + ApplyRequest(req *http.Request) error +} + +type Params map[string]interface{} + +func (p Params) ApplyClient(client *http.Client) {} + +func (p Params) ApplyRequest(req *http.Request) error { + var rawQuery []string + if req.URL.RawQuery != "" { + rawQuery = append(rawQuery, req.URL.RawQuery) + } + + for key, value := range p { + rawQuery = append(rawQuery, fmt.Sprintf("%s=%s", key, value)) + } + req.URL.RawQuery = strings.Join(rawQuery, "&") + return nil +} + +type Json map[string]interface{} + +func (j Json) ApplyClient(client *http.Client) {} +func (j Json) ApplyRequest(req *http.Request) error { + jsonBytes, err := json.Marshal(j) + if err != nil { + return errors.Wrap(ErrInvalidJson, err.Error()) + } + jsonBuffer := bytes.NewBuffer(jsonBytes) + req.Body = ioutil.NopCloser(jsonBuffer) + req.Header.Set("Content-Type", "application/json") + return nil +} + +type Data map[string]interface{} + +func (d Data) ApplyClient(client *http.Client) {} +func (d Data) ApplyRequest(req *http.Request) error { + data, err := form.EncodeToString(d) + if err != nil { + return errors.Wrap(ErrInvalidJson, err.Error()) + } + dataReader := strings.NewReader(data) + req.Body = ioutil.NopCloser(dataReader) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return nil +} diff --git a/requests.go b/requests.go new file mode 100644 index 0000000..ca634c9 --- /dev/null +++ b/requests.go @@ -0,0 +1,52 @@ +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 new file mode 100644 index 0000000..0fe1be7 --- /dev/null +++ b/requests_test.go @@ -0,0 +1,79 @@ +package requests + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +var BaseUrl = "http://127.0.0.1:8080" + +func TestGet(t *testing.T) { + type input struct { + url string + params Params + } + + tests := []struct { + input input + want string + }{ + {input: input{url: BaseUrl + "/get", params: Params{"a": "1"}}, want: "/get?a=1"}, + {input: input{url: BaseUrl + "/get", params: Params{"a": "1", "b": "2"}}, want: "/get?a=1&b=2"}, + {input: input{url: BaseUrl + "/get?", params: Params{"a": "1"}}, want: "/get?a=1"}, + {input: input{url: BaseUrl + "/get?a=1", params: Params{}}, want: "/get?a=1"}, + {input: input{url: BaseUrl + "/get?a=1", params: Params{"b": "2"}}, want: "/get?a=1&b=2"}, + } + + for _, ts := range tests { + resp, err := Get(ts.input.url, ts.input.params) + assert.NoError(t, err) + respData := make(map[string]interface{}) + err = resp.Json(&respData) + assert.NoError(t, err) + got := respData["url"] + assert.Equal(t, ts.want, got) + } +} + +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{Params{"params": "1"}, Json{"b": 2.2}}, want: map[string]interface{}{"params": "1", "b": 2.2}}, + } + + for _, ts := range tests { + resp, err := Post(url, ts.input...) + assert.NoError(t, err) + respData := make(map[string]interface{}) + err = resp.Json(&respData) + assert.NoError(t, err) + got := respData["data"] + assert.EqualValues(t, ts.want, got) + } +} + +func TestPut(t *testing.T) { + url := BaseUrl + "/put" + tests := []struct { + input []Option + want map[string]interface{} + }{ + {input: []Option{Json{"a": 1.1}}, want: map[string]interface{}{"a": 1.1}}, + {input: []Option{Params{"params": "1"}, Json{"b": 2.2}}, want: map[string]interface{}{"params": "1", "b": 2.2}}, + {input: []Option{Params{"params": "1"}, Data{"form": "2.2"}}, want: map[string]interface{}{"params": "1", "form": "2.2"}}, + } + + for _, ts := range tests { + resp, err := Put(url, ts.input...) + assert.NoError(t, err) + respData := make(map[string]interface{}) + err = resp.Json(&respData) + assert.NoError(t, err) + got := respData["data"] + assert.EqualValues(t, ts.want, got) + } +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..1f21fd2 --- /dev/null +++ b/response.go @@ -0,0 +1,102 @@ +package requests + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "os" + "strings" + + "github.com/axgle/mahonia" +) + +// Response is the wrapper for http.Response +type Response struct { + *http.Response + encoding string + Text string + Bytes []byte +} + +func NewResponse(r *http.Response) (*Response, error) { + resp := &Response{ + Response: r, + encoding: "utf-8", + Text: "", + Bytes: []byte{}, + } + + err := resp.bytes() + if err != nil { + return nil, err + } + resp.text() + return resp, nil +} + +func (r *Response) text() { + r.Text = string(r.Bytes) +} + +func (r *Response) bytes() error { + data, err := ioutil.ReadAll(r.Body) + if err != nil { + return err + } + + // for multiple reading + // e.g. goquery.NewDocumentFromReader + r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) + r.Bytes = data + return 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 + } + */ + err := json.Unmarshal(r.Bytes, s) + return err +} + +// SetEncode changes Response.encoding +// and it changes Response.Text every times be invoked +func (r *Response) SetEncode(e string) error { + if r.encoding != e { + r.encoding = strings.ToLower(e) + decoder := mahonia.NewDecoder(e) + if decoder == nil { + return ErrUnrecognizedEncoding + } + r.Text = decoder.ConvertString(r.Text) + } + return nil +} + +// GetEncode returns Response.encoding +func (r Response) GetEncode() string { + return r.encoding +} + +// SaveFile save bytes data to a local file +func (r Response) SaveFile(filename string) error { + dst, err := os.Create(filename) + if err != nil { + return err + } + defer dst.Close() + + _, err = dst.Write(r.Bytes) + if err != nil { + return err + } + return nil +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..8a8ed41 --- /dev/null +++ b/session.go @@ -0,0 +1,99 @@ +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...) +}