Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge branch 'develop'
  • Loading branch information
paulbellamy committed Jun 6, 2011
2 parents 1fc45b2 + 7161fa8 commit d6e78c7
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 11 deletions.
3 changes: 3 additions & 0 deletions Makefile
Expand Up @@ -9,10 +9,13 @@ GOFILES=\
show_errors.go\
sessions.go\
routing.go\
static.go\
jsonp.go\

include $(GOROOT)/src/Make.pkg

format:
${GOFMT} -w ${GOFILES}
${GOFMT} -w mango_test.go
${GOFMT} -w jsonp_test.go
${GOFMT} -w examples/*.go
72 changes: 66 additions & 6 deletions README.markdown
Expand Up @@ -31,34 +31,55 @@ Where:
* mango.Headers is a map[string][]string of the response headers (similar to http.Header)
* mango.Body is a string for the response body

## Installation

$ goinstall github.com/paulbellamy/mango


## Available Modules

* Sessions

Usage: Sessions(app_secret, cookie_name, cookie_domain string)
Usage: mango.Sessions(app_secret, cookie_name, cookie_domain string)

Basic session management. Provides a mango.Env.Session() helper which returns a map[string]interface{} representing the session. Any data stored in here will be serialized into the response session cookie.

* Logger

Usage: Logger(custom_logger \*log.Logger)
Usage: mango.Logger(custom_logger \*log.Logger)

Provides a way to set a custom log.Logger object for the app. If this middleware is not provided Mango will set up a default logger to os.Stdout for the app to log to.

* ShowErrors

Usage: ShowErrors(templateString string)
Usage: mango.ShowErrors(templateString string)

Catch any panics thrown from the app, and display them in an HTML template. If templateString is "", a default template is used. Not recommended to use the default template in production as it could provide information helpful to attackers.

* Routing

Usage: Routing(routes map[string]App)
Usage: mango.Routing(routes map[string]App)

"routes" is of the form { "/path1(.\*)": sub-stack1, "/path2(.\*)": sub-stack2 }. It lets us route different requests to different mango sub-stacks based on regexing the path.

* Static

Usage: mango.Static(directory string)

Serves static files from the directory provided.

* JSONP

Usage: mango.JSONP

Provides JSONP support. If a request has a 'callback' parameter, and your application responds with a Content-Type of "application/json", the JSONP middleware will wrap the response in the callback function and set the Content-Type to "application/javascript".

## Example App

package main

import (
"mango"
"github.com/paulbellamy/mango"
)

func Hello(env mango.Env) (mango.Status, mango.Headers, mango.Body) {
Expand All @@ -69,7 +90,7 @@ Where:
func main() {
stack := new(mango.Stack)
stack.Address = ":3000"
stack.Middleware(ShowErrors(""))
stack.Middleware(mango.ShowErrors(""))
stack.Run(Hello)
}

Expand Down Expand Up @@ -167,6 +188,45 @@ To use our middleware we would do:
stack.Run(Hello)
}

## Routing example

The following example routes "/hello" traffic to the hello handler and
"/bye" traffic to the bye handler, any other traffic goes to
routeNotFound handler returning a 404.

package main

import(
"github.com/paulbellamy/mango"
"fmt"
)

func hello(env mango.Env) (mango.Status, mango.Headers, mango.Body) {
env.Logger().Println("Got a", env.Request().Method, "request for", env.Request().RawURL)
return 200, mango.Headers{}, mango.Body("Hello World!")
}

func bye(env mango.Env) (mango.Status, mango.Headers, mango.Body) {
return 200, mango.Headers{}, mango.Body("Bye Bye!")
}

func routeNotFound(env mango.Env) (mango.Status, mango.Headers, mango.Body) {
return 404, mango.Headers{}, mango.Body("You probably got lost :(")
}

func main() {
routes := make(map[string]mango.App)
routes["/hello"] = new(mango.Stack).Compile(hello)
routes["/bye"] = new(mango.Stack).Compile(bye)

testServer := new(mango.Stack)
testServer.Middleware(mango.ShowErrors("<html><body>{Error|html}</body></html>"), mango.Routing(routes))
testServer.Address = "localhost:3000"
testServer.Run(routeNotFound)
fmt.Printf("Running server on: %s\n", testServer.Address)
}


## About

Mango was written by [Paul Bellamy](http://paulbellamy.com).
Expand Down
4 changes: 2 additions & 2 deletions TODO
Expand Up @@ -4,10 +4,10 @@
POTENTIAL MIDDLEWARE
====================

* Cross-Origin Resource Sharing
* Test module (to allow easy testing of apps)
* Lint for validation (Enforce http spec)
* Static-File/Directory serving
* JSONP support
* Caching support for drop-in HTTP Caching
* Edge-Side Include support
* Multi-part file upload handling
* provided via golang http package?
29 changes: 29 additions & 0 deletions jsonp.go
@@ -0,0 +1,29 @@
package mango

import (
"fmt"
"regexp"
"strings"
)

func JSONP(env Env, app App) (Status, Headers, Body) {
callback := env.Request().FormValue("callback")

if callback != "" {
if matched, err := regexp.MatchString("^[a-zA-Z_$][a-zA-Z_0-9$]*([.]?[a-zA-Z_$][a-zA-Z_0-9$]*)*$", callback); !matched || err != nil {
return 400, Headers{"Content-Type": []string{"text/plain"}, "Content-Length": []string{"11"}}, "Bad Request"
}
}

status, headers, body := app(env)

if callback != "" && strings.Contains(headers.Get("Content-Type"), "application/json") {
headers.Set("Content-Type", strings.Replace(headers.Get("Content-Type"), "json", "javascript", -1))
body = Body(fmt.Sprintf("%s(%s)", callback, body))
if headers.Get("Content-Length") != "" {
headers.Set("Content-Length", fmt.Sprintf("%d", len(body)))
}
}

return status, headers, body
}
150 changes: 150 additions & 0 deletions jsonp_test.go
@@ -0,0 +1,150 @@
package mango

import (
"http"
"testing"
"fmt"
"runtime"
)

func jsonServer(env Env) (Status, Headers, Body) {
return 200, Headers{"Content-Type": []string{"application/json"}, "Content-Length": []string{"13"}}, Body("{\"foo\":\"bar\"}")
}

func nonJsonServer(env Env) (Status, Headers, Body) {
return 200, Headers{"Content-Type": []string{"text/html"}}, Body("<h1>Hello World!</h1>")
}

func init() {
runtime.GOMAXPROCS(4)

fmt.Println("Testing Mango-JSONP Version:", VersionString())
}

func TestJSONPSuccess(t *testing.T) {
// Compile the stack
jsonpStack := new(Stack)
jsonpStack.Middleware(JSONP)
jsonpApp := jsonpStack.Compile(jsonServer)

// Request against it
request, err := http.NewRequest("GET", "http://localhost:3000/?callback=parseResponse", nil)
status, headers, body := jsonpApp(Env{"mango.request": &Request{request}})

if err != nil {
t.Error(err)
}

if status != 200 {
t.Error("Expected status to equal 200, got:", status)
}

if headers.Get("Content-Type") != "application/javascript" {
t.Error("Expected Content-Type to equal \"application/javascript\", got:", headers.Get("Content-Type"))
}

if headers.Get("Content-Length") != "28" {
t.Error("Expected Content-Length to equal \"28\", got:", headers.Get("Content-Length"))
}

expected := "parseResponse({\"foo\":\"bar\"})"
if string(body) != expected {
t.Error("Expected body:", string(body), "to equal:", expected)
}
}

func TestNonJSONPSuccess(t *testing.T) {
// Compile the stack
nonJsonpStack := new(Stack)
nonJsonpStack.Middleware(JSONP)
nonJsonpApp := nonJsonpStack.Compile(nonJsonServer)

// Request against it
request, err := http.NewRequest("GET", "http://localhost:3000/?callback=parseResponse", nil)
status, headers, body := nonJsonpApp(Env{"mango.request": &Request{request}})

if err != nil {
t.Error(err)
}

if status != 200 {
t.Error("Expected status to equal 200, got:", status)
}

if headers.Get("Content-Type") != "text/html" {
t.Error("Expected Content-Type to equal \"text/html\", got:", headers.Get("Content-Type"))
}

if headers.Get("Content-Length") != "" {
t.Error("Expected Content-Length to equal \"\", got:", headers.Get("Content-Length"))
}

expected := "<h1>Hello World!</h1>"
if string(body) != expected {
t.Error("Expected body:", string(body), "to equal:", expected)
}
}

func TestJSONPNoCallback(t *testing.T) {
// Compile the stack
jsonpStack := new(Stack)
jsonpStack.Middleware(JSONP)
jsonpApp := jsonpStack.Compile(jsonServer)

// Request against it
request, err := http.NewRequest("GET", "http://localhost:3000/", nil)
status, headers, body := jsonpApp(Env{"mango.request": &Request{request}})

if err != nil {
t.Error(err)
}

if status != 200 {
t.Error("Expected status to equal 200, got:", status)
}

if headers.Get("Content-Type") != "application/json" {
t.Error("Expected Content-Type to equal \"application/json\", got:", headers.Get("Content-Type"))
}

if headers.Get("Content-Length") != "13" {
t.Error("Expected Content-Length to equal \"13\", got:", headers.Get("Content-Length"))
}

expected := "{\"foo\":\"bar\"}"
if string(body) != expected {
t.Error("Expected body:", string(body), "to equal:", expected)
}
}

func TestJSONPInvalidCallback(t *testing.T) {
// Compile the stack
jsonpStack := new(Stack)
jsonpStack.Middleware(JSONP)
jsonpApp := jsonpStack.Compile(jsonServer)

// Request against it
request, err := http.NewRequest("GET", "http://localhost:3000/?callback=invalid(callback)", nil)
status, headers, body := jsonpApp(Env{"mango.request": &Request{request}})

if err != nil {
t.Error(err)
}

if status != 400 {
t.Error("Expected status to equal 400, got:", status)
}

if headers.Get("Content-Type") != "text/plain" {
t.Error("Expected Content-Type to equal \"text/plain\", got:", headers.Get("Content-Type"))
}

if headers.Get("Content-Length") != "11" {
t.Error("Expected Content-Length to equal \"11\", got:", headers.Get("Content-Length"))
}

expected := "Bad Request"
if string(body) != expected {
t.Error("Expected body:", string(body), "to equal:", expected)
}
}
2 changes: 1 addition & 1 deletion mango.go
Expand Up @@ -101,7 +101,7 @@ type Stack struct {
}

func Version() []int {
return []int{0, 2, 1}
return []int{0, 3, 0}
}

func VersionString() string {
Expand Down
29 changes: 27 additions & 2 deletions mango_test.go
Expand Up @@ -79,7 +79,7 @@ func init() {
Routing(fullStackTestRoutes))
testRoutes["/full_stack(.*)"] = fullStack.Compile(helloWorld)

testServer.Middleware(Routing(testRoutes))
testServer.Middleware(Static("./static"), Routing(testRoutes))
testServer.Address = "localhost:3000"
go testServer.Run(helloWorld)
}
Expand All @@ -98,7 +98,7 @@ func TestHelloWorld(t *testing.T) {

body, _ := ioutil.ReadAll(response.Body)
if string(body) != "Hello World!" {
t.Error("Expected body:", body, "to equal: \"Hello World!\"")
t.Error("Expected body:", string(body), "to equal: \"Hello World!\"")
}
}

Expand Down Expand Up @@ -234,6 +234,31 @@ func BenchmarkRouting(b *testing.B) {
}
}

func TestStatic(t *testing.T) {
// Request against it
response, _, err := client.Get("http://localhost:3000/static.html")

if err != nil {
t.Error(err)
}

if response.StatusCode != 200 {
t.Error("Expected status to equal 200, got:", response.StatusCode)
}

body, _ := ioutil.ReadAll(response.Body)
expected := "<h1>I'm a static test file</h1>\n"
if string(body) != expected {
t.Error("Expected body:", string(body), "to equal: \"", expected, "\"")
}
}

func BenchmarkStatic(b *testing.B) {
for i := 0; i < b.N; i++ {
client.Get("http://localhost:3000/static.html")
}
}

func BenchmarkFullStack(b *testing.B) {
for i := 0; i < b.N; i++ {
client.Get("http://localhost:3000/full_stack/123")
Expand Down

0 comments on commit d6e78c7

Please sign in to comment.