pax_global_header00006660000000000000000000000064130177166160014522gustar00rootroot0000000000000052 comment=ce4b71cdf7ba29ef443d704bd923f5bbf281ee30 go-json-rest-3.3.2/000077500000000000000000000000001301771661600140565ustar00rootroot00000000000000go-json-rest-3.3.2/.travis.yml000066400000000000000000000001051301771661600161630ustar00rootroot00000000000000sudo: false language: go go: - 1.3 - 1.4 - 1.5 - 1.6 - 1.7 go-json-rest-3.3.2/LICENSE000066400000000000000000000020701301771661600150620ustar00rootroot00000000000000Copyright (c) 2013-2016 Antoine Imbert The MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-json-rest-3.3.2/README.md000066400000000000000000001257601301771661600153500ustar00rootroot00000000000000 # Go-Json-Rest *A quick and easy way to setup a RESTful JSON API* [![godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) [![license](https://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/ant0ine/go-json-rest/master/LICENSE) [![build](https://img.shields.io/travis/ant0ine/go-json-rest.svg?style=flat)](https://travis-ci.org/ant0ine/go-json-rest) **Go-Json-Rest** is a thin layer on top of `net/http` that helps building RESTful JSON APIs easily. It provides fast and scalable request routing using a Trie based implementation, helpers to deal with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, Status ... ## Table of content - [Features](#features) - [Install](#install) - [Vendoring](#vendoring) - [Middlewares](#middlewares) - [Examples](#examples) - [Basics](#basics) - [Hello World!](#hello-world) - [Lookup](#lookup) - [Countries](#countries) - [Users](#users) - [Applications](#applications) - [API and static files](#api-and-static-files) - [GORM](#gorm) - [CORS](#cors) - [JSONP](#jsonp) - [Basic Auth](#basic-auth) - [Force HTTPS](#forcessl) - [Status](#status) - [Status Auth](#status-auth) - [Advanced](#advanced) - [JWT](#jwt) - [Streaming](#streaming) - [Non JSON payload](#non-json-payload) - [API Versioning](#api-versioning) - [Statsd](#statsd) - [NewRelic](#newrelic) - [Graceful Shutdown](#graceful-shutdown) - [SPDY](#spdy) - [Google App Engine](#gae) - [Websocket](#websocket) - [External Documentation](#external-documentation) - [Version 3 release notes](#version-3-release-notes) - [Migration guide from v2 to v3](#migration-guide-from-v2-to-v3) - [Version 2 release notes](#version-2-release-notes) - [Migration guide from v1 to v2](#migration-guide-from-v1-to-v2) - [Thanks](#thanks) ## Features - Many examples. - Fast and scalable URL routing. It implements the classic route description syntax using a Trie data structure. - Architecture based on a router(App) sitting on top of a stack of Middlewares. - The Middlewares implement functionalities like Logging, Gzip, CORS, Auth, Status, ... - Implemented as a `net/http` Handler. This standard interface allows combinations with other Handlers. - Test package to help writing tests for your API. - Monitoring statistics inspired by Memcached. ## Install This package is "go-gettable", just do: go get github.com/ant0ine/go-json-rest/rest ## Vendoring The recommended way of using this library in your project is to use the **"vendoring"** method, where this library code is copied in your repository at a specific revision. [This page](https://nathany.com/go-packages/) is a good summary of package management in Go. ## Middlewares Core Middlewares: | Name | Description | |------|-------------| | **AccessLogApache** | Access log inspired by Apache mod_log_config | | **AccessLogJson** | Access log with records as JSON | | **AuthBasic** | Basic HTTP auth | | **ContentTypeChecker** | Verify the request content type | | **Cors** | CORS server side implementation | | **Gzip** | Compress the responses | | **If** | Conditionally execute a Middleware at runtime | | **JsonIndent** | Easy to read JSON | | **Jsonp** | Response as JSONP | | **PoweredBy** | Manage the X-Powered-By response header | | **Recorder** | Record the status code and content length in the Env | | **Status** | Memecached inspired stats about the requests | | **Timer** | Keep track of the elapsed time in the Env | Third Party Middlewares: | Name | Description | |------|-------------| | **[Statsd](https://github.com/ant0ine/go-json-rest-middleware-statsd)** | Send stats to a statsd server | | **[JWT](https://github.com/StephanDollberg/go-json-rest-middleware-jwt)** | Provides authentication via Json Web Tokens | | **[AuthToken](https://github.com/grayj/go-json-rest-middleware-tokenauth)** | Provides a Token Auth implementation | | **[ForceSSL](https://github.com/jadengore/go-json-rest-middleware-force-ssl)** | Forces SSL on requests | | **[SecureRedirect](https://github.com/clyphub/go-json-rest-middleware)** | Redirect clients from HTTP to HTTPS | *If you have a Go-Json-Rest compatible middleware, feel free to submit a PR to add it in this list, and in the examples.* ## Examples All the following examples can be found in dedicated examples repository: https://github.com/ant0ine/go-json-rest-examples ### Basics First examples to try, as an introduction to go-json-rest. #### Hello World! Tradition! curl demo: ``` sh curl -i http://127.0.0.1:8080/ ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Lookup Demonstrate how to use the relaxed placeholder (notation `#paramName`). This placeholder matches everything until the first `/`, including `.` curl demo: ``` curl -i http://127.0.0.1:8080/lookup/google.com curl -i http://127.0.0.1:8080/lookup/notadomain ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/lookup/#host", func(w rest.ResponseWriter, req *rest.Request) { ip, err := net.LookupIP(req.PathParam("host")) if err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&ip) }), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Countries Demonstrate simple POST GET and DELETE operations curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Code":"FR","Name":"France"}' http://127.0.0.1:8080/countries curl -i -H 'Content-Type: application/json' \ -d '{"Code":"US","Name":"United States"}' http://127.0.0.1:8080/countries curl -i http://127.0.0.1:8080/countries/FR curl -i http://127.0.0.1:8080/countries/US curl -i http://127.0.0.1:8080/countries curl -i -X DELETE http://127.0.0.1:8080/countries/FR curl -i http://127.0.0.1:8080/countries curl -i -X DELETE http://127.0.0.1:8080/countries/US curl -i http://127.0.0.1:8080/countries ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" "sync" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/countries", GetAllCountries), rest.Post("/countries", PostCountry), rest.Get("/countries/:code", GetCountry), rest.Delete("/countries/:code", DeleteCountry), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { Code string Name string } var store = map[string]*Country{} var lock = sync.RWMutex{} func GetCountry(w rest.ResponseWriter, r *rest.Request) { code := r.PathParam("code") lock.RLock() var country *Country if store[code] != nil { country = &Country{} *country = *store[code] } lock.RUnlock() if country == nil { rest.NotFound(w, r) return } w.WriteJson(country) } func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { lock.RLock() countries := make([]Country, len(store)) i := 0 for _, country := range store { countries[i] = *country i++ } lock.RUnlock() w.WriteJson(&countries) } func PostCountry(w rest.ResponseWriter, r *rest.Request) { country := Country{} err := r.DecodeJsonPayload(&country) if err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } if country.Code == "" { rest.Error(w, "country code required", 400) return } if country.Name == "" { rest.Error(w, "country name required", 400) return } lock.Lock() store[country.Code] = &country lock.Unlock() w.WriteJson(&country) } func DeleteCountry(w rest.ResponseWriter, r *rest.Request) { code := r.PathParam("code") lock.Lock() delete(store, code) lock.Unlock() w.WriteHeader(http.StatusOK) } ``` #### Users Demonstrate how to use Method Values. Method Values have been [introduced in Go 1.1](https://golang.org/doc/go1.1#method_values). This shows how to map a Route to a method of an instantiated object (i.e: receiver of the method) curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Name":"Antoine"}' http://127.0.0.1:8080/users curl -i http://127.0.0.1:8080/users/0 curl -i -X PUT -H 'Content-Type: application/json' \ -d '{"Name":"Antoine Imbert"}' http://127.0.0.1:8080/users/0 curl -i -X DELETE http://127.0.0.1:8080/users/0 curl -i http://127.0.0.1:8080/users ``` code: ``` go package main import ( "fmt" "github.com/ant0ine/go-json-rest/rest" "log" "net/http" "sync" ) func main() { users := Users{ Store: map[string]*User{}, } api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/users", users.GetAllUsers), rest.Post("/users", users.PostUser), rest.Get("/users/:id", users.GetUser), rest.Put("/users/:id", users.PutUser), rest.Delete("/users/:id", users.DeleteUser), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type User struct { Id string Name string } type Users struct { sync.RWMutex Store map[string]*User } func (u *Users) GetAllUsers(w rest.ResponseWriter, r *rest.Request) { u.RLock() users := make([]User, len(u.Store)) i := 0 for _, user := range u.Store { users[i] = *user i++ } u.RUnlock() w.WriteJson(&users) } func (u *Users) GetUser(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") u.RLock() var user *User if u.Store[id] != nil { user = &User{} *user = *u.Store[id] } u.RUnlock() if user == nil { rest.NotFound(w, r) return } w.WriteJson(user) } func (u *Users) PostUser(w rest.ResponseWriter, r *rest.Request) { user := User{} err := r.DecodeJsonPayload(&user) if err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } u.Lock() id := fmt.Sprintf("%d", len(u.Store)) // stupid user.Id = id u.Store[id] = &user u.Unlock() w.WriteJson(&user) } func (u *Users) PutUser(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") u.Lock() if u.Store[id] == nil { rest.NotFound(w, r) u.Unlock() return } user := User{} err := r.DecodeJsonPayload(&user) if err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) u.Unlock() return } user.Id = id u.Store[id] = &user u.Unlock() w.WriteJson(&user) } func (u *Users) DeleteUser(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") u.Lock() delete(u.Store, id) u.Unlock() w.WriteHeader(http.StatusOK) } ``` ### Applications Common use cases, found in many applications. #### API and static files Combine Go-Json-Rest with other handlers. `api.MakeHandler()` is a valid `http.Handler`, and can be combined with other handlers. In this example the api handler is used under the `/api/` prefix, while a FileServer is instantiated under the `/static/` prefix. curl demo: ``` curl -i http://127.0.0.1:8080/api/message curl -i http://127.0.0.1:8080/static/main.go ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }), ) if err != nil { log.Fatal(err) } api.SetApp(router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) http.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir(".")))) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` #### GORM Demonstrate basic CRUD operation using a store based on MySQL and GORM [GORM](https://github.com/jinzhu/gorm) is simple ORM library for Go. In this example the same struct is used both as the GORM model and as the JSON model. curl demo: ``` curl -i -H 'Content-Type: application/json' \ -d '{"Message":"this is a test"}' http://127.0.0.1:8080/reminders curl -i http://127.0.0.1:8080/reminders/1 curl -i http://127.0.0.1:8080/reminders curl -i -X PUT -H 'Content-Type: application/json' \ -d '{"Message":"is updated"}' http://127.0.0.1:8080/reminders/1 curl -i -X DELETE http://127.0.0.1:8080/reminders/1 ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "log" "net/http" "time" ) func main() { i := Impl{} i.InitDB() i.InitSchema() api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/reminders", i.GetAllReminders), rest.Post("/reminders", i.PostReminder), rest.Get("/reminders/:id", i.GetReminder), rest.Put("/reminders/:id", i.PutReminder), rest.Delete("/reminders/:id", i.DeleteReminder), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Reminder struct { Id int64 `json:"id"` Message string `sql:"size:1024" json:"message"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` DeletedAt time.Time `json:"-"` } type Impl struct { DB *gorm.DB } func (i *Impl) InitDB() { var err error i.DB, err = gorm.Open("mysql", "gorm:gorm@/gorm?charset=utf8&parseTime=True") if err != nil { log.Fatalf("Got error when connect database, the error is '%v'", err) } i.DB.LogMode(true) } func (i *Impl) InitSchema() { i.DB.AutoMigrate(&Reminder{}) } func (i *Impl) GetAllReminders(w rest.ResponseWriter, r *rest.Request) { reminders := []Reminder{} i.DB.Find(&reminders) w.WriteJson(&reminders) } func (i *Impl) GetReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } w.WriteJson(&reminder) } func (i *Impl) PostReminder(w rest.ResponseWriter, r *rest.Request) { reminder := Reminder{} if err := r.DecodeJsonPayload(&reminder); err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } if err := i.DB.Save(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&reminder) } func (i *Impl) PutReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } updated := Reminder{} if err := r.DecodeJsonPayload(&updated); err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } reminder.Message = updated.Message if err := i.DB.Save(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteJson(&reminder) } func (i *Impl) DeleteReminder(w rest.ResponseWriter, r *rest.Request) { id := r.PathParam("id") reminder := Reminder{} if i.DB.First(&reminder, id).Error != nil { rest.NotFound(w, r) return } if err := i.DB.Delete(&reminder).Error; err != nil { rest.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } ``` #### CORS Demonstrate how to setup CorsMiddleware around all the API endpoints. curl demo: ``` curl -i http://127.0.0.1:8080/countries ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.CorsMiddleware{ RejectNonCorsRequests: false, OriginValidator: func(origin string, request *rest.Request) bool { return origin == "http://my.other.host" }, AllowedMethods: []string{"GET", "POST", "PUT"}, AllowedHeaders: []string{ "Accept", "Content-Type", "X-Custom-Header", "Origin"}, AccessControlAllowCredentials: true, AccessControlMaxAge: 3600, }) router, err := rest.MakeRouter( rest.Get("/countries", GetAllCountries), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { Code string Name string } func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { w.WriteJson( []Country{ Country{ Code: "FR", Name: "France", }, Country{ Code: "US", Name: "United States", }, }, ) } ``` #### JSONP Demonstrate how to use the JSONP middleware. curl demo: ``` sh curl -i http://127.0.0.1:8080/ curl -i http://127.0.0.1:8080/?cb=parseResponse ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.JsonpMiddleware{ CallbackNameKey: "cb", }) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Basic Auth Demonstrate how to setup AuthBasicMiddleware as a pre-routing middleware. curl demo: ``` curl -i http://127.0.0.1:8080/ curl -i -u admin:admin http://127.0.0.1:8080/ ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&rest.AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { if userId == "admin" && password == "admin" { return true } return false }, }) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### ForceSSL Demonstrate how to use the [ForceSSL Middleware](https://github.com/jadengore/go-json-rest-middleware-force-ssl) to force HTTPS on requests to a `go-json-rest` API. For the purposes of this demo, we are using HTTP for all requests and checking the `X-Forwarded-Proto` header to see if it is set to HTTPS (many routers set this to show what type of connection the client is using, such as Heroku). To do a true HTTPS test, make sure and use [`http.ListenAndServeTLS`](https://golang.org/pkg/net/http/#ListenAndServeTLS) with a valid certificate and key file. Additional documentation for the ForceSSL middleware can be found [here](https://github.com/jadengore/go-json-rest-middleware-force-ssl). curl demo: ``` sh curl -i 127.0.0.1:8080/ curl -H "X-Forwarded-Proto:https" -i 127.0.0.1:8080/ ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "github.com/jadengore/go-json-rest-middleware-force-ssl" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(&forceSSL.Middleware{ TrustXFPHeader: true, Enable301Redirects: false, }) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"body": "Hello World!"}) })) // For the purposes of this demo, only HTTP connections accepted. // For true HTTPS, use ListenAndServeTLS. // https://golang.org/pkg/net/http/#ListenAndServeTLS log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Status Demonstrate how to setup a `/.status` endpoint Inspired by memcached "stats", this optional feature can be enabled to help monitoring the service. This example shows how to enable the stats, and how to setup the `/.status` route. curl demo: ``` curl -i http://127.0.0.1:8080/.status curl -i http://127.0.0.1:8080/.status ... ``` Output example: ``` { "Pid": 21732, "UpTime": "1m15.926272s", "UpTimeSec": 75.926272, "Time": "2013-03-04 08:00:27.152986 +0000 UTC", "TimeUnix": 1362384027, "StatusCodeCount": { "200": 53, "404": 11 }, "TotalCount": 64, "TotalResponseTime": "16.777ms", "TotalResponseTimeSec": 0.016777, "AverageResponseTime": "262.14us", "AverageResponseTimeSec": 0.00026214 } ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() statusMw := &rest.StatusMiddleware{} api.Use(statusMw) api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/.status", func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(statusMw.GetStatus()) }), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Status Auth Demonstrate how to setup a /.status endpoint protected with basic authentication. This is a good use case of middleware applied to only one API endpoint. curl demo: ``` curl -i http://127.0.0.1:8080/countries curl -i http://127.0.0.1:8080/.status curl -i -u admin:admin http://127.0.0.1:8080/.status ... ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() statusMw := &rest.StatusMiddleware{} api.Use(statusMw) api.Use(rest.DefaultDevStack...) auth := &rest.AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { if userId == "admin" && password == "admin" { return true } return false }, } router, err := rest.MakeRouter( rest.Get("/countries", GetAllCountries), rest.Get("/.status", auth.MiddlewareFunc( func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(statusMw.GetStatus()) }, )), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Country struct { Code string Name string } func GetAllCountries(w rest.ResponseWriter, r *rest.Request) { w.WriteJson( []Country{ Country{ Code: "FR", Name: "France", }, Country{ Code: "US", Name: "United States", }, }, ) } ``` ### Advanced More advanced use cases. #### JWT Demonstrates how to use the [Json Web Token Auth Middleware](https://github.com/StephanDollberg/go-json-rest-middleware-jwt) to authenticate via a JWT token. curl demo: ``` sh curl -d '{"username": "admin", "password": "admin"}' -H "Content-Type:application/json" http://localhost:8080/api/login curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/auth_test curl -H "Authorization:Bearer TOKEN_RETURNED_FROM_ABOVE" http://localhost:8080/api/refresh_token ``` code: ``` go package main import ( "log" "net/http" "time" "github.com/StephanDollberg/go-json-rest-middleware-jwt" "github.com/ant0ine/go-json-rest/rest" ) func handle_auth(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"authed": r.Env["REMOTE_USER"].(string)}) } func main() { jwt_middleware := &jwt.JWTMiddleware{ Key: []byte("secret key"), Realm: "jwt auth", Timeout: time.Hour, MaxRefresh: time.Hour * 24, Authenticator: func(userId string, password string) bool { return userId == "admin" && password == "admin" }} api := rest.NewApi() api.Use(rest.DefaultDevStack...) // we use the IfMiddleware to remove certain paths from needing authentication api.Use(&rest.IfMiddleware{ Condition: func(request *rest.Request) bool { return request.URL.Path != "/login" }, IfTrue: jwt_middleware, }) api_router, _ := rest.MakeRouter( rest.Post("/login", jwt_middleware.LoginHandler), rest.Get("/auth_test", handle_auth), rest.Get("/refresh_token", jwt_middleware.RefreshHandler), ) api.SetApp(api_router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` #### Streaming Demonstrate a streaming REST API, where the data is "flushed" to the client ASAP. The stream format is a Line Delimited JSON. curl demo: ``` curl -i http://127.0.0.1:8080/stream ``` Output: ``` HTTP/1.1 200 OK Content-Type: application/json Date: Sun, 16 Feb 2014 00:39:19 GMT Transfer-Encoding: chunked {"Name":"thing #1"} {"Name":"thing #2"} {"Name":"thing #3"} ``` code: ``` go package main import ( "fmt" "github.com/ant0ine/go-json-rest/rest" "log" "net/http" "time" ) func main() { api := rest.NewApi() api.Use(&rest.AccessLogApacheMiddleware{}) api.Use(rest.DefaultCommonStack...) router, err := rest.MakeRouter( rest.Get("/stream", StreamThings), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } type Thing struct { Name string } func StreamThings(w rest.ResponseWriter, r *rest.Request) { cpt := 0 for { cpt++ w.WriteJson( &Thing{ Name: fmt.Sprintf("thing #%d", cpt), }, ) w.(http.ResponseWriter).Write([]byte("\n")) // Flush the buffer to client w.(http.Flusher).Flush() // wait 3 seconds time.Sleep(time.Duration(3) * time.Second) } } ``` #### Non JSON payload Exceptional use of non JSON payloads. The ResponseWriter implementation provided by go-json-rest is designed to build JSON responses. In order to serve different kind of content, it is recommended to either: a) use another server and configure CORS (see the cors/ example) b) combine the api.MakeHandler() with another http.Handler (see api-and-static/ example) That been said, exceptionally, it can be convenient to return a different content type on a JSON endpoint. In this case, setting the Content-Type and using the type assertion to access the Write method is enough. As shown in this example. curl demo: ``` curl -i http://127.0.0.1:8080/message.txt ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/message.txt", func(w rest.ResponseWriter, req *rest.Request) { w.Header().Set("Content-Type", "text/plain") w.(http.ResponseWriter).Write([]byte("Hello World!")) }), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### API Versioning First, API versioning is not easy and you may want to favor a mechanism that uses only backward compatible changes and deprecation cycles. That been said, here is an example of API versioning using [Semver](http://semver.org/) It defines a middleware that parses the version, checks a min and a max, and makes it available in the `request.Env`. curl demo: ``` sh curl -i http://127.0.0.1:8080/api/1.0.0/message curl -i http://127.0.0.1:8080/api/2.0.0/message curl -i http://127.0.0.1:8080/api/2.0.1/message curl -i http://127.0.0.1:8080/api/0.0.1/message curl -i http://127.0.0.1:8080/api/4.0.1/message ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "github.com/coreos/go-semver/semver" "log" "net/http" ) type SemVerMiddleware struct { MinVersion string MaxVersion string } func (mw *SemVerMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { minVersion, err := semver.NewVersion(mw.MinVersion) if err != nil { panic(err) } maxVersion, err := semver.NewVersion(mw.MaxVersion) if err != nil { panic(err) } return func(writer rest.ResponseWriter, request *rest.Request) { version, err := semver.NewVersion(request.PathParam("version")) if err != nil { rest.Error( writer, "Invalid version: "+err.Error(), http.StatusBadRequest, ) return } if version.LessThan(*minVersion) { rest.Error( writer, "Min supported version is "+minVersion.String(), http.StatusBadRequest, ) return } if maxVersion.LessThan(*version) { rest.Error( writer, "Max supported version is "+maxVersion.String(), http.StatusBadRequest, ) return } request.Env["VERSION"] = version handler(writer, request) } } func main() { svmw := SemVerMiddleware{ MinVersion: "1.0.0", MaxVersion: "3.0.0", } api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/#version/message", svmw.MiddlewareFunc( func(w rest.ResponseWriter, req *rest.Request) { version := req.Env["VERSION"].(*semver.Version) if version.Major == 2 { // https://en.wikipedia.org/wiki/Second-system_effect w.WriteJson(map[string]string{ "Body": "Hello broken World!", }) } else { w.WriteJson(map[string]string{ "Body": "Hello World!", }) } }, )), ) if err != nil { log.Fatal(err) } api.SetApp(router) http.Handle("/api/", http.StripPrefix("/api", api.MakeHandler())) log.Fatal(http.ListenAndServe(":8080", nil)) } ``` #### Statsd Demonstrate how to use the [Statsd Middleware](https://github.com/ant0ine/go-json-rest-middleware-statsd) to collect statistics about the requests/reponses. This middleware is based on the [g2s](https://github.com/peterbourgon/g2s) statsd client. curl demo: ``` sh # start statsd server # monitor network ngrep -d any port 8125 curl -i http://127.0.0.1:8080/message curl -i http://127.0.0.1:8080/doesnotexist ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest-middleware-statsd" "github.com/ant0ine/go-json-rest/rest" "log" "net/http" "time" ) func main() { api := rest.NewApi() api.Use(&statsd.StatsdMiddleware{}) api.Use(rest.DefaultDevStack...) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, req *rest.Request) { // take more than 1ms so statsd can report it time.Sleep(100 * time.Millisecond) w.WriteJson(map[string]string{"Body": "Hello World!"}) })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### NewRelic NewRelic integration based on the GoRelic plugin: [github.com/yvasiyarov/gorelic](https://github.com/yvasiyarov/gorelic) curl demo: ``` sh curl -i http://127.0.0.1:8080/ ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "github.com/yvasiyarov/go-metrics" "github.com/yvasiyarov/gorelic" "log" "net/http" "time" ) type NewRelicMiddleware struct { License string Name string Verbose bool agent *gorelic.Agent } func (mw *NewRelicMiddleware) MiddlewareFunc(handler rest.HandlerFunc) rest.HandlerFunc { mw.agent = gorelic.NewAgent() mw.agent.NewrelicLicense = mw.License mw.agent.HTTPTimer = metrics.NewTimer() mw.agent.Verbose = mw.Verbose mw.agent.NewrelicName = mw.Name mw.agent.CollectHTTPStat = true mw.agent.Run() return func(writer rest.ResponseWriter, request *rest.Request) { handler(writer, request) // the timer middleware keeps track of the time startTime := request.Env["START_TIME"].(*time.Time) mw.agent.HTTPTimer.UpdateSince(*startTime) } } func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.Use(&NewRelicMiddleware{ License: "", Name: "", Verbose: true, }) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` #### Graceful Shutdown This example uses [https://github.com/tylerb/graceful](https://github.com/tylerb/graceful) to try to be nice with the clients waiting for responses during a server shutdown (or restart). The HTTP response takes 10 seconds to be completed, printing a message on the wire every second. 10 seconds is also the timeout set for the graceful shutdown. You can play with these numbers to show that the server waits for the responses to complete. curl demo: ``` sh curl -i http://127.0.0.1:8080/message ``` code: ``` go package main import ( "fmt" "github.com/ant0ine/go-json-rest/rest" "gopkg.in/tylerb/graceful.v1" "log" "net/http" "time" ) func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { for cpt := 1; cpt <= 10; cpt++ { // wait 1 second time.Sleep(time.Duration(1) * time.Second) w.WriteJson(map[string]string{ "Message": fmt.Sprintf("%d seconds", cpt), }) w.(http.ResponseWriter).Write([]byte("\n")) // Flush the buffer to client w.(http.Flusher).Flush() } }), ) if err != nil { log.Fatal(err) } api.SetApp(router) server := &graceful.Server{ Timeout: 10 * time.Second, Server: &http.Server{ Addr: ":8080", Handler: api.MakeHandler(), }, } log.Fatal(server.ListenAndServe()) } ``` #### SPDY Demonstrate how to use SPDY with https://github.com/shykes/spdy-go For a command line client, install spdycat from: https://github.com/tatsuhiro-t/spdylay spdycat demo: ``` spdycat -v --no-tls -2 http://localhost:8080/users/0 ``` code: ``` go package main import ( "github.com/ant0ine/go-json-rest/rest" "github.com/shykes/spdy-go" "log" ) type User struct { Id string Name string } func GetUser(w rest.ResponseWriter, req *rest.Request) { user := User{ Id: req.PathParam("id"), Name: "Antoine", } w.WriteJson(&user) } func main() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( rest.Get("/users/:id", GetUser), ) if err != nil { log.Fatal(err) } api.SetApp(router) log.Fatal(spdy.ListenAndServeTCP(":8080", api.MakeHandler())) } ``` #### GAE Demonstrate a simple Google App Engine app Here are my steps to make it work with the GAE SDK. (Probably not the best ones) Assuming that go-json-rest is installed using "go get" and that the GAE SDK is also installed. Setup: * copy this examples/gae/ dir outside of the go-json-rest/ tree * cd gae/ * mkdir -p github.com/ant0ine * cp -r $GOPATH/src/github.com/ant0ine/go-json-rest github.com/ant0ine/go-json-rest * rm -rf github.com/ant0ine/go-json-rest/examples/ * path/to/google_appengine/dev_appserver.py . curl demo: ``` curl -i http://127.0.0.1:8080/message ``` code: ``` go package gaehelloworld import ( "github.com/ant0ine/go-json-rest/rest" "log" "net/http" ) func init() { api := rest.NewApi() api.Use(rest.DefaultDevStack...) router, err := rest.MakeRouter( &rest.Get("/message", func(w rest.ResponseWriter, req *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) }), ) if err != nil { log.Fatal(err) } api.SetApp(router) http.Handle("/", api.MakeHandler()) } ``` #### Websocket Demonstrate how to run websocket in go-json-rest go client demo: ```go origin := "http://localhost:8080/" url := "ws://localhost:8080/ws" ws, err := websocket.Dial(url, "", origin) if err != nil { log.Fatal(err) } if _, err := ws.Write([]byte("hello, world\n")); err != nil { log.Fatal(err) } var msg = make([]byte, 512) var n int if n, err = ws.Read(msg); err != nil { log.Fatal(err) } log.Printf("Received: %s.", msg[:n]) ``` code: ``` go package main import ( "io" "log" "net/http" "github.com/ant0ine/go-json-rest/rest" "golang.org/x/net/websocket" ) func main() { wsHandler := websocket.Handler(func(ws *websocket.Conn) { io.Copy(ws, ws) }) router, err := rest.MakeRouter( rest.Get("/ws", func(w rest.ResponseWriter, r *rest.Request) { wsHandler.ServeHTTP(w.(http.ResponseWriter), r.Request) }), ) if err != nil { log.Fatal(err) } api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.SetApp(router) log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) } ``` ## External Documentation - [Online Documentation (godoc.org)](https://godoc.org/github.com/ant0ine/go-json-rest/rest) Old v1 blog posts: - [(Blog Post) Introducing Go-Json-Rest] (https://www.ant0ine.com/post/introducing-go-json-rest.html) - [(Blog Post) Better URL Routing ?] (https://www.ant0ine.com/post/better-url-routing-golang.html) ## Version 3 release notes ### What's New in v3 * Public Middlewares. (12 included in the package) * A new App interface. (the router being the provided App) * A new Api object that manages the Middlewares and the App. * Optional and interchangeable App/router. ### Here is for instance the new minimal "Hello World!" ```go api := rest.NewApi() api.Use(rest.DefaultDevStack...) api.SetApp(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { w.WriteJson(map[string]string{"Body": "Hello World!"}) })) http.ListenAndServe(":8080", api.MakeHandler()) ``` *All 19 examples have been updated to use the new API. [See here](https://github.com/ant0ine/go-json-rest#examples)* ### Deprecating the ResourceHandler V3 is about deprecating the ResourceHandler in favor of a new API that exposes the Middlewares. As a consequence, all the Middlewares are now public, and the new Api object helps putting them together as a stack. Some default stack configurations are offered. The router is now an App that sits on top of the stack of Middlewares. Which means that the router is no longer required to use Go-Json-Rest. *Design ideas and discussion [See here](https://github.com/ant0ine/go-json-rest/issues/110)* ## Migration guide from v2 to v3 V3 introduces an API change (see [Semver](http://semver.org/)). But it was possible to maintain backward compatibility, and so, ResourceHandler still works. ResourceHandler does the same thing as in V2, **but it is now considered as deprecated, and will be removed in a few months**. In the meantime, it logs a deprecation warning. ### How to map the ResourceHandler options to the new stack of middlewares ? * `EnableGzip bool`: Just include GzipMiddleware in the stack of middlewares. * `DisableJsonIndent bool`: Just don't include JsonIndentMiddleware in the stack of middlewares. * `EnableStatusService bool`: Include StatusMiddleware in the stack and keep a reference to it to access GetStatus(). * `EnableResponseStackTrace bool`: Same exact option but moved to RecoverMiddleware. * `EnableLogAsJson bool`: Include AccessLogJsonMiddleware, and possibly remove AccessLogApacheMiddleware. * `EnableRelaxedContentType bool`: Just don't include ContentTypeCheckerMiddleware. * `OuterMiddlewares []Middleware`: You are now building the full stack, OuterMiddlewares are the first in the list. * `PreRoutingMiddlewares []Middleware`: You are now building the full stack, OuterMiddlewares are the last in the list. * `Logger *log.Logger`: Same option but moved to AccessLogApacheMiddleware and AccessLogJsonMiddleware. * `LoggerFormat AccessLogFormat`: Same exact option but moved to AccessLogApacheMiddleware. * `DisableLogger bool`: Just don't include any access log middleware. * `ErrorLogger *log.Logger`: Same exact option but moved to RecoverMiddleware. * `XPoweredBy string`: Same exact option but moved to PoweredByMiddleware. * `DisableXPoweredBy bool`: Just don't include PoweredByMiddleware. ## Version 2 release notes * Middlewares, the notion of middleware is now formally defined. They can be setup as global pre-routing Middlewares wrapping all the endpoints, or on a per endpoint basis. In fact the internal code of **go-json-rest** is itself implemented with Middlewares, they are just hidden behind configuration boolean flags to make these very common options even easier to use. * A new ResponseWriter. This is now an interface, and allows Middlewares to wrap the writer. The provided writer implements, in addition of *rest.ResponseWriter*, *http.Flusher*, *http.CloseNotifier*, *http.Hijacker*, and *http.ResponseWriter*. A lot more Go-ish, and very similar to `net/http`. * The AuthBasic and CORS Middlewares have been added. More to come in the future. * Faster, more tasks are performed at init time, and less for each request. * New documentation, with more examples. * A lot of other small improvements, See the [Migration guide to v2](#migration-guide-from-v1-to-v2) ## Migration guide from v1 to v2 **Go-Json-Rest** follows [Semver](http://semver.org/) and a few breaking changes have been introduced with the v2. #### The import path has changed to `github.com/ant0ine/go-json-rest/rest` This is more conform to Go style, and makes [goimports](https://godoc.org/golang.org/x/tools/cmd/goimports) work. This: ``` go import ( "github.com/ant0ine/go-json-rest" ) ``` has to be changed to this: ``` go import ( "github.com/ant0ine/go-json-rest/rest" ) ``` #### rest.ResponseWriter is now an interface This change allows the `ResponseWriter` to be wrapped, like the one of the `net/http` package. This is much more powerful, and allows the creation of Middlewares that wrap the writer. The gzip option, for instance, uses this to encode the payload (see gzip.go). This: ``` go func (w *rest.ResponseWriter, req *rest.Request) { ... } ``` has to be changed to this: ``` go func (w rest.ResponseWriter, req *rest.Request) { ... } ``` #### SetRoutes now takes pointers to Route Instead of copying Route structures everywhere, pointers are now used. This is more elegant, more efficient, and will allow more sophisticated Route manipulations in the future (like reverse route resolution). This: ``` go handler.SetRoutes( rest.Route{ // ... }, ) ``` has to be changed to this: ``` go handler.SetRoutes( &rest.Route{ // ... }, ) ``` #### The notion of Middleware is now formally defined A middleware is an object satisfying this interface: ``` go type Middleware interface { MiddlewareFunc(handler HandlerFunc) HandlerFunc } ``` Code using PreRoutingMiddleware will have to be adapted to provide a list of Middleware objects. See the [Basic Auth example](https://github.com/ant0ine/go-json-rest-examples/blob/master/auth-basic/main.go). #### Flush(), CloseNotify() and Write() are not directly exposed anymore They used to be public methods of the ResponseWriter. The implementation is still there but a type assertion of the corresponding interface is now necessary. Regarding these features, a rest.ResponseWriter now behaves exactly as the http.ResponseWriter implementation provided by net/http. This: ``` go writer.Flush() ``` has to be changed to this: ``` go writer.(http.Flusher).Flush() ``` #### The /.status endpoint is not created automatically anymore The route has to be manually defined. See the [Status example](https://github.com/ant0ine/go-json-rest-examples/blob/master/status/main.go). This is more flexible (the route is customizable), and allows combination with Middlewarres. See for instance how to [protect this status endpoint with the AuthBasic middleware](https://github.com/ant0ine/go-json-rest-examples/blob/master/status-auth/main.go). #### Request utility methods have changed Overall, they provide the same features, but with two methods instead of three, better names, and without the confusing `UriForWithParams`. - `func (r *Request) UriBase() url.URL` is now `func (r *Request) BaseUrl() *url.URL`, Note the pointer as the returned value. - `func (r *Request) UriForWithParams(path string, parameters map[string][]string) url.URL` is now `func (r *Request) UrlFor(path string, queryParams map[string][]string) *url.URL`. - `func (r *Request) UriFor(path string) url.URL` has be removed. ## Thanks - [Franck Cuny](https://github.com/franckcuny) - [Yann Kerhervé](https://github.com/yannk) - [Ask Bjørn Hansen](https://github.com/abh) - [Paul Lam](https://github.com/Quantisan) - [Thanabodee Charoenpiriyakij](https://github.com/wingyplus) - [Sebastien Estienne](https://github.com/sebest) - [Edward Bramanti](https://github.com/jadengore) Copyright (c) 2013-2016 Antoine Imbert [MIT License](https://github.com/ant0ine/go-json-rest/blob/master/LICENSE) [![Analytics](https://ga-beacon.appspot.com/UA-309210-4/go-json-rest/master/readme)](https://github.com/igrigorik/ga-beacon) go-json-rest-3.3.2/rest/000077500000000000000000000000001301771661600150335ustar00rootroot00000000000000go-json-rest-3.3.2/rest/access_log_apache.go000066400000000000000000000144331301771661600207720ustar00rootroot00000000000000package rest import ( "bytes" "fmt" "log" "net" "os" "strings" "text/template" "time" ) // TODO Future improvements: // * support %{strftime}t ? // * support %{
}o to print headers // AccessLogFormat defines the format of the access log record. // This implementation is a subset of Apache mod_log_config. // (See http://httpd.apache.org/docs/2.0/mod/mod_log_config.html) // // %b content length in bytes, - if 0 // %B content length in bytes // %D response elapsed time in microseconds // %h remote address // %H server protocol // %l identd logname, not supported, - // %m http method // %P process id // %q query string // %r first line of the request // %s status code // %S status code preceeded by a terminal color // %t time of the request // %T response elapsed time in seconds, 3 decimals // %u remote user, - if missing // %{User-Agent}i user agent, - if missing // %{Referer}i referer, - is missing // // Some predefined formats are provided as contants. type AccessLogFormat string const ( // CommonLogFormat is the Common Log Format (CLF). CommonLogFormat = "%h %l %u %t \"%r\" %s %b" // CombinedLogFormat is the NCSA extended/combined log format. CombinedLogFormat = "%h %l %u %t \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\"" // DefaultLogFormat is the default format, colored output and response time, convenient for development. DefaultLogFormat = "%t %S\033[0m \033[36;1m%Dμs\033[0m \"%r\" \033[1;30m%u \"%{User-Agent}i\"\033[0m" ) // AccessLogApacheMiddleware produces the access log following a format inspired by Apache // mod_log_config. It depends on TimerMiddleware and RecorderMiddleware that should be in the wrapped // middlewares. It also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogApacheMiddleware struct { // Logger points to the logger object used by this middleware, it defaults to // log.New(os.Stderr, "", 0). Logger *log.Logger // Format defines the format of the access log record. See AccessLogFormat for the details. // It defaults to DefaultLogFormat. Format AccessLogFormat textTemplate *template.Template } // MiddlewareFunc makes AccessLogApacheMiddleware implement the Middleware interface. func (mw *AccessLogApacheMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { mw.Logger = log.New(os.Stderr, "", 0) } // set default format if mw.Format == "" { mw.Format = DefaultLogFormat } mw.convertFormat() return func(w ResponseWriter, r *Request) { // call the handler h(w, r) util := &accessLogUtil{w, r} mw.Logger.Print(mw.executeTextTemplate(util)) } } var apacheAdapter = strings.NewReplacer( "%b", "{{.BytesWritten | dashIf0}}", "%B", "{{.BytesWritten}}", "%D", "{{.ResponseTime | microseconds}}", "%h", "{{.ApacheRemoteAddr}}", "%H", "{{.R.Proto}}", "%l", "-", "%m", "{{.R.Method}}", "%P", "{{.Pid}}", "%q", "{{.ApacheQueryString}}", "%r", "{{.R.Method}} {{.R.URL.RequestURI}} {{.R.Proto}}", "%s", "{{.StatusCode}}", "%S", "\033[{{.StatusCode | statusCodeColor}}m{{.StatusCode}}", "%t", "{{if .StartTime}}{{.StartTime.Format \"02/Jan/2006:15:04:05 -0700\"}}{{end}}", "%T", "{{if .ResponseTime}}{{.ResponseTime.Seconds | printf \"%.3f\"}}{{end}}", "%u", "{{.RemoteUser | dashIfEmptyStr}}", "%{User-Agent}i", "{{.R.UserAgent | dashIfEmptyStr}}", "%{Referer}i", "{{.R.Referer | dashIfEmptyStr}}", ) // Convert the Apache access log format into a text/template func (mw *AccessLogApacheMiddleware) convertFormat() { tmplText := apacheAdapter.Replace(string(mw.Format)) funcMap := template.FuncMap{ "dashIfEmptyStr": func(value string) string { if value == "" { return "-" } return value }, "dashIf0": func(value int64) string { if value == 0 { return "-" } return fmt.Sprintf("%d", value) }, "microseconds": func(dur *time.Duration) string { if dur != nil { return fmt.Sprintf("%d", dur.Nanoseconds()/1000) } return "" }, "statusCodeColor": func(statusCode int) string { if statusCode >= 400 && statusCode < 500 { return "1;33" } else if statusCode >= 500 { return "0;31" } return "0;32" }, } var err error mw.textTemplate, err = template.New("accessLog").Funcs(funcMap).Parse(tmplText) if err != nil { panic(err) } } // Execute the text template with the data derived from the request, and return a string. func (mw *AccessLogApacheMiddleware) executeTextTemplate(util *accessLogUtil) string { buf := bytes.NewBufferString("") err := mw.textTemplate.Execute(buf, util) if err != nil { panic(err) } return buf.String() } // accessLogUtil provides a collection of utility functions that devrive data from the Request object. // This object is used to provide data to the Apache Style template and the the JSON log record. type accessLogUtil struct { W ResponseWriter R *Request } // As stored by the auth middlewares. func (u *accessLogUtil) RemoteUser() string { if u.R.Env["REMOTE_USER"] != nil { return u.R.Env["REMOTE_USER"].(string) } return "" } // If qs exists then return it with a leadin "?", apache log style. func (u *accessLogUtil) ApacheQueryString() string { if u.R.URL.RawQuery != "" { return "?" + u.R.URL.RawQuery } return "" } // When the request entered the timer middleware. func (u *accessLogUtil) StartTime() *time.Time { if u.R.Env["START_TIME"] != nil { return u.R.Env["START_TIME"].(*time.Time) } return nil } // If remoteAddr is set then return is without the port number, apache log style. func (u *accessLogUtil) ApacheRemoteAddr() string { remoteAddr := u.R.RemoteAddr if remoteAddr != "" { if ip, _, err := net.SplitHostPort(remoteAddr); err == nil { return ip } } return "" } // As recorded by the recorder middleware. func (u *accessLogUtil) StatusCode() int { if u.R.Env["STATUS_CODE"] != nil { return u.R.Env["STATUS_CODE"].(int) } return 0 } // As mesured by the timer middleware. func (u *accessLogUtil) ResponseTime() *time.Duration { if u.R.Env["ELAPSED_TIME"] != nil { return u.R.Env["ELAPSED_TIME"].(*time.Duration) } return nil } // Process id. func (u *accessLogUtil) Pid() int { return os.Getpid() } // As recorded by the recorder middleware. func (u *accessLogUtil) BytesWritten() int64 { if u.R.Env["BYTES_WRITTEN"] != nil { return u.R.Env["BYTES_WRITTEN"].(int64) } return 0 } go-json-rest-3.3.2/rest/access_log_apache_test.go000066400000000000000000000036531301771661600220330ustar00rootroot00000000000000package rest import ( "bytes" "github.com/ant0ine/go-json-rest/rest/test" "log" "regexp" "testing" ) func TestAccessLogApacheMiddleware(t *testing.T) { api := NewApi() // the middlewares stack buffer := bytes.NewBufferString("") api.Use(&AccessLogApacheMiddleware{ Logger: log.New(buffer, "", 0), Format: CommonLogFormat, textTemplate: nil, }) api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) req.RemoteAddr = "127.0.0.1:1234" recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() // log tests, eg: '127.0.0.1 - - 29/Nov/2014:22:28:34 +0000 "GET / HTTP/1.1" 200 12' apacheCommon := regexp.MustCompile(`127.0.0.1 - - \d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2} [+\-]\d{4}\ "GET / HTTP/1.1" 200 12`) if !apacheCommon.Match(buffer.Bytes()) { t.Errorf("Got: %s", buffer.String()) } } func TestAccessLogApacheMiddlewareMissingData(t *testing.T) { api := NewApi() // the uncomplete middlewares stack buffer := bytes.NewBufferString("") api.Use(&AccessLogApacheMiddleware{ Logger: log.New(buffer, "", 0), Format: CommonLogFormat, textTemplate: nil, }) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() // not much to log when the Env data is missing, but this should still work apacheCommon := regexp.MustCompile(` - - "GET / HTTP/1.1" 0 -`) if !apacheCommon.Match(buffer.Bytes()) { t.Errorf("Got: %s", buffer.String()) } } go-json-rest-3.3.2/rest/access_log_json.go000066400000000000000000000041371301771661600205220ustar00rootroot00000000000000package rest import ( "encoding/json" "log" "os" "time" ) // AccessLogJsonMiddleware produces the access log with records written as JSON. This middleware // depends on TimerMiddleware and RecorderMiddleware that must be in the wrapped middlewares. It // also uses request.Env["REMOTE_USER"].(string) set by the auth middlewares. type AccessLogJsonMiddleware struct { // Logger points to the logger object used by this middleware, it defaults to // log.New(os.Stderr, "", 0). Logger *log.Logger } // MiddlewareFunc makes AccessLogJsonMiddleware implement the Middleware interface. func (mw *AccessLogJsonMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { mw.Logger = log.New(os.Stderr, "", 0) } return func(w ResponseWriter, r *Request) { // call the handler h(w, r) mw.Logger.Printf("%s", makeAccessLogJsonRecord(r).asJson()) } } // AccessLogJsonRecord is the data structure used by AccessLogJsonMiddleware to create the JSON // records. (Public for documentation only, no public method uses it) type AccessLogJsonRecord struct { Timestamp *time.Time StatusCode int ResponseTime *time.Duration HttpMethod string RequestURI string RemoteUser string UserAgent string } func makeAccessLogJsonRecord(r *Request) *AccessLogJsonRecord { var timestamp *time.Time if r.Env["START_TIME"] != nil { timestamp = r.Env["START_TIME"].(*time.Time) } var statusCode int if r.Env["STATUS_CODE"] != nil { statusCode = r.Env["STATUS_CODE"].(int) } var responseTime *time.Duration if r.Env["ELAPSED_TIME"] != nil { responseTime = r.Env["ELAPSED_TIME"].(*time.Duration) } var remoteUser string if r.Env["REMOTE_USER"] != nil { remoteUser = r.Env["REMOTE_USER"].(string) } return &AccessLogJsonRecord{ Timestamp: timestamp, StatusCode: statusCode, ResponseTime: responseTime, HttpMethod: r.Method, RequestURI: r.URL.RequestURI(), RemoteUser: remoteUser, UserAgent: r.UserAgent(), } } func (r *AccessLogJsonRecord) asJson() []byte { b, err := json.Marshal(r) if err != nil { panic(err) } return b } go-json-rest-3.3.2/rest/access_log_json_test.go000066400000000000000000000022451301771661600215570ustar00rootroot00000000000000package rest import ( "bytes" "encoding/json" "github.com/ant0ine/go-json-rest/rest/test" "log" "testing" ) func TestAccessLogJsonMiddleware(t *testing.T) { api := NewApi() // the middlewares stack buffer := bytes.NewBufferString("") api.Use(&AccessLogJsonMiddleware{ Logger: log.New(buffer, "", 0), }) api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) req.RemoteAddr = "127.0.0.1:1234" recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() // log tests decoded := &AccessLogJsonRecord{} err := json.Unmarshal(buffer.Bytes(), decoded) if err != nil { t.Fatal(err) } if decoded.StatusCode != 200 { t.Errorf("StatusCode 200 expected, got %d", decoded.StatusCode) } if decoded.RequestURI != "/" { t.Errorf("RequestURI / expected, got %s", decoded.RequestURI) } if decoded.HttpMethod != "GET" { t.Errorf("HttpMethod GET expected, got %s", decoded.HttpMethod) } } go-json-rest-3.3.2/rest/api.go000066400000000000000000000041661301771661600161420ustar00rootroot00000000000000package rest import ( "net/http" ) // Api defines a stack of Middlewares and an App. type Api struct { stack []Middleware app App } // NewApi makes a new Api object. The Middleware stack is empty, and the App is nil. func NewApi() *Api { return &Api{ stack: []Middleware{}, app: nil, } } // Use pushes one or multiple middlewares to the stack for middlewares // maintained in the Api object. func (api *Api) Use(middlewares ...Middleware) { api.stack = append(api.stack, middlewares...) } // SetApp sets the App in the Api object. func (api *Api) SetApp(app App) { api.app = app } // MakeHandler wraps all the Middlewares of the stack and the App together, and returns an // http.Handler ready to be used. If the Middleware stack is empty the App is used directly. If the // App is nil, a HandlerFunc that does nothing is used instead. func (api *Api) MakeHandler() http.Handler { var appFunc HandlerFunc if api.app != nil { appFunc = api.app.AppFunc() } else { appFunc = func(w ResponseWriter, r *Request) {} } return http.HandlerFunc( adapterFunc( WrapMiddlewares(api.stack, appFunc), ), ) } // Defines a stack of middlewares convenient for development. Among other things: // console friendly logging, JSON indentation, error stack strace in the response. var DefaultDevStack = []Middleware{ &AccessLogApacheMiddleware{}, &TimerMiddleware{}, &RecorderMiddleware{}, &PoweredByMiddleware{}, &RecoverMiddleware{ EnableResponseStackTrace: true, }, &JsonIndentMiddleware{}, &ContentTypeCheckerMiddleware{}, } // Defines a stack of middlewares convenient for production. Among other things: // Apache CombinedLogFormat logging, gzip compression. var DefaultProdStack = []Middleware{ &AccessLogApacheMiddleware{ Format: CombinedLogFormat, }, &TimerMiddleware{}, &RecorderMiddleware{}, &PoweredByMiddleware{}, &RecoverMiddleware{}, &GzipMiddleware{}, &ContentTypeCheckerMiddleware{}, } // Defines a stack of middlewares that should be common to most of the middleware stacks. var DefaultCommonStack = []Middleware{ &TimerMiddleware{}, &RecorderMiddleware{}, &PoweredByMiddleware{}, &RecoverMiddleware{}, } go-json-rest-3.3.2/rest/api_test.go000066400000000000000000000045311301771661600171750ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestApiNoAppNoMiddleware(t *testing.T) { api := NewApi() if api == nil { t.Fatal("Api object must be instantiated") } handler := api.MakeHandler() if handler == nil { t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) } func TestApiSimpleAppNoMiddleware(t *testing.T) { api := NewApi() api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) handler := api.MakeHandler() if handler == nil { t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Id":"123"}`) } func TestDevStack(t *testing.T) { api := NewApi() api.Use(DefaultDevStack...) api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) handler := api.MakeHandler() if handler == nil { t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs("{\n \"Id\": \"123\"\n}") } func TestProdStack(t *testing.T) { api := NewApi() api.Use(DefaultProdStack...) api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) handler := api.MakeHandler() if handler == nil { t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.ContentEncodingIsGzip() } func TestCommonStack(t *testing.T) { api := NewApi() api.Use(DefaultCommonStack...) api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) handler := api.MakeHandler() if handler == nil { t.Fatal("the http.Handler must have been created") } recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Id":"123"}`) } go-json-rest-3.3.2/rest/auth_basic.go000066400000000000000000000052171301771661600174710ustar00rootroot00000000000000package rest import ( "encoding/base64" "errors" "log" "net/http" "strings" ) // AuthBasicMiddleware provides a simple AuthBasic implementation. On failure, a 401 HTTP response //is returned. On success, the wrapped middleware is called, and the userId is made available as // request.Env["REMOTE_USER"].(string) type AuthBasicMiddleware struct { // Realm name to display to the user. Required. Realm string // Callback function that should perform the authentication of the user based on userId and // password. Must return true on success, false on failure. Required. Authenticator func(userId string, password string) bool // Callback function that should perform the authorization of the authenticated user. Called // only after an authentication success. Must return true on success, false on failure. // Optional, default to success. Authorizator func(userId string, request *Request) bool } // MiddlewareFunc makes AuthBasicMiddleware implement the Middleware interface. func (mw *AuthBasicMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { if mw.Realm == "" { log.Fatal("Realm is required") } if mw.Authenticator == nil { log.Fatal("Authenticator is required") } if mw.Authorizator == nil { mw.Authorizator = func(userId string, request *Request) bool { return true } } return func(writer ResponseWriter, request *Request) { authHeader := request.Header.Get("Authorization") if authHeader == "" { mw.unauthorized(writer) return } providedUserId, providedPassword, err := mw.decodeBasicAuthHeader(authHeader) if err != nil { Error(writer, "Invalid authentication", http.StatusBadRequest) return } if !mw.Authenticator(providedUserId, providedPassword) { mw.unauthorized(writer) return } if !mw.Authorizator(providedUserId, request) { mw.unauthorized(writer) return } request.Env["REMOTE_USER"] = providedUserId handler(writer, request) } } func (mw *AuthBasicMiddleware) unauthorized(writer ResponseWriter) { writer.Header().Set("WWW-Authenticate", "Basic realm="+mw.Realm) Error(writer, "Not Authorized", http.StatusUnauthorized) } func (mw *AuthBasicMiddleware) decodeBasicAuthHeader(header string) (user string, password string, err error) { parts := strings.SplitN(header, " ", 2) if !(len(parts) == 2 && parts[0] == "Basic") { return "", "", errors.New("Invalid authentication") } decoded, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { return "", "", errors.New("Invalid base64") } creds := strings.SplitN(string(decoded), ":", 2) if len(creds) != 2 { return "", "", errors.New("Invalid authentication") } return creds[0], creds[1], nil } go-json-rest-3.3.2/rest/auth_basic_test.go000066400000000000000000000045501301771661600205270ustar00rootroot00000000000000package rest import ( "encoding/base64" "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestAuthBasic(t *testing.T) { // the middleware to test authMiddleware := &AuthBasicMiddleware{ Realm: "test zone", Authenticator: func(userId string, password string) bool { if userId == "admin" && password == "admin" { return true } return false }, Authorizator: func(userId string, request *Request) bool { if request.Method == "GET" { return true } return false }, } // api for testing failure apiFailure := NewApi() apiFailure.Use(authMiddleware) apiFailure.SetApp(AppSimple(func(w ResponseWriter, r *Request) { t.Error("Should never be executed") })) handler := apiFailure.MakeHandler() // simple request fails recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(401) recorded.ContentTypeIsJson() // auth with wrong cred and right method fails wrongCredReq := test.MakeSimpleRequest("GET", "http://localhost/", nil) encoded := base64.StdEncoding.EncodeToString([]byte("admin:AdmIn")) wrongCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, handler, wrongCredReq) recorded.CodeIs(401) recorded.ContentTypeIsJson() // auth with right cred and wrong method fails rightCredReq := test.MakeSimpleRequest("POST", "http://localhost/", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, handler, rightCredReq) recorded.CodeIs(401) recorded.ContentTypeIsJson() // api for testing success apiSuccess := NewApi() apiSuccess.Use(authMiddleware) apiSuccess.SetApp(AppSimple(func(w ResponseWriter, r *Request) { if r.Env["REMOTE_USER"] == nil { t.Error("REMOTE_USER is nil") } user := r.Env["REMOTE_USER"].(string) if user != "admin" { t.Error("REMOTE_USER is expected to be 'admin'") } w.WriteJson(map[string]string{"Id": "123"}) })) // auth with right cred and right method succeeds rightCredReq = test.MakeSimpleRequest("GET", "http://localhost/", nil) encoded = base64.StdEncoding.EncodeToString([]byte("admin:admin")) rightCredReq.Header.Set("Authorization", "Basic "+encoded) recorded = test.RunRequest(t, apiSuccess.MakeHandler(), rightCredReq) recorded.CodeIs(200) recorded.ContentTypeIsJson() } go-json-rest-3.3.2/rest/content_type_checker.go000066400000000000000000000021551301771661600215640ustar00rootroot00000000000000package rest import ( "mime" "net/http" "strings" ) // ContentTypeCheckerMiddleware verifies the request Content-Type header and returns a // StatusUnsupportedMediaType (415) HTTP error response if it's incorrect. The expected // Content-Type is 'application/json' if the content is non-null. Note: If a charset parameter // exists, it MUST be UTF-8. type ContentTypeCheckerMiddleware struct{} // MiddlewareFunc makes ContentTypeCheckerMiddleware implement the Middleware interface. func (mw *ContentTypeCheckerMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { mediatype, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) charset, ok := params["charset"] if !ok { charset = "UTF-8" } // per net/http doc, means that the length is known and non-null if r.ContentLength > 0 && !(mediatype == "application/json" && strings.ToUpper(charset) == "UTF-8") { Error(w, "Bad Content-Type or charset, expected 'application/json'", http.StatusUnsupportedMediaType, ) return } // call the wrapped handler handler(w, r) } } go-json-rest-3.3.2/rest/content_type_checker_test.go000066400000000000000000000031001301771661600226120ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestContentTypeCheckerMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&ContentTypeCheckerMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() // no payload, no content length, no check recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) // JSON payload with correct content type recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"})) recorded.CodeIs(200) // JSON payload with correct content type specifying the utf-8 charset req := test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) req.Header.Set("Content-Type", "application/json; charset=utf-8") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(200) // JSON payload with incorrect content type req = test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) req.Header.Set("Content-Type", "text/x-json") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(415) // JSON payload with correct content type but incorrect charset req = test.MakeSimpleRequest("POST", "http://localhost/", map[string]string{"Id": "123"}) req.Header.Set("Content-Type", "application/json; charset=ISO-8859-1") recorded = test.RunRequest(t, handler, req) recorded.CodeIs(415) } go-json-rest-3.3.2/rest/cors.go000066400000000000000000000105661301771661600163400ustar00rootroot00000000000000package rest import ( "net/http" "strconv" "strings" ) // Possible improvements: // If AllowedMethods["*"] then Access-Control-Allow-Methods is set to the requested methods // If AllowedHeaderss["*"] then Access-Control-Allow-Headers is set to the requested headers // Put some presets in AllowedHeaders // Put some presets in AccessControlExposeHeaders // CorsMiddleware provides a configurable CORS implementation. type CorsMiddleware struct { allowedMethods map[string]bool allowedMethodsCsv string allowedHeaders map[string]bool allowedHeadersCsv string // Reject non CORS requests if true. See CorsInfo.IsCors. RejectNonCorsRequests bool // Function excecuted for every CORS requests to validate the Origin. (Required) // Must return true if valid, false if invalid. // For instance: simple equality, regexp, DB lookup, ... OriginValidator func(origin string, request *Request) bool // List of allowed HTTP methods. Note that the comparison will be made in // uppercase to avoid common mistakes. And that the // Access-Control-Allow-Methods response header also uses uppercase. // (see CorsInfo.AccessControlRequestMethod) AllowedMethods []string // List of allowed HTTP Headers. Note that the comparison will be made with // noarmalized names (http.CanonicalHeaderKey). And that the response header // also uses normalized names. // (see CorsInfo.AccessControlRequestHeaders) AllowedHeaders []string // List of headers used to set the Access-Control-Expose-Headers header. AccessControlExposeHeaders []string // User to se the Access-Control-Allow-Credentials response header. AccessControlAllowCredentials bool // Used to set the Access-Control-Max-Age response header, in seconds. AccessControlMaxAge int } // MiddlewareFunc makes CorsMiddleware implement the Middleware interface. func (mw *CorsMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { // precompute as much as possible at init time mw.allowedMethods = map[string]bool{} normedMethods := []string{} for _, allowedMethod := range mw.AllowedMethods { normed := strings.ToUpper(allowedMethod) mw.allowedMethods[normed] = true normedMethods = append(normedMethods, normed) } mw.allowedMethodsCsv = strings.Join(normedMethods, ",") mw.allowedHeaders = map[string]bool{} normedHeaders := []string{} for _, allowedHeader := range mw.AllowedHeaders { normed := http.CanonicalHeaderKey(allowedHeader) mw.allowedHeaders[normed] = true normedHeaders = append(normedHeaders, normed) } mw.allowedHeadersCsv = strings.Join(normedHeaders, ",") return func(writer ResponseWriter, request *Request) { corsInfo := request.GetCorsInfo() // non CORS requests if !corsInfo.IsCors { if mw.RejectNonCorsRequests { Error(writer, "Non CORS request", http.StatusForbidden) return } // continue, execute the wrapped middleware handler(writer, request) return } // Validate the Origin if mw.OriginValidator(corsInfo.Origin, request) == false { Error(writer, "Invalid Origin", http.StatusForbidden) return } if corsInfo.IsPreflight { // check the request methods if mw.allowedMethods[corsInfo.AccessControlRequestMethod] == false { Error(writer, "Invalid Preflight Request", http.StatusForbidden) return } // check the request headers for _, requestedHeader := range corsInfo.AccessControlRequestHeaders { if mw.allowedHeaders[requestedHeader] == false { Error(writer, "Invalid Preflight Request", http.StatusForbidden) return } } writer.Header().Set("Access-Control-Allow-Methods", mw.allowedMethodsCsv) writer.Header().Set("Access-Control-Allow-Headers", mw.allowedHeadersCsv) writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) if mw.AccessControlAllowCredentials == true { writer.Header().Set("Access-Control-Allow-Credentials", "true") } writer.Header().Set("Access-Control-Max-Age", strconv.Itoa(mw.AccessControlMaxAge)) writer.WriteHeader(http.StatusOK) return } // Non-preflight requests for _, exposed := range mw.AccessControlExposeHeaders { writer.Header().Add("Access-Control-Expose-Headers", exposed) } writer.Header().Set("Access-Control-Allow-Origin", corsInfo.Origin) if mw.AccessControlAllowCredentials == true { writer.Header().Set("Access-Control-Allow-Credentials", "true") } // continure, execute the wrapped middleware handler(writer, request) return } } go-json-rest-3.3.2/rest/cors_test.go000066400000000000000000000020101301771661600173600ustar00rootroot00000000000000package rest import ( "net/http" "testing" "github.com/ant0ine/go-json-rest/rest/test" ) func TestCorsMiddlewareEmptyAccessControlRequestHeaders(t *testing.T) { api := NewApi() // the middleware to test api.Use(&CorsMiddleware{ OriginValidator: func(_ string, _ *Request) bool { return true }, AllowedMethods: []string{ "GET", "POST", "PUT", }, AllowedHeaders: []string{ "Origin", "Referer", }, }) // wrap all handler := api.MakeHandler() req, _ := http.NewRequest("OPTIONS", "http://localhost", nil) req.Header.Set("Origin", "http://another.host") req.Header.Set("Access-Control-Request-Method", "PUT") req.Header.Set("Access-Control-Request-Headers", "") recorded := test.RunRequest(t, handler, req) t.Logf("recorded: %+v\n", recorded.Recorder) recorded.CodeIs(200) recorded.HeaderIs("Access-Control-Allow-Methods", "GET,POST,PUT") recorded.HeaderIs("Access-Control-Allow-Headers", "Origin,Referer") recorded.HeaderIs("Access-Control-Allow-Origin", "http://another.host") } go-json-rest-3.3.2/rest/doc.go000066400000000000000000000025601301771661600161320ustar00rootroot00000000000000// A quick and easy way to setup a RESTful JSON API // // http://ant0ine.github.io/go-json-rest/ // // Go-Json-Rest is a thin layer on top of net/http that helps building RESTful JSON APIs easily. // It provides fast and scalable request routing using a Trie based implementation, helpers to deal // with JSON requests and responses, and middlewares for functionalities like CORS, Auth, Gzip, // Status, ... // // Example: // // package main // // import ( // "github.com/ant0ine/go-json-rest/rest" // "log" // "net/http" // ) // // type User struct { // Id string // Name string // } // // func GetUser(w rest.ResponseWriter, req *rest.Request) { // user := User{ // Id: req.PathParam("id"), // Name: "Antoine", // } // w.WriteJson(&user) // } // // func main() { // api := rest.NewApi() // api.Use(rest.DefaultDevStack...) // router, err := rest.MakeRouter( // rest.Get("/users/:id", GetUser), // ) // if err != nil { // log.Fatal(err) // } // api.SetApp(router) // log.Fatal(http.ListenAndServe(":8080", api.MakeHandler())) // } // // package rest go-json-rest-3.3.2/rest/gzip.go000066400000000000000000000067731301771661600163500ustar00rootroot00000000000000package rest import ( "bufio" "compress/gzip" "net" "net/http" "strings" ) // GzipMiddleware is responsible for compressing the payload with gzip and setting the proper // headers when supported by the client. It must be wrapped by TimerMiddleware for the // compression time to be captured. And It must be wrapped by RecorderMiddleware for the // compressed BYTES_WRITTEN to be captured. type GzipMiddleware struct{} // MiddlewareFunc makes GzipMiddleware implement the Middleware interface. func (mw *GzipMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { // gzip support enabled canGzip := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") // client accepts gzip ? writer := &gzipResponseWriter{w, false, canGzip, nil} defer func() { // need to close gzip writer if writer.gzipWriter != nil { writer.gzipWriter.Close() } }() // call the handler with the wrapped writer h(writer, r) } } // Private responseWriter intantiated by the gzip middleware. // It encodes the payload with gzip and set the proper headers. // It implements the following interfaces: // ResponseWriter // http.ResponseWriter // http.Flusher // http.CloseNotifier // http.Hijacker type gzipResponseWriter struct { ResponseWriter wroteHeader bool canGzip bool gzipWriter *gzip.Writer } // Set the right headers for gzip encoded responses. func (w *gzipResponseWriter) WriteHeader(code int) { // Always set the Vary header, even if this particular request // is not gzipped. w.Header().Add("Vary", "Accept-Encoding") if w.canGzip { w.Header().Set("Content-Encoding", "gzip") } w.ResponseWriter.WriteHeader(code) w.wroteHeader = true } // Make sure the local Write is called. func (w *gzipResponseWriter) WriteJson(v interface{}) error { b, err := w.EncodeJson(v) if err != nil { return err } _, err = w.Write(b) if err != nil { return err } return nil } // Make sure the local WriteHeader is called, and call the parent Flush. // Provided in order to implement the http.Flusher interface. func (w *gzipResponseWriter) Flush() { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } flusher := w.ResponseWriter.(http.Flusher) flusher.Flush() } // Call the parent CloseNotify. // Provided in order to implement the http.CloseNotifier interface. func (w *gzipResponseWriter) CloseNotify() <-chan bool { notifier := w.ResponseWriter.(http.CloseNotifier) return notifier.CloseNotify() } // Provided in order to implement the http.Hijacker interface. func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker := w.ResponseWriter.(http.Hijacker) return hijacker.Hijack() } // Make sure the local WriteHeader is called, and encode the payload if necessary. // Provided in order to implement the http.ResponseWriter interface. func (w *gzipResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } writer := w.ResponseWriter.(http.ResponseWriter) if w.canGzip { // Write can be called multiple times for a given response. // (see the streaming example: // https://github.com/ant0ine/go-json-rest-examples/tree/master/streaming) // The gzipWriter is instantiated only once, and flushed after // each write. if w.gzipWriter == nil { w.gzipWriter = gzip.NewWriter(writer) } count, errW := w.gzipWriter.Write(b) errF := w.gzipWriter.Flush() if errW != nil { return count, errW } if errF != nil { return count, errF } return count, nil } return writer.Write(b) } go-json-rest-3.3.2/rest/gzip_test.go000066400000000000000000000030621301771661600173730ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestGzipEnabled(t *testing.T) { api := NewApi() // the middleware to test api.Use(&GzipMiddleware{}) // router app with success and error paths router, err := MakeRouter( Get("/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) }), Get("/error", func(w ResponseWriter, r *Request) { Error(w, "gzipped error", 500) }), ) if err != nil { t.Fatal(err) } api.SetApp(router) // wrap all handler := api.MakeHandler() recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.ContentEncodingIsGzip() recorded.HeaderIs("Vary", "Accept-Encoding") recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error", nil)) recorded.CodeIs(500) recorded.ContentTypeIsJson() recorded.ContentEncodingIsGzip() recorded.HeaderIs("Vary", "Accept-Encoding") } func TestGzipDisabled(t *testing.T) { api := NewApi() // router app with success and error paths router, err := MakeRouter( Get("/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) }), ) if err != nil { t.Fatal(err) } api.SetApp(router) handler := api.MakeHandler() recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.HeaderIs("Content-Encoding", "") recorded.HeaderIs("Vary", "") } go-json-rest-3.3.2/rest/if.go000066400000000000000000000024601301771661600157620ustar00rootroot00000000000000package rest import ( "log" ) // IfMiddleware evaluates at runtime a condition based on the current request, and decides to // execute one of the other Middleware based on this boolean. type IfMiddleware struct { // Runtime condition that decides of the execution of IfTrue of IfFalse. Condition func(r *Request) bool // Middleware to run when the condition is true. Note that the middleware is initialized // weather if will be used or not. (Optional, pass-through if not set) IfTrue Middleware // Middleware to run when the condition is false. Note that the middleware is initialized // weather if will be used or not. (Optional, pass-through if not set) IfFalse Middleware } // MiddlewareFunc makes TimerMiddleware implement the Middleware interface. func (mw *IfMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { if mw.Condition == nil { log.Fatal("IfMiddleware Condition is required") } var ifTrueHandler HandlerFunc if mw.IfTrue != nil { ifTrueHandler = mw.IfTrue.MiddlewareFunc(h) } else { ifTrueHandler = h } var ifFalseHandler HandlerFunc if mw.IfFalse != nil { ifFalseHandler = mw.IfFalse.MiddlewareFunc(h) } else { ifFalseHandler = h } return func(w ResponseWriter, r *Request) { if mw.Condition(r) { ifTrueHandler(w, r) } else { ifFalseHandler(w, r) } } } go-json-rest-3.3.2/rest/if_test.go000066400000000000000000000022741301771661600170240ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestIfMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&IfMiddleware{ Condition: func(r *Request) bool { if r.URL.Path == "/true" { return true } return false }, IfTrue: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { r.Env["TRUE_MIDDLEWARE"] = true handler(w, r) } }), IfFalse: MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { r.Env["FALSE_MIDDLEWARE"] = true handler(w, r) } }), }) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(r.Env) })) // wrap all handler := api.MakeHandler() recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs("{\"FALSE_MIDDLEWARE\":true}") recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/true", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs("{\"TRUE_MIDDLEWARE\":true}") } go-json-rest-3.3.2/rest/json_indent.go000066400000000000000000000057631301771661600177070ustar00rootroot00000000000000package rest import ( "bufio" "encoding/json" "net" "net/http" ) // JsonIndentMiddleware provides JSON encoding with indentation. // It could be convenient to use it during development. // It works by "subclassing" the responseWriter provided by the wrapping middleware, // replacing the writer.EncodeJson and writer.WriteJson implementations, // and making the parent implementations ignored. type JsonIndentMiddleware struct { // prefix string, as in json.MarshalIndent Prefix string // indentation string, as in json.MarshalIndent Indent string } // MiddlewareFunc makes JsonIndentMiddleware implement the Middleware interface. func (mw *JsonIndentMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { if mw.Indent == "" { mw.Indent = " " } return func(w ResponseWriter, r *Request) { writer := &jsonIndentResponseWriter{w, false, mw.Prefix, mw.Indent} // call the wrapped handler handler(writer, r) } } // Private responseWriter intantiated by the middleware. // It implements the following interfaces: // ResponseWriter // http.ResponseWriter // http.Flusher // http.CloseNotifier // http.Hijacker type jsonIndentResponseWriter struct { ResponseWriter wroteHeader bool prefix string indent string } // Replace the parent EncodeJson to provide indentation. func (w *jsonIndentResponseWriter) EncodeJson(v interface{}) ([]byte, error) { b, err := json.MarshalIndent(v, w.prefix, w.indent) if err != nil { return nil, err } return b, nil } // Make sure the local EncodeJson and local Write are called. // Does not call the parent WriteJson. func (w *jsonIndentResponseWriter) WriteJson(v interface{}) error { b, err := w.EncodeJson(v) if err != nil { return err } _, err = w.Write(b) if err != nil { return err } return nil } // Call the parent WriteHeader. func (w *jsonIndentResponseWriter) WriteHeader(code int) { w.ResponseWriter.WriteHeader(code) w.wroteHeader = true } // Make sure the local WriteHeader is called, and call the parent Flush. // Provided in order to implement the http.Flusher interface. func (w *jsonIndentResponseWriter) Flush() { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } flusher := w.ResponseWriter.(http.Flusher) flusher.Flush() } // Call the parent CloseNotify. // Provided in order to implement the http.CloseNotifier interface. func (w *jsonIndentResponseWriter) CloseNotify() <-chan bool { notifier := w.ResponseWriter.(http.CloseNotifier) return notifier.CloseNotify() } // Provided in order to implement the http.Hijacker interface. func (w *jsonIndentResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker := w.ResponseWriter.(http.Hijacker) return hijacker.Hijack() } // Make sure the local WriteHeader is called, and call the parent Write. // Provided in order to implement the http.ResponseWriter interface. func (w *jsonIndentResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } writer := w.ResponseWriter.(http.ResponseWriter) return writer.Write(b) } go-json-rest-3.3.2/rest/json_indent_test.go000066400000000000000000000011151301771661600207310ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestJsonIndentMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&JsonIndentMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs("{\n \"Id\": \"123\"\n}") } go-json-rest-3.3.2/rest/jsonp.go000066400000000000000000000060501301771661600165140ustar00rootroot00000000000000package rest import ( "bufio" "net" "net/http" ) // JsonpMiddleware provides JSONP responses on demand, based on the presence // of a query string argument specifying the callback name. type JsonpMiddleware struct { // Name of the query string parameter used to specify the // the name of the JS callback used for the padding. // Defaults to "callback". CallbackNameKey string } // MiddlewareFunc returns a HandlerFunc that implements the middleware. func (mw *JsonpMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { if mw.CallbackNameKey == "" { mw.CallbackNameKey = "callback" } return func(w ResponseWriter, r *Request) { callbackName := r.URL.Query().Get(mw.CallbackNameKey) // TODO validate the callbackName ? if callbackName != "" { // the client request JSONP, instantiate JsonpMiddleware. writer := &jsonpResponseWriter{w, false, callbackName} // call the handler with the wrapped writer h(writer, r) } else { // do nothing special h(w, r) } } } // Private responseWriter intantiated by the JSONP middleware. // It adds the padding to the payload and set the proper headers. // It implements the following interfaces: // ResponseWriter // http.ResponseWriter // http.Flusher // http.CloseNotifier // http.Hijacker type jsonpResponseWriter struct { ResponseWriter wroteHeader bool callbackName string } // Overwrite the Content-Type to be text/javascript func (w *jsonpResponseWriter) WriteHeader(code int) { w.Header().Set("Content-Type", "text/javascript") w.ResponseWriter.WriteHeader(code) w.wroteHeader = true } // Make sure the local Write is called. func (w *jsonpResponseWriter) WriteJson(v interface{}) error { b, err := w.EncodeJson(v) if err != nil { return err } // JSONP security fix (http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/) w.Header().Set("Content-Disposition", "filename=f.txt") w.Header().Set("X-Content-Type-Options", "nosniff") w.Write([]byte("/**/" + w.callbackName + "(")) w.Write(b) w.Write([]byte(")")) return nil } // Make sure the local WriteHeader is called, and call the parent Flush. // Provided in order to implement the http.Flusher interface. func (w *jsonpResponseWriter) Flush() { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } flusher := w.ResponseWriter.(http.Flusher) flusher.Flush() } // Call the parent CloseNotify. // Provided in order to implement the http.CloseNotifier interface. func (w *jsonpResponseWriter) CloseNotify() <-chan bool { notifier := w.ResponseWriter.(http.CloseNotifier) return notifier.CloseNotify() } // Provided in order to implement the http.Hijacker interface. func (w *jsonpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker := w.ResponseWriter.(http.Hijacker) return hijacker.Hijack() } // Make sure the local WriteHeader is called. // Provided in order to implement the http.ResponseWriter interface. func (w *jsonpResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } writer := w.ResponseWriter.(http.ResponseWriter) return writer.Write(b) } go-json-rest-3.3.2/rest/jsonp_test.go000066400000000000000000000024601301771661600175540ustar00rootroot00000000000000package rest import ( "testing" "github.com/ant0ine/go-json-rest/rest/test" ) func TestJsonpMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&JsonpMiddleware{}) // router app with success and error paths router, err := MakeRouter( Get("/ok", func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) }), Get("/error", func(w ResponseWriter, r *Request) { Error(w, "jsonp error", 500) }), ) if err != nil { t.Fatal(err) } api.SetApp(router) // wrap all handler := api.MakeHandler() recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/ok?callback=parseResponse", nil)) recorded.CodeIs(200) recorded.HeaderIs("Content-Type", "text/javascript") recorded.HeaderIs("Content-Disposition", "filename=f.txt") recorded.HeaderIs("X-Content-Type-Options", "nosniff") recorded.BodyIs("/**/parseResponse({\"Id\":\"123\"})") recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/error?callback=parseResponse", nil)) recorded.CodeIs(500) recorded.HeaderIs("Content-Type", "text/javascript") recorded.HeaderIs("Content-Disposition", "filename=f.txt") recorded.HeaderIs("X-Content-Type-Options", "nosniff") recorded.BodyIs("/**/parseResponse({\"Error\":\"jsonp error\"})") } go-json-rest-3.3.2/rest/middleware.go000066400000000000000000000044271301771661600175060ustar00rootroot00000000000000package rest import ( "net/http" ) // HandlerFunc defines the handler function. It is the go-json-rest equivalent of http.HandlerFunc. type HandlerFunc func(ResponseWriter, *Request) // App defines the interface that an object should implement to be used as an app in this framework // stack. The App is the top element of the stack, the other elements being middlewares. type App interface { AppFunc() HandlerFunc } // AppSimple is an adapter type that makes it easy to write an App with a simple function. // eg: rest.NewApi(rest.AppSimple(func(w rest.ResponseWriter, r *rest.Request) { ... })) type AppSimple HandlerFunc // AppFunc makes AppSimple implement the App interface. func (as AppSimple) AppFunc() HandlerFunc { return HandlerFunc(as) } // Middleware defines the interface that objects must implement in order to wrap a HandlerFunc and // be used in the middleware stack. type Middleware interface { MiddlewareFunc(handler HandlerFunc) HandlerFunc } // MiddlewareSimple is an adapter type that makes it easy to write a Middleware with a simple // function. eg: api.Use(rest.MiddlewareSimple(func(h HandlerFunc) Handlerfunc { ... })) type MiddlewareSimple func(handler HandlerFunc) HandlerFunc // MiddlewareFunc makes MiddlewareSimple implement the Middleware interface. func (ms MiddlewareSimple) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return ms(handler) } // WrapMiddlewares calls the MiddlewareFunc methods in the reverse order and returns an HandlerFunc // ready to be executed. This can be used to wrap a set of middlewares, post routing, on a per Route // basis. func WrapMiddlewares(middlewares []Middleware, handler HandlerFunc) HandlerFunc { wrapped := handler for i := len(middlewares) - 1; i >= 0; i-- { wrapped = middlewares[i].MiddlewareFunc(wrapped) } return wrapped } // Handle the transition between net/http and go-json-rest objects. // It intanciates the rest.Request and rest.ResponseWriter, ... func adapterFunc(handler HandlerFunc) http.HandlerFunc { return func(origWriter http.ResponseWriter, origRequest *http.Request) { // instantiate the rest objects request := &Request{ origRequest, nil, map[string]interface{}{}, } writer := &responseWriter{ origWriter, false, } // call the wrapped handler handler(writer, request) } } go-json-rest-3.3.2/rest/middleware_test.go000066400000000000000000000020761301771661600205430ustar00rootroot00000000000000package rest import ( "testing" ) type testMiddleware struct { name string } func (mw *testMiddleware) MiddlewareFunc(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { if r.Env["BEFORE"] == nil { r.Env["BEFORE"] = mw.name } else { r.Env["BEFORE"] = r.Env["BEFORE"].(string) + mw.name } handler(w, r) if r.Env["AFTER"] == nil { r.Env["AFTER"] = mw.name } else { r.Env["AFTER"] = r.Env["AFTER"].(string) + mw.name } } } func TestWrapMiddlewares(t *testing.T) { a := &testMiddleware{"A"} b := &testMiddleware{"B"} c := &testMiddleware{"C"} app := func(w ResponseWriter, r *Request) { // do nothing } handlerFunc := WrapMiddlewares([]Middleware{a, b, c}, app) // fake request r := &Request{ nil, nil, map[string]interface{}{}, } handlerFunc(nil, r) before := r.Env["BEFORE"].(string) if before != "ABC" { t.Error("middleware executed in the wrong order, expected ABC") } after := r.Env["AFTER"].(string) if after != "CBA" { t.Error("middleware executed in the wrong order, expected CBA") } } go-json-rest-3.3.2/rest/powered_by.go000066400000000000000000000012401301771661600175160ustar00rootroot00000000000000package rest const xPoweredByDefault = "go-json-rest" // PoweredByMiddleware adds the "X-Powered-By" header to the HTTP response. type PoweredByMiddleware struct { // If specified, used as the value for the "X-Powered-By" response header. // Defaults to "go-json-rest". XPoweredBy string } // MiddlewareFunc makes PoweredByMiddleware implement the Middleware interface. func (mw *PoweredByMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { poweredBy := xPoweredByDefault if mw.XPoweredBy != "" { poweredBy = mw.XPoweredBy } return func(w ResponseWriter, r *Request) { w.Header().Add("X-Powered-By", poweredBy) // call the handler h(w, r) } } go-json-rest-3.3.2/rest/powered_by_test.go000066400000000000000000000011421301771661600205560ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestPoweredByMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&PoweredByMiddleware{ XPoweredBy: "test", }) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.HeaderIs("X-Powered-By", "test") } go-json-rest-3.3.2/rest/recorder.go000066400000000000000000000052141301771661600171710ustar00rootroot00000000000000package rest import ( "bufio" "net" "net/http" ) // RecorderMiddleware keeps a record of the HTTP status code of the response, // and the number of bytes written. // The result is available to the wrapping handlers as request.Env["STATUS_CODE"].(int), // and as request.Env["BYTES_WRITTEN"].(int64) type RecorderMiddleware struct{} // MiddlewareFunc makes RecorderMiddleware implement the Middleware interface. func (mw *RecorderMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { writer := &recorderResponseWriter{w, 0, false, 0} // call the handler h(writer, r) r.Env["STATUS_CODE"] = writer.statusCode r.Env["BYTES_WRITTEN"] = writer.bytesWritten } } // Private responseWriter intantiated by the recorder middleware. // It keeps a record of the HTTP status code of the response. // It implements the following interfaces: // ResponseWriter // http.ResponseWriter // http.Flusher // http.CloseNotifier // http.Hijacker type recorderResponseWriter struct { ResponseWriter statusCode int wroteHeader bool bytesWritten int64 } // Record the status code. func (w *recorderResponseWriter) WriteHeader(code int) { w.ResponseWriter.WriteHeader(code) if w.wroteHeader { return } w.statusCode = code w.wroteHeader = true } // Make sure the local Write is called. func (w *recorderResponseWriter) WriteJson(v interface{}) error { b, err := w.EncodeJson(v) if err != nil { return err } _, err = w.Write(b) if err != nil { return err } return nil } // Make sure the local WriteHeader is called, and call the parent Flush. // Provided in order to implement the http.Flusher interface. func (w *recorderResponseWriter) Flush() { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } flusher := w.ResponseWriter.(http.Flusher) flusher.Flush() } // Call the parent CloseNotify. // Provided in order to implement the http.CloseNotifier interface. func (w *recorderResponseWriter) CloseNotify() <-chan bool { notifier := w.ResponseWriter.(http.CloseNotifier) return notifier.CloseNotify() } // Provided in order to implement the http.Hijacker interface. func (w *recorderResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker := w.ResponseWriter.(http.Hijacker) return hijacker.Hijack() } // Make sure the local WriteHeader is called, and call the parent Write. // Provided in order to implement the http.ResponseWriter interface. func (w *recorderResponseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } writer := w.ResponseWriter.(http.ResponseWriter) written, err := writer.Write(b) w.bytesWritten += int64(written) return written, err } go-json-rest-3.3.2/rest/recorder_test.go000066400000000000000000000063361301771661600202360ustar00rootroot00000000000000package rest import ( "testing" "github.com/ant0ine/go-json-rest/rest/test" ) func TestRecorderMiddleware(t *testing.T) { api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { handler(w, r) if r.Env["STATUS_CODE"] == nil { t.Error("STATUS_CODE is nil") } statusCode := r.Env["STATUS_CODE"].(int) if statusCode != 200 { t.Errorf("STATUS_CODE = 200 expected, got %d", statusCode) } if r.Env["BYTES_WRITTEN"] == nil { t.Error("BYTES_WRITTEN is nil") } bytesWritten := r.Env["BYTES_WRITTEN"].(int64) // '{"Id":"123"}' => 12 chars if bytesWritten != 12 { t.Errorf("BYTES_WRITTEN 12 expected, got %d", bytesWritten) } } })) // the middleware to test api.Use(&RecorderMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() } // See how many bytes are written when gzipping func TestRecorderAndGzipMiddleware(t *testing.T) { api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { handler(w, r) if r.Env["BYTES_WRITTEN"] == nil { t.Error("BYTES_WRITTEN is nil") } bytesWritten := r.Env["BYTES_WRITTEN"].(int64) // Yes, the gzipped version actually takes more space. if bytesWritten != 41 { t.Errorf("BYTES_WRITTEN 41 expected, got %d", bytesWritten) } } })) // the middlewares to test api.Use(&RecorderMiddleware{}) api.Use(&GzipMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) // "Accept-Encoding", "gzip" is set by test.MakeSimpleRequest recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() } //Underlying net/http only allows you to set the status code once func TestRecorderMiddlewareReportsSameStatusCodeAsResponse(t *testing.T) { api := NewApi() const firstCode = 400 const secondCode = 500 // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { handler(w, r) if r.Env["STATUS_CODE"] == nil { t.Error("STATUS_CODE is nil") } statusCode := r.Env["STATUS_CODE"].(int) if statusCode != firstCode { t.Errorf("STATUS_CODE = %d expected, got %d", firstCode, statusCode) } } })) // the middleware to test api.Use(&RecorderMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteHeader(firstCode) w.WriteHeader(secondCode) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(firstCode) recorded.ContentTypeIsJson() } go-json-rest-3.3.2/rest/recover.go000066400000000000000000000032451301771661600170330ustar00rootroot00000000000000package rest import ( "encoding/json" "fmt" "log" "net/http" "os" "runtime/debug" ) // RecoverMiddleware catches the panic errors that occur in the wrapped HandleFunc, // and convert them to 500 responses. type RecoverMiddleware struct { // Custom logger used for logging the panic errors, // optional, defaults to log.New(os.Stderr, "", 0) Logger *log.Logger // If true, the log records will be printed as JSON. Convenient for log parsing. EnableLogAsJson bool // If true, when a "panic" happens, the error string and the stack trace will be // printed in the 500 response body. EnableResponseStackTrace bool } // MiddlewareFunc makes RecoverMiddleware implement the Middleware interface. func (mw *RecoverMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { // set the default Logger if mw.Logger == nil { mw.Logger = log.New(os.Stderr, "", 0) } return func(w ResponseWriter, r *Request) { // catch user code's panic, and convert to http response defer func() { if reco := recover(); reco != nil { trace := debug.Stack() // log the trace message := fmt.Sprintf("%s\n%s", reco, trace) mw.logError(message) // write error response if mw.EnableResponseStackTrace { Error(w, message, http.StatusInternalServerError) } else { Error(w, "Internal Server Error", http.StatusInternalServerError) } } }() // call the handler h(w, r) } } func (mw *RecoverMiddleware) logError(message string) { if mw.EnableLogAsJson { record := map[string]string{ "error": message, } b, err := json.Marshal(&record) if err != nil { panic(err) } mw.Logger.Printf("%s", b) } else { mw.Logger.Print(message) } } go-json-rest-3.3.2/rest/recover_test.go000066400000000000000000000015661301771661600200760ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "io/ioutil" "log" "testing" ) func TestRecoverMiddleware(t *testing.T) { api := NewApi() // the middleware to test api.Use(&RecoverMiddleware{ Logger: log.New(ioutil.Discard, "", 0), EnableLogAsJson: false, EnableResponseStackTrace: true, }) // a simple app that fails api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { panic("test") })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(500) recorded.ContentTypeIsJson() // payload payload := map[string]string{} err := recorded.DecodeJsonPayload(&payload) if err != nil { t.Fatal(err) } if payload["Error"] == "" { t.Errorf("Expected an error message, got: %v", payload) } } go-json-rest-3.3.2/rest/request.go000066400000000000000000000066351301771661600170640ustar00rootroot00000000000000package rest import ( "encoding/json" "errors" "io/ioutil" "net/http" "net/url" "strings" ) var ( // ErrJsonPayloadEmpty is returned when the JSON payload is empty. ErrJsonPayloadEmpty = errors.New("JSON payload is empty") ) // Request inherits from http.Request, and provides additional methods. type Request struct { *http.Request // Map of parameters that have been matched in the URL Path. PathParams map[string]string // Environment used by middlewares to communicate. Env map[string]interface{} } // PathParam provides a convenient access to the PathParams map. func (r *Request) PathParam(name string) string { return r.PathParams[name] } // DecodeJsonPayload reads the request body and decodes the JSON using json.Unmarshal. func (r *Request) DecodeJsonPayload(v interface{}) error { content, err := ioutil.ReadAll(r.Body) r.Body.Close() if err != nil { return err } if len(content) == 0 { return ErrJsonPayloadEmpty } err = json.Unmarshal(content, v) if err != nil { return err } return nil } // BaseUrl returns a new URL object with the Host and Scheme taken from the request. // (without the trailing slash in the host) func (r *Request) BaseUrl() *url.URL { scheme := r.URL.Scheme if scheme == "" { scheme = "http" } host := r.Host if len(host) > 0 && host[len(host)-1] == '/' { host = host[:len(host)-1] } return &url.URL{ Scheme: scheme, Host: host, } } // UrlFor returns the URL object from UriBase with the Path set to path, and the query // string built with queryParams. func (r *Request) UrlFor(path string, queryParams map[string][]string) *url.URL { baseUrl := r.BaseUrl() baseUrl.Path = path if queryParams != nil { query := url.Values{} for k, v := range queryParams { for _, vv := range v { query.Add(k, vv) } } baseUrl.RawQuery = query.Encode() } return baseUrl } // CorsInfo contains the CORS request info derived from a rest.Request. type CorsInfo struct { IsCors bool IsPreflight bool Origin string OriginUrl *url.URL // The header value is converted to uppercase to avoid common mistakes. AccessControlRequestMethod string // The header values are normalized with http.CanonicalHeaderKey. AccessControlRequestHeaders []string } // GetCorsInfo derives CorsInfo from Request. func (r *Request) GetCorsInfo() *CorsInfo { origin := r.Header.Get("Origin") var originUrl *url.URL var isCors bool if origin == "" { isCors = false } else if origin == "null" { isCors = true } else { var err error originUrl, err = url.ParseRequestURI(origin) isCors = err == nil && r.Host != originUrl.Host } reqMethod := r.Header.Get("Access-Control-Request-Method") reqHeaders := []string{} rawReqHeaders := r.Header[http.CanonicalHeaderKey("Access-Control-Request-Headers")] for _, rawReqHeader := range rawReqHeaders { if len(rawReqHeader) == 0 { continue } // net/http does not handle comma delimited headers for us for _, reqHeader := range strings.Split(rawReqHeader, ",") { reqHeaders = append(reqHeaders, http.CanonicalHeaderKey(strings.TrimSpace(reqHeader))) } } isPreflight := isCors && r.Method == "OPTIONS" && reqMethod != "" return &CorsInfo{ IsCors: isCors, IsPreflight: isPreflight, Origin: origin, OriginUrl: originUrl, AccessControlRequestMethod: strings.ToUpper(reqMethod), AccessControlRequestHeaders: reqHeaders, } } go-json-rest-3.3.2/rest/request_test.go000066400000000000000000000116771301771661600201250ustar00rootroot00000000000000package rest import ( "io" "net/http" "strings" "testing" ) func defaultRequest(method string, urlStr string, body io.Reader, t *testing.T) *Request { origReq, err := http.NewRequest(method, urlStr, body) if err != nil { t.Fatal(err) } return &Request{ origReq, nil, map[string]interface{}{}, } } func TestRequestEmptyJson(t *testing.T) { req := defaultRequest("POST", "http://localhost", strings.NewReader(""), t) err := req.DecodeJsonPayload(nil) if err != ErrJsonPayloadEmpty { t.Error("Expected ErrJsonPayloadEmpty") } } func TestRequestBaseUrl(t *testing.T) { req := defaultRequest("GET", "http://localhost", nil, t) urlBase := req.BaseUrl() urlString := urlBase.String() expected := "http://localhost" if urlString != expected { t.Error(expected + " was the expected URL base, but instead got " + urlString) } } func TestRequestUrlScheme(t *testing.T) { req := defaultRequest("GET", "https://localhost", nil, t) urlBase := req.BaseUrl() expected := "https" if urlBase.Scheme != expected { t.Error(expected + " was the expected scheme, but instead got " + urlBase.Scheme) } } func TestRequestUrlFor(t *testing.T) { req := defaultRequest("GET", "http://localhost", nil, t) path := "/foo/bar" urlObj := req.UrlFor(path, nil) if urlObj.Path != path { t.Error(path + " was expected to be the path, but got " + urlObj.Path) } expected := "http://localhost/foo/bar" if urlObj.String() != expected { t.Error(expected + " was expected, but the returned URL was " + urlObj.String()) } } func TestRequestUrlForQueryString(t *testing.T) { req := defaultRequest("GET", "http://localhost", nil, t) params := map[string][]string{ "id": {"foo", "bar"}, } urlObj := req.UrlFor("/foo/bar", params) expected := "http://localhost/foo/bar?id=foo&id=bar" if urlObj.String() != expected { t.Error(expected + " was expected, but the returned URL was " + urlObj.String()) } } func TestCorsInfoSimpleCors(t *testing.T) { req := defaultRequest("GET", "http://localhost", nil, t) req.Request.Header.Set("Origin", "http://another.host") corsInfo := req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if corsInfo.IsPreflight == true { t.Error("This is not a Preflight request") } } func TestCorsInfoNullOrigin(t *testing.T) { req := defaultRequest("GET", "http://localhost", nil, t) req.Request.Header.Set("Origin", "null") corsInfo := req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if corsInfo.IsPreflight == true { t.Error("This is not a Preflight request") } if corsInfo.OriginUrl != nil { t.Error("OriginUrl cannot be set") } } func TestCorsInfoPreflightCors(t *testing.T) { req := defaultRequest("OPTIONS", "http://localhost", nil, t) req.Request.Header.Set("Origin", "http://another.host") corsInfo := req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if corsInfo.IsPreflight == true { t.Error("This is NOT a Preflight request") } // Preflight must have the Access-Control-Request-Method header req.Request.Header.Set("Access-Control-Request-Method", "PUT") corsInfo = req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if corsInfo.IsPreflight == false { t.Error("This is a Preflight request") } if corsInfo.Origin != "http://another.host" { t.Error("Origin must be identical to the header value") } if corsInfo.OriginUrl == nil { t.Error("OriginUrl must be set") } } func TestCorsInfoEmptyAccessControlRequestHeaders(t *testing.T) { req := defaultRequest("OPTIONS", "http://localhost", nil, t) req.Request.Header.Set("Origin", "http://another.host") // make it a preflight request req.Request.Header.Set("Access-Control-Request-Method", "PUT") // WebKit based browsers may send `Access-Control-Request-Headers:` with // no value, in which case, the header will be present in requests // Header map, but its value is an empty string. req.Request.Header.Set("Access-Control-Request-Headers", "") corsInfo := req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if len(corsInfo.AccessControlRequestHeaders) > 0 { t.Error("Access-Control-Request-Headers should have been removed") } req.Request.Header.Set("Access-Control-Request-Headers", "") corsInfo = req.GetCorsInfo() if corsInfo == nil { t.Error("Expected non nil CorsInfo") } if corsInfo.IsCors == false { t.Error("This is a CORS request") } if corsInfo.IsPreflight == false { t.Error("This is a Preflight request") } if len(corsInfo.AccessControlRequestHeaders) > 0 { t.Error("Empty Access-Control-Request-Headers header should have been removed") } } go-json-rest-3.3.2/rest/response.go000066400000000000000000000074551301771661600172330ustar00rootroot00000000000000package rest import ( "bufio" "encoding/json" "net" "net/http" ) // A ResponseWriter interface dedicated to JSON HTTP response. // Note, the responseWriter object instantiated by the framework also implements many other interfaces // accessible by type assertion: http.ResponseWriter, http.Flusher, http.CloseNotifier, http.Hijacker. type ResponseWriter interface { // Identical to the http.ResponseWriter interface Header() http.Header // Use EncodeJson to generate the payload, write the headers with http.StatusOK if // they are not already written, then write the payload. // The Content-Type header is set to "application/json", unless already specified. WriteJson(v interface{}) error // Encode the data structure to JSON, mainly used to wrap ResponseWriter in // middlewares. EncodeJson(v interface{}) ([]byte, error) // Similar to the http.ResponseWriter interface, with additional JSON related // headers set. WriteHeader(int) } // This allows to customize the field name used in the error response payload. // It defaults to "Error" for compatibility reason, but can be changed before starting the server. // eg: rest.ErrorFieldName = "errorMessage" var ErrorFieldName = "Error" // Error produces an error response in JSON with the following structure, '{"Error":"My error message"}' // The standard plain text net/http Error helper can still be called like this: // http.Error(w, "error message", code) func Error(w ResponseWriter, error string, code int) { w.WriteHeader(code) err := w.WriteJson(map[string]string{ErrorFieldName: error}) if err != nil { panic(err) } } // NotFound produces a 404 response with the following JSON, '{"Error":"Resource not found"}' // The standard plain text net/http NotFound helper can still be called like this: // http.NotFound(w, r.Request) func NotFound(w ResponseWriter, r *Request) { Error(w, "Resource not found", http.StatusNotFound) } // Private responseWriter intantiated by the resource handler. // It implements the following interfaces: // ResponseWriter // http.ResponseWriter // http.Flusher // http.CloseNotifier // http.Hijacker type responseWriter struct { http.ResponseWriter wroteHeader bool } func (w *responseWriter) WriteHeader(code int) { if w.Header().Get("Content-Type") == "" { // Per spec, UTF-8 is the default, and the charset parameter should not // be necessary. But some clients (eg: Chrome) think otherwise. // Since json.Marshal produces UTF-8, setting the charset parameter is a // safe option. w.Header().Set("Content-Type", "application/json; charset=utf-8") } w.ResponseWriter.WriteHeader(code) w.wroteHeader = true } func (w *responseWriter) EncodeJson(v interface{}) ([]byte, error) { b, err := json.Marshal(v) if err != nil { return nil, err } return b, nil } // Encode the object in JSON and call Write. func (w *responseWriter) WriteJson(v interface{}) error { b, err := w.EncodeJson(v) if err != nil { return err } _, err = w.Write(b) if err != nil { return err } return nil } // Provided in order to implement the http.ResponseWriter interface. func (w *responseWriter) Write(b []byte) (int, error) { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } return w.ResponseWriter.Write(b) } // Provided in order to implement the http.Flusher interface. func (w *responseWriter) Flush() { if !w.wroteHeader { w.WriteHeader(http.StatusOK) } flusher := w.ResponseWriter.(http.Flusher) flusher.Flush() } // Provided in order to implement the http.CloseNotifier interface. func (w *responseWriter) CloseNotify() <-chan bool { notifier := w.ResponseWriter.(http.CloseNotifier) return notifier.CloseNotify() } // Provided in order to implement the http.Hijacker interface. func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { hijacker := w.ResponseWriter.(http.Hijacker) return hijacker.Hijack() } go-json-rest-3.3.2/rest/response_test.go000066400000000000000000000032261301771661600202620ustar00rootroot00000000000000package rest import ( "testing" "github.com/ant0ine/go-json-rest/rest/test" ) func TestResponseNotIndent(t *testing.T) { writer := responseWriter{ nil, false, } got, err := writer.EncodeJson(map[string]bool{"test": true}) if err != nil { t.Error(err.Error()) } gotStr := string(got) expected := "{\"test\":true}" if gotStr != expected { t.Error(expected + " was the expected, but instead got " + gotStr) } } // The following tests could instantiate only the reponseWriter, // but using the Api object allows to use the rest/test utilities, // and make the tests easier to write. func TestWriteJsonResponse(t *testing.T) { api := NewApi() api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs("{\"Id\":\"123\"}") } func TestErrorResponse(t *testing.T) { api := NewApi() api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { Error(w, "test", 500) })) recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(500) recorded.ContentTypeIsJson() recorded.BodyIs("{\"Error\":\"test\"}") } func TestNotFoundResponse(t *testing.T) { api := NewApi() api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { NotFound(w, r) })) recorded := test.RunRequest(t, api.MakeHandler(), test.MakeSimpleRequest("GET", "http://localhost/", nil)) recorded.CodeIs(404) recorded.ContentTypeIsJson() recorded.BodyIs("{\"Error\":\"Resource not found\"}") } go-json-rest-3.3.2/rest/route.go000066400000000000000000000066431301771661600165310ustar00rootroot00000000000000package rest import ( "strings" ) // Route defines a route as consumed by the router. It can be instantiated directly, or using one // of the shortcut methods: rest.Get, rest.Post, rest.Put, rest.Patch and rest.Delete. type Route struct { // Any HTTP method. It will be used as uppercase to avoid common mistakes. HttpMethod string // A string like "/resource/:id.json". // Placeholders supported are: // :paramName that matches any char to the first '/' or '.' // #paramName that matches any char to the first '/' // *paramName that matches everything to the end of the string // (placeholder names must be unique per PathExp) PathExp string // Code that will be executed when this route is taken. Func HandlerFunc } // MakePath generates the path corresponding to this Route and the provided path parameters. // This is used for reverse route resolution. func (route *Route) MakePath(pathParams map[string]string) string { path := route.PathExp for paramName, paramValue := range pathParams { paramPlaceholder := ":" + paramName relaxedPlaceholder := "#" + paramName splatPlaceholder := "*" + paramName r := strings.NewReplacer(paramPlaceholder, paramValue, splatPlaceholder, paramValue, relaxedPlaceholder, paramValue) path = r.Replace(path) } return path } // Head is a shortcut method that instantiates a HEAD route. See the Route object the parameters definitions. // Equivalent to &Route{"HEAD", pathExp, handlerFunc} func Head(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "HEAD", PathExp: pathExp, Func: handlerFunc, } } // Get is a shortcut method that instantiates a GET route. See the Route object the parameters definitions. // Equivalent to &Route{"GET", pathExp, handlerFunc} func Get(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "GET", PathExp: pathExp, Func: handlerFunc, } } // Post is a shortcut method that instantiates a POST route. See the Route object the parameters definitions. // Equivalent to &Route{"POST", pathExp, handlerFunc} func Post(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "POST", PathExp: pathExp, Func: handlerFunc, } } // Put is a shortcut method that instantiates a PUT route. See the Route object the parameters definitions. // Equivalent to &Route{"PUT", pathExp, handlerFunc} func Put(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "PUT", PathExp: pathExp, Func: handlerFunc, } } // Patch is a shortcut method that instantiates a PATCH route. See the Route object the parameters definitions. // Equivalent to &Route{"PATCH", pathExp, handlerFunc} func Patch(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "PATCH", PathExp: pathExp, Func: handlerFunc, } } // Delete is a shortcut method that instantiates a DELETE route. Equivalent to &Route{"DELETE", pathExp, handlerFunc} func Delete(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "DELETE", PathExp: pathExp, Func: handlerFunc, } } // Options is a shortcut method that instantiates an OPTIONS route. See the Route object the parameters definitions. // Equivalent to &Route{"OPTIONS", pathExp, handlerFunc} func Options(pathExp string, handlerFunc HandlerFunc) *Route { return &Route{ HttpMethod: "OPTIONS", PathExp: pathExp, Func: handlerFunc, } } go-json-rest-3.3.2/rest/route_test.go000066400000000000000000000033141301771661600175600ustar00rootroot00000000000000package rest import ( "testing" ) func TestReverseRouteResolution(t *testing.T) { noParam := &Route{"GET", "/", nil} got := noParam.MakePath(nil) expected := "/" if got != expected { t.Errorf("expected %s, got %s", expected, got) } twoParams := &Route{"GET", "/:id.:format", nil} got = twoParams.MakePath( map[string]string{ "id": "123", "format": "json", }, ) expected = "/123.json" if got != expected { t.Errorf("expected %s, got %s", expected, got) } splatParam := &Route{"GET", "/:id.*format", nil} got = splatParam.MakePath( map[string]string{ "id": "123", "format": "tar.gz", }, ) expected = "/123.tar.gz" if got != expected { t.Errorf("expected %s, got %s", expected, got) } relaxedParam := &Route{"GET", "/#file", nil} got = relaxedParam.MakePath( map[string]string{ "file": "a.txt", }, ) expected = "/a.txt" if got != expected { t.Errorf("expected %s, got %s", expected, got) } } func TestShortcutMethods(t *testing.T) { r := Head("/", nil) if r.HttpMethod != "HEAD" { t.Errorf("expected HEAD, got %s", r.HttpMethod) } r = Get("/", nil) if r.HttpMethod != "GET" { t.Errorf("expected GET, got %s", r.HttpMethod) } r = Post("/", nil) if r.HttpMethod != "POST" { t.Errorf("expected POST, got %s", r.HttpMethod) } r = Put("/", nil) if r.HttpMethod != "PUT" { t.Errorf("expected PUT, got %s", r.HttpMethod) } r = Patch("/", nil) if r.HttpMethod != "PATCH" { t.Errorf("expected PATCH, got %s", r.HttpMethod) } r = Delete("/", nil) if r.HttpMethod != "DELETE" { t.Errorf("expected DELETE, got %s", r.HttpMethod) } r = Options("/", nil) if r.HttpMethod != "OPTIONS" { t.Errorf("expected OPTIONS, got %s", r.HttpMethod) } } go-json-rest-3.3.2/rest/router.go000066400000000000000000000113521301771661600167040ustar00rootroot00000000000000package rest import ( "errors" "github.com/ant0ine/go-json-rest/rest/trie" "net/http" "net/url" "strings" ) type router struct { Routes []*Route disableTrieCompression bool index map[*Route]int trie *trie.Trie } // MakeRouter returns the router app. Given a set of Routes, it dispatches the request to the // HandlerFunc of the first route that matches. The order of the Routes matters. func MakeRouter(routes ...*Route) (App, error) { r := &router{ Routes: routes, } err := r.start() if err != nil { return nil, err } return r, nil } // Handle the REST routing and run the user code. func (rt *router) AppFunc() HandlerFunc { return func(writer ResponseWriter, request *Request) { // find the route route, params, pathMatched := rt.findRouteFromURL(request.Method, request.URL) if route == nil { if pathMatched { // no route found, but path was matched: 405 Method Not Allowed Error(writer, "Method not allowed", http.StatusMethodNotAllowed) return } // no route found, the path was not matched: 404 Not Found NotFound(writer, request) return } // a route was found, set the PathParams request.PathParams = params // run the user code handler := route.Func handler(writer, request) } } // This is run for each new request, perf is important. func escapedPath(urlObj *url.URL) string { // the escape method of url.URL should be public // that would avoid this split. parts := strings.SplitN(urlObj.RequestURI(), "?", 2) return parts[0] } var preEscape = strings.NewReplacer("*", "__SPLAT_PLACEHOLDER__", "#", "__RELAXED_PLACEHOLDER__") var postEscape = strings.NewReplacer("__SPLAT_PLACEHOLDER__", "*", "__RELAXED_PLACEHOLDER__", "#") // This is run at init time only. func escapedPathExp(pathExp string) (string, error) { // PathExp validation if pathExp == "" { return "", errors.New("empty PathExp") } if pathExp[0] != '/' { return "", errors.New("PathExp must start with /") } if strings.Contains(pathExp, "?") { return "", errors.New("PathExp must not contain the query string") } // Get the right escaping // XXX a bit hacky pathExp = preEscape.Replace(pathExp) urlObj, err := url.Parse(pathExp) if err != nil { return "", err } // get the same escaping as find requests pathExp = urlObj.RequestURI() pathExp = postEscape.Replace(pathExp) return pathExp, nil } // This validates the Routes and prepares the Trie data structure. // It must be called once the Routes are defined and before trying to find Routes. // The order matters, if multiple Routes match, the first defined will be used. func (rt *router) start() error { rt.trie = trie.New() rt.index = map[*Route]int{} for i, route := range rt.Routes { // work with the PathExp urlencoded. pathExp, err := escapedPathExp(route.PathExp) if err != nil { return err } // insert in the Trie err = rt.trie.AddRoute( strings.ToUpper(route.HttpMethod), // work with the HttpMethod in uppercase pathExp, route, ) if err != nil { return err } // index rt.index[route] = i } if rt.disableTrieCompression == false { rt.trie.Compress() } return nil } // return the result that has the route defined the earliest func (rt *router) ofFirstDefinedRoute(matches []*trie.Match) *trie.Match { minIndex := -1 var bestMatch *trie.Match for _, result := range matches { route := result.Route.(*Route) routeIndex := rt.index[route] if minIndex == -1 || routeIndex < minIndex { minIndex = routeIndex bestMatch = result } } return bestMatch } // Return the first matching Route and the corresponding parameters for a given URL object. func (rt *router) findRouteFromURL(httpMethod string, urlObj *url.URL) (*Route, map[string]string, bool) { // lookup the routes in the Trie matches, pathMatched := rt.trie.FindRoutesAndPathMatched( strings.ToUpper(httpMethod), // work with the httpMethod in uppercase escapedPath(urlObj), // work with the path urlencoded ) // short cuts if len(matches) == 0 { // no route found return nil, nil, pathMatched } if len(matches) == 1 { // one route found return matches[0].Route.(*Route), matches[0].Params, pathMatched } // multiple routes found, pick the first defined result := rt.ofFirstDefinedRoute(matches) return result.Route.(*Route), result.Params, pathMatched } // Parse the url string (complete or just the path) and return the first matching Route and the corresponding parameters. func (rt *router) findRoute(httpMethod, urlStr string) (*Route, map[string]string, bool, error) { // parse the url urlObj, err := url.Parse(urlStr) if err != nil { return nil, nil, false, err } route, params, pathMatched := rt.findRouteFromURL(httpMethod, urlObj) return route, params, pathMatched, nil } go-json-rest-3.3.2/rest/router_benchmark_test.go000066400000000000000000000052601301771661600217560ustar00rootroot00000000000000package rest import ( "fmt" "net/url" "regexp" "testing" ) func routes() []*Route { // simulate the routes of a real but reasonable app. // 6 + 10 * (5 + 2) + 1 = 77 routes routePaths := []string{ "/", "/signin", "/signout", "/profile", "/settings", "/upload/*file", } for i := 0; i < 10; i++ { for j := 0; j < 5; j++ { routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id/property%d", i, j)) } routePaths = append(routePaths, fmt.Sprintf("/resource%d/:id", i)) routePaths = append(routePaths, fmt.Sprintf("/resource%d", i)) } routePaths = append(routePaths, "/*") routes := []*Route{} for _, path := range routePaths { routes = append(routes, &Route{ HttpMethod: "GET", PathExp: path, }) } return routes } func requestUrls() []*url.URL { // simulate a few requests urlStrs := []string{ "http://example.org/", "http://example.org/resource9/123", "http://example.org/resource9/123/property1", "http://example.org/doesnotexist", } urlObjs := []*url.URL{} for _, urlStr := range urlStrs { urlObj, _ := url.Parse(urlStr) urlObjs = append(urlObjs, urlObj) } return urlObjs } func BenchmarkNoCompression(b *testing.B) { b.StopTimer() r := router{ Routes: routes(), disableTrieCompression: true, } r.start() urlObjs := requestUrls() b.StartTimer() for i := 0; i < b.N; i++ { for _, urlObj := range urlObjs { r.findRouteFromURL("GET", urlObj) } } } func BenchmarkCompression(b *testing.B) { b.StopTimer() r := router{ Routes: routes(), } r.start() urlObjs := requestUrls() b.StartTimer() for i := 0; i < b.N; i++ { for _, urlObj := range urlObjs { r.findRouteFromURL("GET", urlObj) } } } func BenchmarkRegExpLoop(b *testing.B) { // reference benchmark using the usual RegExps + Loop strategy b.StopTimer() routes := routes() urlObjs := requestUrls() // build the route regexps r1, err := regexp.Compile(":[^/\\.]*") if err != nil { panic(err) } r2, err := regexp.Compile("\\*.*") if err != nil { panic(err) } routeRegexps := []*regexp.Regexp{} for _, route := range routes { // generate the regexp string regStr := r2.ReplaceAllString(route.PathExp, "([^/\\.]+)") regStr = r1.ReplaceAllString(regStr, "(.+)") regStr = "^" + regStr + "$" // compile it reg, err := regexp.Compile(regStr) if err != nil { panic(err) } routeRegexps = append(routeRegexps, reg) } b.StartTimer() for i := 0; i < b.N; i++ { // do it for a few urls for _, urlObj := range urlObjs { // stop at the first route that matches for index, reg := range routeRegexps { if reg.FindAllString(urlObj.Path, 1) != nil { _ = routes[index] break } } } } } go-json-rest-3.3.2/rest/router_test.go000066400000000000000000000177031301771661600177510ustar00rootroot00000000000000package rest import ( "net/url" "strings" "testing" "github.com/ant0ine/go-json-rest/rest/test" ) func TestFindRouteAPI(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/", }, }, } err := r.start() if err != nil { t.Fatal(err) } // full url string input := "http://example.org/" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route.PathExp != "/" { t.Error("Expected PathExp to be /") } if len(params) != 0 { t.Error("Expected 0 param") } if pathMatched != true { t.Error("Expected pathMatched to be true") } // part of the url string input = "/" route, params, pathMatched, err = r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route.PathExp != "/" { t.Error("Expected PathExp to be /") } if len(params) != 0 { t.Error("Expected 0 param") } if pathMatched != true { t.Error("Expected pathMatched to be true") } // url object urlObj, err := url.Parse("http://example.org/") if err != nil { t.Fatal(err) } route, params, pathMatched = r.findRouteFromURL("GET", urlObj) if route.PathExp != "/" { t.Error("Expected PathExp to be /") } if len(params) != 0 { t.Error("Expected 0 param") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestNoRoute(t *testing.T) { r := router{ Routes: []*Route{}, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/notfound" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route != nil { t.Error("should not be able to find a route") } if params != nil { t.Error("params must be nil too") } if pathMatched != false { t.Error("Expected pathMatched to be false") } } func TestEmptyPathExp(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "", }, }, } err := r.start() if err == nil || !strings.Contains(err.Error(), "empty") { t.Error("expected the empty PathExp error") } } func TestInvalidPathExp(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "invalid", }, }, } err := r.start() if err == nil || !strings.Contains(err.Error(), "/") { t.Error("expected the / PathExp error") } } func TestUrlEncodedFind(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/with space", // not urlencoded }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/with%20space" // urlencoded route, _, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route.PathExp != "/with space" { t.Error("Expected PathExp to be /with space") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestWithQueryString(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/r/:id", }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/r/123?arg=value" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route == nil { t.Fatal("Expected a match") } if params["id"] != "123" { t.Errorf("expected 123, got %s", params["id"]) } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestNonUrlEncodedFind(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/with%20space", // urlencoded }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/with space" // not urlencoded route, _, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route.PathExp != "/with%20space" { t.Errorf("Expected PathExp to be %s", "/with20space") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestDuplicatedRoute(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/", }, { HttpMethod: "GET", PathExp: "/", }, }, } err := r.start() if err == nil { t.Error("expected the duplicated route error") } } func TestSplatUrlEncoded(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/r/*rest", }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/r/123" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route == nil { t.Fatal("Expected a match") } if params["rest"] != "123" { t.Error("Expected rest to be 123") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestRouteOrder(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/r/:id", }, { HttpMethod: "GET", PathExp: "/r/*rest", }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/r/123" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route == nil { t.Fatal("Expected one route to be matched") } if route.PathExp != "/r/:id" { t.Errorf("both match, expected the first defined, got %s", route.PathExp) } if params["id"] != "123" { t.Error("Expected id to be 123") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestRelaxedPlaceholder(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/r/:id", }, { HttpMethod: "GET", PathExp: "/r/#filename", }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/r/a.txt" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route == nil { t.Fatal("Expected one route to be matched") } if route.PathExp != "/r/#filename" { t.Errorf("expected the second route, got %s", route.PathExp) } if params["filename"] != "a.txt" { t.Error("Expected filename to be a.txt") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestSimpleExample(t *testing.T) { r := router{ Routes: []*Route{ { HttpMethod: "GET", PathExp: "/resources/:id", }, { HttpMethod: "GET", PathExp: "/resources", }, }, } err := r.start() if err != nil { t.Fatal(err) } input := "http://example.org/resources/123" route, params, pathMatched, err := r.findRoute("GET", input) if err != nil { t.Fatal(err) } if route.PathExp != "/resources/:id" { t.Error("Expected PathExp to be /resources/:id") } if params["id"] != "123" { t.Error("Expected id to be 123") } if pathMatched != true { t.Error("Expected pathMatched to be true") } } func TestHttpResponseLayer(t *testing.T) { api := NewApi() router, err := MakeRouter( Get("/r/:id", func(w ResponseWriter, r *Request) { id := r.PathParam("id") w.WriteJson(map[string]string{"Id": id}) }), Post("/r/:id", func(w ResponseWriter, r *Request) { // JSON echo data := map[string]string{} err := r.DecodeJsonPayload(&data) if err != nil { t.Fatal(err) } w.WriteJson(data) }), ) if err != nil { t.Fatal(err) } api.SetApp(router) handler := api.MakeHandler() // valid get resource recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/r/123", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Id":"123"}`) // auto 405 on undefined route (wrong method) recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("DELETE", "http://1.2.3.4/r/123", nil)) recorded.CodeIs(405) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Error":"Method not allowed"}`) // auto 404 on undefined route (wrong path) recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://1.2.3.4/s/123", nil)) recorded.CodeIs(404) recorded.ContentTypeIsJson() recorded.BodyIs(`{"Error":"Resource not found"}`) } go-json-rest-3.3.2/rest/status.go000066400000000000000000000071141301771661600167100ustar00rootroot00000000000000package rest import ( "fmt" "log" "os" "sync" "time" ) // StatusMiddleware keeps track of various stats about the processed requests. // It depends on request.Env["STATUS_CODE"] and request.Env["ELAPSED_TIME"], // recorderMiddleware and timerMiddleware must be in the wrapped middlewares. type StatusMiddleware struct { lock sync.RWMutex start time.Time pid int responseCounts map[string]int totalResponseTime time.Time } // MiddlewareFunc makes StatusMiddleware implement the Middleware interface. func (mw *StatusMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { mw.start = time.Now() mw.pid = os.Getpid() mw.responseCounts = map[string]int{} mw.totalResponseTime = time.Time{} return func(w ResponseWriter, r *Request) { // call the handler h(w, r) if r.Env["STATUS_CODE"] == nil { log.Fatal("StatusMiddleware: Env[\"STATUS_CODE\"] is nil, " + "RecorderMiddleware may not be in the wrapped Middlewares.") } statusCode := r.Env["STATUS_CODE"].(int) if r.Env["ELAPSED_TIME"] == nil { log.Fatal("StatusMiddleware: Env[\"ELAPSED_TIME\"] is nil, " + "TimerMiddleware may not be in the wrapped Middlewares.") } responseTime := r.Env["ELAPSED_TIME"].(*time.Duration) mw.lock.Lock() mw.responseCounts[fmt.Sprintf("%d", statusCode)]++ mw.totalResponseTime = mw.totalResponseTime.Add(*responseTime) mw.lock.Unlock() } } // Status contains stats and status information. It is returned by GetStatus. // These information can be made available as an API endpoint, see the "status" // example to install the following status route. // GET /.status returns something like: // // { // "Pid": 21732, // "UpTime": "1m15.926272s", // "UpTimeSec": 75.926272, // "Time": "2013-03-04 08:00:27.152986 +0000 UTC", // "TimeUnix": 1362384027, // "StatusCodeCount": { // "200": 53, // "404": 11 // }, // "TotalCount": 64, // "TotalResponseTime": "16.777ms", // "TotalResponseTimeSec": 0.016777, // "AverageResponseTime": "262.14us", // "AverageResponseTimeSec": 0.00026214 // } type Status struct { Pid int UpTime string UpTimeSec float64 Time string TimeUnix int64 StatusCodeCount map[string]int TotalCount int TotalResponseTime string TotalResponseTimeSec float64 AverageResponseTime string AverageResponseTimeSec float64 } // GetStatus computes and returns a Status object based on the request informations accumulated // since the start of the process. func (mw *StatusMiddleware) GetStatus() *Status { mw.lock.RLock() now := time.Now() uptime := now.Sub(mw.start) totalCount := 0 for _, count := range mw.responseCounts { totalCount += count } totalResponseTime := mw.totalResponseTime.Sub(time.Time{}) averageResponseTime := time.Duration(0) if totalCount > 0 { avgNs := int64(totalResponseTime) / int64(totalCount) averageResponseTime = time.Duration(avgNs) } status := &Status{ Pid: mw.pid, UpTime: uptime.String(), UpTimeSec: uptime.Seconds(), Time: now.String(), TimeUnix: now.Unix(), StatusCodeCount: mw.responseCounts, TotalCount: totalCount, TotalResponseTime: totalResponseTime.String(), TotalResponseTimeSec: totalResponseTime.Seconds(), AverageResponseTime: averageResponseTime.String(), AverageResponseTimeSec: averageResponseTime.Seconds(), } mw.lock.RUnlock() return status } go-json-rest-3.3.2/rest/status_test.go000066400000000000000000000024511301771661600177460ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" ) func TestStatusMiddleware(t *testing.T) { api := NewApi() // the middlewares status := &StatusMiddleware{} api.Use(status) api.Use(&TimerMiddleware{}) api.Use(&RecorderMiddleware{}) // an app that return the Status api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(status.GetStatus()) })) // wrap all handler := api.MakeHandler() // one request recorded := test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/1", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() // another request recorded = test.RunRequest(t, handler, test.MakeSimpleRequest("GET", "http://localhost/2", nil)) recorded.CodeIs(200) recorded.ContentTypeIsJson() // payload payload := map[string]interface{}{} err := recorded.DecodeJsonPayload(&payload) if err != nil { t.Fatal(err) } if payload["Pid"] == nil { t.Error("Expected a non nil Pid") } if payload["TotalCount"].(float64) != 1 { t.Errorf("TotalCount 1 Expected, got: %f", payload["TotalCount"].(float64)) } if payload["StatusCodeCount"].(map[string]interface{})["200"].(float64) != 1 { t.Errorf("StatusCodeCount 200 1 Expected, got: %f", payload["StatusCodeCount"].(map[string]interface{})["200"].(float64)) } } go-json-rest-3.3.2/rest/test/000077500000000000000000000000001301771661600160125ustar00rootroot00000000000000go-json-rest-3.3.2/rest/test/doc.go000066400000000000000000000024341301771661600171110ustar00rootroot00000000000000// Utility functions to help writing tests for a Go-Json-Rest app // // Go comes with net/http/httptest to help writing test for an http // server. When this http server implements a JSON REST API, some basic // checks end up to be always the same. This test package tries to save // some typing by providing helpers for this particular use case. // // package main // // import ( // "github.com/ant0ine/go-json-rest/rest" // "github.com/ant0ine/go-json-rest/rest/test" // "testing" // ) // // func TestSimpleRequest(t *testing.T) { // api := rest.NewApi() // api.Use(rest.DefaultDevStack...) // router, err := rest.MakeRouter( // rest.Get("/r", func(w rest.ResponseWriter, r *rest.Request) { // w.WriteJson(map[string]string{"Id": "123"}) // }), // ) // if err != nil { // log.Fatal(err) // } // api.SetApp(router) // recorded := test.RunRequest(t, api.MakeHandler(), // test.MakeSimpleRequest("GET", "http://1.2.3.4/r", nil)) // recorded.CodeIs(200) // recorded.ContentTypeIsJson() // } package test go-json-rest-3.3.2/rest/test/util.go000066400000000000000000000062211301771661600173170ustar00rootroot00000000000000package test import ( "encoding/json" "fmt" "io/ioutil" "mime" "net/http" "net/http/httptest" "strings" "testing" ) // MakeSimpleRequest returns a http.Request. The returned request object can be // further prepared by adding headers and query string parmaters, for instance. func MakeSimpleRequest(method string, urlStr string, payload interface{}) *http.Request { var s string if payload != nil { b, err := json.Marshal(payload) if err != nil { panic(err) } s = fmt.Sprintf("%s", b) } r, err := http.NewRequest(method, urlStr, strings.NewReader(s)) if err != nil { panic(err) } r.Header.Set("Accept-Encoding", "gzip") if payload != nil { r.Header.Set("Content-Type", "application/json") } return r } // CodeIs compares the rescorded status code func CodeIs(t *testing.T, r *httptest.ResponseRecorder, expectedCode int) { if r.Code != expectedCode { t.Errorf("Code %d expected, got: %d", expectedCode, r.Code) } } // HeaderIs tests the first value for the given headerKey func HeaderIs(t *testing.T, r *httptest.ResponseRecorder, headerKey, expectedValue string) { value := r.HeaderMap.Get(headerKey) if value != expectedValue { t.Errorf( "%s: %s expected, got: %s", headerKey, expectedValue, value, ) } } func ContentTypeIsJson(t *testing.T, r *httptest.ResponseRecorder) { mediaType, params, _ := mime.ParseMediaType(r.HeaderMap.Get("Content-Type")) charset := params["charset"] if mediaType != "application/json" { t.Errorf( "Content-Type media type: application/json expected, got: %s", mediaType, ) } if charset != "" && strings.ToUpper(charset) != "UTF-8" { t.Errorf( "Content-Type charset: must be empty or UTF-8, got: %s", charset, ) } } func ContentEncodingIsGzip(t *testing.T, r *httptest.ResponseRecorder) { HeaderIs(t, r, "Content-Encoding", "gzip") } func BodyIs(t *testing.T, r *httptest.ResponseRecorder, expectedBody string) { body := r.Body.String() if body != expectedBody { t.Errorf("Body '%s' expected, got: '%s'", expectedBody, body) } } func DecodeJsonPayload(r *httptest.ResponseRecorder, v interface{}) error { content, err := ioutil.ReadAll(r.Body) if err != nil { return err } err = json.Unmarshal(content, v) if err != nil { return err } return nil } type Recorded struct { T *testing.T Recorder *httptest.ResponseRecorder } // RunRequest runs a HTTP request through the given handler func RunRequest(t *testing.T, handler http.Handler, request *http.Request) *Recorded { recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) return &Recorded{t, recorder} } func (rd *Recorded) CodeIs(expectedCode int) { CodeIs(rd.T, rd.Recorder, expectedCode) } func (rd *Recorded) HeaderIs(headerKey, expectedValue string) { HeaderIs(rd.T, rd.Recorder, headerKey, expectedValue) } func (rd *Recorded) ContentTypeIsJson() { ContentTypeIsJson(rd.T, rd.Recorder) } func (rd *Recorded) ContentEncodingIsGzip() { rd.HeaderIs("Content-Encoding", "gzip") } func (rd *Recorded) BodyIs(expectedBody string) { BodyIs(rd.T, rd.Recorder, expectedBody) } func (rd *Recorded) DecodeJsonPayload(v interface{}) error { return DecodeJsonPayload(rd.Recorder, v) } go-json-rest-3.3.2/rest/timer.go000066400000000000000000000012471301771661600165060ustar00rootroot00000000000000package rest import ( "time" ) // TimerMiddleware computes the elapsed time spent during the execution of the wrapped handler. // The result is available to the wrapping handlers as request.Env["ELAPSED_TIME"].(*time.Duration), // and as request.Env["START_TIME"].(*time.Time) type TimerMiddleware struct{} // MiddlewareFunc makes TimerMiddleware implement the Middleware interface. func (mw *TimerMiddleware) MiddlewareFunc(h HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { start := time.Now() r.Env["START_TIME"] = &start // call the handler h(w, r) end := time.Now() elapsed := end.Sub(start) r.Env["ELAPSED_TIME"] = &elapsed } } go-json-rest-3.3.2/rest/timer_test.go000066400000000000000000000023631301771661600175450ustar00rootroot00000000000000package rest import ( "github.com/ant0ine/go-json-rest/rest/test" "testing" "time" ) func TestTimerMiddleware(t *testing.T) { api := NewApi() // a middleware carrying the Env tests api.Use(MiddlewareSimple(func(handler HandlerFunc) HandlerFunc { return func(w ResponseWriter, r *Request) { handler(w, r) if r.Env["ELAPSED_TIME"] == nil { t.Error("ELAPSED_TIME is nil") } elapsedTime := r.Env["ELAPSED_TIME"].(*time.Duration) if elapsedTime.Nanoseconds() <= 0 { t.Errorf( "ELAPSED_TIME is expected to be at least 1 nanosecond %d", elapsedTime.Nanoseconds(), ) } if r.Env["START_TIME"] == nil { t.Error("START_TIME is nil") } start := r.Env["START_TIME"].(*time.Time) if start.After(time.Now()) { t.Errorf( "START_TIME is expected to be in the past %s", start.String(), ) } } })) // the middleware to test api.Use(&TimerMiddleware{}) // a simple app api.SetApp(AppSimple(func(w ResponseWriter, r *Request) { w.WriteJson(map[string]string{"Id": "123"}) })) // wrap all handler := api.MakeHandler() req := test.MakeSimpleRequest("GET", "http://localhost/", nil) recorded := test.RunRequest(t, handler, req) recorded.CodeIs(200) recorded.ContentTypeIsJson() } go-json-rest-3.3.2/rest/trie/000077500000000000000000000000001301771661600157765ustar00rootroot00000000000000go-json-rest-3.3.2/rest/trie/impl.go000066400000000000000000000226251301771661600172750ustar00rootroot00000000000000// Special Trie implementation for HTTP routing. // // This Trie implementation is designed to support strings that includes // :param and *splat parameters. Strings that are commonly used to represent // the Path in HTTP routing. This implementation also maintain for each Path // a map of HTTP Methods associated with the Route. // // You probably don't need to use this package directly. // package trie import ( "errors" "fmt" ) func splitParam(remaining string) (string, string) { i := 0 for len(remaining) > i && remaining[i] != '/' && remaining[i] != '.' { i++ } return remaining[:i], remaining[i:] } func splitRelaxed(remaining string) (string, string) { i := 0 for len(remaining) > i && remaining[i] != '/' { i++ } return remaining[:i], remaining[i:] } type node struct { HttpMethodToRoute map[string]interface{} Children map[string]*node ChildrenKeyLen int ParamChild *node ParamName string RelaxedChild *node RelaxedName string SplatChild *node SplatName string } func (n *node) addRoute(httpMethod, pathExp string, route interface{}, usedParams []string) error { if len(pathExp) == 0 { // end of the path, leaf node, update the map if n.HttpMethodToRoute == nil { n.HttpMethodToRoute = map[string]interface{}{ httpMethod: route, } return nil } else { if n.HttpMethodToRoute[httpMethod] != nil { return errors.New("node.Route already set, duplicated path and method") } n.HttpMethodToRoute[httpMethod] = route return nil } } token := pathExp[0:1] remaining := pathExp[1:] var nextNode *node if token[0] == ':' { // :param case var name string name, remaining = splitParam(remaining) // Check param name is unique for _, e := range usedParams { if e == name { return errors.New( fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), ) } } usedParams = append(usedParams, name) if n.ParamChild == nil { n.ParamChild = &node{} n.ParamName = name } else { if n.ParamName != name { return errors.New( fmt.Sprintf( "Routes sharing a common placeholder MUST name it consistently: %s != %s", n.ParamName, name, ), ) } } nextNode = n.ParamChild } else if token[0] == '#' { // #param case var name string name, remaining = splitRelaxed(remaining) // Check param name is unique for _, e := range usedParams { if e == name { return errors.New( fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), ) } } usedParams = append(usedParams, name) if n.RelaxedChild == nil { n.RelaxedChild = &node{} n.RelaxedName = name } else { if n.RelaxedName != name { return errors.New( fmt.Sprintf( "Routes sharing a common placeholder MUST name it consistently: %s != %s", n.RelaxedName, name, ), ) } } nextNode = n.RelaxedChild } else if token[0] == '*' { // *splat case name := remaining remaining = "" // Check param name is unique for _, e := range usedParams { if e == name { return errors.New( fmt.Sprintf("A route can't have two placeholders with the same name: %s", name), ) } } if n.SplatChild == nil { n.SplatChild = &node{} n.SplatName = name } nextNode = n.SplatChild } else { // general case if n.Children == nil { n.Children = map[string]*node{} n.ChildrenKeyLen = 1 } if n.Children[token] == nil { n.Children[token] = &node{} } nextNode = n.Children[token] } return nextNode.addRoute(httpMethod, remaining, route, usedParams) } func (n *node) compress() { // *splat branch if n.SplatChild != nil { n.SplatChild.compress() } // :param branch if n.ParamChild != nil { n.ParamChild.compress() } // #param branch if n.RelaxedChild != nil { n.RelaxedChild.compress() } // main branch if len(n.Children) == 0 { return } // compressable ? canCompress := true for _, node := range n.Children { if node.HttpMethodToRoute != nil || node.SplatChild != nil || node.ParamChild != nil || node.RelaxedChild != nil { canCompress = false } } // compress if canCompress { merged := map[string]*node{} for key, node := range n.Children { for gdKey, gdNode := range node.Children { mergedKey := key + gdKey merged[mergedKey] = gdNode } } n.Children = merged n.ChildrenKeyLen++ n.compress() // continue } else { for _, node := range n.Children { node.compress() } } } func printFPadding(padding int, format string, a ...interface{}) { for i := 0; i < padding; i++ { fmt.Print(" ") } fmt.Printf(format, a...) } // Private function for now func (n *node) printDebug(level int) { level++ // *splat branch if n.SplatChild != nil { printFPadding(level, "*splat\n") n.SplatChild.printDebug(level) } // :param branch if n.ParamChild != nil { printFPadding(level, ":param\n") n.ParamChild.printDebug(level) } // #param branch if n.RelaxedChild != nil { printFPadding(level, "#relaxed\n") n.RelaxedChild.printDebug(level) } // main branch for key, node := range n.Children { printFPadding(level, "\"%s\"\n", key) node.printDebug(level) } } // utility for the node.findRoutes recursive method type paramMatch struct { name string value string } type findContext struct { paramStack []paramMatch matchFunc func(httpMethod, path string, node *node) } func newFindContext() *findContext { return &findContext{ paramStack: []paramMatch{}, } } func (fc *findContext) pushParams(name, value string) { fc.paramStack = append( fc.paramStack, paramMatch{name, value}, ) } func (fc *findContext) popParams() { fc.paramStack = fc.paramStack[:len(fc.paramStack)-1] } func (fc *findContext) paramsAsMap() map[string]string { r := map[string]string{} for _, param := range fc.paramStack { if r[param.name] != "" { // this is checked at addRoute time, and should never happen. panic(fmt.Sprintf( "placeholder %s already found, placeholder names should be unique per route", param.name, )) } r[param.name] = param.value } return r } type Match struct { // Same Route as in AddRoute Route interface{} // map of params matched for this result Params map[string]string } func (n *node) find(httpMethod, path string, context *findContext) { if n.HttpMethodToRoute != nil && path == "" { context.matchFunc(httpMethod, path, n) } if len(path) == 0 { return } // *splat branch if n.SplatChild != nil { context.pushParams(n.SplatName, path) n.SplatChild.find(httpMethod, "", context) context.popParams() } // :param branch if n.ParamChild != nil { value, remaining := splitParam(path) context.pushParams(n.ParamName, value) n.ParamChild.find(httpMethod, remaining, context) context.popParams() } // #param branch if n.RelaxedChild != nil { value, remaining := splitRelaxed(path) context.pushParams(n.RelaxedName, value) n.RelaxedChild.find(httpMethod, remaining, context) context.popParams() } // main branch length := n.ChildrenKeyLen if len(path) < length { return } token := path[0:length] remaining := path[length:] if n.Children[token] != nil { n.Children[token].find(httpMethod, remaining, context) } } type Trie struct { root *node } // Instanciate a Trie with an empty node as the root. func New() *Trie { return &Trie{ root: &node{}, } } // Insert the route in the Trie following or creating the nodes corresponding to the path. func (t *Trie) AddRoute(httpMethod, pathExp string, route interface{}) error { return t.root.addRoute(httpMethod, pathExp, route, []string{}) } // Reduce the size of the tree, must be done after the last AddRoute. func (t *Trie) Compress() { t.root.compress() } // Private function for now. func (t *Trie) printDebug() { fmt.Print("\n") t.root.printDebug(0) fmt.Print("\n") } // Given a path and an http method, return all the matching routes. func (t *Trie) FindRoutes(httpMethod, path string) []*Match { context := newFindContext() matches := []*Match{} context.matchFunc = func(httpMethod, path string, node *node) { if node.HttpMethodToRoute[httpMethod] != nil { // path and method match, found a route ! matches = append( matches, &Match{ Route: node.HttpMethodToRoute[httpMethod], Params: context.paramsAsMap(), }, ) } } t.root.find(httpMethod, path, context) return matches } // Same as FindRoutes, but return in addition a boolean indicating if the path was matched. // Useful to return 405 func (t *Trie) FindRoutesAndPathMatched(httpMethod, path string) ([]*Match, bool) { context := newFindContext() pathMatched := false matches := []*Match{} context.matchFunc = func(httpMethod, path string, node *node) { pathMatched = true if node.HttpMethodToRoute[httpMethod] != nil { // path and method match, found a route ! matches = append( matches, &Match{ Route: node.HttpMethodToRoute[httpMethod], Params: context.paramsAsMap(), }, ) } } t.root.find(httpMethod, path, context) return matches, pathMatched } // Given a path, and whatever the http method, return all the matching routes. func (t *Trie) FindRoutesForPath(path string) []*Match { context := newFindContext() matches := []*Match{} context.matchFunc = func(httpMethod, path string, node *node) { params := context.paramsAsMap() for _, route := range node.HttpMethodToRoute { matches = append( matches, &Match{ Route: route, Params: params, }, ) } } t.root.find("", path, context) return matches } go-json-rest-3.3.2/rest/trie/impl_test.go000066400000000000000000000156571301771661600203430ustar00rootroot00000000000000package trie import ( "testing" ) func TestPathInsert(t *testing.T) { trie := New() if trie.root == nil { t.Error("Expected to not be nil") } trie.AddRoute("GET", "/", "1") if trie.root.Children["/"] == nil { t.Error("Expected to not be nil") } trie.AddRoute("GET", "/r", "2") if trie.root.Children["/"].Children["r"] == nil { t.Error("Expected to not be nil") } trie.AddRoute("GET", "/r/", "3") if trie.root.Children["/"].Children["r"].Children["/"] == nil { t.Error("Expected to not be nil") } } func TestTrieCompression(t *testing.T) { trie := New() trie.AddRoute("GET", "/abc", "3") trie.AddRoute("GET", "/adc", "3") // before compression if trie.root.Children["/"].Children["a"].Children["b"].Children["c"] == nil { t.Error("Expected to not be nil") } if trie.root.Children["/"].Children["a"].Children["d"].Children["c"] == nil { t.Error("Expected to not be nil") } trie.Compress() // after compression if trie.root.Children["/abc"] == nil { t.Errorf("%+v", trie.root) } if trie.root.Children["/adc"] == nil { t.Errorf("%+v", trie.root) } } func TestParamInsert(t *testing.T) { trie := New() trie.AddRoute("GET", "/:id/", "") if trie.root.Children["/"].ParamChild.Children["/"] == nil { t.Error("Expected to not be nil") } if trie.root.Children["/"].ParamName != "id" { t.Error("Expected ParamName to be id") } trie.AddRoute("GET", "/:id/:property.:format", "") if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamChild == nil { t.Error("Expected to not be nil") } if trie.root.Children["/"].ParamName != "id" { t.Error("Expected ParamName to be id") } if trie.root.Children["/"].ParamChild.Children["/"].ParamName != "property" { t.Error("Expected ParamName to be property") } if trie.root.Children["/"].ParamChild.Children["/"].ParamChild.Children["."].ParamName != "format" { t.Error("Expected ParamName to be format") } } func TestRelaxedInsert(t *testing.T) { trie := New() trie.AddRoute("GET", "/#id/", "") if trie.root.Children["/"].RelaxedChild.Children["/"] == nil { t.Error("Expected to not be nil") } if trie.root.Children["/"].RelaxedName != "id" { t.Error("Expected RelaxedName to be id") } } func TestSplatInsert(t *testing.T) { trie := New() trie.AddRoute("GET", "/*splat", "") if trie.root.Children["/"].SplatChild == nil { t.Error("Expected to not be nil") } } func TestDupeInsert(t *testing.T) { trie := New() trie.AddRoute("GET", "/", "1") err := trie.AddRoute("GET", "/", "2") if err == nil { t.Error("Expected to not be nil") } if trie.root.Children["/"].HttpMethodToRoute["GET"] != "1" { t.Error("Expected to be 1") } } func isInMatches(test string, matches []*Match) bool { for _, match := range matches { if match.Route.(string) == test { return true } } return false } func TestFindRoute(t *testing.T) { trie := New() trie.AddRoute("GET", "/", "root") trie.AddRoute("GET", "/r/:id", "resource") trie.AddRoute("GET", "/r/:id/property", "property") trie.AddRoute("GET", "/r/:id/property.*format", "property_format") trie.AddRoute("GET", "/user/#username/property", "user_property") trie.Compress() matches := trie.FindRoutes("GET", "/") if len(matches) != 1 { t.Errorf("expected one route, got %d", len(matches)) } if !isInMatches("root", matches) { t.Error("expected 'root'") } matches = trie.FindRoutes("GET", "/notfound") if len(matches) != 0 { t.Errorf("expected zero route, got %d", len(matches)) } matches = trie.FindRoutes("GET", "/r/1") if len(matches) != 1 { t.Errorf("expected one route, got %d", len(matches)) } if !isInMatches("resource", matches) { t.Errorf("expected 'resource', got %+v", matches) } if matches[0].Params["id"] != "1" { t.Error("Expected Params id to be 1") } matches = trie.FindRoutes("GET", "/r/1/property") if len(matches) != 1 { t.Errorf("expected one route, got %d", len(matches)) } if !isInMatches("property", matches) { t.Error("expected 'property'") } if matches[0].Params["id"] != "1" { t.Error("Expected Params id to be 1") } matches = trie.FindRoutes("GET", "/r/1/property.json") if len(matches) != 1 { t.Errorf("expected one route, got %d", len(matches)) } if !isInMatches("property_format", matches) { t.Error("expected 'property_format'") } if matches[0].Params["id"] != "1" { t.Error("Expected Params id to be 1") } if matches[0].Params["format"] != "json" { t.Error("Expected Params format to be json") } matches = trie.FindRoutes("GET", "/user/antoine.imbert/property") if len(matches) != 1 { t.Errorf("expected one route, got %d", len(matches)) } if !isInMatches("user_property", matches) { t.Error("expected 'user_property'") } if matches[0].Params["username"] != "antoine.imbert" { t.Error("Expected Params username to be antoine.imbert") } } func TestFindRouteMultipleMatches(t *testing.T) { trie := New() trie.AddRoute("GET", "/r/1", "resource1") trie.AddRoute("GET", "/r/2", "resource2") trie.AddRoute("GET", "/r/:id", "resource_generic") trie.AddRoute("GET", "/s/*rest", "special_all") trie.AddRoute("GET", "/s/:param", "special_generic") trie.AddRoute("GET", "/s/#param", "special_relaxed") trie.AddRoute("GET", "/", "root") trie.Compress() matches := trie.FindRoutes("GET", "/r/1") if len(matches) != 2 { t.Errorf("expected two matches, got %d", len(matches)) } if !isInMatches("resource_generic", matches) { t.Error("Expected resource_generic to match") } if !isInMatches("resource1", matches) { t.Error("Expected resource1 to match") } matches = trie.FindRoutes("GET", "/s/1") if len(matches) != 3 { t.Errorf("expected two matches, got %d", len(matches)) } if !isInMatches("special_all", matches) { t.Error("Expected special_all to match") } if !isInMatches("special_generic", matches) { t.Error("Expected special_generic to match") } if !isInMatches("special_relaxed", matches) { t.Error("Expected special_relaxed to match") } } func TestConsistentPlaceholderName(t *testing.T) { trie := New() trie.AddRoute("GET", "/r/:id", "oneph") err := trie.AddRoute("GET", "/r/:rid/other", "twoph") if err == nil { t.Error("Should have died on inconsistent placeholder name") } trie.AddRoute("GET", "/r/#id", "oneph") err = trie.AddRoute("GET", "/r/#rid/other", "twoph") if err == nil { t.Error("Should have died on inconsistent placeholder name") } trie.AddRoute("GET", "/r/*id", "oneph") err = trie.AddRoute("GET", "/r/*rid", "twoph") if err == nil { t.Error("Should have died on duplicated route") } } func TestDuplicateName(t *testing.T) { trie := New() err := trie.AddRoute("GET", "/r/:id/o/:id", "two") if err == nil { t.Error("Should have died, this route has two placeholder named `id`") } err = trie.AddRoute("GET", "/r/:id/o/*id", "two") if err == nil { t.Error("Should have died, this route has two placeholder named `id`") } err = trie.AddRoute("GET", "/r/:id/o/#id", "two") if err == nil { t.Error("Should have died, this route has two placeholder named `id`") } }