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

date and time http client implementation #1

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
25 changes: 25 additions & 0 deletions .github/workflows/golang-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Go Test

on: push

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.22.4"

- name: Check code formatting
uses: Jerome1337/gofmt-action@v1.0.5

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6.0.1

- name: Test
run: go test -v ./...
13 changes: 13 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/dnephin/pre-commit-golang
rev: master
hooks:
- id: go-fmt
- id: golangci-lint
- id: go-unit-tests
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Eyad Hussein

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# datetime-client-eyadhussein

# DateTime Client

This package provides a simple HTTP client for interacting with a datetime server. It's designed to retrieve the current date and time from a specified server endpoint.

## Features

- Easy-to-use client for fetching current date and time
- Configurable base URL and port
- Built-in retry mechanism with backoff strategy
- Timeout handling

## Installation

To use this package in your Go project, you can install it using:

```
go get github.com/codescalersinternships/datetime-client-eyadhussein
```

## Usage

Here's a basic example of how to use the DateTime Client:

```go
package main

import (
"fmt"
"log"
"time"
"github.com/codescalersinternships/datetime-client-eyadhussein/pkg/datetimeclient"
)

func main() {
// Create a new client
client := datetimeclient.NewRealClient("http://localhost", "8080", 10*time.Second)

// Get the current date and time
dateTime, err := client.GetCurrentDateTime()
if err != nil {
log.Fatal(err)
}

fmt.Printf("Current Date and Time: %q\n", string(dateTime))
}
```

If environment variables are defined, just create a new client with empty string arguments:
```go
package main

import (
"fmt"
"log"
"time"
"github.com/codescalersinternships/datetime-client-eyadhussein/pkg/datetimeclient"
)

func main() {
// Create a new client
client := datetimeclient.NewRealClient("", "", 10*time.Second)

// Get the current date and time
dateTime, err := client.GetCurrentDateTime()
if err != nil {
log.Fatal(err)
}

fmt.Printf("Current Date and Time: %q\n", string(dateTime))
}
```

In the terminal, run:
```bash
SERVER_URL=http://localhost PORT=8080 go run main.go
```

Terminal output:
```bash
2024-07-04 15:11:44
```

# How to Test

Run

```bash
go test -v ./...
```
20 changes: 20 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"log"
"time"

datetimeclient "github.com/codescalersinternships/datetime-client-eyadhussein/pkg/client"
)

func main() {
c := datetimeclient.NewRealClient("http://localhost", "8080", time.Duration(1)*time.Second)

data, err := c.GetCurrentDateTime()

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

log.Println(string(data))
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/codescalersinternships/datetime-client-eyadhussein

go 1.22.5
44 changes: 44 additions & 0 deletions pkg/backoff/backoff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Package backoff provides functionality for retrying operations with a simple backoff strategy.
package backoff

import (
"errors"
"net/http"
"time"
)

// Operation represents a function that returns an HTTP response and an error.
type Operation func() (*http.Response, error)

// BackOff interface defines the method for retrying a request for number of times before failure.
type BackOff interface {
Retry(operation Operation) (*http.Response, error)
}

// RealBackOff implements a simple backoff strategy with a fixed duration between retries.
type RealBackOff struct {
Duration time.Duration // time to wait between retry attempts.
MaxRetry int // maximum number of retry attempts
}

// NewRealBackOff creates and returns a new RealBackOff instance with the specified duration and maximum retries.
func NewRealBackOff(duration time.Duration, maxRetry int) *RealBackOff {
return &RealBackOff{
Duration: duration,
MaxRetry: maxRetry,
}
}

// Retry attempts to execute the given operation, retrying up to MaxRetry times with a fixed Duration between attempts.
// It returns the successful HTTP response or an error if all attempts fail.
func (b *RealBackOff) Retry(operation Operation) (*http.Response, error) {
for i := 0; i < b.MaxRetry; i++ {
resp, err := operation()

if err == nil {
return resp, nil
}
time.Sleep(b.Duration)
}
return &http.Response{}, errors.New("reached maximum retries with no established connection")
}
134 changes: 134 additions & 0 deletions pkg/backoff/backoff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package backoff

import (
"errors"
"io"
"log"
"net/http"
"testing"
"time"
)

const maxRetries = 3
const validRetries = 2

type SpySleeper struct {
Calls int
}

func (s *SpySleeper) Sleep(d time.Duration) {
s.Calls++
}

type MockBackOff struct {
Duration time.Duration
MaxRetry int
sleeper *SpySleeper
}

func NewMockBackOff(duration time.Duration, maxRetry int) *MockBackOff {
return &MockBackOff{
Duration: duration,
MaxRetry: maxRetry,
sleeper: &SpySleeper{},
}
}

func (b *MockBackOff) Retry(operation Operation) (*http.Response, error) {
for i := 0; i < b.MaxRetry; i++ {
resp, err := operation()

if err == nil {
return resp, nil
}
b.sleeper.Sleep(b.Duration)
}
return &http.Response{}, errors.New("reached maximum retries with no established connection")
}

func TestBackOff_Retry(t *testing.T) {
t.Run("successful on first try", func(t *testing.T) {
b := NewMockBackOff(1*time.Second, maxRetries)
operation := func() (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK}, nil
}

resp, err := b.Retry(operation)

assertNoError(t, err)

if resp.StatusCode != http.StatusOK {
t.Errorf("expected success code but got %d", resp.StatusCode)
}

if b.sleeper.Calls != 0 {
t.Errorf("expected no sleep calls but got %d", b.sleeper.Calls)
}
})

t.Run("successful after retries", func(t *testing.T) {
b := NewMockBackOff(1*time.Second, maxRetries)
callCount := 0
operation := func() (*http.Response, error) {
callCount++
if callCount < validRetries {
return nil, errors.New("temporary error")
}
return &http.Response{StatusCode: http.StatusOK}, nil
}

resp, err := b.Retry(operation)

assertNoError(t, err)

if resp.StatusCode != http.StatusOK {
t.Errorf("expected success code but got %d", resp.StatusCode)
}

if b.sleeper.Calls != validRetries-1 {
t.Errorf("expected sleep to be called 1 time but was called %d times", b.sleeper.Calls)
}
})

t.Run("failure after max retries", func(t *testing.T) {
b := NewMockBackOff(1*time.Second, maxRetries)
operation := func() (*http.Response, error) {
return &http.Response{StatusCode: http.StatusServiceUnavailable}, errors.New("service unavailable try again later")
}

_, err := b.Retry(operation)

if err == nil {
t.Error("expected error but got nil")
}
})
}

func assertNoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Errorf("expected nil but got %v", err)
}
}

func ExampleBackOff_Retry() {
backoff := NewRealBackOff(1*time.Second, 3)
operation := func() (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK}, nil
}

resp, err := backoff.Retry(operation)

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

defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}

log.Println(string(body))
}
Loading
Loading