Compare commits

..

9 Commits

Author SHA1 Message Date
d00b9e1c9e Update 'README.md' 2022-09-27 15:05:16 +00:00
e4ca15f0c2 Update 'README.md' 2022-08-28 21:36:38 +00:00
8f1cb33eba Update 'README.md' 2022-08-28 13:24:55 +00:00
9d68dfc3ba Updated README.md 2022-08-28 15:23:17 +02:00
0110d6994b Return JSON-encoded errors 2022-08-28 15:07:46 +02:00
9da60fa4e7 Added link info endpoint 2022-08-28 15:01:02 +02:00
0944a14a97 Merge branch 'main' of ssh://git.cesium.pw:2335/niku/5feet11 2022-08-28 13:46:02 +02:00
9aee40d3b1 Changed shortener endpoint to /api/v1/links 2022-08-28 11:45:07 +02:00
4539c926ec Added CreatedAt field to URL model 2022-08-27 17:22:44 +02:00
12 changed files with 199 additions and 18 deletions

View File

@ -27,10 +27,22 @@ type (
}
)
type (
GetLinkResp {
ID string `json:"id"`
LongUrl string `json:"longUrl"`
CreatedAt string `json:"createdAt"`
Lifespan int64 `json:"lifespan"`
}
)
service fivefeeteleven-api {
@handler ExpandUrl
get /:id(ExpandReq) returns(ExpandResp)
@handler ShortenUrl
post /redirect(ShortenReq) returns(ShortenResp)
post /api/v1/links(ShortenReq) returns(ShortenResp)
@handler GetLink
get /api/v1/links/:id(ExpandReq) returns(GetLinkResp)
}

View File

@ -1,15 +1,39 @@
# 5feet11
A URL shortener that generates links even shorter than 5'11.
A scalable URL shortener.
## Prepare
- ScyllaDB
- Prometheus*
*: Optional
## Run
Update the config at `etc/fivefeeteleven-api.yaml`.
```sh
# Docker
docker run -it $(docker build -q .)
# Compile
make build && ./5feet11
```
## Usage
```bash
# Shorten a URL
curl --header "Content-Type: application/json" \
--request POST \
--data '{"redirectUrl":"https://news.ycombinator.com"}' \
http://localhost:5111/redirect
curl -X POST -H "Content-Type: application/json" \
-d '{"longUrl":"https://news.ycombinator.com"}' \
http://localhost:5111/api/v1/links
# Shorten a URL that expires after N seconds
curl -X POST -H "Content-Type: application/json" \
-d '{"longUrl":"https://news.ycombinator.com", "expiresAfter": 30}' \
http://localhost:5111/api/v1/links
# Expand the URL
curl -iL http://localhost:5111/{id}
# Get information of a specific URL
curl -iL http://localhost:5111/api/v1/links/{id}
```

View File

@ -2,14 +2,17 @@ package main
import (
"flag"
"net/http"
"5feet11/internal/config"
"5feet11/internal/errorx"
"5feet11/internal/handler"
"5feet11/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/rest/httpx"
)
var configFile = flag.String("f", "etc/fivefeeteleven-api.yaml", "the config file")
@ -26,6 +29,15 @@ func main() {
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
httpx.SetErrorHandler(func(err error) (int, interface{}) {
switch e := err.(type) {
case *errorx.CodeError:
return http.StatusBadRequest, e.Data()
default:
return http.StatusInternalServerError, nil
}
})
logx.Infof("Starting server at http://%s:%d", c.Host, c.Port)
server.Start()
}

View File

@ -1,15 +1,21 @@
package db
import "github.com/scylladb/gocqlx/v2/table"
import (
"time"
"github.com/scylladb/gocqlx/v2/table"
)
var UrlTable = table.New(table.Metadata{
Name: "fivefeeteleven.urls",
Columns: []string{"id", "long_url"},
Columns: []string{"id", "long_url", "lifespan", "created_at"},
PartKey: []string{"id"},
SortKey: []string{},
})
type UrlModel struct {
ID string
LongUrl string
ID string
LongUrl string
Lifespan int64
CreatedAt time.Time
}

View File

