// Copyright 2024 Matthew Rich . All rights reserved. // 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() }