Skip to content

Commit

Permalink
Update versions example to use latest render subpkg
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Kieltyka authored and Peter Kieltyka committed Mar 30, 2017
1 parent 2810210 commit 7f80c65
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 131 deletions.
54 changes: 40 additions & 14 deletions _examples/versions/main.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
//
// Versions
// ========
// This example demonstrates the use of the render subpackage and its
// render.Presenter interface to transform a handler response to easily
// handle API versioning.
// This example demonstrates the use of the render subpackage, with
// a quick concept for how to support multiple api versions.
//
package main

Expand All @@ -30,34 +29,38 @@ func main() {
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(render.UsePresenter(v3.Presenter)) // API version 3 (latest) by default.

// Redirect for Example convenience.
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/v3/articles/1", 302)
})

// API version 3.
r.Route("/v3", func(r chi.Router) {
r.Use(apiVersionCtx("v3"))
r.Mount("/articles", articleRouter())
})

// API version 2.
r.Route("/v2", func(r chi.Router) {
r.Use(render.UsePresenter(v2.Presenter))
r.Use(apiVersionCtx("v2"))
r.Mount("/articles", articleRouter())
})

// API version 1.
r.Route("/v1", func(r chi.Router) {
r.Use(randomErrorMiddleware) // Simulate random error, ie. version 1 is buggy.
r.Use(render.UsePresenter(v1.Presenter))
r.Use(apiVersionCtx("v1"))
r.Mount("/articles", articleRouter())
})

http.ListenAndServe(":3333", r)
}

func apiVersionCtx(version string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = r.WithContext(context.WithValue(r.Context(), "api.version", version))
next.ServeHTTP(w, r)
})
}
}

func articleRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", listArticles)
Expand All @@ -70,17 +73,28 @@ func articleRouter() http.Handler {
}