@ -15,7 +15,9 @@ func Seed(session gocqlx.Session) error {
err = session.ExecStmt(`
CREATE TABLE IF NOT EXISTS fivefeeteleven.urls (
id text PRIMARY KEY,
long_url text
long_url text,
created_at timestamp,
lifespan bigint
)`)
if err != nil {

32
internal/errorx/http.go Normal file
View File

@ -0,0 +1,32 @@
package errorx
const defaultCode = 1001
type CodeError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
type CodeErrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func NewCodeError(code int, msg string) error {
return &CodeError{Code: code, Msg: msg}
}
func NewDefaultError(msg string) error {
return NewCodeError(defaultCode, msg)
}
func (e *CodeError) Error() string {
return e.Msg
}
func (e *CodeError) Data() *CodeErrorResponse {
return &CodeErrorResponse{
Code: e.Code,
Msg: e.Msg,
}
}

View File

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"5feet11/internal/logic"
"5feet11/internal/svc"
"5feet11/internal/types"
"github.com/zeromicro/go-zero/rest/httpx"
)
func GetLinkHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ExpandReq
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
l := logic.NewGetLinkLogic(r.Context(), svcCtx)
resp, err := l.GetLink(&req)
if err != nil {
httpx.Error(w, err)
} else {
httpx.OkJson(w, resp)
}
}
}

View File

@ -19,9 +19,14 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
},
{
Method: http.MethodPost,
Path: "/redirect",
Path: "/api/v1/links",
Handler: ShortenUrlHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/api/v1/links/:id",
Handler: GetLinkHandler(serverCtx),
},
},
)
}

View File

@ -2,9 +2,9 @@ package logic
import (
"context"
"fmt"
"5feet11/internal/db"
"5feet11/internal/errorx"
"5feet11/internal/svc"
"5feet11/internal/types"
@ -31,11 +31,11 @@ func (l *ExpandUrlLogic) ExpandUrl(req *types.ExpandReq) (resp *types.ExpandResp
var urls []db.UrlModel
if err := queryUrl.Select(&urls); err != nil {
return nil, err
return nil, errorx.NewDefaultError(err.Error())
}
if len(urls) != 1 {
return nil, fmt.Errorf("no URL found")
return nil, errorx.NewDefaultError("no URL found")
}
resp = &types.ExpandResp{

View File

@ -0,0 +1,49 @@
package logic
import (
"context"
"5feet11/internal/db"
"5feet11/internal/errorx"
"5feet11/internal/svc"
"5feet11/internal/types"
"github.com/zeromicro/go-zero/core/logx"
)
type GetLinkLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLinkLogic {
return &GetLinkLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetLinkLogic) GetLink(req *types.ExpandReq) (resp *types.GetLinkResp, err error) {
queryUrl := db.UrlTable.SelectQuery(l.svcCtx.DB)
queryUrl.BindStruct(db.UrlModel{ID: req.ID})
var urls []db.UrlModel
if err := queryUrl.Select(&urls); err != nil {
return nil, errorx.NewDefaultError(err.Error())
}
if len(urls) != 1 {
return nil, errorx.NewDefaultError("no URL found")
}
resp = &types.GetLinkResp{
ID: urls[0].ID,
LongUrl: urls[0].LongUrl,
CreatedAt: urls[0].CreatedAt.String(),
Lifespan: urls[0].Lifespan,
}
return resp, err
}

View File

@ -5,6 +5,7 @@ import (
"time"
"5feet11/internal/db"
"5feet11/internal/errorx"
"5feet11/internal/svc"
"5feet11/internal/types"
@ -36,16 +37,19 @@ func (l *ShortenUrlLogic) ShortenUrl(req *types.ShortenReq) (resp *types.Shorten
insertUrl := insertBuilder.Query(l.svcCtx.DB)
insertUrl.BindStruct(db.UrlModel{
ID: id,
LongUrl: req.LongUrl,
ID: id,
LongUrl: req.LongUrl,
Lifespan: req.ExpiresAfter,
CreatedAt: time.Now(),
})
if err := insertUrl.ExecRelease(); err != nil {
return nil, err
return nil, errorx.NewDefaultError(err.Error())
}
resp = &types.ShortenResp{
ID: id,
}
return resp, nil
}

View File

@ -17,3 +17,10 @@ type ShortenReq struct {
type ShortenResp struct {
ID string `json:"id"`
}
type GetLinkResp struct {
ID string `json:"id"`
LongUrl string `json:"longUrl"`
CreatedAt string `json:"createdAt"`
Lifespan int64 `json:"lifespan"`
}