Skip to content

Commit

Permalink
Start y-note API
Browse files Browse the repository at this point in the history
  • Loading branch information
AchoArnold committed Nov 7, 2024
1 parent d66dc10 commit 2f7ad96
Show file tree
Hide file tree
Showing 21 changed files with 771 additions and 227 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:

- name: Setup Dependencies
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $GOPATH/bin v1.24.0
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b $GOPATH/bin v1.50.0
golangci-lint --version
go get golang.org/x/tools/cmd/cover
go get -t -v ./...
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/
$path
.idea
.idea
cmd
56 changes: 24 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,38 @@
# go-http-client
# Y-Note Go Client

[![Build](https://github.com/NdoleStudio/go-http-client/actions/workflows/main.yml/badge.svg)](https://github.com/NdoleStudio/go-http-client/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/NdoleStudio/go-http-client/branch/main/graph/badge.svg)](https://codecov.io/gh/NdoleStudio/go-http-client)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/NdoleStudio/go-http-client/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/NdoleStudio/go-http-client/?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/NdoleStudio/go-http-client)](https://goreportcard.com/report/github.com/NdoleStudio/go-http-client)
[![GitHub contributors](https://img.shields.io/github/contributors/NdoleStudio/go-http-client)](https://github.com/NdoleStudio/go-http-client/graphs/contributors)
[![GitHub license](https://img.shields.io/github/license/NdoleStudio/go-http-client?color=brightgreen)](https://github.com/NdoleStudio/go-http-client/blob/master/LICENSE)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/NdoleStudio/go-http-client)](https://pkg.go.dev/github.com/NdoleStudio/go-http-client)
[![Build](https://github.com/NdoleStudio/ynote-go/actions/workflows/main.yml/badge.svg)](https://github.com/NdoleStudio/ynote-go/actions/workflows/main.yml)
[![codecov](https://codecov.io/gh/NdoleStudio/ynote-go/branch/main/graph/badge.svg)](https://codecov.io/gh/NdoleStudio/ynote-go)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/NdoleStudio/ynote-go/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/NdoleStudio/ynote-go/?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/NdoleStudio/ynote-go)](https://goreportcard.com/report/github.com/NdoleStudio/ynote-go)
[![GitHub contributors](https://img.shields.io/github/contributors/NdoleStudio/ynote-go)](https://github.com/NdoleStudio/ynote-go/graphs/contributors)
[![GitHub license](https://img.shields.io/github/license/NdoleStudio/ynote-go?color=brightgreen)](https://github.com/NdoleStudio/ynote-go/blob/master/LICENSE)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/NdoleStudio/ynote-go)](https://pkg.go.dev/github.com/NdoleStudio/ynote-go)


This package provides a generic `go` client template for an HTTP API
This package provides a generic `go` client for the Y-Note API

## Installation

`go-http-client` is compatible with modern Go releases in module mode, with Go installed:
`ynote-go` is compatible with modern Go releases in module mode, with Go installed:

```bash
go get github.com/NdoleStudio/go-http-client
go get github.com/NdoleStudio/ynote-go
```

Alternatively the same can be achieved if you use `import` in a package:

```go
import "github.com/NdoleStudio/go-http-client"
import "github.com/NdoleStudio/ynote-go"
```


## Implemented

- [Status Codes](#status-codes)
- `GET /200`: OK
- **Token**
- `POST {baseURL}/token`: Get Access Token
- **Refund**
- `POST {baseURL}/prod/refund`: Generate pay token.
- `GET {baseURL}/prod/refund/status/{transactionID}`: Get the status of a refund transaction

## Usage

Expand All @@ -41,11 +44,14 @@ An instance of the client can be created using `New()`.
package main

import (
"github.com/NdoleStudio/go-http-client"
"github.com/NdoleStudio/ynote-go"
)

func main() {
statusClient := client.New(client.WithDelay(200))
func main() {
client := ynote.New(
ynote.WithUsername(""),
ynote.WithPassword(""),
)
}
```

Expand All @@ -54,26 +60,12 @@ func main() {
All API calls return an `error` as the last return object. All successful calls will return a `nil` error.

```go
status, response, err := statusClient.Status.Ok(context.Background())
transaction, response, err := client.Refund.Status(context.Background(), "")
if err != nil {
//handle error
}
```

### Status Codes

#### `GET /200`: OK

```go
status, response, err := statusClient.Status.Ok(context.Background())

if err != nil {
log.Fatal(err)
}

log.Println(status.Description) // OK
```

## Testing

You can run the unit tests for this client from the root directory using the command below:
Expand Down
9 changes: 9 additions & 0 deletions access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ynote

// AccessTokenResponse is the response when fetching the access token
type AccessTokenResponse struct {
AccessToken string `json:"access_token"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
111 changes: 82 additions & 29 deletions client.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
package client
package ynote

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)

type service struct {
client *Client
}

// Client is the campay API client.
// Client is the Y-Note API client.
// Do not instantiate this client with Client{}. Use the New method instead.
type Client struct {
httpClient *http.Client
common service
baseURL string
delay int

Status *statusService
customerKey string
customerSecret string
username string
password string
tokenURL string
apiURL string

accessToken string
tokenExpirationTime int64
mutex sync.Mutex

Refund *RefundService
}

// New creates and returns a new campay.Client from a slice of campay.ClientOption.
// New creates and returns a new ynote.Client from a slice of ynote.Option.
func New(options ...Option) *Client {
config := defaultClientConfig()

Expand All @@ -35,20 +47,74 @@ func New(options ...Option) *Client {
}

client := &Client{
httpClient: config.httpClient,
delay: config.delay,
baseURL: config.baseURL,
httpClient: config.httpClient,
tokenURL: config.tokenURL,
apiURL: config.apiURL,
username: config.username,
password: config.password,
customerKey: config.customerKey,
customerSecret: config.customerSecret,
mutex: sync.Mutex{},
}

client.common.client = client
client.Status = (*statusService)(&client.common)
client.Refund = (*RefundService)(&client.common)
return client
}

// AccessToken fetches the access token used to authenticate api requests.
func (client *Client) AccessToken(ctx context.Context) (*AccessTokenResponse, *Response, error) {
data := url.Values{}
data.Set("grant_type", "client_credentials")

request, err := http.NewRequestWithContext(ctx, http.MethodPost, client.tokenURL+"/oauth2/token", strings.NewReader(data.Encode()))
if err != nil {
return nil, nil, err
}

request.SetBasicAuth(client.username, client.password)
request.Header.Add("Content-Type", "application/x-www-form-urlencoded")
request.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))

resp, err := client.do(request)
if err != nil {
return nil, resp, err
}

var token AccessTokenResponse
if err = json.Unmarshal(*resp.Body, &token); err != nil {
return nil, resp, err
}

return &token, resp, nil
}

// refreshToken refreshes the authentication AccessTokenResponse
func (client *Client) refreshToken(ctx context.Context) error {
client.mutex.Lock()
defer client.mutex.Unlock()

if client.tokenExpirationTime > time.Now().UTC().Unix() {
return nil
}

client.accessToken = ""

token, _, err := client.AccessToken(ctx)
if err != nil {
return err
}

client.accessToken = token.AccessToken
client.tokenExpirationTime = time.Now().UTC().Unix() + token.ExpiresIn - 100 // Give extra 100 second buffer

return nil
}

// newRequest creates an API request. A relative URL can be provided in uri,
// in which case it is resolved relative to the BaseURL of the Client.
// URI's should always be specified without a preceding slash.
func (client *Client) newRequest(ctx context.Context, method, uri string, body interface{}) (*http.Request, error) {
func (client *Client) newRequest(ctx context.Context, method, uri string, body any) (*http.Request, error) {
var buf io.ReadWriter
if body != nil {
buf = &bytes.Buffer{}
Expand All @@ -60,31 +126,18 @@ func (client *Client) newRequest(ctx context.Context, method, uri string, body i
}
}

req, err := http.NewRequestWithContext(ctx, method, client.baseURL+uri, buf)
req, err := http.NewRequestWithContext(ctx, method, client.apiURL+uri, buf)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", "Bearer "+client.accessToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

if client.delay > 0 {
client.addURLParams(req, map[string]string{"sleep": strconv.Itoa(client.delay)})
}

return req, nil
}

// addURLParams adds urls parameters to an *http.Request
func (client *Client) addURLParams(request *http.Request, params map[string]string) *http.Request {
q := request.URL.Query()
for key, value := range params {
q.Add(key, value)
}
request.URL.RawQuery = q.Encode()
return request
}

// do carries out an HTTP request and returns a Response
func (client *Client) do(req *http.Request) (*Response, error) {
if req == nil {
Expand All @@ -103,7 +156,7 @@ func (client *Client) do(req *http.Request) (*Response, error) {
return resp, err
}

_, err = io.Copy(ioutil.Discard, httpResponse.Body)
_, err = io.Copy(io.Discard, httpResponse.Body)
if err != nil {
return resp, err
}
Expand All @@ -120,7 +173,7 @@ func (client *Client) newResponse(httpResponse *http.Response) (*Response, error
resp := new(Response)
resp.HTTPResponse = httpResponse

buf, err := ioutil.ReadAll(resp.HTTPResponse.Body)
buf, err := io.ReadAll(resp.HTTPResponse.Body)
if err != nil {
return nil, err
}
Expand Down
16 changes: 10 additions & 6 deletions client_config.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
package client
package ynote

import "net/http"

type clientConfig struct {
httpClient *http.Client
delay int
baseURL string
httpClient *http.Client
customerKey string
customerSecret string
username string
password string
tokenURL string
apiURL string
}

func defaultClientConfig() *clientConfig {
return &clientConfig{
httpClient: http.DefaultClient,
delay: 0,
baseURL: "https://httpstat.us",
tokenURL: "https://omapi-token.ynote.africa",
apiURL: "https://omapi.ynote.africa",
}
}
Loading

0 comments on commit 2f7ad96

Please sign in to comment.