├── api ├── proto │ └── .gitkeep ├── README.md └── rest │ └── swagger.yml ├── tools └── http │ ├── http-client.env.json │ └── endpoints.http ├── pkg ├── postgresql │ ├── config.go │ ├── tx │ │ ├── dependency.go │ │ ├── manager.go │ │ ├── manager_test.go │ │ └── dependency_mock.go │ ├── pgxpool.go │ └── pool.go └── logging │ └── logger.go ├── internal ├── service │ ├── shrink │ │ ├── config.go │ │ ├── dependency.go │ │ ├── shrink.go │ │ ├── shrink_test.go │ │ └── dependency_mock.go │ ├── server │ │ └── rest │ │ │ ├── middleware │ │ │ ├── cors │ │ │ │ ├── config.go │ │ │ │ └── middleware.go │ │ │ ├── config.go │ │ │ └── factory.go │ │ │ ├── config.go │ │ │ ├── mux_adaptor.go │ │ │ ├── gen │ │ │ ├── restapi │ │ │ │ ├── doc.go │ │ │ │ ├── operations │ │ │ │ │ ├── post_shrink.go │ │ │ │ │ ├── get_short_code.go │ │ │ │ │ ├── post_shrink_parameters.go │ │ │ │ │ ├── post_shrink_urlbuilder.go │ │ │ │ │ ├── get_short_code_parameters.go │ │ │ │ │ ├── get_short_code_urlbuilder.go │ │ │ │ │ ├── post_shrink_responses.go │ │ │ │ │ ├── get_short_code_responses.go │ │ │ │ │ └── urlshortener_api.go │ │ │ │ ├── configure_urlshortener.go │ │ │ │ ├── embedded_spec.go │ │ │ │ └── server.go │ │ │ └── models │ │ │ │ ├── validation_error.go │ │ │ │ ├── redirect_url.go │ │ │ │ ├── not_found_error.go │ │ │ │ ├── short_link.go │ │ │ │ ├── internal_error.go │ │ │ │ └── source_link.go │ │ │ ├── handler │ │ │ └── shrink │ │ │ │ ├── mapper.go │ │ │ │ ├── dependency.go │ │ │ │ └── handler.go │ │ │ ├── server.go │ │ │ └── error_handler.go │ ├── statistics │ │ ├── dependency.go │ │ └── service.go │ └── application │ │ └── service.go ├── storage │ └── postgresql │ │ ├── migrator │ │ ├── embed.go │ │ ├── config.go │ │ ├── 20220220183237_create_table_link.sql │ │ ├── 20220220234001_create_table_redirect_statistics.sql │ │ ├── logger_adaptor.go │ │ └── migrator.go │ │ ├── error.go │ │ ├── statistics │ │ └── repository.go │ │ └── link │ │ └── repository.go ├── model │ └── link.go ├── config │ └── config.go └── di │ ├── wire.go │ └── wire_gen.go ├── docker-compose.yml ├── .gitignore ├── config.yml.dist ├── Makefile ├── .github └── workflows │ └── go.yml ├── README.md ├── .golangci.yml ├── cmd └── urlshortener │ └── main.go └── go.mod /api/proto/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/http/http-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "host": "http://127.0.0.1:44444", 4 | } 5 | } -------------------------------------------------------------------------------- /pkg/postgresql/config.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | type Config struct { 4 | DSN string `yaml:"dsn"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/service/shrink/config.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | type Config struct { 4 | ShortUrlTemplate string `yaml:"short_url_template"` 5 | } 6 | -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/embed.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import "embed" 4 | 5 | //go:embed *.sql 6 | var migrations embed.FS 7 | -------------------------------------------------------------------------------- /internal/service/server/rest/middleware/cors/config.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | type Config struct { 4 | AllowedOrigins []string `yaml:"allowed_origins"` 5 | } 6 | -------------------------------------------------------------------------------- /tools/http/endpoints.http: -------------------------------------------------------------------------------- 1 | ### Build graph 2 | POST {{host}}/v1/shrink 3 | Content-Type: application/json 4 | 5 | {"long_url": "https://www.blabla.com/very_long_url"} -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/config.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | type Config struct { 4 | DSN string `yaml:"dsn"` 5 | MigrationsTableName string `yaml:"db_version"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/service/server/rest/middleware/config.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware/cors" 4 | 5 | type Config struct { 6 | Cors cors.Config `yaml:"cors"` 7 | } 8 | -------------------------------------------------------------------------------- /internal/service/statistics/dependency.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import "context" 4 | 5 | type ( 6 | StatisticsRepository interface { 7 | SaveRedirectEvent(ctx context.Context, code string, userAgent string) error 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /internal/service/server/rest/middleware/factory.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware/cors" 5 | ) 6 | 7 | type Factory struct { 8 | Cors *cors.Middleware 9 | } 10 | -------------------------------------------------------------------------------- /internal/service/server/rest/config.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware" 4 | 5 | type Config struct { 6 | Port int `yaml:"port"` 7 | Middleware middleware.Config `yaml:"middleware"` 8 | } 9 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | This is the specification directory. In the real projects it should be in the separate repository and included 2 | as a Git Submodule. 3 | The specification is used to generate the API documentation and client SDKs. So it should be shared between projects 4 | for ease of code generation. -------------------------------------------------------------------------------- /internal/model/link.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "time" 4 | 5 | type Link struct { 6 | ID int 7 | Code string // Код сокращенного URL 8 | LongURL string // Исходный URL 9 | ShortURL string // Сокращённый URL 10 | Tags []string 11 | CreatedAt time.Time 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | db: 5 | image: postgres:14 6 | ports: 7 | - 771:5432 8 | environment: 9 | - POSTGRES_USER=urlshortener 10 | - POSTGRES_PASSWORD=12345 11 | - PGDATA=/var/lib/postgresql/data/pgdata 12 | command: ["postgres", "-c", "log_statement=all"] 13 | -------------------------------------------------------------------------------- /internal/service/server/rest/mux_adaptor.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import "net/http" 4 | 5 | type MuxAdaptor struct { 6 | fn http.HandlerFunc 7 | } 8 | 9 | func NewMuxHandler(fn http.HandlerFunc) *MuxAdaptor { 10 | return &MuxAdaptor{ 11 | fn: fn, 12 | } 13 | } 14 | 15 | func (h *MuxAdaptor) ServeHTTP(w http.ResponseWriter, r *http.Request) { 16 | h.fn(w, r) 17 | } 18 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/doc.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | // Package restapi Urlshortener 4 | // 5 | // Schemes: 6 | // http 7 | // Host: localhost 8 | // BasePath: /v1 9 | // Version: 0.0.1 10 | // 11 | // Consumes: 12 | // - application/json 13 | // 14 | // Produces: 15 | // - application/json 16 | // 17 | // swagger:meta 18 | package restapi 19 | -------------------------------------------------------------------------------- /internal/service/server/rest/handler/shrink/mapper.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | import ( 4 | "github.com/dimuska139/urlshortener/internal/model" 5 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 6 | ) 7 | 8 | func mapLinkToResponse(link model.Link) *models.ShortLink { 9 | return &models.ShortLink{ 10 | ShortURL: link.ShortURL, 11 | LongURL: link.LongURL, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/20220220183237_create_table_link.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | CREATE TABLE link ( 4 | id BIGSERIAL PRIMARY KEY, 5 | full_url text NOT NULL, 6 | code text UNIQUE, 7 | created_at timestamptz NOT NULL 8 | ); 9 | -- +goose StatementEnd 10 | 11 | -- +goose Down 12 | -- +goose StatementBegin 13 | DROP TABLE link; 14 | -- +goose StatementEnd 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Go workspace file 18 | go.work 19 | 20 | .idea 21 | config.yml -------------------------------------------------------------------------------- /config.yml.dist: -------------------------------------------------------------------------------- 1 | env: dev 2 | loglevel: debug 3 | shrink: 4 | short_url_template: http://localhost:44444/%s 5 | postgresql: 6 | dsn: postgres://urlshortener:12345@localhost:771/urlshortener?sslmode=disable 7 | migrator: 8 | dsn: postgres://urlshortener:12345@localhost:771/urlshortener?sslmode=disable 9 | http_server: 10 | port: 44444 11 | version: 1.0.0 12 | middleware: 13 | cors: 14 | allowed_origins: 15 | - "*" -------------------------------------------------------------------------------- /internal/storage/postgresql/error.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import "fmt" 4 | 5 | type ErrQueryExecutionFailed struct { 6 | Query string 7 | Args []any 8 | Err error 9 | } 10 | 11 | func (e *ErrQueryExecutionFailed) Error() string { 12 | return fmt.Sprintf("query execution failed: %s", e.Err.Error()) 13 | } 14 | 15 | func NewErrQueryExecutionFailed(query string, args []any, err error) *ErrQueryExecutionFailed { 16 | return &ErrQueryExecutionFailed{ 17 | Query: query, 18 | Args: args, 19 | Err: err, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run_containers: 2 | docker compose -f ./docker-compose.yml up --remove-orphans 3 | 4 | test_unit: 5 | go test ./... 6 | 7 | migrate: 8 | go run ./cmd/urlshortener/main.go migrate 9 | 10 | run: 11 | go run ./cmd/urlshortener/main.go 12 | 13 | generate_server: 14 | swagger generate server serp -f ./api/rest/swagger.yml --target ./internal/service/server/rest/gen --exclude-main --skip-tag-packages 15 | 16 | validate_server: 17 | swagger validate ./api/rest/swagger.yml 18 | 19 | wire: 20 | wire gen github.com/dimuska139/urlshortener/internal/di 21 | 22 | lint: 23 | golangci-lint run ./... -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/20220220234001_create_table_redirect_statistics.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | -- +goose StatementBegin 3 | CREATE TABLE redirect_statistics ( 4 | id BIGSERIAL PRIMARY KEY, 5 | link_id INT NOT NULL, 6 | user_agent TEXT, 7 | ip INET, 8 | created_at timestamptz NOT NULL, 9 | CONSTRAINT statistics_links_fkey 10 | FOREIGN KEY(link_id) 11 | REFERENCES link(id) 12 | ON DELETE CASCADE 13 | ); 14 | -- +goose StatementEnd 15 | 16 | -- +goose Down 17 | -- +goose StatementBegin 18 | DROP TABLE redirect_statistics; 19 | -- +goose StatementEnd 20 | -------------------------------------------------------------------------------- /internal/service/server/rest/handler/shrink/dependency.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dimuska139/urlshortener/internal/model" 7 | ) 8 | 9 | //go:generate mockgen -source=dependency.go -destination=./dependency_mock.go -package=shrink 10 | 11 | type ( 12 | StatisticsService interface { 13 | SaveRedirectEvent(ctx context.Context, code string, userAgent string) error 14 | } 15 | 16 | ShrinkService interface { 17 | CreateShortCode(ctx context.Context, longUrl string) (model.Link, error) 18 | GetLongUrlByCode(ctx context.Context, shortCode string) (string, error) 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /internal/service/statistics/service.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | type StatisticsService struct { 9 | statisticsRepository StatisticsRepository 10 | } 11 | 12 | func NewStatisticsService(rep StatisticsRepository) *StatisticsService { 13 | return &StatisticsService{ 14 | statisticsRepository: rep, 15 | } 16 | } 17 | 18 | func (s *StatisticsService) SaveRedirectEvent(ctx context.Context, code string, userAgent string) error { 19 | if err := s.statisticsRepository.SaveRedirectEvent(ctx, code, userAgent); err != nil { 20 | return fmt.Errorf("save redirect event: %w", err) 21 | } 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/service/shrink/dependency.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | //go:generate mockgen -source=dependency.go -destination=./dependency_mock.go -package=shrink 4 | 5 | import ( 6 | "context" 7 | "github.com/jackc/pgx/v5" 8 | 9 | "github.com/dimuska139/urlshortener/internal/model" 10 | ) 11 | 12 | type ( 13 | TransactionManager interface { 14 | WithTx(ctx context.Context, fn func(ctx context.Context) error, opts pgx.TxOptions) error 15 | } 16 | 17 | LinkRepository interface { 18 | Create(ctx context.Context, longUrl string) (model.Link, error) 19 | SetShortcode(ctx context.Context, id int, shortcode string) error 20 | GetLongUrlByCode(ctx context.Context, shortCode string) (string, error) 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and upload coverage 2 | 3 | on: 4 | push 5 | 6 | jobs: 7 | lint: 8 | name: Run linters 9 | uses: golangci/golangci-lint-action@v6.2.0 10 | build: 11 | name: Run tests and collect coverage 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | 19 | - name: Install dependencies 20 | run: go mod download 21 | 22 | - name: Run tests with coverage 23 | run: go test -v -coverprofile=coverage.txt ./... 24 | 25 | - name: Upload coverage reports to Codecov 26 | uses: codecov/codecov-action@v5 27 | with: 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | slug: dimuska139/go-api-layout -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/validation_error.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/strfmt" 12 | ) 13 | 14 | // ValidationError validation error 15 | // 16 | // swagger:model ValidationError 17 | type ValidationError map[string]string 18 | 19 | // Validate validates this validation error 20 | func (m ValidationError) Validate(formats strfmt.Registry) error { 21 | return nil 22 | } 23 | 24 | // ContextValidate validates this validation error based on context it is used 25 | func (m ValidationError) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/service/application/service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dimuska139/urlshortener/internal/service/server/rest" 7 | "github.com/dimuska139/urlshortener/pkg/logging" 8 | ) 9 | 10 | type Application struct { 11 | restAPI *rest.Server 12 | } 13 | 14 | func NewApplication( 15 | restAPI *rest.Server) *Application { 16 | return &Application{ 17 | restAPI: restAPI, 18 | } 19 | } 20 | 21 | func (a *Application) Run(ctx context.Context) { 22 | go func() { 23 | if err := a.restAPI.Start(); err != nil { 24 | logging.Fatal(ctx, "Can't start API server", 25 | "err", err.Error()) 26 | } 27 | }() 28 | } 29 | 30 | func (a *Application) Stop(ctx context.Context) { 31 | if err := a.restAPI.Stop(ctx); err != nil { 32 | logging.Fatal(ctx, "Can't stop API server", 33 | "err", err.Error()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/service/server/rest/middleware/cors/middleware.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "github.com/rs/cors" 5 | "net/http" 6 | ) 7 | 8 | type Middleware struct { 9 | config Config 10 | } 11 | 12 | func NewMiddleware(config Config) *Middleware { 13 | return &Middleware{ 14 | config: config, 15 | } 16 | } 17 | 18 | func (m *Middleware) GetMiddleware() func(http.Handler) http.Handler { 19 | return func(next http.Handler) http.Handler { 20 | allowedOrigins := []string{"*"} 21 | if len(m.config.AllowedOrigins) != 0 { 22 | allowedOrigins = m.config.AllowedOrigins 23 | } 24 | 25 | return cors.New(cors.Options{ 26 | AllowedOrigins: allowedOrigins, 27 | AllowedHeaders: []string{"*"}, 28 | AllowedMethods: []string{ 29 | http.MethodHead, 30 | http.MethodPost, 31 | http.MethodGet, 32 | http.MethodPut, 33 | http.MethodDelete, 34 | }, 35 | }).Handler(next) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/logger_adaptor.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/dimuska139/urlshortener/pkg/logging" 8 | ) 9 | 10 | type LoggerAdaptor struct { 11 | } 12 | 13 | func NewLoggerAdaptor() *LoggerAdaptor { 14 | return &LoggerAdaptor{} 15 | } 16 | 17 | func (l *LoggerAdaptor) Fatal(v ...any) { 18 | logging.Fatal(context.Background(), "", v...) 19 | } 20 | 21 | func (l *LoggerAdaptor) Fatalf(format string, v ...any) { 22 | logging.Fatal(context.Background(), fmt.Sprintf(format, v...)) 23 | } 24 | 25 | func (l *LoggerAdaptor) Print(v ...any) { 26 | logging.Fatal(context.Background(), "", v...) 27 | } 28 | 29 | func (l *LoggerAdaptor) Println(v ...any) { 30 | logging.Info(context.Background(), "", v...) 31 | } 32 | 33 | func (l *LoggerAdaptor) Printf(format string, v ...any) { 34 | logging.Info(context.Background(), fmt.Sprintf(format, v...)) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/postgresql/tx/dependency.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source=dependency.go -destination=./dependency_mock.go -package=tx 2 | 3 | package tx 4 | 5 | import ( 6 | "context" 7 | "github.com/jackc/pgx/v5" 8 | "github.com/jackc/pgx/v5/pgconn" 9 | ) 10 | 11 | type Pool interface { 12 | BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) 13 | } 14 | 15 | type Tx interface { 16 | Begin(ctx context.Context) (pgx.Tx, error) 17 | Commit(ctx context.Context) error 18 | Rollback(ctx context.Context) error 19 | CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) 20 | SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults 21 | LargeObjects() pgx.LargeObjects 22 | Prepare(ctx context.Context, name, sql string) (*pgconn.StatementDescription, error) 23 | Exec(ctx context.Context, sql string, arguments ...any) (commandTag pgconn.CommandTag, err error) 24 | Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) 25 | QueryRow(ctx context.Context, sql string, args ...any) pgx.Row 26 | Conn() *pgx.Conn 27 | } 28 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ilyakaznacheev/cleanenv" 6 | 7 | "github.com/dimuska139/urlshortener/internal/service/server/rest" 8 | "github.com/dimuska139/urlshortener/internal/service/shrink" 9 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/migrator" 10 | "github.com/dimuska139/urlshortener/pkg/postgresql" 11 | ) 12 | 13 | type Config struct { 14 | Version string 15 | 16 | Loglevel string `yaml:"loglevel"` 17 | Env string `yaml:"env"` 18 | Shrink shrink.Config `yaml:"shrink"` 19 | PostgreSQL postgresql.Config `yaml:"postgresql"` 20 | Migrator migrator.Config `yaml:"migrator"` 21 | HttpServer rest.Config `yaml:"http_server"` 22 | } 23 | 24 | type VersionParam string 25 | 26 | func NewConfig(configPath string, version VersionParam) (*Config, error) { 27 | var cfg Config 28 | if err := cleanenv.ReadConfig(configPath, &cfg); err != nil { 29 | return nil, fmt.Errorf("read config: %w", err) 30 | } 31 | 32 | cfg.Version = string(version) 33 | 34 | return &cfg, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/storage/postgresql/statistics/repository.go: -------------------------------------------------------------------------------- 1 | package statistics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | sq "github.com/Masterminds/squirrel" 7 | 8 | db "github.com/dimuska139/urlshortener/internal/storage/postgresql" 9 | "github.com/dimuska139/urlshortener/pkg/postgresql" 10 | ) 11 | 12 | type Repository struct { 13 | pgPool *postgresql.PostgresPool 14 | qb sq.StatementBuilderType 15 | } 16 | 17 | func NewRepository(pgPool *postgresql.PostgresPool) *Repository { 18 | return &Repository{ 19 | pgPool: pgPool, 20 | qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), 21 | } 22 | } 23 | 24 | func (r *Repository) SaveRedirectEvent(ctx context.Context, code string, userAgent string) error { 25 | sql, args, err := r.qb.Insert("redirect_statistics"). 26 | Columns("link_id", "user_agent", "created_at"). 27 | Values(sq.Expr("(SELECT id FROM link WHERE code=?)", code), userAgent, "now()"). 28 | ToSql() 29 | if err != nil { 30 | return fmt.Errorf("build query: %w", err) 31 | } 32 | 33 | if _, err := r.pgPool.Exec(ctx, sql, args...); err != nil { 34 | return db.NewErrQueryExecutionFailed(sql, args, err) 35 | } 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/postgresql/pgxpool.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5/pgxpool" 7 | "time" 8 | 9 | "github.com/dimuska139/urlshortener/pkg/logging" 10 | ) 11 | 12 | const ( 13 | DbPoolIdleConns = 2 14 | DbPoolMaxConns = 10 15 | DbPoolHealthcheckPeriod = 3 * time.Second 16 | DbMaxConnIdleTime = 10 * time.Second 17 | DbMaxConnLifetime = 1 * time.Minute 18 | ) 19 | 20 | func NewPgxPool(conf Config) (*pgxpool.Pool, func(), error) { 21 | poolConfig, err := pgxpool.ParseConfig(conf.DSN) 22 | if err != nil { 23 | return nil, nil, fmt.Errorf("parse database dsn: %w", err) 24 | } 25 | 26 | poolConfig.HealthCheckPeriod = DbPoolHealthcheckPeriod 27 | poolConfig.MaxConnIdleTime = DbMaxConnIdleTime 28 | poolConfig.MaxConnLifetime = DbMaxConnLifetime 29 | poolConfig.MaxConns = DbPoolMaxConns 30 | poolConfig.MinConns = DbPoolIdleConns 31 | 32 | pool, err := pgxpool.NewWithConfig(context.Background(), poolConfig) 33 | if err != nil { 34 | return nil, nil, fmt.Errorf("connect to PostgreSQL: %w", err) 35 | } 36 | 37 | return pool, func() { 38 | logging.Info(context.Background(), "Closing PostgreSQL pool...") 39 | pool.Close() 40 | }, nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/redirect_url.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/strfmt" 12 | "github.com/go-openapi/swag" 13 | ) 14 | 15 | // RedirectURL URL для редиректа 16 | // 17 | // swagger:model RedirectURL 18 | type RedirectURL struct { 19 | 20 | // long url 21 | LongURL string `json:"long_url,omitempty"` 22 | } 23 | 24 | // Validate validates this redirect URL 25 | func (m *RedirectURL) Validate(formats strfmt.Registry) error { 26 | return nil 27 | } 28 | 29 | // ContextValidate validates this redirect URL based on context it is used 30 | func (m *RedirectURL) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 31 | return nil 32 | } 33 | 34 | // MarshalBinary interface implementation 35 | func (m *RedirectURL) MarshalBinary() ([]byte, error) { 36 | if m == nil { 37 | return nil, nil 38 | } 39 | return swag.WriteJSON(m) 40 | } 41 | 42 | // UnmarshalBinary interface implementation 43 | func (m *RedirectURL) UnmarshalBinary(b []byte) error { 44 | var res RedirectURL 45 | if err := swag.ReadJSON(b, &res); err != nil { 46 | return err 47 | } 48 | *m = res 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/not_found_error.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/strfmt" 12 | "github.com/go-openapi/swag" 13 | ) 14 | 15 | // NotFoundError Not found 16 | // 17 | // swagger:model NotFoundError 18 | type NotFoundError struct { 19 | 20 | // common 21 | // Example: Link is not found 22 | Common string `json:"common,omitempty"` 23 | } 24 | 25 | // Validate validates this not found error 26 | func (m *NotFoundError) Validate(formats strfmt.Registry) error { 27 | return nil 28 | } 29 | 30 | // ContextValidate validates this not found error based on context it is used 31 | func (m *NotFoundError) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 32 | return nil 33 | } 34 | 35 | // MarshalBinary interface implementation 36 | func (m *NotFoundError) MarshalBinary() ([]byte, error) { 37 | if m == nil { 38 | return nil, nil 39 | } 40 | return swag.WriteJSON(m) 41 | } 42 | 43 | // UnmarshalBinary interface implementation 44 | func (m *NotFoundError) UnmarshalBinary(b []byte) error { 45 | var res NotFoundError 46 | if err := swag.ReadJSON(b, &res); err != nil { 47 | return err 48 | } 49 | *m = res 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/short_link.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/strfmt" 12 | "github.com/go-openapi/swag" 13 | ) 14 | 15 | // ShortLink Short URL 16 | // 17 | // swagger:model ShortLink 18 | type ShortLink struct { 19 | 20 | // long url 21 | LongURL string `json:"long_url,omitempty"` 22 | 23 | // short url 24 | ShortURL string `json:"short_url,omitempty"` 25 | } 26 | 27 | // Validate validates this short link 28 | func (m *ShortLink) Validate(formats strfmt.Registry) error { 29 | return nil 30 | } 31 | 32 | // ContextValidate validates this short link based on context it is used 33 | func (m *ShortLink) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 34 | return nil 35 | } 36 | 37 | // MarshalBinary interface implementation 38 | func (m *ShortLink) MarshalBinary() ([]byte, error) { 39 | if m == nil { 40 | return nil, nil 41 | } 42 | return swag.WriteJSON(m) 43 | } 44 | 45 | // UnmarshalBinary interface implementation 46 | func (m *ShortLink) UnmarshalBinary(b []byte) error { 47 | var res ShortLink 48 | if err := swag.ReadJSON(b, &res); err != nil { 49 | return err 50 | } 51 | *m = res 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/internal_error.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/strfmt" 12 | "github.com/go-openapi/swag" 13 | ) 14 | 15 | // InternalError internal error 16 | // 17 | // swagger:model InternalError 18 | type InternalError struct { 19 | 20 | // common 21 | // Example: Something went wrong 22 | Common string `json:"common,omitempty"` 23 | } 24 | 25 | // Validate validates this internal error 26 | func (m *InternalError) Validate(formats strfmt.Registry) error { 27 | return nil 28 | } 29 | 30 | // ContextValidate validates this internal error based on context it is used 31 | func (m *InternalError) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 32 | return nil 33 | } 34 | 35 | // MarshalBinary interface implementation 36 | func (m *InternalError) MarshalBinary() ([]byte, error) { 37 | if m == nil { 38 | return nil, nil 39 | } 40 | return swag.WriteJSON(m) 41 | } 42 | 43 | // UnmarshalBinary interface implementation 44 | func (m *InternalError) UnmarshalBinary(b []byte) error { 45 | var res InternalError 46 | if err := swag.ReadJSON(b, &res); err != nil { 47 | return err 48 | } 49 | *m = res 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/storage/postgresql/migrator/migrator.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | _ "github.com/lib/pq" 8 | "github.com/pressly/goose/v3" 9 | 10 | "github.com/dimuska139/urlshortener/pkg/logging" 11 | ) 12 | 13 | const ( 14 | defaultMigrationsTableName = "db_version" 15 | ) 16 | 17 | type Migrator struct { 18 | connection *sql.DB 19 | } 20 | 21 | func NewMigrator(config Config) (*Migrator, func(), error) { 22 | if config.MigrationsTableName == "" { 23 | config.MigrationsTableName = defaultMigrationsTableName 24 | } 25 | 26 | connection, err := sql.Open("postgres", config.DSN) 27 | if err != nil { 28 | return nil, nil, fmt.Errorf("open database connection: %w", err) 29 | } 30 | 31 | migrationsTableName := defaultMigrationsTableName 32 | if config.MigrationsTableName != "" { 33 | migrationsTableName = config.MigrationsTableName 34 | } 35 | 36 | goose.SetTableName(migrationsTableName) 37 | goose.SetBaseFS(migrations) 38 | goose.SetLogger(NewLoggerAdaptor()) 39 | 40 | return &Migrator{ 41 | connection: connection, 42 | }, func() { 43 | if err := connection.Close(); err != nil { 44 | logging.Error(context.Background(), "Can't close database connection", 45 | "err", err.Error()) 46 | } 47 | }, nil 48 | } 49 | 50 | func (m *Migrator) Up() error { 51 | if err := goose.Up(m.connection, ".", goose.WithAllowMissing()); err != nil { 52 | return fmt.Errorf("apply migrations: %w", err) 53 | } 54 | 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/models/source_link.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package models 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/go-openapi/errors" 12 | "github.com/go-openapi/strfmt" 13 | "github.com/go-openapi/swag" 14 | "github.com/go-openapi/validate" 15 | ) 16 | 17 | // SourceLink Source URL 18 | // 19 | // swagger:model SourceLink 20 | type SourceLink struct { 21 | 22 | // long url 23 | // Required: true 24 | LongURL *string `json:"long_url"` 25 | } 26 | 27 | // Validate validates this source link 28 | func (m *SourceLink) Validate(formats strfmt.Registry) error { 29 | var res []error 30 | 31 | if err := m.validateLongURL(formats); err != nil { 32 | res = append(res, err) 33 | } 34 | 35 | if len(res) > 0 { 36 | return errors.CompositeValidationError(res...) 37 | } 38 | return nil 39 | } 40 | 41 | func (m *SourceLink) validateLongURL(formats strfmt.Registry) error { 42 | 43 | if err := validate.Required("long_url", "body", m.LongURL); err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | // ContextValidate validates this source link based on context it is used 51 | func (m *SourceLink) ContextValidate(ctx context.Context, formats strfmt.Registry) error { 52 | return nil 53 | } 54 | 55 | // MarshalBinary interface implementation 56 | func (m *SourceLink) MarshalBinary() ([]byte, error) { 57 | if m == nil { 58 | return nil, nil 59 | } 60 | return swag.WriteJSON(m) 61 | } 62 | 63 | // UnmarshalBinary interface implementation 64 | func (m *SourceLink) UnmarshalBinary(b []byte) error { 65 | var res SourceLink 66 | if err := swag.ReadJSON(b, &res); err != nil { 67 | return err 68 | } 69 | *m = res 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/post_shrink.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the generate command 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-openapi/runtime/middleware" 12 | ) 13 | 14 | // PostShrinkHandlerFunc turns a function with the right signature into a post shrink handler 15 | type PostShrinkHandlerFunc func(PostShrinkParams) middleware.Responder 16 | 17 | // Handle executing the request and returning a response 18 | func (fn PostShrinkHandlerFunc) Handle(params PostShrinkParams) middleware.Responder { 19 | return fn(params) 20 | } 21 | 22 | // PostShrinkHandler interface for that can handle valid post shrink params 23 | type PostShrinkHandler interface { 24 | Handle(PostShrinkParams) middleware.Responder 25 | } 26 | 27 | // NewPostShrink creates a new http.Handler for the post shrink operation 28 | func NewPostShrink(ctx *middleware.Context, handler PostShrinkHandler) *PostShrink { 29 | return &PostShrink{Context: ctx, Handler: handler} 30 | } 31 | 32 | /* 33 | PostShrink swagger:route POST /shrink postShrink 34 | 35 | Creates a short URL from a long URL 36 | */ 37 | type PostShrink struct { 38 | Context *middleware.Context 39 | Handler PostShrinkHandler 40 | } 41 | 42 | func (o *PostShrink) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 43 | route, rCtx, _ := o.Context.RouteInfo(r) 44 | if rCtx != nil { 45 | *r = *rCtx 46 | } 47 | var Params = NewPostShrinkParams() 48 | if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params 49 | o.Context.Respond(rw, r, route.Produces, route, err) 50 | return 51 | } 52 | 53 | res := o.Handler.Handle(Params) // actually handle the request 54 | o.Context.Respond(rw, r, route.Produces, route, res) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/get_short_code.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the generate command 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-openapi/runtime/middleware" 12 | ) 13 | 14 | // GetShortCodeHandlerFunc turns a function with the right signature into a get short code handler 15 | type GetShortCodeHandlerFunc func(GetShortCodeParams) middleware.Responder 16 | 17 | // Handle executing the request and returning a response 18 | func (fn GetShortCodeHandlerFunc) Handle(params GetShortCodeParams) middleware.Responder { 19 | return fn(params) 20 | } 21 | 22 | // GetShortCodeHandler interface for that can handle valid get short code params 23 | type GetShortCodeHandler interface { 24 | Handle(GetShortCodeParams) middleware.Responder 25 | } 26 | 27 | // NewGetShortCode creates a new http.Handler for the get short code operation 28 | func NewGetShortCode(ctx *middleware.Context, handler GetShortCodeHandler) *GetShortCode { 29 | return &GetShortCode{Context: ctx, Handler: handler} 30 | } 31 | 32 | /* 33 | GetShortCode swagger:route GET /{shortCode} getShortCode 34 | 35 | Represents a short URL. Tracks the visit and redirects to the corresponding long URL 36 | */ 37 | type GetShortCode struct { 38 | Context *middleware.Context 39 | Handler GetShortCodeHandler 40 | } 41 | 42 | func (o *GetShortCode) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 43 | route, rCtx, _ := o.Context.RouteInfo(r) 44 | if rCtx != nil { 45 | *r = *rCtx 46 | } 47 | var Params = NewGetShortCodeParams() 48 | if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params 49 | o.Context.Respond(rw, r, route.Produces, route, err) 50 | return 51 | } 52 | 53 | res := o.Handler.Handle(Params) // actually handle the request 54 | o.Context.Respond(rw, r, route.Produces, route, res) 55 | 56 | } 57 | -------------------------------------------------------------------------------- /pkg/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "os" 7 | ) 8 | 9 | type LogLevel string 10 | 11 | const ( 12 | LogLevelDebug LogLevel = "debug" 13 | LogLevelInfo LogLevel = "info" 14 | LogLevelWarn LogLevel = "warn" 15 | LogLevelError LogLevel = "error" 16 | ) 17 | 18 | var logger *slog.Logger 19 | 20 | func getSlogLevel(level LogLevel) slog.Level { 21 | switch level { 22 | case LogLevelDebug: 23 | return slog.LevelDebug 24 | 25 | case LogLevelInfo: 26 | return slog.LevelInfo 27 | 28 | case LogLevelWarn: 29 | return slog.LevelWarn 30 | 31 | case LogLevelError: 32 | return slog.LevelError 33 | 34 | default: 35 | return slog.LevelInfo 36 | } 37 | } 38 | 39 | func InitLogger(loglevel LogLevel) { 40 | var opts *slog.HandlerOptions 41 | if loglevel != "" { 42 | opts = &slog.HandlerOptions{ 43 | Level: getSlogLevel(loglevel), 44 | } 45 | } 46 | 47 | handler := slog.NewJSONHandler(os.Stdout, opts) 48 | logger = slog.New(handler) 49 | } 50 | 51 | func Fatal(ctx context.Context, msg string, args ...any) { 52 | if logger == nil { 53 | return 54 | } 55 | 56 | Error(ctx, msg, args...) 57 | 58 | os.Exit(1) 59 | } 60 | 61 | func Write(ctx context.Context, level LogLevel, msg string, args ...any) { 62 | if logger == nil { 63 | return 64 | } 65 | 66 | logger.Log(ctx, getSlogLevel(level), msg, args...) 67 | } 68 | 69 | func Debug(_ context.Context, msg string, args ...any) { 70 | if logger == nil { 71 | return 72 | } 73 | 74 | logger.Debug(msg, args...) 75 | } 76 | 77 | func Info(_ context.Context, msg string, args ...any) { 78 | if logger == nil { 79 | return 80 | } 81 | 82 | logger.Info(msg, args...) 83 | } 84 | 85 | func Warn(_ context.Context, msg string, args ...any) { 86 | if logger == nil { 87 | return 88 | } 89 | 90 | logger.Warn(msg, args...) 91 | } 92 | 93 | func Error(_ context.Context, msg string, args ...any) { 94 | if logger == nil { 95 | return 96 | } 97 | 98 | logger.Error(msg, args...) 99 | } 100 | -------------------------------------------------------------------------------- /internal/service/shrink/shrink.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | 8 | "github.com/dimuska139/urlshortener/internal/model" 9 | ) 10 | 11 | type ShrinkService struct { 12 | config Config 13 | txManager TransactionManager 14 | linkRepository LinkRepository 15 | } 16 | 17 | func NewShrinkService( 18 | config Config, 19 | txManager TransactionManager, 20 | linkRepository LinkRepository, 21 | ) *ShrinkService { 22 | return &ShrinkService{ 23 | config: config, 24 | txManager: txManager, 25 | linkRepository: linkRepository, 26 | } 27 | } 28 | 29 | func generateShortcode(id int) string { 30 | const shortcodeAlphabet = "0123456789abcdefghijklmnopqrstuvwxyz" 31 | 32 | base := len(shortcodeAlphabet) 33 | 34 | var encoded string 35 | for id > 0 { 36 | encoded += string(shortcodeAlphabet[id%base]) 37 | id = id / base 38 | } 39 | 40 | var reversed string 41 | for _, v := range encoded { 42 | reversed = string(v) + reversed 43 | } 44 | 45 | return reversed 46 | } 47 | 48 | func (s *ShrinkService) CreateShortCode(ctx context.Context, longUrl string) (model.Link, error) { 49 | var shortLink model.Link 50 | 51 | if err := s.txManager.WithTx(ctx, func(ctx context.Context) error { 52 | link, err := s.linkRepository.Create(ctx, longUrl) 53 | if err != nil { 54 | return fmt.Errorf("create link: %w", err) 55 | } 56 | 57 | link.Code = generateShortcode(link.ID) 58 | if err := s.linkRepository.SetShortcode(ctx, link.ID, link.Code); err != nil { 59 | return fmt.Errorf("set shortcode: %w", err) 60 | } 61 | 62 | link.ShortURL = fmt.Sprintf(s.config.ShortUrlTemplate, link.Code) 63 | 64 | shortLink = link 65 | 66 | return nil 67 | }, pgx.TxOptions{}); err != nil { 68 | return model.Link{}, fmt.Errorf("tx: %w", err) 69 | } 70 | 71 | return shortLink, nil 72 | } 73 | 74 | func (s *ShrinkService) GetLongUrlByCode(ctx context.Context, shortCode string) (string, error) { 75 | longUrl, err := s.linkRepository.GetLongUrlByCode(ctx, shortCode) 76 | if err != nil { 77 | return "", fmt.Errorf("get long url by code: %w", err) 78 | } 79 | 80 | return longUrl, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/postgresql/tx/manager.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgxpool" 8 | ) 9 | 10 | type contextKey string 11 | 12 | const ( 13 | TransactionContextKey contextKey = "postgresql_transaction" 14 | ) 15 | 16 | type Manager struct { 17 | pool Pool 18 | } 19 | 20 | func NewManager(pool *pgxpool.Pool) *Manager { 21 | return &Manager{ 22 | pool: pool, 23 | } 24 | } 25 | 26 | func (t *Manager) createCtx(ctx context.Context, opts pgx.TxOptions) (context.Context, pgx.Tx, error) { 27 | /* 28 | Обработка случая открытия транзакции внутри другой транзакции 29 | Если в контексте уже есть транзакция, то используем ее 30 | */ 31 | txFromContext := ctx.Value(TransactionContextKey) 32 | if txFromContext != nil { 33 | tx, ok := txFromContext.(pgx.Tx) 34 | if !ok { 35 | return ctx, nil, fmt.Errorf("cast %s to pgx.Tx", TransactionContextKey) 36 | } 37 | 38 | return ctx, tx, nil 39 | } 40 | 41 | tx, err := t.pool.BeginTx(ctx, opts) 42 | if err != nil { 43 | return ctx, nil, fmt.Errorf("begin transaction: %w", err) 44 | } 45 | 46 | return context.WithValue(ctx, TransactionContextKey, tx), tx, nil 47 | } 48 | 49 | func handleTransaction(ctx context.Context, tx pgx.Tx, functionError error) error { 50 | if functionError != nil { 51 | if err := tx.Rollback(ctx); err != nil { 52 | return fmt.Errorf("rollback transaction: %w", err) 53 | } 54 | 55 | return fmt.Errorf("function: %w", functionError) 56 | } 57 | 58 | if err := tx.Commit(ctx); err != nil { 59 | return fmt.Errorf("commit transaction: %w", err) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (t *Manager) WithTx(ctx context.Context, fn func(ctx context.Context) error, opts pgx.TxOptions) error { 66 | isNested := ctx.Value(TransactionContextKey) != nil 67 | 68 | ctx, tx, err := t.createCtx(ctx, opts) 69 | if err != nil { 70 | return fmt.Errorf("create ctx: %w", err) 71 | } 72 | 73 | if isNested { 74 | if err := fn(ctx); err != nil { 75 | return fmt.Errorf("call fn: %w", err) 76 | } 77 | } else { 78 | if err := handleTransaction(ctx, tx, fn(ctx)); err != nil { 79 | return fmt.Errorf("handle transaction: %w", err) 80 | } 81 | } 82 | 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang REST API skeleton 2 | This is a small template of a service written in Go, demonstrating: 3 | 1. My modest vision of a convenient architecture of applications in Go 4 | 2. Interaction with the database and the use of migrations 5 | 3. Generation of API from the Swagger specification (this eliminates the discrepancy between documentation and code) 6 | 4. Using mocks when writing unit tests 7 | 5. Injection of dependencies using Wire 8 | 9 | ## How to run 10 | 1. Copy the `config.yml.dist` file to `config.yml` 11 | 2. Run Docker containers: `make run_containers` 12 | 3. Apply migrations: `make migrate` 13 | 4. Run the service: `make run` 14 | 5. Open the Swagger UI: [http://localhost:44444/v1/docs](http://localhost:44444/v1/docs) 15 | 6. Use Swagger UI to interact with the API 16 | 7. To stop the service, press `Ctrl+C` in the terminal 17 | 18 | ## Dependency injection 19 | Dependency injection is implemented using [Wire](https://github.com/google/wire). 20 | To generate the DI code, run the following command: 21 | ```bash 22 | make wire 23 | ``` 24 | 25 | ## REST API 26 | Code generation is used here from the Swagger 2.0 specification. 27 | It is placed in the [api/rest/swagger.yml](api/rest/swagger.yml) file. 28 | Swagger 2 is used, not OpenAPI 3, because at the moment there are no API code generation 29 | libraries for Go that would fully cover the entire OpenAPI 3 specification. So I 30 | used [go-swagger](https://github.com/go-swagger/go-swagger) here because of it. 31 | 32 | To generate the code from specification, run the following command: 33 | ```bash 34 | make generate_server 35 | ``` 36 | ## Database migrations 37 | Working with the database structure (creating tables, indexes, etc.) is done **strictly through migrations**. 38 | 1. Create a file with the migration, for example: `goose -dir ./internal/storage/postgresql/migrator create create_table_article sql`. 39 | `create_table_article` is an arbitrary migration name. The `sql` argument at the end of the command is needed to generate an SQL migration, not a Go file 40 | 2. In the `./internal/storage/postgresql/migrator/XXX_create_table_article.sql` file, write an SQL query for migration 41 | 3. Build the application 42 | 4. Run the application with the `migrate` flag: `./myapp migrate` 43 | 44 | When developing locally, migrations can be used like this: `make migrate`. 45 | Docker container with PostgreSQL must be running before it (see [docker-compose.yml](docker.compose.yml)). -------------------------------------------------------------------------------- /pkg/postgresql/pool.go: -------------------------------------------------------------------------------- 1 | package postgresql 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/jackc/pgx/v5" 7 | "github.com/jackc/pgx/v5/pgconn" 8 | "github.com/jackc/pgx/v5/pgxpool" 9 | ) 10 | 11 | type PostgresPool struct { 12 | pool *pgxpool.Pool 13 | } 14 | 15 | func newQueryRowErr(err error) QueryRowErr { 16 | return QueryRowErr{ 17 | err: err, 18 | } 19 | } 20 | 21 | type QueryRowErr struct { 22 | err error 23 | } 24 | 25 | func (e QueryRowErr) Scan(dest ...any) error { 26 | return nil 27 | } 28 | 29 | type contextKey string 30 | 31 | const ( 32 | transactionContextKey contextKey = "postgresql_transaction" 33 | ) 34 | 35 | func NewPostgresPool(pool *pgxpool.Pool) (*PostgresPool, error) { 36 | return &PostgresPool{ 37 | pool: pool, 38 | }, nil 39 | } 40 | 41 | func (p *PostgresPool) Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error) { 42 | if ctx.Value(transactionContextKey) == nil { 43 | cmd, err := p.pool.Exec(ctx, sql, args...) 44 | if err != nil { 45 | return pgconn.CommandTag{}, fmt.Errorf("execute: %w", err) 46 | } 47 | 48 | return cmd, nil 49 | } 50 | 51 | tx, ok := ctx.Value(transactionContextKey).(pgx.Tx) 52 | if !ok { 53 | return pgconn.CommandTag{}, fmt.Errorf("cast %s to pgx.Tx", transactionContextKey) 54 | } 55 | 56 | cmd, err := tx.Exec(ctx, sql, args...) 57 | if err != nil { 58 | return pgconn.CommandTag{}, fmt.Errorf("execute: %w", err) 59 | } 60 | 61 | return cmd, nil 62 | } 63 | 64 | func (p *PostgresPool) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { 65 | if ctx.Value(transactionContextKey) == nil { 66 | return p.pool.QueryRow(ctx, sql, args...) 67 | } 68 | 69 | tx, ok := ctx.Value(transactionContextKey).(pgx.Tx) 70 | if !ok { 71 | return newQueryRowErr(fmt.Errorf("cast %s to pgx.Tx", transactionContextKey)) 72 | } 73 | 74 | return tx.QueryRow(ctx, sql, args...) 75 | } 76 | 77 | func (p *PostgresPool) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { 78 | if ctx.Value(transactionContextKey) == nil { 79 | rows, err := p.pool.Query(ctx, sql, args...) 80 | if err != nil { 81 | return nil, fmt.Errorf("query: %w", err) 82 | } 83 | 84 | return rows, nil 85 | } 86 | 87 | tx, ok := ctx.Value(transactionContextKey).(pgx.Tx) 88 | if !ok { 89 | return nil, fmt.Errorf("cast %s to pgx.Tx", transactionContextKey) 90 | } 91 | 92 | rows, err := tx.Query(ctx, sql, args...) 93 | if err != nil { 94 | return nil, fmt.Errorf("query: %w", err) 95 | } 96 | 97 | return rows, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/storage/postgresql/link/repository.go: -------------------------------------------------------------------------------- 1 | package link 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | sq "github.com/Masterminds/squirrel" 8 | "github.com/jackc/pgx/v4" 9 | 10 | "github.com/dimuska139/urlshortener/internal/model" 11 | db "github.com/dimuska139/urlshortener/internal/storage/postgresql" 12 | "github.com/dimuska139/urlshortener/pkg/postgresql" 13 | ) 14 | 15 | type Repository struct { 16 | pgPool *postgresql.PostgresPool 17 | qb sq.StatementBuilderType 18 | } 19 | 20 | func NewRepository(pgPool *postgresql.PostgresPool) *Repository { 21 | return &Repository{ 22 | pgPool: pgPool, 23 | qb: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), 24 | } 25 | } 26 | 27 | // Create сохраняет ссылку в базу данных и возвращает структуру ссылки 28 | func (r *Repository) Create(ctx context.Context, longUrl string) (model.Link, error) { 29 | sql, args, err := r.qb.Insert("link"). 30 | Columns("full_url", "created_at"). 31 | Suffix("RETURNING id, created_at"). 32 | Values(longUrl, "now()"). 33 | ToSql() 34 | if err != nil { 35 | return model.Link{}, fmt.Errorf("build query: %w", err) 36 | } 37 | 38 | var link model.Link 39 | if err := r.pgPool.QueryRow(ctx, sql, args...).Scan(&link.ID, &link.CreatedAt); err != nil { 40 | return model.Link{}, db.NewErrQueryExecutionFailed(sql, args, err) 41 | } 42 | 43 | link.LongURL = longUrl 44 | 45 | return link, nil 46 | } 47 | 48 | // SetShortcode записывает код для ссылки 49 | func (r *Repository) SetShortcode(ctx context.Context, id int, shortcode string) error { 50 | sql, args, err := r.qb.Update("link"). 51 | Set("code", shortcode). 52 | Where("id = ?", id). 53 | ToSql() 54 | if err != nil { 55 | return fmt.Errorf("build query: %w", err) 56 | } 57 | 58 | _, err = r.pgPool.Exec(ctx, sql, args...) 59 | if err != nil { 60 | return db.NewErrQueryExecutionFailed(sql, args, err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // GetLongUrlByCode returns full url by short code 67 | func (r *Repository) GetLongUrlByCode(ctx context.Context, shortCode string) (string, error) { 68 | sql, args, err := r.qb.Select("full_url"). 69 | From("link"). 70 | Where("code = ?", shortCode). 71 | Limit(1). 72 | ToSql() 73 | if err != nil { 74 | return "", fmt.Errorf("unable to build query: %w", err) 75 | } 76 | 77 | var fullUrl string 78 | 79 | if err := r.pgPool.QueryRow(ctx, sql, args...).Scan(&fullUrl); err != nil { 80 | if errors.Is(err, pgx.ErrNoRows) { 81 | return "", nil 82 | } 83 | 84 | return "", db.NewErrQueryExecutionFailed(sql, args, err) 85 | } 86 | 87 | return fullUrl, nil 88 | } 89 | -------------------------------------------------------------------------------- /internal/service/shrink/shrink_test.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "go.uber.org/mock/gomock" 7 | "testing" 8 | ) 9 | 10 | func Test_generateShortcode(t *testing.T) { 11 | type args struct { 12 | id int 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want string 18 | }{ 19 | { 20 | name: "id=0", 21 | args: args{id: 0}, 22 | want: "", 23 | }, { 24 | name: "id != 0", 25 | args: args{id: 123456}, 26 | want: "2n9c", 27 | }, 28 | } 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | assert.Equal(t, tt.want, generateShortcode(tt.args.id)) 32 | }) 33 | } 34 | } 35 | 36 | func TestShrinkService_GetLongUrlByCode(t *testing.T) { 37 | type args struct { 38 | ctx context.Context 39 | shortCode string 40 | } 41 | tests := []struct { 42 | name string 43 | args args 44 | getSessionManagerMock func(ctrl *gomock.Controller) *MockLinkRepository 45 | want string 46 | wantErr bool 47 | }{ 48 | { 49 | name: "success", 50 | args: args{ 51 | ctx: context.Background(), 52 | shortCode: "qwerty", 53 | }, 54 | getSessionManagerMock: func(ctrl *gomock.Controller) *MockLinkRepository { 55 | mock := NewMockLinkRepository(ctrl) 56 | mock.EXPECT(). 57 | GetLongUrlByCode(context.Background(), "qwerty"). 58 | Return("https://google.com", nil). 59 | Times(1) 60 | 61 | return mock 62 | }, 63 | want: "https://google.com", 64 | }, { 65 | name: "failed", 66 | args: args{ 67 | ctx: context.Background(), 68 | shortCode: "qwerty", 69 | }, 70 | getSessionManagerMock: func(ctrl *gomock.Controller) *MockLinkRepository { 71 | mock := NewMockLinkRepository(ctrl) 72 | mock.EXPECT(). 73 | GetLongUrlByCode(context.Background(), "qwerty"). 74 | Return("", assert.AnError). 75 | Times(1) 76 | 77 | return mock 78 | }, 79 | wantErr: true, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | ctrl := gomock.NewController(t) 85 | 86 | s := &ShrinkService{} 87 | if tt.getSessionManagerMock != nil { 88 | s.linkRepository = tt.getSessionManagerMock(ctrl) 89 | } 90 | 91 | got, err := s.GetLongUrlByCode(tt.args.ctx, tt.args.shortCode) 92 | if tt.wantErr { 93 | assert.Error(t, err) 94 | } 95 | 96 | assert.Equal(t, tt.want, got) 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/post_shrink_parameters.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "io" 10 | "net/http" 11 | 12 | "github.com/go-openapi/errors" 13 | "github.com/go-openapi/runtime" 14 | "github.com/go-openapi/runtime/middleware" 15 | "github.com/go-openapi/validate" 16 | 17 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 18 | ) 19 | 20 | // NewPostShrinkParams creates a new PostShrinkParams object 21 | // 22 | // There are no default values defined in the spec. 23 | func NewPostShrinkParams() PostShrinkParams { 24 | 25 | return PostShrinkParams{} 26 | } 27 | 28 | // PostShrinkParams contains all the bound params for the post shrink operation 29 | // typically these are obtained from a http.Request 30 | // 31 | // swagger:parameters PostShrink 32 | type PostShrinkParams struct { 33 | 34 | // HTTP Request Object 35 | HTTPRequest *http.Request `json:"-"` 36 | 37 | /* 38 | Required: true 39 | In: body 40 | */ 41 | Body *models.SourceLink 42 | } 43 | 44 | // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface 45 | // for simple values it will use straight method calls. 46 | // 47 | // To ensure default values, the struct must have been initialized with NewPostShrinkParams() beforehand. 48 | func (o *PostShrinkParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { 49 | var res []error 50 | 51 | o.HTTPRequest = r 52 | 53 | if runtime.HasBody(r) { 54 | defer r.Body.Close() 55 | var body models.SourceLink 56 | if err := route.Consumer.Consume(r.Body, &body); err != nil { 57 | if err == io.EOF { 58 | res = append(res, errors.Required("body", "body", "")) 59 | } else { 60 | res = append(res, errors.NewParseError("body", "body", "", err)) 61 | } 62 | } else { 63 | // validate body object 64 | if err := body.Validate(route.Formats); err != nil { 65 | res = append(res, err) 66 | } 67 | 68 | ctx := validate.WithOperationRequest(r.Context()) 69 | if err := body.ContextValidate(ctx, route.Formats); err != nil { 70 | res = append(res, err) 71 | } 72 | 73 | if len(res) == 0 { 74 | o.Body = &body 75 | } 76 | } 77 | } else { 78 | res = append(res, errors.Required("body", "body", "")) 79 | } 80 | if len(res) > 0 { 81 | return errors.CompositeValidationError(res...) 82 | } 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /internal/service/server/rest/handler/shrink/handler.go: -------------------------------------------------------------------------------- 1 | package shrink 2 | 3 | import ( 4 | "github.com/go-openapi/runtime/middleware" 5 | 6 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 7 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/restapi/operations" 8 | "github.com/dimuska139/urlshortener/pkg/logging" 9 | ) 10 | 11 | type Handler struct { 12 | statisticsService StatisticsService 13 | shrinkService ShrinkService 14 | } 15 | 16 | func NewHandler(statisticsService StatisticsService, shrinkService ShrinkService) *Handler { 17 | return &Handler{ 18 | statisticsService: statisticsService, 19 | shrinkService: shrinkService, 20 | } 21 | } 22 | 23 | func (h *Handler) Shrink(params operations.PostShrinkParams) middleware.Responder { 24 | ctx := params.HTTPRequest.Context() 25 | internalError := operations.NewPostShrinkInternalServerError(). 26 | WithPayload(&models.InternalError{ 27 | Common: "Something went wrong", 28 | }) 29 | 30 | link, err := h.shrinkService.CreateShortCode(ctx, *params.Body.LongURL) 31 | if err != nil { 32 | logging.Error(ctx, "Can't create short code", 33 | "err", err.Error()) 34 | return internalError 35 | } 36 | 37 | return operations.NewPostShrinkOK(). 38 | WithPayload(mapLinkToResponse(link)) 39 | } 40 | 41 | func (h *Handler) Redirect(params operations.GetShortCodeParams) middleware.Responder { 42 | ctx := params.HTTPRequest.Context() 43 | internalError := operations.NewGetShortCodeInternalServerError(). 44 | WithPayload(&models.InternalError{ 45 | Common: "Something went wrong", 46 | }) 47 | 48 | longUrl, err := h.shrinkService.GetLongUrlByCode(params.HTTPRequest.Context(), params.ShortCode) 49 | if err != nil { 50 | logging.Error(ctx, "Can't get long url by code", 51 | "short_code", params.ShortCode, 52 | "err", err.Error(), 53 | ) 54 | 55 | return internalError 56 | } 57 | 58 | if longUrl == "" { 59 | return operations.NewGetShortCodeNotFound().WithPayload(&models.NotFoundError{ 60 | Common: "Not found", 61 | }) 62 | } 63 | 64 | var userAgent string 65 | if params.UserAgent != nil { 66 | userAgent = *params.UserAgent 67 | } 68 | 69 | if err := h.statisticsService.SaveRedirectEvent(ctx, params.ShortCode, userAgent); err != nil { 70 | logging.Error(ctx, "Can't save redirect event", 71 | "short_code", params.ShortCode, 72 | "err", err.Error(), 73 | ) 74 | 75 | return operations.NewGetShortCodeInternalServerError() 76 | } 77 | 78 | return operations.NewGetShortCodeFound(). 79 | WithPayload(&models.RedirectURL{ 80 | LongURL: longUrl, 81 | }). 82 | WithLocation(longUrl) 83 | } 84 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/post_shrink_urlbuilder.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the generate command 7 | 8 | import ( 9 | "errors" 10 | "net/url" 11 | golangswaggerpaths "path" 12 | ) 13 | 14 | // PostShrinkURL generates an URL for the post shrink operation 15 | type PostShrinkURL struct { 16 | _basePath string 17 | } 18 | 19 | // WithBasePath sets the base path for this url builder, only required when it's different from the 20 | // base path specified in the swagger spec. 21 | // When the value of the base path is an empty string 22 | func (o *PostShrinkURL) WithBasePath(bp string) *PostShrinkURL { 23 | o.SetBasePath(bp) 24 | return o 25 | } 26 | 27 | // SetBasePath sets the base path for this url builder, only required when it's different from the 28 | // base path specified in the swagger spec. 29 | // When the value of the base path is an empty string 30 | func (o *PostShrinkURL) SetBasePath(bp string) { 31 | o._basePath = bp 32 | } 33 | 34 | // Build a url path and query string 35 | func (o *PostShrinkURL) Build() (*url.URL, error) { 36 | var _result url.URL 37 | 38 | var _path = "/shrink" 39 | 40 | _basePath := o._basePath 41 | if _basePath == "" { 42 | _basePath = "/v1" 43 | } 44 | _result.Path = golangswaggerpaths.Join(_basePath, _path) 45 | 46 | return &_result, nil 47 | } 48 | 49 | // Must is a helper function to panic when the url builder returns an error 50 | func (o *PostShrinkURL) Must(u *url.URL, err error) *url.URL { 51 | if err != nil { 52 | panic(err) 53 | } 54 | if u == nil { 55 | panic("url can't be nil") 56 | } 57 | return u 58 | } 59 | 60 | // String returns the string representation of the path with query string 61 | func (o *PostShrinkURL) String() string { 62 | return o.Must(o.Build()).String() 63 | } 64 | 65 | // BuildFull builds a full url with scheme, host, path and query string 66 | func (o *PostShrinkURL) BuildFull(scheme, host string) (*url.URL, error) { 67 | if scheme == "" { 68 | return nil, errors.New("scheme is required for a full url on PostShrinkURL") 69 | } 70 | if host == "" { 71 | return nil, errors.New("host is required for a full url on PostShrinkURL") 72 | } 73 | 74 | base, err := o.Build() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | base.Scheme = scheme 80 | base.Host = host 81 | return base, nil 82 | } 83 | 84 | // StringFull returns the string representation of a complete url 85 | func (o *PostShrinkURL) StringFull(scheme, host string) string { 86 | return o.Must(o.BuildFull(scheme, host)).String() 87 | } 88 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - wrapcheck 5 | - wsl 6 | - bodyclose 7 | - forbidigo 8 | - prealloc 9 | - nestif 10 | - dupword 11 | 12 | issues: 13 | exclude-rules: 14 | - path: (.+)_test.go 15 | linters: 16 | - dupl 17 | - wrapcheck 18 | - wsl 19 | - path: (.+).pb.go 20 | linters: 21 | - wsl 22 | 23 | linters-settings: 24 | gomoddirectives: 25 | replace-local: true 26 | 27 | gosec: 28 | config: 29 | G306: "0666" # enable to create files with permissions 0666 (before umask) or lesser 30 | 31 | forbidigo: 32 | forbid: 33 | - (?i)(^|\.)print(f|ln)?$ #forbidden: print, println, fmt.Print, fmt.Println, fmt.Printf 34 | 35 | wsl: 36 | force-case-trailing-whitespace: 1 37 | allow-trailing-comment: true 38 | allow-separated-leading-comment: true 39 | 40 | revive: 41 | enable-all-rules: true 42 | confidence: 0.8 43 | rules: 44 | - name: function-length 45 | severity: warning 46 | disabled: false 47 | arguments: [ 50, 0 ] 48 | - name: function-result-limit 49 | severity: warning 50 | disabled: false 51 | arguments: [ 3 ] 52 | - name: cognitive-complexity 53 | severity: warning 54 | disabled: false 55 | arguments: [ 20 ] 56 | - name: cyclomatic 57 | severity: warning 58 | disabled: false 59 | arguments: [ 10 ] 60 | - name: line-length-limit 61 | severity: warning 62 | disabled: false 63 | arguments: [ 110 ] 64 | - name: argument-limit 65 | severity: warning 66 | disabled: false 67 | arguments: [ 6 ] 68 | - name: unhandled-error 69 | disabled: false 70 | arguments: 71 | - "bytes\\.Buffer\\.Write.*" # always returns nil error 72 | - "strings\\.Builder\\.Write.*" # always returns nil error 73 | # disabled rules 74 | - name: comment-spacings # many false-positives 75 | disabled: true 76 | - name: max-public-structs # quite annoying rule 77 | disabled: true 78 | - name: banned-characters 79 | disabled: true 80 | - name: file-header 81 | disabled: true 82 | - name: flag-parameter # extremely annoying linter, it is absolutely okay to have boolean args 83 | disabled: true 84 | - name: struct-tag # false-positive on tags implemented by other linters 85 | disabled: true 86 | - name: add-constant # dont have exclusions list 87 | disabled: true 88 | - name: empty-lines # it false-positives on one-liners 89 | disabled: true 90 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/get_short_code_parameters.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-openapi/errors" 12 | "github.com/go-openapi/runtime/middleware" 13 | "github.com/go-openapi/strfmt" 14 | ) 15 | 16 | // NewGetShortCodeParams creates a new GetShortCodeParams object 17 | // 18 | // There are no default values defined in the spec. 19 | func NewGetShortCodeParams() GetShortCodeParams { 20 | 21 | return GetShortCodeParams{} 22 | } 23 | 24 | // GetShortCodeParams contains all the bound params for the get short code operation 25 | // typically these are obtained from a http.Request 26 | // 27 | // swagger:parameters GetShortCode 28 | type GetShortCodeParams struct { 29 | 30 | // HTTP Request Object 31 | HTTPRequest *http.Request `json:"-"` 32 | 33 | /*The short code to resolve 34 | Required: true 35 | In: path 36 | */ 37 | ShortCode string 38 | /* 39 | In: header 40 | */ 41 | UserAgent *string 42 | } 43 | 44 | // BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface 45 | // for simple values it will use straight method calls. 46 | // 47 | // To ensure default values, the struct must have been initialized with NewGetShortCodeParams() beforehand. 48 | func (o *GetShortCodeParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { 49 | var res []error 50 | 51 | o.HTTPRequest = r 52 | 53 | rShortCode, rhkShortCode, _ := route.Params.GetOK("shortCode") 54 | if err := o.bindShortCode(rShortCode, rhkShortCode, route.Formats); err != nil { 55 | res = append(res, err) 56 | } 57 | 58 | if err := o.bindUserAgent(r.Header[http.CanonicalHeaderKey("user_agent")], true, route.Formats); err != nil { 59 | res = append(res, err) 60 | } 61 | if len(res) > 0 { 62 | return errors.CompositeValidationError(res...) 63 | } 64 | return nil 65 | } 66 | 67 | // bindShortCode binds and validates parameter ShortCode from path. 68 | func (o *GetShortCodeParams) bindShortCode(rawData []string, hasKey bool, formats strfmt.Registry) error { 69 | var raw string 70 | if len(rawData) > 0 { 71 | raw = rawData[len(rawData)-1] 72 | } 73 | 74 | // Required: true 75 | // Parameter is provided by construction from the route 76 | o.ShortCode = raw 77 | 78 | return nil 79 | } 80 | 81 | // bindUserAgent binds and validates parameter UserAgent from header. 82 | func (o *GetShortCodeParams) bindUserAgent(rawData []string, hasKey bool, formats strfmt.Registry) error { 83 | var raw string 84 | if len(rawData) > 0 { 85 | raw = rawData[len(rawData)-1] 86 | } 87 | 88 | // Required: false 89 | 90 | if raw == "" { // empty values pass all other validations 91 | return nil 92 | } 93 | o.UserAgent = &raw 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /api/rest/swagger.yml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | title: "Urlshortener" 4 | version: "0.0.1" 5 | basePath: "/v1" 6 | definitions: 7 | InternalError: 8 | type: object 9 | properties: 10 | common: 11 | type: string 12 | example: "Something went wrong" 13 | 14 | ValidationError: 15 | type: object 16 | additionalProperties: 17 | type: string 18 | 19 | NotFoundError: 20 | type: object 21 | description: "Not found" 22 | properties: 23 | common: 24 | type: string 25 | example: "Link is not found" 26 | 27 | SourceLink: 28 | type: object 29 | description: "Source URL" 30 | required: 31 | - long_url 32 | properties: 33 | long_url: 34 | type: string 35 | format: url 36 | 37 | ShortLink: 38 | type: object 39 | description: "Short URL" 40 | properties: 41 | long_url: 42 | type: string 43 | format: url 44 | short_url: 45 | type: string 46 | format: url 47 | 48 | RedirectURL: 49 | type: object 50 | description: "URL for redirection" 51 | properties: 52 | long_url: 53 | type: string 54 | format: url 55 | paths: 56 | /shrink: 57 | post: 58 | summary: "Creates a short URL from a long URL" 59 | parameters: 60 | - in: body 61 | name: body 62 | required: true 63 | schema: 64 | $ref: "#/definitions/SourceLink" 65 | responses: 66 | 200: 67 | description: "Short URL successfully created" 68 | schema: 69 | $ref: "#/definitions/ShortLink" 70 | 400: 71 | description: "Validation error" 72 | schema: 73 | $ref: "#/definitions/ValidationError" 74 | 500: 75 | description: "Internal error" 76 | schema: 77 | $ref: "#/definitions/InternalError" 78 | /{shortCode}: 79 | get: 80 | summary: "Represents a short URL. Tracks the visit and redirects to the corresponding long URL" 81 | parameters: 82 | - in: "path" 83 | name: "shortCode" 84 | description: "The short code to resolve" 85 | required: true 86 | type: "string" 87 | - in: "header" 88 | name: "user_agent" 89 | required: false 90 | type: "string" 91 | responses: 92 | 302: 93 | description: "Visit properly tracked and redirected" 94 | headers: 95 | Location: 96 | type: string 97 | description: "Redirect url" 98 | schema: 99 | $ref: "#/definitions/RedirectURL" 100 | 404: 101 | description: "Session is not found" 102 | schema: 103 | $ref: "#/definitions/NotFoundError" 104 | 500: 105 | description: "Internal error" 106 | schema: 107 | $ref: "#/definitions/InternalError" 108 | 109 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/get_short_code_urlbuilder.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the generate command 7 | 8 | import ( 9 | "errors" 10 | "net/url" 11 | golangswaggerpaths "path" 12 | "strings" 13 | ) 14 | 15 | // GetShortCodeURL generates an URL for the get short code operation 16 | type GetShortCodeURL struct { 17 | ShortCode string 18 | 19 | _basePath string 20 | // avoid unkeyed usage 21 | _ struct{} 22 | } 23 | 24 | // WithBasePath sets the base path for this url builder, only required when it's different from the 25 | // base path specified in the swagger spec. 26 | // When the value of the base path is an empty string 27 | func (o *GetShortCodeURL) WithBasePath(bp string) *GetShortCodeURL { 28 | o.SetBasePath(bp) 29 | return o 30 | } 31 | 32 | // SetBasePath sets the base path for this url builder, only required when it's different from the 33 | // base path specified in the swagger spec. 34 | // When the value of the base path is an empty string 35 | func (o *GetShortCodeURL) SetBasePath(bp string) { 36 | o._basePath = bp 37 | } 38 | 39 | // Build a url path and query string 40 | func (o *GetShortCodeURL) Build() (*url.URL, error) { 41 | var _result url.URL 42 | 43 | var _path = "/{shortCode}" 44 | 45 | shortCode := o.ShortCode 46 | if shortCode != "" { 47 | _path = strings.Replace(_path, "{shortCode}", shortCode, -1) 48 | } else { 49 | return nil, errors.New("shortCode is required on GetShortCodeURL") 50 | } 51 | 52 | _basePath := o._basePath 53 | if _basePath == "" { 54 | _basePath = "/v1" 55 | } 56 | _result.Path = golangswaggerpaths.Join(_basePath, _path) 57 | 58 | return &_result, nil 59 | } 60 | 61 | // Must is a helper function to panic when the url builder returns an error 62 | func (o *GetShortCodeURL) Must(u *url.URL, err error) *url.URL { 63 | if err != nil { 64 | panic(err) 65 | } 66 | if u == nil { 67 | panic("url can't be nil") 68 | } 69 | return u 70 | } 71 | 72 | // String returns the string representation of the path with query string 73 | func (o *GetShortCodeURL) String() string { 74 | return o.Must(o.Build()).String() 75 | } 76 | 77 | // BuildFull builds a full url with scheme, host, path and query string 78 | func (o *GetShortCodeURL) BuildFull(scheme, host string) (*url.URL, error) { 79 | if scheme == "" { 80 | return nil, errors.New("scheme is required for a full url on GetShortCodeURL") 81 | } 82 | if host == "" { 83 | return nil, errors.New("host is required for a full url on GetShortCodeURL") 84 | } 85 | 86 | base, err := o.Build() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | base.Scheme = scheme 92 | base.Host = host 93 | return base, nil 94 | } 95 | 96 | // StringFull returns the string representation of a complete url 97 | func (o *GetShortCodeURL) StringFull(scheme, host string) string { 98 | return o.Must(o.BuildFull(scheme, host)).String() 99 | } 100 | -------------------------------------------------------------------------------- /cmd/urlshortener/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/dimuska139/urlshortener/internal/config" 10 | "github.com/dimuska139/urlshortener/internal/di" 11 | "github.com/dimuska139/urlshortener/pkg/logging" 12 | 13 | "github.com/urfave/cli/v2" 14 | "os" 15 | ) 16 | 17 | var Version = "master" 18 | 19 | func runApplication(c *cli.Context) error { 20 | ctx := c.Context 21 | 22 | cfg, err := di.InitConfig(c.String("config"), config.VersionParam(Version)) 23 | if err != nil { 24 | return fmt.Errorf("initialize config: %w", err) 25 | } 26 | 27 | logging.InitLogger(logging.LogLevel(cfg.Loglevel)) 28 | 29 | app, cleanup, err := di.InitApplication(cfg) 30 | if err != nil { 31 | return fmt.Errorf("initialize application: %w", err) 32 | } 33 | 34 | defer cleanup() 35 | 36 | go func() { 37 | app.Run(ctx) 38 | }() 39 | 40 | logging.Info(ctx, fmt.Sprintf("SerpParserAPI started at :%d", cfg.HttpServer.Port)) 41 | 42 | stopSignal := make(chan os.Signal, 1) 43 | signal.Notify(stopSignal, syscall.SIGTERM) 44 | signal.Notify(stopSignal, syscall.SIGINT) 45 | 46 | reloadSignal := make(chan os.Signal, 1) 47 | signal.Notify(reloadSignal, syscall.SIGUSR1) 48 | 49 | for { 50 | select { 51 | case <-stopSignal: 52 | logging.Info(ctx, "Shutdown started") 53 | app.Stop(ctx) 54 | logging.Info(ctx, "Shutdown finished") 55 | os.Exit(0) 56 | 57 | case <-reloadSignal: 58 | break 59 | } 60 | } 61 | } 62 | 63 | func migrate(c *cli.Context) error { 64 | ctx := c.Context 65 | 66 | cfg, err := di.InitConfig(c.String("config"), config.VersionParam(Version)) 67 | if err != nil { 68 | return fmt.Errorf("initialize config: %w", err) 69 | } 70 | 71 | logging.InitLogger(logging.LogLevel(cfg.Loglevel)) 72 | 73 | migrator, cleanup, err := di.InitMigrator(cfg) 74 | if err != nil { 75 | return fmt.Errorf("initialize migrator: %w", err) 76 | } 77 | 78 | defer cleanup() 79 | 80 | logging.Info(ctx, "Applying migrations...") 81 | 82 | if err := migrator.Up(); err != nil { 83 | return fmt.Errorf("up migrations: %w", err) 84 | } 85 | 86 | logging.Info(ctx, "Applying migrations finished") 87 | 88 | return nil 89 | } 90 | 91 | func main() { 92 | app := &cli.App{ 93 | Name: "Shortener", 94 | Usage: "Url shortener service", 95 | Flags: []cli.Flag{ 96 | &cli.StringFlag{ 97 | Name: "config", 98 | Value: "./config.yml", 99 | Usage: "path to the config file", 100 | }, 101 | }, 102 | Action: runApplication, 103 | Commands: []*cli.Command{ 104 | { 105 | Name: "migrate", 106 | Usage: "Apply migrations", 107 | Flags: []cli.Flag{ 108 | &cli.StringFlag{ 109 | Name: "config", 110 | Value: "./config.yml", 111 | Usage: "path to the config file", 112 | }, 113 | }, 114 | Action: migrate, 115 | }, 116 | }, 117 | } 118 | 119 | logging.InitLogger(logging.LogLevelInfo) 120 | 121 | if err := app.Run(os.Args); err != nil { 122 | logging.Fatal(context.Background(), "Can't run application: ", 123 | "err", err.Error()) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/configure_urlshortener.go: -------------------------------------------------------------------------------- 1 | // This file is safe to edit. Once it exists it will not be overwritten 2 | 3 | package restapi 4 | 5 | import ( 6 | "crypto/tls" 7 | "net/http" 8 | 9 | "github.com/go-openapi/errors" 10 | "github.com/go-openapi/runtime" 11 | "github.com/go-openapi/runtime/middleware" 12 | 13 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/restapi/operations" 14 | ) 15 | 16 | //go:generate swagger generate server --target ../../gen --name Urlshortener --spec ../../../../../../api/rest/swagger.yml --principal interface{} --exclude-main 17 | 18 | func configureFlags(api *operations.UrlshortenerAPI) { 19 | // api.CommandLineOptionsGroups = []swag.CommandLineOptionsGroup{ ... } 20 | } 21 | 22 | func configureAPI(api *operations.UrlshortenerAPI) http.Handler { 23 | // configure the api here 24 | api.ServeError = errors.ServeError 25 | 26 | // Set your custom logger if needed. Default one is log.Printf 27 | // Expected interface func(string, ...interface{}) 28 | // 29 | // Example: 30 | // api.Logger = log.Printf 31 | 32 | api.UseSwaggerUI() 33 | // To continue using redoc as your UI, uncomment the following line 34 | // api.UseRedoc() 35 | 36 | api.JSONConsumer = runtime.JSONConsumer() 37 | 38 | api.JSONProducer = runtime.JSONProducer() 39 | 40 | if api.GetShortCodeHandler == nil { 41 | api.GetShortCodeHandler = operations.GetShortCodeHandlerFunc(func(params operations.GetShortCodeParams) middleware.Responder { 42 | return middleware.NotImplemented("operation operations.GetShortCode has not yet been implemented") 43 | }) 44 | } 45 | 46 | if api.PostShrinkHandler == nil { 47 | api.PostShrinkHandler = operations.PostShrinkHandlerFunc(func(params operations.PostShrinkParams) middleware.Responder { 48 | return middleware.NotImplemented("operation operations.PostShrink has not yet been implemented") 49 | }) 50 | } 51 | 52 | api.PreServerShutdown = func() {} 53 | 54 | api.ServerShutdown = func() {} 55 | 56 | return setupGlobalMiddleware(api.Serve(setupMiddlewares)) 57 | } 58 | 59 | // The TLS configuration before HTTPS server starts. 60 | func configureTLS(tlsConfig *tls.Config) { 61 | // Make all necessary changes to the TLS configuration here. 62 | } 63 | 64 | // As soon as server is initialized but not run yet, this function will be called. 65 | // If you need to modify a config, store server instance to stop it individually later, this is the place. 66 | // This function can be called multiple times, depending on the number of serving schemes. 67 | // scheme value will be set accordingly: "http", "https" or "unix". 68 | func configureServer(s *http.Server, scheme, addr string) { 69 | } 70 | 71 | // The middleware configuration is for the handler executors. These do not apply to the swagger.json document. 72 | // The middleware executes after routing but before authentication, binding and validation. 73 | func setupMiddlewares(handler http.Handler) http.Handler { 74 | return handler 75 | } 76 | 77 | // The middleware configuration happens before anything, this middleware also applies to serving the swagger.json document. 78 | // So this is a good place to plug in a panic handling middleware, logging and metrics. 79 | func setupGlobalMiddleware(handler http.Handler) http.Handler { 80 | return handler 81 | } 82 | -------------------------------------------------------------------------------- /internal/service/server/rest/server.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/go-openapi/loads" 8 | "github.com/tidwall/sjson" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/restapi" 13 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/restapi/operations" 14 | shrinkHandler "github.com/dimuska139/urlshortener/internal/service/server/rest/handler/shrink" 15 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware" 16 | "github.com/dimuska139/urlshortener/pkg/logging" 17 | ) 18 | 19 | const version = "1.0.0" 20 | 21 | type Server struct { 22 | server *http.Server 23 | config Config 24 | middlewareFactory *middleware.Factory 25 | 26 | shrinkHandler *shrinkHandler.Handler 27 | } 28 | 29 | func NewServer( 30 | cfg Config, 31 | middlewareFactory *middleware.Factory, 32 | shrinkHandler *shrinkHandler.Handler, 33 | ) (*Server, error) { 34 | ctx := context.Background() 35 | 36 | restApi := &Server{ 37 | config: cfg, 38 | middlewareFactory: middlewareFactory, 39 | shrinkHandler: shrinkHandler, 40 | } 41 | 42 | api, err := restApi.buildAPI(ctx) 43 | if err != nil { 44 | return nil, fmt.Errorf("build API: %w", err) 45 | } 46 | 47 | restApi.server = &http.Server{ 48 | Addr: fmt.Sprintf(":%d", cfg.Port), 49 | ReadHeaderTimeout: 3 * time.Second, 50 | Handler: api, 51 | } 52 | 53 | return restApi, nil 54 | } 55 | 56 | func (s *Server) buildAPI(ctx context.Context) (*http.ServeMux, error) { 57 | specJSON, _ := sjson.Set(string(restapi.SwaggerJSON), "info.version", version) 58 | 59 | swaggerSpec, err := loads.Analyzed([]byte(specJSON), "") 60 | if err != nil { 61 | return nil, fmt.Errorf("analyze Swagger spec: %w", err) 62 | } 63 | 64 | api := operations.NewUrlshortenerAPI(swaggerSpec) 65 | api.Logger = func(format string, v ...any) { 66 | logging.Info(ctx, fmt.Sprintf(format, v...)) 67 | } 68 | 69 | api.PostShrinkHandler = operations.PostShrinkHandlerFunc(s.shrinkHandler.Shrink) 70 | api.GetShortCodeHandler = operations.GetShortCodeHandlerFunc(s.shrinkHandler.Redirect) 71 | api.ServeError = handleErrors 72 | 73 | api.UseSwaggerUI() 74 | 75 | mux := http.DefaultServeMux 76 | mux.Handle("/", api.Serve(s.setupMiddlewares)) 77 | 78 | return mux, nil 79 | } 80 | 81 | func (s *Server) setupMiddlewares(handler http.Handler) http.Handler { 82 | middlewares := []func(handler http.Handler) http.Handler{ 83 | s.middlewareFactory.Cors.GetMiddleware(), 84 | } 85 | 86 | for i := len(middlewares) - 1; i >= 0; i-- { 87 | handler = middlewares[i](handler) 88 | } 89 | 90 | return handler 91 | } 92 | 93 | func (s *Server) Start() error { 94 | if err := s.server.ListenAndServe(); err != nil { 95 | if errors.Is(err, http.ErrServerClosed) { 96 | return nil 97 | } 98 | 99 | return fmt.Errorf("listen and serve: %w", err) 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (s *Server) Stop(ctx context.Context) error { 106 | if err := s.server.Shutdown(ctx); err != nil { 107 | return fmt.Errorf("shutdown server: %w", err) 108 | } 109 | 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dimuska139/urlshortener 2 | 3 | go 1.22 4 | 5 | toolchain go1.23.4 6 | 7 | require ( 8 | github.com/Masterminds/squirrel v1.5.2 9 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 10 | github.com/go-openapi/errors v0.20.1 11 | github.com/go-openapi/loads v0.21.0 12 | github.com/go-openapi/runtime v0.21.0 13 | github.com/go-openapi/spec v0.20.4 14 | github.com/go-openapi/strfmt v0.21.1 15 | github.com/go-openapi/swag v0.19.15 16 | github.com/go-openapi/validate v0.20.3 17 | github.com/google/wire v0.5.0 18 | github.com/ilyakaznacheev/cleanenv v1.5.0 19 | github.com/jackc/pgx/v4 v4.14.1 20 | github.com/jackc/pgx/v5 v5.7.2 21 | github.com/jessevdk/go-flags v1.5.0 22 | github.com/lib/pq v1.10.4 23 | github.com/pressly/goose/v3 v3.5.0 24 | github.com/rs/cors v1.11.1 25 | github.com/stretchr/testify v1.9.0 26 | github.com/tidwall/sjson v1.2.5 27 | github.com/urfave/cli/v2 v2.3.0 28 | go.uber.org/mock v0.5.0 29 | golang.org/x/net v0.25.0 30 | ) 31 | 32 | require ( 33 | github.com/BurntSushi/toml v1.2.1 // indirect 34 | github.com/PuerkitoBio/purell v1.1.1 // indirect 35 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect 36 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 37 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect 38 | github.com/davecgh/go-spew v1.1.1 // indirect 39 | github.com/docker/go-units v0.4.0 // indirect 40 | github.com/go-openapi/analysis v0.20.1 // indirect 41 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 42 | github.com/go-openapi/jsonreference v0.19.6 // indirect 43 | github.com/go-stack/stack v1.8.0 // indirect 44 | github.com/google/go-cmp v0.6.0 // indirect 45 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 46 | github.com/jackc/pgconn v1.10.1 // indirect 47 | github.com/jackc/pgio v1.0.0 // indirect 48 | github.com/jackc/pgpassfile v1.0.0 // indirect 49 | github.com/jackc/pgproto3/v2 v2.2.0 // indirect 50 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 51 | github.com/jackc/pgtype v1.9.1 // indirect 52 | github.com/jackc/puddle/v2 v2.2.2 // indirect 53 | github.com/joho/godotenv v1.5.1 // indirect 54 | github.com/josharian/intern v1.0.0 // indirect 55 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 56 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 57 | github.com/mailru/easyjson v0.7.6 // indirect 58 | github.com/mattn/go-isatty v0.0.14 // indirect 59 | github.com/mitchellh/mapstructure v1.4.3 // indirect 60 | github.com/oklog/ulid v1.3.1 // indirect 61 | github.com/pkg/errors v0.9.1 // indirect 62 | github.com/pmezard/go-difflib v1.0.0 // indirect 63 | github.com/rogpeppe/go-internal v1.6.1 // indirect 64 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 65 | github.com/tidwall/gjson v1.14.2 // indirect 66 | github.com/tidwall/match v1.1.1 // indirect 67 | github.com/tidwall/pretty v1.2.0 // indirect 68 | go.mongodb.org/mongo-driver v1.7.5 // indirect 69 | golang.org/x/crypto v0.31.0 // indirect 70 | golang.org/x/sync v0.10.0 // indirect 71 | golang.org/x/sys v0.28.0 // indirect 72 | golang.org/x/text v0.21.0 // indirect 73 | gopkg.in/yaml.v2 v2.4.0 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /internal/di/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package di 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/jackc/pgx/v5/pgxpool" 9 | 10 | "github.com/dimuska139/urlshortener/internal/config" 11 | "github.com/dimuska139/urlshortener/internal/service/application" 12 | "github.com/dimuska139/urlshortener/internal/service/server/rest" 13 | "github.com/dimuska139/urlshortener/internal/service/server/rest/handler/shrink" 14 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware" 15 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware/cors" 16 | shrinkService "github.com/dimuska139/urlshortener/internal/service/shrink" 17 | statisticsService "github.com/dimuska139/urlshortener/internal/service/statistics" 18 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/link" 19 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/migrator" 20 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/statistics" 21 | "github.com/dimuska139/urlshortener/pkg/postgresql" 22 | "github.com/dimuska139/urlshortener/pkg/postgresql/tx" 23 | ) 24 | 25 | func InitConfig(configPath string, version config.VersionParam) (*config.Config, error) { 26 | panic(wire.Build(config.NewConfig)) 27 | } 28 | 29 | func InitMigrator(_ *config.Config) (*migrator.Migrator, func(), error) { 30 | panic(wire.Build( 31 | wire.FieldsOf(new(*config.Config), "Migrator"), 32 | migrator.NewMigrator, 33 | )) 34 | } 35 | 36 | func initPgxPool(_ *config.Config) (*pgxpool.Pool, func(), error) { 37 | panic(wire.Build( 38 | wire.FieldsOf(new(*config.Config), "PostgreSQL"), 39 | postgresql.NewPgxPool, 40 | )) 41 | } 42 | 43 | func initPostresPool(_ *pgxpool.Pool) (*postgresql.PostgresPool, func(), error) { 44 | panic(wire.Build( 45 | postgresql.NewPostgresPool, 46 | )) 47 | } 48 | 49 | func initTransactionManager(_ *pgxpool.Pool) (*tx.Manager, func(), error) { 50 | panic(wire.Build( 51 | tx.NewManager, 52 | )) 53 | } 54 | 55 | func initLinkRepository(_ *postgresql.PostgresPool) (*link.Repository, func(), error) { 56 | panic(wire.Build( 57 | link.NewRepository, 58 | )) 59 | } 60 | 61 | func initShrinkService( 62 | _ *config.Config, 63 | _ *postgresql.PostgresPool, 64 | _ *tx.Manager) (*shrinkService.ShrinkService, func(), error) { 65 | panic(wire.Build( 66 | initLinkRepository, 67 | wire.Bind(new(shrinkService.LinkRepository), new(*link.Repository)), 68 | wire.Bind(new(shrinkService.TransactionManager), new(*tx.Manager)), 69 | 70 | wire.FieldsOf(new(*config.Config), "shrink"), 71 | shrinkService.NewShrinkService, 72 | )) 73 | } 74 | 75 | func initStatisticsRepository(_ *postgresql.PostgresPool) (*statistics.Repository, func(), error) { 76 | panic(wire.Build( 77 | statistics.NewRepository, 78 | )) 79 | } 80 | 81 | func initStatisticsService(_ *postgresql.PostgresPool) (*statisticsService.StatisticsService, func(), error) { 82 | panic(wire.Build( 83 | initStatisticsRepository, 84 | wire.Bind(new(statisticsService.StatisticsRepository), new(*statistics.Repository)), 85 | statisticsService.NewStatisticsService, 86 | )) 87 | } 88 | 89 | func initCorsMiddleware(_ *middleware.Config) (*cors.Middleware, error) { 90 | panic(wire.Build( 91 | wire.FieldsOf(new(*middleware.Config), "cors"), 92 | cors.NewMiddleware, 93 | )) 94 | } 95 | 96 | func initMiddlewareFactory(_ *rest.Config) (*middleware.Factory, error) { 97 | panic(wire.Build( 98 | wire.FieldsOf(new(*rest.Config), "middleware"), 99 | initCorsMiddleware, 100 | wire.Struct(new(middleware.Factory), "*"), 101 | )) 102 | } 103 | 104 | func initShrinkHandler( 105 | _ *config.Config, 106 | _ *shrinkService.ShrinkService, 107 | _ *statisticsService.StatisticsService, 108 | ) (*shrink.Handler, func(), error) { 109 | panic(wire.Build( 110 | wire.Bind(new(shrink.StatisticsService), new(*statisticsService.StatisticsService)), 111 | wire.Bind(new(shrink.ShrinkService), new(*shrinkService.ShrinkService)), 112 | shrink.NewHandler, 113 | )) 114 | } 115 | 116 | func initRestApiServer( 117 | _ *config.Config, 118 | _ *shrinkService.ShrinkService, 119 | _ *statisticsService.StatisticsService, 120 | ) (*rest.Server, func(), error) { 121 | panic(wire.Build( 122 | initMiddlewareFactory, 123 | initShrinkHandler, 124 | 125 | wire.FieldsOf(new(*config.Config), "HttpServer"), 126 | rest.NewServer, 127 | )) 128 | } 129 | 130 | func InitApplication(_ *config.Config) (*application.Application, func(), error) { 131 | panic(wire.Build( 132 | initPgxPool, 133 | initPostresPool, 134 | initTransactionManager, 135 | 136 | initShrinkService, 137 | initStatisticsService, 138 | 139 | initRestApiServer, 140 | 141 | application.NewApplication, 142 | )) 143 | } 144 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/post_shrink_responses.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-openapi/runtime" 12 | 13 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 14 | ) 15 | 16 | // PostShrinkOKCode is the HTTP code returned for type PostShrinkOK 17 | const PostShrinkOKCode int = 200 18 | 19 | /* 20 | PostShrinkOK Short URL successfully created 21 | 22 | swagger:response postShrinkOK 23 | */ 24 | type PostShrinkOK struct { 25 | 26 | /* 27 | In: Body 28 | */ 29 | Payload *models.ShortLink `json:"body,omitempty"` 30 | } 31 | 32 | // NewPostShrinkOK creates PostShrinkOK with default headers values 33 | func NewPostShrinkOK() *PostShrinkOK { 34 | 35 | return &PostShrinkOK{} 36 | } 37 | 38 | // WithPayload adds the payload to the post shrink o k response 39 | func (o *PostShrinkOK) WithPayload(payload *models.ShortLink) *PostShrinkOK { 40 | o.Payload = payload 41 | return o 42 | } 43 | 44 | // SetPayload sets the payload to the post shrink o k response 45 | func (o *PostShrinkOK) SetPayload(payload *models.ShortLink) { 46 | o.Payload = payload 47 | } 48 | 49 | // WriteResponse to the client 50 | func (o *PostShrinkOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 51 | 52 | rw.WriteHeader(200) 53 | if o.Payload != nil { 54 | payload := o.Payload 55 | if err := producer.Produce(rw, payload); err != nil { 56 | panic(err) // let the recovery middleware deal with this 57 | } 58 | } 59 | } 60 | 61 | // PostShrinkBadRequestCode is the HTTP code returned for type PostShrinkBadRequest 62 | const PostShrinkBadRequestCode int = 400 63 | 64 | /* 65 | PostShrinkBadRequest Validation error 66 | 67 | swagger:response postShrinkBadRequest 68 | */ 69 | type PostShrinkBadRequest struct { 70 | 71 | /* 72 | In: Body 73 | */ 74 | Payload models.ValidationError `json:"body,omitempty"` 75 | } 76 | 77 | // NewPostShrinkBadRequest creates PostShrinkBadRequest with default headers values 78 | func NewPostShrinkBadRequest() *PostShrinkBadRequest { 79 | 80 | return &PostShrinkBadRequest{} 81 | } 82 | 83 | // WithPayload adds the payload to the post shrink bad request response 84 | func (o *PostShrinkBadRequest) WithPayload(payload models.ValidationError) *PostShrinkBadRequest { 85 | o.Payload = payload 86 | return o 87 | } 88 | 89 | // SetPayload sets the payload to the post shrink bad request response 90 | func (o *PostShrinkBadRequest) SetPayload(payload models.ValidationError) { 91 | o.Payload = payload 92 | } 93 | 94 | // WriteResponse to the client 95 | func (o *PostShrinkBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 96 | 97 | rw.WriteHeader(400) 98 | payload := o.Payload 99 | if payload == nil { 100 | // return empty map 101 | payload = models.ValidationError{} 102 | } 103 | 104 | if err := producer.Produce(rw, payload); err != nil { 105 | panic(err) // let the recovery middleware deal with this 106 | } 107 | } 108 | 109 | // PostShrinkInternalServerErrorCode is the HTTP code returned for type PostShrinkInternalServerError 110 | const PostShrinkInternalServerErrorCode int = 500 111 | 112 | /* 113 | PostShrinkInternalServerError Internal error 114 | 115 | swagger:response postShrinkInternalServerError 116 | */ 117 | type PostShrinkInternalServerError struct { 118 | 119 | /* 120 | In: Body 121 | */ 122 | Payload *models.InternalError `json:"body,omitempty"` 123 | } 124 | 125 | // NewPostShrinkInternalServerError creates PostShrinkInternalServerError with default headers values 126 | func NewPostShrinkInternalServerError() *PostShrinkInternalServerError { 127 | 128 | return &PostShrinkInternalServerError{} 129 | } 130 | 131 | // WithPayload adds the payload to the post shrink internal server error response 132 | func (o *PostShrinkInternalServerError) WithPayload(payload *models.InternalError) *PostShrinkInternalServerError { 133 | o.Payload = payload 134 | return o 135 | } 136 | 137 | // SetPayload sets the payload to the post shrink internal server error response 138 | func (o *PostShrinkInternalServerError) SetPayload(payload *models.InternalError) { 139 | o.Payload = payload 140 | } 141 | 142 | // WriteResponse to the client 143 | func (o *PostShrinkInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 144 | 145 | rw.WriteHeader(500) 146 | if o.Payload != nil { 147 | payload := o.Payload 148 | if err := producer.Produce(rw, payload); err != nil { 149 | panic(err) // let the recovery middleware deal with this 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/service/shrink/dependency_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: dependency.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=dependency.go -destination=./dependency_mock.go -package=shrink 7 | // 8 | 9 | // Package shrink is a generated GoMock package. 10 | package shrink 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | model "github.com/dimuska139/urlshortener/internal/model" 17 | v5 "github.com/jackc/pgx/v5" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockTransactionManager is a mock of TransactionManager interface. 22 | type MockTransactionManager struct { 23 | ctrl *gomock.Controller 24 | recorder *MockTransactionManagerMockRecorder 25 | } 26 | 27 | // MockTransactionManagerMockRecorder is the mock recorder for MockTransactionManager. 28 | type MockTransactionManagerMockRecorder struct { 29 | mock *MockTransactionManager 30 | } 31 | 32 | // NewMockTransactionManager creates a new mock instance. 33 | func NewMockTransactionManager(ctrl *gomock.Controller) *MockTransactionManager { 34 | mock := &MockTransactionManager{ctrl: ctrl} 35 | mock.recorder = &MockTransactionManagerMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockTransactionManager) EXPECT() *MockTransactionManagerMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // WithTx mocks base method. 45 | func (m *MockTransactionManager) WithTx(ctx context.Context, fn func(context.Context) error, opts v5.TxOptions) error { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "WithTx", ctx, fn, opts) 48 | ret0, _ := ret[0].(error) 49 | return ret0 50 | } 51 | 52 | // WithTx indicates an expected call of WithTx. 53 | func (mr *MockTransactionManagerMockRecorder) WithTx(ctx, fn, opts any) *gomock.Call { 54 | mr.mock.ctrl.T.Helper() 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithTx", reflect.TypeOf((*MockTransactionManager)(nil).WithTx), ctx, fn, opts) 56 | } 57 | 58 | // MockLinkRepository is a mock of LinkRepository interface. 59 | type MockLinkRepository struct { 60 | ctrl *gomock.Controller 61 | recorder *MockLinkRepositoryMockRecorder 62 | } 63 | 64 | // MockLinkRepositoryMockRecorder is the mock recorder for MockLinkRepository. 65 | type MockLinkRepositoryMockRecorder struct { 66 | mock *MockLinkRepository 67 | } 68 | 69 | // NewMockLinkRepository creates a new mock instance. 70 | func NewMockLinkRepository(ctrl *gomock.Controller) *MockLinkRepository { 71 | mock := &MockLinkRepository{ctrl: ctrl} 72 | mock.recorder = &MockLinkRepositoryMockRecorder{mock} 73 | return mock 74 | } 75 | 76 | // EXPECT returns an object that allows the caller to indicate expected use. 77 | func (m *MockLinkRepository) EXPECT() *MockLinkRepositoryMockRecorder { 78 | return m.recorder 79 | } 80 | 81 | // Create mocks base method. 82 | func (m *MockLinkRepository) Create(ctx context.Context, longUrl string) (model.Link, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "Create", ctx, longUrl) 85 | ret0, _ := ret[0].(model.Link) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // Create indicates an expected call of Create. 91 | func (mr *MockLinkRepositoryMockRecorder) Create(ctx, longUrl any) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockLinkRepository)(nil).Create), ctx, longUrl) 94 | } 95 | 96 | // GetLongUrlByCode mocks base method. 97 | func (m *MockLinkRepository) GetLongUrlByCode(ctx context.Context, shortCode string) (string, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "GetLongUrlByCode", ctx, shortCode) 100 | ret0, _ := ret[0].(string) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // GetLongUrlByCode indicates an expected call of GetLongUrlByCode. 106 | func (mr *MockLinkRepositoryMockRecorder) GetLongUrlByCode(ctx, shortCode any) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLongUrlByCode", reflect.TypeOf((*MockLinkRepository)(nil).GetLongUrlByCode), ctx, shortCode) 109 | } 110 | 111 | // SetShortcode mocks base method. 112 | func (m *MockLinkRepository) SetShortcode(ctx context.Context, id int, shortcode string) error { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "SetShortcode", ctx, id, shortcode) 115 | ret0, _ := ret[0].(error) 116 | return ret0 117 | } 118 | 119 | // SetShortcode indicates an expected call of SetShortcode. 120 | func (mr *MockLinkRepositoryMockRecorder) SetShortcode(ctx, id, shortcode any) *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetShortcode", reflect.TypeOf((*MockLinkRepository)(nil).SetShortcode), ctx, id, shortcode) 123 | } 124 | -------------------------------------------------------------------------------- /internal/service/server/rest/error_handler.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/dgrijalva/jwt-go" 7 | openapiErrors "github.com/go-openapi/errors" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 12 | "github.com/dimuska139/urlshortener/pkg/logging" 13 | ) 14 | 15 | type ErrorResponse struct { 16 | Errors map[string]string `json:"errors"` 17 | } 18 | 19 | func flattenComposite(errs *openapiErrors.CompositeError) *openapiErrors.CompositeError { 20 | var res []error 21 | 22 | for _, er := range errs.Errors { 23 | var compositeError *openapiErrors.CompositeError 24 | 25 | if errors.As(er, &compositeError) && len(compositeError.Errors) > 0 { 26 | flat := flattenComposite(compositeError) 27 | 28 | if len(flat.Errors) > 0 { 29 | res = append(res, flat.Errors...) 30 | } 31 | } else { 32 | res = append(res, er) 33 | } 34 | } 35 | 36 | return openapiErrors.CompositeValidationError(res...) 37 | } 38 | 39 | func getValidationErrors(errs *openapiErrors.CompositeError) map[string]string { 40 | errsMap := make(map[string]string) 41 | 42 | for _, er := range errs.Errors { 43 | switch e := er.(type) { 44 | case *openapiErrors.ParseError: 45 | errsMap["common"] = "Invalid JSON format." 46 | 47 | case *openapiErrors.Validation: 48 | errKey := strings.ReplaceAll(e.Name, "body.", "") 49 | msg := e.Error() 50 | 51 | if strings.Contains(msg, "must be of type int") { 52 | msg = "This value should be of type integer." 53 | } 54 | 55 | if strings.Contains(msg, "in query is required") { 56 | msg = "This query parameter is missing." 57 | } 58 | 59 | if strings.Contains(msg, "in body is required") { 60 | msg = "This parameter is missing." 61 | } 62 | 63 | errsMap[errKey] = msg 64 | 65 | default: 66 | if len(errs.Errors) > 0 { 67 | errsMap["common"] = errs.Errors[0].Error() 68 | } 69 | } 70 | } 71 | 72 | return errsMap 73 | } 74 | 75 | func handleErrors(rw http.ResponseWriter, r *http.Request, err error) { 76 | rw.Header().Set("Content-Type", "application/json") 77 | 78 | ctx := r.Context() 79 | 80 | internalErrorResponse := &models.InternalError{ 81 | Common: "Something went wrong.", 82 | } 83 | 84 | switch e := err.(type) { 85 | case *openapiErrors.CompositeError: 86 | er := flattenComposite(e) 87 | if len(er.Errors) > 0 { 88 | resp := ErrorResponse{ 89 | Errors: getValidationErrors(er), 90 | } 91 | 92 | encoded, _ := json.Marshal(resp) 93 | 94 | rw.WriteHeader(http.StatusBadRequest) 95 | _, _ = rw.Write(encoded) 96 | } else { 97 | handleErrors(rw, r, nil) 98 | } 99 | 100 | case *openapiErrors.MethodNotAllowedError: 101 | rw.Header().Add("Allow", strings.Join(err.(*openapiErrors.MethodNotAllowedError).Allowed, ",")) 102 | rw.WriteHeader(int(e.Code())) 103 | 104 | if r == nil || r.Method != http.MethodHead { 105 | resp := models.InternalError{ 106 | Common: "Method not allowed.", 107 | } 108 | 109 | encoded, _ := json.Marshal(resp) 110 | 111 | _, _ = rw.Write(encoded) 112 | } 113 | 114 | case *jwt.ValidationError: 115 | rw.WriteHeader(http.StatusUnauthorized) 116 | 117 | encoded, _ := json.Marshal(models.InternalError{ 118 | Common: "Authorization required.", 119 | }) 120 | 121 | _, _ = rw.Write(encoded) 122 | 123 | return 124 | 125 | case openapiErrors.Error: 126 | switch e.Code() { 127 | case http.StatusUnauthorized: 128 | rw.WriteHeader(int(e.Code())) 129 | 130 | encoded, _ := json.Marshal(models.InternalError{ 131 | Common: "Authorization required.", 132 | }) 133 | 134 | _, _ = rw.Write(encoded) 135 | 136 | case http.StatusForbidden: 137 | rw.WriteHeader(int(e.Code())) 138 | 139 | encoded, _ := json.Marshal(models.InternalError{ 140 | Common: e.Error(), 141 | }) 142 | 143 | _, _ = rw.Write(encoded) 144 | 145 | case http.StatusNotFound: 146 | rw.WriteHeader(int(e.Code())) 147 | 148 | encoded, _ := json.Marshal(models.InternalError{ 149 | Common: "Not found.", 150 | }) 151 | 152 | _, _ = rw.Write(encoded) 153 | 154 | default: 155 | logging.Error(ctx, "internal server error", 156 | "err", err.Error()) 157 | rw.WriteHeader(http.StatusInternalServerError) 158 | 159 | encoded, _ := json.Marshal(internalErrorResponse) 160 | 161 | _, _ = rw.Write(encoded) 162 | } 163 | 164 | return 165 | 166 | case nil: 167 | rw.WriteHeader(http.StatusInternalServerError) 168 | 169 | encoded, _ := json.Marshal(internalErrorResponse) 170 | _, _ = rw.Write(encoded) 171 | 172 | default: 173 | rw.WriteHeader(http.StatusInternalServerError) 174 | 175 | if r == nil || r.Method != http.MethodHead { 176 | encoded, _ := json.Marshal(internalErrorResponse) 177 | _, _ = rw.Write(encoded) 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /pkg/postgresql/tx/manager_test.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgx/v5" 6 | "github.com/jackc/pgx/v5/pgxpool" 7 | "github.com/stretchr/testify/assert" 8 | "go.uber.org/mock/gomock" 9 | "testing" 10 | ) 11 | 12 | func TestManager_WithTx(t *testing.T) { 13 | type args struct { 14 | ctx context.Context 15 | fn func(ctx context.Context) error 16 | opts pgx.TxOptions 17 | } 18 | tests := []struct { 19 | name string 20 | args args 21 | getMockedPool func(ctrl *gomock.Controller) *MockPool 22 | wantErr bool 23 | }{ 24 | { 25 | name: "nested transaction", 26 | args: args{ 27 | ctx: context.WithValue(context.Background(), TransactionContextKey, &pgxpool.Tx{}), 28 | fn: func(ctx context.Context) error { 29 | return nil 30 | }, 31 | opts: pgx.TxOptions{}, 32 | }, 33 | }, { 34 | name: "can't create transaction", 35 | args: args{ 36 | ctx: context.Background(), 37 | fn: func(ctx context.Context) error { 38 | return nil 39 | }, 40 | opts: pgx.TxOptions{ 41 | IsoLevel: pgx.Serializable, 42 | }, 43 | }, 44 | getMockedPool: func(ctrl *gomock.Controller) *MockPool { 45 | pool := NewMockPool(ctrl) 46 | pool. 47 | EXPECT(). 48 | BeginTx( 49 | context.Background(), 50 | pgx.TxOptions{ 51 | IsoLevel: pgx.Serializable, 52 | }, 53 | ).Return(nil, pgx.ErrTxClosed) 54 | 55 | return pool 56 | }, 57 | wantErr: true, 58 | }, { 59 | name: "with rollback", 60 | args: args{ 61 | ctx: context.Background(), 62 | fn: func(ctx context.Context) error { 63 | return assert.AnError 64 | }, 65 | opts: pgx.TxOptions{ 66 | IsoLevel: pgx.Serializable, 67 | }, 68 | }, 69 | getMockedPool: func(ctrl *gomock.Controller) *MockPool { 70 | txMock := NewMockTx(ctrl) 71 | txMock. 72 | EXPECT(). 73 | Rollback(gomock.Any()). 74 | Return(nil) 75 | 76 | pool := NewMockPool(ctrl) 77 | pool. 78 | EXPECT(). 79 | BeginTx( 80 | context.Background(), 81 | pgx.TxOptions{ 82 | IsoLevel: pgx.Serializable, 83 | }, 84 | ).Return(txMock, nil) 85 | 86 | return pool 87 | }, 88 | wantErr: true, 89 | }, { 90 | name: "success", 91 | args: args{ 92 | ctx: context.Background(), 93 | fn: func(ctx context.Context) error { 94 | return nil 95 | }, 96 | opts: pgx.TxOptions{ 97 | IsoLevel: pgx.Serializable, 98 | }, 99 | }, 100 | getMockedPool: func(ctrl *gomock.Controller) *MockPool { 101 | txMock := NewMockTx(ctrl) 102 | txMock. 103 | EXPECT(). 104 | Commit(gomock.Any()). 105 | Return(nil) 106 | 107 | pool := NewMockPool(ctrl) 108 | pool. 109 | EXPECT(). 110 | BeginTx( 111 | context.Background(), 112 | pgx.TxOptions{ 113 | IsoLevel: pgx.Serializable, 114 | }, 115 | ).Return(txMock, nil) 116 | 117 | return pool 118 | }, 119 | }, { 120 | name: "rollback failed", 121 | args: args{ 122 | ctx: context.Background(), 123 | fn: func(ctx context.Context) error { 124 | return assert.AnError 125 | }, 126 | opts: pgx.TxOptions{ 127 | IsoLevel: pgx.Serializable, 128 | }, 129 | }, 130 | getMockedPool: func(ctrl *gomock.Controller) *MockPool { 131 | txMock := NewMockTx(ctrl) 132 | txMock. 133 | EXPECT(). 134 | Rollback(gomock.Any()). 135 | Return(assert.AnError) 136 | 137 | pool := NewMockPool(ctrl) 138 | pool. 139 | EXPECT(). 140 | BeginTx( 141 | context.Background(), 142 | pgx.TxOptions{ 143 | IsoLevel: pgx.Serializable, 144 | }, 145 | ).Return(txMock, nil) 146 | 147 | return pool 148 | }, 149 | wantErr: true, 150 | }, { 151 | name: "commit failed", 152 | args: args{ 153 | ctx: context.Background(), 154 | fn: func(ctx context.Context) error { 155 | return nil 156 | }, 157 | opts: pgx.TxOptions{ 158 | IsoLevel: pgx.Serializable, 159 | }, 160 | }, 161 | getMockedPool: func(ctrl *gomock.Controller) *MockPool { 162 | txMock := NewMockTx(ctrl) 163 | txMock. 164 | EXPECT(). 165 | Commit(gomock.Any()). 166 | Return(assert.AnError) 167 | 168 | pool := NewMockPool(ctrl) 169 | pool. 170 | EXPECT(). 171 | BeginTx( 172 | context.Background(), 173 | pgx.TxOptions{ 174 | IsoLevel: pgx.Serializable, 175 | }, 176 | ).Return(txMock, nil) 177 | 178 | return pool 179 | }, 180 | wantErr: true, 181 | }, 182 | } 183 | for _, tt := range tests { 184 | t.Run(tt.name, func(t *testing.T) { 185 | txManager := &Manager{} 186 | 187 | ctrl := gomock.NewController(t) 188 | if tt.getMockedPool != nil { 189 | txManager.pool = tt.getMockedPool(ctrl) 190 | } 191 | 192 | err := txManager.WithTx(tt.args.ctx, tt.args.fn, tt.args.opts) 193 | if tt.wantErr { 194 | assert.Error(t, err) 195 | } else { 196 | assert.NoError(t, err) 197 | } 198 | }) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/get_short_code_responses.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/go-openapi/runtime" 12 | 13 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/models" 14 | ) 15 | 16 | // GetShortCodeFoundCode is the HTTP code returned for type GetShortCodeFound 17 | const GetShortCodeFoundCode int = 302 18 | 19 | /* 20 | GetShortCodeFound Visit properly tracked and redirected 21 | 22 | swagger:response getShortCodeFound 23 | */ 24 | type GetShortCodeFound struct { 25 | /*Redirect url 26 | 27 | */ 28 | Location string `json:"Location"` 29 | 30 | /* 31 | In: Body 32 | */ 33 | Payload *models.RedirectURL `json:"body,omitempty"` 34 | } 35 | 36 | // NewGetShortCodeFound creates GetShortCodeFound with default headers values 37 | func NewGetShortCodeFound() *GetShortCodeFound { 38 | 39 | return &GetShortCodeFound{} 40 | } 41 | 42 | // WithLocation adds the location to the get short code found response 43 | func (o *GetShortCodeFound) WithLocation(location string) *GetShortCodeFound { 44 | o.Location = location 45 | return o 46 | } 47 | 48 | // SetLocation sets the location to the get short code found response 49 | func (o *GetShortCodeFound) SetLocation(location string) { 50 | o.Location = location 51 | } 52 | 53 | // WithPayload adds the payload to the get short code found response 54 | func (o *GetShortCodeFound) WithPayload(payload *models.RedirectURL) *GetShortCodeFound { 55 | o.Payload = payload 56 | return o 57 | } 58 | 59 | // SetPayload sets the payload to the get short code found response 60 | func (o *GetShortCodeFound) SetPayload(payload *models.RedirectURL) { 61 | o.Payload = payload 62 | } 63 | 64 | // WriteResponse to the client 65 | func (o *GetShortCodeFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 66 | 67 | // response header Location 68 | 69 | location := o.Location 70 | if location != "" { 71 | rw.Header().Set("Location", location) 72 | } 73 | 74 | rw.WriteHeader(302) 75 | if o.Payload != nil { 76 | payload := o.Payload 77 | if err := producer.Produce(rw, payload); err != nil { 78 | panic(err) // let the recovery middleware deal with this 79 | } 80 | } 81 | } 82 | 83 | // GetShortCodeNotFoundCode is the HTTP code returned for type GetShortCodeNotFound 84 | const GetShortCodeNotFoundCode int = 404 85 | 86 | /* 87 | GetShortCodeNotFound Session is not found 88 | 89 | swagger:response getShortCodeNotFound 90 | */ 91 | type GetShortCodeNotFound struct { 92 | 93 | /* 94 | In: Body 95 | */ 96 | Payload *models.NotFoundError `json:"body,omitempty"` 97 | } 98 | 99 | // NewGetShortCodeNotFound creates GetShortCodeNotFound with default headers values 100 | func NewGetShortCodeNotFound() *GetShortCodeNotFound { 101 | 102 | return &GetShortCodeNotFound{} 103 | } 104 | 105 | // WithPayload adds the payload to the get short code not found response 106 | func (o *GetShortCodeNotFound) WithPayload(payload *models.NotFoundError) *GetShortCodeNotFound { 107 | o.Payload = payload 108 | return o 109 | } 110 | 111 | // SetPayload sets the payload to the get short code not found response 112 | func (o *GetShortCodeNotFound) SetPayload(payload *models.NotFoundError) { 113 | o.Payload = payload 114 | } 115 | 116 | // WriteResponse to the client 117 | func (o *GetShortCodeNotFound) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 118 | 119 | rw.WriteHeader(404) 120 | if o.Payload != nil { 121 | payload := o.Payload 122 | if err := producer.Produce(rw, payload); err != nil { 123 | panic(err) // let the recovery middleware deal with this 124 | } 125 | } 126 | } 127 | 128 | // GetShortCodeInternalServerErrorCode is the HTTP code returned for type GetShortCodeInternalServerError 129 | const GetShortCodeInternalServerErrorCode int = 500 130 | 131 | /* 132 | GetShortCodeInternalServerError Internal error 133 | 134 | swagger:response getShortCodeInternalServerError 135 | */ 136 | type GetShortCodeInternalServerError struct { 137 | 138 | /* 139 | In: Body 140 | */ 141 | Payload *models.InternalError `json:"body,omitempty"` 142 | } 143 | 144 | // NewGetShortCodeInternalServerError creates GetShortCodeInternalServerError with default headers values 145 | func NewGetShortCodeInternalServerError() *GetShortCodeInternalServerError { 146 | 147 | return &GetShortCodeInternalServerError{} 148 | } 149 | 150 | // WithPayload adds the payload to the get short code internal server error response 151 | func (o *GetShortCodeInternalServerError) WithPayload(payload *models.InternalError) *GetShortCodeInternalServerError { 152 | o.Payload = payload 153 | return o 154 | } 155 | 156 | // SetPayload sets the payload to the get short code internal server error response 157 | func (o *GetShortCodeInternalServerError) SetPayload(payload *models.InternalError) { 158 | o.Payload = payload 159 | } 160 | 161 | // WriteResponse to the client 162 | func (o *GetShortCodeInternalServerError) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { 163 | 164 | rw.WriteHeader(500) 165 | if o.Payload != nil { 166 | payload := o.Payload 167 | if err := producer.Produce(rw, payload); err != nil { 168 | panic(err) // let the recovery middleware deal with this 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /internal/di/wire_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by Wire. DO NOT EDIT. 2 | 3 | //go:generate go run -mod=mod github.com/google/wire/cmd/wire 4 | //go:build !wireinject 5 | // +build !wireinject 6 | 7 | package di 8 | 9 | import ( 10 | "github.com/dimuska139/urlshortener/internal/config" 11 | "github.com/dimuska139/urlshortener/internal/service/application" 12 | "github.com/dimuska139/urlshortener/internal/service/server/rest" 13 | shrink2 "github.com/dimuska139/urlshortener/internal/service/server/rest/handler/shrink" 14 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware" 15 | "github.com/dimuska139/urlshortener/internal/service/server/rest/middleware/cors" 16 | "github.com/dimuska139/urlshortener/internal/service/shrink" 17 | statistics2 "github.com/dimuska139/urlshortener/internal/service/statistics" 18 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/link" 19 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/migrator" 20 | "github.com/dimuska139/urlshortener/internal/storage/postgresql/statistics" 21 | "github.com/dimuska139/urlshortener/pkg/postgresql" 22 | "github.com/dimuska139/urlshortener/pkg/postgresql/tx" 23 | "github.com/jackc/pgx/v5/pgxpool" 24 | ) 25 | 26 | // Injectors from wire.go: 27 | 28 | func InitConfig(configPath string, version config.VersionParam) (*config.Config, error) { 29 | configConfig, err := config.NewConfig(configPath, version) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return configConfig, nil 34 | } 35 | 36 | func InitMigrator(configConfig *config.Config) (*migrator.Migrator, func(), error) { 37 | migratorConfig := configConfig.Migrator 38 | migratorMigrator, cleanup, err := migrator.NewMigrator(migratorConfig) 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | return migratorMigrator, func() { 43 | cleanup() 44 | }, nil 45 | } 46 | 47 | func initPgxPool(configConfig *config.Config) (*pgxpool.Pool, func(), error) { 48 | postgresqlConfig := configConfig.PostgreSQL 49 | pool, cleanup, err := postgresql.NewPgxPool(postgresqlConfig) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | return pool, func() { 54 | cleanup() 55 | }, nil 56 | } 57 | 58 | func initPostresPool(pool *pgxpool.Pool) (*postgresql.PostgresPool, func(), error) { 59 | postgresPool, err := postgresql.NewPostgresPool(pool) 60 | if err != nil { 61 | return nil, nil, err 62 | } 63 | return postgresPool, func() { 64 | }, nil 65 | } 66 | 67 | func initTransactionManager(pool *pgxpool.Pool) (*tx.Manager, func(), error) { 68 | manager := tx.NewManager(pool) 69 | return manager, func() { 70 | }, nil 71 | } 72 | 73 | func initLinkRepository(postgresPool *postgresql.PostgresPool) (*link.Repository, func(), error) { 74 | repository := link.NewRepository(postgresPool) 75 | return repository, func() { 76 | }, nil 77 | } 78 | 79 | func initShrinkService(configConfig *config.Config, postgresPool *postgresql.PostgresPool, manager *tx.Manager) (*shrink.ShrinkService, func(), error) { 80 | shrinkConfig := configConfig.Shrink 81 | repository, cleanup, err := initLinkRepository(postgresPool) 82 | if err != nil { 83 | return nil, nil, err 84 | } 85 | shrinkService := shrink.NewShrinkService(shrinkConfig, manager, repository) 86 | return shrinkService, func() { 87 | cleanup() 88 | }, nil 89 | } 90 | 91 | func initStatisticsRepository(postgresPool *postgresql.PostgresPool) (*statistics.Repository, func(), error) { 92 | repository := statistics.NewRepository(postgresPool) 93 | return repository, func() { 94 | }, nil 95 | } 96 | 97 | func initStatisticsService(postgresPool *postgresql.PostgresPool) (*statistics2.StatisticsService, func(), error) { 98 | repository, cleanup, err := initStatisticsRepository(postgresPool) 99 | if err != nil { 100 | return nil, nil, err 101 | } 102 | statisticsService := statistics2.NewStatisticsService(repository) 103 | return statisticsService, func() { 104 | cleanup() 105 | }, nil 106 | } 107 | 108 | func initCorsMiddleware(middlewareConfig *middleware.Config) (*cors.Middleware, error) { 109 | corsConfig := middlewareConfig.Cors 110 | corsMiddleware := cors.NewMiddleware(corsConfig) 111 | return corsMiddleware, nil 112 | } 113 | 114 | func initMiddlewareFactory(restConfig *rest.Config) (*middleware.Factory, error) { 115 | middlewareConfig := &restConfig.Middleware 116 | corsMiddleware, err := initCorsMiddleware(middlewareConfig) 117 | if err != nil { 118 | return nil, err 119 | } 120 | factory := &middleware.Factory{ 121 | Cors: corsMiddleware, 122 | } 123 | return factory, nil 124 | } 125 | 126 | func initShrinkHandler(configConfig *config.Config, shrinkService *shrink.ShrinkService, statisticsService *statistics2.StatisticsService) (*shrink2.Handler, func(), error) { 127 | handler := shrink2.NewHandler(statisticsService, shrinkService) 128 | return handler, func() { 129 | }, nil 130 | } 131 | 132 | func initRestApiServer(configConfig *config.Config, shrinkService *shrink.ShrinkService, statisticsService *statistics2.StatisticsService) (*rest.Server, func(), error) { 133 | restConfig := configConfig.HttpServer 134 | config2 := &configConfig.HttpServer 135 | factory, err := initMiddlewareFactory(config2) 136 | if err != nil { 137 | return nil, nil, err 138 | } 139 | handler, cleanup, err := initShrinkHandler(configConfig, shrinkService, statisticsService) 140 | if err != nil { 141 | return nil, nil, err 142 | } 143 | server, err := rest.NewServer(restConfig, factory, handler) 144 | if err != nil { 145 | cleanup() 146 | return nil, nil, err 147 | } 148 | return server, func() { 149 | cleanup() 150 | }, nil 151 | } 152 | 153 | func InitApplication(configConfig *config.Config) (*application.Application, func(), error) { 154 | pool, cleanup, err := initPgxPool(configConfig) 155 | if err != nil { 156 | return nil, nil, err 157 | } 158 | postgresPool, cleanup2, err := initPostresPool(pool) 159 | if err != nil { 160 | cleanup() 161 | return nil, nil, err 162 | } 163 | manager, cleanup3, err := initTransactionManager(pool) 164 | if err != nil { 165 | cleanup2() 166 | cleanup() 167 | return nil, nil, err 168 | } 169 | shrinkService, cleanup4, err := initShrinkService(configConfig, postgresPool, manager) 170 | if err != nil { 171 | cleanup3() 172 | cleanup2() 173 | cleanup() 174 | return nil, nil, err 175 | } 176 | statisticsService, cleanup5, err := initStatisticsService(postgresPool) 177 | if err != nil { 178 | cleanup4() 179 | cleanup3() 180 | cleanup2() 181 | cleanup() 182 | return nil, nil, err 183 | } 184 | server, cleanup6, err := initRestApiServer(configConfig, shrinkService, statisticsService) 185 | if err != nil { 186 | cleanup5() 187 | cleanup4() 188 | cleanup3() 189 | cleanup2() 190 | cleanup() 191 | return nil, nil, err 192 | } 193 | applicationApplication := application.NewApplication(server) 194 | return applicationApplication, func() { 195 | cleanup6() 196 | cleanup5() 197 | cleanup4() 198 | cleanup3() 199 | cleanup2() 200 | cleanup() 201 | }, nil 202 | } 203 | -------------------------------------------------------------------------------- /pkg/postgresql/tx/dependency_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: dependency.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=dependency.go -destination=./dependency_mock.go -package=tx 7 | // 8 | 9 | // Package tx is a generated GoMock package. 10 | package tx 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | pgx "github.com/jackc/pgx/v5" 17 | pgconn "github.com/jackc/pgx/v5/pgconn" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockPool is a mock of Pool interface. 22 | type MockPool struct { 23 | ctrl *gomock.Controller 24 | recorder *MockPoolMockRecorder 25 | } 26 | 27 | // MockPoolMockRecorder is the mock recorder for MockPool. 28 | type MockPoolMockRecorder struct { 29 | mock *MockPool 30 | } 31 | 32 | // NewMockPool creates a new mock instance. 33 | func NewMockPool(ctrl *gomock.Controller) *MockPool { 34 | mock := &MockPool{ctrl: ctrl} 35 | mock.recorder = &MockPoolMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockPool) EXPECT() *MockPoolMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // BeginTx mocks base method. 45 | func (m *MockPool) BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "BeginTx", ctx, txOptions) 48 | ret0, _ := ret[0].(pgx.Tx) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // BeginTx indicates an expected call of BeginTx. 54 | func (mr *MockPoolMockRecorder) BeginTx(ctx, txOptions any) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTx", reflect.TypeOf((*MockPool)(nil).BeginTx), ctx, txOptions) 57 | } 58 | 59 | // MockTx is a mock of Tx interface. 60 | type MockTx struct { 61 | ctrl *gomock.Controller 62 | recorder *MockTxMockRecorder 63 | } 64 | 65 | // MockTxMockRecorder is the mock recorder for MockTx. 66 | type MockTxMockRecorder struct { 67 | mock *MockTx 68 | } 69 | 70 | // NewMockTx creates a new mock instance. 71 | func NewMockTx(ctrl *gomock.Controller) *MockTx { 72 | mock := &MockTx{ctrl: ctrl} 73 | mock.recorder = &MockTxMockRecorder{mock} 74 | return mock 75 | } 76 | 77 | // EXPECT returns an object that allows the caller to indicate expected use. 78 | func (m *MockTx) EXPECT() *MockTxMockRecorder { 79 | return m.recorder 80 | } 81 | 82 | // Begin mocks base method. 83 | func (m *MockTx) Begin(ctx context.Context) (pgx.Tx, error) { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "Begin", ctx) 86 | ret0, _ := ret[0].(pgx.Tx) 87 | ret1, _ := ret[1].(error) 88 | return ret0, ret1 89 | } 90 | 91 | // Begin indicates an expected call of Begin. 92 | func (mr *MockTxMockRecorder) Begin(ctx any) *gomock.Call { 93 | mr.mock.ctrl.T.Helper() 94 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockTx)(nil).Begin), ctx) 95 | } 96 | 97 | // Commit mocks base method. 98 | func (m *MockTx) Commit(ctx context.Context) error { 99 | m.ctrl.T.Helper() 100 | ret := m.ctrl.Call(m, "Commit", ctx) 101 | ret0, _ := ret[0].(error) 102 | return ret0 103 | } 104 | 105 | // Commit indicates an expected call of Commit. 106 | func (mr *MockTxMockRecorder) Commit(ctx any) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTx)(nil).Commit), ctx) 109 | } 110 | 111 | // Conn mocks base method. 112 | func (m *MockTx) Conn() *pgx.Conn { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "Conn") 115 | ret0, _ := ret[0].(*pgx.Conn) 116 | return ret0 117 | } 118 | 119 | // Conn indicates an expected call of Conn. 120 | func (mr *MockTxMockRecorder) Conn() *gomock.Call { 121 | mr.mock.ctrl.T.Helper() 122 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Conn", reflect.TypeOf((*MockTx)(nil).Conn)) 123 | } 124 | 125 | // CopyFrom mocks base method. 126 | func (m *MockTx) CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error) { 127 | m.ctrl.T.Helper() 128 | ret := m.ctrl.Call(m, "CopyFrom", ctx, tableName, columnNames, rowSrc) 129 | ret0, _ := ret[0].(int64) 130 | ret1, _ := ret[1].(error) 131 | return ret0, ret1 132 | } 133 | 134 | // CopyFrom indicates an expected call of CopyFrom. 135 | func (mr *MockTxMockRecorder) CopyFrom(ctx, tableName, columnNames, rowSrc any) *gomock.Call { 136 | mr.mock.ctrl.T.Helper() 137 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyFrom", reflect.TypeOf((*MockTx)(nil).CopyFrom), ctx, tableName, columnNames, rowSrc) 138 | } 139 | 140 | // Exec mocks base method. 141 | func (m *MockTx) Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error) { 142 | m.ctrl.T.Helper() 143 | varargs := []any{ctx, sql} 144 | for _, a := range arguments { 145 | varargs = append(varargs, a) 146 | } 147 | ret := m.ctrl.Call(m, "Exec", varargs...) 148 | ret0, _ := ret[0].(pgconn.CommandTag) 149 | ret1, _ := ret[1].(error) 150 | return ret0, ret1 151 | } 152 | 153 | // Exec indicates an expected call of Exec. 154 | func (mr *MockTxMockRecorder) Exec(ctx, sql any, arguments ...any) *gomock.Call { 155 | mr.mock.ctrl.T.Helper() 156 | varargs := append([]any{ctx, sql}, arguments...) 157 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockTx)(nil).Exec), varargs...) 158 | } 159 | 160 | // LargeObjects mocks base method. 161 | func (m *MockTx) LargeObjects() pgx.LargeObjects { 162 | m.ctrl.T.Helper() 163 | ret := m.ctrl.Call(m, "LargeObjects") 164 | ret0, _ := ret[0].(pgx.LargeObjects) 165 | return ret0 166 | } 167 | 168 | // LargeObjects indicates an expected call of LargeObjects. 169 | func (mr *MockTxMockRecorder) LargeObjects() *gomock.Call { 170 | mr.mock.ctrl.T.Helper() 171 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LargeObjects", reflect.TypeOf((*MockTx)(nil).LargeObjects)) 172 | } 173 | 174 | // Prepare mocks base method. 175 | func (m *MockTx) Prepare(ctx context.Context, name, sql string) (*pgconn.StatementDescription, error) { 176 | m.ctrl.T.Helper() 177 | ret := m.ctrl.Call(m, "Prepare", ctx, name, sql) 178 | ret0, _ := ret[0].(*pgconn.StatementDescription) 179 | ret1, _ := ret[1].(error) 180 | return ret0, ret1 181 | } 182 | 183 | // Prepare indicates an expected call of Prepare. 184 | func (mr *MockTxMockRecorder) Prepare(ctx, name, sql any) *gomock.Call { 185 | mr.mock.ctrl.T.Helper() 186 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockTx)(nil).Prepare), ctx, name, sql) 187 | } 188 | 189 | // Query mocks base method. 190 | func (m *MockTx) Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error) { 191 | m.ctrl.T.Helper() 192 | varargs := []any{ctx, sql} 193 | for _, a := range args { 194 | varargs = append(varargs, a) 195 | } 196 | ret := m.ctrl.Call(m, "Query", varargs...) 197 | ret0, _ := ret[0].(pgx.Rows) 198 | ret1, _ := ret[1].(error) 199 | return ret0, ret1 200 | } 201 | 202 | // Query indicates an expected call of Query. 203 | func (mr *MockTxMockRecorder) Query(ctx, sql any, args ...any) *gomock.Call { 204 | mr.mock.ctrl.T.Helper() 205 | varargs := append([]any{ctx, sql}, args...) 206 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockTx)(nil).Query), varargs...) 207 | } 208 | 209 | // QueryRow mocks base method. 210 | func (m *MockTx) QueryRow(ctx context.Context, sql string, args ...any) pgx.Row { 211 | m.ctrl.T.Helper() 212 | varargs := []any{ctx, sql} 213 | for _, a := range args { 214 | varargs = append(varargs, a) 215 | } 216 | ret := m.ctrl.Call(m, "QueryRow", varargs...) 217 | ret0, _ := ret[0].(pgx.Row) 218 | return ret0 219 | } 220 | 221 | // QueryRow indicates an expected call of QueryRow. 222 | func (mr *MockTxMockRecorder) QueryRow(ctx, sql any, args ...any) *gomock.Call { 223 | mr.mock.ctrl.T.Helper() 224 | varargs := append([]any{ctx, sql}, args...) 225 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockTx)(nil).QueryRow), varargs...) 226 | } 227 | 228 | // Rollback mocks base method. 229 | func (m *MockTx) Rollback(ctx context.Context) error { 230 | m.ctrl.T.Helper() 231 | ret := m.ctrl.Call(m, "Rollback", ctx) 232 | ret0, _ := ret[0].(error) 233 | return ret0 234 | } 235 | 236 | // Rollback indicates an expected call of Rollback. 237 | func (mr *MockTxMockRecorder) Rollback(ctx any) *gomock.Call { 238 | mr.mock.ctrl.T.Helper() 239 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockTx)(nil).Rollback), ctx) 240 | } 241 | 242 | // SendBatch mocks base method. 243 | func (m *MockTx) SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults { 244 | m.ctrl.T.Helper() 245 | ret := m.ctrl.Call(m, "SendBatch", ctx, b) 246 | ret0, _ := ret[0].(pgx.BatchResults) 247 | return ret0 248 | } 249 | 250 | // SendBatch indicates an expected call of SendBatch. 251 | func (mr *MockTxMockRecorder) SendBatch(ctx, b any) *gomock.Call { 252 | mr.mock.ctrl.T.Helper() 253 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendBatch", reflect.TypeOf((*MockTx)(nil).SendBatch), ctx, b) 254 | } 255 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/embedded_spec.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package restapi 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "encoding/json" 10 | ) 11 | 12 | var ( 13 | // SwaggerJSON embedded version of the swagger document used at generation time 14 | SwaggerJSON json.RawMessage 15 | // FlatSwaggerJSON embedded flattened version of the swagger document used at generation time 16 | FlatSwaggerJSON json.RawMessage 17 | ) 18 | 19 | func init() { 20 | SwaggerJSON = json.RawMessage([]byte(`{ 21 | "swagger": "2.0", 22 | "info": { 23 | "title": "Urlshortener", 24 | "version": "0.0.1" 25 | }, 26 | "basePath": "/v1", 27 | "paths": { 28 | "/shrink": { 29 | "post": { 30 | "summary": "Creates a short URL from a long URL", 31 | "parameters": [ 32 | { 33 | "name": "body", 34 | "in": "body", 35 | "required": true, 36 | "schema": { 37 | "$ref": "#/definitions/SourceLink" 38 | } 39 | } 40 | ], 41 | "responses": { 42 | "200": { 43 | "description": "Short URL successfully created", 44 | "schema": { 45 | "$ref": "#/definitions/ShortLink" 46 | } 47 | }, 48 | "400": { 49 | "description": "Validation error", 50 | "schema": { 51 | "$ref": "#/definitions/ValidationError" 52 | } 53 | }, 54 | "500": { 55 | "description": "Internal error", 56 | "schema": { 57 | "$ref": "#/definitions/InternalError" 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "/{shortCode}": { 64 | "get": { 65 | "summary": "Represents a short URL. Tracks the visit and redirects to the corresponding long URL", 66 | "parameters": [ 67 | { 68 | "type": "string", 69 | "description": "The short code to resolve", 70 | "name": "shortCode", 71 | "in": "path", 72 | "required": true 73 | }, 74 | { 75 | "type": "string", 76 | "name": "user_agent", 77 | "in": "header" 78 | } 79 | ], 80 | "responses": { 81 | "302": { 82 | "description": "Visit properly tracked and redirected", 83 | "schema": { 84 | "$ref": "#/definitions/RedirectURL" 85 | }, 86 | "headers": { 87 | "Location": { 88 | "type": "string", 89 | "description": "Redirect url" 90 | } 91 | } 92 | }, 93 | "404": { 94 | "description": "Session is not found", 95 | "schema": { 96 | "$ref": "#/definitions/NotFoundError" 97 | } 98 | }, 99 | "500": { 100 | "description": "Internal error", 101 | "schema": { 102 | "$ref": "#/definitions/InternalError" 103 | } 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | "definitions": { 110 | "InternalError": { 111 | "type": "object", 112 | "properties": { 113 | "common": { 114 | "type": "string", 115 | "example": "Something went wrong" 116 | } 117 | } 118 | }, 119 | "NotFoundError": { 120 | "description": "Not found", 121 | "type": "object", 122 | "properties": { 123 | "common": { 124 | "type": "string", 125 | "example": "Link is not found" 126 | } 127 | } 128 | }, 129 | "RedirectURL": { 130 | "description": "URL для редиректа", 131 | "type": "object", 132 | "properties": { 133 | "long_url": { 134 | "type": "string", 135 | "format": "url" 136 | } 137 | } 138 | }, 139 | "ShortLink": { 140 | "description": "Short URL", 141 | "type": "object", 142 | "properties": { 143 | "long_url": { 144 | "type": "string", 145 | "format": "url" 146 | }, 147 | "short_url": { 148 | "type": "string", 149 | "format": "url" 150 | } 151 | } 152 | }, 153 | "SourceLink": { 154 | "description": "Source URL", 155 | "type": "object", 156 | "required": [ 157 | "long_url" 158 | ], 159 | "properties": { 160 | "long_url": { 161 | "type": "string", 162 | "format": "url" 163 | } 164 | } 165 | }, 166 | "ValidationError": { 167 | "type": "object", 168 | "additionalProperties": { 169 | "type": "string" 170 | } 171 | } 172 | } 173 | }`)) 174 | FlatSwaggerJSON = json.RawMessage([]byte(`{ 175 | "swagger": "2.0", 176 | "info": { 177 | "title": "Urlshortener", 178 | "version": "0.0.1" 179 | }, 180 | "basePath": "/v1", 181 | "paths": { 182 | "/shrink": { 183 | "post": { 184 | "summary": "Creates a short URL from a long URL", 185 | "parameters": [ 186 | { 187 | "name": "body", 188 | "in": "body", 189 | "required": true, 190 | "schema": { 191 | "$ref": "#/definitions/SourceLink" 192 | } 193 | } 194 | ], 195 | "responses": { 196 | "200": { 197 | "description": "Short URL successfully created", 198 | "schema": { 199 | "$ref": "#/definitions/ShortLink" 200 | } 201 | }, 202 | "400": { 203 | "description": "Validation error", 204 | "schema": { 205 | "$ref": "#/definitions/ValidationError" 206 | } 207 | }, 208 | "500": { 209 | "description": "Internal error", 210 | "schema": { 211 | "$ref": "#/definitions/InternalError" 212 | } 213 | } 214 | } 215 | } 216 | }, 217 | "/{shortCode}": { 218 | "get": { 219 | "summary": "Represents a short URL. Tracks the visit and redirects to the corresponding long URL", 220 | "parameters": [ 221 | { 222 | "type": "string", 223 | "description": "The short code to resolve", 224 | "name": "shortCode", 225 | "in": "path", 226 | "required": true 227 | }, 228 | { 229 | "type": "string", 230 | "name": "user_agent", 231 | "in": "header" 232 | } 233 | ], 234 | "responses": { 235 | "302": { 236 | "description": "Visit properly tracked and redirected", 237 | "schema": { 238 | "$ref": "#/definitions/RedirectURL" 239 | }, 240 | "headers": { 241 | "Location": { 242 | "type": "string", 243 | "description": "Redirect url" 244 | } 245 | } 246 | }, 247 | "404": { 248 | "description": "Session is not found", 249 | "schema": { 250 | "$ref": "#/definitions/NotFoundError" 251 | } 252 | }, 253 | "500": { 254 | "description": "Internal error", 255 | "schema": { 256 | "$ref": "#/definitions/InternalError" 257 | } 258 | } 259 | } 260 | } 261 | } 262 | }, 263 | "definitions": { 264 | "InternalError": { 265 | "type": "object", 266 | "properties": { 267 | "common": { 268 | "type": "string", 269 | "example": "Something went wrong" 270 | } 271 | } 272 | }, 273 | "NotFoundError": { 274 | "description": "Not found", 275 | "type": "object", 276 | "properties": { 277 | "common": { 278 | "type": "string", 279 | "example": "Link is not found" 280 | } 281 | } 282 | }, 283 | "RedirectURL": { 284 | "description": "URL для редиректа", 285 | "type": "object", 286 | "properties": { 287 | "long_url": { 288 | "type": "string", 289 | "format": "url" 290 | } 291 | } 292 | }, 293 | "ShortLink": { 294 | "description": "Short URL", 295 | "type": "object", 296 | "properties": { 297 | "long_url": { 298 | "type": "string", 299 | "format": "url" 300 | }, 301 | "short_url": { 302 | "type": "string", 303 | "format": "url" 304 | } 305 | } 306 | }, 307 | "SourceLink": { 308 | "description": "Source URL", 309 | "type": "object", 310 | "required": [ 311 | "long_url" 312 | ], 313 | "properties": { 314 | "long_url": { 315 | "type": "string", 316 | "format": "url" 317 | } 318 | } 319 | }, 320 | "ValidationError": { 321 | "type": "object", 322 | "additionalProperties": { 323 | "type": "string" 324 | } 325 | } 326 | } 327 | }`)) 328 | } 329 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/operations/urlshortener_api.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package operations 4 | 5 | // This file was generated by the swagger tool. 6 | // Editing this file might prove futile when you re-run the swagger generate command 7 | 8 | import ( 9 | "fmt" 10 | "net/http" 11 | "strings" 12 | 13 | "github.com/go-openapi/errors" 14 | "github.com/go-openapi/loads" 15 | "github.com/go-openapi/runtime" 16 | "github.com/go-openapi/runtime/middleware" 17 | "github.com/go-openapi/runtime/security" 18 | "github.com/go-openapi/spec" 19 | "github.com/go-openapi/strfmt" 20 | "github.com/go-openapi/swag" 21 | ) 22 | 23 | // NewUrlshortenerAPI creates a new Urlshortener instance 24 | func NewUrlshortenerAPI(spec *loads.Document) *UrlshortenerAPI { 25 | return &UrlshortenerAPI{ 26 | handlers: make(map[string]map[string]http.Handler), 27 | formats: strfmt.Default, 28 | defaultConsumes: "application/json", 29 | defaultProduces: "application/json", 30 | customConsumers: make(map[string]runtime.Consumer), 31 | customProducers: make(map[string]runtime.Producer), 32 | PreServerShutdown: func() {}, 33 | ServerShutdown: func() {}, 34 | spec: spec, 35 | useSwaggerUI: false, 36 | ServeError: errors.ServeError, 37 | BasicAuthenticator: security.BasicAuth, 38 | APIKeyAuthenticator: security.APIKeyAuth, 39 | BearerAuthenticator: security.BearerAuth, 40 | 41 | JSONConsumer: runtime.JSONConsumer(), 42 | 43 | JSONProducer: runtime.JSONProducer(), 44 | 45 | GetShortCodeHandler: GetShortCodeHandlerFunc(func(params GetShortCodeParams) middleware.Responder { 46 | return middleware.NotImplemented("operation GetShortCode has not yet been implemented") 47 | }), 48 | PostShrinkHandler: PostShrinkHandlerFunc(func(params PostShrinkParams) middleware.Responder { 49 | return middleware.NotImplemented("operation PostShrink has not yet been implemented") 50 | }), 51 | } 52 | } 53 | 54 | /*UrlshortenerAPI the urlshortener API */ 55 | type UrlshortenerAPI struct { 56 | spec *loads.Document 57 | context *middleware.Context 58 | handlers map[string]map[string]http.Handler 59 | formats strfmt.Registry 60 | customConsumers map[string]runtime.Consumer 61 | customProducers map[string]runtime.Producer 62 | defaultConsumes string 63 | defaultProduces string 64 | Middleware func(middleware.Builder) http.Handler 65 | useSwaggerUI bool 66 | 67 | // BasicAuthenticator generates a runtime.Authenticator from the supplied basic auth function. 68 | // It has a default implementation in the security package, however you can replace it for your particular usage. 69 | BasicAuthenticator func(security.UserPassAuthentication) runtime.Authenticator 70 | 71 | // APIKeyAuthenticator generates a runtime.Authenticator from the supplied token auth function. 72 | // It has a default implementation in the security package, however you can replace it for your particular usage. 73 | APIKeyAuthenticator func(string, string, security.TokenAuthentication) runtime.Authenticator 74 | 75 | // BearerAuthenticator generates a runtime.Authenticator from the supplied bearer token auth function. 76 | // It has a default implementation in the security package, however you can replace it for your particular usage. 77 | BearerAuthenticator func(string, security.ScopedTokenAuthentication) runtime.Authenticator 78 | 79 | // JSONConsumer registers a consumer for the following mime types: 80 | // - application/json 81 | JSONConsumer runtime.Consumer 82 | 83 | // JSONProducer registers a producer for the following mime types: 84 | // - application/json 85 | JSONProducer runtime.Producer 86 | 87 | // GetShortCodeHandler sets the operation handler for the get short code operation 88 | GetShortCodeHandler GetShortCodeHandler 89 | // PostShrinkHandler sets the operation handler for the post shrink operation 90 | PostShrinkHandler PostShrinkHandler 91 | 92 | // ServeError is called when an error is received, there is a default handler 93 | // but you can set your own with this 94 | ServeError func(http.ResponseWriter, *http.Request, error) 95 | 96 | // PreServerShutdown is called before the HTTP(S) server is shutdown 97 | // This allows for custom functions to get executed before the HTTP(S) server stops accepting traffic 98 | PreServerShutdown func() 99 | 100 | // ServerShutdown is called when the HTTP(S) server is shut down and done 101 | // handling all active connections and does not accept connections any more 102 | ServerShutdown func() 103 | 104 | // Custom command line argument groups with their descriptions 105 | CommandLineOptionsGroups []swag.CommandLineOptionsGroup 106 | 107 | // User defined logger function. 108 | Logger func(string, ...interface{}) 109 | } 110 | 111 | // UseRedoc for documentation at /docs 112 | func (o *UrlshortenerAPI) UseRedoc() { 113 | o.useSwaggerUI = false 114 | } 115 | 116 | // UseSwaggerUI for documentation at /docs 117 | func (o *UrlshortenerAPI) UseSwaggerUI() { 118 | o.useSwaggerUI = true 119 | } 120 | 121 | // SetDefaultProduces sets the default produces media type 122 | func (o *UrlshortenerAPI) SetDefaultProduces(mediaType string) { 123 | o.defaultProduces = mediaType 124 | } 125 | 126 | // SetDefaultConsumes returns the default consumes media type 127 | func (o *UrlshortenerAPI) SetDefaultConsumes(mediaType string) { 128 | o.defaultConsumes = mediaType 129 | } 130 | 131 | // SetSpec sets a spec that will be served for the clients. 132 | func (o *UrlshortenerAPI) SetSpec(spec *loads.Document) { 133 | o.spec = spec 134 | } 135 | 136 | // DefaultProduces returns the default produces media type 137 | func (o *UrlshortenerAPI) DefaultProduces() string { 138 | return o.defaultProduces 139 | } 140 | 141 | // DefaultConsumes returns the default consumes media type 142 | func (o *UrlshortenerAPI) DefaultConsumes() string { 143 | return o.defaultConsumes 144 | } 145 | 146 | // Formats returns the registered string formats 147 | func (o *UrlshortenerAPI) Formats() strfmt.Registry { 148 | return o.formats 149 | } 150 | 151 | // RegisterFormat registers a custom format validator 152 | func (o *UrlshortenerAPI) RegisterFormat(name string, format strfmt.Format, validator strfmt.Validator) { 153 | o.formats.Add(name, format, validator) 154 | } 155 | 156 | // Validate validates the registrations in the UrlshortenerAPI 157 | func (o *UrlshortenerAPI) Validate() error { 158 | var unregistered []string 159 | 160 | if o.JSONConsumer == nil { 161 | unregistered = append(unregistered, "JSONConsumer") 162 | } 163 | 164 | if o.JSONProducer == nil { 165 | unregistered = append(unregistered, "JSONProducer") 166 | } 167 | 168 | if o.GetShortCodeHandler == nil { 169 | unregistered = append(unregistered, "GetShortCodeHandler") 170 | } 171 | if o.PostShrinkHandler == nil { 172 | unregistered = append(unregistered, "PostShrinkHandler") 173 | } 174 | 175 | if len(unregistered) > 0 { 176 | return fmt.Errorf("missing registration: %s", strings.Join(unregistered, ", ")) 177 | } 178 | 179 | return nil 180 | } 181 | 182 | // ServeErrorFor gets a error handler for a given operation id 183 | func (o *UrlshortenerAPI) ServeErrorFor(operationID string) func(http.ResponseWriter, *http.Request, error) { 184 | return o.ServeError 185 | } 186 | 187 | // AuthenticatorsFor gets the authenticators for the specified security schemes 188 | func (o *UrlshortenerAPI) AuthenticatorsFor(schemes map[string]spec.SecurityScheme) map[string]runtime.Authenticator { 189 | return nil 190 | } 191 | 192 | // Authorizer returns the registered authorizer 193 | func (o *UrlshortenerAPI) Authorizer() runtime.Authorizer { 194 | return nil 195 | } 196 | 197 | // ConsumersFor gets the consumers for the specified media types. 198 | // MIME type parameters are ignored here. 199 | func (o *UrlshortenerAPI) ConsumersFor(mediaTypes []string) map[string]runtime.Consumer { 200 | result := make(map[string]runtime.Consumer, len(mediaTypes)) 201 | for _, mt := range mediaTypes { 202 | switch mt { 203 | case "application/json": 204 | result["application/json"] = o.JSONConsumer 205 | } 206 | 207 | if c, ok := o.customConsumers[mt]; ok { 208 | result[mt] = c 209 | } 210 | } 211 | return result 212 | } 213 | 214 | // ProducersFor gets the producers for the specified media types. 215 | // MIME type parameters are ignored here. 216 | func (o *UrlshortenerAPI) ProducersFor(mediaTypes []string) map[string]runtime.Producer { 217 | result := make(map[string]runtime.Producer, len(mediaTypes)) 218 | for _, mt := range mediaTypes { 219 | switch mt { 220 | case "application/json": 221 | result["application/json"] = o.JSONProducer 222 | } 223 | 224 | if p, ok := o.customProducers[mt]; ok { 225 | result[mt] = p 226 | } 227 | } 228 | return result 229 | } 230 | 231 | // HandlerFor gets a http.Handler for the provided operation method and path 232 | func (o *UrlshortenerAPI) HandlerFor(method, path string) (http.Handler, bool) { 233 | if o.handlers == nil { 234 | return nil, false 235 | } 236 | um := strings.ToUpper(method) 237 | if _, ok := o.handlers[um]; !ok { 238 | return nil, false 239 | } 240 | if path == "/" { 241 | path = "" 242 | } 243 | h, ok := o.handlers[um][path] 244 | return h, ok 245 | } 246 | 247 | // Context returns the middleware context for the urlshortener API 248 | func (o *UrlshortenerAPI) Context() *middleware.Context { 249 | if o.context == nil { 250 | o.context = middleware.NewRoutableContext(o.spec, o, nil) 251 | } 252 | 253 | return o.context 254 | } 255 | 256 | func (o *UrlshortenerAPI) initHandlerCache() { 257 | o.Context() // don't care about the result, just that the initialization happened 258 | if o.handlers == nil { 259 | o.handlers = make(map[string]map[string]http.Handler) 260 | } 261 | 262 | if o.handlers["GET"] == nil { 263 | o.handlers["GET"] = make(map[string]http.Handler) 264 | } 265 | o.handlers["GET"]["/{shortCode}"] = NewGetShortCode(o.context, o.GetShortCodeHandler) 266 | if o.handlers["POST"] == nil { 267 | o.handlers["POST"] = make(map[string]http.Handler) 268 | } 269 | o.handlers["POST"]["/shrink"] = NewPostShrink(o.context, o.PostShrinkHandler) 270 | } 271 | 272 | // Serve creates a http handler to serve the API over HTTP 273 | // can be used directly in http.ListenAndServe(":8000", api.Serve(nil)) 274 | func (o *UrlshortenerAPI) Serve(builder middleware.Builder) http.Handler { 275 | o.Init() 276 | 277 | if o.Middleware != nil { 278 | return o.Middleware(builder) 279 | } 280 | if o.useSwaggerUI { 281 | return o.context.APIHandlerSwaggerUI(builder) 282 | } 283 | return o.context.APIHandler(builder) 284 | } 285 | 286 | // Init allows you to just initialize the handler cache, you can then recompose the middleware as you see fit 287 | func (o *UrlshortenerAPI) Init() { 288 | if len(o.handlers) == 0 { 289 | o.initHandlerCache() 290 | } 291 | } 292 | 293 | // RegisterConsumer allows you to add (or override) a consumer for a media type. 294 | func (o *UrlshortenerAPI) RegisterConsumer(mediaType string, consumer runtime.Consumer) { 295 | o.customConsumers[mediaType] = consumer 296 | } 297 | 298 | // RegisterProducer allows you to add (or override) a producer for a media type. 299 | func (o *UrlshortenerAPI) RegisterProducer(mediaType string, producer runtime.Producer) { 300 | o.customProducers[mediaType] = producer 301 | } 302 | 303 | // AddMiddlewareFor adds a http middleware to existing handler 304 | func (o *UrlshortenerAPI) AddMiddlewareFor(method, path string, builder middleware.Builder) { 305 | um := strings.ToUpper(method) 306 | if path == "/" { 307 | path = "" 308 | } 309 | o.Init() 310 | if h, ok := o.handlers[um][path]; ok { 311 | o.handlers[um][path] = builder(h) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /internal/service/server/rest/gen/restapi/server.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-swagger; DO NOT EDIT. 2 | 3 | package restapi 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "errors" 10 | "fmt" 11 | "log" 12 | "net" 13 | "net/http" 14 | "os" 15 | "os/signal" 16 | "strconv" 17 | "sync" 18 | "sync/atomic" 19 | "syscall" 20 | "time" 21 | 22 | "github.com/go-openapi/runtime/flagext" 23 | "github.com/go-openapi/swag" 24 | flags "github.com/jessevdk/go-flags" 25 | "golang.org/x/net/netutil" 26 | 27 | "github.com/dimuska139/urlshortener/internal/service/server/rest/gen/restapi/operations" 28 | ) 29 | 30 | const ( 31 | schemeHTTP = "http" 32 | schemeHTTPS = "https" 33 | schemeUnix = "unix" 34 | ) 35 | 36 | var defaultSchemes []string 37 | 38 | func init() { 39 | defaultSchemes = []string{ 40 | schemeHTTP, 41 | } 42 | } 43 | 44 | // NewServer creates a new api urlshortener server but does not configure it 45 | func NewServer(api *operations.UrlshortenerAPI) *Server { 46 | s := new(Server) 47 | 48 | s.shutdown = make(chan struct{}) 49 | s.api = api 50 | s.interrupt = make(chan os.Signal, 1) 51 | return s 52 | } 53 | 54 | // ConfigureAPI configures the API and handlers. 55 | func (s *Server) ConfigureAPI() { 56 | if s.api != nil { 57 | s.handler = configureAPI(s.api) 58 | } 59 | } 60 | 61 | // ConfigureFlags configures the additional flags defined by the handlers. Needs to be called before the parser.Parse 62 | func (s *Server) ConfigureFlags() { 63 | if s.api != nil { 64 | configureFlags(s.api) 65 | } 66 | } 67 | 68 | // Server for the urlshortener API 69 | type Server struct { 70 | EnabledListeners []string `long:"scheme" description:"the listeners to enable, this can be repeated and defaults to the schemes in the swagger spec"` 71 | CleanupTimeout time.Duration `long:"cleanup-timeout" description:"grace period for which to wait before killing idle connections" default:"10s"` 72 | GracefulTimeout time.Duration `long:"graceful-timeout" description:"grace period for which to wait before shutting down the server" default:"15s"` 73 | MaxHeaderSize flagext.ByteSize `long:"max-header-size" description:"controls the maximum number of bytes the server will read parsing the request header's keys and values, including the request line. It does not limit the size of the request body." default:"1MiB"` 74 | 75 | SocketPath flags.Filename `long:"socket-path" description:"the unix socket to listen on" default:"/var/run/urlshortener.sock"` 76 | domainSocketL net.Listener 77 | 78 | Host string `long:"host" description:"the IP to listen on" default:"localhost" env:"HOST"` 79 | Port int `long:"port" description:"the port to listen on for insecure connections, defaults to a random value" env:"PORT"` 80 | ListenLimit int `long:"listen-limit" description:"limit the number of outstanding requests"` 81 | KeepAlive time.Duration `long:"keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)" default:"3m"` 82 | ReadTimeout time.Duration `long:"read-timeout" description:"maximum duration before timing out read of the request" default:"30s"` 83 | WriteTimeout time.Duration `long:"write-timeout" description:"maximum duration before timing out write of the response" default:"60s"` 84 | httpServerL net.Listener 85 | 86 | TLSHost string `long:"tls-host" description:"the IP to listen on for tls, when not specified it's the same as --host" env:"TLS_HOST"` 87 | TLSPort int `long:"tls-port" description:"the port to listen on for secure connections, defaults to a random value" env:"TLS_PORT"` 88 | TLSCertificate flags.Filename `long:"tls-certificate" description:"the certificate to use for secure connections" env:"TLS_CERTIFICATE"` 89 | TLSCertificateKey flags.Filename `long:"tls-key" description:"the private key to use for secure connections" env:"TLS_PRIVATE_KEY"` 90 | TLSCACertificate flags.Filename `long:"tls-ca" description:"the certificate authority file to be used with mutual tls auth" env:"TLS_CA_CERTIFICATE"` 91 | TLSListenLimit int `long:"tls-listen-limit" description:"limit the number of outstanding requests"` 92 | TLSKeepAlive time.Duration `long:"tls-keep-alive" description:"sets the TCP keep-alive timeouts on accepted connections. It prunes dead TCP connections ( e.g. closing laptop mid-download)"` 93 | TLSReadTimeout time.Duration `long:"tls-read-timeout" description:"maximum duration before timing out read of the request"` 94 | TLSWriteTimeout time.Duration `long:"tls-write-timeout" description:"maximum duration before timing out write of the response"` 95 | httpsServerL net.Listener 96 | 97 | api *operations.UrlshortenerAPI 98 | handler http.Handler 99 | hasListeners bool 100 | shutdown chan struct{} 101 | shuttingDown int32 102 | interrupted bool 103 | interrupt chan os.Signal 104 | } 105 | 106 | // Logf logs message either via defined user logger or via system one if no user logger is defined. 107 | func (s *Server) Logf(f string, args ...interface{}) { 108 | if s.api != nil && s.api.Logger != nil { 109 | s.api.Logger(f, args...) 110 | } else { 111 | log.Printf(f, args...) 112 | } 113 | } 114 | 115 | // Fatalf logs message either via defined user logger or via system one if no user logger is defined. 116 | // Exits with non-zero status after printing 117 | func (s *Server) Fatalf(f string, args ...interface{}) { 118 | if s.api != nil && s.api.Logger != nil { 119 | s.api.Logger(f, args...) 120 | os.Exit(1) 121 | } else { 122 | log.Fatalf(f, args...) 123 | } 124 | } 125 | 126 | // SetAPI configures the server with the specified API. Needs to be called before Serve 127 | func (s *Server) SetAPI(api *operations.UrlshortenerAPI) { 128 | if api == nil { 129 | s.api = nil 130 | s.handler = nil 131 | return 132 | } 133 | 134 | s.api = api 135 | s.handler = configureAPI(api) 136 | } 137 | 138 | func (s *Server) hasScheme(scheme string) bool { 139 | schemes := s.EnabledListeners 140 | if len(schemes) == 0 { 141 | schemes = defaultSchemes 142 | } 143 | 144 | for _, v := range schemes { 145 | if v == scheme { 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | 152 | // Serve the api 153 | func (s *Server) Serve() (err error) { 154 | if !s.hasListeners { 155 | if err = s.Listen(); err != nil { 156 | return err 157 | } 158 | } 159 | 160 | // set default handler, if none is set 161 | if s.handler == nil { 162 | if s.api == nil { 163 | return errors.New("can't create the default handler, as no api is set") 164 | } 165 | 166 | s.SetHandler(s.api.Serve(nil)) 167 | } 168 | 169 | wg := new(sync.WaitGroup) 170 | once := new(sync.Once) 171 | signalNotify(s.interrupt) 172 | go handleInterrupt(once, s) 173 | 174 | servers := []*http.Server{} 175 | 176 | if s.hasScheme(schemeUnix) { 177 | domainSocket := new(http.Server) 178 | domainSocket.MaxHeaderBytes = int(s.MaxHeaderSize) 179 | domainSocket.Handler = s.handler 180 | if int64(s.CleanupTimeout) > 0 { 181 | domainSocket.IdleTimeout = s.CleanupTimeout 182 | } 183 | 184 | configureServer(domainSocket, "unix", string(s.SocketPath)) 185 | 186 | servers = append(servers, domainSocket) 187 | wg.Add(1) 188 | s.Logf("Serving urlshortener at unix://%s", s.SocketPath) 189 | go func(l net.Listener) { 190 | defer wg.Done() 191 | if err := domainSocket.Serve(l); err != nil && err != http.ErrServerClosed { 192 | s.Fatalf("%v", err) 193 | } 194 | s.Logf("Stopped serving urlshortener at unix://%s", s.SocketPath) 195 | }(s.domainSocketL) 196 | } 197 | 198 | if s.hasScheme(schemeHTTP) { 199 | httpServer := new(http.Server) 200 | httpServer.MaxHeaderBytes = int(s.MaxHeaderSize) 201 | httpServer.ReadTimeout = s.ReadTimeout 202 | httpServer.WriteTimeout = s.WriteTimeout 203 | httpServer.SetKeepAlivesEnabled(int64(s.KeepAlive) > 0) 204 | if s.ListenLimit > 0 { 205 | s.httpServerL = netutil.LimitListener(s.httpServerL, s.ListenLimit) 206 | } 207 | 208 | if int64(s.CleanupTimeout) > 0 { 209 | httpServer.IdleTimeout = s.CleanupTimeout 210 | } 211 | 212 | httpServer.Handler = s.handler 213 | 214 | configureServer(httpServer, "http", s.httpServerL.Addr().String()) 215 | 216 | servers = append(servers, httpServer) 217 | wg.Add(1) 218 | s.Logf("Serving urlshortener at http://%s", s.httpServerL.Addr()) 219 | go func(l net.Listener) { 220 | defer wg.Done() 221 | if err := httpServer.Serve(l); err != nil && err != http.ErrServerClosed { 222 | s.Fatalf("%v", err) 223 | } 224 | s.Logf("Stopped serving urlshortener at http://%s", l.Addr()) 225 | }(s.httpServerL) 226 | } 227 | 228 | if s.hasScheme(schemeHTTPS) { 229 | httpsServer := new(http.Server) 230 | httpsServer.MaxHeaderBytes = int(s.MaxHeaderSize) 231 | httpsServer.ReadTimeout = s.TLSReadTimeout 232 | httpsServer.WriteTimeout = s.TLSWriteTimeout 233 | httpsServer.SetKeepAlivesEnabled(int64(s.TLSKeepAlive) > 0) 234 | if s.TLSListenLimit > 0 { 235 | s.httpsServerL = netutil.LimitListener(s.httpsServerL, s.TLSListenLimit) 236 | } 237 | if int64(s.CleanupTimeout) > 0 { 238 | httpsServer.IdleTimeout = s.CleanupTimeout 239 | } 240 | httpsServer.Handler = s.handler 241 | 242 | // Inspired by https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go 243 | httpsServer.TLSConfig = &tls.Config{ 244 | // Causes servers to use Go's default ciphersuite preferences, 245 | // which are tuned to avoid attacks. Does nothing on clients. 246 | PreferServerCipherSuites: true, 247 | // Only use curves which have assembly implementations 248 | // https://github.com/golang/go/tree/master/src/crypto/elliptic 249 | CurvePreferences: []tls.CurveID{tls.CurveP256}, 250 | // Use modern tls mode https://wiki.mozilla.org/Security/Server_Side_TLS#Modern_compatibility 251 | NextProtos: []string{"h2", "http/1.1"}, 252 | // https://www.owasp.org/index.php/Transport_Layer_Protection_Cheat_Sheet#Rule_-_Only_Support_Strong_Protocols 253 | MinVersion: tls.VersionTLS12, 254 | // These ciphersuites support Forward Secrecy: https://en.wikipedia.org/wiki/Forward_secrecy 255 | CipherSuites: []uint16{ 256 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 257 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 258 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 259 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 260 | tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, 261 | tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, 262 | }, 263 | } 264 | 265 | // build standard config from server options 266 | if s.TLSCertificate != "" && s.TLSCertificateKey != "" { 267 | httpsServer.TLSConfig.Certificates = make([]tls.Certificate, 1) 268 | httpsServer.TLSConfig.Certificates[0], err = tls.LoadX509KeyPair(string(s.TLSCertificate), string(s.TLSCertificateKey)) 269 | if err != nil { 270 | return err 271 | } 272 | } 273 | 274 | if s.TLSCACertificate != "" { 275 | // include specified CA certificate 276 | caCert, caCertErr := os.ReadFile(string(s.TLSCACertificate)) 277 | if caCertErr != nil { 278 | return caCertErr 279 | } 280 | caCertPool := x509.NewCertPool() 281 | ok := caCertPool.AppendCertsFromPEM(caCert) 282 | if !ok { 283 | return fmt.Errorf("cannot parse CA certificate") 284 | } 285 | httpsServer.TLSConfig.ClientCAs = caCertPool 286 | httpsServer.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert 287 | } 288 | 289 | // call custom TLS configurator 290 | configureTLS(httpsServer.TLSConfig) 291 | 292 | if len(httpsServer.TLSConfig.Certificates) == 0 && httpsServer.TLSConfig.GetCertificate == nil { 293 | // after standard and custom config are passed, this ends up with no certificate 294 | if s.TLSCertificate == "" { 295 | if s.TLSCertificateKey == "" { 296 | s.Fatalf("the required flags `--tls-certificate` and `--tls-key` were not specified") 297 | } 298 | s.Fatalf("the required flag `--tls-certificate` was not specified") 299 | } 300 | if s.TLSCertificateKey == "" { 301 | s.Fatalf("the required flag `--tls-key` was not specified") 302 | } 303 | // this happens with a wrong custom TLS configurator 304 | s.Fatalf("no certificate was configured for TLS") 305 | } 306 | 307 | configureServer(httpsServer, "https", s.httpsServerL.Addr().String()) 308 | 309 | servers = append(servers, httpsServer) 310 | wg.Add(1) 311 | s.Logf("Serving urlshortener at https://%s", s.httpsServerL.Addr()) 312 | go func(l net.Listener) { 313 | defer wg.Done() 314 | if err := httpsServer.Serve(l); err != nil && err != http.ErrServerClosed { 315 | s.Fatalf("%v", err) 316 | } 317 | s.Logf("Stopped serving urlshortener at https://%s", l.Addr()) 318 | }(tls.NewListener(s.httpsServerL, httpsServer.TLSConfig)) 319 | } 320 | 321 | wg.Add(1) 322 | go s.handleShutdown(wg, &servers) 323 | 324 | wg.Wait() 325 | return nil 326 | } 327 | 328 | // Listen creates the listeners for the server 329 | func (s *Server) Listen() error { 330 | if s.hasListeners { // already done this 331 | return nil 332 | } 333 | 334 | if s.hasScheme(schemeHTTPS) { 335 | // Use http host if https host wasn't defined 336 | if s.TLSHost == "" { 337 | s.TLSHost = s.Host 338 | } 339 | // Use http listen limit if https listen limit wasn't defined 340 | if s.TLSListenLimit == 0 { 341 | s.TLSListenLimit = s.ListenLimit 342 | } 343 | // Use http tcp keep alive if https tcp keep alive wasn't defined 344 | if int64(s.TLSKeepAlive) == 0 { 345 | s.TLSKeepAlive = s.KeepAlive 346 | } 347 | // Use http read timeout if https read timeout wasn't defined 348 | if int64(s.TLSReadTimeout) == 0 { 349 | s.TLSReadTimeout = s.ReadTimeout 350 | } 351 | // Use http write timeout if https write timeout wasn't defined 352 | if int64(s.TLSWriteTimeout) == 0 { 353 | s.TLSWriteTimeout = s.WriteTimeout 354 | } 355 | } 356 | 357 | if s.hasScheme(schemeUnix) { 358 | domSockListener, err := net.Listen("unix", string(s.SocketPath)) 359 | if err != nil { 360 | return err 361 | } 362 | s.domainSocketL = domSockListener 363 | } 364 | 365 | if s.hasScheme(schemeHTTP) { 366 | listener, err := net.Listen("tcp", net.JoinHostPort(s.Host, strconv.Itoa(s.Port))) 367 | if err != nil { 368 | return err 369 | } 370 | 371 | h, p, err := swag.SplitHostPort(listener.Addr().String()) 372 | if err != nil { 373 | return err 374 | } 375 | s.Host = h 376 | s.Port = p 377 | s.httpServerL = listener 378 | } 379 | 380 | if s.hasScheme(schemeHTTPS) { 381 | tlsListener, err := net.Listen("tcp", net.JoinHostPort(s.TLSHost, strconv.Itoa(s.TLSPort))) 382 | if err != nil { 383 | return err 384 | } 385 | 386 | sh, sp, err := swag.SplitHostPort(tlsListener.Addr().String()) 387 | if err != nil { 388 | return err 389 | } 390 | s.TLSHost = sh 391 | s.TLSPort = sp 392 | s.httpsServerL = tlsListener 393 | } 394 | 395 | s.hasListeners = true 396 | return nil 397 | } 398 | 399 | // Shutdown server and clean up resources 400 | func (s *Server) Shutdown() error { 401 | if atomic.CompareAndSwapInt32(&s.shuttingDown, 0, 1) { 402 | close(s.shutdown) 403 | } 404 | return nil 405 | } 406 | 407 | func (s *Server) handleShutdown(wg *sync.WaitGroup, serversPtr *[]*http.Server) { 408 | // wg.Done must occur last, after s.api.ServerShutdown() 409 | // (to preserve old behaviour) 410 | defer wg.Done() 411 | 412 | <-s.shutdown 413 | 414 | servers := *serversPtr 415 | 416 | ctx, cancel := context.WithTimeout(context.TODO(), s.GracefulTimeout) 417 | defer cancel() 418 | 419 | // first execute the pre-shutdown hook 420 | s.api.PreServerShutdown() 421 | 422 | shutdownChan := make(chan bool) 423 | for i := range servers { 424 | server := servers[i] 425 | go func() { 426 | var success bool 427 | defer func() { 428 | shutdownChan <- success 429 | }() 430 | if err := server.Shutdown(ctx); err != nil { 431 | // Error from closing listeners, or context timeout: 432 | s.Logf("HTTP server Shutdown: %v", err) 433 | } else { 434 | success = true 435 | } 436 | }() 437 | } 438 | 439 | // Wait until all listeners have successfully shut down before calling ServerShutdown 440 | success := true 441 | for range servers { 442 | success = success && <-shutdownChan 443 | } 444 | if success { 445 | s.api.ServerShutdown() 446 | } 447 | } 448 | 449 | // GetHandler returns a handler useful for testing 450 | func (s *Server) GetHandler() http.Handler { 451 | return s.handler 452 | } 453 | 454 | // SetHandler allows for setting a http handler on this server 455 | func (s *Server) SetHandler(handler http.Handler) { 456 | s.handler = handler 457 | } 458 | 459 | // UnixListener returns the domain socket listener 460 | func (s *Server) UnixListener() (net.Listener, error) { 461 | if !s.hasListeners { 462 | if err := s.Listen(); err != nil { 463 | return nil, err 464 | } 465 | } 466 | return s.domainSocketL, nil 467 | } 468 | 469 | // HTTPListener returns the http listener 470 | func (s *Server) HTTPListener() (net.Listener, error) { 471 | if !s.hasListeners { 472 | if err := s.Listen(); err != nil { 473 | return nil, err 474 | } 475 | } 476 | return s.httpServerL, nil 477 | } 478 | 479 | // TLSListener returns the https listener 480 | func (s *Server) TLSListener() (net.Listener, error) { 481 | if !s.hasListeners { 482 | if err := s.Listen(); err != nil { 483 | return nil, err 484 | } 485 | } 486 | return s.httpsServerL, nil 487 | } 488 | 489 | func handleInterrupt(once *sync.Once, s *Server) { 490 | once.Do(func() { 491 | for range s.interrupt { 492 | if s.interrupted { 493 | s.Logf("Server already shutting down") 494 | continue 495 | } 496 | s.interrupted = true 497 | s.Logf("Shutting down... ") 498 | if err := s.Shutdown(); err != nil { 499 | s.Logf("HTTP server Shutdown: %v", err) 500 | } 501 | } 502 | }) 503 | } 504 | 505 | func signalNotify(interrupt chan<- os.Signal) { 506 | signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM) 507 | } 508 | --------------------------------------------------------------------------------