Skip to content

Commit

Permalink
feat: add API endpoints
Browse files Browse the repository at this point in the history
Signed-off-by: Ales Verbic <verbotenj@gmail.com>
  • Loading branch information
verbotenj committed Oct 12, 2023
1 parent 13b7858 commit 9e97163
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 0 deletions.
154 changes: 154 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package api

import (
"encoding/json"
"log"
"sync"
"time"

"github.com/gin-gonic/gin"
)

type API interface {
Start() error
AddRoute(method, path string, handler gin.HandlerFunc)
}

type APIv1 struct {
engine *gin.Engine
apiGroup *gin.RouterGroup
host string
port string
}

type APIRouteRegistrar interface {
RegisterRoutes()
}

type APIOption func(*APIv1)

func WithGroup(group string) APIOption {
// Expects '/v1' as the group
return func(a *APIv1) {
a.apiGroup = a.engine.Group(group)
}
}

func WithHost(host string) APIOption {
return func(a *APIv1) {
a.host = host
}
}

func WithPort(port string) APIOption {
return func(a *APIv1) {
a.port = port
}
}

var apiInstance *APIv1
var once sync.Once

func NewAPI(debug bool, options ...APIOption) *APIv1 {
once.Do(func() {
apiInstance = &APIv1{
engine: ConfigureRouter(debug),
host: "localhost",
port: "8080",
}
for _, opt := range options {
opt(apiInstance)
}
})
return apiInstance
}

func GetInstance() *APIv1 {
return apiInstance
}

func (a *APIv1) Engine() *gin.Engine {
return a.engine
}

func (a *APIv1) Start() error {
address := a.host + ":" + a.port
go a.engine.Run(address)

Check failure on line 76 in api/api.go

View workflow job for this annotation

GitHub Actions / lint (1.21.x, ubuntu-latest)

Error return value of `a.engine.Run` is not checked (errcheck)
return nil
}

func (a *APIv1) AddRoute(method, path string, handler gin.HandlerFunc) {
// Inner function to add routes to a given target
//(either gin.Engine or gin.RouterGroup)
addRouteToTarget := func(target gin.IRoutes) {
switch method {
case "GET":
target.GET(path, handler)
case "POST":
target.POST(path, handler)
case "PUT":
target.PUT(path, handler)
case "DELETE":
target.DELETE(path, handler)
case "PATCH":
target.PATCH(path, handler)
case "HEAD":
target.HEAD(path, handler)
case "OPTIONS":
target.OPTIONS(path, handler)
default:
log.Printf("Unsupported HTTP method: %s", method)
}
}

// Check if a specific apiGroup is set
// If so, add the route to it. Otherwise, add to the main engine.
if a.apiGroup != nil {
addRouteToTarget(a.apiGroup)
} else {
addRouteToTarget(a.engine)
}
}

func ConfigureRouter(debug bool) *gin.Engine {
if !debug {
gin.SetMode(gin.ReleaseMode)
}
gin.DisableConsoleColor()
g := gin.New()
g.Use(gin.Recovery())
// Custom access logging
g.Use(gin.LoggerWithFormatter(accessLogger))
// Healthcheck endpoint
g.GET("/healthcheck", handleHealthcheck)
// No-op API endpoint for testing
g.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
// Swagger UI
// TODO: add swagger UI
// g.Static("/swagger-ui", "swagger-ui")
return g
}

func accessLogger(param gin.LogFormatterParams) string {
logEntry := gin.H{
"type": "access",
"client_ip": param.ClientIP,
"timestamp": param.TimeStamp.Format(time.RFC1123),
"method": param.Method,
"path": param.Path,
"proto": param.Request.Proto,
"status_code": param.StatusCode,
"latency": param.Latency,
"user_agent": param.Request.UserAgent(),
"error_message": param.ErrorMessage,
}
ret, _ := json.Marshal(logEntry)
return string(ret) + "\n"
}

func handleHealthcheck(c *gin.Context) {
// TODO: add some actual health checking here
c.JSON(200, gin.H{"failed": false})
}
42 changes: 42 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package api_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/blinklabs-io/snek/api"
"github.com/blinklabs-io/snek/output/push"
"github.com/stretchr/testify/assert"
)

func TestRouteRegistration(t *testing.T) {
// Initialize the API and set it to debug mode for testing
apiInstance := api.NewAPI(true)

// Check if Fcm implements APIRouteRegistrar and register its routes
// TODO: update this with actual plugin
fcmPlugin := &push.Fcm{}
if registrar, ok := interface{}(fcmPlugin).(api.APIRouteRegistrar); ok {
registrar.RegisterRoutes()
} else {
t.Fatal("push.Fcm does NOT implement APIRouteRegistrar")
}

// Create a test request to one of the registered routes
req, err := http.NewRequest(http.MethodGet, "/v1/fcm/someToken", nil)
if err != nil {
t.Fatal(err)
}

// Record the response
rr := httptest.NewRecorder()
apiInstance.Engine().ServeHTTP(rr, req)

// Check the status code
assert.Equal(t, http.StatusNotFound, rr.Code, "Expected status not found")

// You can also check the response body, headers, etc.
// TODO check for JSON response
// assert.Equal(t, `{"fcmToken":"someToken"}`, rr.Body.String())
}
9 changes: 9 additions & 0 deletions cmd/snek/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"net/http"
"os"

"github.com/blinklabs-io/snek/api"
_ "github.com/blinklabs-io/snek/filter"
_ "github.com/blinklabs-io/snek/input"
"github.com/blinklabs-io/snek/internal/config"
Expand Down Expand Up @@ -103,6 +104,11 @@ func main() {
}()
}

// Create API instance with debug disabled
apiInstance := api.NewAPI(false,
api.WithGroup("/v1"),
api.WithPort("8080"))

// Create pipeline
pipe := pipeline.New()

Expand All @@ -126,6 +132,9 @@ func main() {
}
pipe.AddOutput(output)

// Start API after plugins are configured
apiInstance.Start()

Check failure on line 136 in cmd/snek/main.go

View workflow job for this annotation

GitHub Actions / lint (1.21.x, ubuntu-latest)

Error return value of `apiInstance.Start` is not checked (errcheck)

// Start pipeline and wait for error
if err := pipe.Start(); err != nil {
logger.Fatalf("failed to start pipeline: %s\n", err)
Expand Down
30 changes: 30 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/blinklabs-io/gouroboros v0.54.0
github.com/gen2brain/beeep v0.0.0-20230602101333-f384c29b62dd
github.com/gin-gonic/gin v1.9.1
github.com/kelseyhightower/envconfig v1.4.0
go.uber.org/zap v1.26.0
gopkg.in/yaml.v2 v2.4.0
Expand All @@ -14,14 +15,43 @@ require (
// replace github.com/blinklabs-io/gouroboros v0.52.0 => ../gouroboros

require (
github.com/bytedance/sonic v1.10.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.5.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 9e97163

Please sign in to comment.