ソースを参照

Make alias generation actually work for base strategy.

- new storage facade: Truncate() and helper AddToTruncateList() for tests.
- longurl schema now implemented
- more generation cases handled
Frederic G. MARAND 10 年 前
コミット
ea64784e28

+ 28 - 3
data/db-schema.sql

@@ -3,7 +3,7 @@
 -- http://www.phpmyadmin.net
 --
 -- Host: localhost
--- Generation Time: Jan 24, 2015 at 05:32 PM
+-- Generation Time: Jan 25, 2015 at 05:22 PM
 -- Server version: 5.5.40
 -- PHP Version: 5.4.36-1+deb.sury.org~precise+2
 
@@ -16,6 +16,19 @@ SET time_zone = "+00:00";
 
 -- --------------------------------------------------------
 
+--
+-- Table structure for table `longurl`
+--
+
+CREATE TABLE IF NOT EXISTS `longurl` (
+  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+  `url` varchar(32) NOT NULL COMMENT 'The short URL itself',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `url` (`url`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+-- --------------------------------------------------------
+
 --
 -- Table structure for table `shorturl`
 --
@@ -23,11 +36,23 @@ SET time_zone = "+00:00";
 CREATE TABLE IF NOT EXISTS `shorturl` (
   `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
   `url` varchar(32) NOT NULL COMMENT 'The short URL itself',
+  `longurl` bigint(20) unsigned NOT NULL,
   `domain` int(11) NOT NULL,
   `strategy` varchar(8) NOT NULL DEFAULT 'base',
   `submittedBy` int(11) NOT NULL,
   `submittedInfo` int(11) NOT NULL,
   `isEnabled` tinyint(1) NOT NULL,
   PRIMARY KEY (`id`),
-  UNIQUE KEY `url` (`url`)
-) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+  UNIQUE KEY `url` (`url`),
+  KEY `longurl` (`longurl`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
+
+--
+-- Constraints for dumped tables
+--
+
+--
+-- Constraints for table `shorturl`
+--
+ALTER TABLE `shorturl`
+  ADD CONSTRAINT `shorturl_fk_longurl` FOREIGN KEY (`longurl`) REFERENCES `longurl` (`id`);

+ 22 - 2
storage/storage.go

@@ -19,11 +19,13 @@ import (
 	"database/sql"
 	"flag"
 	_ "github.com/go-sql-driver/mysql"
+	"log"
 )
 
 type Storage struct {
-	DB  *sql.DB
-	DSN string
+	DB           *sql.DB
+	DSN          string
+	truncateList []string
 }
 
 var Service Storage = Storage{}
@@ -47,11 +49,29 @@ Close() closes the database connection and releases its information.
 */
 func (s *Storage) Close() {
 	if s.DB != nil {
+		for _, name := range s.truncateList {
+			s.Truncate(name)
+		}
 		s.DB.Close()
 	}
 	s.DB = nil
 }
 
+func (s *Storage) AddToTruncateList(name string) {
+	s.truncateList = append(s.truncateList, name)
+}
+
+func (s *Storage) Truncate(name string) {
+	if s.DB == nil {
+		panic("Cannot truncate a non-connected database.")
+	}
+	// Cannot use truncate on a master table (longurl vs shorturl) with MySQL.
+	_, err := s.DB.Exec("DELETE FROM " + name)
+	if err != nil {
+		log.Printf("Error truncating %s: %+v\n", name, err)
+	}
+}
+
 /*
 SetDSN() sets the DSN information for the storage.
 */

+ 1 - 1
strategy/hexcrc32.go

@@ -23,7 +23,7 @@ func (y HexCrc32Strategy) Name() string {
 	return "hexCrc32"
 }
 
-func (y HexCrc32Strategy) Alias(short url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
+func (y HexCrc32Strategy) Alias(short *url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
 	var ret url.ShortUrl
 
 	return ret, errors.New("HexCrc32 not implemented yet")

+ 3 - 3
strategy/manual.go

@@ -21,7 +21,7 @@ Alias() implements AliasingStrategy.Alias().
 
   - options is expected to be a non empty single string
 */
-func (y ManualStrategy) Alias(long url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
+func (y ManualStrategy) Alias(long *url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
 	var ret url.ShortUrl
 	var err error
 	if len(options) != 1 {
@@ -30,7 +30,7 @@ func (y ManualStrategy) Alias(long url.LongUrl, s storage.Storage, options ...in
 	} else {
 		requestedAlias, ok := options[0].(string)
 		if !ok {
-			err = errors.New(fmt.Sprintf("ManualString.Alias() optional parameter must be a string, got: %+V", requestedAlias))
+			err = errors.New(fmt.Sprintf("ManualString.Alias() optional parameter must be a string, got: %+v", requestedAlias))
 			return ret, err
 		}
 
@@ -40,7 +40,7 @@ func (y ManualStrategy) Alias(long url.LongUrl, s storage.Storage, options ...in
 		 */
 		ret = url.ShortUrl{
 			Value:       requestedAlias,
-			ShortFor:    long,
+			ShortFor:    *long,
 			Domain:      long.Domain(),
 			Strategy:    y.Name(),
 			SubmittedBy: storage.CurrentUser(),

+ 1 - 1
strategy/strategies.go

@@ -60,7 +60,7 @@ GROUP BY strategy
 		}
 
 		if !Strategies.IsValid(strategyResult.String) {
-			log.Fatalf("'%#V' is not a valid strategy\n", strategyResult)
+			log.Fatalf("'%#v' is not a valid strategy\n", strategyResult)
 		}
 		ss[strategyResult.String] = countResult.Int64
 	}

+ 56 - 10
strategy/strategy.go

@@ -23,9 +23,9 @@ The options parameter for Alias() MAY be used by some strategies, in which case
 have to define their expectations about it.
 */
 type AliasingStrategy interface {
-	Name() string                                                                           // Return the name of the strategy object
-	Alias(url url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) // Return the short URL (alias) for a given long (source) URL
-	UseCount(storage storage.Storage) int                                                   // Return the number of short URLs (aliases) using this strategy.
+	Name() string                                                                            // Return the name of the strategy object
+	Alias(url *url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) // Return the short URL (alias) for a given long (source) URL
+	UseCount(storage storage.Storage) int                                                    // Return the number of short URLs (aliases) using this strategy.
 }
 
 type baseStrategy struct{}
@@ -34,16 +34,62 @@ func (y baseStrategy) Name() string {
 	return "base"
 }
 
-func (y baseStrategy) Alias(long url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
+/*
+Make sure a longurl instance has a DB ID, allocating it if needed.
+
+For speed reasons, assumes nonzero IDs to be valid without checking.
+*/
+func (y baseStrategy) ensureLongId(long *url.LongUrl, s storage.Storage) error {
+	var err error
+	var long_id int64
+
+	// If long does not have an Id, check if it is already known.
+	if long.Id == 0 {
+		sql := `
+SELECT id
+FROM longurl
+WHERE url = ?
+		`
+		err = s.DB.QueryRow(sql, y.Name()).Scan(&long_id)
+		if err != nil {
+			long_id = 0
+			// log.Printf("Failed querying database for long url %s: %v\n", long.Value, err)
+		}
+
+		sql = `
+INSERT INTO longurl(url)
+VALUES (?)
+			`
+		result, err := s.DB.Exec(sql, long.Value)
+		if err != nil {
+			log.Printf("Failed inserting long URL %s: %+v", long.Value, err)
+			return err
+		} else {
+			long_id, _ = result.LastInsertId()
+		}
+
+		long.Id = long_id
+	}
+
+	return nil
+}
+
+func (y baseStrategy) Alias(long *url.LongUrl, s storage.Storage, options ...interface{}) (url.ShortUrl, error) {
 	var short url.ShortUrl
+	var sql string
 	var err error
 
+	err = y.ensureLongId(long, s)
+	if err != nil {
+		return short, err
+	}
+
 	/** TODO
 	 * - validate alias is available
 	 */
 	short = url.ShortUrl{
 		Value:       long.Value,
-		ShortFor:    long,
+		ShortFor:    *long,
 		Domain:      long.Domain(),
 		Strategy:    y.Name(),
 		SubmittedBy: storage.CurrentUser(),
@@ -51,13 +97,13 @@ func (y baseStrategy) Alias(long url.LongUrl, s storage.Storage, options ...inte
 		IsEnabled:   true,
 	}
 
-	sql := `
-		INSERT INTO shorturl(url, domain, strategy, submittedBy, submittedInfo, isEnabled)
-		VALUES (?, ?, ?, ?, ?, ?)
+	sql = `
+INSERT INTO shorturl(url, longurl, domain, strategy, submittedBy, submittedInfo, isEnabled)
+VALUES (?, ?, ?, ?, ?, ?, ?)
 		`
-	result, err := s.DB.Exec(sql, short.Value, short.Domain, short.Strategy, short.SubmittedBy.Id, short.SubmittedOn, short.IsEnabled)
+	result, err := s.DB.Exec(sql, short.Value, short.ShortFor.Id, short.Domain, short.Strategy, short.SubmittedBy.Id, short.SubmittedOn, short.IsEnabled)
 	if err != nil {
-		log.Printf("Failed inserting %s: %#V", short.Value, err)
+		log.Printf("Failed inserting short %s: %#v", short.Value, err)
 	} else {
 		short.Id, _ = result.LastInsertId()
 	}

+ 17 - 11
strategy/strategy_test.go

@@ -12,7 +12,7 @@ func initTestStorage(t *testing.T) {
 	storage.Service.SetDSN(DSN)
 	err := storage.Service.Open()
 	if err != nil {
-		t.Fatalf("Failed opening the test database: %+V", err)
+		t.Fatalf("Failed opening the test database: %+v", err)
 	}
 }
 
@@ -28,24 +28,28 @@ func TestBaseAlias(t *testing.T) {
 	}
 
 	initTestStorage(t)
+	// defers are executed LIFO
 	defer storage.Service.Close()
 
+	storage.Service.Truncate("shorturl")
+	storage.Service.Truncate("longurl")
+	storage.Service.AddToTruncateList("shorturl")
+	storage.Service.AddToTruncateList("longurl")
+
 	sourceUrl := url.LongUrl{
 		Value: "http://www.example.com",
 	}
-	alias, err := strategy.Alias(sourceUrl, storage.Service)
+	alias, err := strategy.Alias(&sourceUrl, storage.Service)
 	if err != nil {
 		t.Errorf("Failed during Alias(): %+v", err)
 	}
-	if alias.ShortFor != sourceUrl {
-		t.Errorf("Aliasing does not point to proper long URL: expected %+V, got %+V", sourceUrl, alias.ShortFor)
+	if alias.ShortFor.Id != sourceUrl.Id {
+		t.Errorf("Aliasing does not point to proper long URL: expected %+v, got %+v", sourceUrl, alias.ShortFor)
 	}
 
 	if alias.Value != sourceUrl.Value {
-		t.Errorf("Aliasing does not build the proper URL: expected %+V, got %+V", sourceUrl.Value, alias.Value)
+		t.Errorf("Aliasing does not build the proper URL: expected %+v, got %+v", sourceUrl.Value, alias.Value)
 	}
-
-	storage.Service.DB.Exec("TRUNCATE shorturl")
 }
 
 func TestUseCounts(t *testing.T) {
@@ -61,6 +65,10 @@ func TestUseCounts(t *testing.T) {
 
 	initTestStorage(t)
 	defer storage.Service.Close()
+	storage.Service.Truncate("shorturl")
+	storage.Service.Truncate("longurl")
+	storage.Service.AddToTruncateList("shorturl")
+	storage.Service.AddToTruncateList("longurl")
 
 	initialCount := strategy.UseCount(storage.Service)
 	if initialCount != 0 {
@@ -70,7 +78,7 @@ func TestUseCounts(t *testing.T) {
 	sourceUrl := url.LongUrl{
 		Value: "http://www.example.com",
 	}
-	_, err := strategy.Alias(sourceUrl, storage.Service)
+	_, err := strategy.Alias(&sourceUrl, storage.Service)
 	if err != nil {
 		t.Errorf("Failed during Alias(): %+v", err)
 	}
@@ -83,7 +91,7 @@ func TestUseCounts(t *testing.T) {
 	sourceUrl = url.LongUrl{
 		Value: "http://www2.example.com",
 	}
-	_, err = strategy.Alias(sourceUrl, storage.Service)
+	_, err = strategy.Alias(&sourceUrl, storage.Service)
 	if err != nil {
 		t.Errorf("Failed during Alias(): %+v", err)
 	}
@@ -92,6 +100,4 @@ func TestUseCounts(t *testing.T) {
 	if nextCount != initialCount+2 {
 		t.Errorf("Found %d record(s) in test database, expecting %d.", nextCount, initialCount+2)
 	}
-
-	storage.Service.DB.Exec("TRUNCATE shorturl")
 }

+ 1 - 0
url/longurl.go

@@ -3,6 +3,7 @@ package url
 import "github.com/FGM/kurz/storage"
 
 type LongUrl struct {
+	Id    int64
 	Value string
 }