Skip to content

Commit 3a1e00d

Browse files
authored
Merge pull request #17 from lxzan/dev
添加WithBaseURL选项
2 parents 0d45eaf + 192629a commit 3a1e00d

File tree

8 files changed

+400
-27
lines changed

8 files changed

+400
-27
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,19 @@ after := hasaki.WithAfter(func(ctx context.Context, response *http.Response) (co
132132
var url = "https://api.github.com/search/repositories"
133133
cli, _ := hasaki.NewClient(before, after)
134134
cli.Get(url).Send(nil)
135+
```
136+
137+
#### Base URL
138+
139+
You can set a base URL for all requests made by the client.
140+
141+
```go
142+
// Create a client with base URL
143+
cli, _ := hasaki.NewClient(hasaki.WithBaseURL("https://api.example.com"))
144+
145+
// GET https://api.example.com/users
146+
resp := cli.Get("/users").Send(nil)
147+
148+
// GET https://api.example.com/api/v1/products
149+
resp := cli.Get("/api/v1/products").Send(nil)
135150
```

client.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88
)
99

10+
// Client HTTP客户端
11+
// HTTP client
1012
type Client struct {
1113
config *config
1214
}
@@ -23,39 +25,60 @@ func NewClient(options ...Option) (*Client, error) {
2325
return client, nil
2426
}
2527

28+
// Get 发送GET请求
29+
// Send GET request
2630
func (c *Client) Get(url string, args ...any) *Request {
2731
return c.Request(http.MethodGet, url, args...)
2832
}
2933

34+
// Post 发送POST请求
35+
// Send POST request
3036
func (c *Client) Post(url string, args ...any) *Request {
3137
return c.Request(http.MethodPost, url, args...)
3238
}
3339

40+
// Put 发送PUT请求
41+
// Send PUT request
3442
func (c *Client) Put(url string, args ...any) *Request {
3543
return c.Request(http.MethodPut, url, args...)
3644
}
3745

46+
// Delete 发送DELETE请求
47+
// Send DELETE request
3848
func (c *Client) Delete(url string, args ...any) *Request {
3949
return c.Request(http.MethodDelete, url, args...)
4050
}
4151

52+
// Head 发送HEAD请求
53+
// Send HEAD request
4254
func (c *Client) Head(url string, args ...any) *Request {
4355
return c.Request(http.MethodHead, url, args...)
4456
}
4557

58+
// Options 发送OPTIONS请求
59+
// Send OPTIONS request
4660
func (c *Client) Options(url string, args ...any) *Request {
4761
return c.Request(http.MethodOptions, url, args...)
4862
}
4963

64+
// Patch 发送PATCH请求
65+
// Send PATCH request
5066
func (c *Client) Patch(url string, args ...any) *Request {
5167
return c.Request(http.MethodPatch, url, args...)
5268
}
5369

70+
// Request 创建HTTP请求
71+
// Create HTTP request
5472
func (c *Client) Request(method string, url string, args ...any) *Request {
5573
if len(args) > 0 {
5674
url = fmt.Sprintf(url, args...)
5775
}
5876

77+
// 如果配置了 BaseURL,则合并 URL
78+
if c.config.BaseURL != "" {
79+
url = c.config.BaseURL + url
80+
}
81+
5982
r := &Request{
6083
ctx: context.Background(),
6184
client: c.config.HTTPClient,

client_test.go

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package hasaki
22

33
import (
44
"context"
5+
"encoding/xml"
56
"fmt"
67
"net"
78
"net/http"
89
"net/url"
910
"strconv"
11+
"strings"
1012
"sync/atomic"
1113
"testing"
1214
"time"
@@ -263,6 +265,7 @@ func TestMiddleware(t *testing.T) {
263265

264266
after := WithAfter(func(ctx context.Context, response *http.Response) (context.Context, error) {
265267
t0 := ctx.Value("t0").(time.Time)
268+
time.Sleep(time.Millisecond)
266269
return context.WithValue(ctx, "latency", time.Since(t0).Nanoseconds()), nil
267270
})
268271

@@ -410,3 +413,191 @@ func TestRequest_ReadBody(t *testing.T) {
410413
assert.NoError(t, err)
411414
})
412415
}
416+
417+
func TestWithBaseURL(t *testing.T) {
418+
addr := nextAddr()
419+
srv := &http.Server{Addr: addr}
420+
srv.Handler = http.Handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
421+
switch request.URL.Path {
422+
case "/api/users":
423+
writer.WriteHeader(http.StatusOK)
424+
case "/users":
425+
writer.WriteHeader(http.StatusOK)
426+
default:
427+
writer.WriteHeader(http.StatusOK)
428+
}
429+
}))
430+
go srv.ListenAndServe()
431+
time.Sleep(100 * time.Millisecond)
432+
433+
t.Run("base url with relative path", func(t *testing.T) {
434+
baseURL := "http://" + addr
435+
cli, _ := NewClient(WithBaseURL(baseURL))
436+
req := cli.Get("/api/users")
437+
expectedURL := baseURL + "/api/users"
438+
assert.Equal(t, req.url, expectedURL)
439+
})
440+
441+
t.Run("base url with absolute path", func(t *testing.T) {
442+
baseURL := "http://" + addr
443+
cli, _ := NewClient(WithBaseURL(baseURL))
444+
req := cli.Get("http://example.com/users")
445+
// 当传入绝对 URL 时,应该直接使用该 URL(当前实现是字符串拼接)
446+
expectedURL := baseURL + "http://example.com/users"
447+
assert.Equal(t, req.url, expectedURL)
448+
})
449+
450+
t.Run("base url with empty path", func(t *testing.T) {
451+
baseURL := "http://" + addr
452+
cli, _ := NewClient(WithBaseURL(baseURL))
453+
req := cli.Get("")
454+
expectedURL := baseURL
455+
assert.Equal(t, req.url, expectedURL)
456+
})
457+
458+
t.Run("base url with formatted path", func(t *testing.T) {
459+
baseURL := "http://" + addr
460+
cli, _ := NewClient(WithBaseURL(baseURL))
461+
req := cli.Get("/api/%s", "users")
462+
expectedURL := baseURL + "/api/users"
463+
assert.Equal(t, req.url, expectedURL)
464+
})
465+
466+
t.Run("base url actual request", func(t *testing.T) {
467+
baseURL := "http://" + addr
468+
cli, _ := NewClient(WithBaseURL(baseURL))
469+
resp := cli.Get("/api/users").Send(nil)
470+
assert.NoError(t, resp.Err())
471+
assert.Equal(t, resp.StatusCode, http.StatusOK)
472+
})
473+
474+
t.Run("base url without trailing slash", func(t *testing.T) {
475+
baseURL := "http://" + addr
476+
cli, _ := NewClient(WithBaseURL(baseURL))
477+
req := cli.Get("/users")
478+
expectedURL := baseURL + "/users"
479+
assert.Equal(t, req.url, expectedURL)
480+
})
481+
482+
t.Run("base url with trailing slash", func(t *testing.T) {
483+
baseURL := "http://" + addr + "/"
484+
cli, _ := NewClient(WithBaseURL(baseURL))
485+
req := cli.Get("users")
486+
expectedURL := baseURL + "users"
487+
assert.Equal(t, req.url, expectedURL)
488+
})
489+
}
490+
491+
func TestResponse_BindJSON(t *testing.T) {
492+
addr := nextAddr()
493+
srv := &http.Server{Addr: addr}
494+
srv.Handler = http.Handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
495+
writer.WriteHeader(http.StatusOK)
496+
writer.Write([]byte(`{"name":"test","age":18}`))
497+
}))
498+
go srv.ListenAndServe()
499+
time.Sleep(100 * time.Millisecond)
500+
501+
t.Run("bind json success", func(t *testing.T) {
502+
type User struct {
503+
Name string `json:"name"`
504+
Age int `json:"age"`
505+
}
506+
var user User
507+
resp := Get("http://%s", addr).Send(nil)
508+
err := resp.BindJSON(&user)
509+
assert.NoError(t, err)
510+
assert.Equal(t, user.Name, "test")
511+
assert.Equal(t, user.Age, 18)
512+
})
513+
514+
t.Run("bind json with error response", func(t *testing.T) {
515+
type User struct {
516+
Name string `json:"name"`
517+
}
518+
var user User
519+
resp := Get("http://127.0.0.1:xx").Send(nil)
520+
err := resp.BindJSON(&user)
521+
assert.Error(t, err)
522+
})
523+
}
524+
525+
func TestResponse_BindXML(t *testing.T) {
526+
addr := nextAddr()
527+
srv := &http.Server{Addr: addr}
528+
srv.Handler = http.Handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
529+
writer.WriteHeader(http.StatusOK)
530+
writer.Write([]byte(`<user><name>test</name><age>18</age></user>`))
531+
}))
532+
go srv.ListenAndServe()
533+
time.Sleep(100 * time.Millisecond)
534+
535+
t.Run("bind xml success", func(t *testing.T) {
536+
type User struct {
537+
XMLName xml.Name `xml:"user"`
538+
Name string `xml:"name"`
539+
Age int `xml:"age"`
540+
}
541+
var user User
542+
resp := Get("http://%s", addr).Send(nil)
543+
err := resp.BindXML(&user)
544+
assert.NoError(t, err)
545+
assert.Equal(t, user.Name, "test")
546+
assert.Equal(t, user.Age, 18)
547+
})
548+
}
549+
550+
func TestResponse_BindForm(t *testing.T) {
551+
addr := nextAddr()
552+
srv := &http.Server{Addr: addr}
553+
srv.Handler = http.Handler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
554+
writer.WriteHeader(http.StatusOK)
555+
writer.Write([]byte("name=test&age=18"))
556+
}))
557+
go srv.ListenAndServe()
558+
time.Sleep(100 * time.Millisecond)
559+
560+
t.Run("bind form success", func(t *testing.T) {
561+
var params url.Values
562+
resp := Get("http://%s", addr).Send(nil)
563+
err := resp.BindForm(&params)
564+
assert.NoError(t, err)
565+
assert.Equal(t, params.Get("name"), "test")
566+
assert.Equal(t, params.Get("age"), "18")
567+
})
568+
}
569+
570+
func TestRequest_SetEncoder(t *testing.T) {
571+
req := Get("https://api.example.com")
572+
req.SetEncoder(FormCodec)
573+
assert.Equal(t, req.headers.Get("Content-Type"), MimeForm)
574+
575+
req.SetEncoder(JsonCodec)
576+
assert.Equal(t, req.headers.Get("Content-Type"), MimeJson)
577+
578+
req.SetEncoder(XmlCodec)
579+
assert.Equal(t, req.headers.Get("Content-Type"), MimeXml)
580+
}
581+
582+
func TestJsonCodec_Decode(t *testing.T) {
583+
t.Run("decode success", func(t *testing.T) {
584+
type User struct {
585+
Name string `json:"name"`
586+
Age int `json:"age"`
587+
}
588+
var user User
589+
reader := strings.NewReader(`{"name":"test","age":18}`)
590+
err := JsonCodec.Decode(reader, &user)
591+
assert.NoError(t, err)
592+
assert.Equal(t, user.Name, "test")
593+
assert.Equal(t, user.Age, 18)
594+
})
595+
596+
t.Run("decode map", func(t *testing.T) {
597+
var result map[string]any
598+
reader := strings.NewReader(`{"name":"test","age":18}`)
599+
err := JsonCodec.Decode(reader, &result)
600+
assert.NoError(t, err)
601+
assert.Equal(t, result["name"], "test")
602+
})
603+
}

codec.go

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,74 @@ import (
1313
)
1414

1515
const (
16-
MimeJson = "application/json;charset=utf-8"
17-
MimeYaml = "application/x-yaml;charset=utf-8"
18-
MimeXml = "application/xml;charset=utf-8"
16+
// MimeJson JSON MIME类型
17+
// JSON MIME type
18+
MimeJson = "application/json;charset=utf-8"
19+
// MimeYaml YAML MIME类型
20+
// YAML MIME type
21+
MimeYaml = "application/x-yaml;charset=utf-8"
22+
// MimeXml XML MIME类型
23+
// XML MIME type
24+
MimeXml = "application/xml;charset=utf-8"
25+
// MimeProtoBuf Protobuf MIME类型
26+
// Protobuf MIME type
1927
MimeProtoBuf = "application/x-protobuf"
20-
MimeForm = "application/x-www-form-urlencoded"
21-
MimeStream = "application/octet-stream"
22-
MimeJpeg = "image/jpeg"
23-
MimeGif = "image/gif"
24-
MimePng = "image/png"
25-
MimeMp4 = "video/mpeg4"
28+
// MimeForm 表单 MIME类型
29+
// Form MIME type
30+
MimeForm = "application/x-www-form-urlencoded"
31+
// MimeStream 流式 MIME类型
32+
// Stream MIME type
33+
MimeStream = "application/octet-stream"
34+
// MimeJpeg JPEG图片 MIME类型
35+
// JPEG image MIME type
36+
MimeJpeg = "image/jpeg"
37+
// MimeGif GIF图片 MIME类型
38+
// GIF image MIME type
39+
MimeGif = "image/gif"
40+
// MimePng PNG图片 MIME类型
41+
// PNG image MIME type
42+
MimePng = "image/png"
43+
// MimeMp4 MP4视频 MIME类型
44+
// MP4 video MIME type
45+
MimeMp4 = "video/mpeg4"
2646
)
2747

48+
// Any 通用类型映射
49+
// Generic type map
2850
type Any map[string]any
2951

3052
type (
53+
// Codec 编解码器接口,同时包含编码和解码功能
54+
// Codec interface, includes both encoding and decoding functionality
3155
Codec interface {
3256
Encoder
3357
Decoder
3458
}
3559

60+
// Encoder 编码器接口
61+
// Encoder interface
3662
Encoder interface {
3763
Encode(v any) (io.Reader, error)
3864
ContentType() string
3965
}
4066

67+
// Decoder 解码器接口
68+
// Decoder interface
4169
Decoder interface {
4270
Decode(r io.Reader, v any) error
4371
}
4472
)
4573

4674
var (
75+
// JsonCodec JSON编解码器
76+
// JSON codec
4777
JsonCodec = new(jsonCodec)
78+
// FormCodec 表单编解码器
79+
// Form codec
4880
FormCodec = new(formCodec)
49-
XmlCodec = new(xmlCodec)
81+
// XmlCodec XML编解码器
82+
// XML codec
83+
XmlCodec = new(xmlCodec)
5084
)
5185

5286
type (
@@ -130,6 +164,8 @@ type streamEncoder struct {
130164
contentType string
131165
}
132166

167+
// NewStreamEncoder 创建流式编码器
168+
// Create a stream encoder
133169
func NewStreamEncoder(contentType string) Encoder {
134170
return &streamEncoder{contentType: contentType}
135171
}

0 commit comments

Comments
 (0)