func listArticles(w http.ResponseWriter, r *http.Request) {
articles := make(chan *data.Article, 5)
articles := make(chan render.Renderer, 5)

// Load data asynchronously into the channel (simulate slow storage):
go func() {
for i := 1; i <= 10; i++ {
articles <- &data.Article{
article := &data.Article{
ID: i,
Title: fmt.Sprintf("Article #%v", i),
Data: []string{"one", "two", "three", "four"},
CustomDataForAuthUsers: "secret data for auth'd users only",
}

apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
articles <- v1.NewArticleResponse(article)
case "v2":
articles <- v2.NewArticleResponse(article)
default:
articles <- v3.NewArticleResponse(article)
}

time.Sleep(100 * time.Millisecond)
}
close(articles)
Expand Down Expand Up @@ -114,7 +128,19 @@ func getArticle(w http.ResponseWriter, r *http.Request) {
return
}

render.Respond(w, r, article)
var payload render.Renderer

apiVersion := r.Context().Value("api.version").(string)
switch apiVersion {
case "v1":
payload = v1.NewArticleResponse(article)
case "v2":
payload = v2.NewArticleResponse(article)
default:
payload = v3.NewArticleResponse(article)
}

render.Render(w, r, payload)
}

func randomErrorMiddleware(next http.Handler) http.Handler {
Expand Down
25 changes: 6 additions & 19 deletions _examples/versions/presenter/v1/article.go
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
package v1

import (
"fmt"
"net/http"

"github.com/pressly/chi/_examples/versions/presenter/v2"
"github.com/pressly/chi/render"
"github.com/pressly/chi/_examples/versions/data"
)

// Article presented in API version 1.
type Article struct {
*v2.Article `json:",inline" xml:",inline"`
*data.Article

Data map[string]bool `json:"data" xml:"data"`
}

var Presenter = render.NewPresenter(ArticleV2ToV1)

func init() {
Presenter.CopyFrom(v2.Presenter)
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
return nil
}

func ArticleV2ToV1(r *http.Request, from *v2.Article) (*Article, error) {
to := &Article{
Article: from,
Data: map[string]bool{},
}
to.SelfURL = fmt.Sprintf("http://localhost:3333/v1?id=%v", from.ID)
to.APIVersion = "v1"
for _, item := range from.Data {
to.Data[item] = true
}
return to, nil
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
23 changes: 9 additions & 14 deletions _examples/versions/presenter/v2/article.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"fmt"
"net/http"

"github.com/pressly/chi/_examples/versions/presenter/v3"
"github.com/pressly/chi/render"
"github.com/pressly/chi/_examples/versions/data"
)

// Article presented in API version 2.
type Article struct {
*v3.Article `json:",inline" xml:",inline"`
// *v3.Article `json:",inline" xml:",inline"`

*data.Article

// Additional fields.
SelfURL string `json:"self_url" xml:"self_url"`
Expand All @@ -19,17 +20,11 @@ type Article struct {
URL interface{} `json:"url,omitempty" xml:"url,omitempty"`
}

var Presenter = render.NewPresenter(ArticleV3ToV2)

func init() {
Presenter.CopyFrom(v3.Presenter)
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.SelfURL = fmt.Sprintf("http://localhost:3333/v2?id=%v", a.ID)
return nil
}

func ArticleV3ToV2(r *http.Request, from *v3.Article) (*Article, error) {
to := &Article{
Article: from,
SelfURL: fmt.Sprintf("http://localhost:3333/v2?id=%v", from.ID),
}
to.APIVersion = "v2"
return to, nil
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
84 changes: 7 additions & 77 deletions _examples/versions/presenter/v3/article.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ package v3

import (
"fmt"
"log"
"math/rand"
"net/http"
"reflect"
"time"

"github.com/pressly/chi/_examples/versions/data"
"github.com/pressly/chi/render"
)

// Article presented in API version 2.
Expand All @@ -26,84 +22,18 @@ type Article struct {
CustomDataForAuthUsers interface{} `json:"custom_data,omitempty" xml:"custom_data,omitempty"`
}

var Presenter = render.NewPresenter(CatchAll, ArticleToV3, ArticleChanToV3Chan, ArticleSliceToV3Slice)
func (a *Article) Render(w http.ResponseWriter, r *http.Request) error {
a.ViewsCount = rand.Int63n(100000)
a.URL = fmt.Sprintf("http://localhost:3333/v3/?id=%v", a.ID)

func ArticleToV3(r *http.Request, from *data.Article) (*Article, error) {
log.Printf("item presenter!")

rand.Seed(time.Now().Unix())
to := &Article{
Article: from,
ViewsCount: rand.Int63n(100000),
URL: fmt.Sprintf("http://localhost:3333/v3/?id=%v", from.ID),
APIVersion: "v3",
}
// Only show to auth'd user.
if _, ok := r.Context().Value("auth").(bool); ok {
to.CustomDataForAuthUsers = from.CustomDataForAuthUsers
}
return to, nil
}

// An optional, optimized presenter for channnel of Articles.
// If not defined, each item will be preseted using ArticleToV3() func.
func ArticleChanToV3Chan(r *http.Request, fromChan chan *data.Article) (chan *Article, error) {
log.Printf("channel presenter!")

rand.Seed(time.Now().Unix())

toChan := make(chan *Article, 5)
go func() {
for from := range fromChan {
to := &Article{
Article: from,
ViewsCount: rand.Int63n(100000),
URL: fmt.Sprintf("http://localhost:3333/v3/?id=%v", from.ID),
APIVersion: "v3",
}
// Only show to auth'd user.
if _, ok := r.Context().Value("auth").(bool); ok {
to.CustomDataForAuthUsers = from.CustomDataForAuthUsers
}

toChan <- to
}
close(toChan)
}()

return toChan, nil
}

// An optional, optimized presenter for slice of Articles.
// If not defined, each item will be preseted using ArticleToV3() func.
func ArticleSliceToV3Slice(r *http.Request, fromSlice []*data.Article) ([]*Article, error) {
log.Printf("slice presenter!")

rand.Seed(time.Now().Unix())

toSlice := make([]*Article, len(fromSlice))
for i, from := range fromSlice {
to := &Article{
Article: from,
ViewsCount: rand.Int63n(100000),
URL: fmt.Sprintf("http://localhost:3333/v3/?id=%v", from.ID),
APIVersion: "v3",
}
// Only show to auth'd user.
if _, ok := r.Context().Value("auth").(bool); ok {
to.CustomDataForAuthUsers = from.CustomDataForAuthUsers
}
toSlice[i] = to
a.CustomDataForAuthUsers = a.Article.CustomDataForAuthUsers
}

return toSlice, nil
return nil
}

func CatchAll(r *http.Request, v interface{}) (*http.Request, interface{}) {
if val := reflect.ValueOf(v); val.IsValid() {
if err, ok := val.Interface().(error); ok {
return data.PresentError(r, err)
}
}
return r, v
func NewArticleResponse(article *data.Article) *Article {
return &Article{Article: article}
}
1 change: 0 additions & 1 deletion mux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,6 @@ func TestMuxNestedNotFound(t *testing.T) {
})

sr1 := NewRouter()
sr1.Use()

sr1.Get("/sub", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("sub"))
Expand Down
12 changes: 6 additions & 6 deletions render/responder.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ func channelEventStream(w http.ResponseWriter, r *http.Request, v interface{}) {
v := recv.Interface()

// Build each channel item.
if renderer, ok := v.(Renderer); ok {
err := renderer.Render(w, r)
if rv, ok := v.(Renderer); ok {
err := renderer(w, r, rv)
if err != nil {
v = err
} else {
v = renderer
v = rv
}
}

Expand Down Expand Up @@ -211,12 +211,12 @@ func channelIntoSlice(w http.ResponseWriter, r *http.Request, from interface{})
v := recv.Interface()

// Render each channel item.
if renderer, ok := v.(Renderer); ok {
err := renderer.Render(w, r)
if rv, ok := v.(Renderer); ok {
err := renderer(w, r, rv)
if err != nil {
v = err
} else {
v = renderer
v = rv
}
}

Expand Down

0 comments on commit 7f80c65

Please sign in to comment.