Skip to content

ysyesilyurt/go-restclient

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

go-restclient

A Handy Go Package to Call Internal HTTP APIs with Builder Pattern

go get github.com/ysyesilyurt/go-restclient/restclient

Write Once, Use Everywhere

Essentially go-restclient is a handy builder wrapper around net/http for all the pretty standard stuff that needs to be repeated in all HTTP Request/Response cycles. The purpose is to save you from the pain of massive code duplication those ordinary cycles may lead.

As the number of distinct Internal HTTP requests you are making grows, the need for such a wrapper package also exponentially grows. In that sense handiness level of go-restclient is directly proportional to your need for such request/responses 🙂

Execution Flow

After a Build() call on restclient.RequestBuilder() upon one or many builder method calls:

  • Constructs URL by escaping path components using provided components
  • Sets queryParams if exists
  • Validates that resulting URI is valid
  • Sets custom and universal headers
  • Sets authentication strategy to be used
  • Sets Authorization header by applying provided authentication strategy
  • Sets provided RequestBody if exists (or marshals then sets if given as JSON Body)
  • Builds the request object
  • Sets request context timeout and defer its cancellation
  • Does Request (Times and Logs it if needed)
  • Handles Response Status Code and any errors that can be returned from client's call (returns a detailed restclient.RequestError)
  • Reads the body and unmarshal it into given variable's reference

Usage

  • First build your HTTP Request using restclient.RequestBuilder() constructor and with all the builder methods you need.
// https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1
req, reqErr := restclient.RequestBuilder().
		Scheme("https").
		Host("ysyesilyurt.com").
		PathElements([]string{"tasks", "1"}).
		QueryParams(&queryParams).
		Header(&headers).
		Auth(testAuth).
		ResponseReference(&response).
		LoggingEnabled(true).
		Timeout(30 * time.Second).
		Build()
if reqErr != nil {
	return errors.Wrap(reqErr, "Failed to construct HTTP request")
}
  • Then make your call...
reqErr = req.Get()
if reqErr != nil {
	return errors.Wrap(reqErr, "Failed to perform HTTP GET request")
}

Available HTTP Calls

  • Get() RequestError
  • Post() RequestError
  • Put() RequestError
  • Patch() RequestError
  • Delete() RequestError

Available Builders

  • Scheme(scheme string) -> Sets scheme field of your URL. Example:
req, reqErr := restclient.RequestBuilder().
                Scheme("https").
                Build()
  • Host(host string) -> Sets host field of your URL. Example:
req, reqErr := restclient.RequestBuilder().
                Scheme("https").
                Host("ysyesilyurt.com").
                Build()
  • PathElements(pe []string) -> Sets elements that reside in your URL's Path (not the query params) in the order given in the array. Example:
req, reqErr := restclient.RequestBuilder().
                Scheme("https").
                Host("ysyesilyurt.com").
                PathElements([]string{"tasks", "1"}).
                Build()
  • QueryParams(qp *url.Values) -> Sets the Query Parameters that reside in your URL in the order given in the array. Example:
req, reqErr := restclient.RequestBuilder().
                Scheme("https").
                Host("ysyesilyurt.com").
                PathElements([]string{"tasks", "1"}).
                QueryParams(&url.Values{"tenantId": []string{"d90c3101-53bc-4c54-94db-21582bab8e17"}, "vectorId": []string{"1"}}).
                Build()
  • RawUrl(rawUrl string) -> Use this one if you do not want to parse your URL into several builder methods above and see your URL as a whole. This one will automatically parse your URL and set the fields accordingly. Example:
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Build()
  • Header(header *http.Header) -> Sets the headers that you want to include in your request. Example:
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                Build()
  • Body(body io.Reader) -> Sets the request body in the form of io.Reader for your request, if you want to include a body for your request of course. Example:
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                Body(bytes.NewBufferString(query)).
                Build()
  • BodyJson(bodyJson interface{}) -> Use this one instead of Body(body io.Reader) if your request body is in JSON form. You can pass your struct object directly to this builder method, it is going to marshal your object and set the body as io.Reader. Example:
requestBody := dummyBodyDto{
    Id:   123,
    Name: "1234",
}
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                BodyJson(requestBody).
                Build()
  • Auth(auth Authenticator) -> Sets the Authentication Strategy for your request. Implement restclient.Authenticator to create your own Authenticator, an example Basic Auth implementation can be found in basic_authenticator.go. Example:
basicAuth := newBasicAuthenticator("ysyesilyurt", "0123")
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                Auth(basicAuth).
                Build()
  • ResponseReference(respRef interface{}) -> Sets the reference of the variable to map response object returned from request. Example:
var response dummyHttpResponse
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                ResponseReference(&response).
                Build()
  • Request(req *http.Request) -> If you happen to have a pre-prepared valid http.Request object and want to build a new request upon this request then you can simply use this builder method. Example:
var response dummyHttpResponse
req, reqErr := restclient.RequestBuilder().
                Request("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Header(&http.Header{"Content-Type": []string{"application/json"}, "Cookie": []string{"test-1234"}}).
                ResponseReference(&response).
                Build()
  • Timeout(timeout time.Duration) -> Sets the timeout value that should be used for the request. Default is 60 seconds. Example:
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                Timeout(30 * time.Second)             
                Build()
  • LoggingEnabled(enabled bool) -> Decides whether responses should be logged or not. Default is false. Example:
req, reqErr := restclient.RequestBuilder().
                RawUrl("https://ysyesilyurt.com/tasks/1?tenantId=d90c3101-53bc-4c54-94db-21582bab8e17&vectorId=1").
                LoggingEnabled(true)             
                Build()

Error Handling

go-restclient defines restclient.RequestError interface to cover all the errors that can be returned from restclient.RequestBuilder.Build() and result of the HTTP calls. The top level errors returned by the client can be seen in errors.go . The methods that can be used within the boundaries of restclient.RequestError are as follows:

type RequestError interface {
	Error() string
	GetTopLevelError() error   // GetTopLevelError returns top level error that originates the title
	GetUnderlyingError() error // GetUnderlyingError returns underlying error that originates the message
	GetTitle() string          // GetTitle returns top level error 'title' referring to message
	GetMessage() string        // GetMessage returns detailed error message for the request
	GetStatusCode() int        // GetStatusCode returns status code for the request. '0' means request failed for some reason and can be checked using methods below
	Timeout() bool             // Timeout returns if request failed due to a timeout
	ConnectionError() bool     // ConnectionError returns if request failed due to a connection error (Failed to get response for some reason)
	ResponseParseError() bool  // ResponseParseError returns if response of the request could not be parsed into given response reference variable
	RequestBuildError() bool   // RequestBuildError returns if request could not be built due to some reason
}

Legacy Version

You can also use the legacy version which is located on legacy branch if you want to use go-restclient using traditional ways (with constructing the helper objects and using those altogether blah blah...) or if you want to use a version that allows reusing the HTTP clients that's being used internally for sending the requests (Thanks to separation of these objects, this legacy version of go-restclient gives you the slightly extended feature set like separation of client timeouts and request-specific timeouts and etc.). You can find more information about the version itself and its usage from here. But all in all, I think the builder version is more handy and elegant so I just wanted to keep builder version on the master branch.

Contribution

Unit tests are implemented for the codebase but all the edge cases might not be covered, weird bugs may appear in such cases. Feel free to open an issue if you spot a bug. In addition if you have an improvement idea you can request it in the form of an issue (with providing a clear and brief description of the feature) or you can develop the improvement and open a PR with again a brief description of the reasoning.