├── example ├── website │ ├── routes.yaml │ ├── routes.go │ ├── generate.go │ └── methods.go └── server.go ├── router ├── routes.yaml ├── generate.go ├── errors.go ├── params_test.go ├── util.go ├── params.go ├── routes.go ├── router_test.go └── router.go ├── README.md ├── routify.go ├── LICENSE └── logo.svg /example/website/routes.yaml: -------------------------------------------------------------------------------- 1 | GET: 2 | /: index 3 | hello/$str: hello 4 | printnum/$num: printnum 5 | 6 | params: 7 | $num: validateNumber -------------------------------------------------------------------------------- /router/routes.yaml: -------------------------------------------------------------------------------- 1 | GET: 2 | /: exampleHandler 3 | nofunc/$a/$b/$c/$d/$e/$f/$g/$h/$i/$j/$k/$l/$m/$n/$o/$p/$q/$r/$s/$t/$u: exampleHandler 4 | static/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u: exampleHandler 5 | schemas/$schema/archives/$year/$month/$day: exampleHandler 6 | schemas/$schema: exampleHandler 7 | testing/hello/world: exampleHandler 8 | 9 | schemas/$schema: 10 | POST: exampleHandler 11 | PUT: exampleHandler 12 | DELETE: exampleHandler 13 | PATCH: exampleHandler 14 | 15 | params: 16 | $year: IsYear 17 | $month: IsMonth 18 | $day: IsDay -------------------------------------------------------------------------------- /example/website/routes.go: -------------------------------------------------------------------------------- 1 | package website 2 | 3 | import "github.com/martingallagher/routify/router" 4 | 5 | var Routes = &router.Router{ 6 | Routes: router.Routes{ 7 | "GET": &router.Route{ 8 | Children: router.Routes{ 9 | "hello": &router.Route{ 10 | Child: &router.Route{ 11 | Param: "str", 12 | HandlerFunc: hello, 13 | }, 14 | }, 15 | "printnum": &router.Route{ 16 | Child: &router.Route{ 17 | Param: "num", 18 | Check: validateNumber, 19 | HandlerFunc: printnum, 20 | }, 21 | }, 22 | "/": &router.Route{ 23 | HandlerFunc: index, 24 | }, 25 | }, 26 | }, 27 | }, 28 | Validators: router.Validators{ 29 | "$num": validateNumber, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /example/website/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate routify -p website -v Routes 16 | //go:generate gofmt -w -s routes.go 17 | 18 | package website 19 | -------------------------------------------------------------------------------- /router/generate.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate routify -i routes.yaml -p router -v routes 16 | // Strip import for in-package testing 17 | //go:generate sed -i "s/^.*import.*//g" routes.go 18 | // Strip package identifiers 19 | //go:generate sed -i "s/router\\.//g" routes.go 20 | // Format 21 | //go:generate gofmt -w -s routes.go 22 | 23 | package router 24 | -------------------------------------------------------------------------------- /example/server.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "log" 19 | "net/http" 20 | "os" 21 | "os/signal" 22 | "syscall" 23 | "time" 24 | 25 | "./website" 26 | ) 27 | 28 | func main() { 29 | addr := "0.0.0.0:1337" 30 | server := &http.Server{ 31 | Addr: addr, 32 | Handler: website.Routes, 33 | ReadTimeout: 5 * time.Second, 34 | WriteTimeout: 5 * time.Second, 35 | } 36 | 37 | // Fire up goroutine so we can capture signals 38 | go func() { 39 | log.Fatal(server.ListenAndServe()) 40 | }() 41 | 42 | log.Printf("server started: http://%s", addr) 43 | 44 | // Capture signals e.g. CTRL+C 45 | sig := make(chan os.Signal, 1) 46 | 47 | signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 48 | 49 | // Wait for signal 50 | <-sig 51 | } 52 | -------------------------------------------------------------------------------- /example/website/methods.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package website 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | 21 | "github.com/martingallagher/routify/router" 22 | ) 23 | 24 | func index(w http.ResponseWriter, r *http.Request, p router.Params) { 25 | w.Write([]byte("Welcome! Please try /hello/$name or /printnum/123")) 26 | } 27 | 28 | func hello(w http.ResponseWriter, r *http.Request, p router.Params) { 29 | fmt.Fprintf(w, "Hello %s, have a good day!", p.Get("str")) 30 | } 31 | 32 | func printnum(w http.ResponseWriter, r *http.Request, p router.Params) { 33 | fmt.Fprintf(w, "printnum() = %s", p.Get("num")) 34 | } 35 | 36 | func validateNumber(s string) bool { 37 | if s == "" { 38 | return false 39 | } 40 | 41 | for _, r := range s { 42 | if r < '0' || r > '9' { 43 | return false 44 | } 45 | } 46 | 47 | return true 48 | } 49 | -------------------------------------------------------------------------------- /router/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | import "net/http" 18 | 19 | var ( 20 | // ErrInvalidMethod represents an invalid HTTP method. 21 | ErrInvalidMethod = NewError(http.StatusMethodNotAllowed, "invalid HTTP method") 22 | // ErrRouteNotFound represents HTTP 404. 23 | ErrRouteNotFound = NewError(http.StatusNotFound, "route not found") 24 | // ErrBadRequest represents HTTP 400. 25 | ErrBadRequest = NewError(http.StatusBadRequest, "bad request") 26 | ) 27 | 28 | // Error represents a routing error. 29 | type Error struct { 30 | code int 31 | err string 32 | } 33 | 34 | // StatusCode returns the HTTP status code 35 | // associated with the error. 36 | func (e *Error) StatusCode() int { 37 | return e.code 38 | } 39 | 40 | // Error returns the error string. 41 | func (e *Error) Error() string { 42 | return e.err 43 | } 44 | 45 | // NewError returns a new error with 46 | // the given HTTP status code and error message. 47 | func NewError(c int, s string) *Error { 48 | return &Error{c, s} 49 | } 50 | -------------------------------------------------------------------------------- /router/params_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | import ( 18 | "errors" 19 | "net/http" 20 | "strconv" 21 | "testing" 22 | "time" 23 | ) 24 | 25 | type month time.Month 26 | 27 | func (m *month) Scan(i interface{}) error { 28 | v, ok := i.(string) 29 | 30 | if !ok { 31 | return errors.New("unsupported type") 32 | } 33 | 34 | c, err := strconv.Atoi(v) 35 | 36 | if err != nil { 37 | return err 38 | } else if c < 0 || c > 12 { 39 | return errors.New("month out of bounds") 40 | } 41 | 42 | *m = month(c) 43 | 44 | return nil 45 | } 46 | 47 | func TestParams(t *testing.T) { 48 | req, err := http.NewRequest("GET", shortParam, nil) 49 | 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | _, p, _ := routes.Get(req) 55 | 56 | if v, err := p.GetInt("year"); err != nil { 57 | t.Fatal(err) 58 | } else if v != 2015 { 59 | t.Fatal("unexpected value") 60 | } 61 | 62 | var b []byte 63 | 64 | if err := p.Scan("day", &b); err != nil { 65 | t.Fatal(err) 66 | } else if string(b) != "12" { 67 | t.Fatal("unexpected value") 68 | } 69 | 70 | var m month 71 | 72 | if err := p.Scan("month", &m); err != nil { 73 | t.Fatal(err) 74 | } else if time.Month(m).String() != "February" { 75 | t.Fatal("unexpected value") 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /router/util.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | // IsYear tests if the string is a valid year (YYYY). 18 | func IsYear(s string) bool { 19 | return len(s) == 4 && 20 | s[0] > 47 || s[0] < 58 && 21 | s[1] > 47 || s[1] < 58 && 22 | s[2] > 47 || s[2] < 58 && 23 | s[3] > 47 || s[3] < 58 24 | } 25 | 26 | // IsMonth tests if the string is a valid month (MM). 27 | func IsMonth(s string) bool { 28 | if len(s) != 2 || s[0] > 49 || s[1] > 57 || s[0] == 48 && s[1] == 48 { 29 | return false 30 | } 31 | 32 | return s[0] == 48 || s[1] < 51 33 | } 34 | 35 | // IsDay tests if the string is a valid day (DD). 36 | func IsDay(s string) bool { 37 | if len(s) != 2 || s[0] > 51 || s[1] > 57 || s[0] == 48 && s[1] == 48 { 38 | return false 39 | } 40 | 41 | return s[0] < 51 || s[1] < 50 42 | } 43 | 44 | func staticPath(p []string) (string, int) { 45 | for _, v := range p { 46 | if v == "" || v[0] == ':' || v[0] == '$' { 47 | return p[0], 0 48 | } 49 | } 50 | 51 | s := "" 52 | c := -1 53 | 54 | for i, v := range p { 55 | if v == "" || v[0] == ':' || v[0] == '$' { 56 | break 57 | } 58 | 59 | if i > 0 { 60 | s += "/" 61 | } 62 | 63 | s += v 64 | c++ 65 | } 66 | 67 | return s, c 68 | } 69 | 70 | func stripSlashes(s string) string { 71 | l := len(s) - 1 72 | 73 | if l < 1 { 74 | return "" 75 | } 76 | 77 | // Head 78 | if s[0] == '/' { 79 | l-- 80 | s = s[1:] 81 | } 82 | 83 | // Tail 84 | if l > 1 && s[l] == '/' { 85 | s = s[:l] 86 | } 87 | 88 | return s 89 | } 90 | -------------------------------------------------------------------------------- /router/params.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | import ( 18 | "errors" 19 | "reflect" 20 | "strconv" 21 | ) 22 | 23 | var ( 24 | // ErrUnsupportedType - unsupported variable destination types. 25 | ErrUnsupportedType = errors.New("unsupported destination type") 26 | // ErrValueDestination - destination isn't a pointer. 27 | ErrValueDestination = errors.New("destination is a value, not a pointer") 28 | ) 29 | 30 | type param struct{ k, v string } 31 | 32 | // Params contains the parsed URL parameters. 33 | type Params []param 34 | 35 | // Get returns the parameter value for the given key. 36 | func (p Params) Get(k string) string { 37 | for _, c := range p { 38 | if c.k == k { 39 | return c.v 40 | } 41 | } 42 | 43 | return "" 44 | } 45 | 46 | // GetInt attempts to get the given key as int64. 47 | func (p Params) GetInt(k string) (int64, error) { 48 | v := p.Get(k) 49 | 50 | if v == "" { 51 | return 0, nil 52 | } 53 | 54 | return strconv.ParseInt(v, 10, 64) 55 | } 56 | 57 | // GetUint attempts to get the given key as uint64. 58 | func (p Params) GetUint(k string) (uint64, error) { 59 | v := p.Get(k) 60 | 61 | if v == "" { 62 | return 0, nil 63 | } 64 | 65 | return strconv.ParseUint(v, 10, 64) 66 | } 67 | 68 | // Scan implements the Scanner interface. 69 | func (p Params) Scan(k string, dst interface{}) error { 70 | switch v := dst.(type) { 71 | case *string: 72 | *v = p.Get(k) 73 | 74 | return nil 75 | 76 | case *[]byte: 77 | *v = []byte(p.Get(k)) 78 | 79 | return nil 80 | } 81 | 82 | dpv := reflect.ValueOf(dst) 83 | 84 | if dpv.Kind() != reflect.Ptr { 85 | return ErrValueDestination 86 | } 87 | 88 | dv := reflect.Indirect(dpv) 89 | 90 | switch dv.Kind() { 91 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 92 | c, err := p.GetInt(k) 93 | 94 | if err != nil { 95 | return err 96 | } 97 | 98 | dv.SetInt(c) 99 | 100 | return nil 101 | 102 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 103 | c, err := p.GetUint(k) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | dv.SetUint(c) 110 | 111 | return nil 112 | 113 | case reflect.Float32, reflect.Float64: 114 | f, err := strconv.ParseFloat(p.Get(k), dv.Type().Bits()) 115 | 116 | if err != nil { 117 | return err 118 | } 119 | 120 | dv.SetFloat(f) 121 | 122 | return nil 123 | } 124 | 125 | // Utilize Scanner interface 126 | if s, ok := dst.(Scanner); ok { 127 | return s.Scan(p.Get(k)) 128 | } 129 | 130 | return ErrUnsupportedType 131 | } 132 | 133 | // Scanner is an interface used by Scan. 134 | type Scanner interface { 135 | // An error should be returned if the value can not be stored 136 | // without loss of information. 137 | Scan(src interface{}) error 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Routify](http://praegress.us/routify-logo.png) 2 | A usable, efficient and simple router implementation for Go. 3 | 4 | ## Overview 5 | Routify is a route generation tool and routing package. The primary goal is to provide a simple and usable way of handling routing in Go. The router's performance and garbage creation overhead is comparable to other Go routing packages, but at present this isn't a primary goal - it's void of exotic data structures. 6 | 7 | ## Installation 8 | go get -u github.com/martingallagher/routify 9 | 10 | The following assumes your Go $GOPATH/bin is on your $PATH environmental variable (`export PATH=$PATH:$GOPATH/bin`). 11 | 12 | ## Routing Example 13 | Routes are defined in a simple YAML file. For example: 14 | 15 | ```yaml 16 | # Method -> Route format 17 | GET: 18 | /: indexHandler 19 | blog: blogHandler 20 | blog/$year: blogArchiveHandler 21 | blog/$year/$month: blogArchiveHandler 22 | blog/$year/$month/$day: blogArchiveHandler 23 | 24 | # Route -> Method format 25 | blog/post: 26 | POST: newPost 27 | DELETE: deletePost 28 | 29 | # Params defines URL paramters to be captured and validated. 30 | # URL components prefixed with "$" with no matching validation function 31 | # will be captured but not validated. 32 | params: 33 | $year: IsYear 34 | $month: IsMonth 35 | $day: IsDay 36 | ``` 37 | 38 | # Traditional Routing 39 | ```go 40 | r := &Router{} 41 | r.AddValidator(":year", router.IsYear) 42 | r.AddValidator(":month", router.IsMonth) 43 | r.AddValidator(":day", router.IsDay) 44 | 45 | if err := r.Add("GET", "blog/archives/:year/:month/:day", blogArchivesHandler); err != nil { 46 | // Handle error 47 | } 48 | ``` 49 | 50 | # Accessing Parameters 51 | ```go 52 | _, params, err := routes.Get(r) // Handle error 53 | 54 | // String value, if not found returns empty string 55 | v := params.Get("year") 56 | 57 | // Integer helpers 58 | v, err := params.GetInt("year") // int64 59 | v, err := params.GetUint("id") // uint64 60 | 61 | // Scanner interface 62 | type month time.Month 63 | 64 | func (m *month) Scan(i interface{}) error { 65 | v, ok := i.(string) 66 | 67 | if !ok { 68 | return errors.New("unsupported type") 69 | } 70 | 71 | c, err := strconv.Atoi(v) 72 | 73 | if err != nil { 74 | return err 75 | } else if c < 0 || c > 12 { 76 | return errors.New("month out of bounds") 77 | } 78 | 79 | *m = month(c) 80 | 81 | return nil 82 | } 83 | 84 | // Scan example 85 | var m month 86 | 87 | err := params.Scan("month", &m) 88 | 89 | // Assuming month=02 90 | fmt.Println(time.Month(m).String() == "February") 91 | ``` 92 | 93 | # `routify` Command Line Tool 94 | The routify tool generates the Go routes file. Run `routify -h` for full options. 95 | 96 | **Example:** 97 | 98 | `routify -i routes.yaml -p blog -v routes` 99 | 100 | ## Using `go generate` 101 | Routify works great in tandem with `go generate`, making route generation easy with the standard Go tools. 102 | 103 | **Example `generate.go` file:** 104 | 105 | ```go 106 | // Generate routes.go for the blog package 107 | //go:generate routify -i routes.yaml -p blog 108 | // gofmt the routify Go output 109 | //go:generate gofmt -w -s routes.go 110 | 111 | package blog 112 | ``` 113 | 114 | # Server Example 115 | A simple reference example is available in the `example` directory. 116 | 117 | # Contributions 118 | Bug fixes and feature requests welcome. 119 | 120 | # Contributors 121 | - [Martin Gallagher](http://martingallagher.com/) -------------------------------------------------------------------------------- /router/routes.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | var routes = &Router{ 4 | Routes: Routes{ 5 | "DELETE": &Route{ 6 | Children: Routes{ 7 | "schemas": &Route{ 8 | Child: &Route{ 9 | Param: "schema", 10 | HandlerFunc: exampleHandler, 11 | }, 12 | }, 13 | }, 14 | }, 15 | "PATCH": &Route{ 16 | Children: Routes{ 17 | "schemas": &Route{ 18 | Child: &Route{ 19 | Param: "schema", 20 | HandlerFunc: exampleHandler, 21 | }, 22 | }, 23 | }, 24 | }, 25 | "POST": &Route{ 26 | Children: Routes{ 27 | "schemas": &Route{ 28 | Child: &Route{ 29 | Param: "schema", 30 | HandlerFunc: exampleHandler, 31 | }, 32 | }, 33 | }, 34 | }, 35 | "PUT": &Route{ 36 | Children: Routes{ 37 | "schemas": &Route{ 38 | Child: &Route{ 39 | Param: "schema", 40 | HandlerFunc: exampleHandler, 41 | }, 42 | }, 43 | }, 44 | }, 45 | "GET": &Route{ 46 | Children: Routes{ 47 | "nofunc": &Route{ 48 | Child: &Route{ 49 | Param: "a", 50 | Child: &Route{ 51 | Param: "b", 52 | Child: &Route{ 53 | Param: "c", 54 | Child: &Route{ 55 | Param: "d", 56 | Child: &Route{ 57 | Param: "e", 58 | Child: &Route{ 59 | Param: "f", 60 | Child: &Route{ 61 | Param: "g", 62 | Child: &Route{ 63 | Param: "h", 64 | Child: &Route{ 65 | Param: "i", 66 | Child: &Route{ 67 | Param: "j", 68 | Child: &Route{ 69 | Param: "k", 70 | Child: &Route{ 71 | Param: "l", 72 | Child: &Route{ 73 | Param: "m", 74 | Child: &Route{ 75 | Param: "n", 76 | Child: &Route{ 77 | Param: "o", 78 | Child: &Route{ 79 | Param: "p", 80 | Child: &Route{ 81 | Param: "q", 82 | Child: &Route{ 83 | Param: "r", 84 | Child: &Route{ 85 | Param: "s", 86 | Child: &Route{ 87 | Param: "t", 88 | Child: &Route{ 89 | Param: "u", 90 | HandlerFunc: exampleHandler, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | }, 113 | "static/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u": &Route{ 114 | HandlerFunc: exampleHandler, 115 | }, 116 | "schemas": &Route{ 117 | Child: &Route{ 118 | Param: "schema", 119 | HandlerFunc: exampleHandler, 120 | Children: Routes{ 121 | "archives": &Route{ 122 | Child: &Route{ 123 | Param: "year", 124 | Check: IsYear, 125 | Child: &Route{ 126 | Param: "month", 127 | Check: IsMonth, 128 | Child: &Route{ 129 | Param: "day", 130 | Check: IsDay, 131 | HandlerFunc: exampleHandler, 132 | }, 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | "testing/hello/world": &Route{ 140 | HandlerFunc: exampleHandler, 141 | }, 142 | "/": &Route{ 143 | HandlerFunc: exampleHandler, 144 | }, 145 | }, 146 | }, 147 | }, 148 | Validators: Validators{ 149 | "year": IsYear, 150 | "month": IsMonth, 151 | "day": IsDay, 152 | }, 153 | } 154 | -------------------------------------------------------------------------------- /router/router_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | import ( 18 | "net/http" 19 | "testing" 20 | ) 21 | 22 | const ( 23 | longStatic = "/static/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u" 24 | shortParam = "/schemas/test/archives/2015/02/12" 25 | longParam = "/nofunc/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u" 26 | ) 27 | 28 | var testData = []struct{ method, route, url, param, value string }{ 29 | {"GET", "/schemas/:schema/archives/:year/:month/:day", shortParam, "year", "2015"}, 30 | {"POST", "/authorizations/", "/authorizations/", "", ""}, 31 | {"POST", "/authorizations/:id", "/authorizations/123", "id", "123"}, 32 | {"GET", "/repos/$owner/$repo/events", "/repos/1/2/events", "owner", "1"}, 33 | {"GET", "/users/:user/received_events/public", "/users/1/received_events/public", "user", "1"}, 34 | {"GET", "/users/:user/events/orgs/:org", "/users/1/events/orgs/praegressus", "org", "praegressus"}, 35 | } 36 | 37 | func TestRuntimeRouter(t *testing.T) { 38 | r := &Router{} 39 | r.AddValidator(":year", IsYear) 40 | r.AddValidator(":month", IsMonth) 41 | r.AddValidator(":day", IsDay) 42 | 43 | for _, c := range testData { 44 | if err := r.Add(c.method, c.route, exampleHandler); err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | req, err := http.NewRequest(c.method, c.url, nil) 49 | 50 | if err != nil { 51 | t.Fatal(err) 52 | } else if h, p, err := r.Get(req); err != nil { 53 | t.Fatal(err) 54 | } else if h == nil { 55 | t.Fatal("nil handler") 56 | } else if c.param != "" && p.Get(c.param) != c.value { 57 | t.Fatal("unexpected value") 58 | } 59 | } 60 | } 61 | 62 | func TestRouter(t *testing.T) { 63 | req, err := http.NewRequest("GET", shortParam, nil) 64 | 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | h, p, err := routes.Get(req) 70 | 71 | if err != nil { 72 | t.Fatal(err) 73 | } else if len(p) == 0 { 74 | t.Fatal("empty params") 75 | } else if h == nil { 76 | t.Fatal("nil handler") 77 | } 78 | 79 | t.Logf("%#v", p) 80 | } 81 | 82 | func TestRouterLongSimple(t *testing.T) { 83 | req, err := http.NewRequest("GET", longParam, nil) 84 | 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | h, p, err := routes.Get(req) 90 | 91 | if err != nil { 92 | t.Fatal(err) 93 | } else if len(p) == 0 || p.Get("g") == "" { 94 | t.Fatal("empty params") 95 | } else if h == nil { 96 | t.Fatal("nil handler") 97 | } 98 | 99 | t.Logf("%#v", p) 100 | } 101 | 102 | func TestRouterLongStatic(t *testing.T) { 103 | req, err := http.NewRequest("GET", longStatic, nil) 104 | 105 | if err != nil { 106 | t.Fatal(err) 107 | } else if h, _, err := routes.Get(req); err != nil { 108 | t.Fatal(err) 109 | } else if h == nil { 110 | t.Fatal("nil handler") 111 | } 112 | } 113 | 114 | func BenchmarkRouterLongSimple(b *testing.B) { 115 | req, err := http.NewRequest("GET", longParam, nil) 116 | 117 | if err != nil { 118 | b.Fatal(err) 119 | } 120 | 121 | b.ReportAllocs() 122 | 123 | for i := 0; i < b.N; i++ { 124 | routes.Get(req) 125 | } 126 | } 127 | 128 | func BenchmarkRouterLongStatic(b *testing.B) { 129 | req, err := http.NewRequest("GET", longStatic, nil) 130 | 131 | if err != nil { 132 | b.Fatal(err) 133 | } 134 | 135 | b.ReportAllocs() 136 | 137 | for i := 0; i < b.N; i++ { 138 | routes.Get(req) 139 | } 140 | } 141 | 142 | func BenchmarkRouterMultiParam(b *testing.B) { 143 | req, err := http.NewRequest("GET", shortParam, nil) 144 | 145 | if err != nil { 146 | b.Fatal(err) 147 | } 148 | 149 | b.ReportAllocs() 150 | 151 | for i := 0; i < b.N; i++ { 152 | routes.Get(req) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /router/router.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package router 16 | 17 | import ( 18 | "errors" 19 | "net/http" 20 | "strings" 21 | ) 22 | 23 | var ( 24 | // ErrInvalidPath - unable to construct route due to invalid / empty values. 25 | ErrInvalidRoute = errors.New("invalid route") 26 | // ErrInvalidPath - URL parse error; invalid route path. 27 | ErrInvalidPath = errors.New("invalid route path") 28 | ) 29 | 30 | // HandlerFunc defines the interface for 31 | // routify functions, identical to http.HandlerFunc 32 | // except that it includes the parsed URL parameters. 33 | type HandlerFunc func(http.ResponseWriter, *http.Request, Params) 34 | 35 | // Router represents the defined routes and parameter validators. 36 | type Router struct { 37 | Routes Routes 38 | Validators map[string]func(string) bool 39 | } 40 | 41 | // Routes holds static route mappings. 42 | type Routes map[string]*Route 43 | 44 | // Validators holds parameter validating functions. 45 | type Validators map[string]func(string) bool 46 | 47 | // Route represents an individual route/end-point. 48 | type Route struct { 49 | Param string // Parameter name 50 | Check func(string) bool // Function to check if section is valid 51 | HandlerFunc HandlerFunc // Handler function use to serve 52 | Child *Route // Child route (parameter capture) 53 | Children Routes // Child map (static paths) 54 | } 55 | 56 | // Get attempts to get a route for the given request. 57 | func (r *Router) Get(req *http.Request) (HandlerFunc, Params, error) { 58 | u := req.URL.Path 59 | 60 | if u == "" { 61 | return nil, nil, ErrBadRequest 62 | } 63 | 64 | route, exists := r.Routes[req.Method] 65 | 66 | if !exists { 67 | return nil, nil, ErrInvalidMethod 68 | } else if u == "/" { 69 | if route, exists = route.Children["/"]; exists && route.HandlerFunc != nil { 70 | return route.HandlerFunc, nil, nil 71 | } 72 | 73 | return nil, nil, ErrRouteNotFound 74 | } 75 | 76 | u = stripSlashes(u) 77 | 78 | // Exit early for full static match 79 | if v, exists := route.Children[u]; exists { 80 | if v.HandlerFunc == nil { 81 | return nil, nil, ErrRouteNotFound 82 | } 83 | 84 | return v.HandlerFunc, nil, nil 85 | } 86 | 87 | var p Params 88 | 89 | for { 90 | // Early exit for optimized paths 91 | if v, exists := route.Children[u]; exists { 92 | if v.HandlerFunc != nil { 93 | return v.HandlerFunc, p, nil 94 | } 95 | 96 | route = v 97 | 98 | continue 99 | } 100 | 101 | s := u 102 | i := strings.IndexByte(u, '/') 103 | 104 | if i != -1 { 105 | s = u[:i] 106 | u = u[i+1:] 107 | } 108 | 109 | if route.Child != nil { 110 | // Capture parameter 111 | route = route.Child 112 | 113 | if route.Check != nil && !route.Check(s) { 114 | return nil, nil, ErrRouteNotFound 115 | } 116 | 117 | p = append(p, param{route.Param, s}) 118 | } else if route, exists = route.Children[s]; !exists { 119 | // Static 120 | return nil, nil, ErrRouteNotFound 121 | } 122 | 123 | if i == -1 { 124 | break 125 | } 126 | } 127 | 128 | if route == nil || route.HandlerFunc == nil { 129 | return nil, nil, ErrRouteNotFound 130 | } 131 | 132 | return route.HandlerFunc, p, nil 133 | } 134 | 135 | // Add adds a route for the given method to the routes map. 136 | func (r *Router) Add(m, u string, h HandlerFunc) error { 137 | if m == "" || u == "" || h == nil { 138 | return ErrInvalidRoute 139 | } 140 | 141 | m = strings.ToUpper(m) 142 | 143 | var c *Route 144 | 145 | if r.Routes == nil { 146 | r.Routes = Routes{m: &Route{}} 147 | } else if _, exists := r.Routes[m]; !exists { 148 | r.Routes[m] = &Route{} 149 | } 150 | 151 | c = r.Routes[m] 152 | 153 | var p []string 154 | 155 | if u != "/" { 156 | p = strings.Split(stripSlashes(u), "/") 157 | } else { 158 | p = []string{"/"} 159 | } 160 | 161 | for i, l := 0, len(p); i < l; i++ { 162 | if p[i] == "" { 163 | return ErrInvalidPath 164 | } 165 | 166 | // Parameter 167 | if p[i][0] == ':' || p[i][0] == '$' { 168 | if c.Child == nil || c.Child.Param != p[i][1:] { 169 | c.Child = &Route{ 170 | Param: p[i][1:], 171 | Check: r.Validators[p[i][1:]], 172 | } 173 | } 174 | 175 | c = c.Child 176 | 177 | continue 178 | } 179 | 180 | v, n := staticPath(p[i:]) 181 | i += n 182 | 183 | if c.Children == nil { 184 | c.Children = Routes{v: &Route{}} 185 | } else if _, exists := c.Children[v]; !exists { 186 | c.Children[v] = &Route{} 187 | } 188 | 189 | c = c.Children[v] 190 | } 191 | 192 | c.HandlerFunc = h 193 | 194 | return nil 195 | } 196 | 197 | // AddValidator adds a validating function to 198 | // the validators map. 199 | func (r *Router) AddValidator(n string, f func(string) bool) { 200 | if n == "" { 201 | return 202 | } else if n[0] == ':' || n[0] == '$' { 203 | n = n[1:] 204 | } 205 | 206 | if r.Validators == nil { 207 | r.Validators = Validators{n: f} 208 | } else { 209 | r.Validators[n] = f 210 | } 211 | } 212 | 213 | // ServeHTTP implements the Handler interface. 214 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 215 | h, p, err := r.Get(req) 216 | 217 | if err != nil { 218 | if e, ok := err.(*Error); ok { 219 | w.WriteHeader(e.code) 220 | } else { 221 | w.WriteHeader(http.StatusInternalServerError) 222 | } 223 | 224 | return 225 | } 226 | 227 | h(w, req, p) 228 | } 229 | 230 | // VOID handler for testing. 231 | func exampleHandler(w http.ResponseWriter, r *http.Request, p Params) { 232 | } 233 | -------------------------------------------------------------------------------- /routify.go: -------------------------------------------------------------------------------- 1 | // Copyright Praegressus Limited. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "flag" 20 | "fmt" 21 | "io/ioutil" 22 | "log" 23 | "os" 24 | "strings" 25 | 26 | "github.com/martingallagher/routify/router" 27 | "gopkg.in/yaml.v2" 28 | ) 29 | 30 | var ( 31 | inputFile = flag.String("i", "routes.yaml", "Routes input file") 32 | outputFile = flag.String("o", "routes.go", "Routes output file") 33 | packageName = flag.String("p", "", "Package name") 34 | varName = flag.String("v", "routes", "Variable name") 35 | errInvalidInput = errors.New("missing routes input file") 36 | ) 37 | 38 | type routemap map[string]*route 39 | 40 | type routes struct { 41 | params map[string]string 42 | routes routemap 43 | } 44 | 45 | type route struct { 46 | child *route 47 | children routemap 48 | param, check, handle string 49 | } 50 | 51 | func main() { 52 | flag.Parse() 53 | log.SetFlags(log.Lmicroseconds) 54 | 55 | if *inputFile == "" { 56 | log.Fatal("input filename is required (use -i flag)") 57 | } else if *packageName == "" { 58 | log.Fatal("package name is required (use -p flag)") 59 | } 60 | 61 | f, err := os.OpenFile(*outputFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 62 | 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | defer f.Close() 68 | 69 | r, err := loadRoutes() 70 | 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | fmt.Fprintf(f, `package %s 76 | 77 | import "github.com/martingallagher/routify/router" 78 | 79 | var %s = &router.Router{ 80 | Routes: router.Routes{ 81 | `, *packageName, *varName) 82 | 83 | for k, v := range r.routes { 84 | if len(v.children) == 0 { 85 | continue 86 | } 87 | 88 | r.writeRule(f, k, v) 89 | } 90 | 91 | f.WriteString("\n},") 92 | 93 | if len(r.params) > 0 { 94 | f.WriteString("\nValidators: router.Validators{\n") 95 | 96 | for k, v := range r.params { 97 | fmt.Fprintf(f, "\"%s\": %s,\n", k[1:], v) 98 | } 99 | 100 | f.WriteString("},") 101 | } 102 | 103 | f.WriteString("\n}") 104 | 105 | if err = f.Sync(); err != nil { 106 | log.Fatal(err) 107 | } 108 | } 109 | 110 | func staticPath(p []string) (string, int) { 111 | for _, v := range p { 112 | if v == "" || v[0] == ':' || v[0] == '$' { 113 | return p[0], 0 114 | } 115 | } 116 | 117 | s := "" 118 | c := -1 119 | 120 | for i, v := range p { 121 | if v[0] == ':' || v[0] == '$' { 122 | break 123 | } 124 | 125 | if i > 0 { 126 | s += "/" 127 | } 128 | 129 | s += v 130 | c++ 131 | } 132 | 133 | return s, c 134 | } 135 | 136 | func (r *routes) add(method, path, handle string) error { 137 | if _, exists := r.routes[method]; !exists { 138 | r.routes[method] = &route{children: routemap{}} 139 | } 140 | 141 | var ( 142 | p []string 143 | c = r.routes[method] 144 | ) 145 | 146 | if path != "/" { 147 | p = strings.Split(path, "/") 148 | } else { 149 | p = []string{"/"} 150 | } 151 | 152 | for i, l := 0, len(p); i < l; i++ { 153 | if p[i] == "" { 154 | return router.ErrInvalidPath 155 | } 156 | 157 | // Parameter 158 | if p[i][0] == '$' { 159 | if c.child == nil || c.child.param != p[i][1:] { 160 | c.child = &route{ 161 | param: p[i][1:], 162 | check: r.params[p[i]], 163 | children: routemap{}, 164 | } 165 | } 166 | 167 | c = c.child 168 | 169 | continue 170 | } 171 | 172 | v, n := staticPath(p[i:]) 173 | i += n 174 | 175 | // Allocate map for static routes 176 | if _, exists := c.children[v]; !exists { 177 | c.children[v] = &route{children: routemap{}} 178 | } 179 | 180 | c = c.children[v] 181 | } 182 | 183 | c.handle = handle 184 | 185 | return nil 186 | } 187 | 188 | func (r *routes) writeChild(f *os.File, c *route) { 189 | fmt.Fprintf(f, "Child: &router.Route{\nParam: \"%s\",\n", c.param) 190 | 191 | if c.check != "" { 192 | fmt.Fprintf(f, "Check: %s,\n", c.check) 193 | } 194 | 195 | if c.handle != "" { 196 | fmt.Fprintf(f, "HandlerFunc: %s,\n", c.handle) 197 | } 198 | 199 | if len(c.children) > 0 { 200 | r.writeChildren(f, c) 201 | } else if c.child != nil { 202 | r.writeChild(f, c.child) 203 | } 204 | 205 | f.WriteString("},\n") 206 | } 207 | 208 | func (r *routes) writeChildren(f *os.File, c *route) { 209 | f.WriteString("Children: router.Routes{\n") 210 | 211 | for k, v := range c.children { 212 | r.writeRule(f, k, v) 213 | } 214 | 215 | f.WriteString("},\n") 216 | } 217 | 218 | func (r *routes) writeRule(f *os.File, p string, c *route) { 219 | fmt.Fprintf(f, "\"%s\": &router.Route{\n", p) 220 | 221 | if c.handle != "" { 222 | fmt.Fprintf(f, "HandlerFunc: %s,\n", c.handle) 223 | } 224 | 225 | if len(c.children) > 0 { 226 | r.writeChildren(f, c) 227 | } else if c.child != nil { 228 | r.writeChild(f, c.child) 229 | } 230 | 231 | f.WriteString("},\n") 232 | } 233 | 234 | func loadRoutes() (*routes, error) { 235 | f, err := os.Open(*inputFile) 236 | 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | defer f.Close() 242 | 243 | b, err := ioutil.ReadAll(f) 244 | 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | f.Close() 250 | 251 | var m map[string]interface{} 252 | 253 | if err = yaml.Unmarshal(b, &m); err != nil { 254 | return nil, err 255 | } 256 | 257 | var ( 258 | l [][]string 259 | r = &routes{map[string]string{}, routemap{}} 260 | ) 261 | 262 | for k, v := range m { 263 | p, ok := v.(map[interface{}]interface{}) 264 | 265 | if !ok { 266 | continue 267 | } 268 | 269 | switch u := strings.ToUpper(k); u { 270 | case "PARAMS", "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HELP": 271 | for a, b := range p { 272 | t, ok := a.(string) 273 | 274 | if !ok { 275 | break 276 | } 277 | 278 | f, ok := b.(string) 279 | 280 | if !ok { 281 | break 282 | } 283 | 284 | if u == "PARAMS" { 285 | r.params[t] = f 286 | 287 | continue 288 | } 289 | 290 | l = append(l, []string{u, t, f}) 291 | } 292 | 293 | default: 294 | for a, b := range p { 295 | t, ok := a.(string) 296 | 297 | if !ok { 298 | break 299 | } else if t != "GET" && t != "POST" && t != "PUT" && t != "PATCH" && t != "DELETE" && t != "OPTIONS" && t != "HELP" { 300 | continue 301 | } 302 | 303 | f, ok := b.(string) 304 | 305 | if !ok { 306 | continue 307 | } 308 | 309 | l = append(l, []string{t, k, f}) 310 | } 311 | } 312 | } 313 | 314 | for _, c := range l { 315 | if err = r.add(c[0], c[1], c[2]); err != nil { 316 | return nil, err 317 | } 318 | } 319 | 320 | return r, nil 321 | } 322 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 62 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | 135 | 136 | 137 | 138 | --------------------------------------------------------------------------------