Skip to content

Commit

Permalink
feat: encode request body using content-type header
Browse files Browse the repository at this point in the history
  • Loading branch information
sonirico committed Aug 30, 2022
1 parent d22e91f commit acd972b
Show file tree
Hide file tree
Showing 12 changed files with 349 additions and 83 deletions.
95 changes: 82 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Build http requests and parse their responses with fluent syntax and wit. This package aims
to quickly configure http roundtrips by covering common scenarios, while leaving all details
of http requests and responses open for the user to allow maximun flexibility.
of http requests and responses open for developers to allow maximum flexibility.

Supported underlying http implementations are:

Expand All @@ -12,19 +12,48 @@ Supported underlying http implementations are:

#### Query Restful endpoints

```go
type GithubRepoInfo struct {
ID int `json:"id"`
URL string `json:"html_url"`
}

func GetRepoInfo(user, repo string) (GithubRepoInfo, error) {

call := withttp.NewCall[GithubRepoInfo](withttp.NewDefaultFastHttpHttpClientAdapter()).
WithURL(fmt.Sprintf("https://api.github.com/repos/%s/%s", user, repo)).
WithMethod(http.MethodGet).
WithHeader("User-Agent", "withttp/0.1.0 See https://github.com/sonirico/withttp", false).
WithParseJSON().
WithExpectedStatusCodes(http.StatusOK)

err := call.Call(context.Background())

return call.BodyParsed, err
}

func main() {
info, _ := GetRepoInfo("sonirico", "withttp")
log.Println(info)
}
```

In case of a wide range catalog of endpoints, predefined parameters and behaviours can be
defined by employing an endpoint definition.

```go
var (
githubApi = withttp.NewEndpoint("GithubAPI").
Request(withttp.WithURL("https://api.github.com/"))
Request(withttp.WithBaseURL("https://api.github.com/"))
)

type githubRepoInfo struct {
type GithubRepoInfo struct {
ID int `json:"id"`
URL string `json:"html_url"`
}

func GetRepoInfo(user, repo string) (githubRepoInfo, error) {
call := withttp.NewCall[githubRepoInfo](withttp.NewDefaultFastHttpHttpClientAdapter()).
func GetRepoInfo(user, repo string) (GithubRepoInfo, error) {
call := withttp.NewCall[GithubRepoInfo](withttp.NewDefaultFastHttpHttpClientAdapter()).
WithURI(fmt.Sprintf("repos/%s/%s", user, repo)).
WithMethod(http.MethodGet).
WithHeader("User-Agent", "withttp/0.1.0 See https://github.com/sonirico/withttp", false).
Expand All @@ -34,17 +63,63 @@ func GetRepoInfo(user, repo string) (githubRepoInfo, error) {
override = true
return
}).
WithJSON().
WithParseJSON().
WithExpectedStatusCodes(http.StatusOK)

err := call.Call(context.Background(), githubApi)
err := call.CallEndpoint(context.Background(), githubApi)

return call.BodyParsed, err
}

type GithubCreateIssueResponse struct {
ID int `json:"id"`
URL string `json:"url"`
}

func CreateRepoIssue(user, repo, title, body, assignee string) (GithubCreateIssueResponse, error) {
type payload struct {
Title string `json:"title"`
Body string `json:"body"`
Assignee string `json:"assignee"`
}

p := payload{
Title: title,
Body: body,
Assignee: assignee,
}

call := withttp.NewCall[GithubCreateIssueResponse](
withttp.NewDefaultFastHttpHttpClientAdapter(),
).
WithURI(fmt.Sprintf("repos/%s/%s/issues", user, repo)).
WithMethod(http.MethodPost).
WithContentType("application/vnd+github+json").
WithBody(p).
WithHeaderFunc(func() (key, value string, override bool) {
key = "Authorization"
value = fmt.Sprintf("Bearer %s", "S3cret")
override = true
return
}).
WithExpectedStatusCodes(http.StatusCreated)

err := call.CallEndpoint(context.Background(), githubApi)

log.Println("req body", string(call.Req.Body()))

return call.BodyParsed, err
}

func main() {
// Fetch repo info
info, _ := GetRepoInfo("sonirico", "withttp")
log.Println(info)

// Create an issue
res, err := CreateRepoIssue("sonirico", "withttp", "test",
"This is a test", "sonirico")
log.Println(res, err)
}
```

