Skip to content

Commit

Permalink
chore: general update (#87)
Browse files Browse the repository at this point in the history
dantasrafael authored Jun 17, 2024
1 parent 87cf522 commit 54f6351
Showing 58 changed files with 3,324 additions and 2,006 deletions.
Original file line number Diff line number Diff line change
@@ -16,4 +16,4 @@ CREATE TABLE IF NOT EXISTS dog
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
characteristics TEXT[]
);
);
13 changes: 6 additions & 7 deletions development-environment/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ version: "3"

services:
wiremock:
image: wiremock/wiremock:2.32.0-alpine
image: wiremock/wiremock:3.3.1-alpine
command: "--local-response-templating"
ports:
- "5050:8080"
@@ -12,19 +12,18 @@ services:
- dev

localstack:
image: localstack/localstack:1.4
image: localstack/localstack:3.1
ports:
- "127.0.0.1:4510-4559:4510-4559" # external service port range
- "127.0.0.1:4566:4566" # LocalStack Edge Proxy
environment:
- DEBUG=${DEBUG-}
- SERVICES=sns,sqs,s3,dynamodb
- DATA_DIR=${DATA_DIR-}
- LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR-}
- HOST_TMP_FOLDER=${TMPDIR:-/tmp/}localstack
- DEBUG=${DEBUG:-0}
- SERVICES=sns,sqs,s3
- DOCKER_HOST=unix:///var/run/docker.sock
volumes:
- "${TMPDIR:-/tmp}/localstack:/var/lib/localstack"
- "/var/run/docker.sock:/var/run/docker.sock"
- ./localstack/start-localstack.sh:/etc/localstack/init/ready.d/start-localstack.sh
networks:
- dev

Original file line number Diff line number Diff line change
@@ -4,16 +4,15 @@ awslocal sns create-topic --name COLIBRI_PROJECT_USER_CREATE
awslocal sqs create-queue --queue-name COLIBRI_PROJECT_USER_CREATE_APP_CONSUMER
awslocal sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:COLIBRI_PROJECT_USER_CREATE \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:queue:COLIBRI_PROJECT_USER_CREATE_APP_CONSUMER

--notification-endpoint arn:aws:sqs:us-east-1:000000000000:COLIBRI_PROJECT_USER_CREATE_APP_CONSUMER

awslocal sns create-topic --name COLIBRI_PROJECT_FAIL_USER_CREATE
awslocal sqs create-queue --queue-name COLIBRI_PROJECT_FAIL_USER_CREATE_APP_CONSUMER
awslocal sqs create-queue --queue-name COLIBRI_PROJECT_FAIL_USER_CREATE_APP_CONSUMER_DLQ
awslocal sns subscribe --topic-arn arn:aws:sns:us-east-1:000000000000:COLIBRI_PROJECT_FAIL_USER_CREATE \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:queue:COLIBRI_PROJECT_FAIL_USER_CREATE_APP_CONSUMER
--notification-endpoint arn:aws:sqs:us-east-1:000000000000:COLIBRI_PROJECT_FAIL_USER_CREATE_APP_CONSUMER

awslocal s3api create-bucket --bucket my-bucket --acl public-read

