Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: initialize client && request #1

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 263 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
/*
* Copyright 2024 CloudWeGo Authors
*
* 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 easy_http

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"sync"

"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/common/config"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)

type Client struct {
baseURL string
header http.Header

beforeRequest []RequestMiddleware
afterResponse []ResponseMiddleware
afterResponseLock *sync.RWMutex

enableDiscovery bool

client *client.Client
options []config.ClientOption
}

type (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

middleware 可以不用支持, hertz client 本身就有 mw 能力,可以把 hertz client mw 以配置的形式注入,就别在 封装的这一层再搞一层 Middleware 了

Copy link

@FGYFFFF FGYFFFF May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

看了下面这个 Middleware 是在用来创建 hertz 的 Request;这块进行不要对外暴露就好,保证easy_http 内部可用就好

RequestMiddleware func(*Client, *Request) error
ResponseMiddleware func(*Client, *Response) error
)

var (
hdrContentTypeKey = http.CanonicalHeaderKey(consts.HeaderContentType)
hostHeader = "Host"

plainTextType = consts.MIMETextPlainUTF8
jsonContentType = consts.MIMEApplicationJSON
formContentType = consts.MIMEApplicationHTMLForm
formDataContentType = consts.MIMEMultipartPOSTForm
)

// createClient creates a new client instance with configured request and response middleware.
// It accepts a client.Client pointer and optional config.ClientOption parameters.
//
// For Example:
//
// cc := &client.Client{}
// opts := []config.ClientOption{...}
// client := createClient(cc, opts...)
//
// Note: This function configures middleware for request processing and response parsing.
func createClient(cc *client.Client, opts ...config.ClientOption) *Client {
c := &Client{
afterResponseLock: &sync.RWMutex{},

client: cc,
options: opts,
}

c.beforeRequest = []RequestMiddleware{
parseRequestURL,
parseRequestHeader,
createHTTPRequest,
}

c.afterResponse = []ResponseMiddleware{
parseResponseBody,
}

return c
}

// R initializes and returns a new Request instance.
// It sets up QueryParam, Header, PathParams, and RawRequest fields.
//
// For Example:
//
// client := &Client{}
// req := client.R()
//
// Note: This method does not take any parameters.
func (c *Client) R() *Request {
r := &Request{
QueryParam: url.Values{},
Header: http.Header{},
PathParams: map[string]string{},
RawRequest: &protocol.Request{},

client: c,
}
return r
}

// EnableServiceDiscovery enables service discovery for the client.
// It sets the enableDiscovery field to true and returns the modified client.
//
// Example:
//
// client.EnableServiceDiscovery()
func (c *Client) EnableServiceDiscovery() *Client {
c.enableDiscovery = true
return c
}

// UseMiddleware adds one or more middleware to the client's request processing chain.
// It returns the client instance for chaining.
//
// For Example:
//
// client.UseMiddleware(middleware1, middleware2)
func (c *Client) UseMiddleware(mws ...client.Middleware) *Client {
c.client.Use(mws...)
return c
}

// AddHeader method adds a custom HTTP header to the Client instance.
// It accepts header name and value as parameters.
//
// For Example:
//
// client.AddHeader("Authorization", "Bearer token").
// AddHeader("Content-Type", "application/json")
//
// Returns the updated Client instance for chaining.
func (c *Client) AddHeader(header, value string) *Client {
c.header.Add(header, value)
return c
}

// AddHeaders adds multiple HTTP headers to the client instance.
// It iterates over the provided map and calls AddHeader for each key-value pair.
//
// For Example:
//
// client.AddHeaders(map[string]string{
// "Authorization": "Bearer token",
// "Content-Type": "application/json",
// })
//
// Returns the updated client instance.
func (c *Client) AddHeaders(headers map[string]string) *Client {
for k, v := range headers {
c.AddHeader(k, v)
}
return c
}

// GetClient retrieves the underlying client.Client instance from the Client.
// It returns a pointer to the client.Client instance.
//
// For Example:
//
// client := &Client{client: &client.Client{}}
// underlyingClient := client.GetClient()
func (c *Client) GetClient() *client.Client {
return c.client
}

// SetBaseURL sets the base URL for the client and trims trailing slashes.
// It returns the updated client instance.
//
// For Example:
//
// client.SetBaseURL("https://example.com/")
//
// Note: trailing slashes are removed from the URL.
func (c *Client) SetBaseURL(url string) *Client {
c.baseURL = strings.TrimRight(url, "/")
return c
}

// SetServiceName sets the service name and updates the base URL.
// It formats the name as the base URL and updates the client.
//
// For Example:
//
// client.SetServiceName("example.com")
//
// Note: This method does not check the validity of the URL.
func (c *Client) SetServiceName(name string) *Client {
c.SetBaseURL(fmt.Sprintf("http://%s", name))
return c
}

// NewRequest creates a new Request instance.
// It calls the R method of the Client.
//
// For Example:
//
// req := client.NewRequest()
//
// Note: This method does not take any parameters.
func (c *Client) NewRequest() *Request {
return c.R()
}

// execute method executes an HTTP request and processes the response.
// It locks the post-request hooks to prevent concurrency issues.
//
// req := &Request{}
// resp, err := client.execute(req)
// if err != nil {
// log.Fatalf("Request failed: %v", err)
// }
//
// Note: Handles request and response middleware, and sets Host header.
func (c *Client) execute(req *Request) (*Response, error) {
// Lock the post-request hooks.
c.afterResponseLock.RLock()
defer c.afterResponseLock.RUnlock()
// Apply Request middleware
var err error
for _, f := range c.beforeRequest {
if err = f(c, req); err != nil {
return nil, err
}
}

if hostHeader := req.Header.Get(hostHeader); hostHeader != "" {
req.RawRequest.SetHost(hostHeader)
}
req.hasCreate = true

resp := &protocol.Response{}
err = c.client.Do(context.Background(), req.RawRequest, resp)
response := &Response{
Request: req,
RawResponse: resp,
}

if err != nil {
return response, err
}

// Apply Response middleware
for _, f := range c.afterResponse {
if err = f(c, response); err != nil {
break
}
}

return response, err
}
66 changes: 66 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package easy_http

import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"net/url"
"strings"
"testing"
"time"
)

func runHertz(t *testing.T) {
hertz := server.Default(server.WithHostPorts("127.0.0.1:6666"))
hertz.GET("/test_query", func(c context.Context, ctx *app.RequestContext) {
type Query struct {
Q1 []string `query:"q1"`
Q2 string `query:"q2"`
Q3 string `query:"q3"`
}
var req Query
err := ctx.BindQuery(&req)
if err != nil {
t.Fatal(err)
}
if len(req.Q1) != 2 {
t.Errorf("expected q1 has 2 element, but get %d", len(req.Q1))
}
if req.Q2 != "q2" {
t.Errorf("expected q2, but get %s", req.Q2)
}
if req.Q3 != "q3" {
t.Errorf("expected q3, but get %s", req.Q3)
}
ctx.JSON(200, map[string]string{
"q1": strings.Join(req.Q1, ","),
"q2": req.Q2,
"q3": req.Q3,
})
})

go hertz.Spin()
time.Sleep(100 * time.Millisecond)
}

func TestSetQueryParam(t *testing.T) {
runHertz(t)
//c := MustNewClient().UseMiddleware().SetBaseURL("http://127.0.0.1:6666")
res := make(map[string]string)
value := url.Values{}
value.Set("q3", "q3")
resp, err := R().
SetResult(res).
AddQueryParam("q1", "q1").
AddQueryParams(map[string]string{"q2": "q2", "q1": "q11"}).
AddQueryParamsFromValues(value).
Get(context.Background(), "http://127.0.0.1:6666/test_query")
if err != nil {
t.Fatal(err)
}
fmt.Println(resp.BodyString())
fmt.Println(resp.Result())
fmt.Println(res)

}
59 changes: 59 additions & 0 deletions easy_http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2024 CloudWeGo Authors
*
* 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 easy_http

import (
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/common/config"
"github.com/cloudwego/hertz/pkg/network/standard"
)

// NewClient creates a new client instance supporting HTTPS.
// It accepts a variadic list of config.ClientOption for configuration.
//
// For Example:
//
// client, err := NewClient(config.WithDialer(customDialer))
// if err != nil {
// log.Fatalf("Failed to create client: %v", err)
// }
//
// Note: Uses standard library dialer by default for HTTPS support.
func NewClient(opts ...config.ClientOption) (*Client, error) {
// use standard network library to support https by default
opts = append(opts, client.WithDialer(standard.NewDialer()))
c, err := client.NewClient(opts...)
return createClient(c, opts...), err
}

// MustNewClient creates a new client instance with given options.
// It defaults to using the standard library for HTTPS support.
//
// For Example:
//
// client := MustNewClient(config.WithTimeout(5 * time.Second))
//
// Note: It panics if client creation fails.
func MustNewClient(opts ...config.ClientOption) *Client {
// use standard network library to support https by default
opts = append(opts, client.WithDialer(standard.NewDialer()))
c, err := client.NewClient(opts...)
if err != nil {
panic(err)
}
return createClient(c, opts...)
}
Loading
Loading