Browse Source

Some structural and commenting cleanup.

Frederic G. MARAND 10 năm trước cách đây
mục cha
commit
6484db1121
14 tập tin đã thay đổi với 399 bổ sung161 xóa
  1. 68 0
      doc/deps.svg
  2. 4 3
      kurz.go
  3. 0 4
      storage/longurl.go
  4. 0 4
      storage/shorturl.go
  5. 46 8
      storage/storage.go
  6. 0 142
      storage/strategy.go
  7. 8 0
      storage/user.go
  8. 34 0
      strategy/hexcrc32.go
  9. 56 0
      strategy/manual.go
  10. 98 0
      strategy/strategies.go
  11. 61 0
      strategy/strategy.go
  12. 8 0
      url/longurl.go
  13. 15 0
      url/shorturl.go
  14. 1 0
      web/web.go

+ 68 - 0
doc/deps.svg

@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.26.3 (20100126.1600)
+ -->
+<!-- Title: _anonymous_0 Pages: 1 -->
+<svg width="274pt" height="332pt"
+ viewBox="0.00 0.00 274.00 332.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph1" class="graph" transform="scale(1 1) rotate(0) translate(4 328)">
+<title>_anonymous_0</title>
+<polygon fill="white" stroke="white" points="-4,5 -4,-328 271,-328 271,5 -4,5"/>
+<!-- N0 -->
+<g id="node1" class="node"><title>N0</title>
+<polygon fill="none" stroke="black" points="228,-180 40,-180 40,-144 228,-144 228,-180"/>
+<text text-anchor="middle" x="134" y="-158.4" font-family="Times Roman,serif" font-size="14.00">github.com/FGM/kurz/url</text>
+</g>
+<!-- N1 -->
+<g id="node2" class="node"><title>N1</title>
+<polygon fill="none" stroke="black" points="244,-108 24,-108 24,-72 244,-72 244,-108"/>
+<text text-anchor="middle" x="134" y="-86.4" font-family="Times Roman,serif" font-size="14.00">github.com/FGM/kurz/storage</text>
+</g>
+<!-- N0&#45;&gt;N1 -->
+<g id="edge12" class="edge"><title>N0&#45;&gt;N1</title>
+<path fill="none" stroke="black" d="M134,-143.831C134,-136.131 134,-126.974 134,-118.417"/>
+<polygon fill="black" stroke="black" points="137.5,-118.413 134,-108.413 130.5,-118.413 137.5,-118.413"/>
+</g>
+<!-- N4 -->
+<g id="node5" class="node"><title>N4</title>
+<polygon fill="none" stroke="black" points="248,-36 20,-36 20,-1.77636e-14 248,-3.55271e-15 248,-36"/>
+<text text-anchor="middle" x="134" y="-14.4" font-family="Times Roman,serif" font-size="14.00">github.com/go&#45;sql&#45;driver/mysql</text>
+</g>
+<!-- N1&#45;&gt;N4 -->
+<g id="edge6" class="edge"><title>N1&#45;&gt;N4</title>
+<path fill="none" stroke="black" d="M134,-71.8314C134,-64.131 134,-54.9743 134,-46.4166"/>
+<polygon fill="black" stroke="black" points="137.5,-46.4132 134,-36.4133 130.5,-46.4133 137.5,-46.4132"/>
+</g>
+<!-- N2 -->
+<g id="node3" class="node"><title>N2</title>
+<polygon fill="none" stroke="black" points="164,-324 8.52651e-14,-324 0,-288 164,-288 164,-324"/>
+<text text-anchor="middle" x="82" y="-302.4" font-family="Times Roman,serif" font-size="14.00">github.com/FGM/kurz</text>
+</g>
+<!-- N2&#45;&gt;N1 -->
+<g id="edge2" class="edge"><title>N2&#45;&gt;N1</title>
+<path fill="none" stroke="black" d="M58.1509,-287.928C47.7058,-278.504 36.574,-266.02 31,-252 13.2664,-207.396 6.15863,-185.072 31,-144 39.0144,-130.749 51.4722,-120.591 64.9914,-112.862"/>
+<polygon fill="black" stroke="black" points="66.7728,-115.88 73.9926,-108.126 63.5132,-109.685 66.7728,-115.88"/>
+</g>
+<!-- N3 -->
+<g id="node4" class="node"><title>N3</title>
+<polygon fill="none" stroke="black" points="266,-252 40,-252 40,-216 266,-216 266,-252"/>
+<text text-anchor="middle" x="153" y="-230.4" font-family="Times Roman,serif" font-size="14.00">github.com/FGM/kurz/strategy</text>
+</g>
+<!-- N2&#45;&gt;N3 -->
+<g id="edge4" class="edge"><title>N2&#45;&gt;N3</title>
+<path fill="none" stroke="black" d="M99.9163,-287.831C108.344,-279.285 118.541,-268.944 127.736,-259.62"/>
+<polygon fill="black" stroke="black" points="130.313,-261.991 134.842,-252.413 125.329,-257.076 130.313,-261.991"/>
+</g>
+<!-- N3&#45;&gt;N0 -->
+<g id="edge10" class="edge"><title>N3&#45;&gt;N0</title>
+<path fill="none" stroke="black" d="M148.205,-215.831C146.151,-208.046 143.704,-198.773 141.424,-190.135"/>
+<polygon fill="black" stroke="black" points="144.795,-189.189 138.859,-180.413 138.027,-190.975 144.795,-189.189"/>
+</g>
+<!-- N3&#45;&gt;N1 -->
+<g id="edge8" class="edge"><title>N3&#45;&gt;N1</title>
+<path fill="none" stroke="black" d="M197.979,-215.986C213.174,-207.476 228.443,-195.673 237,-180 244.667,-165.957 245.28,-157.691 237,-144 228.986,-130.749 216.528,-120.591 203.009,-112.862"/>
+<polygon fill="black" stroke="black" points="204.487,-109.685 194.007,-108.126 201.227,-115.88 204.487,-109.685"/>
+</g>
+</g>
+</svg>