echo "localstack emulator started"
echo "localstack emulator started"
2 changes: 1 addition & 1 deletion development-environment/rest/resp.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"msg": "test file"
}
}
2 changes: 1 addition & 1 deletion development-environment/storage/file.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Test file
Test file
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"priority": 1,
"request": {
"method": "PATCH",
"urlPattern": "/users-api/v1/users/100",
"bodyPatterns": [
{
"equalToJson": {
"name": "User 100 edited"
}
}
]
},
"response": {
"status": 204
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"priority": 1,
"request": {
"method": "POST",
"urlPattern": "/users-api/v1/upload",
"headers": {
"Content-Type": {
"matches": "multipart/form-data;\\s*boundary=[^\\s]+"
}
},
"bodyPatterns": [
{
"matches": ".+Content-Disposition: form-data; name=\"myfile\"; filename=\"test.txt\".*Content-Type: text/plain.+"
}
]
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json; charset=utf-8"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"priority": 1,
"request": {
"method": "POST",
"urlPattern": "/users-api/v1/upload",
"headers": {
"Content-Type": {
"matches": "multipart/form-data;\\s*boundary=[^\\s]+"
}
},
"bodyPatterns": [
{
"matches": ".+Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\".*Content-Type: application/octet-stream.+"
}
]
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json; charset=utf-8"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"priority": 1,
"request": {
"method": "POST",
"urlPattern": "/users-api/v1",
"headers": {
"Content-Type": {
"matches": "multipart/form-data;\\s*boundary=[^\\s]+"
}
},
"bodyPatterns": [
{
"matches": ".+Content-Disposition: form-data; name=\"name\".*User 100.+"
},
{
"matches": ".+Content-Disposition: form-data; name=\"email\".*user_100@email.com.+"
}
]
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"jsonBody": {
"id": 10,
"name": "User 100",
"email": "user_100@email.com"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"priority": 1,
"request": {
"method": "POST",
"urlPattern": "/token",
"bodyPatterns": [
{
"equalToJson": {
"id": 10,
"name": "User 10",
"email": "user_10@email.com"
}
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json; charset=utf-8"
},
"jsonBody": {
"type": "Bearer",
"token": "1A2B3C4D5E",
"expiresIn": 123456789
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"priority": 1,
"request": {
"method": "POST",
"urlPattern": "/users-api/v1/users",
"bodyPatterns": [
{
"equalToJson": {
"name": "User 100",
"email": "user_100@email.com"
}
}
]
},
"response": {
"status": 201,
"headers": {
"Content-Length": "0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"priority": 1,
"request": {
"method": "PUT",
"urlPattern": "/users-api/v1/users/100",
"bodyPatterns": [
{
"equalToJson": {
"id": 100,
"name": "User 100 edited",
"email": "user_100@email.com"
}
}
]
},
"response": {
"status": 204,
"headers": {
"Content-Length": "0"
}
}
}
48 changes: 23 additions & 25 deletions go.mod
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ require (
cloud.google.com/go/storage v1.33.0
firebase.google.com/go v3.13.0+incompatible
github.com/aws/aws-sdk-go v1.45.28
github.com/docker/docker v24.0.5+incompatible
github.com/docker/go-connections v0.4.0
github.com/go-playground/form/v4 v4.2.1
github.com/go-playground/validator/v10 v10.15.5
@@ -39,31 +40,28 @@ require (

require (
cloud.google.com/go v0.110.8 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute v1.23.0 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
cloud.google.com/go/firestore v1.13.0 // indirect
cloud.google.com/go/iam v1.1.3 // indirect
cloud.google.com/go/longrunning v0.5.2 // indirect
cloud.google.com/go/iam v1.1.2 // indirect
cloud.google.com/go/longrunning v0.5.1 // indirect
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v3 v3.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/containerd v1.7.11 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/containerd v1.7.3 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
@@ -75,16 +73,16 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
@@ -98,14 +96,14 @@ require (
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc4 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
github.com/swaggo/files/v2 v2.0.0 // indirect
github.com/swaggo/swag v1.16.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
@@ -114,22 +112,22 @@ require (
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.13.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.13.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect
google.golang.org/grpc v1.58.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect
144 changes: 86 additions & 58 deletions go.sum

Large diffs are not rendered by default.

30 changes: 19 additions & 11 deletions pkg/base/test/localstack_container.go
Original file line number Diff line number Diff line change
@@ -8,14 +8,16 @@ import (

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/config"
"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/go-connections/nat"
"github.com/google/uuid"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

const (
localstackDockerImage = "localstack/localstack:1.4"
localstackDockerImage = "localstack/localstack:3.1"
localstackSvcPort = "4566"
)

@@ -43,17 +45,21 @@ func newLocalstackContainer(ctx context.Context, configPath string) *LocalstackC
ExposedPorts: []string{localstackSvcPort},
Name: fmt.Sprintf("colibri-project-test-localstack-%s", uuid.New().String()),
Env: map[string]string{
"DEBUG": "${DEBUG-}",
"SERVICES": "sns,sqs,s3,dynamodb",
"DATA_DIR": "${DATA_DIR-}",
"LAMBDA_EXECUTOR": "${LAMBDA_EXECUTOR-}",
"HOST_TMP_FOLDER": "${TMPDIR:-/tmp/}localstack",
"DOCKER_HOST": "unix:///var/run/docker.sock",
"DEBUG": "1",
"SERVICES": "sns,sqs,s3,dynamodb",
},
HostConfigModifier: func(hostConfig *container.HostConfig) {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: configPath,
Target: "/etc/localstack/init/ready.d/",
})
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: "/var/run/docker.sock",
Target: "/var/run/docker.sock",
})
},
Mounts: testcontainers.Mounts(
testcontainers.BindMount(configPath, "/docker-entrypoint-initaws.d/"),
testcontainers.BindMount("/var/run/docker.sock", "/var/run/docker.sock"),
),
WaitingFor: wait.ForAll(
wait.ForListeningPort(localstackSvcPort),
wait.ForLog("localstack emulator started"),
@@ -72,10 +78,12 @@ func (c *LocalstackContainer) start() {
if err != nil {
logging.Fatal(err.Error())
}

localstackPort, err := c.lsContainer.MappedPort(c.ctx, localstackSvcPort)
if err != nil {
logging.Fatal(err.Error())
}

log.Printf("Test localstack started at port: %s", localstackPort)
c.setEnv(localstackPort)
}
4 changes: 2 additions & 2 deletions pkg/base/test/postgres_container.go
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import (
)

const (
postgresDockerImage = "postgres:14-alpine"
postgresDockerImage = "postgres:16-alpine"
testDbHost = "localhost"
testDbName = "test_db"
testDbUser = "test_user"
@@ -88,7 +88,7 @@ func (c *PostgresContainer) start() {
logging.Fatal(err.Error())
}

log.Printf("Test database started at port: %s", testDbPort)
log.Printf("Test postgres started at port: %s", testDbPort)
c.setDatabaseEnv(testDbPort)
databaseURL := fmt.Sprintf(config.SQL_DB_CONNECTION_URI_DEFAULT,
os.Getenv(config.ENV_SQL_DB_HOST),
3 changes: 2 additions & 1 deletion pkg/base/test/redis_container.go
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import (
)

const (
redisDockerImage = "redis:7.0-alpine"
redisDockerImage = "redis:alpine"
testRedisSvcPort = "6379"
)

@@ -52,6 +52,7 @@ func newRedisContainer() *RedisContainer {
func (c *RedisContainer) start() {
var err error
ctx := context.Background()

c.redisContainer, err = testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: *c.redisContainerRequest,
Started: true,
23 changes: 15 additions & 8 deletions pkg/base/test/wiremock_container.go
Original file line number Diff line number Diff line change
@@ -5,13 +5,15 @@ import (
"fmt"

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/google/uuid"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

const (
wiremockDockerImage = "wiremock/wiremock:2.32.0-alpine"
wiremockDockerImage = "wiremock/wiremock:3.3.1-alpine"
wiremockSvcPort = "8080"
)

@@ -34,12 +36,17 @@ func UseWiremockContainer(configPath string) *WiremockContainer {

func newWiremockContainer(configPath string) *WiremockContainer {
req := testcontainers.ContainerRequest{
Image: wiremockDockerImage,
Name: fmt.Sprintf("colibri-project-test-wiremock-%s", uuid.New().String()),
ExposedPorts: []string{wiremockSvcPort},
Env: map[string]string{},
Mounts: testcontainers.ContainerMounts{
testcontainers.ContainerMount{Source: testcontainers.GenericBindMountSource{HostPath: configPath}, Target: "/home/wiremock"},
Image: wiremockDockerImage,
ImagePlatform: "linux/amd64",
Name: fmt.Sprintf("colibri-project-test-wiremock-%s", uuid.New().String()),
ExposedPorts: []string{wiremockSvcPort},
Env: map[string]string{},
HostConfigModifier: func(hostConfig *container.HostConfig) {
hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{
Type: mount.TypeBind,
Source: configPath,
Target: "/home/wiremock",
})
},
Cmd: []string{"--local-response-templating"},
WaitingFor: wait.ForListeningPort(wiremockSvcPort),
@@ -61,7 +68,7 @@ func (c *WiremockContainer) start() {

runningPort, _ := c.wContainer.MappedPort(ctx, wiremockSvcPort)
c.instancePort = runningPort.Int()
logging.Info("Wiremock container exposed port: %s", runningPort.Port())
logging.Info("Test wiremock started at port: %s", runningPort.Port())
}

func (c *WiremockContainer) Port() int {
4 changes: 2 additions & 2 deletions pkg/base/types/json_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package types

import (
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func TestJsonB(t *testing.T) {

t.Run("should error with nil value", func(t *testing.T) {
var result JsonB
err := result.Scan(nil)
58 changes: 58 additions & 0 deletions pkg/base/types/null_bool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullBoll for empty boolean field
type NullBool sql.NullBool

func (t *NullBool) Scan(value interface{}) error {
var i sql.NullBool
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullBool{i.Bool, false}
} else {
*t = NullBool{i.Bool, true}
}

return nil
}

func (n NullBool) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Bool, nil
}

func (t NullBool) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Bool)
}

func (t *NullBool) UnmarshalJSON(data []byte) error {
var ptr *bool
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Bool = *ptr
} else {
t.Valid = false
}

return nil
}
97 changes: 97 additions & 0 deletions pkg/base/types/null_bool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullBool(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullBool
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, false, result.Bool)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullBool
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, false, result.Bool)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := true

var result NullBool
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Bool)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullBool{true, true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Bool, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullBool{false, false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullBool{false, false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullBool{true, true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "true", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullBool
err := result.UnmarshalJSON([]byte("true"))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, true, result.Bool)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullBool
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, false, result.Bool)
})
}
59 changes: 59 additions & 0 deletions pkg/base/types/null_date_time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
"time"
)

// NullDateTime for empty date/time field
type NullDateTime sql.NullTime

func (t *NullDateTime) Scan(value interface{}) error {
var i sql.NullTime
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullDateTime{i.Time, false}
} else {
*t = NullDateTime{i.Time, true}
}

return nil
}

func (n NullDateTime) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Time, nil
}

func (t NullDateTime) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Time)
}

func (t *NullDateTime) UnmarshalJSON(data []byte) error {
var ptr *time.Time
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Time = *ptr
} else {
t.Valid = false
}

return nil
}
98 changes: 98 additions & 0 deletions pkg/base/types/null_date_time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package types

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestNullDateTime(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullDateTime
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullDateTime
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := time.Now()

var result NullDateTime
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Time)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullDateTime{time.Now(), true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Time, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullDateTime{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullDateTime{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullDateTime{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "\"2022-01-01T00:00:00Z\"", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullDateTime
err := result.UnmarshalJSON([]byte("\"2022-01-01T00:00:00Z\""))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullDateTime
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})
}
58 changes: 58 additions & 0 deletions pkg/base/types/null_float_64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullFloat64 for empty float field
type NullFloat64 sql.NullFloat64

func (t *NullFloat64) Scan(value interface{}) error {
var i sql.NullFloat64
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullFloat64{i.Float64, false}
} else {
*t = NullFloat64{i.Float64, true}
}

return nil
}

func (n NullFloat64) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Float64, nil
}

func (t NullFloat64) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Float64)
}

func (t *NullFloat64) UnmarshalJSON(data []byte) error {
var ptr *float64
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Float64 = *ptr
} else {
t.Valid = false
}

return nil
}
97 changes: 97 additions & 0 deletions pkg/base/types/null_float_64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullFloat64(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullFloat64
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, 0.00, result.Float64)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullFloat64
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, 0.00, result.Float64)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := 123.45

var result NullFloat64
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Float64)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullFloat64{123.45, true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Float64, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullFloat64{0.00, false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullFloat64{0.00, false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullFloat64{123.45, true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "123.45", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullFloat64
err := result.UnmarshalJSON([]byte("123.45"))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, 123.45, result.Float64)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullFloat64
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, 0.00, result.Float64)
})
}
58 changes: 58 additions & 0 deletions pkg/base/types/null_int_16.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullInt16 for empty int16 field
type NullInt16 sql.NullInt16

func (t *NullInt16) Scan(value interface{}) error {
var i sql.NullInt16
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullInt16{i.Int16, false}
} else {
*t = NullInt16{i.Int16, true}
}

return nil
}

func (n NullInt16) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Int16, nil
}

func (t NullInt16) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Int16)
}

