-
Notifications
You must be signed in to change notification settings - Fork 0
Iter5 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Iter5 #5
Changes from 15 commits
6d6a53e
99b61eb
dd4b6c8
0cddb61
7b0b190
491b1e7
c146465
e4bd004
86ff933
d6a19a0
8fcb776
e595f4c
f4296de
06c9355
53def49
809a583
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,3 +21,4 @@ vendor/ | |
| # IDEs directories | ||
| .idea | ||
| .vscode | ||
| cmd/.DS_Store | ||
| 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() | ||
| } |
| 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 |
| 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= |
| 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) | ||
| } | ||
| 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{ | ||
|
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) | ||
| } | ||
| 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) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } |
| 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)) | ||
| } |
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Мапа при конкурентном доступе к ней выдает панику, учти это на будущее
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.