Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ vendor/
# IDEs directories
.idea
.vscode
cmd/.DS_Store
14 changes: 13 additions & 1 deletion cmd/shortener/main.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
package main

func main() {}
import (
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/server"
"github.com/shilin-anton/urlreducer/internal/app/storage"
)

func main() {
config.ParseConfig()

myStorage := storage.New()
myServer := server.New(myStorage)
myServer.Start()
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/shilin-anton/urlreducer

go 1.21.3

require github.com/go-chi/chi/v5 v5.0.11
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
27 changes: 27 additions & 0 deletions internal/app/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package config

import (
"flag"
"fmt"
"os"
)

var RunAddr string
var BaseAddr string

const defaultRunURL = "localhost:8080"
const defaultBaseURL = "http://localhost:8080"

func ParseConfig() {
flag.StringVar(&RunAddr, "a", defaultRunURL, "address and port to run server")
flag.StringVar(&BaseAddr, "b", defaultBaseURL, "base URL before short link")
flag.Parse()

if envRunAddr := os.Getenv("SERVER_ADDRESS"); envRunAddr != "" {
RunAddr = envRunAddr
}
if envBaseAddr := os.Getenv("BASE_URL"); envBaseAddr != "" {
BaseAddr = envBaseAddr
}
fmt.Printf("Server is running on %s\nBase URL is %s\n", RunAddr, BaseAddr)
Comment thread
shilin-anton marked this conversation as resolved.
Outdated
}
76 changes: 76 additions & 0 deletions internal/app/handlers/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package handlers

import (
"crypto/md5"
"encoding/hex"
"github.com/go-chi/chi/v5"
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/storage"
"io"
"net/http"
)

type Storage interface {
Add(short string, url string)
Get(short string) (string, bool)
}

type Server struct {
data Storage
handler http.Handler
}

func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.handler.ServeHTTP(w, r)
}

func New() *Server {
r := chi.NewRouter()

S := &Server{
Comment thread
shilin-anton marked this conversation as resolved.
Outdated
data: make(storage.Storage),
handler: r,
}
r.Get("/{short}", S.GetHandler)
r.Post("/", S.PostHandler)

return S
}

func shortenURL(url string) string {
// Решил использовать хэширование и первые символы результата, как короткую форму URL
hash := md5.Sum([]byte(url))
hashString := hex.EncodeToString(hash[:])
shortURL := hashString[:8]
return shortURL
}

func (s Server) PostHandler(res http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
if err != nil {
http.Error(res, "Error reading request body", http.StatusInternalServerError)
return
}
defer req.Body.Close()

url := string(body)
short := shortenURL(url)

s.data.Add(short, url)

res.Header().Set("Content-Type", "text/plain")
res.WriteHeader(http.StatusCreated)
res.Write([]byte(config.BaseAddr + "/" + short))
}

func (s Server) GetHandler(res http.ResponseWriter, req *http.Request) {
short := chi.URLParam(req, "short")

url, ok := s.data.Get(short)
if !ok {
http.NotFound(res, req)
return
}
res.Header().Set("Location", url)
res.WriteHeader(http.StatusTemporaryRedirect)
}
94 changes: 94 additions & 0 deletions internal/app/handlers/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package handlers

import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestPostHandler(t *testing.T) {
srv := New()

tests := []struct {
name string
method string
url string
requestBody string
wantStatusCode int
}{
{
name: "Valid POST request",
method: http.MethodPost,
url: "/",
requestBody: "http://example.com",
wantStatusCode: http.StatusCreated,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, test.url, strings.NewReader(test.requestBody))
w := httptest.NewRecorder()

srv.handler.ServeHTTP(w, req)

resp := w.Result()
defer resp.Body.Close()

if resp.StatusCode != test.wantStatusCode {
t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode)
}
})
}
}

func TestGetHandler(t *testing.T) {
srv := New()
srv.data.Add("test_short", "https://smth.ru")

tests := []struct {
name string
method string
url string
wantStatusCode int
wantLocationHeader string
}{
{
name: "Valid GET request with existing short link",
method: http.MethodGet,
url: "/test_short",
wantStatusCode: http.StatusTemporaryRedirect,
wantLocationHeader: "https://smth.ru",
},
{
name: "Invalid GET request with non-existing short link",
method: http.MethodGet,
url: "/non_existing_short_link",
wantStatusCode: http.StatusNotFound,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(test.method, test.url, nil)
w := httptest.NewRecorder()

srv.handler.ServeHTTP(w, req)

resp := w.Result()
defer resp.Body.Close()

if resp.StatusCode != test.wantStatusCode {
t.Errorf("unexpected status code: got %d, want %d", resp.StatusCode, test.wantStatusCode)
}

if test.wantLocationHeader != "" {
location := resp.Header.Get("Location")
if location != test.wantLocationHeader {
t.Errorf("unexpected Location header: got %s, want %s", location, test.wantLocationHeader)
}
}
})
}
}
31 changes: 31 additions & 0 deletions internal/app/server/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package server

import (
"github.com/shilin-anton/urlreducer/internal/app/config"
"github.com/shilin-anton/urlreducer/internal/app/handlers"
"log"
"net/http"
)

type Storage interface {
Add(short string, url string)
Get(short string) (string, bool)
}

type server struct {
handler http.Handler
storage Storage
}

func New(storage Storage) *server {
handler := handlers.New()
S := &server{
handler: handler,
storage: storage,
}
return S
}

func (s server) Start() {
log.Fatal(http.ListenAndServe(config.RunAddr, s.handler))
}
17 changes: 17 additions & 0 deletions internal/app/storage/storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package storage

type Storage map[string]string

func (s Storage) Add(short string, url string) {
s[short] = url
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мапа при конкурентном доступе к ней выдает панику, учти это на будущее

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

можно ли решить эту проблему, переделав Storage в структуру, в которой будет мапа и мьютекс?
и перед каждым обращением делать лок, с последующим анлоком?

}

func (s Storage) Get(short string) (string, bool) {
url, ok := s[short]
return url, ok
}

func New() Storage {
storage := make(map[string]string)
return storage
}