func (t *NullInt16) UnmarshalJSON(data []byte) error {
var ptr *int16
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Int16 = *ptr
} else {
t.Valid = false
}

return nil
}
97 changes: 97 additions & 0 deletions pkg/base/types/null_int_16_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullInt16(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullInt16
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int16(0), result.Int16)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullInt16
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int16(0), result.Int16)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := int16(123)

var result NullInt16
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Int16)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullInt16{12, true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Int16, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullInt16{0, false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullInt16{0, false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullInt16{123, true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "123", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullInt16
err := result.UnmarshalJSON([]byte("123"))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, int16(123), result.Int16)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullInt16
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int16(0), result.Int16)
})
}
58 changes: 58 additions & 0 deletions pkg/base/types/null_int_32.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullInt32 for empty int32 field
type NullInt32 sql.NullInt32

func (t *NullInt32) Scan(value interface{}) error {
var i sql.NullInt32
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullInt32{i.Int32, false}
} else {
*t = NullInt32{i.Int32, true}
}

return nil
}

func (n NullInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Int32, nil
}

func (t NullInt32) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Int32)
}

func (t *NullInt32) UnmarshalJSON(data []byte) error {
var ptr *int32
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Int32 = *ptr
} else {
t.Valid = false
}

return nil
}
97 changes: 97 additions & 0 deletions pkg/base/types/null_int_32_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullInt32(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullInt32
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int32(0), result.Int32)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullInt32
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int32(0), result.Int32)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := int32(123)

var result NullInt32
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Int32)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullInt32{12, true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Int32, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullInt32{0, false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullInt32{0, false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullInt32{123, true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "123", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullInt32
err := result.UnmarshalJSON([]byte("123"))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, int32(123), result.Int32)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullInt32
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int32(0), result.Int32)
})
}
58 changes: 58 additions & 0 deletions pkg/base/types/null_int_64.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullInt64 for empty int64 field
type NullInt64 sql.NullInt64

func (t *NullInt64) Scan(value interface{}) error {
var i sql.NullInt64
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullInt64{i.Int64, false}
} else {
*t = NullInt64{i.Int64, true}
}

return nil
}

func (n NullInt64) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}

return n.Int64, nil
}

func (t NullInt64) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Int64)
}

func (t *NullInt64) UnmarshalJSON(data []byte) error {
var ptr *int64
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.Int64 = *ptr
} else {
t.Valid = false
}

return nil
}
97 changes: 97 additions & 0 deletions pkg/base/types/null_int_64_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullInt64(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullInt64
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int64(0), result.Int64)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
value := "invalid"

var result NullInt64
err := result.Scan(value)

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int64(0), result.Int64)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := int64(123)

var result NullInt64
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.Int64)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullInt64{123, true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Int64, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullInt64{0, false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullInt64{0, false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullInt64{123, true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "123", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullInt64
err := result.UnmarshalJSON([]byte("123"))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, int64(123), result.Int64)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullInt64
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, int64(0), result.Int64)
})
}
63 changes: 63 additions & 0 deletions pkg/base/types/null_iso_date.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
"time"
)

// NullIsoDate for empty date field
type NullIsoDate sql.NullTime

func (t *NullIsoDate) Scan(value interface{}) error {
var i sql.NullTime
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullIsoDate{i.Time, false}
} else {
*t = NullIsoDate{i.Time, true}
}

return nil
}

func (t NullIsoDate) Value() (driver.Value, error) {
if !t.Valid {
return nil, nil
}

return t.Time, nil
}

func (t NullIsoDate) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Time.Format(time.DateOnly))
}

func (t *NullIsoDate) UnmarshalJSON(data []byte) error {
var ptr *string
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr == nil {
t.Valid = false
return nil
}

parsedDate, err := time.Parse(time.DateOnly, *ptr)
if err != nil {
return err
}

t.Time, t.Valid = parsedDate, true
return nil
}
101 changes: 101 additions & 0 deletions pkg/base/types/null_iso_date_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package types

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestNullIsoDate(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullIsoDate
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
var result NullIsoDate
err := result.Scan("invalid")

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
expected := time.Now()

var result NullIsoDate
err := result.Scan(expected)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, expected, result.Time)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullIsoDate{time.Now(), true}

result, err := expected.Value()

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Time, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullIsoDate{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullIsoDate{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), false}

json, err := expected.MarshalJSON()
result := string(json)

assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullIsoDate{time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), true}

json, err := expected.MarshalJSON()
result := string(json)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "\"2022-01-01\"", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullIsoDate
err := result.UnmarshalJSON([]byte("\"2022-01-01\""))

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, time.Time(time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullIsoDate
err := result.UnmarshalJSON([]byte("invalid"))

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})
}
63 changes: 63 additions & 0 deletions pkg/base/types/null_iso_time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
"time"
)

// NullIsoTime for empty time field
type NullIsoTime sql.NullTime

func (t *NullIsoTime) Scan(value interface{}) error {
var i sql.NullTime
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullIsoTime{i.Time, false}
} else {
*t = NullIsoTime{i.Time, true}
}

return nil
}

func (t NullIsoTime) Value() (driver.Value, error) {
if !t.Valid {
return nil, nil
}

return t.Time, nil
}

func (t NullIsoTime) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.Time.Format(time.TimeOnly))
}

func (t *NullIsoTime) UnmarshalJSON(data []byte) error {
var ptr *string
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr == nil {
t.Valid = false
return nil
}

parsedTime, err := time.Parse(time.TimeOnly, *ptr)
if err != nil {
return err
}

t.Time, t.Valid = parsedTime, true
return nil
}
101 changes: 101 additions & 0 deletions pkg/base/types/null_iso_time_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package types

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestNullIsoTime(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullIsoTime
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should error when scan with a invalid value", func(t *testing.T) {
var result NullIsoTime
err := result.Scan("invalid")

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
expected := time.Now()

var result NullIsoTime
err := result.Scan(expected)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, expected, result.Time)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullIsoTime{time.Now(), true}

result, err := expected.Value()

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.Time, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullIsoTime{time.Time(time.Date(1, time.January, 1, 10, 20, 30, 0, time.UTC)), false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullIsoTime{time.Time(time.Date(1, time.January, 1, 10, 20, 30, 0, time.UTC)), false}

json, err := expected.MarshalJSON()
result := string(json)

assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullIsoTime{time.Time(time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)), true}

json, err := expected.MarshalJSON()
result := string(json)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "\"10:20:30\"", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullIsoTime
err := result.UnmarshalJSON([]byte("\"10:20:30\""))

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, time.Time(time.Date(0, time.January, 1, 10, 20, 30, 0, time.UTC)), result.Time)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullIsoTime
err := result.UnmarshalJSON([]byte("invalid"))

assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, time.Time(time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC)), result.Time)
})
}
58 changes: 58 additions & 0 deletions pkg/base/types/null_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding/json"
"reflect"
)

// NullString for empty string field
type NullString sql.NullString

func (t *NullString) Scan(value interface{}) error {
var i sql.NullString
if err := i.Scan(value); err != nil {
return err
}

if reflect.TypeOf(value) == nil {
*t = NullString{i.String, false}
} else {
*t = NullString{i.String, true}
}

return nil
}

func (ns NullString) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}

return ns.String, nil
}

func (t NullString) MarshalJSON() ([]byte, error) {
if !t.Valid {
return json.Marshal(nil)
}

return json.Marshal(t.String)
}

func (t *NullString) UnmarshalJSON(data []byte) error {
var ptr *string
if err := json.Unmarshal(data, &ptr); err != nil {
return err
}

if ptr != nil {
t.Valid = true
t.String = *ptr
} else {
t.Valid = false
}

return nil
}
85 changes: 85 additions & 0 deletions pkg/base/types/null_string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNullString(t *testing.T) {
t.Run("Should error when scan with a nil value", func(t *testing.T) {
var result NullString
err := result.Scan(nil)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, "", result.String)
})

t.Run("Should scan with a valid value", func(t *testing.T) {
value := "string test"

var result NullString
err := result.Scan(value)

assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, value, result.String)
})

