hazelcast/hazelcast-cloud-sdk-go

View on GitHub
hazelcast_cloud.go

Summary

Maintainability
A
2 hrs
Test Coverage
C
71%
// Package hazelcastcloud is the Hazelcast Cloud API client for Go.
package hazelcastcloud

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "github.com/hazelcast/hazelcast-cloud-sdk-go/graphql"
    "github.com/hazelcast/hazelcast-cloud-sdk-go/models"
    "io"
    "io/ioutil"
    "mime/multipart"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "sync"
)

const (
    // This is the version of the Library.
    libraryVersion = "1.0.0"
    // This is the default base url.
    defaultBaseURL = "https://cloud.hazelcast.com/api/v1"
    // This is the User-Agent of Client.
    userAgent = "hazelcast-cloud-sdk-go/" + libraryVersion
    // This is the Http Accept type.
    jsonMediaType = "application/json"
    // This is the header key of total Rate Limit.
    headerRateLimit = "X-RateLimit-Limit"
    // This is the header key of total Rate Remaining.
    headerRateRemaining = "X-RateLimit-Remaining"
    // This is the header key of total Rate Reset.
    headerRateReset = "X-RateLimit-Reset"
    // This is status code of Rate Limit.
    statusCodeRateLimit = 429
)

// This is the RequestCompletionCallback to intercept response when request complete.
type RequestCompletionCallback func(*http.Request, *http.Response)

// This is the main Client.
type Client struct {
    client    *http.Client
    BaseURL   *url.URL
    UserAgent string
    Rate      Rate
    rateMutex sync.Mutex
    Token     string

    ServerlessCluster ServerlessClusterService
    StarterCluster    StarterClusterService
    EnterpriseCluster EnterpriseClusterService
    CloudProvider     CloudProviderService
    Region            RegionService
    InstanceType      InstanceTypeService
    HazelcastVersion  HazelcastVersionService
    Auth              AuthService
    GcpPeering        GcpPeeringService
    AzurePeering      AzurePeeringService
    AwsPeering        AwsPeeringService

    onRequestCompleted RequestCompletionCallback
}

// This is the OnRequestCompleted method to assign callback.
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
    c.onRequestCompleted = rc
}

type Option func(*Client)

func OptionEndpoint(e string) func(*Client) {
    return func(c *Client) {
        baseURL, err := url.Parse(e)
        if err != nil {
            panic(err)
        }
        c.BaseURL = baseURL
    }
}

// This creates a new client with provided http client.
func NewClient(httpClient *http.Client, options ...Option) *Client {
    if httpClient == nil {
        httpClient = http.DefaultClient
    }

    baseURL, _ := url.Parse(defaultBaseURL)

    c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}

    c.Auth = NewAuthService(c)
    c.ServerlessCluster = NewServerlessClusterService(c)
    c.StarterCluster = NewStarterClusterService(c)
    c.EnterpriseCluster = NewEnterpriseClusterService(c)
    c.CloudProvider = NewCloudProviderService(c)
    c.Region = NewRegionService(c)
    c.InstanceType = NewInstanceTypeService(c)
    c.HazelcastVersion = NewHazelcastVersionService(c)
    c.GcpPeering = NewGcpPeeringService(c)
    c.AzurePeering = NewAzurePeeringService(c)
    c.AwsPeering = NewAwsPeeringService(c)

    for _, option := range options {
        option(c)
    }

    return c
}

// This function creates a new client.
func New(options ...Option) (*Client, *Response, error) {
    apiKey := os.Getenv("HZ_CLOUD_API_KEY")
    apiSecret := os.Getenv("HZ_CLOUD_API_SECRET")
    if len(strings.TrimSpace(apiKey)) == 0 || len(strings.TrimSpace(apiSecret)) == 0 {
        return nil, nil, &ErrorResponse{
            CorrelationId: "",
            Message:       "You need to provide HZ_CLOUD_API_KEY and HZ_CLOUD_API_SECRET in your environment variables.",
            Response:      nil,
        }
    }
    return NewFromCredentials(apiKey, apiSecret, options...)
}

