add gin service
21
.gitea/workflows/lint.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
name: Lint
|
||||
on:
|
||||
- push
|
||||
jobs:
|
||||
lint:
|
||||
name: golangci-lint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21.1"
|
||||
cache: false
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.55
|
18
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "tagger"
|
54
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
name: Declarative Tests
|
||||
run-name: ${{ gitea.actor }} testing
|
||||
on:
|
||||
- push
|
||||
- workflow_dispatch
|
||||
- issue_comment
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: registry.cv.mazarbul.net/declarative/build-golang:1.21.1-alpine-x86_64
|
||||
env:
|
||||
GOPATH: /
|
||||
SSH_AUTH_SOCK: /tmp/ssh.sock
|
||||
ENVIRONMENT: dev
|
||||
volumes:
|
||||
- ${{ env.HOME }}/.ssh/known_hosts:/root/.ssh/known_hosts
|
||||
- ${{ env.SSH_AUTH_SOCK }}:/tmp/ssh.sock
|
||||
- /etc/gitconfig:/etc/gitconfig
|
||||
- /etc/ssl/certs:/etc/ssl/certs
|
||||
- ${{ vars.GITEA_WORKSPACE }}:/go/src
|
||||
options: --cpus 1
|
||||
steps:
|
||||
- run: apk add --no-cache nodejs
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
- run: echo "The workflow is now ready to test your code on the runner."
|
||||
- name: Run tests
|
||||
run: |
|
||||
go test -coverprofile=artifacts/coverage.profile ./...
|
||||
- name: Run build
|
||||
run: |
|
||||
make build
|
||||
- name: cli tests
|
||||
run: |
|
||||
go test
|
||||
- run: echo "This job's status is ${{ job.status }}."
|
||||
- run: echo "This job's status is ${{ job.status }}."
|
||||
- name: coverage report
|
||||
run: |
|
||||
go tool cover -html=artifacts/coverage.profile -o artifacts/code-coverage.html
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-coverage-report
|
||||
path: artifacts/code-coverage.html
|
||||
- name: Archive binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: tagger
|
||||
path: tagger
|
||||
- run: echo "This job's status is ${{ job.status }}."
|
11
Makefile
Normal file
@ -0,0 +1,11 @@
|
||||
LDFLAGS?=--ldflags '-extldflags "-static"'
|
||||
export CGO_ENABLED=0
|
||||
|
||||
build: tagger
|
||||
|
||||
tagger:
|
||||
go build -o tagger $(LDFLAGS)
|
||||
tagger-cli:
|
||||
go build -o tagger-cli $(LDFLAGS) cmd/cli/main.go
|
||||
test:
|
||||
go test ./...
|
@ -1,3 +1,4 @@
|
||||
# tagger
|
||||
Tagging service
|
||||
|
||||
REST API for creating resource tags
|
||||
Provides a REST API and swagger docs for tagging resources.
|
||||
|
5
build_deps.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- make
|
||||
- libc-dev
|
||||
- git
|
||||
- openssh-client
|
||||
- gcc
|
169
cmd/cli/main.go
Normal file
@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
_ "io/ioutil"
|
||||
"fmt"
|
||||
"strings"
|
||||
"regexp"
|
||||
"tagger/internal/models"
|
||||
"tagger/internal/client"
|
||||
"flag"
|
||||
"net/http"
|
||||
"golang.org/x/net/html"
|
||||
"container/list"
|
||||
_ "unicode"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type htmlNode html.Node
|
||||
|
||||
func metaKeywords(n *htmlNode) []string {
|
||||
var name, content string
|
||||
re := regexp.MustCompile(`[^-,\w\s]+`)
|
||||
fmt.Printf("meta ")
|
||||
for _, attribute := range n.Attr {
|
||||
fmt.Printf(" %s=%s", attribute.Key, attribute.Val)
|
||||
if attribute.Key == "name" {
|
||||
if attribute.Val != "keywords" && attribute.Val != "title" && attribute.Val != "description" {
|
||||
fmt.Printf("\n")
|
||||
return []string{}
|
||||
} else {
|
||||
name = attribute.Val
|
||||
}
|
||||
}
|
||||
if attribute.Key == "content" {
|
||||
content,_ = url.PathUnescape(attribute.Val)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
if name != "" {
|
||||
var terms []string
|
||||
if name == "keywords" {
|
||||
terms = strings.Split(re.ReplaceAllString(content, ""), ",")
|
||||
} else {
|
||||
terms = []string{strings.ReplaceAll(re.ReplaceAllString(content, ""), ",", "")}
|
||||
}
|
||||
for i,t := range(terms) {
|
||||
terms[i] = strings.ToLower(strings.ReplaceAll(strings.TrimSpace(t), " ", "-"))
|
||||
}
|
||||
return terms
|
||||
//return strings.FieldsFunc(content, func(r rune) bool { return ! ( unicode.IsLetter(r) || unicode.IsNumber(r) || r == '-' ) })
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
|
||||
func (h *htmlNode) FindAll(tagName string) []*htmlNode {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return h.FindNodes(tagName, false)
|
||||
}
|
||||
|
||||
func (h *htmlNode) Find(tagName string) *htmlNode {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
results := h.FindNodes(tagName, true)
|
||||
if len(results) > 0 {
|
||||
return results[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *htmlNode) FindNodes(tagName string, first bool) []*htmlNode {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
n := (*html.Node)(h)
|
||||
q := list.New()
|
||||
|
||||
var results []*htmlNode
|
||||
|
||||
q.PushBack(n)
|
||||
|
||||
for q.Len() > 0 {
|
||||
v := (*html.Node)(q.Remove(q.Front()).(*html.Node))
|
||||
if v.Type == html.ElementNode && v.Data == tagName {
|
||||
results = append(results, (*htmlNode)(v))
|
||||
if first {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for c := v.FirstChild; c != nil; c = c.NextSibling {
|
||||
q.PushBack(c)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func extractTagsMetaDataFromUrl(resource io.ReadCloser) ([]string, error) {
|
||||
var results []string
|
||||
doc,e := html.Parse(resource)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
for _,v := range (*htmlNode)(doc).Find("head").FindAll("meta") {
|
||||
results = append(results, metaKeywords(v)...)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
tag := flag.String("tag", "tag-name", "Tag name")
|
||||
//resource := flag.String("resource", "resource URL", "Resource URL")
|
||||
|
||||
endpoint := flag.String("endpoint", "http://localhost:8080/api/v1", "API endpoint URL")
|
||||
|
||||
extractUrl := flag.String("extract", "", "Extract tags from resource URL")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
fmt.Printf("%#v\n", extractUrl)
|
||||
|
||||
|
||||
cli := client.New(*endpoint)
|
||||
|
||||
if e := cli.Ping(); e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
if len(*extractUrl) > 0 {
|
||||
s,e := http.Get(*extractUrl)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
terms, e := extractTagsMetaDataFromUrl(s.Body)
|
||||
// filter terms
|
||||
|
||||
fmt.Printf("%s\n", terms)
|
||||
|
||||
for _,t := range(terms) {
|
||||
cli.AddTagResource(t, *extractUrl)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
requestUrl := fmt.Sprintf("%s/tags/%s", *endpoint, *tag)
|
||||
r,e := http.Get(requestUrl)
|
||||
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
defer r.Body.Close()
|
||||
|
||||
tagModel, e := models.NewTagFromJson(r.Body)
|
||||
|
||||
if e != nil {
|
||||
//http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
fmt.Printf("%#v\n", tagModel)
|
||||
}
|
72
cmd/cli/main_test.go
Normal file
@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
/*
|
||||
<meta name="title" content="Yahoo Finance - Stock Market Live, Quotes, Business & Finance News">
|
||||
<meta name="description" content="At Yahoo Finance, you get free stock quotes, up-to-date news, portfolio management resources, international market data, social interaction and mortgage rates that help you manage your financial life.">
|
||||
<meta name="keywords" content="401k, Business, Financial Information, Investing, Investor, Market News, Stock Research, Stock Valuation, business news, economy, finance, investment tools, mortgage, mutual funds, personal finance, quote, real estate, retirement, stock, stocks, Suze Orman, tax, track portfolio">
|
||||
<meta http-equiv="x-dns-prefetch-control" content="on">
|
||||
<meta name="theme-color" content="#037B66">
|
||||
<meta name="oath:guce:consent-host" content="guce.yahoo.com">
|
||||
<meta property="twitter:dnt" content="on">
|
||||
<meta name="twitter:site" content="@YahooFinance">
|
||||
<meta property="fb:app_id" content="458584288257241">
|
||||
<meta property="fb:pages" content="458584288257241">
|
||||
<meta property="og:image" content="https://s.yimg.com/cv/apiv2/social/images/yahoo_default_logo.png">
|
||||
<meta property="og:description" content="At Yahoo Finance, you get free stock quotes, up-to-date news, portfolio management resources, international market data, social interaction and mortgage rates that help you manage your financial life.">
|
||||
<meta property="og:title" content="Yahoo Finance - Stock Market Live, Quotes, Business & Finance News">
|
||||
<meta property="twitter:description" content="At Yahoo Finance, you get free stock quotes, up-to-date news, portfolio management resources, international market data, social interaction and mortgage rates that help you manage your financial life.">
|
||||
<meta property="twitter:title" content="Yahoo Finance - Stock Market Live, Quotes, Business & Finance News">
|
||||
<meta property="al:ios:app_store_id" content="328412701">
|
||||
<meta property="al:ios:app_name" content="Yahoo Finance">
|
||||
<meta property="al:android:url" content="intent://#Intent;scheme=yfinance;action=android.intent.action.VIEW;package=com.yahoo.mobile.client.android.finance;S.browser_fallback_url=https%3A%2F%2Fplay.google.com%2Fstore%2Fapps%2Fdetails%3Fid%3Dcom.yahoo.mobile.client.android.finance;end">
|
||||
<meta property="al:android:app_name" content="Yahoo Finance">
|
||||
<meta property="al:android:package" content="com.yahoo.mobile.client.android.finance">
|
||||
<meta property="apple-itunes-app" content="app-id=328412701, app-clip-bundle-id=com.yahoo.finance.clip-qsp, affiliate-data=ct=us.fin.mbl.smart-banner&pt=9029, app-argument=https://yfinance.onelink.me/3068494570?pid=AppPromo&c=US_Acquisition_YMktg_337__&af_sub1=Acquisition&af_sub2=US_YMktg&af_sub3=&af_sub4=100000591&af_sub5=SmartBanner__SmartBanner_">
|
||||
|
||||
|
||||
*/
|
||||
func TestExtractUrl(t *testing.T) {
|
||||
htmlData := `
|
||||
<html><head>
|
||||
<meta name="title" content="Yahoo Finance - Stock Market Live, Quotes, Business & Finance News">
|
||||
<meta name="description" content="At Yahoo Finance, you get free stock quotes, up-to-date news, portfolio management resources, international market data, social interaction and mortgage rates that help you manage your financial life.">
|
||||
<meta name="keywords" content="401k, Business, Financial Information, Investing, Investor, Market News, Stock Research, Stock Valuation, business news, economy, finance, investment tools, mortgage, mutual funds, personal finance, quote, real estate, retirement, stock, stocks, Suze Orman, tax, track portfolio">
|
||||
<meta name="theme-color" content="#037B66">
|
||||
</head><body></body></html>`
|
||||
|
||||
reader := io.NopCloser(strings.NewReader(htmlData))
|
||||
|
||||
meta, _ := extractTagsMetaDataFromUrl(reader)
|
||||
assert.Greater(t, len(meta), 0)
|
||||
assert.Contains(t, meta, "international")
|
||||
assert.Contains(t, meta, "401k")
|
||||
}
|
||||
|
||||
func TestHtmlFile(t *testing.T) {
|
||||
htmlData := `
|
||||
<html><head>
|
||||
<meta name="title" content="Yahoo Finance - Stock Market Live, Quotes, Business & Finance News">
|
||||
<meta name="description" content="At Yahoo Finance, you get free stock quotes, up-to-date news, portfolio management resources, international market data, social interaction and mortgage rates that help you manage your financial life.">
|
||||
<meta name="keywords" content="401k, Business, Financial Information, Investing, Investor, Market News, Stock Research, Stock Valuation, business news, economy, finance, investment tools, mortgage, mutual funds, personal finance, quote, real estate, retirement, stock, stocks, Suze Orman, tax, track portfolio">
|
||||
<meta name="theme-color" content="#037B66">
|
||||
</head><body></body></html>`
|
||||
|
||||
reader := io.NopCloser(strings.NewReader(htmlData))
|
||||
doc,_ := html.Parse(reader)
|
||||
|
||||
v := (*htmlNode)(doc).Find("head").FindAll("meta")
|
||||
|
||||
for i := 0; i < len(v); i++ {
|
||||
fmt.Printf("%#v\n", v[i].Data)
|
||||
}
|
||||
assert.Equal(t, 4, len(v))
|
||||
}
|
9
data/data.go
Normal file
@ -0,0 +1,9 @@
|
||||
package data
|
||||
|
||||
type Reader interface {
|
||||
Read()
|
||||
}
|
||||
|
||||
type Connector interface {
|
||||
Connect() DataSource
|
||||
}
|
28
data/data_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDataSource(t *testing.T) {
|
||||
ds := NewSource()
|
||||
if ds == nil {
|
||||
t.Errorf("Failed creating new data source")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataSourceOpen(t *testing.T) {
|
||||
ds := NewSource()
|
||||
assert.Equal(t, ds.Open(), nil)
|
||||
}
|
||||
|
||||
type MockConnection struct {}
|
||||
|
||||
func (m *MockConnection) LRange(context.Context, string, int64, int64) (*redis.StringSliceCmd) { return }
|
||||
|
||||
func TestDataSourceGet(t *testing.T) {
|
||||
ds := NewSource()
|
||||
ds.Use(&MockConnection{})
|
||||
}
|
318
docs/docs.go
Normal file
@ -0,0 +1,318 @@
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
import "github.com/swaggo/swag"
|
||||
|
||||
const docTemplate = `{
|
||||
"schemes": {{ marshal .Schemes }},
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "{{escape .Description}}",
|
||||
"title": "{{.Title}}",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "matthewrich.conf@gmail.com"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "{{.Version}}"
|
||||
},
|
||||
"host": "{{.Host}}",
|
||||
"basePath": "{{.BasePath}}",
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"get": {
|
||||
"description": "ping alive endpoint",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"ping"
|
||||
],
|
||||
"summary": "ping endpoint",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/resources": {
|
||||
"get": {
|
||||
"description": "Get resource to tags",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Get resource tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Resource URL",
|
||||
"name": "resource",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Resource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/resources/{resource}": {
|
||||
"get": {
|
||||
"description": "Get resource to tags",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Get resource tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Base64 encoded Resource URL",
|
||||
"name": "resource",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Resource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags": {
|
||||
"post": {
|
||||
"description": "Add a tag for a resource",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Add a tag for a resource",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Tag JSON object",
|
||||
"name": "tag",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/{name}": {
|
||||
"get": {
|
||||
"description": "Get resources associated with a Tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Get list of resources tagged",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Updates the resources associated with a tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Update a tag",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Tag JSON object",
|
||||
"name": "tag",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/{tag}": {
|
||||
"post": {
|
||||
"description": "Add a resource to a tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Add a resource to a tag",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "tag",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Resource JSON object",
|
||||
"name": "resource",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"main.tag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Resource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
||||
var SwaggerInfo = &swag.Spec{
|
||||
Version: "0.31",
|
||||
Host: "localhost:8080",
|
||||
BasePath: "/api/v1",
|
||||
Schemes: []string{},
|
||||
Title: "Tagger API",
|
||||
Description: "This is the Tagger API.",
|
||||
InfoInstanceName: "swagger",
|
||||
SwaggerTemplate: docTemplate,
|
||||
LeftDelim: "{{",
|
||||
RightDelim: "}}",
|
||||
}
|
||||
|
||||
func init() {
|
||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
||||
}
|
294
docs/swagger.json
Normal file
@ -0,0 +1,294 @@
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is the Tagger API.",
|
||||
"title": "Tagger API",
|
||||
"termsOfService": "http://swagger.io/terms/",
|
||||
"contact": {
|
||||
"name": "API Support",
|
||||
"url": "http://www.swagger.io/support",
|
||||
"email": "matthewrich.conf@gmail.com"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
},
|
||||
"version": "0.31"
|
||||
},
|
||||
"host": "localhost:8080",
|
||||
"basePath": "/api/v1",
|
||||
"paths": {
|
||||
"/ping": {
|
||||
"get": {
|
||||
"description": "ping alive endpoint",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"ping"
|
||||
],
|
||||
"summary": "ping endpoint",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/resources": {
|
||||
"get": {
|
||||
"description": "Get resource to tags",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Get resource tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Resource URL",
|
||||
"name": "resource",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Resource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/resources/{resource}": {
|
||||
"get": {
|
||||
"description": "Get resource to tags",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Get resource tags",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Base64 encoded Resource URL",
|
||||
"name": "resource",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.Resource"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags": {
|
||||
"post": {
|
||||
"description": "Add a tag for a resource",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Add a tag for a resource",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Tag JSON object",
|
||||
"name": "tag",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/{name}": {
|
||||
"get": {
|
||||
"description": "Get resources associated with a Tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Get list of resources tagged",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"description": "Updates the resources associated with a tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Update a tag",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Tag JSON object",
|
||||
"name": "tag",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tags/{tag}": {
|
||||
"post": {
|
||||
"description": "Add a resource to a tag",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"tag resource"
|
||||
],
|
||||
"summary": "Add a resource to a tag",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Tag name",
|
||||
"name": "tag",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Resource JSON object",
|
||||
"name": "resource",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/main.tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"main.tag": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Resource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"resource": {
|
||||
"type": "string"
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
docs/swagger.yaml
Normal file
@ -0,0 +1,194 @@
|
||||
basePath: /api/v1
|
||||
definitions:
|
||||
main.tag:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
resources:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
models.Resource:
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
resource:
|
||||
type: string
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
host: localhost:8080
|
||||
info:
|
||||
contact:
|
||||
email: matthewrich.conf@gmail.com
|
||||
name: API Support
|
||||
url: http://www.swagger.io/support
|
||||
description: This is the Tagger API.
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
termsOfService: http://swagger.io/terms/
|
||||
title: Tagger API
|
||||
version: "0.31"
|
||||
paths:
|
||||
/ping:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: ping alive endpoint
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
type: string
|
||||
summary: ping endpoint
|
||||
tags:
|
||||
- ping
|
||||
/resources:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get resource to tags
|
||||
parameters:
|
||||
- description: Resource URL
|
||||
in: query
|
||||
name: resource
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.Resource'
|
||||
summary: Get resource tags
|
||||
tags:
|
||||
- tag resource
|
||||
/resources/{resource}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get resource to tags
|
||||
parameters:
|
||||
- description: Base64 encoded Resource URL
|
||||
in: path
|
||||
name: resource
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.Resource'
|
||||
summary: Get resource tags
|
||||
tags:
|
||||
- tag resource
|
||||
/tags:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Add a tag for a resource
|
||||
parameters:
|
||||
- description: Tag JSON object
|
||||
in: body
|
||||
name: tag
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.tag'
|
||||
summary: Add a tag for a resource
|
||||
tags:
|
||||
- tag resource
|
||||
/tags/{name}:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Get resources associated with a Tag
|
||||
parameters:
|
||||
- description: Tag name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
summary: Get list of resources tagged
|
||||
tags:
|
||||
- Tags
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Updates the resources associated with a tag
|
||||
parameters:
|
||||
- description: Tag name
|
||||
in: path
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: Tag JSON object
|
||||
in: body
|
||||
name: tag
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.tag'
|
||||
summary: Update a tag
|
||||
tags:
|
||||
- tag resource
|
||||
/tags/{tag}:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Add a resource to a tag
|
||||
parameters:
|
||||
- description: Tag name
|
||||
in: path
|
||||
name: tag
|
||||
required: true
|
||||
type: string
|
||||
- description: Resource JSON object
|
||||
in: body
|
||||
name: resource
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/main.tag'
|
||||
summary: Add a resource to a tag
|
||||
tags:
|
||||
- tag resource
|
||||
swagger: "2.0"
|
55
go.mod
Normal file
@ -0,0 +1,55 @@
|
||||
module tagger
|
||||
|
||||
go 1.21.1
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/redis/go-redis/v9 v9.4.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.0
|
||||
github.com/swaggo/swag v1.16.3
|
||||
github.com/twmb/murmur3 v1.1.8
|
||||
golang.org/x/net v0.21.0
|
||||
google.golang.org/protobuf v1.32.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/cors v1.5.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.4 // indirect
|
||||
github.com/go-openapi/spec v0.20.14 // indirect
|
||||
github.com/go-openapi/swag v0.22.9 // 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.18.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
161
go.sum
Normal file
@ -0,0 +1,161 @@
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
|
||||
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk=
|
||||
github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
|
||||
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
|
||||
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
|
||||
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
|
||||
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
|
||||
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
|
||||
github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE=
|
||||
github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
|
||||
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
|
||||
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
1
go_deps.yml
Normal file
@ -0,0 +1 @@
|
||||
- github.com/swaggo/swag/cmd/swag@latest
|
26
httptest/stream/stream.go
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
package stream
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
)
|
||||
|
||||
type Recorder struct {
|
||||
*httptest.ResponseRecorder
|
||||
closeStream chan bool
|
||||
}
|
||||
|
||||
func (r *Recorder) CloseNotify() <-chan bool {
|
||||
return r.closeStream
|
||||
}
|
||||
|
||||
func (r *Recorder) Close() {
|
||||
r.closeStream <- true
|
||||
}
|
||||
|
||||
func NewRecorder() *Recorder{
|
||||
return &Recorder{
|
||||
httptest.NewRecorder(),
|
||||
make(chan bool),
|
||||
}
|
||||
}
|
57
httptest/stream/stream_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package stream
|
||||
|
||||
import (
|
||||
_ "fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
_ "encoding/json"
|
||||
_ "strings"
|
||||
)
|
||||
|
||||
func TestStreamRecorder(t *testing.T) {
|
||||
r := NewRecorder()
|
||||
if r == nil {
|
||||
t.Errorf("Failed creating new recorder")
|
||||
}
|
||||
if r.CloseNotify() == nil {
|
||||
t.Errorf("Failed creating CloseNotify channel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamClose(t *testing.T) {
|
||||
r := NewRecorder()
|
||||
go r.Close()
|
||||
|
||||
select {
|
||||
case flag, open := <-r.CloseNotify():
|
||||
if flag && open {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("Failed to close the stream close channel")
|
||||
}
|
||||
|
||||
func TestStream(t *testing.T) {
|
||||
r := NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
|
||||
g := gin.New()
|
||||
g.Handle(http.MethodGet, "/test", func(c *gin.Context){
|
||||
c.Stream(func(w io.Writer) bool { c.SSEvent("message", "msg"); return false })
|
||||
<-c.Writer.CloseNotify()
|
||||
})
|
||||
|
||||
go g.ServeHTTP(r, request)
|
||||
|
||||
|
||||
|
||||
for !r.Flushed {}
|
||||
|
||||
r.Close()
|
||||
assert.Equal(t, r.Body.String(), "event:message\ndata:msg\n\n")
|
||||
}
|
47
internal/client/client.go
Normal file
@ -0,0 +1,47 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
_ "io"
|
||||
"fmt"
|
||||
"strings"
|
||||
_ "encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type TaggerApi struct {
|
||||
client *http.Client
|
||||
endpoint string
|
||||
}
|
||||
|
||||
func New(endpointUrl string) *TaggerApi {
|
||||
return &TaggerApi{
|
||||
client: &http.Client{},
|
||||
endpoint: endpointUrl,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TaggerApi) assertReceiver() {
|
||||
if t == nil {
|
||||
panic("Method can't be called on nil receiver")
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TaggerApi) Ping() error {
|
||||
t.assertReceiver()
|
||||
resp, e := t.client.Get(fmt.Sprintf("%s/ping", t.endpoint))
|
||||
defer resp.Body.Close()
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (t *TaggerApi) AddTagResource(tagName string, resourceUrl string) error {
|
||||
t.assertReceiver()
|
||||
|
||||
jsonReader := strings.NewReader(fmt.Sprintf(`{ "resource": "%s" }`, resourceUrl))
|
||||
|
||||
resp, e := t.client.Post(fmt.Sprintf("%s/tags/%s", t.endpoint, tagName), "application/json", jsonReader)
|
||||
if e == nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
return e
|
||||
}
|
53
internal/client/client_test.go
Normal file
@ -0,0 +1,53 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
_ "strings"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
cli := New("http://localhost:8080")
|
||||
assert.NotEqual(t, nil, cli)
|
||||
}
|
||||
|
||||
func TestClientConnection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, req.URL.String(), "/api/v1/ping")
|
||||
rw.Write([]byte(`pong`))
|
||||
}))
|
||||
|
||||
defer server.Close()
|
||||
|
||||
cli := New(fmt.Sprintf("%s/api/v1", server.URL))
|
||||
e := cli.Ping()
|
||||
assert.Equal(t, nil, e)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestClientDecodeJsonResponse(t *testing.T) {
|
||||
cli := New()
|
||||
statusJsonDocument := strings.NewReader(`{"status": "success"}`)
|
||||
|
||||
v := cli.decodeJsonResponse(statusJsonDocument)
|
||||
assert.Greater(t, 0, len(v))
|
||||
}
|
||||
*/
|
||||
|
||||
func TestClientAddTagResource(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
assert.Equal(t, req.URL.String(), "/api/v1/tags/finance")
|
||||
rw.Write([]byte(`{ "status": "success" }`))
|
||||
}))
|
||||
|
||||
defer server.Close()
|
||||
|
||||
|
||||
cli := New(fmt.Sprintf("%s/api/v1", server.URL))
|
||||
|
||||
err := cli.AddTagResource("finance", "https://finance.yahoo.com")
|
||||
assert.Equal(t, nil, err)
|
||||
}
|
42
internal/data/mock_redis_client_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type MockRedisClient struct {
|
||||
InjectPing func(ctx context.Context) *redis.StatusCmd
|
||||
InjectLRange func(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd
|
||||
InjectSInter func(ctx context.Context, keys ...string) *redis.StringSliceCmd
|
||||
InjectKeys func(ctx context.Context, pattern string) *redis.StringSliceCmd
|
||||
InjectSAdd func(ctx context.Context, key string, members ...interface{}) *redis.IntCmd
|
||||
InjectSIsMember func(ctx context.Context, key string, member interface{}) *redis.BoolCmd
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Ping(ctx context.Context) *redis.StatusCmd {
|
||||
if m.InjectPing == nil {
|
||||
return redis.NewStatusCmd(ctx, "ping")
|
||||
}
|
||||
return m.InjectPing(ctx)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd {
|
||||
return m.InjectLRange(ctx, key, start, stop)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) SInter(ctx context.Context, keys ...string) *redis.StringSliceCmd {
|
||||
return m.InjectSInter(ctx, keys...)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) Keys(ctx context.Context, pattern string) *redis.StringSliceCmd {
|
||||
return m.InjectKeys(ctx, pattern)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd {
|
||||
return m.InjectSAdd(ctx, key, members...)
|
||||
}
|
||||
|
||||
func (m *MockRedisClient) SIsMember(ctx context.Context, key string, member interface{}) *redis.BoolCmd {
|
||||
return m.InjectSIsMember(ctx, key, member)
|
||||
}
|
61
internal/data/redis.go
Normal file
@ -0,0 +1,61 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type RedisClientConnector interface {
|
||||
Ping(ctx context.Context) *redis.StatusCmd
|
||||
LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd
|
||||
SInter(ctx context.Context, keys ...string) *redis.StringSliceCmd
|
||||
Keys(ctx context.Context, pattern string) *redis.StringSliceCmd
|
||||
SAdd(ctx context.Context, key string, members ...interface{}) *redis.IntCmd
|
||||
SIsMember(ctx context.Context, key string, member interface{}) *redis.BoolCmd
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
client RedisClientConnector
|
||||
}
|
||||
|
||||
func NewRedisConnector(client RedisClientConnector) *Redis {
|
||||
if client == nil {
|
||||
client = redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
}
|
||||
return &Redis{ client: client }
|
||||
}
|
||||
|
||||
func (r *Redis) Tags(context context.Context) []string {
|
||||
result,_ := r.client.Keys(context, "*").Result()
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Redis) Connected(context context.Context) bool {
|
||||
if e := r.client.Ping(context).Err(); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Redis) Query(context context.Context, terms []string) ([]string, error) {
|
||||
result,_ := r.client.SInter(context, terms...).Result()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *Redis) AddTag(context context.Context, key string, resource string) error {
|
||||
if r,e := r.client.SAdd(context, key, resource).Result(); r != 1 || e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Redis) ResourceHasTag(context context.Context, resource string, tag string) bool {
|
||||
if r,e := r.client.SIsMember(context, tag, resource).Result(); r == false || e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
88
internal/data/redis_test.go
Normal file
@ -0,0 +1,88 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func TestNewRedisConnector(t *testing.T) {
|
||||
r := NewRedisConnector(nil)
|
||||
assert.NotEqual(t, r, nil)
|
||||
}
|
||||
|
||||
func TestRedisConnectorConnected(t *testing.T) {
|
||||
m := &MockRedisClient{}
|
||||
r := NewRedisConnector(m)
|
||||
|
||||
assert.Equal(t, r.Connected(context.Background()), true)
|
||||
}
|
||||
|
||||
func TestRedisConnectorQuery(t *testing.T) {
|
||||
expected := []string { "https://www.google.com" }
|
||||
|
||||
m := &MockRedisClient{
|
||||
InjectSInter: func(ctx context.Context, keys ...string) *redis.StringSliceCmd {
|
||||
return redis.NewStringSliceResult(expected, nil)
|
||||
},
|
||||
}
|
||||
r := NewRedisConnector(m)
|
||||
|
||||
results, e := r.Query(context.Background(), []string { "search-engine", "ai" })
|
||||
|
||||
assert.Equal(t, e, nil)
|
||||
assert.Equal(t, len(expected), len(results))
|
||||
|
||||
for i,e := range expected {
|
||||
assert.Equal(t, results[i], e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisConnectorTags(t *testing.T) {
|
||||
expected := []string { "search-engine", "ai" }
|
||||
|
||||
m := &MockRedisClient {
|
||||
InjectKeys: func(ctx context.Context, pattern string) *redis.StringSliceCmd {
|
||||
return redis.NewStringSliceResult(expected, nil)
|
||||
},
|
||||
}
|
||||
|
||||
r := NewRedisConnector(m)
|
||||
|
||||
results := r.Tags(context.Background())
|
||||
|
||||
assert.Equal(t, len(expected), len(results))
|
||||
|
||||
for i,e := range expected {
|
||||
assert.Equal(t, results[i], e)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedisConnectorAddTag(t *testing.T) {
|
||||
var expected int64 = 1
|
||||
m := &MockRedisClient {
|
||||
InjectSAdd: func(ctx context.Context, key string, members ...interface{}) *redis.IntCmd {
|
||||
return redis.NewIntResult(expected, nil)
|
||||
},
|
||||
}
|
||||
|
||||
r := NewRedisConnector(m)
|
||||
|
||||
e := r.AddTag(context.Background(), "search-engine", "https://www.yahoo.com")
|
||||
|
||||
assert.Equal(t, e, nil)
|
||||
|
||||
}
|
||||
|
||||
func TestRedisConnectorResourceHasTag(t *testing.T) {
|
||||
m := &MockRedisClient {
|
||||
InjectSIsMember: func(ctx context.Context, key string, member interface{}) *redis.BoolCmd {
|
||||
return redis.NewBoolResult(true, nil)
|
||||
},
|
||||
}
|
||||
|
||||
r := NewRedisConnector(m)
|
||||
|
||||
assert.Equal(t, r.ResourceHasTag(context.Background(), "https://www.yahoo.com", "search-engine"), true)
|
||||
}
|
5
internal/models/models.go
Normal file
@ -0,0 +1,5 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
|
||||
)
|
9
internal/models/models_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewModel(t *testing.T) {
|
||||
|
||||
}
|
26
internal/models/resource.go
Normal file
@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"io"
|
||||
"encoding/json"
|
||||
_ "fmt"
|
||||
)
|
||||
|
||||
type Resource struct {
|
||||
Id string `json:"id",omitempty`
|
||||
Resource string `json:"resource",omitempty`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
func NewResource() *Resource {
|
||||
return &Resource{}
|
||||
}
|
||||
|
||||
func NewResourceFromJson(jsonReader io.Reader) (*Resource, error) {
|
||||
r := NewResource()
|
||||
err := json.NewDecoder(jsonReader).Decode(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
17
internal/models/resource_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewResourceFromJson(t *testing.T) {
|
||||
testJsonDocument := strings.NewReader(`{ "resource": "bar", "tags": [ "foo" ] }`)
|
||||
|
||||
r,e := NewResourceFromJson(io.NopCloser(testJsonDocument))
|
||||
assert.Equal(t, e, nil)
|
||||
assert.Equal(t, r.Resource, "bar")
|
||||
assert.Contains(t, r.Tags, "foo")
|
||||
}
|
25
internal/models/tag.go
Normal file
@ -0,0 +1,25 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"io"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Id string `json:"id",omitempty`
|
||||
Name string `json:"name",omitempty`
|
||||
Resources []string `json:"resources"`
|
||||
}
|
||||
|
||||
func NewTag() *Tag {
|
||||
return &Tag{}
|
||||
}
|
||||
|
||||
func NewTagFromJson(jsonReader io.ReadCloser) (*Tag, error) {
|
||||
t := NewTag()
|
||||
err := json.NewDecoder(jsonReader).Decode(t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
16
internal/models/tag_test.go
Normal file
@ -0,0 +1,16 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewTagFromJson(t *testing.T) {
|
||||
testJsonDocument := strings.NewReader(`{ "name": "foo", "resources": [ "bar" ] }`)
|
||||
|
||||
tag,e := NewTagFromJson(io.NopCloser(testJsonDocument))
|
||||
assert.Equal(t, e, nil)
|
||||
assert.Equal(t, tag.Name, "foo")
|
||||
}
|
13
internal/service/data.go
Normal file
@ -0,0 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type DataConnector interface {
|
||||
Connected(ctx context.Context) bool
|
||||
Query(ctx context.Context, terms []string) ([]string, error)
|
||||
Tags(ctx context.Context) []string
|
||||
AddTag(ctx context.Context, TagName string, Resource string) error
|
||||
ResourceHasTag(ctx context.Context, Resource string, TagName string) bool
|
||||
}
|
36
internal/service/mock_data_connector_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type MockDataConnector struct {
|
||||
InjectConnected func(context.Context) bool
|
||||
InjectQuery func(context.Context, []string) ([]string, error)
|
||||
InjectTags func(context.Context) []string
|
||||
InjectAddTag func(context.Context, string, string) error
|
||||
InjectResourceHasTag func(context.Context, string, string) bool
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Connected(ctx context.Context) bool {
|
||||
if m.InjectConnected == nil {
|
||||
return true
|
||||
}
|
||||
return m.InjectConnected(ctx)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Query(ctx context.Context, terms []string) ([]string, error) {
|
||||
return m.InjectQuery(ctx, terms)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Tags(ctx context.Context) []string {
|
||||
return m.InjectTags(ctx)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) AddTag(ctx context.Context, TagName string, Resource string) error {
|
||||
return m.InjectAddTag(ctx, TagName, Resource)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) ResourceHasTag(ctx context.Context, TagName string, Resource string) bool {
|
||||
return m.InjectResourceHasTag(ctx, TagName, Resource)
|
||||
}
|
51
internal/service/service.go
Normal file
@ -0,0 +1,51 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
_ "io"
|
||||
"context"
|
||||
_ "fmt"
|
||||
_ "net/http"
|
||||
_ "encoding/json"
|
||||
_ "github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
connector DataConnector
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Tags(ctx context.Context) []string {
|
||||
return s.connector.Tags(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) Query(ctx context.Context, terms []string) []string {
|
||||
result, _ := s.connector.Query(ctx, terms)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) UseDataConnector(ctx context.Context, d DataConnector) error {
|
||||
if d != nil && d.Connected(ctx) {
|
||||
s.connector = d
|
||||
return nil
|
||||
}
|
||||
return errors.New("The connector is not connected")
|
||||
}
|
||||
|
||||
func (s *Service) AddTag(ctx context.Context, TagName string, Resource string) error {
|
||||
return s.connector.AddTag(ctx, TagName, Resource)
|
||||
}
|
||||
|
||||
func (s *Service) ResourceTags(ctx context.Context, Resource string) []string {
|
||||
tags := s.connector.Tags(ctx)
|
||||
results := make([]string, 0, len(tags)/10 )
|
||||
for _,t := range(s.connector.Tags(ctx)) {
|
||||
if(s.connector.ResourceHasTag(ctx, Resource, t)) {
|
||||
results = append(results, t)
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
95
internal/service/service_test.go
Normal file
@ -0,0 +1,95 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
assert.NotEqual(t, NewService(), nil)
|
||||
}
|
||||
|
||||
func TestServiceTags(t *testing.T) {
|
||||
s := NewService()
|
||||
|
||||
expected := []string { "search-engines", "ai", "been-there" }
|
||||
ctx := context.Background()
|
||||
m := &MockDataConnector {
|
||||
InjectTags: func(ctx context.Context) []string { return expected },
|
||||
}
|
||||
assert.Equal(t, s.UseDataConnector(ctx, m), nil)
|
||||
|
||||
results := s.Tags(ctx)
|
||||
assert.Equal(t, len(results), len(expected))
|
||||
for i,r := range results {
|
||||
assert.Equal(t, r, expected[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceTagQuery(t *testing.T) {
|
||||
s := NewService()
|
||||
|
||||
expected := []string {
|
||||
"https://www.google.com",
|
||||
"https://www.bing.com",
|
||||
}
|
||||
ctx := context.Background()
|
||||
m := &MockDataConnector {
|
||||
InjectQuery: func(ctx context.Context, terms []string) ([]string, error) { return expected, nil },
|
||||
}
|
||||
assert.Equal(t, s.UseDataConnector(ctx, m), nil)
|
||||
|
||||
results := s.Query(ctx, []string{"search-engine", "ai"})
|
||||
assert.Equal(t, len(results), len(expected))
|
||||
for i,r := range results {
|
||||
assert.Equal(t, r, expected[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUseDataConnector(t *testing.T) {
|
||||
s := NewService()
|
||||
m := &MockDataConnector {}
|
||||
|
||||
assert.Equal(t, s.UseDataConnector(context.Background(), m), nil)
|
||||
}
|
||||
|
||||
func TestServiceAddTag(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := NewService()
|
||||
m := &MockDataConnector {
|
||||
InjectAddTag: func(ctx context.Context, TagName string, Resource string) error { return nil },
|
||||
}
|
||||
assert.Equal(t, s.UseDataConnector(ctx, m), nil)
|
||||
|
||||
s.AddTag(ctx, "search-engine", "https://www.yahoo.com")
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
func TestServiceExtractTags(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := NewService()
|
||||
m := &MockDataConnector {
|
||||
|
||||
}
|
||||
assert.Equal(t, s.
|
||||
}
|
||||
*/
|
||||
|
||||
func TestServiceTagsByResource(t *testing.T) {
|
||||
tags := []string{ "search-engine", "yahoo-search" }
|
||||
ctx := context.Background()
|
||||
s := NewService()
|
||||
|
||||
m := &MockDataConnector {
|
||||
InjectTags: func(ctx context.Context) []string { return tags },
|
||||
InjectResourceHasTag: func(ctx context.Context, TagName string, Resource string) bool { return true },
|
||||
}
|
||||
|
||||
assert.Equal(t, s.UseDataConnector(ctx, m), nil)
|
||||
v := s.ResourceTags(ctx, "https://www.yahoo.com")
|
||||
// search through all key sets and check if the resource exists (at best O(N) for all possible tags)
|
||||
// or just store the set of tags for each resource
|
||||
assert.Contains(t, v, "search-engine")
|
||||
}
|
348
main.go
Normal file
@ -0,0 +1,348 @@
|
||||
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()
|
||||
}
|
222
main_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "fmt"
|
||||
"context"
|
||||
"testing"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
_ "net/url"
|
||||
_ "io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"encoding/json"
|
||||
_ "encoding/base64"
|
||||
"strings"
|
||||
"tagger/httptest/stream"
|
||||
"github.com/gin-contrib/sse"
|
||||
"tagger/tests/mocks"
|
||||
"tagger/internal/models"
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func TestRouter(t *testing.T) {
|
||||
app := NewApp(nil)
|
||||
assert.NotNil(t, app)
|
||||
}
|
||||
|
||||
func TestPingRoute(t *testing.T) {
|
||||
app := NewApp(nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", app.BasePath() + "/ping", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Equal(t, "pong", w.Body.String())
|
||||
}
|
||||
|
||||
func TestTagsRoute(t *testing.T) {
|
||||
expected := []string { "search-engine", "evil", "done-that" }
|
||||
var results []string
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectTags: func(context.Context) ([]string) { return expected },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", app.BasePath() + "/tags", nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
json.NewDecoder(w.Body).Decode(&results)
|
||||
assert.Equal(t, len(expected), len(results))
|
||||
for i,v := range(results) {
|
||||
assert.Equal(t, expected[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagsStreamRoute(t *testing.T) {
|
||||
expected := []string { "search-engine", "evil", "done-that" }
|
||||
//var results []string
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectTags: func(context.Context) ([]string) { return expected },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := stream.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", app.BasePath() + "/tags", nil)
|
||||
req.Header.Set("Content-Type", "text/event-stream")
|
||||
go app.ServeHTTP(w, req)
|
||||
|
||||
for !w.Flushed {}
|
||||
w.Close()
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
events, _ := sse.Decode(w.Body)
|
||||
assert.Equal(t, len(expected), len(events))
|
||||
for i,v := range(events) {
|
||||
assert.Equal(t, expected[i], v.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagRoute(t *testing.T) {
|
||||
expected := []string { "https://www.google.com", "https://www.bing.com" }
|
||||
var result models.Tag
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectQuery: func(context.Context, []string) ([]string, error) { return expected, nil },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", app.BasePath() + "/tags/search-engine", nil)
|
||||
app.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
json.NewDecoder(w.Body).Decode(&result)
|
||||
assert.Equal(t, len(expected), len(result.Resources))
|
||||
assert.Equal(t, expected, result.Resources)
|
||||
}
|
||||
|
||||
|
||||
func TestTagStream(t *testing.T) {
|
||||
app := NewApp(nil)
|
||||
w := stream.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", app.BasePath() + "/tags", nil)
|
||||
req.Header.Set("Content-Type", "text/event-stream")
|
||||
go app.ServeHTTP(w, req)
|
||||
|
||||
w.Close()
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
}
|
||||
|
||||
func TestTagPost(t *testing.T) {
|
||||
expected := []string { "https://www.google.com", "https://www.bing.com", "https://www.yahoo.com" }
|
||||
var result models.Tag
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectQuery: func(context.Context, []string) ([]string, error) { return expected, nil },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
jsonReader := strings.NewReader(`{ "resource": "https://www.yahoo.com" }`)
|
||||
reqPost, _ := http.NewRequest("PUT", app.BasePath() + "/tags/search-engine", jsonReader)
|
||||
|
||||
wget := httptest.NewRecorder()
|
||||
reqGet, _ := http.NewRequest("GET", app.BasePath() + "/tags/search-engine", nil)
|
||||
app.ServeHTTP(w, reqPost)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
app.ServeHTTP(wget, reqGet)
|
||||
|
||||
assert.Equal(t, 200, wget.Code)
|
||||
json.NewDecoder(wget.Body).Decode(&result)
|
||||
assert.Contains(t, result.Resources, "https://www.yahoo.com")
|
||||
}
|
||||
|
||||
func TestTagUpdate(t *testing.T) {
|
||||
expected := []string { "https://www.google.com", "https://www.bing.com", "https://www.yahoo.com" }
|
||||
var result models.Tag
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectQuery: func(context.Context, []string) ([]string, error) { return expected, nil },
|
||||
InjectAddTag: func(context.Context, string, string) (error) { return nil },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
jsonReader := strings.NewReader(`{ "name": "search-engine", "resources": [ "https://www.yahoo.com" ] }`)
|
||||
reqPost, _ := http.NewRequest("POST", app.BasePath() + "/tags", jsonReader)
|
||||
|
||||
wget := httptest.NewRecorder()
|
||||
reqGet, _ := http.NewRequest("GET", app.BasePath() + "/tags/search-engine", nil)
|
||||
app.ServeHTTP(w, reqPost)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
app.ServeHTTP(wget, reqGet)
|
||||
|
||||
assert.Equal(t, 200, wget.Code)
|
||||
json.NewDecoder(wget.Body).Decode(&result)
|
||||
assert.Contains(t, result.Resources, "https://www.yahoo.com")
|
||||
}
|
||||
|
||||
func TestTagAddResource(t *testing.T) {
|
||||
expected := []string { "https://www.google.com", "https://www.bing.com", "https://www.yahoo.com" }
|
||||
var result models.Tag
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectQuery: func(context.Context, []string) ([]string, error) { return expected, nil },
|
||||
InjectAddTag: func(context.Context, string, string) (error) { return nil },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
jsonReader := strings.NewReader(`{ "resource": "https://www.yahoo.com" }`)
|
||||
reqPost, _ := http.NewRequest("POST", app.BasePath() + "/tags/search-engine", jsonReader)
|
||||
|
||||
wget := httptest.NewRecorder()
|
||||
reqGet, _ := http.NewRequest("GET", app.BasePath() + "/tags/search-engine", nil)
|
||||
app.ServeHTTP(w, reqPost)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
app.ServeHTTP(wget, reqGet)
|
||||
|
||||
assert.Equal(t, 200, wget.Code)
|
||||
json.NewDecoder(wget.Body).Decode(&result)
|
||||
assert.Contains(t, result.Resources, "https://www.yahoo.com")
|
||||
|
||||
}
|
||||
|
||||
func TestGetResourceTags(t *testing.T) {
|
||||
|
||||
m := &mocks.MockDataConnector {
|
||||
InjectTags: func(context.Context) ([]string) { return []string{ "search-engine", "news" } },
|
||||
InjectResourceHasTag: func(ctx context.Context, res string, tag string) bool { if(tag == "search-engine") { return true } else { return false } },
|
||||
}
|
||||
|
||||
app := NewApp(m)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
reqGet, _ := http.NewRequest("GET", app.BasePath() + "/resources/" + base64.StdEncoding.EncodeToString([]byte("https://www.yahoo.com")), nil)
|
||||
reqGet.Header.Set("Content-Type", "application/json")
|
||||
app.ServeHTTP(w, reqGet)
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
result, e := models.NewResourceFromJson(w.Body)
|
||||
assert.Equal(t, nil, e)
|
||||
assert.Contains(t, result.Tags, "search-engine")
|
||||
}
|
BIN
static/icons/tagger128-bl.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
static/icons/tagger128-wh.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
static/icons/tagger128.ico
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
static/icons/tagger128.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
static/icons/tagger16-mb.ico
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
static/icons/tagger16-mw.ico
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
static/icons/tagger16.ico
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
static/icons/tagger256-bl.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
static/icons/tagger256-wh.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
static/icons/tagger32-bl.png
Normal file
After Width: | Height: | Size: 803 B |
BIN
static/icons/tagger32-wh.png
Normal file
After Width: | Height: | Size: 846 B |
BIN
static/icons/tagger32-whg.png
Normal file
After Width: | Height: | Size: 954 B |
BIN
static/icons/tagger32.ico
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
static/icons/tagger4.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
static/icons/tagger48.ico
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
static/icons/tagger64-bl.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
static/icons/tagger64-wh.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
static/icons/tagger64-whg.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/icons/tagger64.ico
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
static/icons/tagger96.ico
Normal file
After Width: | Height: | Size: 37 KiB |
2
templates/footer.tmpl
Normal file
@ -0,0 +1,2 @@
|
||||
</body>
|
||||
</html>
|
17
templates/header.tmpl
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>{{ .title }}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
|
||||
<link rel="icon" href="favicon.ico">
|
||||
<!--Use bootstrap to make the application look nice-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
39
templates/index.tmpl
Normal file
@ -0,0 +1,39 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
|
||||
<style>
|
||||
.tagger-icon {
|
||||
background-size: 20px;
|
||||
background: url("icons/tagger32-whg.png") no-repeat scroll left;
|
||||
padding-left: 30px
|
||||
}
|
||||
</style>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
<center>
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-4">
|
||||
<h1 class="display-1">Tagger</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/" method="POST" class="form-inline">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-4">
|
||||
<div class="form-group mb-2">
|
||||
<input class="form-control tagger-icon" style="background-color: black; color: white; font-size: 18pt;" type="text" name="terms[]" id="query">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-4">
|
||||
<button type="submit" class="btn btn-primary mb-2">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
{{ template "footer.tmpl" . }}
|
18
templates/query.tmpl
Normal file
@ -0,0 +1,18 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
|
||||
Terms: {{ range .terms }}{{ . }} {{ end }}
|
||||
|
||||
<br />
|
||||
<br />
|
||||
{{ range .resources }}
|
||||
|
||||
<div class="card" style="width: 18rem;">
|
||||
<img class="img-circle" width="32" height="32" src="{{ . }}/favicon.ico">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ . }}</h5>
|
||||
<p class="card-text"></p>
|
||||
<a href="{{ . }}">{{ . }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ end }}
|
11
templates/tags.tmpl
Normal file
@ -0,0 +1,11 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
|
||||
<table>
|
||||
{{ range .tags }}
|
||||
<tr>
|
||||
<td>
|
||||
{{ . }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
36
tests/mocks/data_connector.go
Normal file
@ -0,0 +1,36 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type MockDataConnector struct {
|
||||
InjectConnected func(context.Context) bool
|
||||
InjectQuery func(context.Context, []string) ([]string, error)
|
||||
InjectTags func(context.Context) []string
|
||||
InjectAddTag func(context.Context, string, string) error
|
||||
InjectResourceHasTag func(context.Context, string, string) bool
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Connected(context context.Context) bool {
|
||||
if m.InjectConnected == nil {
|
||||
return true
|
||||
}
|
||||
return m.InjectConnected(context)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Query(context context.Context, terms []string) ([]string, error) {
|
||||
return m.InjectQuery(context, terms)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) Tags(context context.Context) []string {
|
||||
return m.InjectTags(context)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) AddTag(context context.Context, TagName string, Resource string) error {
|
||||
return m.InjectAddTag(context, TagName, Resource)
|
||||
}
|
||||
|
||||
func (m *MockDataConnector) ResourceHasTag(context context.Context, Resource string, TagName string) bool {
|
||||
return m.InjectResourceHasTag(context, Resource, TagName)
|
||||
}
|
16
tests/mocks/http_client.go
Normal file
@ -0,0 +1,16 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
_ "testing"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type HttpClientRoundTrip func(req *http.Request) *http.Response
|
||||
|
||||
func (h HttpClientRoundTrip) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return h(r), nil
|
||||
}
|
||||
|
||||
func NewHttpClient(h HttpClientRoundTrip) *http.Client {
|
||||
return &http.Client{ Transport: h }
|
||||
}
|