t.Run("Should get value with a valid value", func(t *testing.T) {
expected := NullString{"string test", true}

result, err := expected.Value()
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, expected.String, result)
})

t.Run("Should return nil when get value with a invalid value", func(t *testing.T) {
expected := NullString{"", false}

result, err := expected.Value()
assert.Nil(t, err)
assert.Nil(t, result)
})

t.Run("Should return null when get json value with a invalid value", func(t *testing.T) {
expected := NullString{"", false}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.Equal(t, "null", result)
})

t.Run("Should get json value with a valid value", func(t *testing.T) {
expected := NullString{"string test", true}

json, err := expected.MarshalJSON()
result := string(json)
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, "\"string test\"", result)
})

t.Run("Should get value with a valid json", func(t *testing.T) {
var result NullString
err := result.UnmarshalJSON([]byte("\"string test\""))
assert.Nil(t, err)
assert.NotNil(t, result)
assert.Equal(t, true, result.Valid)
assert.Equal(t, "string test", result.String)
})

t.Run("Should return error when get value with a invalid json", func(t *testing.T) {
var result NullString
err := result.UnmarshalJSON([]byte("invalid"))
assert.NotNil(t, err)
assert.NotNil(t, result)
assert.Equal(t, false, result.Valid)
assert.Equal(t, "", result.String)
})
}
467 changes: 0 additions & 467 deletions pkg/base/types/null_types.go

This file was deleted.

812 changes: 0 additions & 812 deletions pkg/base/types/null_types_test.go

This file was deleted.

19 changes: 0 additions & 19 deletions pkg/base/types/page.go
Original file line number Diff line number Diff line change
@@ -11,25 +11,6 @@ type Page[T any] struct {
TotalElements uint64 `json:"totalElements"`
}

// SortDirection is the field sort direction
type SortDirection string

const (
ASC SortDirection = "ASC"
DESC SortDirection = "DESC"
)

// Sort is the contract to sort an field
type Sort struct {
Direction SortDirection
Field string
}

// NewSort returns a new Sort
func NewSort(direction SortDirection, field string) Sort {
return Sort{direction, field}
}

