mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
Split in three the _examples/tutorial/url-shortener and add the link of the updated article
Former-commit-id: 23e21f5e6317c6f47f1ff8e0565480266d9006f5
This commit is contained in:
parent
bd61bf2405
commit
4229e5859a
|
@ -38,6 +38,10 @@ These types of projects need heart and sacrifices to continue offer the best dev
|
|||
|
||||
### 📑 Table of contents
|
||||
|
||||
<a href="https://github.com/kataras/iris/_examples" alt="documentation and examples">
|
||||
<img style="float:right" src="learn.jpg" width="140px" />
|
||||
</a>
|
||||
|
||||
* [Installation](#-installation)
|
||||
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#mo-10-july-2017--v800)
|
||||
* [Learn](#-learn)
|
||||
|
@ -58,7 +62,7 @@ These types of projects need heart and sacrifices to continue offer the best dev
|
|||
* [Miscellaneous](_examples/#miscellaneous)
|
||||
* [Typescript Automation Tools](typescript/#table-of-contents)
|
||||
* [Tutorial: Online Visitors](_examples/tutorial/online-visitors)
|
||||
* [Tutorial: URL Shortener using BoltDB](_examples/tutorial/url-shortener)
|
||||
* [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7)
|
||||
* [Middleware](middleware/)
|
||||
* [Dockerize](https://github.com/iris-contrib/cloud-native-go)
|
||||
* [Philosophy](#-philosophy)
|
||||
|
@ -74,6 +78,7 @@ The only requirement is the [Go Programming Language](https://golang.org/dl/), a
|
|||
|
||||
```sh
|
||||
$ go get -u github.com/kataras/iris
|
||||
$ go get -u github.com/iris-contrib/middleware/... # useful handlers, optionally
|
||||
```
|
||||
|
||||
> _iris_ takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes.
|
||||
|
|
50
_examples/tutorial/url-shortener/factory.go
Normal file
50
_examples/tutorial/url-shortener/factory.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/satori/go.uuid"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Generator the type to generate keys(short urls)
|
||||
type Generator func() string
|
||||
|
||||
// DefaultGenerator is the defautl url generator
|
||||
var DefaultGenerator = func() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
// Factory is responsible to generate keys(short urls)
|
||||
type Factory struct {
|
||||
store Store
|
||||
generator Generator
|
||||
}
|
||||
|
||||
// NewFactory receives a generator and a store and returns a new url Factory.
|
||||
func NewFactory(generator Generator, store Store) *Factory {
|
||||
return &Factory{
|
||||
store: store,
|
||||
generator: generator,
|
||||
}
|
||||
}
|
||||
|
||||
// Gen generates the key.
|
||||
func (f *Factory) Gen(uri string) (key string, err error) {
|
||||
// we don't return the parsed url because #hash are converted to uri-compatible
|
||||
// and we don't want to encode/decode all the time, there is no need for that,
|
||||
// we save the url as the user expects if the uri validation passed.
|
||||
_, err = url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key = f.generator()
|
||||
// Make sure that the key is unique
|
||||
for {
|
||||
if v := f.store.Get(key); v == "" {
|
||||
break
|
||||
}
|
||||
key = f.generator()
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
|
@ -1,22 +1,19 @@
|
|||
// Package main shows how you can create a simple URL SHortener.
|
||||
// Package main shows how you can create a simple URL Shortener.
|
||||
//
|
||||
// Article: https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7
|
||||
//
|
||||
// $ go get github.com/boltdb/bolt/...
|
||||
// $ go run main.go
|
||||
// $ start http://localhost:8080
|
||||
// $ go get github.com/satori/go.uuid
|
||||
// $ cd $GOPATH/src/github.com/kataras/iris/_examples/tutorial/url-shortener
|
||||
// $ go build
|
||||
// $ ./url-shortener
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"net/url"
|
||||
|
||||
"github.com/kataras/iris"
|
||||
"github.com/kataras/iris/context"
|
||||
"github.com/kataras/iris/view"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -39,7 +36,7 @@ func newApp(db *DB) *iris.Application {
|
|||
factory := NewFactory(DefaultGenerator, db)
|
||||
|
||||
// serve the "./templates" directory's "*.html" files with the HTML std view engine.
|
||||
tmpl := view.HTML("./templates", ".html").Reload(true)
|
||||
tmpl := iris.HTML("./templates", ".html").Reload(true)
|
||||
// register any template func(s) here.
|
||||
//
|
||||
// Look ./templates/index.html#L16
|
||||
|
@ -117,236 +114,3 @@ func newApp(db *DB) *iris.Application {
|
|||
|
||||
return app
|
||||
}
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
// | |
|
||||
// | Store |
|
||||
// | |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
// Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS
|
||||
var Panic = func(v interface{}) {
|
||||
panic(v)
|
||||
}
|
||||
|
||||
// Store is the store interface for urls.
|
||||
// Note: no Del functionality.
|
||||
type Store interface {
|
||||
Set(key string, value string) error // error if something went wrong
|
||||
Get(key string) string // empty value if not found
|
||||
Len() int // should return the number of all the records/tables/buckets
|
||||
Close() // release the store or ignore
|
||||
}
|
||||
|
||||
var (
|
||||
tableURLs = []byte("urls")
|
||||
)
|
||||
|
||||
// DB representation of a Store.
|
||||
// Only one table/bucket which contains the urls, so it's not a fully Database,
|
||||
// it works only with single bucket because that all we need.
|
||||
type DB struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
var _ Store = &DB{}
|
||||
|
||||
// openDatabase open a new database connection
|
||||
// and returns its instance.
|
||||
func openDatabase(stumb string) *bolt.DB {
|
||||
// Open the data(base) file in the current working directory.
|
||||
// It will be created if it doesn't exist.
|
||||
db, err := bolt.Open(stumb, 0600, nil)
|
||||
if err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
|
||||
// create the buckets here
|
||||
var tables = [...][]byte{
|
||||
tableURLs,
|
||||
}
|
||||
|
||||
db.Update(func(tx *bolt.Tx) (err error) {
|
||||
for _, table := range tables {
|
||||
_, err = tx.CreateBucketIfNotExists(table)
|
||||
if err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// NewDB returns a new DB instance, its connection is opened.
|
||||
// DB implements the Store.
|
||||
func NewDB(stumb string) *DB {
|
||||
return &DB{
|
||||
db: openDatabase(stumb),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets a shorten url and its key
|
||||
// Note: Caller is responsible to generate a key.
|
||||
func (d *DB) Set(key string, value string) error {
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists(tableURLs)
|
||||
// Generate ID for the url
|
||||
// Note: we could use that instead of a random string key
|
||||
// but we want to simulate a real-world url shortener
|
||||
// so we skip that.
|
||||
// id, _ := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := []byte(key)
|
||||
valueB := []byte(value)
|
||||
c := b.Cursor()
|
||||
|
||||
found := false
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(valueB, v) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// if value already exists don't re-put it.
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.Put(k, []byte(value))
|
||||
})
|
||||
}
|
||||
|
||||
// Clear clears all the database entries for the table urls.
|
||||
func (d *DB) Clear() error {
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.DeleteBucket(tableURLs)
|
||||
})
|
||||
}
|
||||
|
||||
// Get returns a url by its key.
|
||||
//
|
||||
// Returns an empty string if not found.
|
||||
func (d *DB) Get(key string) (value string) {
|
||||
keyB := []byte(key)
|
||||
d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(keyB, k) {
|
||||
value = string(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByValue returns all keys for a specific (original) url value.
|
||||
func (d *DB) GetByValue(value string) (keys []string) {
|
||||
valueB := []byte(value)
|
||||
d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
// first for the bucket's table "urls"
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(valueB, v) {
|
||||
keys = append(keys, string(k))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Len returns all the "shorted" urls length
|
||||
func (d *DB) Len() (num int) {
|
||||
d.db.View(func(tx *bolt.Tx) error {
|
||||
|
||||
// Assume bucket exists and has keys
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.ForEach(func([]byte, []byte) error {
|
||||
num++
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Close shutdowns the data(base) connection.
|
||||
func (d *DB) Close() {
|
||||
if err := d.db.Close(); err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// +------------------------------------------------------------+
|
||||
// | |
|
||||
// | Factory |
|
||||
// | |
|
||||
// +------------------------------------------------------------+
|
||||
|
||||
// Generator the type to generate keys(short urls)
|
||||
type Generator func() string
|
||||
|
||||
// DefaultGenerator is the defautl url generator
|
||||
var DefaultGenerator = func() string {
|
||||
return uuid.NewV4().String()
|
||||
}
|
||||
|
||||
// Factory is responsible to generate keys(short urls)
|
||||
type Factory struct {
|
||||
store Store
|
||||
generator Generator
|
||||
}
|
||||
|
||||
// NewFactory receives a generator and a store and returns a new url Factory.
|
||||
func NewFactory(generator Generator, store Store) *Factory {
|
||||
return &Factory{
|
||||
store: store,
|
||||
generator: generator,
|
||||
}
|
||||
}
|
||||
|
||||
// Gen generates the key.
|
||||
func (f *Factory) Gen(uri string) (key string, err error) {
|
||||
// we don't return the parsed url because #hash are converted to uri-compatible
|
||||
// and we don't want to encode/decode all the time, there is no need for that,
|
||||
// we save the url as the user expects if the uri validation passed.
|
||||
_, err = url.ParseRequestURI(uri)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
key = f.generator()
|
||||
// Make sure that the key is unique
|
||||
for {
|
||||
if v := f.store.Get(key); v == "" {
|
||||
break
|
||||
}
|
||||
key = f.generator()
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
|
|
@ -3,13 +3,15 @@ package main
|
|||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/httptest"
|
||||
)
|
||||
|
||||
// TestURLShortener tests the simple tasks of our url shortener application.
|
||||
// Note that it's a pure test.
|
||||
// The rest possible checks is up to you, take it as as an exercise!
|
||||
func TestURLShortener(t *testing.T) {
|
||||
// temp db file
|
||||
f, err := ioutil.TempFile("", "shortener")
|
||||
|
@ -41,6 +43,7 @@ func TestURLShortener(t *testing.T) {
|
|||
e.POST("/shorten").
|
||||
WithFormField("url", originalURL).Expect().
|
||||
Status(httptest.StatusOK).Body().Contains("<pre><a target='_new' href=")
|
||||
|
||||
keys2 := db.GetByValue(originalURL)
|
||||
if got := len(keys2); got != 1 {
|
||||
t.Fatalf("expected to have 1 keys even if we save the same original url but saved %d short urls", got)
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
package main
|
||||
|
||||
// Version is the current version of the iris url-shortener example.
|
||||
const Version = "0.0.2"
|
184
_examples/tutorial/url-shortener/store.go
Normal file
184
_examples/tutorial/url-shortener/store.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS
|
||||
var Panic = func(v interface{}) {
|
||||
panic(v)
|
||||
}
|
||||
|
||||
// Store is the store interface for urls.
|
||||
// Note: no Del functionality.
|
||||
type Store interface {
|
||||
Set(key string, value string) error // error if something went wrong
|
||||
Get(key string) string // empty value if not found
|
||||
Len() int // should return the number of all the records/tables/buckets
|
||||
Close() // release the store or ignore
|
||||
}
|
||||
|
||||
var (
|
||||
tableURLs = []byte("urls")
|
||||
)
|
||||
|
||||
// DB representation of a Store.
|
||||
// Only one table/bucket which contains the urls, so it's not a fully Database,
|
||||
// it works only with single bucket because that all we need.
|
||||
type DB struct {
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
var _ Store = &DB{}
|
||||
|
||||
// openDatabase open a new database connection
|
||||
// and returns its instance.
|
||||
func openDatabase(stumb string) *bolt.DB {
|
||||
// Open the data(base) file in the current working directory.
|
||||
// It will be created if it doesn't exist.
|
||||
db, err := bolt.Open(stumb, 0600, nil)
|
||||
if err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
|
||||
// create the buckets here
|
||||
var tables = [...][]byte{
|
||||
tableURLs,
|
||||
}
|
||||
|
||||
db.Update(func(tx *bolt.Tx) (err error) {
|
||||
for _, table := range tables {
|
||||
_, err = tx.CreateBucketIfNotExists(table)
|
||||
if err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
// NewDB returns a new DB instance, its connection is opened.
|
||||
// DB implements the Store.
|
||||
func NewDB(stumb string) *DB {
|
||||
return &DB{
|
||||
db: openDatabase(stumb),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets a shorten url and its key
|
||||
// Note: Caller is responsible to generate a key.
|
||||
func (d *DB) Set(key string, value string) error {
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
b, err := tx.CreateBucketIfNotExists(tableURLs)
|
||||
// Generate ID for the url
|
||||
// Note: we could use that instead of a random string key
|
||||
// but we want to simulate a real-world url shortener
|
||||
// so we skip that.
|
||||
// id, _ := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k := []byte(key)
|
||||
valueB := []byte(value)
|
||||
c := b.Cursor()
|
||||
|
||||
found := false
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(valueB, v) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// if value already exists don't re-put it.
|
||||
if found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return b.Put(k, []byte(value))
|
||||
})
|
||||
}
|
||||
|
||||
// Clear clears all the database entries for the table urls.
|
||||
func (d *DB) Clear() error {
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.DeleteBucket(tableURLs)
|
||||
})
|
||||
}
|
||||
|
||||
// Get returns a url by its key.
|
||||
//
|
||||
// Returns an empty string if not found.
|
||||
func (d *DB) Get(key string) (value string) {
|
||||
keyB := []byte(key)
|
||||
d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(keyB, k) {
|
||||
value = string(v)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetByValue returns all keys for a specific (original) url value.
|
||||
func (d *DB) GetByValue(value string) (keys []string) {
|
||||
valueB := []byte(value)
|
||||
d.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
// first for the bucket's table "urls"
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
if bytes.Equal(valueB, v) {
|
||||
keys = append(keys, string(k))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Len returns all the "shorted" urls length
|
||||
func (d *DB) Len() (num int) {
|
||||
d.db.View(func(tx *bolt.Tx) error {
|
||||
|
||||
// Assume bucket exists and has keys
|
||||
b := tx.Bucket(tableURLs)
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.ForEach(func([]byte, []byte) error {
|
||||
num++
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Close shutdowns the data(base) connection.
|
||||
func (d *DB) Close() {
|
||||
if err := d.db.Close(); err != nil {
|
||||
Panic(err)
|
||||
}
|
||||
}
|
1
learn.jpg.REMOVED.git-id
Normal file
1
learn.jpg.REMOVED.git-id
Normal file
|
@ -0,0 +1 @@
|
|||
efa10f1e68d7042b7340b8db37c38e18e351bc3f
|
Loading…
Reference in New Issue
Block a user