├── LICENSE ├── README.md ├── app.go ├── app_test.go ├── context.go ├── context_test.go ├── decorators.go ├── error.go ├── examples ├── api │ ├── README.md │ ├── controllers │ │ ├── error.go │ │ └── movie.go │ ├── main.go │ └── models │ │ └── movie.go ├── blog │ ├── README.md │ ├── controllers │ │ └── root_controller.go │ ├── main.go │ ├── models │ │ └── post.go │ ├── static │ │ ├── css │ │ │ ├── blog-old-ie.css │ │ │ └── blog.css │ │ └── img │ │ │ └── avatar.jpg │ └── views │ │ ├── index.html │ │ └── partials │ │ ├── _footer.html │ │ └── _head.html ├── hello_world │ ├── README.md │ └── main.go └── swagger │ ├── README.md │ ├── controllers │ ├── movie.go │ └── swagger.go │ ├── main.go │ ├── models │ └── movie.go │ └── static │ └── swagger-ui │ └── dist │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map ├── json.go ├── parser.go ├── parser_test.go ├── response.go ├── route.go ├── router.go ├── router_test.go ├── test_helpers.go ├── testing ├── index.html └── partials │ ├── _footer.html │ └── _header.html └── utilities.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016, Zack Patrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fireball 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/zpatrick/fireball/blob/master/LICENSE) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/zpatrick/fireball)](https://goreportcard.com/report/github.com/zpatrick/fireball) 5 | [![Go Doc](https://godoc.org/github.com/zpatrick/fireball?status.svg)](https://godoc.org/github.com/zpatrick/fireball) 6 | 7 | 8 | ## Overview 9 | Fireball is a package for Go web applications. 10 | The primary goal of this package is to make routing, response writing, and error handling as easy as possible for developers, so they can focus more on their application logic, and less on repeated patterns. 11 | 12 | ## Installation 13 | To install this package, run: 14 | ```bash 15 | go get github.com/zpatrick/fireball 16 | ``` 17 | 18 | ## Getting Started 19 | The following snipped shows a simple "Hello, World" application using Fireball: 20 | ```go 21 | package main 22 | 23 | import ( 24 | "github.com/zpatrick/fireball" 25 | "net/http" 26 | ) 27 | 28 | func index(c *fireball.Context) (fireball.Response, error) { 29 | return fireball.NewResponse(200, []byte("Hello, World!"), nil), nil 30 | } 31 | 32 | func main() { 33 | indexRoute := &fireball.Route{ 34 | Path: "/", 35 | Handlers: fireball.Handlers{ 36 | "GET": index, 37 | }, 38 | } 39 | 40 | routes := []*fireball.Route{indexRoute} 41 | app := fireball.NewApp(routes) 42 | http.ListenAndServe(":8000", app) 43 | } 44 | ``` 45 | 46 | This will run a new webserver at `localhost:8000` 47 | 48 | ## Handlers 49 | [Handlers](https://godoc.org/github.com/zpatrick/fireball#Handler) perform the business logic associated with requests. 50 | Handlers take a [Context](https://godoc.org/github.com/zpatrick/fireball#Context) object and returns either a [Response](https://godoc.org/github.com/zpatrick/fireball#Response) or an error. 51 | 52 | ### HTTP Response 53 | The [HTTP Response](https://godoc.org/github.com/zpatrick/fireball#HTTPResponse) is a simple object that implements the [Response](https://godoc.org/github.com/zpatrick/fireball#Response) interface. 54 | When the Write call is executed, the specified Body, Status, and Headers will be written to the http.ResponseWriter. 55 | 56 | Examples: 57 | ```go 58 | func Index(c *fireball.Context) (fireball.Response, error) { 59 | return fireball.NewResponse(200, []byte("Hello, World"), nil), nil 60 | } 61 | ``` 62 | 63 | ```go 64 | func Index(c *fireball.Context) (fireball.Response, error) { 65 | html := []byte("

Hello, World

") 66 | return fireball.NewResponse(200, html, fireball.HTMLHeaders), nil 67 | } 68 | ``` 69 | 70 | ### HTTP Error 71 | If a Handler returns a non-nil error, the Fireball Application will call its [ErrorHandler](https://godoc.org/github.com/zpatrick/fireball#App) function. 72 | By default (if your Application object uses the [DefaultErrorHandler](https://godoc.org/github.com/zpatrick/fireball#DefaultErrorHandler)), the Application will check if the error implements the [Response](https://godoc.org/github.com/zpatrick/fireball#Response) interface. 73 | If so, the the error's Write function will be called. 74 | Otherwise, a 500 with the content of err.Error() will be written. 75 | 76 | The [HTTPError](https://godoc.org/github.com/zpatrick/fireball#HTTPError) is a simple object that implements both the [Error](https://golang.org/pkg/builtin/#error) and [Response](https://godoc.org/github.com/zpatrick/fireball#Response) interfaces. 77 | When the Write is executed, the specified status, error, and headers will be written to the http.ResponseWriter. 78 | 79 | Examples: 80 | ```go 81 | func Index(c *fireball.Context) (fireball.Response, error) { 82 | return nil, fmt.Errorf("an error occurred") 83 | } 84 | ``` 85 | ```go 86 | func Index(c *fireball.Context) (fireball.Response, error) { 87 | if err := do(); err != nil { 88 | return nil, fireball.NewError(500, err, nil) 89 | } 90 | 91 | ... 92 | } 93 | ``` 94 | 95 | ## Routing 96 | 97 | ### Basic Router 98 | By default, Fireball uses the [BasicRouter](https://godoc.org/github.com/zpatrick/fireball#BasicRouter) object to match requests to [Route](https://godoc.org/github.com/zpatrick/fireball#Route) objects. 99 | The Route's Path field determines which URL patterns should be dispached to your Route. 100 | The Route's Handlers field maps different HTTP methods to different [Handlers](https://godoc.org/github.com/zpatrick/fireball#Handler). 101 | 102 | You can use `:variable` notation in the Path to match any string that doesn't contain a `"/"` character. 103 | The variables defined in the Route's Path field can be accessed using the [Context](https://godoc.org/github.com/zpatrick/fireball#Context) object. 104 | 105 | Example: 106 | ```go 107 | route := &Fireball.Route{ 108 | Path: "/users/:userID/orders/:orderID", 109 | Methods: fireball.Handlers{ 110 | "GET": printUserOrder, 111 | }, 112 | } 113 | 114 | func printUserOrder(c *fireball.Context) (fireball.Response, error) { 115 | userID := c.PathVariables["userID"] 116 | orderID := c.PathVariables["orderID"] 117 | message := fmt.Sprintf("User %s ordered item %s", userID, orderID) 118 | 119 | return fireball.NewResponse(200, []byte(message), nil) 120 | } 121 | ``` 122 | 123 | ### Static Routing 124 | The built-in [FileServer](https://golang.org/pkg/net/http/#FileServer) can be used to serve static content. 125 | The follow snippet would serve files from the `static` directory: 126 | ```go 127 | app := fireball.NewApp(...) 128 | http.Handle("/", app) 129 | 130 | fs := http.FileServer(http.Dir("static")) 131 | http.Handle("/static/", http.StripPrefix("/static", fs)) 132 | 133 | http.ListenAndServe(":8000", nil) 134 | ``` 135 | 136 | If the application workspace contained: 137 | ```go 138 | app/ 139 | main.go 140 | static/ 141 | hello_world.txt 142 | ``` 143 | 144 | A request to `/static/hello_world.txt` would serve the desired file. 145 | 146 | 147 | # HTML Templates 148 | By default, Fireball uses the [GlobParser](https://godoc.org/github.com/zpatrick/fireball#GlobParser) to render HTML templates. 149 | This object recursively searches a given directory for template files matching the given glob pattern. 150 | The default root directory is `"views"`, and the default glob pattern is `"*.html"` 151 | The name of the templates are `path/from/root/directory` + `filename`. 152 | 153 | For example, if the filesystem contained: 154 | ```go 155 | views/ 156 | index.html 157 | partials/ 158 | login.html 159 | ``` 160 | 161 | The templates names generated would be `"index.html"`, and `"partials/login.html"`. 162 | The [Context](https://godoc.org/github.com/zpatrick/fireball#Context) contains a helper function, [HTML](https://godoc.org/github.com/zpatrick/fireball#Context.HTML), which renders templates as HTML. 163 | 164 | Example: 165 | ```go 166 | func Index(c *fireball.Context) (fireball.Response, error) { 167 | data := "Hello, World!" 168 | return c.HTML(200, "index.html", data) 169 | } 170 | ``` 171 | 172 | # Decorators 173 | [Decorators](https://godoc.org/github.com/zpatrick/fireball#Decorator) can be used to wrap additional logic around [Handlers](https://godoc.org/github.com/zpatrick/fireball#Handler). 174 | Fireball has some built-in decorators: 175 | * [BasicAuthDecorator](https://godoc.org/github.com/zpatrick/fireball#BasicAuthDecorator) adds basic authentication using a specified username and password 176 | * [LogDecorator](https://godoc.org/github.com/zpatrick/fireball#LogDecorator) logs incoming requests 177 | 178 | In addition to Decorators, the [Before](https://godoc.org/github.com/zpatrick/fireball#App) and [After](https://godoc.org/github.com/zpatrick/fireball#App) functions on the [Application](https://godoc.org/github.com/zpatrick/fireball#App) object can be used to perform logic when the request is received and after the response has been sent. 179 | 180 | # Examples & Extras 181 | * [JSON](https://github.com/zpatrick/fireball/blob/master/examples/api/controllers/movie_controller.go#L49) 182 | * [Logging](https://github.com/zpatrick/fireball/tree/master/examples/blog/main.go#L15) 183 | * [Authentication](https://github.com/zpatrick/fireball/tree/master/examples/blog/main.go#L14) 184 | * [HTML Templates](https://github.com/zpatrick/fireball/blob/master/examples/blog/controllers/root_controller.go#L71) 185 | * [Redirect](https://godoc.org/github.com/zpatrick/fireball#Redirect) 186 | 187 | # License 188 | This work is published under the MIT license. 189 | 190 | Please see the `LICENSE` file for details. 191 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // App is the main structure of fireball applications. 8 | // It can be invoked as an http.Handler 9 | type App struct { 10 | // The After function is called after each request has completed 11 | After func(http.ResponseWriter, *http.Request) 12 | // The Before function is called before each request is routed 13 | Before func(http.ResponseWriter, *http.Request) 14 | // The ErrorHandler is called whenever a Handler returns a non-nil error 15 | ErrorHandler func(http.ResponseWriter, *http.Request, error) 16 | // The NotFoundHandler is called whenever the Router returns a nil RouteMatch 17 | NotFoundHandler func(http.ResponseWriter, *http.Request) 18 | // The template parser is passed into the Context 19 | Parser TemplateParser 20 | // The router is used to match a request to a Handler whenever a request is made 21 | Router Router 22 | } 23 | 24 | // NewApp returns a new App object with all of the default fields 25 | func NewApp(routes []*Route) *App { 26 | return &App{ 27 | After: func(http.ResponseWriter, *http.Request) {}, 28 | Before: func(http.ResponseWriter, *http.Request) {}, 29 | ErrorHandler: DefaultErrorHandler, 30 | NotFoundHandler: http.NotFound, 31 | Parser: NewGlobParser("views/", "*.html"), 32 | Router: NewBasicRouter(routes), 33 | } 34 | } 35 | 36 | func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { 37 | a.Before(w, r) 38 | defer a.After(w, r) 39 | 40 | match, err := a.Router.Match(r) 41 | if err != nil { 42 | a.ErrorHandler(w, r, err) 43 | return 44 | } 45 | 46 | if match == nil { 47 | a.NotFoundHandler(w, r) 48 | return 49 | } 50 | 51 | c := &Context{ 52 | PathVariables: match.PathVariables, 53 | Parser: a.Parser, 54 | Request: r, 55 | Meta: map[string]interface{}{}, 56 | } 57 | 58 | response, err := match.Handler(c) 59 | if err != nil { 60 | a.ErrorHandler(w, r, err) 61 | return 62 | } 63 | 64 | response.Write(w, r) 65 | } 66 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestBeforeIsExecuted(t *testing.T) { 11 | var executed bool 12 | 13 | app := NewApp(nil) 14 | app.Before = func(http.ResponseWriter, *http.Request) { 15 | executed = true 16 | } 17 | 18 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 19 | if !executed { 20 | t.Fail() 21 | } 22 | } 23 | 24 | func TestAfterIsExecuted(t *testing.T) { 25 | var executed bool 26 | 27 | app := NewApp(nil) 28 | app.After = func(http.ResponseWriter, *http.Request) { 29 | executed = true 30 | } 31 | 32 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 33 | if !executed { 34 | t.Fail() 35 | } 36 | } 37 | 38 | func TestNotFoundHandlerIsExecuted(t *testing.T) { 39 | var executed bool 40 | 41 | app := NewApp(nil) 42 | app.NotFoundHandler = func(http.ResponseWriter, *http.Request) { 43 | executed = true 44 | } 45 | 46 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 47 | if !executed { 48 | t.Fail() 49 | } 50 | } 51 | 52 | func TestErrorFromRouterIsHandled(t *testing.T) { 53 | var executed bool 54 | 55 | app := NewApp(nil) 56 | app.Router = RouterFunc(func(*http.Request) (*RouteMatch, error) { 57 | return nil, errors.New("") 58 | }) 59 | 60 | app.ErrorHandler = func(http.ResponseWriter, *http.Request, error) { 61 | executed = true 62 | } 63 | 64 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 65 | if !executed { 66 | t.Fail() 67 | } 68 | } 69 | 70 | func TestErrorFromHandlerIsHandled(t *testing.T) { 71 | var executed bool 72 | 73 | app := NewApp(nil) 74 | app.Router = RouterFunc(func(*http.Request) (*RouteMatch, error) { 75 | handler := func(*Context) (Response, error) { 76 | return nil, errors.New("") 77 | } 78 | 79 | return &RouteMatch{Handler: handler}, nil 80 | }) 81 | 82 | app.ErrorHandler = func(http.ResponseWriter, *http.Request, error) { 83 | executed = true 84 | } 85 | 86 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 87 | if !executed { 88 | t.Fail() 89 | } 90 | } 91 | 92 | func TestResponseWriteIsExecuted(t *testing.T) { 93 | var executed bool 94 | 95 | app := NewApp(nil) 96 | app.Router = RouterFunc(func(*http.Request) (*RouteMatch, error) { 97 | handler := func(*Context) (Response, error) { 98 | response := ResponseFunc(func(http.ResponseWriter, *http.Request) { 99 | executed = true 100 | }) 101 | 102 | return response, nil 103 | } 104 | 105 | return &RouteMatch{Handler: handler}, nil 106 | }) 107 | 108 | app.ServeHTTP(httptest.NewRecorder(), newRequest("", "")) 109 | if !executed { 110 | t.Fail() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | var ( 8 | HTMLHeaders = map[string]string{"Content-Type": "text/html"} 9 | JSONHeaders = map[string]string{"Content-Type": "application/json"} 10 | TextHeaders = map[string]string{"Content-Type": "text/plain"} 11 | CORSHeaders = map[string]string{ 12 | "Access-Control-Allow-Origin": "*", 13 | "Access-Control-Allow-Credentials": "true", 14 | "Access-Control-Allow-Headers": "Authorization, Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers", 15 | "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, COPY, HEAD, OPTIONS, LINK, UNLINK, CONNECT, TRACE, PURGE", 16 | } 17 | ) 18 | 19 | // Context is passed into Handlers 20 | // It contains fields and helper functions related to the request 21 | type Context struct { 22 | // PathVariables are the URL-related variables returned by the Router 23 | PathVariables map[string]string 24 | // Meta can be used to pass information along Decorators 25 | Meta map[string]interface{} 26 | // Parser is used to render html templates 27 | Parser TemplateParser 28 | // Request is the originating *http.Request 29 | Request *http.Request 30 | } 31 | 32 | // Context.HTML calls HTML with the Context's template parser 33 | func (c *Context) HTML(status int, templateName string, data interface{}) (*HTTPResponse, error) { 34 | return HTML(c.Parser, status, templateName, data) 35 | } 36 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | ) 7 | 8 | func TestHTML(t *testing.T) { 9 | parser := TemplateParserFunc(func() (*template.Template, error) { 10 | tmpl, err := template.New("template_name").Parse("

{{ . }}

") 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | 15 | return tmpl, nil 16 | }) 17 | 18 | context := &Context{Parser: parser} 19 | response, err := context.HTML(200, "template_name", "some data") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if v, want := string(response.Body), "

some data

"; v != want { 25 | t.Errorf("\nExpected: %#v \nReceived: %#v", want, v) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /decorators.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // A Decorator wraps logic around a Handler 9 | type Decorator func(Handler) Handler 10 | 11 | // Decorate is a helper function that decorates each Handler in each Route with the given Decorators 12 | func Decorate(routes []*Route, decorators ...Decorator) []*Route { 13 | decorated := make([]*Route, len(routes)) 14 | 15 | for i, route := range routes { 16 | decorated[i] = &Route{ 17 | Path: route.Path, 18 | Handlers: map[string]Handler{}, 19 | } 20 | 21 | for method, handler := range route.Handlers { 22 | for _, decorator := range decorators { 23 | handler = decorator(handler) 24 | } 25 | 26 | decorated[i].Handlers[method] = handler 27 | } 28 | } 29 | 30 | return decorated 31 | } 32 | 33 | // LogDecorator will print the method and url of each request 34 | func LogDecorator() Decorator { 35 | return func(handler Handler) Handler { 36 | return func(c *Context) (Response, error) { 37 | log.Printf("%s %s \n", c.Request.Method, c.Request.URL.String()) 38 | return handler(c) 39 | } 40 | } 41 | } 42 | 43 | // BasicAuthDecorator will add basic authentication using the specified username and password 44 | func BasicAuthDecorator(username, password string) Decorator { 45 | return func(handler Handler) Handler { 46 | return func(c *Context) (Response, error) { 47 | user, pass, ok := c.Request.BasicAuth() 48 | if ok && user == username && pass == password { 49 | return handler(c) 50 | } 51 | 52 | headers := map[string]string{"WWW-Authenticate": "Basic realm=\"Restricted\""} 53 | response := NewResponse(401, []byte("401 Unauthorized\n"), headers) 54 | return response, nil 55 | } 56 | } 57 | } 58 | 59 | // HeaderResponseDecorator will add the specified headers to each response 60 | func HeaderResponseDecorator(headers map[string]string) Decorator { 61 | return func(handler Handler) Handler { 62 | return func(c *Context) (Response, error) { 63 | response, err := handler(c) 64 | var wrappedResponse ResponseFunc = func(w http.ResponseWriter, r *http.Request) { 65 | for key, val := range headers { 66 | if v := w.Header().Get(key); v == "" { 67 | w.Header().Set(key, val) 68 | } 69 | } 70 | 71 | response.Write(w, r) 72 | } 73 | 74 | return wrappedResponse, err 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // HTTPError implements the Response and Error interfaces 8 | type HTTPError struct { 9 | *HTTPResponse 10 | Err error 11 | } 12 | 13 | // NewError returns a new HTTPError 14 | func NewError(status int, err error, headers map[string]string) *HTTPError { 15 | return &HTTPError{ 16 | HTTPResponse: NewResponse(status, []byte(err.Error()), headers), 17 | Err: err, 18 | } 19 | } 20 | 21 | // Error calls the internal Err.Error function 22 | func (e *HTTPError) Error() string { 23 | return e.Err.Error() 24 | } 25 | 26 | // DefaultErrorHandler is the default ErrorHandler used by an App 27 | // If the error implements the Response interface, it will call its Write function 28 | // Otherwise, a 500 with the error message is returned 29 | func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) { 30 | if err, ok := err.(Response); ok { 31 | err.Write(w, r) 32 | return 33 | } 34 | 35 | http.Error(w, err.Error(), http.StatusInternalServerError) 36 | } 37 | -------------------------------------------------------------------------------- /examples/api/README.md: -------------------------------------------------------------------------------- 1 | # API 2 | This example application shows a basic API implementation using Fireball. 3 | The following elements are shown here: 4 | * Returning JSON responses from Handlers 5 | * Using a custom App.ErrorHandler to send all errors as JSON 6 | * Using path variables in routes 7 | 8 | 9 | 10 | ## Run Example 11 | From this directory, run: 12 | ``` 13 | go run main.go 14 | ``` 15 | 16 | Navigate to `http://localhost:9090/movies`. 17 | The username and password is "user" and "pass". 18 | -------------------------------------------------------------------------------- /examples/api/controllers/error.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/zpatrick/fireball" 8 | ) 9 | 10 | func JSONErrorHandler(w http.ResponseWriter, r *http.Request, err error) { 11 | response, err := fireball.NewJSONError(500, err) 12 | if err != nil { 13 | log.Println(err) 14 | response := fireball.NewError(500, err, nil) 15 | response.Write(w, r) 16 | return 17 | } 18 | 19 | response.Write(w, r) 20 | } 21 | -------------------------------------------------------------------------------- /examples/api/controllers/movie.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/zpatrick/fireball" 8 | "github.com/zpatrick/fireball/examples/api/models" 9 | ) 10 | 11 | type MovieController struct { 12 | Movies map[string]models.Movie 13 | } 14 | 15 | func NewMovieController() *MovieController { 16 | return &MovieController{ 17 | Movies: map[string]models.Movie{}, 18 | } 19 | } 20 | 21 | func (m *MovieController) Routes() []*fireball.Route { 22 | routes := []*fireball.Route{ 23 | { 24 | Path: "/movies", 25 | Handlers: fireball.Handlers{ 26 | "GET": m.ListMovies, 27 | "POST": m.AddMovie, 28 | }, 29 | }, 30 | { 31 | Path: "/movies/:title", 32 | Handlers: fireball.Handlers{ 33 | "GET": m.GetMovie, 34 | "DELETE": m.DeleteMovie, 35 | }, 36 | }, 37 | } 38 | 39 | return routes 40 | } 41 | 42 | func (m *MovieController) ListMovies(c *fireball.Context) (fireball.Response, error) { 43 | movies := []models.Movie{} 44 | for _, movie := range m.Movies { 45 | movies = append(movies, movie) 46 | } 47 | 48 | return fireball.NewJSONResponse(200, movies) 49 | } 50 | 51 | func (m *MovieController) AddMovie(c *fireball.Context) (fireball.Response, error) { 52 | var movie models.Movie 53 | if err := json.NewDecoder(c.Request.Body).Decode(&movie); err != nil { 54 | return nil, err 55 | } 56 | 57 | m.Movies[movie.Title] = movie 58 | return fireball.NewJSONResponse(200, movie) 59 | } 60 | 61 | func (m *MovieController) GetMovie(c *fireball.Context) (fireball.Response, error) { 62 | title := c.PathVariables["title"] 63 | movie, ok := m.Movies[title] 64 | if !ok { 65 | return nil, fmt.Errorf("Movie with title '%s' does not exist", title) 66 | } 67 | 68 | return fireball.NewJSONResponse(200, movie) 69 | } 70 | 71 | func (m *MovieController) DeleteMovie(c *fireball.Context) (fireball.Response, error) { 72 | title := c.PathVariables["title"] 73 | if _, ok := m.Movies[title]; !ok { 74 | return nil, fmt.Errorf("Movie with title '%s' does not exist", title) 75 | } 76 | 77 | delete(m.Movies, title) 78 | return fireball.NewJSONResponse(200, nil) 79 | } 80 | -------------------------------------------------------------------------------- /examples/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/zpatrick/fireball" 8 | "github.com/zpatrick/fireball/examples/api/controllers" 9 | ) 10 | 11 | func main() { 12 | routes := controllers.NewMovieController().Routes() 13 | routes = fireball.Decorate(routes, 14 | fireball.BasicAuthDecorator("user", "pass"), 15 | fireball.LogDecorator()) 16 | 17 | app := fireball.NewApp(routes) 18 | app.ErrorHandler = controllers.JSONErrorHandler 19 | 20 | log.Println("Running on port 9090") 21 | log.Fatal(http.ListenAndServe(":9090", app)) 22 | } 23 | -------------------------------------------------------------------------------- /examples/api/models/movie.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Movie struct { 4 | Title string `json:"title"` 5 | Year int `json:"year"` 6 | } 7 | -------------------------------------------------------------------------------- /examples/blog/README.md: -------------------------------------------------------------------------------- 1 | # Blog 2 | This example application shows a basic blog implementation using Fireball. 3 | The following elements are shown here: 4 | * Serving Static Files 5 | * Using HTML templates and partials 6 | * Logging requests 7 | * Using the BasicAuth decorator 8 | 9 | 10 | ## Run Example 11 | From this directory, run: 12 | ``` 13 | go run main.go 14 | ``` 15 | 16 | The username and password is "user" and "pass" 17 | 18 | ### Acknowledgements 19 | CSS and HTML Layout by http://purecss.io/ -------------------------------------------------------------------------------- /examples/blog/controllers/root_controller.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/zpatrick/fireball" 7 | "github.com/zpatrick/fireball/examples/blog/models" 8 | ) 9 | 10 | type RootController struct{} 11 | 12 | func NewRootController() *RootController { 13 | return &RootController{} 14 | } 15 | 16 | func (h *RootController) Routes() []*fireball.Route { 17 | routes := []*fireball.Route{ 18 | { 19 | Path: "/", 20 | Handlers: fireball.Handlers{ 21 | "GET": h.index, 22 | }, 23 | }, 24 | } 25 | 26 | return routes 27 | } 28 | 29 | func (h *RootController) index(c *fireball.Context) (fireball.Response, error) { 30 | data := struct { 31 | PinnedPost *models.Post 32 | RecentPosts []*models.Post 33 | }{ 34 | PinnedPost: &models.Post{ 35 | Title: "Lorem Ipsum", 36 | Date: time.Now(), 37 | Author: "John Doe", 38 | Body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent sit amet eleifend sapien. 39 | Fusce non facilisis est, in laoreet purus. In mattis urna ut urna interdum ornare. 40 | Sed viverra egestas quam, sed porta arcu placerat a. Nullam ut nibh dolor. 41 | Integer ultricies id sem sed facilisis. Etiam semper laoreet hendrerit`, 42 | }, 43 | RecentPosts: []*models.Post{ 44 | { 45 | Title: "Duis at suscipit purus", 46 | Date: time.Now(), 47 | Author: "John Doe", 48 | Body: `Etiam a lacus euismod, pharetra nulla sit amet, condimentum felis. 49 | Phasellus varius lectus in ornare vulputate. Integer gravida nisl eget accumsan ullamcorper. 50 | Duis efficitur velit nec erat vehicula, vitae bibendum felis eleifend. Nunc cursus, 51 | nulla vitae pulvinar pulvinar, nibh lectus scelerisque dui, eu tincidunt lacus tellus sit amet nisi.`, 52 | }, 53 | { 54 | Title: "Etiam Aliquet", 55 | Date: time.Now(), 56 | Author: "John Doe", 57 | Body: `Nam mollis lacus non lectus vulputate fringilla. Proin quis dui non tortor porta porta at ut urna. 58 | Duis eu ex volutpat, dignissim justo eu, molestie ex. Vivamus auctor lorem eu tellus accumsan sollicitudin. 59 | Nam enim orci, aliquet sit amet tellus eget, euismod suscipit nunc.`, 60 | }, 61 | { 62 | Title: "Duis id Nibh", 63 | Date: time.Now(), 64 | Author: "John Doe", 65 | Body: `Morbi interdum tincidunt lorem non consectetur. In hac habitasse platea dictumst. 66 | Nunc sodales hendrerit felis id condimentum. Quisque quis interdum lacus. 67 | Morbi porta auctor leo, non efficitur felis consequat non.`, 68 | }, 69 | }, 70 | } 71 | 72 | return c.HTML(200, "index.html", data) 73 | } 74 | -------------------------------------------------------------------------------- /examples/blog/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/zpatrick/fireball" 8 | "github.com/zpatrick/fireball/examples/blog/controllers" 9 | ) 10 | 11 | func main() { 12 | controller := controllers.NewRootController() 13 | routes := fireball.Decorate( 14 | controller.Routes(), 15 | fireball.BasicAuthDecorator("user", "pass"), 16 | fireball.LogDecorator()) 17 | 18 | app := fireball.NewApp(routes) 19 | http.Handle("/", app) 20 | 21 | fs := http.FileServer(http.Dir("static")) 22 | http.Handle("/static/", http.StripPrefix("/static", fs)) 23 | 24 | log.Println("Running on port 8000") 25 | log.Fatal(http.ListenAndServe(":8000", nil)) 26 | } 27 | -------------------------------------------------------------------------------- /examples/blog/models/post.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Post struct { 8 | Title string 9 | Date time.Time 10 | Author string 11 | Body string 12 | } 13 | -------------------------------------------------------------------------------- /examples/blog/static/css/blog-old-ie.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | 12 | a:hover, 13 | a:focus { 14 | text-decoration: underline; 15 | } 16 | 17 | h3 { 18 | font-weight: 100; 19 | } 20 | 21 | /* LAYOUT CSS */ 22 | 23 | .pure-img-responsive { 24 | max-width: 100%; 25 | height: auto; 26 | } 27 | 28 | #layout { 29 | padding: 0; 30 | } 31 | 32 | .header { 33 | text-align: center; 34 | top: auto; 35 | margin: 3em auto; 36 | } 37 | 38 | .sidebar { 39 | background: rgb(61, 79, 93); 40 | color: #fff; 41 | } 42 | 43 | .brand-title, 44 | .brand-tagline { 45 | margin: 0; 46 | } 47 | 48 | .brand-title { 49 | text-transform: uppercase; 50 | } 51 | 52 | .brand-tagline { 53 | font-weight: 300; 54 | color: rgb(176, 202, 219); 55 | } 56 | 57 | .nav-list { 58 | margin: 0; 59 | padding: 0; 60 | list-style: none; 61 | } 62 | 63 | .nav-item { 64 | display: inline-block; 65 | *display: inline; 66 | zoom: 1; 67 | } 68 | 69 | .nav-item a { 70 | background: transparent; 71 | border: 2px solid rgb(176, 202, 219); 72 | color: #fff; 73 | margin-top: 1em; 74 | letter-spacing: 0.05em; 75 | text-transform: uppercase; 76 | font-size: 85%; 77 | } 78 | 79 | .nav-item a:hover, 80 | .nav-item a:focus { 81 | border: 2px solid rgb(61, 146, 201); 82 | text-decoration: none; 83 | } 84 | 85 | .content-subhead { 86 | text-transform: uppercase; 87 | color: #aaa; 88 | border-bottom: 1px solid #eee; 89 | padding: 0.4em 0; 90 | font-size: 80%; 91 | font-weight: 500; 92 | letter-spacing: 0.1em; 93 | } 94 | 95 | .content { 96 | padding: 2em 1em 0; 97 | } 98 | 99 | .post { 100 | padding-bottom: 2em; 101 | } 102 | 103 | .post-title { 104 | font-size: 2em; 105 | color: #222; 106 | margin-bottom: 0.2em; 107 | } 108 | 109 | .post-avatar { 110 | border-radius: 50px; 111 | float: right; 112 | margin-left: 1em; 113 | } 114 | 115 | .post-description { 116 | font-family: Georgia, "Cambria", serif; 117 | color: #444; 118 | line-height: 1.8em; 119 | } 120 | 121 | .post-meta { 122 | color: #999; 123 | font-size: 90%; 124 | margin: 0; 125 | } 126 | 127 | .post-category { 128 | margin: 0 0.1em; 129 | padding: 0.3em 1em; 130 | color: #fff; 131 | background: #999; 132 | font-size: 80%; 133 | } 134 | 135 | .post-category-design { 136 | background: #5aba59; 137 | } 138 | 139 | .post-category-pure { 140 | background: #4d85d1; 141 | } 142 | 143 | .post-category-yui { 144 | background: #8156a7; 145 | } 146 | 147 | .post-category-js { 148 | background: #df2d4f; 149 | } 150 | 151 | .post-images { 152 | margin: 1em 0; 153 | } 154 | 155 | .post-image-meta { 156 | margin-top: -3.5em; 157 | margin-left: 1em; 158 | color: #fff; 159 | text-shadow: 0 1px 1px #333; 160 | } 161 | 162 | .footer { 163 | text-align: center; 164 | padding: 1em 0; 165 | } 166 | 167 | .footer a { 168 | color: #ccc; 169 | font-size: 80%; 170 | } 171 | 172 | .footer .pure-menu a:hover, 173 | .footer .pure-menu a:focus { 174 | background: none; 175 | } 176 | 177 | .content { 178 | padding: 2em 3em 0; 179 | margin-left: 25%; 180 | } 181 | 182 | .header { 183 | margin: 80% 2em 0; 184 | text-align: right; 185 | } 186 | 187 | .sidebar { 188 | position: fixed; 189 | top: 0; 190 | bottom: 0; 191 | } -------------------------------------------------------------------------------- /examples/blog/static/css/blog.css: -------------------------------------------------------------------------------- 1 | * { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | a { 8 | text-decoration: none; 9 | color: rgb(61, 146, 201); 10 | } 11 | a:hover, 12 | a:focus { 13 | text-decoration: underline; 14 | } 15 | 16 | h3 { 17 | font-weight: 100; 18 | } 19 | 20 | /* LAYOUT CSS */ 21 | .pure-img-responsive { 22 | max-width: 100%; 23 | height: auto; 24 | } 25 | 26 | #layout { 27 | padding: 0; 28 | } 29 | 30 | .header { 31 | text-align: center; 32 | top: auto; 33 | margin: 3em auto; 34 | } 35 | 36 | .sidebar { 37 | background: rgb(61, 79, 93); 38 | color: #fff; 39 | } 40 | 41 | .brand-title, 42 | .brand-tagline { 43 | margin: 0; 44 | } 45 | .brand-title { 46 | text-transform: uppercase; 47 | } 48 | .brand-tagline { 49 | font-weight: 300; 50 | color: rgb(176, 202, 219); 51 | } 52 | 53 | .nav-list { 54 | margin: 0; 55 | padding: 0; 56 | list-style: none; 57 | } 58 | .nav-item { 59 | display: inline-block; 60 | *display: inline; 61 | zoom: 1; 62 | } 63 | .nav-item a { 64 | background: transparent; 65 | border: 2px solid rgb(176, 202, 219); 66 | color: #fff; 67 | margin-top: 1em; 68 | letter-spacing: 0.05em; 69 | text-transform: uppercase; 70 | font-size: 85%; 71 | } 72 | .nav-item a:hover, 73 | .nav-item a:focus { 74 | border: 2px solid rgb(61, 146, 201); 75 | text-decoration: none; 76 | } 77 | 78 | .content-subhead { 79 | text-transform: uppercase; 80 | color: #aaa; 81 | border-bottom: 1px solid #eee; 82 | padding: 0.4em 0; 83 | font-size: 80%; 84 | font-weight: 500; 85 | letter-spacing: 0.1em; 86 | } 87 | 88 | .content { 89 | padding: 2em 1em 0; 90 | } 91 | 92 | .post { 93 | padding-bottom: 2em; 94 | } 95 | .post-title { 96 | font-size: 2em; 97 | color: #222; 98 | margin-bottom: 0.2em; 99 | } 100 | .post-avatar { 101 | border-radius: 50px; 102 | float: right; 103 | margin-left: 1em; 104 | } 105 | .post-description { 106 | font-family: Georgia, "Cambria", serif; 107 | color: #444; 108 | line-height: 1.8em; 109 | } 110 | .post-meta { 111 | color: #999; 112 | font-size: 90%; 113 | margin: 0; 114 | } 115 | 116 | .post-category { 117 | margin: 0 0.1em; 118 | padding: 0.3em 1em; 119 | color: #fff; 120 | background: #999; 121 | font-size: 80%; 122 | } 123 | .post-category-design { 124 | background: #5aba59; 125 | } 126 | .post-category-pure { 127 | background: #4d85d1; 128 | } 129 | .post-category-yui { 130 | background: #8156a7; 131 | } 132 | .post-category-js { 133 | background: #df2d4f; 134 | } 135 | 136 | .post-images { 137 | margin: 1em 0; 138 | } 139 | .post-image-meta { 140 | margin-top: -3.5em; 141 | margin-left: 1em; 142 | color: #fff; 143 | text-shadow: 0 1px 1px #333; 144 | } 145 | 146 | .footer { 147 | text-align: center; 148 | padding: 1em 0; 149 | } 150 | .footer a { 151 | color: #ccc; 152 | font-size: 80%; 153 | } 154 | .footer .pure-menu a:hover, 155 | .footer .pure-menu a:focus { 156 | background: none; 157 | } 158 | 159 | @media (min-width: 48em) { 160 | .content { 161 | padding: 2em 3em 0; 162 | margin-left: 25%; 163 | } 164 | 165 | .header { 166 | margin: 80% 2em 0; 167 | text-align: right; 168 | } 169 | 170 | .sidebar { 171 | position: fixed; 172 | top: 0; 173 | bottom: 0; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /examples/blog/static/img/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridgelines/fireball/f42d3a4bbbbc36f2d8cd1a13f0ee9365c96832d6/examples/blog/static/img/avatar.jpg -------------------------------------------------------------------------------- /examples/blog/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ template "partials/head" "Blog Example" }} 4 | 5 | 6 |
7 | 24 | 25 |
26 |
27 | 28 |
29 | 30 | {{ with .PinnedPost }} 31 |

Pinned Post

32 | 33 | 34 |
35 |
36 | avatar 37 | 38 |

{{ .Title }}

39 | 40 | 43 |
44 | 45 |
46 |

47 | {{ .Body }} 48 |

49 |
50 |
51 |
52 | {{ end }} 53 | 54 |
55 |

Recent Posts

56 | 57 | {{ range .RecentPosts }} 58 |
59 |
60 | avatar 61 | 62 |

{{ .Title }}

63 | 64 | 67 |
68 | 69 |
70 |

71 | {{ .Body }} 72 |

73 |
74 |
75 | {{ end }} 76 | 77 | {{ template "partials/footer" }} 78 |
79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /examples/blog/views/partials/_footer.html: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 11 | {{end}} 12 | -------------------------------------------------------------------------------- /examples/blog/views/partials/_head.html: -------------------------------------------------------------------------------- 1 | {{ define "head" }} 2 | 3 | 4 | 5 | 6 | {{ . }} 7 | 8 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | {{ end }} 22 | -------------------------------------------------------------------------------- /examples/hello_world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World 2 | This example application shows a basic "Hello, World" implementation using Fireball. 3 | 4 | ## Run Example 5 | From this directory, run: 6 | ``` 7 | go run main.go 8 | ``` -------------------------------------------------------------------------------- /examples/hello_world/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/zpatrick/fireball" 8 | ) 9 | 10 | func index(c *fireball.Context) (fireball.Response, error) { 11 | return fireball.NewResponse(200, []byte("Hello, World!"), nil), nil 12 | } 13 | 14 | func main() { 15 | indexRoute := &fireball.Route{ 16 | Path: "/", 17 | Handlers: fireball.Handlers{ 18 | "GET": index, 19 | }, 20 | } 21 | 22 | routes := []*fireball.Route{indexRoute} 23 | app := fireball.NewApp(routes) 24 | 25 | log.Println("Running on port 8000") 26 | log.Fatal(http.ListenAndServe(":8000", app)) 27 | } 28 | -------------------------------------------------------------------------------- /examples/swagger/README.md: -------------------------------------------------------------------------------- 1 | # Swagger 2 | This example application adds [Swagger](http://swagger.io/) documentation to the [API example](https://github.com/zpatrick/fireball/tree/master/examples/api). 3 | 4 | ## Run Example 5 | From this directory, run: 6 | ``` 7 | go run main.go 8 | ``` 9 | 10 | By default, if you navigate to `http://localhost:9090/api/`, it will serve Swagger's default [Petstore](http://petstore.swagger.io/) example. 11 | To use the local configuration, enter `http://localhost:9090/swagger.json` into the **Explore** box on the top right of the page, 12 | or navigate to `http://localhost:9090/api/?url=http://localhost:9090/swagger.json` 13 | 14 | ## Getting Swagger UI 15 | The Swagger UI in the `static/swagger-ui/dist` directory was cloned from the [Swagger UI Repo](https://github.com/swagger-api/swagger-ui/tree/master/dist). 16 | The `dist` directory holds the required files to needed to serve the Swagger UI. 17 | -------------------------------------------------------------------------------- /examples/swagger/controllers/movie.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/zpatrick/fireball" 8 | "github.com/zpatrick/fireball/examples/swagger/models" 9 | ) 10 | 11 | type MovieController struct { 12 | Movies map[string]models.Movie 13 | } 14 | 15 | func NewMovieController() *MovieController { 16 | return &MovieController{ 17 | Movies: map[string]models.Movie{}, 18 | } 19 | } 20 | 21 | func (m *MovieController) Routes() []*fireball.Route { 22 | routes := []*fireball.Route{ 23 | { 24 | Path: "/movies", 25 | Handlers: fireball.Handlers{ 26 | "GET": m.ListMovies, 27 | "POST": m.AddMovie, 28 | }, 29 | }, 30 | { 31 | Path: "/movies/:title", 32 | Handlers: fireball.Handlers{ 33 | "GET": m.GetMovie, 34 | "DELETE": m.DeleteMovie, 35 | }, 36 | }, 37 | } 38 | 39 | return routes 40 | } 41 | 42 | func (m *MovieController) ListMovies(c *fireball.Context) (fireball.Response, error) { 43 | movies := []models.Movie{} 44 | for _, movie := range m.Movies { 45 | movies = append(movies, movie) 46 | } 47 | 48 | return fireball.NewJSONResponse(200, movies) 49 | } 50 | 51 | func (m *MovieController) AddMovie(c *fireball.Context) (fireball.Response, error) { 52 | var movie models.Movie 53 | if err := json.NewDecoder(c.Request.Body).Decode(&movie); err != nil { 54 | return nil, err 55 | } 56 | 57 | m.Movies[movie.Title] = movie 58 | return fireball.NewJSONResponse(200, movie) 59 | } 60 | 61 | func (m *MovieController) GetMovie(c *fireball.Context) (fireball.Response, error) { 62 | title := c.PathVariables["title"] 63 | movie, ok := m.Movies[title] 64 | if !ok { 65 | return nil, fmt.Errorf("Movie with title '%s' does not exist", title) 66 | } 67 | 68 | return fireball.NewJSONResponse(200, movie) 69 | } 70 | 71 | func (m *MovieController) DeleteMovie(c *fireball.Context) (fireball.Response, error) { 72 | title := c.PathVariables["title"] 73 | if _, ok := m.Movies[title]; !ok { 74 | return nil, fmt.Errorf("Movie with title '%s' does not exist", title) 75 | } 76 | 77 | delete(m.Movies, title) 78 | return fireball.NewJSONResponse(200, nil) 79 | } 80 | -------------------------------------------------------------------------------- /examples/swagger/controllers/swagger.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/zpatrick/fireball" 5 | "github.com/zpatrick/fireball/examples/swagger/models" 6 | swagger "github.com/zpatrick/go-plugin-swagger" 7 | ) 8 | 9 | type SwaggerController struct{} 10 | 11 | func NewSwaggerController() *SwaggerController { 12 | return &SwaggerController{} 13 | } 14 | 15 | func (s *SwaggerController) Routes() []*fireball.Route { 16 | routes := []*fireball.Route{ 17 | { 18 | Path: "/swagger.json", 19 | Handlers: fireball.Handlers{ 20 | "GET": s.ServeSwaggerSpec, 21 | }, 22 | }, 23 | } 24 | 25 | return routes 26 | } 27 | 28 | func (s *SwaggerController) ServeSwaggerSpec(c *fireball.Context) (fireball.Response, error) { 29 | spec := swagger.Spec{ 30 | SwaggerVersion: "2.0", 31 | Schemes: []string{"http"}, 32 | Info: &swagger.Info{ 33 | Title: "Swagger Example", 34 | Version: "0.0.1", 35 | }, 36 | Definitions: map[string]swagger.Definition{ 37 | "Movie": models.Movie{}.Definition(), 38 | }, 39 | Tags: []swagger.Tag{ 40 | { 41 | Name: "Movies", 42 | Description: "Methods related to movies", 43 | }, 44 | }, 45 | Paths: map[string]swagger.Path{ 46 | "/movies": map[string]swagger.Method{ 47 | "get": { 48 | Summary: "List all Movies", 49 | Tags: []string{"Movies"}, 50 | Responses: map[string]swagger.Response{ 51 | "200": { 52 | Description: "An array of movies", 53 | Schema: swagger.NewObjectSliceSchema("Movie"), 54 | }, 55 | }, 56 | }, 57 | "post": { 58 | Summary: "Add a Movie", 59 | Tags: []string{"Movies"}, 60 | Parameters: []swagger.Parameter{ 61 | swagger.NewBodyParam("Movie", "Movie to add", true), 62 | }, 63 | Responses: map[string]swagger.Response{ 64 | "200": { 65 | Description: "The added movie", 66 | Schema: swagger.NewObjectSchema("Movie"), 67 | }, 68 | }, 69 | }, 70 | }, 71 | "/movies/{title}": map[string]swagger.Method{ 72 | "get": { 73 | Summary: "Describe a Movie", 74 | Tags: []string{"Movies"}, 75 | Parameters: []swagger.Parameter{ 76 | swagger.NewStringPathParam("title", "Title of the movie to describe", true), 77 | }, 78 | Responses: map[string]swagger.Response{ 79 | "200": { 80 | Description: "The desired movie", 81 | Schema: swagger.NewObjectSchema("Movie"), 82 | }, 83 | }, 84 | }, 85 | "delete": { 86 | Summary: "Delete a Movie", 87 | Tags: []string{"Movies"}, 88 | Parameters: []swagger.Parameter{ 89 | swagger.NewStringPathParam("title", "Title of the movie to delete", true), 90 | }, 91 | Responses: map[string]swagger.Response{ 92 | "200": { 93 | Description: "Success", 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | } 100 | 101 | return fireball.NewJSONResponse(200, spec) 102 | } 103 | -------------------------------------------------------------------------------- /examples/swagger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/zpatrick/fireball" 8 | "github.com/zpatrick/fireball/examples/swagger/controllers" 9 | ) 10 | 11 | const ( 12 | SWAGGER_URL = "/api/" 13 | SWAGGER_UI_PATH = "static/swagger-ui/dist" 14 | ) 15 | 16 | func serveSwaggerUI(w http.ResponseWriter, r *http.Request) { 17 | dir := http.Dir(SWAGGER_UI_PATH) 18 | fileServer := http.FileServer(dir) 19 | http.StripPrefix(SWAGGER_URL, fileServer).ServeHTTP(w, r) 20 | } 21 | 22 | func main() { 23 | http.HandleFunc(SWAGGER_URL, serveSwaggerUI) 24 | 25 | routes := controllers.NewSwaggerController().Routes() 26 | routes = append(routes, controllers.NewMovieController().Routes()...) 27 | routes = fireball.Decorate(routes, 28 | fireball.LogDecorator()) 29 | 30 | app := fireball.NewApp(routes) 31 | http.Handle("/", app) 32 | 33 | log.Println("Running on port 9090") 34 | log.Fatal(http.ListenAndServe(":9090", nil)) 35 | } 36 | -------------------------------------------------------------------------------- /examples/swagger/models/movie.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/zpatrick/go-plugin-swagger" 5 | ) 6 | 7 | type Movie struct { 8 | Title string `json:"title"` 9 | Year int `json:"year"` 10 | } 11 | 12 | func (m Movie) Definition() swagger.Definition { 13 | return swagger.Definition{ 14 | Type: "object", 15 | Properties: map[string]swagger.Property{ 16 | "title": swagger.NewStringProperty(), 17 | "year": swagger.NewIntProperty(), 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridgelines/fireball/f42d3a4bbbbc36f2d8cd1a13f0ee9365c96832d6/examples/swagger/static/swagger-ui/dist/favicon-16x16.png -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ridgelines/fireball/f42d3a4bbbbc36f2d8cd1a13f0ee9365c96832d6/examples/swagger/static/swagger-ui/dist/favicon-32x32.png -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 84 | -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/swagger-ui-bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;AAu/FA;AA6+FA;;;;;;;;;;;;;;;;;;;;;;;;;;AA0dA;;;;;;AAoIA;AAk7FA;AAmtCA;;;;;AA0uIA;AA66IA;AA27FA;AAuwGA;AAilFA;AAikFA;AAs9CA;AA8jDA;AA2qCA;AA4tEA;AAgkIA;;;;;;;;;;;;;;AAw4GA;AAyoIA;AAiuJA;AA8kHA;AAonGA;AAukEA;AA02DA;AAyxDA;AAw6BA;;;;;;AA8vEA;AA+zFA;;;;;AA23CA;AA2qFA;AAq2CA;AA0kCA;AAs/CA;AA0wEA;AA49FA;;;;;;;;;AA20BA;AA2zIA;AAi4DA;AA6tDA","sourceRoot":""} -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/swagger-ui-standalone-preset.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SwaggerUIStandalonePreset=t():e.SwaggerUIStandalonePreset=t()}(this,function(){return function(e){function t(r){if(o[r])return o[r].exports;var n=o[r]={exports:{},id:r,loaded:!1};return e[r].call(n.exports,n,n.exports,t),n.loaded=!0,n.exports}var o={};return t.m=e,t.c=o,t.p="/dist",t(0)}([function(e,t,o){e.exports=o(1)},function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}var n=o(2),i=r(n);o(30);var a=o(34),s=r(a),l=[s.default,function(){return{components:{StandaloneLayout:i.default}}}];e.exports=l},function(e,t,o){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var s=function(){function e(e,t){for(var o=0;o1){for(var m=Array(b),x=0;x1){for(var h=Array(w),y=0;y>"),j={array:a("array"),bool:a("boolean"),func:a("function"),number:a("number"),object:a("object"),string:a("string"),symbol:a("symbol"),any:s(),arrayOf:l,element:p(),instanceOf:u,node:g(),objectOf:f,oneOf:c,oneOfType:d,shape:b};n.prototype=Error.prototype,e.exports=j},function(e,t){"use strict";var o="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";e.exports=o},function(e,t){"use strict";e.exports="15.4.2"},function(e,t,o){"use strict";function r(e){return i.isValidElement(e)?void 0:n("143"),e}var n=o(8),i=o(10);o(9);e.exports=r},function(e,t,o){var r=o(31);"string"==typeof r&&(r=[[e.id,r,""]]);o(33)(r,{});r.locals&&(e.exports=r.locals)},function(e,t,o){t=e.exports=o(32)(),t.push([e.id,"@charset \"UTF-8\";.swagger-ui html{box-sizing:border-box}.swagger-ui *,.swagger-ui :after,.swagger-ui :before{box-sizing:inherit}.swagger-ui body{margin:0;background:#fafafa}.swagger-ui .wrapper{width:100%;max-width:1460px;margin:0 auto;padding:0 20px}.swagger-ui .opblock-tag-section{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .opblock-tag{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 20px 10px 10px;cursor:pointer;-webkit-transition:all .2s;transition:all .2s;border-bottom:1px solid rgba(59,65,81,.3);-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock-tag:hover{background:rgba(0,0,0,.02)}.swagger-ui .opblock-tag{font-size:24px;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock-tag.no-desc span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .opblock-tag svg{-webkit-transition:all .4s;transition:all .4s}.swagger-ui .opblock-tag small{font-size:14px;font-weight:400;padding:0 10px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parаmeter__type{font-size:12px;padding:5px 0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .view-line-link{position:relative;top:3px;width:20px;margin:0 5px;cursor:pointer;-webkit-transition:all .5s;transition:all .5s}.swagger-ui .opblock{margin:0 0 15px;border:1px solid #000;border-radius:4px;box-shadow:0 0 3px rgba(0,0,0,.19)}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid #000}.swagger-ui .opblock .opblock-section-header{padding:8px 20px;background:hsla(0,0%,100%,.8);box-shadow:0 1px 2px rgba(0,0,0,.1)}.swagger-ui .opblock .opblock-section-header,.swagger-ui .opblock .opblock-section-header label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-section-header label{font-size:12px;font-weight:700;margin:0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-section-header label span{padding:0 10px 0 0}.swagger-ui .opblock .opblock-section-header h4{font-size:14px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary-method{font-size:14px;font-weight:700;min-width:80px;padding:6px 15px;text-align:center;border-radius:3px;background:#000;text-shadow:0 1px 0 rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 10px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-summary-path .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated .view-line-link{position:relative;top:2px;width:0;margin:0;cursor:pointer;-webkit-transition:all .5s;transition:all .5s}.swagger-ui .opblock .opblock-summary-path:hover .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated:hover .view-line-link{width:18px;margin:0 5px}.swagger-ui .opblock .opblock-summary-path__deprecated{text-decoration:line-through}.swagger-ui .opblock .opblock-summary-description{font-size:13px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary{display:-webkit-box;display:-ms-flexbox;display:flex;padding:5px;cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock.opblock-post{border-color:#49cc90;background:rgba(73,204,144,.1)}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:#49cc90}.swagger-ui .opblock.opblock-put{border-color:#fca130;background:rgba(252,161,48,.1)}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:#fca130}.swagger-ui .opblock.opblock-delete{border-color:#f93e3e;background:rgba(249,62,62,.1)}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:#f93e3e}.swagger-ui .opblock.opblock-get{border-color:#61affe;background:rgba(97,175,254,.1)}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:#61affe}.swagger-ui .opblock.opblock-patch{border-color:#50e3c2;background:rgba(80,227,194,.1)}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:#50e3c2}.swagger-ui .opblock.opblock-head{border-color:#9012fe;background:rgba(144,18,254,.1)}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:#9012fe}.swagger-ui .opblock.opblock-options{border-color:#0d5aa7;background:rgba(13,90,167,.1)}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:#0d5aa7}.swagger-ui .opblock.opblock-deprecated{opacity:.6;border-color:#ebebeb;background:hsla(0,0%,92%,.1)}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#ebebeb}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:#ebebeb}.swagger-ui .opblock .opblock-schemes{padding:8px 20px}.swagger-ui .opblock .opblock-schemes .schemes-title{padding:0 10px 0 0}.swagger-ui .tab{display:-webkit-box;display:-ms-flexbox;display:flex;margin:20px 0 10px;padding:0;list-style:none}.swagger-ui .tab li{font-size:12px;min-width:100px;min-width:90px;padding:0;cursor:pointer;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .tab li:first-of-type{position:relative;padding-left:0}.swagger-ui .tab li:first-of-type:after{position:absolute;top:0;right:6px;width:1px;height:100%;content:\"\";background:rgba(0,0,0,.2)}.swagger-ui .tab li.active{font-weight:700}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-title_normal{padding:15px 20px}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-title_normal,.swagger-ui .opblock-title_normal h4{font-size:12px;margin:0 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-title_normal p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .execute-wrapper{padding:20px;text-align:right}.swagger-ui .execute-wrapper .btn{width:100%;padding:8px 40px}.swagger-ui .body-param-options{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .body-param-options .body-param-edit{padding:10px 0}.swagger-ui .body-param-options label{padding:8px 0}.swagger-ui .body-param-options label select{margin:3px 0 0}.swagger-ui .responses-inner{padding:20px}.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{font-size:12px;margin:10px 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status .response-undocumented{font-size:11px;font-family:Source Code Pro,monospace;font-weight:600;color:#999}.swagger-ui .response-col_description__inner span{font-size:12px;font-style:italic;display:block;margin:10px 0;padding:10px;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .response-col_description__inner span p{margin:0}.swagger-ui .opblock-body pre{font-size:12px;margin:0;padding:10px;white-space:pre-wrap;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .opblock-body pre span{color:#fff!important}.swagger-ui .scheme-container{margin:0 0 20px;padding:30px 0;background:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.15)}.swagger-ui .scheme-container .schemes{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .scheme-container .schemes>label{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:\"loading\";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:\"\";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;-webkit-transition:all .3s;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{-webkit-transition:all .3s;transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;-webkit-transition:opacity .5s;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url(\"data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E\") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:\"\";background:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E\") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;-webkit-transition:all .2s;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{-webkit-transition:all .4s;transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;-webkit-transition:all .5s;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:\"required\";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .topbar .download-url-wrapper input[type=text]{min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;-webkit-transition:all .4s;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}",""]); 7 | },function(e,t){e.exports=function(){var e=[];return e.toString=function(){for(var e=[],t=0;t=0&&h.splice(t,1)}function s(e){var t=document.createElement("style");return t.type="text/css",i(e,t),t}function l(e){var t=document.createElement("link");return t.rel="stylesheet",i(e,t),t}function p(e,t){var o,r,n;if(t.singleton){var i=w++;o=x||(x=s(t)),r=u.bind(null,o,i,!1),n=u.bind(null,o,i,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(o=l(t),r=f.bind(null,o),n=function(){a(o),o.href&&URL.revokeObjectURL(o.href)}):(o=s(t),r=c.bind(null,o),n=function(){a(o)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else n()}}function u(e,t,o,r){var n=o?"":r.css;if(e.styleSheet)e.styleSheet.cssText=y(t,n);else{var i=document.createTextNode(n),a=e.childNodes;a[t]&&e.removeChild(a[t]),a.length?e.insertBefore(i,a[t]):e.appendChild(i)}}function c(e,t){var o=t.css,r=t.media;t.sourceMap;if(r&&e.setAttribute("media",r),e.styleSheet)e.styleSheet.cssText=o;else{for(;e.firstChild;)e.removeChild(e.firstChild);e.appendChild(document.createTextNode(o))}}function f(e,t){var o=t.css,r=(t.media,t.sourceMap);r&&(o+="\n/*# sourceMappingURL=data:application/json;base64,"+btoa(unescape(encodeURIComponent(JSON.stringify(r))))+" */");var n=new Blob([o],{type:"text/css"}),i=e.href;e.href=URL.createObjectURL(n),i&&URL.revokeObjectURL(i)}var d={},g=function(e){var t;return function(){return"undefined"==typeof t&&(t=e.apply(this,arguments)),t}},b=g(function(){return/msie [6-9]\b/.test(window.navigator.userAgent.toLowerCase())}),m=g(function(){return document.head||document.getElementsByTagName("head")[0]}),x=null,w=0,h=[];e.exports=function(e,t){t=t||{},"undefined"==typeof t.singleton&&(t.singleton=b()),"undefined"==typeof t.insertAt&&(t.insertAt="bottom");var o=n(e);return r(o,t),function(e){for(var i=[],a=0;alabel{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:"loading";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:"";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;-webkit-transition:all .3s;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{-webkit-transition:all .3s;transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;-webkit-transition:opacity .5s;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url("data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;-webkit-transition:-webkit-transform .15s ease-in;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:"";background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;-webkit-transition:all .2s;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{-webkit-transition:all .4s;transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;-webkit-transition:all .5s;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:"required";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .topbar .download-url-wrapper input[type=text]{min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;-webkit-transition:all .4s;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.swagger-ui .Resizer.vertical.disabled{display:none} 2 | /*# sourceMappingURL=swagger-ui.css.map*/ -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} -------------------------------------------------------------------------------- /examples/swagger/static/swagger-ui/dist/swagger-ui.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAuwCA;AAoyHA;AA2wHA;AA07FA;AA+nCA;AAohCA;AAghCA;AAk4BA","sourceRoot":""} -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // NewJSONResponse returns a new HTTPResponse in JSON format 8 | func NewJSONResponse(status int, data interface{}) (*HTTPResponse, error) { 9 | bytes, err := json.Marshal(data) 10 | if err != nil { 11 | return nil, err 12 | } 13 | 14 | response := NewResponse(status, bytes, JSONHeaders) 15 | return response, nil 16 | } 17 | 18 | // NewJSONError returns a new HTTPError in JSON format 19 | func NewJSONError(status int, err error) (*HTTPError, error) { 20 | e := struct { 21 | Error string 22 | }{ 23 | Error: err.Error(), 24 | } 25 | 26 | bytes, err := json.Marshal(e) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | response := &HTTPError{ 32 | HTTPResponse: NewResponse(status, bytes, JSONHeaders), 33 | Err: err, 34 | } 35 | 36 | return response, nil 37 | } 38 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // TemplateParser is an interface object that is used to parse HTML templates 13 | type TemplateParser interface { 14 | Parse() (*template.Template, error) 15 | } 16 | 17 | // TemplateParserFunc is a function which implements the TemplateParser interface 18 | type TemplateParserFunc func() (*template.Template, error) 19 | 20 | func (tpf TemplateParserFunc) Parse() (*template.Template, error) { 21 | return tpf() 22 | } 23 | 24 | // GlobParser generates a template by recusively searching the specified root directory 25 | // and parses templates that match the specified glob pattern 26 | type GlobParser struct { 27 | Root string 28 | Glob string 29 | cache *template.Template 30 | } 31 | 32 | // NewGlobParser returns a GlobParser with the specified root and glob pattern 33 | func NewGlobParser(root, glob string) *GlobParser { 34 | return &GlobParser{ 35 | Root: root, 36 | Glob: glob, 37 | } 38 | } 39 | 40 | // Parse recursively searches the root directory and parses templates 41 | // that match the specified glob pattern. 42 | // Template names are generated by path/from/root + filename. 43 | // 44 | // For example, if GlobParser.Root == "views", the following template names would be generated: 45 | // Files: 46 | // views/ 47 | // index.html 48 | // partials/ 49 | // login.html 50 | // 51 | // Template Names: 52 | // "index.html" 53 | // "partials/login.html" 54 | func (p *GlobParser) Parse() (*template.Template, error) { 55 | if p.cache != nil { 56 | return p.cache, nil 57 | } 58 | 59 | root := template.New("root") 60 | 61 | walkf := func(path string, info os.FileInfo, err error) error { 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if info.IsDir() { 67 | path = filepath.Join(path, p.Glob) 68 | current, err := template.ParseGlob(path) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | for _, t := range current.Templates() { 74 | name := p.generateTemplateName(path, t) 75 | 76 | if _, err := root.AddParseTree(name, t.Tree); err != nil { 77 | return err 78 | } 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | 85 | if err := filepath.Walk(p.Root, walkf); err != nil { 86 | return nil, err 87 | } 88 | 89 | p.cache = root 90 | return root, nil 91 | } 92 | 93 | func (p *GlobParser) generateTemplateName(path string, t *template.Template) string { 94 | path = strings.Replace(filepath.Dir(path), "\\", "/", -1) 95 | path = fmt.Sprintf("%s/%s", path, t.Name()) 96 | name := strings.TrimPrefix(path, p.Root) 97 | return name 98 | } 99 | 100 | // HTML is a helper function that returns a response generated from the given templateName and data 101 | func HTML(parser TemplateParser, status int, templateName string, data interface{}) (*HTTPResponse, error) { 102 | tmpl, err := parser.Parse() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | var buffer bytes.Buffer 108 | if err := tmpl.ExecuteTemplate(&buffer, templateName, data); err != nil { 109 | return nil, err 110 | } 111 | 112 | response := NewResponse(status, buffer.Bytes(), HTMLHeaders) 113 | return response, nil 114 | } 115 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var golden = ` 9 | 10 | 11 | Title 12 | 13 | 14 |

Header

15 |

Hello, World!

16 |

Footer

17 | 18 | 19 | ` 20 | 21 | func TestGlobParse(t *testing.T) { 22 | parser := NewGlobParser("testing/", "*.html") 23 | 24 | tmpl, err := parser.Parse() 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | 29 | var buffer bytes.Buffer 30 | if err := tmpl.ExecuteTemplate(&buffer, "index.html", nil); err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | if v, want := buffer.String(), golden; v != want { 35 | t.Errorf("\nExpected: %#v \nReceived: %#v", want, v) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // Response is an object that writes to an http.ResponseWriter 9 | // A Response object implements the http.Handler interface 10 | type Response interface { 11 | Write(http.ResponseWriter, *http.Request) 12 | } 13 | 14 | // ResponseFunc is a function which implements the Response interface 15 | type ResponseFunc func(http.ResponseWriter, *http.Request) 16 | 17 | func (rf ResponseFunc) Write(w http.ResponseWriter, r *http.Request) { 18 | rf(w, r) 19 | } 20 | 21 | // HTTPResponse objects write the specified status, headers, and body to 22 | // a http.ResponseWriter 23 | type HTTPResponse struct { 24 | Status int 25 | Body []byte 26 | Headers map[string]string 27 | } 28 | 29 | // NewResponse returns a new HTTPResponse with the specified status, body, and headers 30 | func NewResponse(status int, body []byte, headers map[string]string) *HTTPResponse { 31 | return &HTTPResponse{ 32 | Status: status, 33 | Body: body, 34 | Headers: headers, 35 | } 36 | } 37 | 38 | // Write will write the specified status, headers, and body to the http.ResponseWriter 39 | func (h *HTTPResponse) Write(w http.ResponseWriter, r *http.Request) { 40 | for key, val := range h.Headers { 41 | w.Header().Set(key, val) 42 | } 43 | 44 | w.WriteHeader(h.Status) 45 | if _, err := w.Write(h.Body); err != nil { 46 | log.Println(err) 47 | http.Error(w, err.Error(), http.StatusInternalServerError) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /route.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | // Handler performs the business logic on a request 4 | type Handler func(c *Context) (Response, error) 5 | 6 | // Handlers maps a http method to a Handler 7 | type Handlers map[string]Handler 8 | 9 | // Routes are used to map a request to a RouteMatch 10 | type Route struct { 11 | // Path is used to determine if a request's URL matches this Route 12 | Path string 13 | // Handlers map common HTTP methods to different Handlers 14 | Handlers map[string]Handler 15 | } 16 | 17 | // RouteMatch objects are returned by the router when a request is successfully matched 18 | type RouteMatch struct { 19 | Handler Handler 20 | PathVariables map[string]string 21 | } 22 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/zpatrick/go-cache" 8 | ) 9 | 10 | // Router is an interface that matches an *http.Request to a RouteMatch. 11 | // If no matches are found, a nil RouteMatch should be returned. 12 | type Router interface { 13 | Match(*http.Request) (*RouteMatch, error) 14 | } 15 | 16 | // RouterFunc is a function which implements the Router interface 17 | type RouterFunc func(*http.Request) (*RouteMatch, error) 18 | 19 | func (rf RouterFunc) Match(r *http.Request) (*RouteMatch, error) { 20 | return rf(r) 21 | } 22 | 23 | // BasicRouter attempts to match requests based on its Routes. 24 | // This router supports variables in the URL by using ":variable" notation in URL sections. 25 | // For example, the following are all valid Paths: 26 | // "/home" 27 | // "/movies/:id" 28 | // "/users/:userID/purchases/:purchaseID" 29 | // Matched Path Variables can be retrieved in Handlers by the Context: 30 | // func Handler(c *Context) (Response, error) { 31 | // id := c.PathVariables["id"] 32 | // ... 33 | // } 34 | type BasicRouter struct { 35 | Routes []*Route 36 | cache *cache.Cache 37 | } 38 | 39 | // NewBasicRouter returns a new BasicRouter with the specified Routes 40 | func NewBasicRouter(routes []*Route) *BasicRouter { 41 | return &BasicRouter{ 42 | Routes: routes, 43 | cache: cache.New(), 44 | } 45 | } 46 | 47 | // Match attempts to match the *http.Request to a Route. 48 | // Successful matches are cached for improved performance. 49 | func (r *BasicRouter) Match(req *http.Request) (*RouteMatch, error) { 50 | key := r.cacheKey(req) 51 | if rm, ok := r.cache.GetOK(key); ok { 52 | return rm.(*RouteMatch), nil 53 | } 54 | 55 | for _, route := range r.Routes { 56 | if rm := r.matchRoute(route, req); rm != nil { 57 | r.cache.Set(key, rm) 58 | return rm, nil 59 | } 60 | } 61 | 62 | return nil, nil 63 | } 64 | 65 | func (r *BasicRouter) matchRoute(route *Route, req *http.Request) *RouteMatch { 66 | handler := route.Handlers[req.Method] 67 | if handler == nil { 68 | return nil 69 | } 70 | 71 | pathVariables, ok := r.matchPathVariables(route, req.URL.Path) 72 | if !ok { 73 | return nil 74 | } 75 | 76 | routeMatch := &RouteMatch{ 77 | Handler: handler, 78 | PathVariables: pathVariables, 79 | } 80 | 81 | return routeMatch 82 | } 83 | 84 | func (r *BasicRouter) matchPathVariables(route *Route, url string) (map[string]string, bool) { 85 | if url != "/" { 86 | url = strings.TrimSuffix(url, "/") 87 | } 88 | 89 | if route.Path != "/" { 90 | route.Path = strings.TrimSuffix(route.Path, "/") 91 | } 92 | 93 | routeSections := strings.Split(route.Path, "/") 94 | urlSections := strings.Split(url, "/") 95 | 96 | if len(routeSections) != len(urlSections) { 97 | return nil, false 98 | } 99 | 100 | variables := map[string]string{} 101 | for i, routeSection := range routeSections { 102 | urlSection := urlSections[i] 103 | 104 | if strings.HasPrefix(routeSection, ":") { 105 | key := routeSection[1:] 106 | variables[key] = urlSection 107 | } else if routeSection != urlSection { 108 | return nil, false 109 | } 110 | } 111 | 112 | return variables, true 113 | } 114 | 115 | func (r *BasicRouter) cacheKey(req *http.Request) string { 116 | return req.Method + req.URL.String() 117 | } 118 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "math/rand" 5 | "net/http" 6 | "net/url" 7 | "reflect" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func nilHandler(*Context) (Response, error) { 13 | return nil, nil 14 | } 15 | 16 | func newRequest(method, path string) *http.Request { 17 | return &http.Request{ 18 | Method: method, 19 | URL: &url.URL{ 20 | Path: path, 21 | }, 22 | } 23 | } 24 | 25 | func newRouteWithNilHandler(method, path string) *Route { 26 | return &Route{ 27 | Path: path, 28 | Handlers: map[string]Handler{ 29 | method: nilHandler, 30 | }, 31 | } 32 | } 33 | 34 | func TestMethodMatch(t *testing.T) { 35 | for _, method := range []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"} { 36 | route := newRouteWithNilHandler(method, "/") 37 | request := newRequest(method, "/") 38 | router := NewBasicRouter([]*Route{route}) 39 | 40 | match, err := router.Match(request) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if match == nil { 46 | t.Errorf("Error on method '%s': Match was nil", method) 47 | } 48 | } 49 | } 50 | 51 | func TestPathVariableMatch(t *testing.T) { 52 | testCases := []struct { 53 | Route *Route 54 | Request *http.Request 55 | PathVariables map[string]string 56 | }{ 57 | { 58 | Route: newRouteWithNilHandler("GET", "/"), 59 | Request: newRequest("GET", "/"), 60 | PathVariables: map[string]string{}, 61 | }, 62 | { 63 | Route: newRouteWithNilHandler("GET", "/items"), 64 | Request: newRequest("GET", "/items"), 65 | PathVariables: map[string]string{}, 66 | }, 67 | { 68 | Route: newRouteWithNilHandler("GET", "/items/:itemID"), 69 | Request: newRequest("GET", "/items/item34"), 70 | PathVariables: map[string]string{ 71 | "itemID": "item34", 72 | }, 73 | }, 74 | { 75 | Route: newRouteWithNilHandler("GET", "/items/:itemID/:count"), 76 | Request: newRequest("GET", "/items/item34/83"), 77 | PathVariables: map[string]string{ 78 | "itemID": "item34", 79 | "count": "83", 80 | }, 81 | }, 82 | } 83 | 84 | for _, testCase := range testCases { 85 | router := NewBasicRouter([]*Route{testCase.Route}) 86 | 87 | match, err := router.Match(testCase.Request) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | if v, want := match.PathVariables, testCase.PathVariables; !reflect.DeepEqual(v, want) { 93 | t.Errorf("\nExpected: %#v \nReceived: %#v", want, v) 94 | } 95 | } 96 | } 97 | 98 | func TestNilMatch(t *testing.T) { 99 | testCases := []struct { 100 | Route *Route 101 | Request *http.Request 102 | }{ 103 | { 104 | Route: newRouteWithNilHandler("GET", "/"), 105 | Request: newRequest("PUT", "/"), 106 | }, 107 | { 108 | Route: newRouteWithNilHandler("GET", "/items"), 109 | Request: newRequest("GET", "/itemss"), 110 | }, 111 | { 112 | Route: newRouteWithNilHandler("GET", "/items/:itemID"), 113 | Request: newRequest("GET", "/items/item34/other"), 114 | }, 115 | } 116 | 117 | for _, testCase := range testCases { 118 | router := NewBasicRouter([]*Route{testCase.Route}) 119 | 120 | match, err := router.Match(testCase.Request) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | if match != nil { 126 | t.Errorf("Error on Route '%s': Match was not nil", testCase.Route.Path) 127 | } 128 | } 129 | } 130 | 131 | func TestConcurrentAccess(t *testing.T) { 132 | rand.Seed(time.Now().UnixNano()) 133 | 134 | router := NewBasicRouter([]*Route{ 135 | newRouteWithNilHandler("GET", "/"), 136 | newRouteWithNilHandler("PUT", "/"), 137 | newRouteWithNilHandler("POST", "/"), 138 | newRouteWithNilHandler("DELETE", "/"), 139 | }) 140 | 141 | requests := []*http.Request{ 142 | newRequest("GET", "/"), 143 | newRequest("PUT", "/"), 144 | newRequest("POST", "/"), 145 | newRequest("DELETE", "/"), 146 | } 147 | 148 | numCalls := 0 149 | done := make(chan bool) 150 | for i := 0; i < 5; i++ { 151 | go func() { 152 | for { 153 | select { 154 | case <-done: 155 | return 156 | default: 157 | i := rand.Int() % len(requests) 158 | req := requests[i] 159 | router.Match(req) 160 | numCalls++ 161 | } 162 | } 163 | }() 164 | } 165 | 166 | for numCalls < 1000 { 167 | } 168 | 169 | close(done) 170 | } 171 | -------------------------------------------------------------------------------- /test_helpers.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | ) 9 | 10 | func RecordJSONResponse(t *testing.T, resp Response, v interface{}) *httptest.ResponseRecorder { 11 | recorder := httptest.NewRecorder() 12 | resp.Write(recorder, nil) 13 | if v != nil { 14 | if err := json.Unmarshal(recorder.Body.Bytes(), v); err != nil { 15 | t.Fatal(err) 16 | } 17 | } 18 | 19 | return recorder 20 | } 21 | -------------------------------------------------------------------------------- /testing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Title 5 | 6 | 7 | {{ template "partials/_header" }} 8 |

Hello, World!

9 | {{ template "partials/_footer" }} 10 | 11 | 12 | -------------------------------------------------------------------------------- /testing/partials/_footer.html: -------------------------------------------------------------------------------- 1 | {{ define "_footer" }}

Footer

{{ end }} 2 | -------------------------------------------------------------------------------- /testing/partials/_header.html: -------------------------------------------------------------------------------- 1 | {{ define "_header" }}

Header

{{ end }} 2 | -------------------------------------------------------------------------------- /utilities.go: -------------------------------------------------------------------------------- 1 | package fireball 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Redirect wraps http.Redirect in a ResponseFunc 8 | func Redirect(status int, url string) Response { 9 | return ResponseFunc(func(w http.ResponseWriter, r *http.Request) { 10 | http.Redirect(w, r, url, status) 11 | }) 12 | } 13 | 14 | // EnableCORS decorates each route by adding CORS headers to each response 15 | // An OPTIONS Handler is added to each route if one doesn't already exist 16 | func EnableCORS(routes []*Route) []*Route { 17 | decorated := Decorate(routes, HeaderResponseDecorator(CORSHeaders)) 18 | 19 | for _, route := range decorated { 20 | if _, exists := route.Handlers["OPTIONS"]; !exists { 21 | route.Handlers["OPTIONS"] = func(c *Context) (Response, error) { 22 | return NewResponse(200, nil, CORSHeaders), nil 23 | } 24 | } 25 | } 26 | 27 | return decorated 28 | } 29 | --------------------------------------------------------------------------------