// PageRequest is the contract of request page
type PageRequest struct {
Page uint16
64 changes: 28 additions & 36 deletions pkg/base/types/page_test.go
Original file line number Diff line number Diff line change
@@ -6,57 +6,49 @@ import (
"github.com/stretchr/testify/assert"
)

func TestNewSort(t *testing.T) {

t.Run("NewSort", func(t *testing.T) {
s := NewSort(ASC, "name")
assert.NotNil(t, s)
assert.Equal(t, ASC, s.Direction)
assert.Equal(t, "name", s.Field)
})
}

func TestNewPageRequest(t *testing.T) {
const (
page uint16 = 1
size uint16 = 10
)

t.Run("Should return a new PageRequest with nil sort list", func(t *testing.T) {
s := NewPageRequest(page, size, nil)
assert.NotNil(t, s)
assert.Equal(t, s.Page, page)
assert.Equal(t, s.Size, size)
assert.Nil(t, s.Order)
assert.Equal(t, "", s.GetOrder())
expected := &PageRequest{Page: page, Size: size}

result := NewPageRequest(expected.Page, expected.Size, nil)

assert.NotNil(t, result)
assert.EqualValues(t, expected, result)
assert.Equal(t, "", result.GetOrder())
})

t.Run("Should return a new PageRequest with empty sort list", func(t *testing.T) {
s := NewPageRequest(page, size, []Sort{})
assert.NotNil(t, s)
assert.Equal(t, s.Page, page)
assert.Equal(t, s.Size, size)
assert.NotNil(t, s.Order)
assert.Equal(t, "", s.GetOrder())
expected := &PageRequest{Page: page, Size: size, Order: []Sort{}}

result := NewPageRequest(expected.Page, expected.Size, expected.Order)

assert.NotNil(t, result)
assert.EqualValues(t, expected, result)
assert.Equal(t, "", result.GetOrder())
})

t.Run("Should return a new PageRequest with one populated sort list", func(t *testing.T) {
sort := []Sort{NewSort(ASC, "field1")}
s := NewPageRequest(page, size, sort)
assert.NotNil(t, s)
assert.Equal(t, s.Page, page)
assert.Equal(t, s.Size, size)
assert.NotNil(t, s.Order)
assert.Equal(t, "field1 ASC", s.GetOrder())
expected := &PageRequest{Page: page, Size: size, Order: []Sort{NewSort(ASC, "field1")}}

result := NewPageRequest(expected.Page, expected.Size, expected.Order)

assert.NotNil(t, result)
assert.EqualValues(t, expected, result)
assert.EqualValues(t, "field1 ASC", result.GetOrder())
})

t.Run("Should return a new PageRequest with many populated sort list", func(t *testing.T) {
sort := []Sort{NewSort(ASC, "field1"), NewSort(DESC, "field2")}
s := NewPageRequest(page, size, sort)
assert.NotNil(t, s)
assert.Equal(t, s.Page, page)
assert.Equal(t, s.Size, size)
assert.NotNil(t, s.Order)
assert.Equal(t, "field1 ASC, field2 DESC", s.GetOrder())
expected := &PageRequest{Page: page, Size: size, Order: []Sort{NewSort(ASC, "field1"), NewSort(DESC, "field2")}}

result := NewPageRequest(expected.Page, expected.Size, expected.Order)

assert.NotNil(t, result)
assert.EqualValues(t, expected, result)
assert.Equal(t, "field1 ASC, field2 DESC", result.GetOrder())
})
}
20 changes: 20 additions & 0 deletions pkg/base/types/sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package types

// SortDirection is the field sort direction
type SortDirection string

const (
ASC SortDirection = "ASC"
DESC SortDirection = "DESC"
)

// Sort is the contract to sort an field
type Sort struct {
Direction SortDirection
Field string
}

// NewSort returns a new Sort
func NewSort(direction SortDirection, field string) Sort {
return Sort{direction, field}
}
18 changes: 18 additions & 0 deletions pkg/base/types/sort_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package types

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewSort(t *testing.T) {
t.Run("Should return new sort", func(t *testing.T) {
expected := Sort{Direction: ASC, Field: "name"}

result := NewSort(expected.Direction, expected.Field)

assert.NotNil(t, result)
assert.EqualValues(t, expected, result)
})
}
51 changes: 47 additions & 4 deletions pkg/storage/aws.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package storage
import (
"context"
"mime/multipart"
"os"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
@@ -12,10 +13,15 @@ import (
)

type awsStorage struct {
s3Service *s3.S3
uploader *s3manager.Uploader
s3Service *s3.S3
uploader *s3manager.Uploader
downloader *s3manager.Downloader
}

// newAwsStorage creates a new awsStorage instance and initializes the S3 service, uploader, and downloader.
//
// No parameters.
// Returns a pointer to the awsStorage instance.
func newAwsStorage() *awsStorage {
var s awsStorage
s.s3Service = s3.New(cloud.GetAwsSession())
@@ -24,10 +30,40 @@ func newAwsStorage() *awsStorage {
}

s.uploader = s3manager.NewUploader(cloud.GetAwsSession())
s.downloader = s3manager.NewDownloader(cloud.GetAwsSession())
return &s
}

func (s awsStorage) uploadFile(ctx context.Context, bucket, key string, file *multipart.File) (string, error) {
// downloadFile downloads a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is downloaded.
// key: the key or identifier of the file to be downloaded.
// Returns a file pointer and an error.
func (s *awsStorage) downloadFile(ctx context.Context, bucket, key string) (*os.File, error) {
file, err := os.CreateTemp("", "tempFile")
if err != nil {
return nil, err
}

if _, err := s.downloader.DownloadWithContext(ctx, file, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}); err != nil {
return nil, err
}

return file, nil
}

// uploadFile uploads a file to the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket to upload the file to.
// key: the key or identifier of the file to be uploaded.
// file: the file to be uploaded.
// Returns the location of the uploaded file and an error, if any.
func (s *awsStorage) uploadFile(ctx context.Context, bucket, key string, file *multipart.File) (string, error) {
result, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
@@ -40,10 +76,17 @@ func (s awsStorage) uploadFile(ctx context.Context, bucket, key string, file *mu
return result.Location, nil
}

func (s awsStorage) deleteFile(ctx context.Context, bucket, key string) error {
// deleteFile deletes a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is deleted.
// key: the key or identifier of the file to be deleted.
// Returns an error.
func (s *awsStorage) deleteFile(ctx context.Context, bucket, key string) error {
_, err := s.s3Service.DeleteObjectWithContext(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})

return err
}
47 changes: 47 additions & 0 deletions pkg/storage/gcp.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"mime/multipart"
"os"

gcp_storage "cloud.google.com/go/storage"
"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
@@ -14,6 +15,10 @@ type gcpStorage struct {
client *gcp_storage.Client
}

// newGcpStorage creates a new instance of gcpStorage by initializing the client.
//
// No parameters.
// Returns a pointer to gcpStorage.
func newGcpStorage() *gcpStorage {
client, err := gcp_storage.NewClient(context.Background())
if err != nil {
@@ -24,6 +29,42 @@ func newGcpStorage() *gcpStorage {

}

// downloadFile downloads a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is downloaded.
// key: the key or identifier of the file to be downloaded.
// Returns a file pointer and an error.
func (s *gcpStorage) downloadFile(ctx context.Context, bucket, key string) (*os.File, error) {
file, err := os.CreateTemp("", "tempFile")
if err != nil {
return nil, err
}

if _, err := s.client.Bucket(bucket).Object(key).NewReader(ctx); err != nil {
return nil, err
}

fileReader, err := s.client.Bucket(bucket).Object(key).NewReader(ctx)
if err != nil {
return nil, err
}
defer fileReader.Close()

if _, err := io.Copy(file, fileReader); err != nil {
return nil, err
}

return file, nil
}

// uploadFile uploads a file to the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket to upload the file to.
// key: the key or identifier of the file to be uploaded.
// file: the file to be uploaded.
// Returns the location of the uploaded file and an error, if any.
func (s *gcpStorage) uploadFile(ctx context.Context, bucket, key string, file *multipart.File) (string, error) {
writer := s.client.Bucket(bucket).Object(key).NewWriter(ctx)
writer.CacheControl = "no-cache, max-age=1"
@@ -37,6 +78,12 @@ func (s *gcpStorage) uploadFile(ctx context.Context, bucket, key string, file *m
return fmt.Sprintf("https://storage.googleapis.com/%s/%s", bucket, key), nil
}

// deleteFile deletes a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is deleted.
// key: the key or identifier of the file to be deleted.
// Returns an error.
func (s *gcpStorage) deleteFile(ctx context.Context, bucket, key string) error {
return s.client.Bucket(bucket).Object(key).Delete(ctx)
}
40 changes: 39 additions & 1 deletion pkg/storage/storage.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package storage
import (
"context"
"mime/multipart"
"os"

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/config"
"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
@@ -15,13 +16,17 @@ const (
)

type storage interface {
downloadFile(ctx context.Context, bucket, key string) (*os.File, error)
uploadFile(ctx context.Context, bucket, key string, file *multipart.File) (string, error)
deleteFile(ctx context.Context, bucket, key string) error
}

var instance storage

// Initialize loads the storage settings according to the configured environment.
// Initialize initializes the storage provider based on the configured cloud.
//
// No parameters.
// No return values.
func Initialize() {
switch config.CLOUD {
case config.CLOUD_AWS:
@@ -33,6 +38,33 @@ func Initialize() {
logging.Info("Storage provider connected")
}

// DownloadFile downloads a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is downloaded.
// key: the key or identifier of the file to be downloaded.
// Returns a file pointer and an error.
func DownloadFile(ctx context.Context, bucket, key string) (*os.File, error) {
txn := monitoring.GetTransactionInContext(ctx)
if txn != nil {
segment := monitoring.StartTransactionSegment(ctx, storage_transaction, map[string]string{
"method": "Download",
"bucket": bucket,
"key": key,
})
defer monitoring.EndTransactionSegment(segment)
}

return instance.downloadFile(ctx, bucket, key)
}

// UploadFile uploads a file to the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket to upload the file to.
// key: the key or identifier of the file to be uploaded.
// file: the file to be uploaded.
// Returns the location of the uploaded file and an error, if any.
func UploadFile(ctx context.Context, bucket, key string, file *multipart.File) (string, error) {
txn := monitoring.GetTransactionInContext(ctx)
if txn != nil {
@@ -47,6 +79,12 @@ func UploadFile(ctx context.Context, bucket, key string, file *multipart.File) (
return instance.uploadFile(ctx, bucket, key, file)
}

// DeleteFile deletes a file from the storage provider.
//
// ctx: the context for the operation.
// bucket: the storage bucket from which the file is deleted.
// key: the key or identifier of the file to be deleted.
// Returns an error.
func DeleteFile(ctx context.Context, bucket, key string) error {
txn := monitoring.GetTransactionInContext(ctx)
if txn != nil {
84 changes: 84 additions & 0 deletions pkg/storage/storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package storage

import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"os"
"strings"
"testing"

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/test"
"github.com/stretchr/testify/assert"
)

const (
BUCKET = "my-bucket"
ID = "a0264d4f-cb3b-41e7-9632-2f8f86f6b28d"
)

func TestMain(m *testing.M) {
test.InitializeTestLocalstack()

Initialize()

m.Run()
}

func TestStorage(t *testing.T) {
ctx := context.Background()
file, err := generateFile()

t.Run("Should upload with success", func(t *testing.T) {
expected := fmt.Sprintf("/%s/%s", BUCKET, ID)

assert.NoError(t, err)

result, err := UploadFile(ctx, BUCKET, ID, file)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, strings.Contains(result, expected))
})

t.Run("Should download with success", func(t *testing.T) {
_, err := UploadFile(ctx, BUCKET, ID, file)
assert.NoError(t, err)
result, err := DownloadFile(ctx, BUCKET, ID)
assert.NoError(t, err)
assert.NotNil(t, result)
})

t.Run("Should delete with success", func(t *testing.T) {
err := DeleteFile(ctx, BUCKET, ID)
assert.NoError(t, err)
})
}

func generateFile() (*multipart.File, error) {
filePath := "./../../development-environment/storage/file.txt"
fieldName := "file"
body := new(bytes.Buffer)

multipartWriter := multipart.NewWriter(body)
defer multipartWriter.Close()

file, err := os.Open(filePath)
if err != nil {
return nil, err
}

fileWriter, err := multipartWriter.CreateFormFile(fieldName, filePath)
if err != nil {
return nil, err
}

if _, err := io.Copy(fileWriter, file); err != nil {
return nil, err
}

filePtr := new(multipart.File)
*filePtr = file
return filePtr, nil
}
331 changes: 32 additions & 299 deletions pkg/web/restclient/client.go
Original file line number Diff line number Diff line change
@@ -1,328 +1,61 @@
package restclient

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
"github.com/colibri-project-io/colibri-sdk-go/pkg/base/monitoring"
"github.com/mercari/go-circuitbreaker"
"github.com/newrelic/go-agent/v3/newrelic"
)

const (
timeoutDefault uint = 1
restClientTransaction string = "REST-CLIENT"
timeoutDefault uint = 1
restClientTransaction string = "REST-CLIENT"
errServiceNotAvailable string = "service not available"
errResponseWithEmptyBody string = "response returned with empty body and %d status code"
)

var ErrServiceNotAvailable = errors.New("service not available")

type ResponseBody interface {
any
}

type RequestData interface {
any
}

type ResponseErrorData interface {
any
}

// RestClient client http to encapsulate rest calls
// RestClient struct with http client and circuit breaker
// name: Represents the name of the REST client.
// baseURL: Holds the base URL of the REST client.
// retries: Specifies the number of retries for the REST client.
// retrySleep: Indicates the time to sleep between retries.
// client: Is a pointer to an http.Client for making HTTP requests.
// cb: Points to a circuitbreaker.CircuitBreaker for managing circuit breaking behavior in the REST client.
type RestClient struct {
name string
baseURL string
client *http.Client
cb *circuitbreaker.CircuitBreaker
name string
baseURL string
retries uint8
retrySleep uint
client *http.Client
cb *circuitbreaker.CircuitBreaker
}

// NewRestClient create new client http with timeout configuration and New Relic transport configuration
func NewRestClient(name string, baseURL string, timeout uint) *RestClient {
if timeout == 0 {
timeout = timeoutDefault
// NewRestClient creates a new REST client based on the provided configuration.
//
// config: A pointer to RestClientConfig containing the configuration details for the REST client.
// Returns a pointer to RestClient.
func NewRestClient(config *RestClientConfig) *RestClient {
if config == nil {
return nil
}

if config.Timeout == 0 {
config.Timeout = timeoutDefault
}
client := &http.Client{Timeout: time.Duration(timeout) * time.Second}
client := &http.Client{Timeout: time.Duration(config.Timeout) * time.Second}
client.Transport = newrelic.NewRoundTripper(client.Transport)
return &RestClient{
name: name,
baseURL: baseURL,
name: config.Name,
baseURL: config.BaseURL,
client: client,
cb: circuitbreaker.New(
circuitbreaker.WithOpenTimeout(time.Second*10),
circuitbreaker.WithTripFunc(circuitbreaker.NewTripFuncConsecutiveFailures(5)),
circuitbreaker.WithOnStateChangeHookFn(func(oldState, newState circuitbreaker.State) {
logging.Info("[%s] state changed: old [%s] -> new [%s]", name, string(oldState), string(newState))
logging.Info("[%s] state changed: old [%s] -> new [%s]", config.Name, string(oldState), string(newState))
}),
),
}
}

// Get call get request
func Get[T ResponseBody](ctx context.Context, client *RestClient, path string, headers map[string]string) ResponseData[T] {
resp, _ := callRequestWithoutBody[T, any](ctx, http.MethodGet, client, path, headers)
return resp
}

// GetWithErrorData call get request and when error occurs decode err into ResponseErrorData
func GetWithErrorData[T ResponseBody, E ResponseErrorData](ctx context.Context, client *RestClient, path string, headers map[string]string) (ResponseData[T], *E) {
return callRequestWithoutBody[T, E](ctx, http.MethodGet, client, path, headers)
}

// Post call post request
func Post[T ResponseBody, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) ResponseData[T] {
resp, _ := callRequest[T, any](ctx, http.MethodPost, client, path, entityBody, headers)
return resp
}

// PostWithErrorData call post request and when error occurs decode err into ResponseErrorData
func PostWithErrorData[T ResponseBody, E ResponseErrorData, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) (ResponseData[T], *E) {
return callRequest[T, E](ctx, http.MethodPost, client, path, entityBody, headers)
}

// PostBodyString call post request with string as body
// to set Content-type pass is header map
//
// Ex.: to make a form url encoded request
// `PostBodyString[MyReturnData](ctx, client, "/my-path", "body string for urlencoded...", map[string]string{"Content-Type": "application/x-www-form-urlencoded"})
func PostBodyString[T ResponseBody](ctx context.Context, client *RestClient, path string, bodyString string, headers map[string]string) ResponseData[T] {
return callRequestBodyString[T](ctx, http.MethodPost, client, path, bodyString, headers)
}

// Put call put request
func Put[T ResponseBody, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) ResponseData[T] {
resp, _ := callRequest[T, any](ctx, http.MethodPut, client, path, entityBody, headers)
return resp
}

// PutWithErrorData call put request and when error occurs decode err into ResponseErrorData
func PutWithErrorData[T ResponseBody, E ResponseErrorData, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) (ResponseData[T], *E) {
return callRequest[T, E](ctx, http.MethodPut, client, path, entityBody, headers)
}

// Patch call patch request
func Patch[T ResponseBody, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) ResponseData[T] {
resp, _ := callRequest[T, any](ctx, http.MethodPatch, client, path, entityBody, headers)
return resp
}

// PatchWithErrorData call patch request and when error occurs decode err into ResponseErrorData
func PatchWithErrorData[T ResponseBody, E ResponseErrorData, R RequestData](ctx context.Context, client *RestClient, path string, entityBody *R, headers map[string]string) (ResponseData[T], *E) {
return callRequest[T, E](ctx, http.MethodPatch, client, path, entityBody, headers)
}

// Delete call delete request
func Delete[T ResponseBody](ctx context.Context, client *RestClient, path string, headers map[string]string) ResponseData[T] {
resp, _ := callRequestWithoutBody[T, any](ctx, http.MethodDelete, client, path, headers)
return resp
}

// DeleteWithErrorData call delete request and when error occurs decode err into ResponseErrorData
func DeleteWithErrorData[T ResponseBody, E ResponseErrorData](ctx context.Context, client *RestClient, path string, headers map[string]string) (ResponseData[T], *E) {
return callRequestWithoutBody[T, E](ctx, http.MethodDelete, client, path, headers)
}

func callRequestWithoutBody[T ResponseBody, E ResponseErrorData](ctx context.Context, method string, client *RestClient, path string, headers map[string]string) (_ ResponseData[T], _ *E) {
if !client.cb.Ready() {
return newResponseData[T](http.StatusInternalServerError, nil, nil, ErrServiceNotAvailable), nil
}

var err error
defer func() {
err = client.cb.Done(ctx, err)
}()

if monitoring.GetTransactionInContext(ctx) != nil {
segment := createSegment(ctx, method, path)
defer monitoring.EndTransactionSegment(segment)
}

url := fmt.Sprintf("%s%s", client.baseURL, path)
req, err := http.NewRequest(method, url, nil)
if err != nil {
fErr := fmt.Errorf("could not create new http request %s: %w", url, err)
return newResponseData[T](http.StatusInternalServerError, nil, nil, fErr), nil
}

addHeaders(req, headers)
resp, err := client.client.Do(req)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, err), nil
}

statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300
if !statusOK {
respBErr, err := processErrorResponse[E](resp)
return newResponseData[T](resp.StatusCode, nil, resp.Header, err), respBErr
}

r, err := decodeResponse[T](resp)
return newResponseData(resp.StatusCode, r, resp.Header, err), nil
}

func callRequest[T ResponseBody, E ResponseErrorData, R RequestData](ctx context.Context, method string, client *RestClient, path string, entityBody *R, headers map[string]string) (_ ResponseData[T], _ *E) {
if !client.cb.Ready() {
return newResponseData[T](http.StatusInternalServerError, nil, nil, ErrServiceNotAvailable), nil
}

var err error
defer func() {
err = client.cb.Done(ctx, err)
}()

if monitoring.GetTransactionInContext(ctx) != nil {
segment := createSegment(ctx, method, path)
defer monitoring.EndTransactionSegment(segment)
}

url := fmt.Sprintf("%s%s", client.baseURL, path)
bytesBody, err := makeBytesBody(entityBody)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, err), nil
}
req, err := http.NewRequest(method, url, bytesBody)

addHeaders(req, headers)
resp, err := client.client.Do(req)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, err), nil
}

statusOK := resp.StatusCode >= 200 && resp.StatusCode < 300
if !statusOK {
errBody, err := processErrorResponse[E](resp)
return newResponseData[T](resp.StatusCode, nil, resp.Header, err), errBody
}

r, err := decodeResponse[T](resp)
return newResponseData(resp.StatusCode, r, resp.Header, err), nil
}

func callRequestBodyString[T ResponseBody](ctx context.Context, method string, client *RestClient, path string, bodyString string, headers map[string]string) ResponseData[T] {
if !client.cb.Ready() {
return newResponseData[T](http.StatusInternalServerError, nil, nil, ErrServiceNotAvailable)
}

var err error
defer func() {
err = client.cb.Done(ctx, err)
}()

if monitoring.GetTransactionInContext(ctx) != nil {
segment := createSegment(ctx, method, path)
defer monitoring.EndTransactionSegment(segment)
}

dataIOReader := strings.NewReader(bodyString)

url := fmt.Sprintf("%s%s", client.baseURL, path)

req, err := http.NewRequest(method, url, dataIOReader)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, fmt.Errorf("could not create new request: %w", err))
}

addHeaders(req, headers)

resp, err := client.client.Do(req)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, err)
}
defer func() {
if err = resp.Body.Close(); err != nil {
logging.Error("could not close response body: %v", err)
}
}()
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
return newResponseData[T](http.StatusInternalServerError, nil, nil, err)
}

var r T
if err := json.Unmarshal(bodyText, &r); err != nil {
return newResponseData[T](resp.StatusCode, nil, resp.Header, err)
}
return newResponseData(resp.StatusCode, &r, resp.Header, err)
}

func createSegment(ctx context.Context, method string, path string) interface{} {
segment := monitoring.StartTransactionSegment(ctx, restClientTransaction, map[string]string{
"method": method,
"path": path,
})
return segment
}

func processErrorResponse[E ResponseErrorData](resp *http.Response) (*E, error) {
respErr, derr := decodeResponseErrorData[E](resp)
if respErr != nil {
return respErr, fmt.Errorf("%d statusCode", resp.StatusCode)
} else if derr != nil {
return nil, fmt.Errorf("%d statusCode. Body decoder Error: %w", resp.StatusCode, derr)
}
return nil, fmt.Errorf("%d statusCode", resp.StatusCode)
}

func makeBytesBody(entityBody any) (*bytes.Buffer, error) {
if entityBody == nil {
return nil, nil
}
requestBody, err := json.Marshal(entityBody)
if err != nil {
return nil, fmt.Errorf("could not marshal entity body: %w", err)
}
return bytes.NewBuffer(requestBody), nil
}

func decodeResponse[T ResponseBody](resp *http.Response) (*T, error) {
defer func() {
closeBody(resp)
}()
if resp.StatusCode == http.StatusNoContent {
return nil, nil
}
var responseModel T
err := json.NewDecoder(resp.Body).Decode(&responseModel)
if err != nil {
return nil, fmt.Errorf("could not decode response: %w", err)
}
return &responseModel, nil
}

func decodeResponseErrorData[E ResponseErrorData](resp *http.Response) (*E, error) {
defer func() {
closeBody(resp)
}()
var responseModel E
err := json.NewDecoder(resp.Body).Decode(&responseModel)
switch {
case err == io.EOF:
return nil, nil
case err != nil:
return nil, fmt.Errorf("could not decode response: %w", err)
}
return &responseModel, nil
}

func addHeaders(req *http.Request, headers map[string]string) {
for k, v := range headers {
req.Header.Set(k, v)
}
}

func closeBody(resp *http.Response) {
if resp == nil {
return
}

if err := resp.Body.Close(); err != nil {
logging.Error("error when close response body: %v\n", err)
}
}
9 changes: 9 additions & 0 deletions pkg/web/restclient/client_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package restclient

type RestClientConfig struct {
Name string
BaseURL string
Timeout uint
Retries uint8
RetrySleepInSeconds uint
}
551 changes: 452 additions & 99 deletions pkg/web/restclient/client_test.go

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions pkg/web/restclient/multipart_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package restclient

import "io"

type MultipartFile struct {
FileName string
File io.Reader
ContentType string
}
304 changes: 304 additions & 0 deletions pkg/web/restclient/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
package restclient

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"reflect"
"strings"
"time"

"github.com/colibri-project-io/colibri-sdk-go/pkg/base/logging"
"github.com/colibri-project-io/colibri-sdk-go/pkg/database/cacheDB"
)

