├── api └── http.go ├── go.mod ├── go.sum ├── main.go ├── repository ├── mongo │ └── repository.go └── redis │ └── repository.go ├── serializer ├── json │ └── serializer.go └── msgpack │ └── serializer.go ├── shortener ├── logic.go ├── model.go ├── repository.go ├── serializer.go └── service.go └── tool └── msgpack.go /api/http.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/go-chi/chi" 9 | "github.com/pkg/errors" 10 | 11 | js "github.com/tensor-programming/hex-microservice/serializer/json" 12 | ms "github.com/tensor-programming/hex-microservice/serializer/msgpack" 13 | "github.com/tensor-programming/hex-microservice/shortener" 14 | ) 15 | 16 | type RedirectHandler interface { 17 | Get(http.ResponseWriter, *http.Request) 18 | Post(http.ResponseWriter, *http.Request) 19 | } 20 | 21 | type handler struct { 22 | redirectService shortener.RedirectService 23 | } 24 | 25 | func NewHandler(redirectService shortener.RedirectService) RedirectHandler { 26 | return &handler{redirectService: redirectService} 27 | } 28 | 29 | func setupResponse(w http.ResponseWriter, contentType string, body []byte, statusCode int) { 30 | w.Header().Set("Content-Type", contentType) 31 | w.WriteHeader(statusCode) 32 | _, err := w.Write(body) 33 | if err != nil { 34 | log.Println(err) 35 | } 36 | } 37 | 38 | func (h *handler) serializer(contentType string) shortener.RedirectSerializer { 39 | if contentType == "application/x-msgpack" { 40 | return &ms.Redirect{} 41 | } 42 | return &js.Redirect{} 43 | } 44 | 45 | func (h *handler) Get(w http.ResponseWriter, r *http.Request) { 46 | code := chi.URLParam(r, "code") 47 | redirect, err := h.redirectService.Find(code) 48 | if err != nil { 49 | if errors.Cause(err) == shortener.ErrRedirectNotFound { 50 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 51 | return 52 | } 53 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 54 | return 55 | } 56 | http.Redirect(w, r, redirect.URL, http.StatusMovedPermanently) 57 | } 58 | 59 | func (h *handler) Post(w http.ResponseWriter, r *http.Request) { 60 | contentType := r.Header.Get("Content-Type") 61 | requestBody, err := ioutil.ReadAll(r.Body) 62 | if err != nil { 63 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 64 | return 65 | } 66 | redirect, err := h.serializer(contentType).Decode(requestBody) 67 | if err != nil { 68 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 69 | return 70 | } 71 | err = h.redirectService.Store(redirect) 72 | if err != nil { 73 | if errors.Cause(err) == shortener.ErrRedirectInvalid { 74 | http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) 75 | return 76 | } 77 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 78 | return 79 | } 80 | responseBody, err := h.serializer(contentType).Encode(redirect) 81 | if err != nil { 82 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 83 | return 84 | } 85 | setupResponse(w, contentType, responseBody, http.StatusCreated) 86 | } 87 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tensor-programming/hex-microservice 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 // indirect 7 | github.com/go-chi/chi v4.0.2+incompatible 8 | github.com/go-redis/redis v6.15.6+incompatible 9 | github.com/go-stack/stack v1.8.0 // indirect 10 | github.com/golang/snappy v0.0.1 // indirect 11 | github.com/jakm/msgpack-cli v0.0.0-20161211155839-006c2f6bdcb0 // indirect 12 | github.com/kr/pretty v0.1.0 // indirect 13 | github.com/pkg/errors v0.8.1 14 | github.com/stretchr/testify v1.3.0 15 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf 16 | github.com/ugorji/go v1.1.7 // indirect 17 | github.com/vmihailenco/msgpack v4.0.4+incompatible 18 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c // indirect 19 | github.com/xdg/stringprep v1.0.0 // indirect 20 | go.mongodb.org/mongo-driver v1.1.2 21 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect 22 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect 23 | golang.org/x/text v0.3.2 // indirect 24 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 25 | gopkg.in/dealancer/validate.v2 v2.1.0 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= 4 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 5 | github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= 6 | github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 7 | github.com/go-redis/redis v6.15.6+incompatible h1:H9evprGPLI8+ci7fxQx6WNZHJSb7be8FqJQRhdQZ5Sg= 8 | github.com/go-redis/redis v6.15.6+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 9 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 10 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 11 | github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= 12 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 13 | github.com/jakm/msgpack-cli v0.0.0-20161211155839-006c2f6bdcb0 h1:2Ig18vH8bW+Ljd25HS810c08cWB96eOuAyGkA+7WpKM= 14 | github.com/jakm/msgpack-cli v0.0.0-20161211155839-006c2f6bdcb0/go.mod h1:jKlZ4nvhPznT+MK21pObl03+aDNggtjRCgdu00og3nE= 15 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 16 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 17 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 18 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 19 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 20 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 21 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 22 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 23 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 28 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 29 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf h1:Z2X3Os7oRzpdJ75iPqWZc0HeJWFYNCvKsfpQwFpRNTA= 30 | github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= 31 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 32 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 33 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 34 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 35 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 36 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 37 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk= 38 | github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= 39 | github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0= 40 | github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= 41 | go.mongodb.org/mongo-driver v1.1.2 h1:jxcFYjlkl8xaERsgLo+RNquI0epW6zuy/ZRQs6jnrFA= 42 | go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 45 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 49 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 50 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 51 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 53 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 54 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 55 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 56 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 57 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/dealancer/validate.v2 v2.1.0 h1:XY95SZhVH1rBe8uwtnQEsOO79rv8GPwK+P3VWhQfJbA= 59 | gopkg.in/dealancer/validate.v2 v2.1.0/go.mod h1:EipWMj8hVO2/dPXVlYRe9yKcgVd5OttpQDiM1/wZ0DE= 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/go-chi/chi" 6 | "github.com/go-chi/chi/middleware" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "syscall" 13 | 14 | h "github.com/tensor-programming/hex-microservice/api" 15 | mr "github.com/tensor-programming/hex-microservice/repository/mongo" 16 | rr "github.com/tensor-programming/hex-microservice/repository/redis" 17 | 18 | "github.com/tensor-programming/hex-microservice/shortener" 19 | ) 20 | 21 | // https://www.google.com -> 98sj1-293 22 | // http://localhost:8000/98sj1-293 -> https://www.google.com 23 | 24 | // repo <- service -> serializer -> http 25 | 26 | func main() { 27 | repo := chooseRepo() 28 | service := shortener.NewRedirectService(repo) 29 | handler := h.NewHandler(service) 30 | 31 | r := chi.NewRouter() 32 | r.Use(middleware.RequestID) 33 | r.Use(middleware.RealIP) 34 | r.Use(middleware.Logger) 35 | r.Use(middleware.Recoverer) 36 | 37 | r.Get("/{code}", handler.Get) 38 | r.Post("/", handler.Post) 39 | 40 | errs := make(chan error, 2) 41 | go func() { 42 | fmt.Println("Listening on port :8000") 43 | errs <- http.ListenAndServe(httpPort(), r) 44 | 45 | }() 46 | 47 | go func() { 48 | c := make(chan os.Signal, 1) 49 | signal.Notify(c, syscall.SIGINT) 50 | errs <- fmt.Errorf("%s", <-c) 51 | }() 52 | 53 | fmt.Printf("Terminated %s", <-errs) 54 | 55 | } 56 | 57 | func httpPort() string { 58 | port := "8000" 59 | if os.Getenv("PORT") != "" { 60 | port = os.Getenv("PORT") 61 | } 62 | return fmt.Sprintf(":%s", port) 63 | } 64 | 65 | func chooseRepo() shortener.RedirectRepository { 66 | switch os.Getenv("URL_DB") { 67 | case "redis": 68 | redisURL := os.Getenv("REDIS_URL") 69 | repo, err := rr.NewRedisRepository(redisURL) 70 | if err != nil { 71 | log.Fatal(err) 72 | } 73 | return repo 74 | case "mongo": 75 | mongoURL := os.Getenv("MONGO_URL") 76 | mongodb := os.Getenv("MONGO_DB") 77 | mongoTimeout, _ := strconv.Atoi(os.Getenv("MONGO_TIMEOUT")) 78 | repo, err := mr.NewMongoRepository(mongoURL, mongodb, mongoTimeout) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | return repo 83 | } 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /repository/mongo/repository.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "go.mongodb.org/mongo-driver/bson" 9 | "go.mongodb.org/mongo-driver/mongo" 10 | "go.mongodb.org/mongo-driver/mongo/options" 11 | "go.mongodb.org/mongo-driver/mongo/readpref" 12 | 13 | "github.com/tensor-programming/hex-microservice/shortener" 14 | ) 15 | 16 | type mongoRepository struct { 17 | client *mongo.Client 18 | database string 19 | timeout time.Duration 20 | } 21 | 22 | func newMongoClient(mongoURL string, mongoTimeout int) (*mongo.Client, error) { 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Duration(mongoTimeout)*time.Second) 24 | defer cancel() 25 | client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURL)) 26 | if err != nil { 27 | return nil, err 28 | } 29 | err = client.Ping(ctx, readpref.Primary()) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return client, nil 34 | } 35 | 36 | func NewMongoRepository(mongoURL, mongoDB string, mongoTimeout int) (shortener.RedirectRepository, error) { 37 | repo := &mongoRepository{ 38 | timeout: time.Duration(mongoTimeout) * time.Second, 39 | database: mongoDB, 40 | } 41 | client, err := newMongoClient(mongoURL, mongoTimeout) 42 | if err != nil { 43 | return nil, errors.Wrap(err, "repository.NewMongoRepo") 44 | } 45 | repo.client = client 46 | return repo, nil 47 | } 48 | 49 | func (r *mongoRepository) Find(code string) (*shortener.Redirect, error) { 50 | ctx, cancel := context.WithTimeout(context.Background(), r.timeout) 51 | defer cancel() 52 | redirect := &shortener.Redirect{} 53 | collection := r.client.Database(r.database).Collection("redirects") 54 | filter := bson.M{"code": code} 55 | err := collection.FindOne(ctx, filter).Decode(&redirect) 56 | if err != nil { 57 | if err == mongo.ErrNoDocuments { 58 | return nil, errors.Wrap(shortener.ErrRedirectNotFound, "repository.Redirect.Find") 59 | } 60 | return nil, errors.Wrap(err, "repository.Redirect.Find") 61 | } 62 | return redirect, nil 63 | } 64 | 65 | func (r *mongoRepository) Store(redirect *shortener.Redirect) error { 66 | ctx, cancel := context.WithTimeout(context.Background(), r.timeout) 67 | defer cancel() 68 | collection := r.client.Database(r.database).Collection("redirects") 69 | _, err := collection.InsertOne( 70 | ctx, 71 | bson.M{ 72 | "code": redirect.Code, 73 | "url": redirect.URL, 74 | "created_at": redirect.CreatedAt, 75 | }, 76 | ) 77 | if err != nil { 78 | return errors.Wrap(err, "repository.Redirect.Store") 79 | } 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /repository/redis/repository.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/go-redis/redis" 8 | "github.com/pkg/errors" 9 | 10 | "github.com/tensor-programming/hex-microservice/shortener" 11 | ) 12 | 13 | type redisRepository struct { 14 | client *redis.Client 15 | } 16 | 17 | func newRedisClient(redisURL string) (*redis.Client, error) { 18 | opts, err := redis.ParseURL(redisURL) 19 | if err != nil { 20 | return nil, err 21 | } 22 | client := redis.NewClient(opts) 23 | _, err = client.Ping().Result() 24 | if err != nil { 25 | return nil, err 26 | } 27 | return client, nil 28 | } 29 | 30 | func NewRedisRepository(redisURL string) (shortener.RedirectRepository, error) { 31 | repo := &redisRepository{} 32 | client, err := newRedisClient(redisURL) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "repository.NewRedisRepository") 35 | } 36 | repo.client = client 37 | return repo, nil 38 | } 39 | 40 | func (r *redisRepository) generateKey(code string) string { 41 | return fmt.Sprintf("redirect:%s", code) 42 | } 43 | 44 | func (r *redisRepository) Find(code string) (*shortener.Redirect, error) { 45 | redirect := &shortener.Redirect{} 46 | key := r.generateKey(code) 47 | data, err := r.client.HGetAll(key).Result() 48 | if err != nil { 49 | return nil, errors.Wrap(err, "repository.Redirect.Find") 50 | } 51 | if len(data) == 0 { 52 | return nil, errors.Wrap(shortener.ErrRedirectNotFound, "repository.Redirect.Find") 53 | } 54 | createdAt, err := strconv.ParseInt(data["created_at"], 10, 64) 55 | if err != nil { 56 | return nil, errors.Wrap(err, "repository.Redirect.Find") 57 | } 58 | redirect.Code = data["code"] 59 | redirect.URL = data["url"] 60 | redirect.CreatedAt = createdAt 61 | return redirect, nil 62 | } 63 | 64 | func (r *redisRepository) Store(redirect *shortener.Redirect) error { 65 | key := r.generateKey(redirect.Code) 66 | data := map[string]interface{}{ 67 | "code": redirect.Code, 68 | "url": redirect.URL, 69 | "created_at": redirect.CreatedAt, 70 | } 71 | _, err := r.client.HMSet(key, data).Result() 72 | if err != nil { 73 | return errors.Wrap(err, "repository.Redirect.Store") 74 | } 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /serializer/json/serializer.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pkg/errors" 6 | "github.com/tensor-programming/hex-microservice/shortener" 7 | ) 8 | 9 | type Redirect struct{} 10 | 11 | func (r *Redirect) Decode(input []byte) (*shortener.Redirect, error) { 12 | redirect := &shortener.Redirect{} 13 | if err := json.Unmarshal(input, redirect); err != nil { 14 | return nil, errors.Wrap(err, "serializer.Redirect.Decode") 15 | } 16 | return redirect, nil 17 | } 18 | 19 | func (r *Redirect) Encode(input *shortener.Redirect) ([]byte, error) { 20 | rawMsg, err := json.Marshal(input) 21 | if err != nil { 22 | return nil, errors.Wrap(err, "serializer.Redirect.Encode") 23 | } 24 | return rawMsg, nil 25 | } 26 | -------------------------------------------------------------------------------- /serializer/msgpack/serializer.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/tensor-programming/hex-microservice/shortener" 6 | "github.com/vmihailenco/msgpack" 7 | ) 8 | 9 | type Redirect struct{} 10 | 11 | func (r *Redirect) Decode(input []byte) (*shortener.Redirect, error) { 12 | redirect := &shortener.Redirect{} 13 | if err := msgpack.Unmarshal(input, redirect); err != nil { 14 | return nil, errors.Wrap(err, "serializer.Redirect.Decode") 15 | } 16 | return redirect, nil 17 | } 18 | 19 | func (r *Redirect) Encode(input *shortener.Redirect) ([]byte, error) { 20 | rawMsg, err := msgpack.Marshal(input) 21 | if err != nil { 22 | return nil, errors.Wrap(err, "serializer.Redirect.Encode") 23 | } 24 | return rawMsg, nil 25 | } 26 | -------------------------------------------------------------------------------- /shortener/logic.go: -------------------------------------------------------------------------------- 1 | package shortener 2 | 3 | import ( 4 | "errors" 5 | errs "github.com/pkg/errors" 6 | "github.com/teris-io/shortid" 7 | "gopkg.in/dealancer/validate.v2" 8 | "time" 9 | ) 10 | 11 | var ( 12 | ErrRedirectNotFound = errors.New("Redirect Not Found") 13 | ErrRedirectInvalid = errors.New("Redirect Invalid") 14 | ) 15 | 16 | type redirectService struct { 17 | redirectRepo RedirectRepository 18 | } 19 | 20 | func NewRedirectService(redirectRepo RedirectRepository) RedirectService { 21 | return &redirectService{ 22 | redirectRepo, 23 | } 24 | } 25 | 26 | func (r *redirectService) Find(code string) (*Redirect, error) { 27 | return r.redirectRepo.Find(code) 28 | } 29 | 30 | func (r *redirectService) Store(redirect *Redirect) error { 31 | if err := validate.Validate(redirect); err != nil { 32 | return errs.Wrap(ErrRedirectInvalid, "service.Redirect.Store") 33 | } 34 | redirect.Code = shortid.MustGenerate() 35 | redirect.CreatedAt = time.Now().UTC().Unix() 36 | return r.redirectRepo.Store(redirect) 37 | } 38 | -------------------------------------------------------------------------------- /shortener/model.go: -------------------------------------------------------------------------------- 1 | package shortener 2 | 3 | type Redirect struct { 4 | Code string `json:"code" bson:"code" msgpack:"code"` 5 | URL string `json:"url" bson:"url" msgpack:"url" validate:"empty=false & format=url` 6 | CreatedAt int64 `json:"created_at" bson:"created_at" msgpack:"created_at"` 7 | } 8 | -------------------------------------------------------------------------------- /shortener/repository.go: -------------------------------------------------------------------------------- 1 | package shortener 2 | 3 | type RedirectRepository interface { 4 | Find(code string) (*Redirect, error) 5 | Store(redirect *Redirect) error 6 | } 7 | -------------------------------------------------------------------------------- /shortener/serializer.go: -------------------------------------------------------------------------------- 1 | package shortener 2 | 3 | type RedirectSerializer interface { 4 | Decode(input []byte) (*Redirect, error) 5 | Encode(input *Redirect) ([]byte, error) 6 | } 7 | -------------------------------------------------------------------------------- /shortener/service.go: -------------------------------------------------------------------------------- 1 | package shortener 2 | 3 | type RedirectService interface { 4 | Find(code string) (*Redirect, error) 5 | Store(redirect *Redirect) error 6 | } 7 | -------------------------------------------------------------------------------- /tool/msgpack.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/vmihailenco/msgpack" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/tensor-programming/hex-microservice/shortener" 13 | ) 14 | 15 | func httpPort() string { 16 | port := "8000" 17 | if os.Getenv("PORT") != "" { 18 | port = os.Getenv("PORT") 19 | } 20 | return fmt.Sprintf(":%s", port) 21 | } 22 | 23 | func main() { 24 | address := fmt.Sprintf("http://localhost%s", httpPort()) 25 | redirect := shortener.Redirect{} 26 | redirect.URL = "https://github.com/tensor-programming?tab=repositories" 27 | 28 | body, err := msgpack.Marshal(&redirect) 29 | if err != nil { 30 | log.Fatalln(err) 31 | } 32 | 33 | resp, err := http.Post(address, "application/x-msgpack", bytes.NewBuffer(body)) 34 | if err != nil { 35 | log.Fatalln(err) 36 | } 37 | defer resp.Body.Close() 38 | 39 | body, err = ioutil.ReadAll(resp.Body) 40 | if err != nil { 41 | log.Fatalln(err) 42 | } 43 | 44 | msgpack.Unmarshal(body, &redirect) 45 | 46 | log.Printf("%v\n", redirect) 47 | } 48 | --------------------------------------------------------------------------------