diff --git a/.golangci.yaml b/.golangci.yaml index 9580476..2a8e3b5 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -5,16 +5,6 @@ run: # Default: 1m timeout: 5m tests: false - # Which dirs to skip: issues from them won't be reported. - skip-dirs: - - test - - third_party - # Which files to skip: they will be analyzed, but issues from them won't be reported. - skip-files: - - _test.go - - _mock.go - - ".*\\.pb\\.go" - - ".*\\.gen\\.go" linters: disable-all: true enable: # please keep this alphabetized @@ -29,7 +19,7 @@ linters: - goconst - gocritic - gofmt - - gomnd + - mnd - gocyclo - gosec - gosimple @@ -62,8 +52,6 @@ linters-settings: # please keep this alphabetized - name: atomic severity: warning - name: package-comments - - name: unhandled-error - arguments : ["fmt.Printf"] - name: blank-imports - name: context-as-argument - name: context-keys-type @@ -110,10 +98,6 @@ linters-settings: # please keep this alphabetized staticcheck: checks: - "all" - - "-SA1019" # TODO(fix) Using a deprecated function, variable, constant or field - stylecheck: - checks: - - "ST1019" # Importing the same package multiple times. lll: line-length: 120 @@ -123,6 +107,16 @@ linters-settings: # please keep this alphabetized min-complexity: 8 issues: + # Which dirs to skip: issues from them won't be reported. + exclude-dirs: + - test + - third_party + # Which files to skip: they will be analyzed, but issues from them won't be reported. + exclude-files: + - _test.go + - _mock.go + - ".*\\.pb\\.go" + - ".*\\.gen\\.go" # List of regexps of issue texts to exclude. include: - EXC0012 # EXC0012 revive: exported (.+) should have comment( \(or a comment on this block\))? or be unexported @@ -146,7 +140,7 @@ output: # Example: "checkstyle:report.json,colored-line-number" # # Default: colored-line-number - format: colored-line-number,github-actions + formats: colored-line-number # Print lines of code with issue. # Default: true print-issued-lines: false diff --git a/app/turl/server.go b/app/turl/server.go index b8c336e..29f8ecd 100644 --- a/app/turl/server.go +++ b/app/turl/server.go @@ -1,19 +1,33 @@ // Package turl implements the business logic of the tiny URL service. package turl -// type TinyURL struct { -// db db.TinyURL -// cache cache.TinyURL -// } -// -// func New(db db.TinyURL, cache cache.Interface) *TinyURL { -// return &TinyURL{ -// db: db, -// cache: cache, -// } +import ( + "github.com/beiai0xff/turl/pkg/cache" + "github.com/beiai0xff/turl/pkg/storage" +) + +// TinyURL represents the tiny URL service. +type TinyURL struct { + db storage.Storage + distributedCache cache.Interface + localCache cache.Interface +} + +// New creates a new TinyURL service. +func newTinyURL(db storage.Storage, dcache, lcache cache.Interface) *TinyURL { + return &TinyURL{ + db: db, + distributedCache: dcache, + localCache: lcache, + } +} + +// // Create creates a new tiny URL. +// func (t *TinyURL) Create(longURL []byte) error { +// return nil // } // -// func (t *TinyURL) Create(longURL string) error { +// // Retrieve a tiny URL. +// func (t *TinyURL) Retrieve(short string) error { // return nil -// // } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go new file mode 100644 index 0000000..0614996 --- /dev/null +++ b/pkg/storage/storage.go @@ -0,0 +1,77 @@ +// Package pkg provides the implementation of the storage interface. +package storage + +import ( + "context" + + "github.com/redis/go-redis/v9" + "gorm.io/gorm" +) + +// Ensuring that *storage implements the Storage interface +var _ Storage = (*storage)(nil) + +// Storage is an interface that defines the methods that a storage system must implement. +type Storage interface { + // Insert adds a new TinyURL record to the storage. + Insert(ctx context.Context, short uint64, longURL []byte) error + // GetTinyURLByID retrieves a TinyURL record by its ID. + GetTinyURLByID(ctx context.Context, short uint64) (*TinyURL, error) + // Close closes the storage. + Close() error +} + +// TinyURL represents a shortened URL record. +type TinyURL struct { + gorm.Model + LongURL []byte `gorm:"type:VARCHAR(500);not null" json:"long_url"` // The original URL. + Short uint64 `gorm:"type:BIGINT;index;not null" json:"short"` // The shortened URL ID. +} + +// storage is a concrete implementation of the Storage interface. +type storage struct { + db *gorm.DB // Database client. + rdb redis.UniversalClient // Redis client. +} + +// New creates a new storage instance. +func New(db *gorm.DB, rdb redis.UniversalClient) Storage { + return newStorage(db, rdb) +} + +// newStorage is a helper function that creates a new storage instance. +func newStorage(db *gorm.DB, rdb redis.UniversalClient) *storage { + return &storage{ + db: db, + rdb: rdb, + } +} + +// Insert adds a new TinyURL record to the storage. +func (s *storage) Insert(ctx context.Context, short uint64, long []byte) error { + t := TinyURL{ + Short: short, + LongURL: long, + } + + // Create a new record in the database. + return s.db.WithContext(ctx).Create(&t).Error +} + +// GetTinyURLByID retrieves a TinyURL record by its ID. +func (s *storage) GetTinyURLByID(ctx context.Context, short uint64) (*TinyURL, error) { + t := TinyURL{} + // Query the database for the record. + res := s.db.WithContext(ctx).Where("short = ?", short).Take(&t) + + if res.Error != nil { + return nil, res.Error + } + + return &t, nil +} + +// Close closes the storage. +func (s *storage) Close() error { + return s.rdb.Close() +} diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go new file mode 100644 index 0000000..1f4e5bd --- /dev/null +++ b/pkg/storage/storage_test.go @@ -0,0 +1,63 @@ +package storage + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/beiai0xff/turl/pkg/db/mysql" + "github.com/beiai0xff/turl/pkg/db/redis" + "github.com/beiai0xff/turl/test" +) + +func TestMain(m *testing.M) { + db, _ := mysql.New(test.DSN) + + db.AutoMigrate(&TinyURL{}) + + os.Exit(m.Run()) +} + +func TestNew(t *testing.T) { + db, _ := mysql.New(test.DSN) + rdb := redis.Client(test.RedisAddr) + + s := New(db, rdb) + t.Cleanup(func() { + s.Close() + }) + + require.NotNil(t, s) +} + +func Test_newStorage(t *testing.T) { + db, _ := mysql.New(test.DSN) + rdb := redis.Client(test.RedisAddr) + + s := newStorage(db, rdb) + t.Cleanup(func() { + s.Close() + }) + + require.NotNil(t, s) +} + +func Test_storage_GetTinyURLByID(t *testing.T) { + db, _ := mysql.New(test.DSN) + rdb := redis.Client(test.RedisAddr) + + short, long := uint64(10000), []byte("www.google.com") + s, ctx := newStorage(db, rdb), context.Background() + t.Cleanup(func() { s.Close() }) + + require.NoError(t, s.Insert(ctx, short, long)) + got, err := s.GetTinyURLByID(ctx, short) + require.NoError(t, err) + require.Equal(t, long, got.LongURL) + + got, err = s.GetTinyURLByID(ctx, 100) + require.ErrorIs(t, err, gorm.ErrRecordNotFound) +} diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index e9fe68e..0433e20 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.1' - services: mysql: @@ -18,14 +16,16 @@ services: interval: 3s redis: - image: redis:latest + image: redis/redis-stack-server:7.2.0-v11 container_name: redis restart: always ports: - '6379:6379' - command: redis-server --save 60 1 --loglevel warning + command: ["redis-server" , "--protected-mode", "no"] + environment: + - REDIS_ARGS="--save 10 1 --appendonly yes" healthcheck: test: ["CMD", "redis-cli", "ping"] timeout: 10s retries: 5 - interval: 3s \ No newline at end of file + interval: 3s