// This function create new client with ApiKey and ApiSecret.
func NewFromCredentials(apiKey string, apiSecret string, options ...Option) (*Client, *Response, error) {
    client := NewClient(nil, options...)
    login, loginResp, loginErr := client.Auth.Login(context.Background(), &models.LoginInput{
        ApiKey:    apiKey,
        ApiSecret: apiSecret,
    })

    if loginErr != nil {
        return nil, loginResp, loginErr
    }
    client.Token = login.Token
    return client, loginResp, nil
}

//This function creates a new http request from graphql request. It returns an error if it can not build request.
func (c *Client) NewRequest(body *models.GraphqlRequest) (*http.Request, error) {
    graphqlBody := GraphQLQuery{
        OperationName: "",
        Query:         graphql.Query(body.Name, body.Operation, body.Input, body.Args, body.Response),
        Variables:     map[string]interface{}{"input": graphql.Variables(body.Input)},
    }

    buf := new(bytes.Buffer)
    encodeErr := json.NewEncoder(buf).Encode(graphqlBody)
    if encodeErr != nil {
        return nil, encodeErr
    }

    req, requestErr := http.NewRequest(http.MethodPost, c.BaseURL.String(), buf)
    if requestErr != nil {
        return nil, requestErr
    }

    req.Header.Add("Content-Type", jsonMediaType)
    req.Header.Add("Accept", jsonMediaType)
    req.Header.Add("User-Agent", c.UserAgent)
    if c.Token != "" {
        req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token))
    }
    return req, nil
}

func (c *Client) NewUploadFileRequest(request *models.GraphqlRequest) (*http.Request, error) {
    graphqlBody := GraphQLQuery{
        OperationName: "",
        Query:         graphql.QueryUploadFile(request.Name, request.Operation, request.Args, request.Response),
        Variables:     map[string]interface{}{"file": nil},
    }
    query := new(bytes.Buffer)
    if err := json.NewEncoder(query).Encode(graphqlBody); err != nil {
        return nil, err
    }
    formData := new(bytes.Buffer)
    writer := multipart.NewWriter(formData)
    if err := writer.WriteField("operations", query.String()); err != nil {
        return nil, err
    }
    if err := writer.WriteField("map", "{ \"0\": [\"variables.file\"] }"); err != nil {
        return nil, err
    }
    if _, err := writer.CreateFormFile("0", request.UploadFile.FileName); err != nil {
        return nil, err
    }
    begin := make([]byte, formData.Len())
    if _, err := formData.Read(begin); err != nil {
        return nil, err
    }
    if err := writer.Close(); err != nil {
        return nil, err
    }
    end := make([]byte, formData.Len())
    if _, err := formData.Read(end); err != nil {
        return nil, err
    }
    req, err := http.NewRequest(http.MethodPost, c.BaseURL.String(),
        io.MultiReader(bytes.NewReader(begin), request.UploadFile.Content, bytes.NewReader(end)))
    if err != nil {
        return nil, err
    }
    req.Header.Add("Content-Type", writer.FormDataContentType())
    req.Header.Add("Accept", jsonMediaType)
    req.Header.Add("User-Agent", c.UserAgent)
    if c.Token != "" {
        req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.Token))
    }
    return req, nil
}

//This function sends http request to the server. Then it creates a response according to type interface provided as v.
//It returns error if response is not successful.
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
    resp, respErr := DoRequestWithClient(ctx, c.client, req)

    if respErr != nil {
        return nil, respErr
    }

    if c.onRequestCompleted != nil {
        c.onRequestCompleted(req, resp)
    }

    defer resp.Body.Close()

    response := newResponse(resp)
    c.rateMutex.Lock()
    c.Rate = response.Rate
    c.rateMutex.Unlock()

    responseData, checkRespErr := AugmentResponse(resp)
    if checkRespErr != nil {
        return response, checkRespErr
    }

    if v != nil {
        if w, ok := v.(io.Writer); ok {
            _, copyErr := io.Copy(w, resp.Body)
            if copyErr != nil {
                return response, copyErr
            }
        } else {
            var objectMap map[string]interface{}
            if err := json.Unmarshal(responseData, &objectMap); err != nil {
                return response, err
            }
            dataMarshall, _ := json.Marshal(objectMap["data"].(map[string]interface{})["response"])
            decodeErr := json.NewDecoder(bytes.NewReader(dataMarshall)).Decode(v)
            if decodeErr != nil {
                return response, decodeErr
            }
        }
    }

    return response, nil
}

