Skip to content

Commit

Permalink
httpx: Request 和 Response 初步设计 (#236)
Browse files Browse the repository at this point in the history
* httpx Request 和 Response 初步设计

* 支持 LogRoundTrip
  • Loading branch information
flycash authored Jan 1, 2024
1 parent 14dba09 commit 5a23504
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 0 deletions.
67 changes: 67 additions & 0 deletions net/httpx/log_round_trip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"bytes"
"io"
"net/http"
)

type LogRoundTrip struct {
delegate http.RoundTripper
// l 绝对不会为 nil
log func(l Log, err error)
}

func NewLogRoundTrip(rp http.RoundTripper, log func(l Log, err error)) *LogRoundTrip {
return &LogRoundTrip{
delegate: rp,
log: log,
}
}

func (l *LogRoundTrip) RoundTrip(request *http.Request) (resp *http.Response, err error) {
log := Log{
URL: request.URL.String(),
}
defer func() {
if resp != nil {
log.RespStatus = resp.Status
if resp.Body != nil {
// 出现 error 了这里也不知道怎么处理,暂时忽略
body, _ := io.ReadAll(resp.Body)
resp.Body = io.NopCloser(bytes.NewReader(body))
log.RespBody = string(body)
}
}
l.log(log, err)
}()
if request.Body != nil {
// 出现 error 了这里也不知道怎么处理,暂时忽略
body, _ := io.ReadAll(request.Body)
request.Body = io.NopCloser(bytes.NewReader(body))
log.ReqBody = string(body)
}
resp, err = l.delegate.RoundTrip(request)
return
}

type Log struct {
URL string
ReqBody string
RespBody string
RespStatus string
}
57 changes: 57 additions & 0 deletions net/httpx/log_round_trip_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"bytes"
"context"
"io"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
)

func TestLogRoundTrip(t *testing.T) {
client := http.DefaultClient
var acceptLog Log
var acceptError error
client.Transport = NewLogRoundTrip(&doNothingRoundTrip{}, func(l Log, err error) {
acceptLog = l
acceptError = err
})
NewRequest(context.Background(),
http.MethodGet, "http://localhost/test").
JSONBody(User{Name: "Tom"}).
Client(client).
Do()
assert.Equal(t, nil, acceptError)
assert.Equal(t, Log{
URL: "http://localhost/test",
ReqBody: `{"Name":"Tom"}`,
RespBody: "resp body",
RespStatus: "200 OK",
}, acceptLog)
}

type doNothingRoundTrip struct {
}

func (d *doNothingRoundTrip) RoundTrip(request *http.Request) (*http.Response, error) {
return &http.Response{
Status: "200 OK",
Body: io.NopCloser(bytes.NewBuffer([]byte("resp body"))),
}, nil
}
72 changes: 72 additions & 0 deletions net/httpx/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"context"
"io"
"net/http"

"github.com/ecodeclub/ekit/iox"
)

type Request struct {
req *http.Request
err error
client *http.Client
}

func NewRequest(ctx context.Context, method, url string) *Request {
req, err := http.NewRequestWithContext(ctx, method, url, nil)
return &Request{
req: req,
err: err,
client: http.DefaultClient,
}
}

// JSONBody 使用 JSON body
func (req *Request) JSONBody(val any) *Request {
req.req.Body = io.NopCloser(iox.NewJSONReader(val))
req.req.Header.Set("Content-Type", "application/json")
return req
}

func (req *Request) Client(cli *http.Client) *Request {
req.client = cli
return req
}

// AddParam 添加查询参数
// 这个方法性能不好,但是好用
func (req *Request) AddParam(key string, value string) *Request {
q := req.req.URL.Query()
q.Add(key, value)
req.req.URL.RawQuery = q.Encode()
return req
}

func (req *Request) Do() *Response {
if req.err != nil {
return &Response{
err: req.err,
}
}
resp, err := req.client.Do(req.req)
return &Response{
Response: resp,
err: err,
}
}
110 changes: 110 additions & 0 deletions net/httpx/request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"context"
"errors"
"net"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestRequest_Client(t *testing.T) {
req := NewRequest(context.Background(), http.MethodPost, "/abc")
assert.Equal(t, http.DefaultClient, req.client)
cli := &http.Client{}
req = req.Client(&http.Client{})
assert.Equal(t, cli, req.client)
}

func TestRequest_JSONBody(t *testing.T) {
req := NewRequest(context.Background(), http.MethodPost, "/abc")
assert.Nil(t, req.req.Body)
req = req.JSONBody(User{})
assert.NotNil(t, req.req.Body)
assert.Equal(t, "application/json", req.req.Header.Get("Content-Type"))
}

func TestRequest_Do(t *testing.T) {
l, err := net.Listen("unix", "/tmp/test.sock")
require.NoError(t, err)
server := http.Server{}
go func() {
http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
_, _ = writer.Write([]byte("OK"))
})
_ = server.Serve(l)
}()
defer func() {
_ = l.Close()
}()
testCases := []struct {
name string
req func() *Request
wantErr error
}{
{
name: "构造请求的时候有 error",
req: func() *Request {
return &Request{
err: errors.New("mock error"),
}
},
wantErr: errors.New("mock error"),
},
{
name: "成功",
req: func() *Request {
req := NewRequest(context.Background(), http.MethodGet, "http://localhost:8081/hello")
return req.Client(&http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context,
network, addr string) (net.Conn, error) {
return net.Dial("unix", "/tmp/test.sock")
},
},
})
},
},
}

// 确保前面的 http 端口启动成功
time.Sleep(time.Second)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := tc.req()
resp := req.Do()
assert.Equal(t, tc.wantErr, resp.err)
})
}
}

func TestRequest_AddParam(t *testing.T) {
req := NewRequest(context.Background(),
http.MethodGet, "http://localhost").
AddParam("key1", "value1").
AddParam("key2", "value2")
assert.Equal(t, "http://localhost?key1=value1&key2=value2", req.req.URL.String())
}

type User struct {
Name string
}
34 changes: 34 additions & 0 deletions net/httpx/response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"encoding/json"
"net/http"
)

type Response struct {
*http.Response
err error
}

// JSONScan 将 Body 按照 JSON 反序列化为结构体
func (r *Response) JSONScan(val any) error {
if r.err != nil {
return r.err
}
err := json.NewDecoder(r.Body).Decode(val)
return err
}
54 changes: 54 additions & 0 deletions net/httpx/response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2021 ecodeclub
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package httpx

import (
"io"
"net/http"
"testing"

"github.com/ecodeclub/ekit/iox"
"github.com/stretchr/testify/assert"
)

func TestResponse_JSONScan(t *testing.T) {
testCases := []struct {
name string
resp *Response
wantVal User
wantErr error
}{
{
name: "scan成功",
resp: &Response{
Response: &http.Response{
Body: io.NopCloser(iox.NewJSONReader(User{Name: "Tom"})),
},
},
wantVal: User{
Name: "Tom",
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var u User
err := tc.resp.JSONScan(&u)
assert.Equal(t, tc.wantErr, err)
assert.Equal(t, tc.wantVal, u)
})
}
}

0 comments on commit 5a23504

Please sign in to comment.