Expand Down Expand Up @@ -79,12 +154,6 @@ func main() {
WithURL("https://github.com/").
WithMethod(http.MethodGet).
WithHeader("User-Agent", "withttp/0.1.0 See https://github.com/sonirico/withttp", false).
WithHeaderFunc(func() (key, value string, override bool) {
key = "X-Date"
value = time.Now().String()
override = true
return
}).
WithJSONEachRowChan(res).
WithExpectedStatusCodes(http.StatusOK)

Expand Down
8 changes: 8 additions & 0 deletions adapter_fasthttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ func (a *fastHttpReqAdapter) SetBody(body []byte) {
a.req.SetBody(body)
}

func (a *fastHttpReqAdapter) Body() []byte {
return a.req.Body()
}

func (a *fastHttpReqAdapter) BodyStream() io.ReadCloser {
return io.NopCloser(bytes.NewReader(a.req.Body()))
}

func (a *fastHttpReqAdapter) URL() *url.URL {
uri := a.req.URI()

Expand Down
9 changes: 9 additions & 0 deletions adapter_native.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ func (a *nativeReqAdapter) SetBody(payload []byte) {
a.req.Body = io.NopCloser(bytes.NewReader(payload))
}

func (a *nativeReqAdapter) Body() []byte {
bts, _ := io.ReadAll(a.req.Body)
return bts
}

func (a *nativeReqAdapter) BodyStream() io.ReadCloser {
return a.req.Body
}

func (a *nativeReqAdapter) URL() *url.URL {
return a.req.URL
}
Expand Down
107 changes: 66 additions & 41 deletions call.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,18 @@ type (
Parse(c *Call[T], r Response) error
}

CalReqOption[T any] interface {
Configure(c *Call[T], r Request) error
}

CallReqOptionFunc[T any] func(c *Call[T], res Request) error

CallResOptionFunc[T any] func(c *Call[T], res Response) error

Call[T any] struct {
client client

reqOptions []ReqOption
reqOptions []ReqOption // TODO: Linked Lists
resOptions []ResOption

Req Request
Expand All @@ -26,14 +32,19 @@ type (
BodyRaw []byte
BodyParsed T

ReqBodyRaw []byte
ReqContentType ContentType
ReqBodyRaw []byte
}
)

func (f CallResOptionFunc[T]) Parse(c *Call[T], res Response) error {
return f(c, res)
}

func (f CallReqOptionFunc[T]) Configure(c *Call[T], req Request) error {
return f(c, req)
}

func NewCall[T any](client client) *Call[T] {
return &Call[T]{client: client}
}
Expand All @@ -57,10 +68,12 @@ func (c *Call[T]) withRes(fn CallResOption[T]) *Call[T] {
return c
}

func (c *Call[T]) withReq(fn ReqOption) *Call[T] {
func (c *Call[T]) withReq(fn CallReqOptionFunc[T]) *Call[T] {
c.reqOptions = append(
c.reqOptions,
fn,
ReqOptionFunc(func(req Request) error {
return fn.Configure(c, req)
}),
)
return c
}
Expand All @@ -84,9 +97,7 @@ func (c *Call[T]) configureReq(req Request) error {
}

func (c *Call[T]) Request(opts ...ReqOption) *Call[T] {
for _, opt := range opts {
c.withReq(opt)
}
c.reqOptions = append(c.reqOptions, opts...)
return c
}

Expand All @@ -95,8 +106,36 @@ func (c *Call[T]) Response(opts ...ResOption) *Call[T] {
return c
}

func (c *Call[T]) Call(ctx context.Context, e *Endpoint) (err error) {
func (c *Call[T]) Call(ctx context.Context) (err error) {
req, err := c.client.Request()
defer func() { c.Req = req }()

if err != nil {
return
}

if err = c.configureReq(req); err != nil {
return
}

res, err := c.client.Do(ctx, req)

if err != nil {
return
}

defer func() { c.Res = res }()

if err = c.parseRes(res); err != nil {
return
}

return
}

func (c *Call[T]) CallEndpoint(ctx context.Context, e *Endpoint) (err error) {
req, err := c.client.Request()
defer func() { c.Req = req }()

if err != nil {
return
Expand All @@ -118,6 +157,8 @@ func (c *Call[T]) Call(ctx context.Context, e *Endpoint) (err error) {
return
}

defer func() { c.Res = res }()

for _, opt := range e.responseOpts {
if err = opt.Parse(res); err != nil {
return
Expand All @@ -132,65 +173,49 @@ func (c *Call[T]) Call(ctx context.Context, e *Endpoint) (err error) {
}

func (c *Call[T]) WithURL(raw string) *Call[T] {
return c.withReq(WithURL(raw))
return c.withReq(WithURL[T](raw))
}

func (c *Call[T]) WithURI(raw string) *Call[T] {
return c.withReq(WithURI(raw))
return c.withReq(WithURI[T](raw))
}

func (c *Call[T]) WithMethod(method string) *Call[T] {
return c.withReq(
ReqOptionFunc(func(req Request) error {
req.SetMethod(method)
return nil
}),
)
return c.withReq(WithMethod[T](method))
}

// WithBodyStream receives a stream of data to set on the request. Second parameter `bodySize` indicates
// the estimated content-length of this stream. Required when employing fasthttp http client.
func (c *Call[T]) WithBodyStream(rc io.ReadCloser, bodySize int) *Call[T] {
return c.withReq(
ReqOptionFunc(func(req Request) error {
req.SetBodyStream(rc, bodySize)
return nil
}),
)
return c.withReq(WithBodyStream[T](rc, bodySize))
}

func (c *Call[T]) WithBody(payload any) *Call[T] {
return c.withReq(WithBody[T](payload))
}

func (c *Call[T]) WithRawBody(payload []byte) *Call[T] {
return c.withReq(
ReqOptionFunc(func(req Request) error {
req.SetBody(payload)
return nil
}),
)
return c.withReq(WithRawBody[T](payload))
}

func (c *Call[T]) WithContentLength(length int) *Call[T] {
return c.WithHeader("content-length", strconv.FormatInt(int64(length), 10), true)
}

func (c *Call[T]) WithHeader(key, value string, override bool) *Call[T] {
return c.withReq(
ReqOptionFunc(func(req Request) error {
return ConfigureHeader(req, key, value, override)
}),
)
return c.withReq(WithHeader[T](key, value, override))
}

func (c *Call[T]) WithHeaderFunc(fn func() (key, value string, override bool)) *Call[T] {
return c.withReq(
ReqOptionFunc(func(req Request) error {
key, value, override := fn()
return ConfigureHeader(req, key, value, override)
}),
)
return c.withReq(WithHeaderFunc[T](fn))
}

func (c *Call[T]) WithContentType(ct ContentType) *Call[T] {
return c.withReq(WithContentType[T](ct))
}

func (c *Call[T]) WithReadBody() *Call[T] {
return c.withRes(WithRawBody[T]())
return c.withRes(WithParseBodyRaw[T]())
}

func (c *Call[T]) WithStreamChan(factory StreamFactory[T], ch chan<- T) *Call[T] {
Expand All @@ -213,7 +238,7 @@ func (c *Call[T]) WithIgnoreBody() *Call[T] {
return c.withRes(WithIgnoredBody[T]())
}

func (c *Call[T]) WithJSON() *Call[T] {
func (c *Call[T]) WithParseJSON() *Call[T] {
return c.withRes(WithJSON[T]())
}

Expand Down
5 changes: 5 additions & 0 deletions codec/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package codec

var (
NativeJSONCodec = NewNativeJsonCodec()
)
Loading

0 comments on commit acd972b

Please sign in to comment.