+ 4 - 3
kurz.go

@@ -1,17 +1,18 @@
 package main
 
 import (
+	"fmt"
 	"github.com/FGM/kurz/storage"
-	"github.com/davecgh/go-spew/spew"
+	"github.com/FGM/kurz/strategy"
 	"log"
 )
 
 func main() {
-	err := storage.Service.Open("someuser:somepass@tcp(localhost:3306)/go_kurz")
+	err := storage.Service.Open()
 	if err != nil {
 		log.Fatal(err)
 	}
 	defer storage.Service.Close()
 
-	spew.Dump(storage.StrategyStatistics(storage.Service))
+	fmt.Print(strategy.Statistics.Refresh(storage.Service))
 }

+ 0 - 4
storage/longurl.go

@@ -1,4 +0,0 @@
-package storage
-
-type LongUrl struct {
-}

+ 0 - 4
storage/shorturl.go

@@ -1,4 +0,0 @@
-package storage
-
-type ShortUrl struct {
-}

+ 46 - 8
storage/storage.go

@@ -1,31 +1,69 @@
+/*
+The "storage" package in Kurz provides the data model.
+
+In its current implementation:
+
+  - it is specific to MySQL/MariaDB and compatibles SQL engines.
+  - data model and data access are not isolated
+  - it assumes the DB has been initialized by importing the data/db-schema.sql file
+  - it initializes the connection info based on the "dsn" CLI flag
+*/
 package storage
 
 /**
 TODO use a CLI flag to clear the database on init
+TODO use a CLI flag to install the database schema
 */
 
 import (
 	"database/sql"
+	"flag"
 	_ "github.com/go-sql-driver/mysql"
-	"log"
 )
 
 type Storage struct {
-	Name string
-	DB   *sql.DB
+	DB  *sql.DB
+	DSN string
 }
 
 var Service Storage = Storage{}
 
