-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 9868ee3
Showing
17 changed files
with
814 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
version: 2 | ||
updates: | ||
- package-ecosystem: "gomod" | ||
directory: "/" | ||
schedule: | ||
interval: "daily" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
name: Test | ||
on: push | ||
jobs: | ||
test: | ||
name: Test | ||
runs-on: ubuntu-latest | ||
container: golang | ||
services: | ||
cache: | ||
image: memcached:latest | ||
ports: | ||
- 11211:11211 | ||
database: | ||
image: postgres:latest | ||
ports: | ||
- 5432:5432 | ||
env: | ||
POSTGRES_USER: postgres | ||
POSTGRES_PASSWORD: pass | ||
POSTGRES_DB: url-shortener | ||
options: >- | ||
--health-cmd pg_isready | ||
--health-interval 10s | ||
--health-timeout 5s | ||
--health-retries 5 | ||
steps: | ||
- name: Pull repository | ||
uses: actions/checkout@v3 | ||
- name: Set up Go | ||
uses: actions/setup-go@v3 | ||
with: | ||
cache: true | ||
- name: Run tests | ||
run: go test -coverprofile=coverage.txt | ||
env: | ||
MEMCACHED_HOST: cache | ||
POSTGRES_HOST: database | ||
- name: Upload test coverage report | ||
uses: codecov/codecov-action@v3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
FROM golang as builder | ||
RUN mkdir /build | ||
COPY . /build/ | ||
WORKDIR /build | ||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o main . | ||
FROM scratch | ||
LABEL maintainer="Bruno Fernandes <bfdes@users.noreply.github.com>" | ||
COPY --from=builder /build/main /app/ | ||
WORKDIR /app | ||
CMD ["./main"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# url-shortener | ||
|
||
![GitHub Actions](https://github.com/bfdes/url-shortener/workflows/Build/badge.svg) | ||
[![Codecov](https://codecov.io/gh/bfdes/url-shortener/branch/master/graph/badge.svg)](https://codecov.io/gh/bfdes/url-shortener) | ||
|
||
URL shortener and redirect service [designed for](https://www.notion.so/URL-shortening-8272c692648143698859d9f3524a8b5e#a2becb53582444cfb3e9cce1dd8978ba) low-latency, read-heavy use. | ||
|
||
## Usage | ||
|
||
### Requirements | ||
|
||
- [Go](https://golang.org/) 1.20.* | ||
- [Docker Engine](https://docs.docker.com/engine/) 20.* | ||
- [Docker Compose](https://docs.docker.com/compose/) 2.* | ||
|
||
Run the following command within the repository root to start container dependencies in the background: | ||
|
||
```shell | ||
docker compose up --detach cache database | ||
``` | ||
|
||
Then, when the databases are ready to accept connections, start the server with `go run .`. | ||
|
||
### Shorten a URL | ||
|
||
```shell | ||
curl http://localhost:8080/api/links \ | ||
--request POST \ | ||
--data '{"url": "http://example.com"}' | ||
# {"url": "http://example.com", "slug": "<SLUG>" } | ||
``` | ||
|
||
### Redirect a URL | ||
|
||
```shell | ||
curl http://localhost:8080/<SLUG> | ||
``` | ||
|
||
### Testing | ||
|
||
Run unit and integration tests with `go test` after starting container dependencies. | ||
|
||
[GitHub Actions](https://github.com/bfdes/url-shortener/actions) will run tests for every code push. | ||
|
||
## Deployment | ||
|
||
This URL shortener is unsuited for production use; it does not support logging or metric collection. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
// Characters defines the character set for base 62 encoding | ||
const Characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
|
||
const base = len(Characters) | ||
|
||
var digits = make(map[rune]int) | ||
|
||
func init() { | ||
for i, char := range Characters { | ||
digits[char] = i | ||
} | ||
} | ||
|
||
// Encode a non-negative integer into a base 62 symbol string | ||
func Encode(id int) (string, error) { | ||
if id < 0 { | ||
return "", errors.New("argument to Encode must be non-negative") | ||
} | ||
var sb strings.Builder | ||
for id > 0 { | ||
rem := id % base | ||
sb.WriteByte(Characters[rem]) | ||
id = id / base | ||
} | ||
return sb.String(), nil | ||
} | ||
|
||
// Decode a base 62 encoded string | ||
func Decode(str string) (int, error) { | ||
id := 0 | ||
coeff := 1 | ||
for _, char := range str { | ||
digit := digits[char] | ||
if char != '0' && digit == 0 { | ||
return 0, fmt.Errorf(`argument "%s" contains illegal character(s)`, str) | ||
} | ||
id += coeff * digit | ||
coeff *= base | ||
} | ||
return id, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package main | ||
|
||
import ( | ||
"math/rand" | ||
"testing" | ||
) | ||
|
||
type testInput struct { | ||
decoded int | ||
encoded string | ||
} | ||
|
||
var testInputs = []testInput{ | ||
{0, ""}, | ||
{1, "1"}, | ||
{62, "01"}, | ||
{1504, "go"}, | ||
} | ||
|
||
func TestEncode(t *testing.T) { | ||
for _, testInput := range testInputs { | ||
actual, err := Encode(testInput.decoded) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if actual != testInput.encoded { | ||
t.Errorf("%d encoded to %s, not %s", testInput.decoded, actual, testInput.encoded) | ||
} | ||
} | ||
} | ||
|
||
func TestEncodeNegative(t *testing.T) { | ||
arg := -1 | ||
encoded, err := Encode(arg) | ||
if err == nil { | ||
t.Errorf("negative argument %d encoded to %s", arg, encoded) | ||
} | ||
} | ||
|
||
func TestDecode(t *testing.T) { | ||
for _, testInput := range testInputs { | ||
actual, err := Decode(testInput.encoded) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if actual != testInput.decoded { | ||
t.Errorf("%s decoded to %d, not %d", testInput.encoded, actual, testInput.decoded) | ||
} | ||
} | ||
} | ||
|
||
func TestDecodeIllegalCharacter(t *testing.T) { | ||
arg := "!llegal" | ||
decoded, err := Decode(arg) | ||
if err == nil { | ||
t.Errorf("malformed slug %s decoded to %d", arg, decoded) | ||
} | ||
} | ||
|
||
func TestEncodeDecode(t *testing.T) { | ||
testInputs := []int{0, 1, 62} | ||
for i := 0; i < 20; i++ { | ||
testInputs = append(testInputs, rand.Int()) | ||
} | ||
for _, testInput := range testInputs { | ||
encoded, err := Encode(testInput) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
actual, err := Decode(encoded) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if actual != testInput { | ||
t.Errorf("%d does not roundtrip", testInput) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
version: '3.8' | ||
services: | ||
cache: | ||
image: memcached:latest | ||
ports: | ||
- 11211:11211 | ||
database: | ||
image: postgres:latest | ||
environment: | ||
- POSTGRES_USER=postgres | ||
- POSTGRES_PASSWORD=pass | ||
- POSTGRES_DB=url-shortener | ||
volumes: | ||
- data:/var/lib/postgresql/data/ | ||
ports: | ||
- 5432:5432 | ||
volumes: | ||
data: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module github.com/bfdes/url-shortener | ||
|
||
go 1.20 | ||
|
||
require ( | ||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b | ||
github.com/google/uuid v1.3.0 | ||
github.com/lib/pq v1.10.9 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= | ||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= | ||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= | ||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"io/ioutil" | ||
"net/http" | ||
"strings" | ||
) | ||
|
||
func RedirectHandler(service LinkService) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != http.MethodGet { | ||
msg := http.StatusText(http.StatusMethodNotAllowed) | ||
http.Error(w, msg, http.StatusMethodNotAllowed) | ||
return | ||
} | ||
slug := r.URL.Path[1:] | ||
url, err := service.Get(slug) | ||
if err == ErrDecodeFailure { | ||
msg := http.StatusText(http.StatusBadRequest) | ||
http.Error(w, msg, http.StatusBadRequest) | ||
} else if err == ErrNotFound { | ||
http.NotFound(w, r) | ||
} else { | ||
// `err` must be `nil`... | ||
http.Redirect(w, r, url, http.StatusPermanentRedirect) | ||
} | ||
}) | ||
} | ||
|
||
func CreateLinkHandler(service LinkService) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != http.MethodPost { | ||
msg := http.StatusText(http.StatusMethodNotAllowed) | ||
http.Error(w, msg, http.StatusMethodNotAllowed) | ||
return | ||
} | ||
if r.Body == nil || r.Body == http.NoBody { | ||
msg := http.StatusText(http.StatusBadRequest) | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
payload := Link{} | ||
body, err := ioutil.ReadAll(r.Body) // DOS attack vector | ||
defer r.Body.Close() | ||
if err == nil { | ||
err = json.Unmarshal(body, &payload) | ||
} | ||
if err != nil { | ||
msg := http.StatusText(http.StatusBadRequest) | ||
http.Error(w, msg, http.StatusBadRequest) | ||
return | ||
} | ||
url := strings.TrimSpace(payload.URL) | ||
link, err := service.Create(url) | ||
if err != nil { | ||
msg := http.StatusText(http.StatusInternalServerError) | ||
http.Error(w, msg, http.StatusInternalServerError) | ||
return | ||
} | ||
res, _ := json.Marshal(link) | ||
w.Header().Set("content-type", "application/json") | ||
w.WriteHeader(http.StatusCreated) | ||
w.Write(res) | ||
}) | ||
} |
Oops, something went wrong.