Skip to content

Commit

Permalink
feat(v1): add cache control (#10)
Browse files Browse the repository at this point in the history
* feat(v1): update responses to use cache control headers

* feat(v1): add required params error

* feat(v1): add better comments

* chore: squash
  • Loading branch information
kieranroneill authored Jun 12, 2024
1 parent b98f4bc commit 0ae00e7
Show file tree
Hide file tree
Showing 22 changed files with 189 additions and 53 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@

### 1.1. Project structure

The project structure is based on the [DigitalOcean functions][https://docs.digitalocean.com/products/functions/how-to/structure-projects/] project structure.
The project structure is based on the DigitalOcean [functions](https://docs.digitalocean.com/products/functions/how-to/structure-projects) project structure.
However, the core directories `lib` and `packages` have specific functionality:

* `packages` - This is where each function resides. Each package/function relates to the API path. For example, `achievements/quest` will correspond to an API path `https://<endpoint>/achievements/quests`
* `lib` - This contains is independent modules that are referenced from the packages.
* `packages` - This is where each function resides. Each package/function relates to a versioned API path. For example, `v1/quests` will correspond to an API path `https://<endpoint>/v1/quests`
* `lib` - This contains independent modules that are referenced from the packages.

> ⚠️ **NOTE:** Each path must parse the request header and handle the method, i.e. GET, POST, DELETE e.t.c.
Expand Down Expand Up @@ -91,15 +91,15 @@ To start using your own Doppler config, go to the project on [Doppler](https://d

Once the branch project config has been setup, follow the instructions [here](https://docs.doppler.com/docs/install-cli#project-setup) to:
* login to Doppler, and;
* setup Doppler to use the kibisis-api project with your personal config.
* setup Doppler to use the `kibisis-api` project with your personal config.

> ⚠️ **NOTE:** When naming your token, it is recommended you use: "<name>-<device_name>".
> ⚠️ **NOTE:** When naming your token, it is recommended you use: "<your_name>-<device_name>".
<sup>[Back to top ^][table-of-contents]</sup>

### 2.4. Setup `doctl`

The DigitalOcean CLI client, `doctl`, is used to deploy the function to a remote environment tha can be used to develop.
The DigitalOcean CLI client, `doctl`, is used to deploy the function to a remote environment that can be used to develop.

Follow the instructions outlined in the [documentation][doctl].

Expand Down
4 changes: 4 additions & 0 deletions lib/constants/durations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package constants

const HourInSeconds = 3600
const TwentyFourHoursInSeconds = 604800
4 changes: 3 additions & 1 deletion lib/constants/errorcode.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package constants

const (
// general
UnknownError int = 1000
// validation errors
InvalidAddressError int = 2000
RequiredParamsError int = 2000
InvalidAddressError int = 2001
// integration errors
PostHogError int = 4000
)
8 changes: 5 additions & 3 deletions lib/errors/invalidaddresserror.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ import (
)

type InvalidAddressError struct {
Address string
Code int
Message string
Address string `json:"address"`
Code int `json:"code"`
Message string `json:"message"`
Name string `json:"name"`
}

func NewInvalidAddressError(address string) *InvalidAddressError {
return &InvalidAddressError{
Address: address,
Code: constants.InvalidAddressError,
Message: fmt.Sprintf("address \"%s\" is not a valid address", address),
Name: "InvalidAddressError",
}
}
8 changes: 5 additions & 3 deletions lib/errors/posthogerror.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package errors
import "lib/constants"

type PostHogError struct {
Code int
Error error
Message string
Code int `json:"code"`
Error error `json:"error"`
Message string `json:"message"`
Name string `json:"name"`
}

func NewPostHogError(message string, error error) *PostHogError {
return &PostHogError{
Code: constants.PostHogError,
Error: error,
Message: message,
Name: "PostHogError",
}
}
22 changes: 22 additions & 0 deletions lib/errors/requiredparamserror.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package errors

import (
"fmt"
"lib/constants"
)

type RequiredParamsError struct {
Code int `json:"code"`
Message string `json:"message"`
Name string `json:"name"`
Params []string `json:"params"`
}

func NewRequiredParamsError(params []string) *RequiredParamsError {
return &RequiredParamsError{
Code: constants.RequiredParamsError,
Message: fmt.Sprintf("the params \"%s\" are required", params),
Name: "RequiredParamsError",
Params: params,
}
}
8 changes: 5 additions & 3 deletions lib/errors/unknownerror.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@ package errors
import "lib/constants"

type UnknownError struct {
Code int
Error error
Message string
Code int `json:"code"`
Error error `json:"error"`
Message string `json:"message"`
Name string `json:"name"`
}

func NewUnknownError(message string, error error) *UnknownError {
return &UnknownError{
Code: constants.UnknownError,
Error: error,
Message: message,
Name: "UnknownError",
}
}
2 changes: 1 addition & 1 deletion packages/v1/quests/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module quests

go 1.21
go 1.20

require (
github.com/algorand/go-algorand-sdk v1.24.0
Expand Down
8 changes: 6 additions & 2 deletions packages/v1/quests/internal/types/dailyquest.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package types

// DailyQuest
// @Description The ID of the quest and the amount of times it has been completed.
type DailyQuest struct {
Completed int `json:"completed"`
Id string `json:"id"`
// The amount of times the quest has been completed
Completed int `json:"completed" example:"22"`
// The ID of the quest
Id string `json:"id" example:"send-native-currency-action"`
}
5 changes: 3 additions & 2 deletions packages/v1/quests/internal/types/response.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

type Response struct {
Body ResponseBody `json:"body,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
Body ResponseBody `json:"body,omitempty"`
Headers ResponseHeaders `json:"headers,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
}
8 changes: 5 additions & 3 deletions packages/v1/quests/internal/types/responsebody.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package types

type ResponseBody struct {
Account string `json:"account,omitempty"`
Error interface{} `json:"error,omitempty"`
Quests []DailyQuest `json:"quests,omitempty"`
// The account address
Account string `json:"account,omitempty" example:"TESTK4BURRDGVVHAX2FBY7CPRC2RTTVRRN4C2TVDCHRCXNTFGL3TVSDROE"`
Error interface{} `json:"error,omitempty" swaggertype:"object"`
// The completed quests
Quests []DailyQuest `json:"quests,omitempty"`
}
6 changes: 6 additions & 0 deletions packages/v1/quests/internal/types/responseheaders.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package types

type ResponseHeaders struct {
CacheControl string `json:"Cache-Control"`
ContentType string `json:"Content-Type"`
}
32 changes: 28 additions & 4 deletions packages/v1/quests/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
algosdktypes "github.com/algorand/go-algorand-sdk/types"
"golang.org/x/exp/slices"
"lib/constants"
"lib/errors"
"lib/utils"
"net/http"
Expand All @@ -17,30 +18,50 @@ import (
// @Produce json
// @Param account query string true "account to get daily quests" Example(TESTK4BURRDGVVHAX2FBY7CPRC2RTTVRRN4C2TVDCHRCXNTFGL3TVSDROE)
// @Success 200 {object} _types.ResponseBody
// @failure 400 {object} _types.ResponseBody "if the account param is not provided or invalid"
// @failure 405 {object} _types.ResponseBody "will return if it is not a GET request"
// @failure 500 {object} _types.ResponseBody
// @Failure 400 "If the account param is not provided"
// @Failure 400 "If the account param is an invalid AVM address"
// @Failure 405 "If it is not a GET request"
// @Failure 500
// @Header all {string} Cache-Control "public, max-age=3600"
// @Router /v1/quests [get]
func Main(request _types.Request) *_types.Response {
var dailyQuests []_types.DailyQuest

logger := utils.NewLogger()
headers := _types.ResponseHeaders{
CacheControl: fmt.Sprintf("public, max-age=%d", constants.HourInSeconds),
ContentType: "application/json",
}

// only accept get requests
if request.Http.Method != http.MethodGet {
return &_types.Response{
Headers: headers,
StatusCode: http.StatusMethodNotAllowed,
}
}

logger.Debug(fmt.Sprintf("validating account \"%s\"", request.Account))
logger.Debug("validating params")

if request.Account == "" {
return &_types.Response{
Body: _types.ResponseBody{
Error: errors.NewRequiredParamsError([]string{"account"}),
},
Headers: headers,
StatusCode: http.StatusBadRequest,
}
}

logger.Debug(fmt.Sprintf("validating account \"%s\" param", request.Account))

_, err := algosdktypes.DecodeAddress(request.Account)
if err != nil {
return &_types.Response{
Body: _types.ResponseBody{
Error: errors.NewInvalidAddressError(request.Account),
},
Headers: headers,
StatusCode: http.StatusBadRequest,
}
}
Expand All @@ -53,6 +74,7 @@ func Main(request _types.Request) *_types.Response {
Body: _types.ResponseBody{
Error: errors.NewPostHogError("failed to fetch event references from posthog", err),
},
Headers: headers,
StatusCode: http.StatusInternalServerError,
}
}
Expand All @@ -67,6 +89,7 @@ func Main(request _types.Request) *_types.Response {
Body: _types.ResponseBody{
Error: errors.NewPostHogError("failed to fetch daily events from posthog", err),
},
Headers: headers,
StatusCode: http.StatusInternalServerError,
}
}
Expand Down Expand Up @@ -96,6 +119,7 @@ func Main(request _types.Request) *_types.Response {
Account: request.Account,
Quests: dailyQuests,
},
Headers: headers,
StatusCode: http.StatusOK,
}
}
4 changes: 4 additions & 0 deletions packages/v1/swagger/go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
module docs

go 1.20

require lib v0.0.0

replace lib v0.0.0 => ../../../lib
3 changes: 2 additions & 1 deletion packages/v1/swagger/internal/types/responseheaders.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package types

type ResponseHeaders struct {
ContentType string `json:"Content-Type"`
CacheControl string `json:"Cache-Control"`
ContentType string `json:"Content-Type"`
}
5 changes: 4 additions & 1 deletion packages/v1/swagger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package main
import (
_types "docs/internal/types"
_ "embed"
"fmt"
"lib/constants"
"net/http"
)

Expand All @@ -22,7 +24,8 @@ func Main() *_types.Response {
return &_types.Response{
Body: swaggerJSON,
Headers: _types.ResponseHeaders{
ContentType: "application/json",
CacheControl: fmt.Sprintf("public, max-age=%d", constants.TwentyFourHoursInSeconds),
ContentType: "application/json",
},
StatusCode: http.StatusOK,
}
Expand Down
Loading

0 comments on commit 0ae00e7

Please sign in to comment.