diff --git a/cmd/config/config.go b/cmd/config/config.go new file mode 100644 index 0000000..5736a65 --- /dev/null +++ b/cmd/config/config.go @@ -0,0 +1,27 @@ +package config + +import ( + "fmt" + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "github.com/nabbat/23_kogorta_shotener/internal/flags" +) + +func SetEnv() *envirements.EnvConfig { + fl := flags.ParseFlags() + en := envirements.ParseEnv() + c := &envirements.EnvConfig{} + + if en.RunAddr != "" { + c.RunAddr = en.RunAddr + } else { + c.RunAddr = fl.RunAddr + } + + if en.ResultURL != "" && en.RunAddr != "http://" { + c.ResultURL = en.ResultURL + } else { + c.ResultURL = fl.ResultURL + } + fmt.Println(c) + return c +} diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go new file mode 100644 index 0000000..bdd04a3 --- /dev/null +++ b/cmd/config/config_test.go @@ -0,0 +1,23 @@ +package config + +import ( + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "reflect" + "testing" +) + +func TestSetEnv(t *testing.T) { + tests := []struct { + name string + want *envirements.EnvConfig + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := SetEnv(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("SetEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/shortener/main.go b/cmd/shortener/main.go index 38dd16d..fcf9168 100644 --- a/cmd/shortener/main.go +++ b/cmd/shortener/main.go @@ -1,3 +1,34 @@ package main -func main() {} +import ( + "fmt" + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/cmd/config" + "github.com/nabbat/23_kogorta_shotener/internal/handlers" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage" + "net/http" +) + +func main() { + // Получаем переменные если они есть + c := config.SetEnv() + + // Создаем хранилище + storage := urlstorage.NewURLStorage() + // Создаем хэндлеры + redirectHandler := &handlers.RedirectHandler{} + shortenURLHandler := &handlers.ShortenURLHandler{} + + r := mux.NewRouter() + r.Use(handlers.PanicHandler) // Добавляем PanicHandler middleware + + r.HandleFunc("/", shortenURLHandler.HandleShortenURL(storage, c)).Methods("POST") + r.HandleFunc("/{idShortenURL}", redirectHandler.HandleRedirect(storage)).Methods("GET") + + fmt.Println("RunAddr: ResultURL: ", c.RunAddr, c.ResultURL) + fmt.Println("Running server on", c.RunAddr) + err := http.ListenAndServe(c.RunAddr, r) + if err != nil { + panic(err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5bb4e30 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/nabbat/23_kogorta_shotener + +go 1.20 + +require ( + github.com/gorilla/mux v1.8.0 + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.25.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d735a8b --- /dev/null +++ b/go.sum @@ -0,0 +1,19 @@ +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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/README.md b/internal/app/README.md deleted file mode 100644 index ba14e13..0000000 --- a/internal/app/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# internal/app - -В данной директории будет содержаться имплементация вашего сервиса \ No newline at end of file diff --git a/internal/envirements/envirements.go b/internal/envirements/envirements.go new file mode 100644 index 0000000..5bd2e52 --- /dev/null +++ b/internal/envirements/envirements.go @@ -0,0 +1,23 @@ +package envirements + +import ( + "os" + "strings" +) + +type EnvConfig struct { + RunAddr string + ResultURL string +} + +// ParseEnv Get system environments +func ParseEnv() *EnvConfig { + env := &EnvConfig{} + env.RunAddr = os.Getenv("RUN_ADDR") + env.ResultURL = os.Getenv("SERVER_ADDRESS") + // парсим переданные серверу аргументы в зарегистрированные переменные + if !strings.HasPrefix(env.ResultURL, "http://") && env.ResultURL != "" { + env.ResultURL = "http://" + env.ResultURL + } + return env +} diff --git a/internal/envirements/envirements_test.go b/internal/envirements/envirements_test.go new file mode 100644 index 0000000..eb68724 --- /dev/null +++ b/internal/envirements/envirements_test.go @@ -0,0 +1,22 @@ +package envirements + +import ( + "reflect" + "testing" +) + +func TestParseEnv(t *testing.T) { + tests := []struct { + name string + want *EnvConfig + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseEnv(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseEnv() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/flags/flags.go b/internal/flags/flags.go new file mode 100644 index 0000000..db3f284 --- /dev/null +++ b/internal/flags/flags.go @@ -0,0 +1,27 @@ +package flags + +import ( + flag "github.com/spf13/pflag" + "strings" +) + +// Flags структура для хранения настроек +type Flags struct { + RunAddr string + ResultURL string +} + +// ParseFlags обрабатывает аргументы командной строки +// и сохраняет их значения в соответствующих переменных +func ParseFlags() *Flags { + // Create a Config instance + flg := &Flags{} + flag.StringVarP(&flg.RunAddr, "a", "a", "localhost:8080", "Адрес запуска HTTP-сервера.") + flag.StringVarP(&flg.ResultURL, "b", "b", "http://localhost:8080", "Адрес результирующего сокращённого URL.") + // парсим переданные серверу аргументы в зарегистрированные переменные + flag.Parse() + if !strings.HasPrefix(flg.ResultURL, "http://") { + flg.ResultURL = "http://" + flg.ResultURL + } + return flg +} diff --git a/internal/flags/flags_test.go b/internal/flags/flags_test.go new file mode 100644 index 0000000..40a1f67 --- /dev/null +++ b/internal/flags/flags_test.go @@ -0,0 +1,22 @@ +package flags + +import ( + "reflect" + "testing" +) + +func TestParseFlags(t *testing.T) { + tests := []struct { + name string + want *Flags + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseFlags(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ParseFlags() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/handlers/urlhandlers.go b/internal/handlers/urlhandlers.go new file mode 100644 index 0000000..192da38 --- /dev/null +++ b/internal/handlers/urlhandlers.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "fmt" + "github.com/gorilla/mux" + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + "github.com/nabbat/23_kogorta_shotener/internal/shotenermaker" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage" + "io" + "log" + "net/http" +) + +type RedirectHandlerInterface interface { + HandleRedirect(storage *urlstorage.URLStorage) http.HandlerFunc +} + +type ShortenURLHandlerInterface interface { + HandleShortenURL(storage *urlstorage.URLStorage, c *envirements.EnvConfig) http.HandlerFunc +} + +type RedirectHandler struct{} + +func (rh *RedirectHandler) HandleRedirect(storage *urlstorage.URLStorage) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "invalid request type", http.StatusBadRequest) + return + } + + // Получаем идентификатор из URL-пути + vars := mux.Vars(r) + shortURL := vars["idShortenURL"] + + // Получаем оригинальный URL + originalURL := storage.GetOriginalURL(shortURL) + + if originalURL == "" { + http.Error(w, "Ссылка не найдена", http.StatusBadRequest) + return + } + // Устанавливаем заголовок Location и возвращаем ответ с кодом 307 + w.Header().Set("Location", originalURL) + w.WriteHeader(http.StatusTemporaryRedirect) + + } +} + +type ShortenURLHandler struct{} + +func (sh *ShortenURLHandler) HandleShortenURL(storage *urlstorage.URLStorage, c *envirements.EnvConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Читаем тело запроса (URL) + defer r.Body.Close() + urlBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Ошибка чтения запроса", http.StatusBadRequest) + return + } + + // Генерируем уникальный идентификатор сокращённого URL + shortURL := shotenermaker.GenerateID(urlBytes) + + // Добавляем соответствие в словарь + storage.AddURL(shortURL, string(urlBytes)) + + // Отправляем ответ с сокращённым URL + shortenedURL := fmt.Sprintf("%s/%s", c.ResultURL, shortURL) + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusCreated) + if _, err := io.WriteString(w, shortenedURL); err != nil { + log.Fatal(err) + } + } +} + +func PanicHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/internal/handlers/urlhandlers_test.go b/internal/handlers/urlhandlers_test.go new file mode 100644 index 0000000..c1a77b6 --- /dev/null +++ b/internal/handlers/urlhandlers_test.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "github.com/nabbat/23_kogorta_shotener/internal/envirements" + urlstorage "github.com/nabbat/23_kogorta_shotener/internal/storage" + "net/http" + "reflect" + "testing" +) + +func TestPanicHandler(t *testing.T) { + type args struct { + next http.Handler + } + tests := []struct { + name string + args args + want http.Handler + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := PanicHandler(tt.args.next); !reflect.DeepEqual(got, tt.want) { + t.Errorf("PanicHandler() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRedirectHandler_HandleRedirect(t *testing.T) { + type args struct { + storage *urlstorage.URLStorage + } + tests := []struct { + name string + args args + want http.HandlerFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rh := &RedirectHandler{} + if got := rh.HandleRedirect(tt.args.storage); !reflect.DeepEqual(got, tt.want) { + t.Errorf("HandleRedirect() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShortenURLHandler_HandleShortenURL(t *testing.T) { + type args struct { + storage *urlstorage.URLStorage + c *envirements.EnvConfig + } + tests := []struct { + name string + args args + want http.HandlerFunc + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sh := &ShortenURLHandler{} + if got := sh.HandleShortenURL(tt.args.storage, tt.args.c); !reflect.DeepEqual(got, tt.want) { + t.Errorf("HandleShortenURL() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/shotenermaker/makeurl.go b/internal/shotenermaker/makeurl.go new file mode 100644 index 0000000..fbde6e4 --- /dev/null +++ b/internal/shotenermaker/makeurl.go @@ -0,0 +1,13 @@ +package shotenermaker + +import "encoding/base64" + +// GenerateID Функция для генерации уникального идентификатора +func GenerateID(fullURL []byte) string { + encodedStr := base64.URLEncoding.EncodeToString(fullURL) + // Возвращаем первые 6 символов закодированной строки + if len(encodedStr) > 6 { + return encodedStr[:6] + } + return encodedStr +} diff --git a/internal/shotenermaker/makeurl_test.go b/internal/shotenermaker/makeurl_test.go new file mode 100644 index 0000000..43ba915 --- /dev/null +++ b/internal/shotenermaker/makeurl_test.go @@ -0,0 +1,23 @@ +package shotenermaker + +import "testing" + +func TestGenerateID(t *testing.T) { + type args struct { + fullURL []byte + } + tests := []struct { + name string + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenerateID(tt.args.fullURL); got != tt.want { + t.Errorf("GenerateID() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/storage/urlstorage.go b/internal/storage/urlstorage.go new file mode 100644 index 0000000..7048c25 --- /dev/null +++ b/internal/storage/urlstorage.go @@ -0,0 +1,24 @@ +package storage + +type URLStorage struct { + urlMap map[string]string +} + +func NewURLStorage() *URLStorage { + storage := &URLStorage{ + urlMap: make(map[string]string), + } + // Добавляем тестовое соответствие + storage.AddURL("aHR0cH", "https://practicum.yandex.ru/") + return storage +} + +// AddURL добавляет пару сокращенный URL -> оригинальный URL +func (storage *URLStorage) AddURL(shortURL, originalURL string) { + storage.urlMap[shortURL] = originalURL +} + +// GetOriginalURL возвращает оригинальный URL по сокращенному URL +func (storage *URLStorage) GetOriginalURL(shortURL string) string { + return storage.urlMap[shortURL] +} diff --git a/internal/storage/urlstorage_test.go b/internal/storage/urlstorage_test.go new file mode 100644 index 0000000..0f6ab6a --- /dev/null +++ b/internal/storage/urlstorage_test.go @@ -0,0 +1,74 @@ +package storage + +import ( + "reflect" + "testing" +) + +func TestNewURLStorage(t *testing.T) { + tests := []struct { + name string + want *URLStorage + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewURLStorage(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewURLStorage() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestURLStorage_AddURL(t *testing.T) { + type fields struct { + urlMap map[string]string + } + type args struct { + shortURL string + originalURL string + } + tests := []struct { + name string + fields fields + args args + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + storage := &URLStorage{ + urlMap: tt.fields.urlMap, + } + storage.AddURL(tt.args.shortURL, tt.args.originalURL) + }) + } +} + +func TestURLStorage_GetOriginalURL(t *testing.T) { + type fields struct { + urlMap map[string]string + } + type args struct { + shortURL string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + storage := &URLStorage{ + urlMap: tt.fields.urlMap, + } + if got := storage.GetOriginalURL(tt.args.shortURL); got != tt.want { + t.Errorf("GetOriginalURL() = %v, want %v", got, tt.want) + } + }) + } +}