-func (s *Storage) Open(dbName string) error {
+/*
+Open() established the database connection, making sure it is actually available instead of just preparing the connection info.
+*/
+func (s *Storage) Open() error {
 	var err error
-	s.DB, err = sql.Open("mysql", dbName)
-	if err != nil {
-		log.Fatal(err)
+
+	s.DB, err = sql.Open("mysql", s.DSN)
+	if err == nil {
+		err = s.DB.Ping()
 	}
+
 	return err
 }
 
+/*
+Close() closes the database connection and releases its information.
+*/
 func (s *Storage) Close() {
-	s.DB.Close()
+	if s.DB != nil {
+		s.DB.Close()
+	}
+	s.DB = nil
+}
+
+/*
+SetDSN() sets the DSN information for the storage.
+*/
+func (s *Storage) SetDSN(dsn string) {
+	s.DSN = dsn
+}
+
+/*
+init() initializes the storage information from the command-line flag "dsn".
+*/
+func init() {
+	var dsn = flag.String("dsn", "root:@tcp(localhost:3306)/go_kurz", "some_user:some_pass@tcp(some_host:some_port)/some_db")
+	flag.Parse()
+	Service.SetDSN(*dsn)
 }

+ 0 - 142
storage/strategy.go

@@ -1,142 +0,0 @@
-package storage
-
-import (
-	"database/sql"
-	"errors"
-	"log"
-)
-
-type Strategy interface {
-	Name() string
-	Alias(url LongUrl) (ShortUrl, error)
-	UseCount(storage Storage) int
-}
-
-type baseStrategy struct{}
-
-func (y baseStrategy) Name() string {
-	return "base"
-}
-
-func (y baseStrategy) Alias(long LongUrl) (ShortUrl, error) {
-	var ret ShortUrl
-	var err error = errors.New("Base strategy is abstract")
-
-	return ret, err
-}
-
-/**
-Any nonzero result is likely an error.
-*/
-func (y baseStrategy) UseCount(s Storage) int {
-	sql := `
-SELECT COUNT(*)
-FROM shorturl
-WHERE strategy = ?
-	`
-	var count int
-	err := s.DB.QueryRow(sql, y.Name()).Scan(&count)
-	if err != nil {
-		count = 0
-		log.Printf("Failed querying database for base strategy use count: %v\n", err)
-	}
-
-	return count
-}
-
-/*
-Legacy strategy : hex dump for crc32 hash of source URL.
-
-  - Pros :
-    - URLs are easy to communicate over the phone, especially to programmers, even in poor sound conditions.
-  - Cons :
-    - They are rather long
-    - Does not handle collisions: first come, first serve. Later entries are simply rejected.
-*/
-type HexCrc32Strategy struct {
-	base baseStrategy
-}
-
-func (s HexCrc32Strategy) Name() string {
-	return "hexCrc32"
-}
-
-func (s HexCrc32Strategy) Alias(url LongUrl) (ShortUrl, error) {
-	var ret ShortUrl
-
-	return ret, errors.New("HexCrc32 not implemented yet")
-}
-
-func (y HexCrc32Strategy) UseCount(s Storage) int {
-	return y.base.UseCount(s)
-}
-
-type ManualStrategy struct {
-	base baseStrategy
-}
-
-func (y ManualStrategy) Name() string {
-	return "manual"
-}
-
-func (y ManualStrategy) Alias(long LongUrl) (ShortUrl, error) {
-	var ret ShortUrl
-
-	return ret, errors.New("Manual not implemented yet")
-}
-
-func (y ManualStrategy) UseCount(s Storage) int {
-	return y.base.UseCount(s)
-}
-
-var Strategies map[string]Strategy
-
-func init() {
-	var strategyImplementations []Strategy = []Strategy{
-		baseStrategy{},
-		HexCrc32Strategy{},
-		ManualStrategy{},
-	}
-
-	Strategies = make(map[string]Strategy, len(strategyImplementations))
-	for _, s := range strategyImplementations {
-		Strategies[s.Name()] = s
-	}
-}
-
-func StrategyStatistics(s Storage) map[string]int64 {
-	var ret map[string]int64 = make(map[string]int64, len(Strategies))
-	var err error
-	var strategyResult sql.NullString
-	var countResult sql.NullInt64
-
-	sql := `
-SELECT strategy, COUNT(*)
-FROM shorturl
-GROUP BY strategy
-	`
-
-	rows, err := s.DB.Query(sql)
-	if err != nil {
-		log.Printf("Failed querying database for strategy statistics: %v\n", err)
-	}
-	defer rows.Close()
-	for rows.Next() {
-		if err = rows.Scan(&strategyResult, &countResult); err != nil {
-			log.Fatal(err)
-		}
-		validStrategy, ok := Strategies[strategyResult.String]
-		if !ok {
-			log.Fatalf("'%s' is not a valid strategy\n", strategyResult)
-		}
-		ret[validStrategy.Name()] = countResult.Int64
-	}
-
-	for name, _ := range Strategies {
-		_, ok := ret[name]
-		if !ok {
-			ret[name] = 0
-		}
-	}
-	return ret
-}

+ 8 - 0
storage/user.go

@@ -0,0 +1,8 @@
+package storage
+
+type User struct{}
+
+func CurrentUser() User {
+	var ret User
+	return ret
+}

+ 34 - 0
strategy/hexcrc32.go

@@ -0,0 +1,34 @@
+package strategy
+
+import (
+	"errors"
+	"github.com/FGM/kurz/storage"
+	"github.com/FGM/kurz/url"
+)
+
+/*
+HexCrc32Strategy is a legacy AliasingStrategy : hex dump for crc32 hash of source URL.
+
+  - Pros :
+    - URLs are easy to communicate over the phone, especially to programmers, even in poor sound conditions.
+  - Cons :
+    - They are rather long
+    - Does not handle collisions: first come, first serve. Later entries are simply rejected.
+*/
+type HexCrc32Strategy struct {
+	base baseStrategy
+}
+
+func (s HexCrc32Strategy) Name() string {
+	return "hexCrc32"
+}
+
+func (s HexCrc32Strategy) Alias(short url.LongUrl, options ...interface{}) (url.ShortUrl, error) {
+	var ret url.ShortUrl
+
+	return ret, errors.New("HexCrc32 not implemented yet")
+}
+
+func (y HexCrc32Strategy) UseCount(s storage.Storage) int {
+	return y.base.UseCount(s)
+}

+ 56 - 0
strategy/manual.go

@@ -0,0 +1,56 @@
+package strategy
+
+import (
+	"errors"
+	"fmt"
+	"github.com/FGM/kurz/storage"
+	"github.com/FGM/kurz/url"
+	"time"
+)
+
+type ManualStrategy struct {
+	base baseStrategy
+}
+
+func (y ManualStrategy) Name() string {
+	return "manual"
+}
+
+/*
+Alias() implements AliasingStrategy.Alias().
+
+  - options is expected to be a non empty single string
+*/
+func (y ManualStrategy) Alias(long url.LongUrl, options ...interface{}) (url.ShortUrl, error) {
+	var ret url.ShortUrl
+	var err error
+	if len(options) != 1 {
+		err = errors.New("ManualString.Alias() takes a single optional parameter, which is the requested short URL.")
+		return ret, err
+	} else {
+		requestedAlias, ok := options[0].(string)
+		if !ok {
+			err = errors.New(fmt.Sprintf("ManualString.Alias() optional parameter must be a string, got: %+V", requestedAlias))
+			return ret, err
+		}
+
+		err = nil
+		/** TODO
+		 * - validate alias is available
+		 */
+		ret = url.ShortUrl{
+			Value:       requestedAlias,
+			ShortFor:    long,
+			Domain:      long.Domain(),
+			Strategy:    y.Name(),
+			SubmittedBy: storage.CurrentUser(),
+			SubmittedOn: time.Now().UTC().Unix(),
+			IsEnabled:   true,
+		}
+	}
+	return ret, err
+}
+
+func (y ManualStrategy) UseCount(s storage.Storage) int {
+	return y.base.UseCount(s)
+}

+ 98 - 0
strategy/strategies.go

@@ -0,0 +1,98 @@
+package strategy
+
+import (
+	"bytes"
+	"database/sql"
+	"fmt"
+	"github.com/FGM/kurz/storage"
+	"log"
+)
+
+// StrategyMap adds helper methods to a map of AliasingStrategy
+type StrategyMap map[string]AliasingStrategy
+
+// IsValid() checks whether a strategy is registered for a given name.
+func (m StrategyMap) IsValid(name string) bool {
+	var ok bool
+	_, ok = m[name]
+	return ok
+}
+
+// Strategies is a global instance of the registered strategies.
+var Strategies StrategyMap
+
+func MakeStrategies(list []AliasingStrategy) StrategyMap {
+	ret := make(map[string]AliasingStrategy, len(list))
+
+	for _, s := range list {
+		ret[s.Name()] = s
+	}
+
+	return ret
+}
+
+// StatisticsMap adds helper methods to a map of AliasingStrategy use counts.
+type StatisticsMap map[string]int64
+
+// Statistics is a global instance of the AliasingStrategy use counts.
+var Statistics StatisticsMap
+
+// Get() fetches the AliasingStrategy use counts from the database and returns the updated Statistics.
+func (ss StatisticsMap) Refresh(s storage.Storage) StatisticsMap {
+	var err error
+	var strategyResult sql.NullString
+	var countResult sql.NullInt64
+
+	sql := `
+SELECT strategy, COUNT(*)
+FROM shorturl
+GROUP BY strategy
+	`
+
+	rows, err := s.DB.Query(sql)
+	if err != nil {
+		log.Printf("Failed querying database for strategy statistics: %v\n", err)
+	}
+	defer rows.Close()
+	for rows.Next() {
+		if err = rows.Scan(&strategyResult, &countResult); err != nil {
+			log.Fatal(err)
+		}
+
+		if !Strategies.IsValid(strategyResult.String) {
+			log.Fatalf("'%#V' is not a valid strategy\n", strategyResult)
+		}
+		ss[strategyResult.String] = countResult.Int64
+	}
+
+	for name, _ := range Strategies {
+		_, ok := ss[name]
+		if !ok {
+			ss[name] = 0
+		}
+	}
+
+	return ss
+}
+
+// String() implements the fmt.Stringer interface.
+func (ss StatisticsMap) String() string {
+	var buf bytes.Buffer
+
+	for name, count := range ss {
+		buf.WriteString(fmt.Sprintf("%-8s: %d\n", name, count))
+	}
+
+	return fmt.Sprint(buf.String())
+}
+
+// init() initializes the global variables provided by the package.
+func init() {
+	Strategies = MakeStrategies([]AliasingStrategy{
+		baseStrategy{},
+		HexCrc32Strategy{},
+		ManualStrategy{},
+	})
+
+	Statistics = make(map[string]int64, len(Strategies))
+}

+ 61 - 0
strategy/strategy.go

@@ -0,0 +1,61 @@
+/*
+The "strategy" package in Kurz provides the aliasing strategies.
+
+Files
+	- strategy.go contains the interface and base implementation
+	- strategies.go contains the strategy instances and utilities
+	- manual.go contains the "manual" strategy
+	- hexrcr32.go contains the "hexcrc32" strategy
+*/
+package strategy
+
+import (
+	"errors"
+	"github.com/FGM/kurz/storage"
+	"github.com/FGM/kurz/url"
+	"log"
+)
+
+/*
+AliasingStrategy defines the operations provided by the various aliasing implementations:
+
+The options parameter for Alias() MAY be used by some strategies, in which case they
+have to define their expectations about it.
+*/
+type AliasingStrategy interface {
+	Name() string                                                        // Return the name of the strategy object
+	Alias(url url.LongUrl, 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{}
+
+func (y baseStrategy) Name() string {
+	return "base"
+}
+
+func (y baseStrategy) Alias(long url.LongUrl, options ...interface{}) (url.ShortUrl, error) {
+	var ret url.ShortUrl
+	var err error = errors.New("Base strategy is abstract")
+
+	return ret, err
+}
+
+/**
+Any nonzero result is likely an error.
+*/
+func (y baseStrategy) UseCount(s storage.Storage) int {
+	sql := `
+SELECT COUNT(*)
+FROM shorturl
+WHERE strategy = ?
+	`
+	var count int
+	err := s.DB.QueryRow(sql, y.Name()).Scan(&count)
+	if err != nil {
+		count = 0
+		log.Printf("Failed querying database for base strategy use count: %v\n", err)
+	}
+
+	return count
+}

+ 8 - 0
url/longurl.go

@@ -0,0 +1,8 @@
+package url
+
+type LongUrl struct {
+}
+
+func (l LongUrl) Domain() string {
+	return "default"
+}

+ 15 - 0
url/shorturl.go

@@ -0,0 +1,15 @@
+package url
+
+import (
+	"github.com/FGM/kurz/storage"
+)
+
+type ShortUrl struct {
+	Value       string
+	ShortFor    LongUrl
+	Domain      string
+	Strategy    string // name of AliasingStrategy
+	SubmittedBy storage.User
+	SubmittedOn int64 // UNIX timestamp
+	IsEnabled   bool
+}

+ 1 - 0
web/web.go

@@ -0,0 +1 @@
+package web