const (
request_ctx_is_empty string = "context is empty"
request_client_is_empty string = "client is empty"
request_method_is_empty string = "http method is empty"
)

// Request struct for http requests
type Request[T ResponseSuccessData, E ResponseErrorData] struct {
Ctx context.Context
Client *RestClient
HttpMethod string // use http.methodXXX

Cache *cacheDB.Cache[T]
Path string
Headers map[string]string
Body any
MultipartFields map[string]any
writer *multipart.Writer
}

// Call executes the HTTP request and handles retries and caching.
//
// It validates the request, checks cache, executes the request with retries, handles success, and logs errors.
// Returns the response data.
func (req Request[T, E]) Call() (response ResponseData[T, E]) {
if err := req.validate(); err != nil {
return newResponseData[T, E](http.StatusInternalServerError, nil, nil, nil, err)
}

if req.hasCache() {
data, _ := req.Cache.One(req.Ctx)
if data != nil {
return newResponseData[T, E](http.StatusNotModified, nil, data, nil, nil)
}
}

for execution := uint8(0); execution <= req.Client.retries; execution++ {
response = req.execute()
if response.HasSuccess() {
if req.hasCache() {
req.Cache.Set(req.Ctx, response.SuccessBody())
}
break
}

time.Sleep(req.getSleepDuration())
if req.Client.retries != 0 {
logging.Warn("[%dx] call to the url '%s'. status code = %d | general error: %v | response error: %v", execution+1, req.getUrl(), response.StatusCode(), response.Error(), response.ErrorBody())
}
}

return
}

