tagger/main.go

353 lines
8.9 KiB
Go
Raw Permalink Normal View History

2024-04-04 20:37:54 +00:00
// Copyright 2024 Matthew Rich <matthewrich.conf@gmail.com>. All rights reserved.
//
2024-04-04 20:30:41 +00:00
package main
import (
"io"
"context"
_ "fmt"
_ "log"
"strings"
"net/http"
_ "encoding/json"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
"github.com/redis/go-redis/v9"
"tagger/internal/data"
"tagger/internal/models"
"tagger/internal/service"
docs "tagger/docs"
swaggerfiles "github.com/swaggo/files"
ginswagger "github.com/swaggo/gin-swagger"
"encoding/base64"
)
// @title Tagger API
// @version 0.31
// @description This is the Tagger API.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email matthewrich.conf@gmail.com
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
type App struct {
apiBasePath string
service *service.Service
router *gin.Engine
connector service.DataConnector
}
type tag struct {
Id string `json:"id",omitempty`
Name string `json:"name",omitempty`
Resources []string `json:"resources"`
}
type query struct {
Terms string `form:"terms[]"`
}
// @BasePath /api/v1
// Ping godoc
// @Summary ping endpoint
// @Schemes
// @Description ping alive endpoint
// @Tags ping
// @Accept json
// @Produce json
// @Success 200 {string} pong
// @Router /ping [get]
func (a *App) Ping(c *gin.Context) {
c.String(200, "pong")
}
func SSEHeadersMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("Content-Type") == "text/event-stream" {
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Transfer-Encoding", "chunked")
}
c.Next()
}
}
// @BasePath /api/v1
// Tag godoc
// @Summary Update a tag
// @Schemes
// @Description Updates the resources associated with a tag
// @Tags tag resource
// @Accept json
// @Produce json
// @Success 200 {object} tag
// @Param name path string true "Tag name"
// @Param tag body string true "Tag JSON object"
// @Router /tags/{name} [put]
func (a *App) updateTag(c *gin.Context) {
var t tag
tagName := c.Param("tag")
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for _,resource := range(t.Resources) {
a.service.AddTag(c, tagName, resource)
}
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
// @BasePath /api/v1
// Tag godoc
// @Summary Add a tag for a resource
// @Schemes
// @Description Add a tag for a resource
// @Tags tag resource
// @Accept json
// @Produce json
// @Success 200 {object} tag
// @Param tag body string true "Tag JSON object"
// @Router /tags [post]
func (a *App) createTag(c *gin.Context) {
var t tag
if err := c.ShouldBindJSON(&t); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for _,resource := range(t.Resources) {
a.service.AddTag(c, t.Name, resource)
}
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
// @BasePath /api/v1
// Tags godoc
// @Summary Get list of resources tagged
// @Schemes
// @Description Get resources associated with a Tag
// @Tags Tags
// @Accept json
// @Produce json
// @Success 200 {array} string
// @Param name path string true "Tag name"
// @Router /tags/{name} [get]
func (a *App) getTag(c *gin.Context) {
var tag models.Tag
tag.Name = c.Param("tag")
tag.Resources = a.service.Query(c, []string { tag.Name })
c.JSON(http.StatusOK, tag)
}
// @BasePath /api/v1
// Tag godoc
// @Summary Add a resource to a tag
// @Schemes
// @Description Add a resource to a tag
// @Tags tag resource
// @Accept json
// @Produce json
// @Success 200 {object} tag
// @Param tag path string true "Tag name"
// @Param resource body string true "Resource JSON object"
// @Router /tags/{tag} [post]
func (a *App) createResource(c *gin.Context) {
var body map[string]string
tagName := c.Param("tag")
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
a.service.AddTag(c, tagName, body["resource"])
c.JSON(http.StatusOK, gin.H{"status": "success"})
}
func (a *App) streamTags(c *gin.Context) {
c.Stream(func(w io.Writer) bool {
tags := a.service.Tags(c)
for _,v := range(tags) {
//jsonData, _ := json.Marshal(message)
c.SSEvent("message", v)
}
return false
})
<-c.Writer.CloseNotify()
}
func (a *App) streamResourceTags(c *gin.Context, resourceUrl string) {
c.Stream(func(w io.Writer) bool {
tags := a.service.ResourceTags(c, resourceUrl)
for _,v := range(tags) {
//jsonData, _ := json.Marshal(message)
c.SSEvent("message", v)
}
return false
})
<-c.Writer.CloseNotify()
}
func (a *App) getTags(c *gin.Context) {
if c.GetHeader("Content-Type") == "text/event-stream" {
a.streamTags(c)
} else {
c.JSON(http.StatusOK, a.service.Tags(c))
}
return
}
// @BasePath /api/v1
// Tag godoc
// @Summary Get resource tags
// @Schemes
// @Description Get resource to tags
// @Tags tag resource
// @Accept json
// @Produce json
// @Success 200 {object} models.Resource
// @Param resource path string true "Base64 encoded Resource URL"
// @Router /resources/{resource} [get]
func (a *App) getResourceTags(c *gin.Context) {
var result models.Resource
resourceUrl, e := base64.StdEncoding.DecodeString(c.Param("resource"))
if e != nil {
c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
}
result.Resource = string(resourceUrl)
if c.GetHeader("Content-Type") == "text/event-stream" {
a.streamResourceTags(c, result.Resource)
} else {
result.Tags = a.service.ResourceTags(c, result.Resource)
c.JSON(http.StatusOK, result)
}
return
}
// @BasePath /api/v1
// Tag godoc
// @Summary Get resource tags
// @Schemes
// @Description Get resource to tags
// @Tags tag resource
// @Accept json
// @Produce json
// @Success 200 {object} models.Resource
// @Param resource query string true "Resource URL"
// @Router /resources [get]
func (a *App) getResourceTagsByQuery(c *gin.Context) {
var result models.Resource
result.Resource = c.DefaultQuery("resource", "")
if result.Resource == "" {
// return list of unfiltered Resource models
} else {
if c.GetHeader("Content-Type") == "text/event-stream" {
a.streamResourceTags(c, result.Resource)
} else {
result.Tags = a.service.ResourceTags(c, result.Resource)
c.JSON(http.StatusOK, result)
}
}
return
}
func (a *App) BasePath() string {
return a.apiBasePath
}
func (a *App) Run() {
a.router.Run(":8080")
}
func (a *App) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
a.router.ServeHTTP(rw,req)
}
func (a *App) queryFormHandler(c *gin.Context) {
var form query
c.Bind(&form)
terms := strings.Fields(form.Terms)
results := a.service.Query(c, terms)
c.HTML(http.StatusOK, "query.tmpl", gin.H{ "resources": results, "terms": terms })
}
func NewApp(connector service.DataConnector) *App {
if connector == nil {
connector = data.NewRedisConnector(redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
}))
}
a := &App {
apiBasePath: "/api/v1",
service: service.NewService(),
router: gin.Default(),
connector: connector,
}
/*
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
*/
docs.SwaggerInfo.BasePath = a.apiBasePath
a.service.UseDataConnector(context.Background(), a.connector)
a.router.Use(cors.Default())
a.router.GET("/swagger/*any", ginswagger.WrapHandler(swaggerfiles.Handler))
a.router.StaticFile("/favicon.ico", "./static/icons/tagger16-mb.ico")
a.router.Static("/icons", "./static/icons/")
a.router.LoadHTMLGlob("templates/*")
api := a.router.Group(a.apiBasePath)
api.GET("/ping", a.Ping)
api.GET("/resources/:resource", SSEHeadersMiddleware(), a.getResourceTags)
api.GET("/resources", SSEHeadersMiddleware(), a.getResourceTagsByQuery)
api.GET("/tags", SSEHeadersMiddleware(), a.getTags)
//r.GET("/tags", SSEHeadersMiddleware(), getTags)
api.GET("/tags/:tag", a.getTag)
api.POST("/tags", a.createTag)
api.POST("/tags/:tag", a.createResource)
api.PUT("/tags/:tag", a.updateTag)
a.router.GET("/tags", func(c *gin.Context) {
c.HTML(http.StatusOK, "resources.tmpl", gin.H{"title": "Resources", "tags": a.service.Tags(c) })
})
a.router.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{ "title": "Resources" })
})
a.router.POST("/", a.queryFormHandler)
return a
}
func main() {
app := NewApp(nil)
app.Run()
}