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: added support for application/json content type in request body #239

Merged
merged 6 commits into from
Apr 4, 2024
Merged
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
[![Tests](https://github.com/twilio/twilio-go/actions/workflows/test-and-deploy.yml/badge.svg)](https://github.com/twilio/twilio-go/actions/workflows/test-and-deploy.yml)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/twilio/twilio-go)](https://pkg.go.dev/github.com/twilio/twilio-go)
[![Release](https://img.shields.io/github/release/twilio/twilio-go.svg)](https://github.com/twilio/twilio-go/releases/latest)
[![Learn OSS Contribution in TwilioQuest](https://img.shields.io/static/v1?label=TwilioQuest&message=Learn%20to%20contribute%21&color=F22F46&labelColor=1f243c&style=flat-square&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAASFBMVEUAAAAZGRkcHBwjIyMoKCgAAABgYGBoaGiAgICMjIyzs7PJycnMzMzNzc3UoBfd3d3m5ubqrhfrMEDu7u739/f4vSb/3AD///9tbdyEAAAABXRSTlMAAAAAAMJrBrEAAAKoSURBVHgB7ZrRcuI6EESdyxXGYoNFvMD//+l2bSszRgyUYpFAsXOeiJGmj4NkuWx1Qeh+Ekl9DgEXOBwOx+Px5xyQhDykfgq4wG63MxxaR4ddIkg6Ul3g84vCIcjPBA5gmUMeXESrlukuoK33+33uID8TWeLAdOWsKpJYzwVMB7bOzYSGOciyUlXSn0/ABXTosJ1M1SbypZ4O4MbZuIDMU02PMbauhhHMHXbmebmALIiEbbbbbUrpF1gwE9kFfRNAJaP+FQEXCCTGyJ4ngDrjOFo3jEL5JdqjF/pueR4cCeCGgAtwmuRS6gDwaRiGvu+DMFwSBLTE3+jF8JyuV1okPZ+AC4hDFhCHyHQjdjPHUKFDlHSJkHQXMB3KpSwXNGJPcwwTdZiXlRN0gSp0zpWxNtM0beYE0nRH6QIbO7rawwXaBYz0j78gxjokDuv12gVeUuBD0MDi0OQCLvDaAho4juP1Q/jkAncXqIcCfd+7gAu4QLMACCLxpRsSuQh0igu0C9Svhi7weAGZg50L3IE3cai4IfkNZAC8dfdhsUD3CgKBVC9JE5ABAFzg4QL/taYPAAWrHdYcgfLaIgAXWJ7OV38n1LEF8tt2TH29E+QAoDoO5Ve/LtCQDmKM9kPbvCEBApK+IXzbcSJ0cIGF6e8gpcRhUDogWZ8JnaWjPXc/fNnBBUKRngiHgTUSivSzDRDgHZQOLvBQgf8rRt+VdBUUhwkU6VpJ+xcOwQUqZr+mR0kvBUgv6cB4+37hQAkXqE8PwGisGhJtN4xAHMzrsgvI7rccXqSvKh6jltGlrOHA3Xk1At3LC4QiPdX9/0ndHpGVvTjR4bZA1ypAKgVcwE5vx74ulwIugDt8e/X7JgfkucBMIAr26ndnB4UCLnDOqvteQsHlgX9N4A+c4cW3DXSPbwAAAABJRU5ErkJggg==)](https://twil.io/learn-open-source)

All the code [here](./rest) was generated by [twilio-oai-generator](https://github.com/twilio/twilio-oai-generator) by
leveraging [openapi-generator](https://github.com/OpenAPITools/openapi-generator)
and [twilio-oai](https://github.com/twilio/twilio-oai). If you find an issue with the generation or the OpenAPI specs,
please go ahead and open an issue or a PR against the relevant repositories.

## 🚀 Feature Update
Twilio Go Helper Library's version 1.20.0 adds support for the application/json content type in the request body. See example [here](#messaging-bulk).
Behind the scenes Go Helper is now auto-generated via OpenAPI with this release.
This enables us to rapidly add new features and enhance consistency across versions and languages.

## Documentation

The documentation for the Twilio API can be found [here][apidocs].
Expand Down Expand Up @@ -297,6 +301,48 @@ func main() {
}
```

### Send Bulk Message <a id="messaging-bulk"></a>

Try sending a message to multiple recipients with JSON request body support.

```go
package main

import (
"encoding/json"
"fmt"

"github.com/twilio/twilio-go"
previewMessaging "github.com/twilio/twilio-go/rest/preview_messaging/v1"
)

func main() {
accountSid := "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
authToken := "f2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: accountSid,
Password: authToken,
})

// create multiple recipients
msg1 := previewMessaging.MessagingV1Message{To: "+XXXXXXXXXX"}
msg2 := previewMessaging.MessagingV1Message{To: "+XXXXXXXXXX"}

// create message request object
req := &previewMessaging.CreateMessagesRequest{Messages: []previewMessaging.MessagingV1Message{msg1, msg2}, Body: "Hello from Go!", From: "+XXXXXXXXXX"}
params := &previewMessaging.CreateMessagesParams{CreateMessagesRequest: req}

resp, err := client.PreviewMessagingV1.CreateMessages(params)
if err != nil {
fmt.Println("Error sending SMS message: " + err.Error())
} else {
response, _ := json.Marshal(*resp)
fmt.Println("Response: " + string(response))
}
}
```

### Iterate through records

This library also offers paging functionality. Collections such as calls and messages have `ListXxx` and `StreamXxx`
Expand Down
2 changes: 1 addition & 1 deletion client/base_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ type BaseClient interface {
AccountSid() string
SetTimeout(timeout time.Duration)
SendRequest(method string, rawURL string, data url.Values,
headers map[string]interface{}) (*http.Response, error)
headers map[string]interface{}, body ...byte) (*http.Response, error)
}
66 changes: 47 additions & 19 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
Expand Down Expand Up @@ -64,10 +65,20 @@ func (c *Client) SetTimeout(timeout time.Duration) {
c.HTTPClient.Timeout = timeout
}

func extractContentTypeHeader(headers map[string]interface{}) (cType string) {
headerType, ok := headers["Content-Type"]
if !ok {
return urlEncodedContentType
}
return headerType.(string)
}

const (
keepZeros = true
delimiter = '.'
escapee = '\\'
urlEncodedContentType = "application/x-www-form-urlencoded"
jsonContentType = "application/json"
keepZeros = true
delimiter = '.'
escapee = '\\'
)

func (c *Client) doWithErr(req *http.Request) (*http.Response, error) {
Expand Down Expand Up @@ -117,16 +128,24 @@ func (c *Client) validateCredentials() error {

// SendRequest verifies, constructs, and authorizes an HTTP request.
func (c *Client) SendRequest(method string, rawURL string, data url.Values,
headers map[string]interface{}) (*http.Response, error) {
headers map[string]interface{}, body ...byte) (*http.Response, error) {

contentType := extractContentTypeHeader(headers)

u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}

valueReader := &strings.Reader{}
goVersion := runtime.Version()
var req *http.Request

if method == http.MethodGet {
//For HTTP GET Method there are no body parameters. All other parameters like query, path etc
// are added as information in the url itself. Also while Content-Type is json, we are sending
// json body. In that case, data variable conatins all other parameters than body, which is the
//same case as GET method. In that case as well all parameters will be added to url
if method == http.MethodGet || contentType == jsonContentType {
if data != nil {
v, _ := form.EncodeToStringWith(data, delimiter, escapee, keepZeros)
s := delimitingRegex.ReplaceAllString(v, "")
Expand All @@ -135,18 +154,32 @@ func (c *Client) SendRequest(method string, rawURL string, data url.Values,
}
}

if method == http.MethodPost {
valueReader = strings.NewReader(data.Encode())
}
//data is already processed and information will be added to u(the url) in the
//previous step. Now body will solely contain json payload
if contentType == jsonContentType {
req, err = http.NewRequest(method, u.String(), bytes.NewBuffer(body))
if err != nil {
return nil, err
}
} else {
//Here the HTTP POST methods which is not having json content type are processed
//All the values will be added in data and encoded (all body, query, path parameters)
if method == http.MethodPost {
valueReader = strings.NewReader(data.Encode())
}
credErr := c.validateCredentials()
if credErr != nil {
return nil, credErr
}
req, err = http.NewRequest(method, u.String(), valueReader)
if err != nil {
return nil, err
}

credErr := c.validateCredentials()
if credErr != nil {
return nil, credErr
}

req, err := http.NewRequest(method, u.String(), valueReader)
if err != nil {
return nil, err
if contentType == urlEncodedContentType {
req.Header.Add("Content-Type", urlEncodedContentType)
}

req.SetBasicAuth(c.basicAuth())
Expand All @@ -160,14 +193,9 @@ func (c *Client) SendRequest(method string, rawURL string, data url.Values,

req.Header.Add("User-Agent", userAgent)

if method == http.MethodPost {
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
}

for k, v := range headers {
req.Header.Add(k, fmt.Sprint(v))
}

return c.doWithErr(req)
}

Expand Down
2 changes: 1 addition & 1 deletion client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func TestClient_SetAccountSid(t *testing.T) {
func TestClient_DefaultUserAgentHeaders(t *testing.T) {
headerServer := httptest.NewServer(http.HandlerFunc(
func(writer http.ResponseWriter, request *http.Request) {
assert.Regexp(t, regexp.MustCompile(`^twilio-go/[0-9.]+\s\(\w+\s\w+\)\sgo/\S+$`), request.Header.Get("User-Agent"))
assert.Regexp(t, regexp.MustCompile(`^twilio-go/[0-9.]+(-rc.[0-9])*\s\(\w+\s\w+\)\sgo/\S+$`), request.Header.Get("User-Agent"))
}))

resp, _ := testClient.SendRequest("GET", headerServer.URL, nil, nil)
Expand Down
9 changes: 4 additions & 5 deletions client/request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,12 @@ func NewRequestHandler(client BaseClient) *RequestHandler {
}

func (c *RequestHandler) sendRequest(method string, rawURL string, data url.Values,
headers map[string]interface{}) (*http.Response, error) {
headers map[string]interface{}, body ...byte) (*http.Response, error) {
parsedURL, err := c.BuildUrl(rawURL)
if err != nil {
return nil, err
}

return c.Client.SendRequest(method, parsedURL, data, headers)
return c.Client.SendRequest(method, parsedURL, data, headers, body...)
}

// BuildUrl builds the target host string taking into account region and edge configurations.
Expand Down Expand Up @@ -83,8 +82,8 @@ func (c *RequestHandler) BuildUrl(rawURL string) (string, error) {
return u.String(), nil
}

func (c *RequestHandler) Post(path string, bodyData url.Values, headers map[string]interface{}) (*http.Response, error) {
return c.sendRequest(http.MethodPost, path, bodyData, headers)
func (c *RequestHandler) Post(path string, bodyData url.Values, headers map[string]interface{}, body ...byte) (*http.Response, error) {
return c.sendRequest(http.MethodPost, path, bodyData, headers, body...)
}

func (c *RequestHandler) Get(path string, queryData url.Values, headers map[string]interface{}) (*http.Response, error) {
Expand Down
52 changes: 52 additions & 0 deletions client/request_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package client_test

import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"testing"

Expand Down Expand Up @@ -65,3 +67,53 @@ func assertAndGetURL(t *testing.T, requestHandler *client.RequestHandler, rawURL
assert.Nil(t, err)
return parsedURL
}

func TestRequestHandler_SendGetRequest(t *testing.T) {
errorResponse := `{
"status": 400,
"code":20001,
"message":"Bad request",
"more_info":"https://www.twilio.com/docs/errors/20001"
}`
errorServer := httptest.NewServer(http.HandlerFunc(
func(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(400)
_, _ = resp.Write([]byte(errorResponse))
}))
defer errorServer.Close()

requestHandler := NewRequestHandler("user", "pass")
resp, err := requestHandler.Get(errorServer.URL, nil, nil) //nolint:bodyclose
twilioError := err.(*client.TwilioRestError)
assert.Nil(t, resp)
assert.Equal(t, 400, twilioError.Status)
assert.Equal(t, 20001, twilioError.Code)
assert.Equal(t, "https://www.twilio.com/docs/errors/20001", twilioError.MoreInfo)
assert.Equal(t, "Bad request", twilioError.Message)
assert.Nil(t, twilioError.Details)
}

func TestRequestHandler_SendPostRequest(t *testing.T) {
errorResponse := `{
"status": 400,
"code":20001,
"message":"Bad request",
"more_info":"https://www.twilio.com/docs/errors/20001"
}`
errorServer := httptest.NewServer(http.HandlerFunc(
func(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(400)
_, _ = resp.Write([]byte(errorResponse))
}))
defer errorServer.Close()

requestHandler := NewRequestHandler("user", "pass")
resp, err := requestHandler.Post(errorServer.URL, nil, nil) //nolint:bodyclose
twilioError := err.(*client.TwilioRestError)
assert.Nil(t, resp)
assert.Equal(t, 400, twilioError.Status)
assert.Equal(t, 20001, twilioError.Code)
assert.Equal(t, "https://www.twilio.com/docs/errors/20001", twilioError.MoreInfo)
assert.Equal(t, "Bad request", twilioError.Message)
assert.Nil(t, twilioError.Details)
}
2 changes: 1 addition & 1 deletion rest/accounts/v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This is the public Twilio REST API.
## Overview
This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project from the OpenAPI specs located at [twilio/twilio-oai](https://github.com/twilio/twilio-oai/tree/main/spec). By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client.

- API version: 1.55.3
- API version: 1.0.0
- Package version: 1.0.0
- Build package: com.twilio.oai.TwilioGoGenerator
For more information, please visit [https://support.twilio.com](https://support.twilio.com)
Expand Down
2 changes: 1 addition & 1 deletion rest/accounts/v1/docs/ListCredentialAwsResponseMeta.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**FirstPageUrl** | **string** | |[optional]
**Key** | **string** | |[optional]
**NextPageUrl** | Pointer to **string** | |
**Page** | **int** | |[optional]
**PageSize** | **int** | |[optional]
**PreviousPageUrl** | Pointer to **string** | |
**Url** | **string** | |[optional]
**Key** | **string** | |[optional]

[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ package openapi
// ListCredentialAwsResponseMeta struct for ListCredentialAwsResponseMeta
type ListCredentialAwsResponseMeta struct {
FirstPageUrl string `json:"first_page_url,omitempty"`
Key string `json:"key,omitempty"`
NextPageUrl *string `json:"next_page_url,omitempty"`
Page int `json:"page,omitempty"`
PageSize int `json:"page_size,omitempty"`
PreviousPageUrl *string `json:"previous_page_url,omitempty"`
Url string `json:"url,omitempty"`
Key string `json:"key,omitempty"`
}
Loading
Loading