// validate checks if the Request struct fields are valid and returns an error if any are missing.
//
// No parameters.
// Returns an error.
func (rc *Request[T, E]) validate() error {
if rc.Ctx == nil {
return errors.New(request_ctx_is_empty)
}

if rc.Client == nil {
return errors.New(request_client_is_empty)
}

if rc.HttpMethod == "" {
return errors.New(request_method_is_empty)
}

return nil
}

// hasCache checks if the Request struct has a cache attached.
//
// No parameters.
// Returns a boolean.
func (rc *Request[T, E]) hasCache() bool {
return rc.Cache != nil
}

// getUrl returns the full URL by combining the base URL with the path.
//
// No parameters.
// Returns a string.
func (rc *Request[T, E]) getUrl() string {
return fmt.Sprintf("%s%s", rc.Client.baseURL, rc.Path)
}

// getSleepDuration returns the sleep duration as a time.Duration.
//
// No parameters.
// Returns a time.Duration.
func (rc *Request[T, E]) getSleepDuration() time.Duration {
return time.Duration(rc.Client.retrySleep) * time.Second
}

// getBytesBody returns the body as an io.Reader and an error.
//
// No parameters.
// Returns an io.Reader and an error.
func (rc *Request[T, E]) getBytesBody() (io.Reader, error) {
if rc.Body == nil && len(rc.MultipartFields) == 0 {
return nil, nil
}

if len(rc.MultipartFields) > 0 {
return rc.processMultipartFields()
}

if reflect.ValueOf(rc.Body).Kind() == reflect.String {
return strings.NewReader(fmt.Sprintf("%v", rc.Body)), nil
}

requestBody, err := json.Marshal(rc.Body)
if err != nil {
return nil, fmt.Errorf("could not marshal body: %w", err)
}

return bytes.NewBuffer(requestBody), nil
}

// processMultipartFields processes the multipart fields and returns the io.Reader and an error.
//
// No parameters.
// Returns an io.Reader and an error.
func (rc *Request[T, E]) processMultipartFields() (io.Reader, error) {
body := &bytes.Buffer{}
rc.writer = multipart.NewWriter(body)
for fieldName, contentField := range rc.MultipartFields {
if err := rc.processField(fieldName, contentField); err != nil {
return nil, err
}
}

if err := rc.writer.Close(); err != nil {
return nil, err
}

return body, nil
}

// processField processes the field based on its type and performs the necessary actions accordingly.
//
// fieldName: the name of the field being processed (string).
// contentField: the content of the field being processed (interface{}).
// Returns an error if any issues occur during processing.
func (rc *Request[T, E]) processField(fieldName string, contentField interface{}) error {
if file, ok := contentField.(MultipartFile); ok {
part, err := rc.createFilePart(fieldName, file)
if err != nil {
return err
}

if _, err := io.Copy(part, file.File); err != nil {
return err
}
} else if str, ok := contentField.(string); ok {
if err := rc.writer.WriteField(fieldName, str); err != nil {
return err
}
} else {
return errors.New("error while sending the multipart/form-data: data type not allowed")
}

return nil
}

// createFilePart creates a file part for the request based on the field name and the file.
//
// fieldName: the name of the field for the file part (string).
// file: the file to be processed (MultipartFile).
// Returns an io.Writer and an error.
func (rc *Request[T, E]) createFilePart(fieldName string, file MultipartFile) (io.Writer, error) {
if file.ContentType != "" {
return rc.createCustomContentTypeFormFile(fieldName, file)
}

return rc.writer.CreateFormFile(fieldName, file.FileName)
}

// createCustomContentTypeFormFile creates a custom content type form file for the request.
//
// fieldName: the name of the field for the file part (string).
// file: the file to be processed (MultipartFile).
// Returns an io.Writer and an error.
func (rc *Request[T, E]) createCustomContentTypeFormFile(fieldName string, file MultipartFile) (io.Writer, error) {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, file.FileName))
h.Set("Content-Type", file.ContentType)

return rc.writer.CreatePart(h)
}

