Ver código fonte

Some structural and commenting cleanup.

Frederic G. MARAND 10 anos atrás
pai
commit
6484db1121
14 arquivos alterados com 399 adições e 161 exclusões
  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