// This function augments the response and returns and error if response has
func AugmentResponse(response *http.Response) ([]byte, error) {
    responseData, err := ioutil.ReadAll(response.Body)
    if err != nil {
        return nil, err
    }

    if response.StatusCode == statusCodeRateLimit {
        return nil, &ErrorResponse{
            CorrelationId: response.Header.Get("X-B3-TraceId"),
            Message:       "Too Many Request",
            Response:      response,
        }
    }

    var objectMap map[string]interface{}
    if err := json.Unmarshal(responseData, &objectMap); err != nil {
        return responseData, fmt.Errorf("%v\n%s", err, string(responseData))
    }

    errorObject, errorKeyFound := objectMap["errors"]
    if !errorKeyFound {
        return responseData, nil
    }

    errorObjectJson, errorObjectJsonErr := json.Marshal(errorObject)
    if errorObjectJsonErr != nil {
        return responseData, errorObjectJsonErr
    }

    var errorResponse []ErrorResponse
    if errorUnmarshal := json.Unmarshal(errorObjectJson, &errorResponse); errorUnmarshal != nil {
        return responseData, errorUnmarshal
    }

    firstError := &errorResponse[0]
    firstError.Response = response
    firstError.CorrelationId = response.Header.Get("X-B3-TraceId")
    return nil, firstError
}

//This method returns an Error string of ErrorResponse.
func (r *ErrorResponse) Error() string {
    if r.CorrelationId != "" {
        return fmt.Sprintf("Method:%v URL:%v: Status:%d TraceId:%s Message:%v",
            r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.CorrelationId, r.Message)
    }
    return fmt.Sprintf("Method:%v URL:%v: Status:%d Message:%v", r.Response.Request.Method, r.Response.Request.URL,
        r.Response.StatusCode, r.Message)
}

//Error response is the main type of response for the errors this library handles
type ErrorResponse struct {
    CorrelationId string `json:"correlation_id,omitempty"`
    Message       string `json:"message"`
    Response      *http.Response
}

//This function creates a new response with populating rate from http response.
func newResponse(r *http.Response) *Response {
    response := Response{Response: r}
    response.populateRate()

    return &response
}

//This functions populates the rates from http headers
func (r *Response) populateRate() {
    if limit := r.Header.Get(headerRateLimit); limit != "" {
        r.Rate.Limit, _ = strconv.Atoi(limit)
    }
    if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
        r.Rate.Remaining, _ = strconv.Atoi(remaining)
    }
    if reset := r.Header.Get(headerRateReset); reset != "" {
        resetTime, _ := strconv.ParseInt(reset, 10, 64)
        r.Rate.Reset = resetTime
    }
}

//This function does request with the request and client provided.
func DoRequestWithClient(
    ctx context.Context,
    client *http.Client,
    req *http.Request) (*http.Response, error) {
    req = req.WithContext(ctx)
    return client.Do(req)
}

//Type of the main response
type Response struct {
    *http.Response
    Rate
}

//Type of the rate limiting params we follow
type Rate struct {
    Limit     int   `json:"limit"`
    Remaining int   `json:"remaining"`
    Reset     int64 `json:"reset"`
}

//Type of graphql query
type GraphQLQuery struct {
    OperationName string                 `json:"operationName,omitempty"`
    Query         string                 `json:"query"`
    Variables     map[string]interface{} `json:"variables"`
}