// addHeadersInRequest sets the headers in the HTTP request based on the headers map in the Request struct.
//
// req: the HTTP request to add headers to (*http.Request).
// Return type: void
func (rc *Request[T, E]) addHeadersInRequest(req *http.Request) {
for key, value := range rc.Headers {
req.Header.Set(key, value)
}
}

// execute executes the HTTP request and processes the response data.
//
// No parameters.
// Returns a ResponseData containing the response data and any errors.
func (req *Request[T, E]) execute() (response ResponseData[T, E]) {
if !req.Client.cb.Ready() {
return newResponseData[T, E](http.StatusInternalServerError, nil, nil, nil, errors.New(errServiceNotAvailable))
}

var err error
defer func() {
err = req.Client.cb.Done(req.Ctx, err)
}()

bytesBody, err := req.getBytesBody()
if err != nil {
return newResponseData[T, E](http.StatusInternalServerError, nil, nil, nil, err)
}

request, err := http.NewRequestWithContext(req.Ctx, string(req.HttpMethod), req.getUrl(), bytesBody)
req.addHeadersInRequest(request)
if len(req.MultipartFields) > 0 {
request.Header.Add("Content-Type", req.writer.FormDataContentType())
}

resp, err := req.Client.client.Do(request)
if err != nil {
return newResponseData[T, E](http.StatusInternalServerError, nil, nil, nil, err)
}

defer resp.Body.Close()
if !statusCodeIsOK(resp) {
errBody, err := processErrorResponse[E](resp)
return newResponseData[T, E](resp.StatusCode, resp.Header, nil, errBody, err)
}

decodedResponse, err := decodeResponse[T](resp)
return newResponseData[T, E](resp.StatusCode, resp.Header, decodedResponse, nil, err)
}

func statusCodeIsOK(resp *http.Response) bool {
return resp.StatusCode >= 200 && resp.StatusCode < 300
}

// processErrorResponse decodes the response error data and handles different error scenarios.
//
// Takes an http.Response as input.
// Returns a pointer to the response error data and an error.
func processErrorResponse[E ResponseErrorData](resp *http.Response) (*E, error) {
respErr, derr := decodeResponse[E](resp)
if respErr != nil {
return respErr, fmt.Errorf("error body decoded with %d status code", resp.StatusCode)
} else if derr != nil {
return nil, fmt.Errorf("%d status code. body decoder error: %w", resp.StatusCode, derr)
}

return nil, fmt.Errorf(errResponseWithEmptyBody, resp.StatusCode)
}

// decodeResponse decodes the response body from an http response into a generic type T.
//
// resp: the http response object to decode
// Returns a pointer to the decoded response model of type T and an error
func decodeResponse[T any](resp *http.Response) (*T, error) {
if resp.ContentLength == 0 {
return nil, nil
}

var responseModel T
err := json.NewDecoder(resp.Body).Decode(&responseModel)
switch {
case err == io.EOF:
return nil, nil
case err != nil:
return nil, fmt.Errorf("could not decode response: %w", err)
}

return &responseModel, nil
}
6 changes: 6 additions & 0 deletions pkg/web/restclient/request_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package restclient

// RequestData interface to encapsulate request data
type RequestData interface {
any
}
144 changes: 125 additions & 19 deletions pkg/web/restclient/response_data.go
Original file line number Diff line number Diff line change
@@ -2,37 +2,143 @@ package restclient

import "net/http"

// ResponseData interface to encapsulate response data with Status, Body, Header and Error
type ResponseData[Body any] interface {
Status() int
Body() *Body
Header() http.Header
// ResponseData interface to encapsulate response data with Status code, Headers, Success body, Error body and Error
type ResponseData[T any, E any] interface {
StatusCode() int
Headers() http.Header
SuccessBody() *T
ErrorBody() *E
Error() error

IsInformationalResponse() bool
IsSuccessfulResponse() bool
IsRedirectionMessage() bool
IsClientErrorResponse() bool
IsServerErrorResponse() bool
HasError() bool
HasSuccess() bool
}

// newResponseData creates a new ResponseData instance with the provided data.
//
// Parameters:
// - statusCode: the status code of the response
// - headers: the headers of the response
// - successBody: the success body of the response
// - errorBody: the error body of the response
// - err: the error associated with the response
//
// Returns a ResponseData instance.
func newResponseData[T any, E any](statusCode int, headers http.Header, successBody *T, errorBody *E, err error) ResponseData[T, E] {
return &responseData[T, E]{
statusCode: statusCode,
headers: headers,
successBody: successBody,
errorBody: errorBody,
err: err,
}
}

func newResponseData[Body any](status int, body *Body, header http.Header, err error) ResponseData[Body] {
return &responseData[Body]{status: status, body: body, header: header, err: err}
// responseData implements the ResponseData interface.
type responseData[T any, E any] struct {
statusCode int
headers http.Header
successBody *T
errorBody *E
err error
}

type responseData[B any] struct {
status int
body *B
header http.Header
err error
// StatusCode returns the status code of the response.
//
// No parameters.
// Returns an integer.
func (r *responseData[T, E]) StatusCode() int {
return r.statusCode
}

func (r *responseData[B]) Status() int {
return r.status
// Headers returns the headers of the response.
//
// No parameters.
// Returns http.Header.
func (r *responseData[T, E]) Headers() http.Header {
return r.headers
}

func (r *responseData[B]) Body() *B {
return r.body
// SuccessBody returns the success body of the response.
//
// No parameters.
// Returns a pointer to the success body type.
func (r *responseData[T, E]) SuccessBody() *T {
return r.successBody
}

func (r *responseData[B]) Header() http.Header {
return r.header
// ErrorBody returns the error body of the response.
//
// No parameters.
// Returns a pointer to the error body.
func (r *responseData[T, E]) ErrorBody() *E {
return r.errorBody
}

func (r *responseData[B]) Error() error {
// Error returns the error associated with the response.
//
// No parameters.
// Returns an error.
func (r *responseData[T, E]) Error() error {
return r.err
}

// IsInformationalResponse checks if the response status code is in the informational range (100-199).
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) IsInformationalResponse() bool {
return r.statusCode >= 100 && r.statusCode <= 199
}

// IsSuccessfulResponse checks if the response status code is within the successful range (200-299).
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) IsSuccessfulResponse() bool {
return r.statusCode >= 200 && r.statusCode <= 299
}

// IsRedirectionMessage checks if the response status code is in the redirection range (300-399).
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) IsRedirectionMessage() bool {
return r.statusCode >= 300 && r.statusCode <= 399
}

// IsClientErrorResponse checks if the response status code is within the client error range (400-499).
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) IsClientErrorResponse() bool {
return r.statusCode >= 400 && r.statusCode <= 499
}

// IsServerErrorResponse checks if the response status code is within the server error range (500-599).
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) IsServerErrorResponse() bool {
return r.statusCode >= 500 && r.statusCode <= 599
}

// HasError checks if the response has an error based on the presence of err or errorBody.
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) HasError() bool {
return r.err != nil || r.errorBody != nil
}

// HasSuccess checks if the response has no errors and no error body.
//
// No parameters.
// Returns a boolean.
func (r *responseData[T, E]) HasSuccess() bool {
return r.err == nil && r.errorBody == nil
}
6 changes: 6 additions & 0 deletions pkg/web/restclient/response_error_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package restclient

// ResponseErrorData interface to encapsulate response error data
type ResponseErrorData interface {
any
}
6 changes: 6 additions & 0 deletions pkg/web/restclient/response_success_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package restclient

// ResponseSuccessData interface to encapsulate response success data
type ResponseSuccessData interface {
any
}
4 changes: 2 additions & 2 deletions pkg/web/restserver/route.go
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ type healtCheck struct {
}

func addHealthCheckRoute() {
const route = "/management/health"
const route = "/health"
srvRoutes = append(srvRoutes, Route{
URI: route,
Method: http.MethodGet,
@@ -47,7 +47,7 @@ func addHealthCheckRoute() {

func addDocumentationRoute() {
if slices.Contains([]string{config.ENVIRONMENT_SANDBOX, config.ENVIRONMENT_DEVELOPMENT}, config.ENVIRONMENT) {
const route = "/v2/api-docs"
const route = "/api-docs"
srvRoutes = append(srvRoutes, Route{
URI: route,
Method: http.MethodGet,
Loading

0 comments on commit 54f6351

Please sign in to comment.