diff --git a/HISTORY.md b/HISTORY.md index 95eed0fa6e..e604938d2b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -19,15 +19,16 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. -**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris@v11.2.0`. +**How to upgrade**: Open your command-line and execute this command: `go get github.com/kataras/iris@master`. +# Tu, 30 July 2019 | v11.2.3 -# We, 24 July 2019 | v11.2.1 +TODO: -- https://github.com/kataras/iris/issues/1298 -- https://github.com/kataras/iris/issues/1207 +- Different parameter types in the same path (done). +- [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) (in-progress) -## v11.2.2 +# We, 24 July 2019 | v11.2.2 Sessions as middleware: @@ -47,6 +48,11 @@ app.Get("/path", func(ctx iris.Context){ - Add `Session.Len() int` to return the total number of stored values/entries. - Make `Context.HTML` and `Context.Text` to accept an optional, variadic, `args ...interface{}` input arg(s) too. +## v11.1.1 + +- https://github.com/kataras/iris/issues/1298 +- https://github.com/kataras/iris/issues/1207 + # Tu, 23 July 2019 | v11.2.0 -Read about the new release at: https://dev.to/kataras/iris-version-11-2-released-22bc +Read about the new release at: https://www.facebook.com/iris.framework/posts/3276606095684693 diff --git a/README.md b/README.md index 05985b155f..42cf09976d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ Iris is a fast, simple yet fully featured and very efficient web framework for G Learn what [others say about Iris](https://iris-go.com/testimonials/) and **star** this github repository. -> Version 11.2 **released!** [Spread the news](https://dev.to/kataras/iris-version-11-2-released-22bc). +> Version 11.2 **released!** + +[![https://www.facebook.com/iris.framework/posts/3276606095684693](https://iris-go.com/images/iris-112-released.png)](https://www.facebook.com/iris.framework/posts/3276606095684693) ## Learning Iris @@ -53,9 +55,10 @@ For a more detailed technical documentation you can head over to our [godocs](ht ### Do you like to read while traveling? + Book cover + You can [request](https://bit.ly/iris-req-book) a PDF version and online access of the **E-Book** today and be participated in the development of Iris. -[![https://iris-go.com/images/iris-book-overview.png](https://iris-go.com/images/iris-book-overview.png)](https://bit.ly/iris-req-book) ## Contributing diff --git a/README_ZH.md b/README_ZH.md index f72149901f..bdfb98f3ed 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -8,7 +8,7 @@ Iris 是基于 Go 编写的一个快速,简单但功能齐全且非常高效 看看 [其他人如何评价 Iris](https://iris-go.com/testimonials/),同时欢迎各位点亮 **star**。 -> 新版本 11.2 发布! [散布消息](https://dev.to/kataras/iris-version-11-2-released-22bc). +> 新版本 11.2 发布! [散布消息](https://www.facebook.com/iris.framework/posts/3276606095684693). ## 学习 Iris diff --git a/VERSION b/VERSION index 1608649b8e..ead23276fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11.2.1:https://dev.to/kataras/iris-version-11-2-released-22bc \ No newline at end of file +11.2.3:https://github.com/kataras/iris/releases/tag/v11.2.3 \ No newline at end of file diff --git a/_examples/README.md b/_examples/README.md index f81d887321..8b4c742e0c 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -224,6 +224,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [Read JSON](http_request/read-json/main.go) * [Struct Validation](http_request/read-json-struct-validation/main.go) - [Read XML](http_request/read-xml/main.go) +- [Read YAML](http_request/read-yaml/main.go) **NEW** - [Read Form](http_request/read-form/main.go) - [Read Query](http_request/read-query/main.go) **NEW** - [Read Custom per type](http_request/read-custom-per-type/main.go) @@ -237,6 +238,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### How to Write to `context.ResponseWriter() http.ResponseWriter` +- [Content Negotiation](http_responsewriter/content-negotiation) **NEW** - [Write `valyala/quicktemplate` templates](http_responsewriter/quicktemplate) - [Write `shiyanhui/hero` templates](http_responsewriter/herotemplate) - [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go) diff --git a/_examples/README_ZH.md b/_examples/README_ZH.md index 98e927cf1f..8f86ac9f1c 100644 --- a/_examples/README_ZH.md +++ b/_examples/README_ZH.md @@ -330,6 +330,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [读取JSON](http_request/read-json/main.go) - [读取XML](http_request/read-xml/main.go) +- [读取YAML](http_request/read-yaml/main.go) **更新** - [读取Form](http_request/read-form/main.go) - [读取Query](http_request/read-query/main.go) **更新** - [读取每个类型的自定义结果Custom per type](http_request/read-custom-per-type/main.go) @@ -342,6 +343,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### 如何写入`context.ResponseWriter() http.ResponseWriter` +- [Content Negotiation](http_responsewriter/content-negotiation) **更新** - [`valyala/quicktemplate`模版](http_responsewriter/quicktemplate) - [`shiyanhui/hero`模版](http_responsewriter/herotemplate) - [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go) diff --git a/_examples/http_request/read-yaml/main.go b/_examples/http_request/read-yaml/main.go new file mode 100644 index 0000000000..f0098a1e9d --- /dev/null +++ b/_examples/http_request/read-yaml/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/kataras/iris" +) + +func newApp() *iris.Application { + app := iris.New() + app.Post("/", handler) + + return app +} + +// simple yaml stuff, read more at https://yaml.org/start.html +type product struct { + Invoice int `yaml:"invoice"` + Tax float32 `yaml:"tax"` + Total float32 `yaml:"total"` + Comments string `yaml:"comments"` +} + +func handler(ctx iris.Context) { + var p product + if err := ctx.ReadYAML(&p); err != nil { + ctx.StatusCode(iris.StatusBadRequest) + ctx.WriteString(err.Error()) + return + } + + ctx.Writef("Received: %#+v", p) +} + +func main() { + app := newApp() + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/http_request/read-yaml/main_test.go b/_examples/http_request/read-yaml/main_test.go new file mode 100644 index 0000000000..ab6b13a628 --- /dev/null +++ b/_examples/http_request/read-yaml/main_test.go @@ -0,0 +1,24 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestReadYAML(t *testing.T) { + app := newApp() + e := httptest.New(t, app) + + expectedResponse := `Received: main.product{Invoice:34843, Tax:251.42, Total:4443.52, Comments:"Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338."}` + send := `invoice: 34843 +tax : 251.42 +total: 4443.52 +comments: > + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338.` + + e.POST("/").WithHeader("Content-Type", "application/x-yaml").WithBytes([]byte(send)).Expect(). + Status(httptest.StatusOK).Body().Equal(expectedResponse) +} diff --git a/_examples/http_responsewriter/content-negotiation/main.go b/_examples/http_responsewriter/content-negotiation/main.go new file mode 100644 index 0000000000..7a61dbd044 --- /dev/null +++ b/_examples/http_responsewriter/content-negotiation/main.go @@ -0,0 +1,114 @@ +// Package main contains three different ways to render content based on the client's accepted. +package main + +import "github.com/kataras/iris" + +type testdata struct { + Name string `json:"name" xml:"Name"` + Age int `json:"age" xml:"Age"` +} + +func newApp() *iris.Application { + app := iris.New() + app.Logger().SetLevel("debug") + + // app.Use(func(ctx iris.Context) { + // requestedMime := ctx.URLParamDefault("type", "application/json") + // + // ctx.Negotiation().Accept.Override().MIME(requestedMime, nil) + // ctx.Next() + // }) + + app.Get("/resource", func(ctx iris.Context) { + data := testdata{ + Name: "test name", + Age: 26, + } + + // Server allows response only JSON and XML. These values + // are compared with the clients mime needs. Iris comes with default mime types responses + // but you can add a custom one by the `Negotiation().Mime(mime, content)` method, + // same for the "accept". + // You can also pass a custom ContentSelector(mime string) or ContentNegotiator to the + // `Context.Negotiate` method if you want to perform more advanced things. + // + // + // By-default the client accept mime is retrieved by the "Accept" header + // Indeed you can override or update it by `Negotiation().Accept.XXX` i.e + // ctx.Negotiation().Accept.Override().XML() + // + // All these values can change inside middlewares, the `Negotiation().Override()` and `.Accept.Override()` + // can override any previously set values. + // Order matters, if the client accepts anything (*/*) + // then the first prioritized mime's response data will be rendered. + ctx.Negotiation().JSON().XML() + // Accept-Charset vs: + ctx.Negotiation().Charset("utf-8", "iso-8859-7") + // Alternatively you can define the content/data per mime type + // anywhere in the handlers chain using the optional "v" variadic + // input argument of the Context.Negotiation().JSON,XML,YAML,Binary,Text,HTML(...) and e.t.c + // example (order matters): + // ctx.Negotiation().JSON(data).XML(data).Any("content for */*") + // ctx.Negotiate(nil) + + // if not nil passed in the `Context.Negotiate` method + // then it overrides any contents made by the negotitation builder above. + _, err := ctx.Negotiate(data) + if err != nil { + ctx.Writef("%v", err) + } + }) + + app.Get("/resource2", func(ctx iris.Context) { + jsonAndXML := testdata{ + Name: "test name", + Age: 26, + } + + // I prefer that one, as it gives me the freedom to modify + // response data per accepted mime content type on middlewares as well. + ctx.Negotiation(). + JSON(jsonAndXML). + XML(jsonAndXML). + HTML("

Test Name

Age 26

") + + ctx.Negotiate(nil) + }) + + app.Get("/resource3", func(ctx iris.Context) { + // If that line is missing and the requested + // mime type of content is */* or application/xml or application/json + // then 406 Not Acceptable http error code will be rendered instead. + // + // We also add the "gzip" algorithm as an option to encode + // resources on send. + ctx.Negotiation().JSON().XML().HTML().EncodingGzip() + + jsonAndXML := testdata{ + Name: "test name", + Age: 26, + } + + // Prefer that way instead of the '/resource2' above + // if "iris.N" is a static one and can be declared + // outside of a handler. + ctx.Negotiate(iris.N{ + // Text: for text/plain, + // Markdown: for text/mardown, + // Binary: for application/octet-stream, + // YAML: for application/x-yaml, + // JSONP: for application/javascript + // Other: for anything else, + JSON: jsonAndXML, // for application/json + XML: jsonAndXML, // for application/xml or text/xml + HTML: "

Test Name

Age 26

", // for text/html + }) + }) + + return app +} + +func main() { + app := newApp() + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/http_responsewriter/content-negotiation/main_test.go b/_examples/http_responsewriter/content-negotiation/main_test.go new file mode 100644 index 0000000000..0c69a09278 --- /dev/null +++ b/_examples/http_responsewriter/content-negotiation/main_test.go @@ -0,0 +1,78 @@ +package main + +import ( + "bytes" + "compress/gzip" + "encoding/xml" + "io/ioutil" + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestContentNegotiation(t *testing.T) { + var ( + expectedJSONResponse = testdata{ + Name: "test name", + Age: 26, + } + expectedXMLResponse, _ = xml.Marshal(expectedJSONResponse) + expectedHTMLResponse = "

Test Name

Age 26

" + ) + + e := httptest.New(t, newApp()) + + e.GET("/resource").WithHeader("Accept", "application/json"). + Expect().Status(httptest.StatusOK). + ContentType("application/json", "utf-8"). + JSON().Equal(expectedJSONResponse) + e.GET("/resource").WithHeader("Accept", "application/xml").WithHeader("Accept-Charset", "iso-8859-7"). + Expect().Status(httptest.StatusOK). + ContentType("application/xml", "iso-8859-7"). + Body().Equal(string(expectedXMLResponse)) + + e.GET("/resource2").WithHeader("Accept", "application/json"). + Expect().Status(httptest.StatusOK). + ContentType("application/json", "utf-8"). + JSON().Equal(expectedJSONResponse) + e.GET("/resource2").WithHeader("Accept", "application/xml"). + Expect().Status(httptest.StatusOK). + ContentType("application/xml", "utf-8"). + Body().Equal(string(expectedXMLResponse)) + e.GET("/resource2").WithHeader("Accept", "text/html"). + Expect().Status(httptest.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal(expectedHTMLResponse) + + e.GET("/resource3").WithHeader("Accept", "application/json"). + Expect().Status(httptest.StatusOK). + ContentType("application/json", "utf-8"). + JSON().Equal(expectedJSONResponse) + e.GET("/resource3").WithHeader("Accept", "application/xml"). + Expect().Status(httptest.StatusOK). + ContentType("application/xml", "utf-8"). + Body().Equal(string(expectedXMLResponse)) + + // test html with "gzip" encoding algorithm. + rawGzipResponse := e.GET("/resource3").WithHeader("Accept", "text/html"). + WithHeader("Accept-Encoding", "gzip"). + Expect().Status(httptest.StatusOK). + ContentType("text/html", "utf-8"). + ContentEncoding("gzip"). + Body().Raw() + + zr, err := gzip.NewReader(bytes.NewReader([]byte(rawGzipResponse))) + if err != nil { + t.Fatal(err) + } + + rawResponse, err := ioutil.ReadAll(zr) + if err != nil { + t.Fatal(err) + } + + if expected, got := expectedHTMLResponse, string(rawResponse); expected != got { + t.Fatalf("expected response to be:\n%s but got:\n%s", expected, got) + } + +} diff --git a/_examples/miscellaneous/pprof/main.go b/_examples/miscellaneous/pprof/main.go index 2b2a79e94c..80cb9429e3 100644 --- a/_examples/miscellaneous/pprof/main.go +++ b/_examples/miscellaneous/pprof/main.go @@ -13,7 +13,9 @@ func main() { ctx.HTML("

Please click here") }) - app.Any("/debug/pprof/{action:path}", pprof.New()) + p := pprof.New() + app.Any("/debug/pprof", p) + app.Any("/debug/pprof/{action:path}", p) // ___________ app.Run(iris.Addr(":8080")) } diff --git a/_examples/routing/basic/main.go b/_examples/routing/basic/main.go index d03ea46c0d..9bb530d9eb 100644 --- a/_examples/routing/basic/main.go +++ b/_examples/routing/basic/main.go @@ -4,8 +4,9 @@ import ( "github.com/kataras/iris" ) -func main() { +func newApp() *iris.Application { app := iris.New() + app.Logger().SetLevel("debug") // registers a custom handler for 404 not found http (error) status code, // fires when route not found or manually by ctx.StatusCode(iris.StatusNotFound). @@ -17,8 +18,8 @@ func main() { // // Third receiver should contains the route's handler(s), they are executed by order. app.Handle("GET", "/", func(ctx iris.Context) { - // navigate to the middle of $GOPATH/src/github.com/kataras/iris/context/context.go - // to overview all context's method (there a lot of them, read that and you will learn how iris works too) + // navigate to the https://github.com/kataras/iris/wiki/Routing-context-methods + // to overview all context's method. ctx.HTML("Hello from " + ctx.Path()) // Hello from / }) @@ -26,7 +27,46 @@ func main() { ctx.Writef(`Same as app.Handle("GET", "/", [...])`) }) - app.Get("/donate", donateHandler, donateFinishHandler) + // Different path parameters types in the same path. + app.Get("/u/{p:path}", func(ctx iris.Context) { + ctx.Writef(":string, :int, :uint, :alphabetical and :path in the same path pattern.") + }) + + app.Get("/u/{username:string}", func(ctx iris.Context) { + ctx.Writef("before username (string), current route name: %s\n", ctx.RouteName()) + ctx.Next() + }, func(ctx iris.Context) { + ctx.Writef("username (string): %s", ctx.Params().Get("username")) + }) + + app.Get("/u/{id:int}", func(ctx iris.Context) { + ctx.Writef("before id (int), current route name: %s\n", ctx.RouteName()) + ctx.Next() + }, func(ctx iris.Context) { + ctx.Writef("id (int): %d", ctx.Params().GetIntDefault("id", 0)) + }) + + app.Get("/u/{uid:uint}", func(ctx iris.Context) { + ctx.Writef("before uid (uint), current route name: %s\n", ctx.RouteName()) + ctx.Next() + }, func(ctx iris.Context) { + ctx.Writef("uid (uint): %d", ctx.Params().GetUintDefault("uid", 0)) + }) + + app.Get("/u/{firstname:alphabetical}", func(ctx iris.Context) { + ctx.Writef("before firstname (alphabetical), current route name: %s\n", ctx.RouteName()) + ctx.Next() + }, func(ctx iris.Context) { + ctx.Writef("firstname (alphabetical): %s", ctx.Params().Get("firstname")) + }) + + /* + /u/some/path/here maps to :path + /u/abcd maps to :alphabetical (if :alphabetical registered otherwise :string) + /u/42 maps to :uint (if :uint registered otherwise :int) + /u/-1 maps to :int (if :int registered otherwise :string) + /u/abcd123 maps to :string + */ // Pssst, don't forget dynamic-path example for more "magic"! app.Get("/api/users/{userid:uint64 min(1)}", func(ctx iris.Context) { @@ -92,8 +132,9 @@ func main() { { // braces are optional, it's just type of style, to group the routes visually. // http://v1.localhost:8080 + // Note: for versioning-specific features checkout the _examples/versioning instead. v1.Get("/", func(ctx iris.Context) { - ctx.HTML("Version 1 API. go to /api/users") + ctx.HTML(`Version 1 API. go to /api/users`) }) usersAPI := v1.Party("/api/users") @@ -117,9 +158,14 @@ func main() { }) } + return app +} + +func main() { + app := newApp() + // http://localhost:8080 // http://localhost:8080/home - // http://localhost:8080/donate // http://localhost:8080/api/users/42 // http://localhost:8080/admin // http://localhost:8080/admin/login @@ -128,6 +174,12 @@ func main() { // http://localhost:8080/api/users/blabla // http://localhost:8080/wontfound // + // http://localhost:8080/u/abcd + // http://localhost:8080/u/42 + // http://localhost:8080/u/-1 + // http://localhost:8080/u/abcd123 + // http://localhost:8080/u/some/path/here + // // if hosts edited: // http://v1.localhost:8080 // http://v1.localhost:8080/api/users @@ -141,24 +193,6 @@ func adminMiddleware(ctx iris.Context) { ctx.Next() // to move to the next handler, or don't that if you have any auth logic. } -func donateHandler(ctx iris.Context) { - ctx.Writef("Just like an inline handler, but it can be " + - "used by other package, anywhere in your project.") - - // let's pass a value to the next handler - // Values is the way handlers(or middleware) are communicating between each other. - ctx.Values().Set("donate_url", "https://github.com/kataras/iris#-people") - ctx.Next() // in order to execute the next handler in the chain, look donate route. -} - -func donateFinishHandler(ctx iris.Context) { - // values can be any type of object so we could cast the value to a string - // but iris provides an easy to do that, if donate_url is not defined, then it returns an empty string instead. - donateURL := ctx.Values().GetString("donate_url") - ctx.Application().Logger().Infof("donate_url value was: " + donateURL) - ctx.Writef("\n\nDonate sent(?).") -} - func notFoundHandler(ctx iris.Context) { ctx.HTML("Custom route for 404 not found http code, here you can render a view, html, json any valid response.") } diff --git a/_examples/routing/basic/main_test.go b/_examples/routing/basic/main_test.go new file mode 100644 index 0000000000..0f1b98cd2f --- /dev/null +++ b/_examples/routing/basic/main_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/kataras/iris/httptest" +) + +// Shows a very basic usage of the httptest. +// The tests are written in a way to be easy to understand, +// for a more comprehensive testing examples check out the: +// _examples/routing/main_test.go, +// _examples/subdomains/www/main_test.go +// _examples/file-server and e.t.c. +// Almost every example which covers +// a new feature from you to learn +// contains a test file as well. +func TestRoutingBasic(t *testing.T) { + expectedUResponse := func(paramName, paramType, paramValue string) string { + s := fmt.Sprintf("before %s (%s), current route name: GET/u/{%s:%s}\n", paramName, paramType, paramName, paramType) + s += fmt.Sprintf("%s (%s): %s", paramName, paramType, paramValue) + return s + } + + var ( + expectedNotFoundResponse = "Custom route for 404 not found http code, here you can render a view, html, json any valid response." + + expectedIndexResponse = "Hello from /" + expectedHomeResponse = `Same as app.Handle("GET", "/", [...])` + + expectedUpathResponse = ":string, :int, :uint, :alphabetical and :path in the same path pattern." + expectedUStringResponse = expectedUResponse("username", "string", "abcd123") + expectedUIntResponse = expectedUResponse("id", "int", "-1") + expectedUUintResponse = expectedUResponse("uid", "uint", "42") + expectedUAlphabeticalResponse = expectedUResponse("firstname", "alphabetical", "abcd") + + expectedAPIUsersIndexResponse = map[string]interface{}{"user_id": 42} + + expectedAdminIndexResponse = "

Hello from admin/

" + + expectedSubdomainV1IndexResponse = `Version 1 API. go to /api/users` + expectedSubdomainV1APIUsersIndexResponse = "All users" + expectedSubdomainV1APIUsersIndexWithParamResponse = "user with id: 42" + + expectedSubdomainWildcardIndexResponse = "Subdomain can be anything, now you're here from: any-subdomain-here" + ) + + app := newApp() + e := httptest.New(t, app) + + e.GET("/anotfound").Expect().Status(httptest.StatusNotFound). + Body().Equal(expectedNotFoundResponse) + + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Equal(expectedIndexResponse) + e.GET("/home").Expect().Status(httptest.StatusOK). + Body().Equal(expectedHomeResponse) + + e.GET("/u/some/path/here").Expect().Status(httptest.StatusOK). + Body().Equal(expectedUpathResponse) + e.GET("/u/abcd123").Expect().Status(httptest.StatusOK). + Body().Equal(expectedUStringResponse) + e.GET("/u/-1").Expect().Status(httptest.StatusOK). + Body().Equal(expectedUIntResponse) + e.GET("/u/42").Expect().Status(httptest.StatusOK). + Body().Equal(expectedUUintResponse) + e.GET("/u/abcd").Expect().Status(httptest.StatusOK). + Body().Equal(expectedUAlphabeticalResponse) + + e.GET("/api/users/42").Expect().Status(httptest.StatusOK). + JSON().Equal(expectedAPIUsersIndexResponse) + + e.GET("/admin").Expect().Status(httptest.StatusOK). + Body().Equal(expectedAdminIndexResponse) + + e.Request("GET", "/").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). + Body().Equal(expectedSubdomainV1IndexResponse) + + e.Request("GET", "/api/users").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). + Body().Equal(expectedSubdomainV1APIUsersIndexResponse) + + e.Request("GET", "/api/users/42").WithURL("http://v1.example.com").Expect().Status(httptest.StatusOK). + Body().Equal(expectedSubdomainV1APIUsersIndexWithParamResponse) + + e.Request("GET", "/").WithURL("http://any-subdomain-here.example.com").Expect().Status(httptest.StatusOK). + Body().Equal(expectedSubdomainWildcardIndexResponse) + +} diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go index b1ac1510e2..a5f0061821 100644 --- a/_examples/routing/dynamic-path/main.go +++ b/_examples/routing/dynamic-path/main.go @@ -284,5 +284,10 @@ func main() { // Last, do not confuse `ctx.Params()` with `ctx.Values()`. // Path parameter's values can be retrieved from `ctx.Params()`, // context's local storage that can be used to communicate between handlers and middleware(s) can be stored to `ctx.Values()`. + // + // When registering different parameter types in the same exact path pattern, the path parameter's name + // should differ e.g. + // /path/{name:string} + // /path/{id:uint} app.Run(iris.Addr(":8080")) } diff --git a/_examples/routing/main.go b/_examples/routing/main.go index 9f4503274e..ccd35c9e7c 100644 --- a/_examples/routing/main.go +++ b/_examples/routing/main.go @@ -1,8 +1,6 @@ package main import ( - "io/ioutil" - "github.com/kataras/iris" ) @@ -108,7 +106,7 @@ func newApp() *iris.Application { // to protect ourselves from "over heating". app.Post("/", iris.LimitRequestBodySize(maxBodySize), func(ctx iris.Context) { // get request body - b, err := ioutil.ReadAll(ctx.Request().Body) + b, err := ctx.GetBody() // if is larger then send a bad request status if err != nil { ctx.StatusCode(iris.StatusBadRequest) diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index e48f736f86..1effb9adc6 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -21,6 +21,7 @@ func main() { Password: "", Database: "", Prefix: "", + Delim: "-", }) // optionally configure the bridge between your redis server. // close connection when control+C/cmd+C diff --git a/_examples/websocket/basic/browser/index.html b/_examples/websocket/basic/browser/index.html index 836ef22362..493b79875e 100644 --- a/_examples/websocket/basic/browser/index.html +++ b/_examples/websocket/basic/browser/index.html @@ -53,6 +53,8 @@ }; } + const username = window.prompt("Your username?"); + async function runExample() { // You can omit the "default" and simply define only Events, the namespace will be an empty string"", // however if you decide to make any changes on this example make sure the changes are reflecting inside the ../server.go file as well. @@ -70,6 +72,10 @@ addMessage(msg.Body); } } + },{ + headers: { + "X-Username": username, + } }); // You can either wait to conenct or just conn.connect("connect") diff --git a/_examples/websocket/basic/go-client/client.go b/_examples/websocket/basic/go-client/client.go index edfff93620..80a68ecfbc 100644 --- a/_examples/websocket/basic/go-client/client.go +++ b/_examples/websocket/basic/go-client/client.go @@ -42,7 +42,10 @@ func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(dialAndConnectTimeout)) defer cancel() - client, err := websocket.Dial(ctx, websocket.DefaultGorillaDialer, endpoint, clientEvents) + // username := "my_username" + // dialer := websocket.GobwasDialer(websocket.GobwasDialerOptions{Header: websocket.GobwasHeader{"X-Username": []string{username}}}) + dialer := websocket.DefaultGobwasDialer + client, err := websocket.Dial(ctx, dialer, endpoint, clientEvents) if err != nil { panic(err) } diff --git a/_examples/websocket/basic/server.go b/_examples/websocket/basic/server.go index f71a49a680..a333ce0b4a 100644 --- a/_examples/websocket/basic/server.go +++ b/_examples/websocket/basic/server.go @@ -68,8 +68,17 @@ func main() { SigningMethod: jwt.SigningMethodHS256, }) + idGen := func(ctx iris.Context) string { + if username := ctx.GetHeader("X-Username"); username != "" { + return username + } + + return websocket.DefaultIDGenerator(ctx) + } + // serves the endpoint of ws://localhost:8080/echo - websocketRoute := app.Get("/echo", websocket.Handler(websocketServer)) + // with optional custom ID generator. + websocketRoute := app.Get("/echo", websocket.Handler(websocketServer, idGen)) if enableJWT { // Register the jwt middleware (on handshake): diff --git a/context/context.go b/context/context.go index 11eb036481..c334f08c20 100644 --- a/context/context.go +++ b/context/context.go @@ -586,6 +586,10 @@ type Context interface { // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go ReadXML(xmlObjectPtr interface{}) error + // ReadYAML reads YAML from request's body and binds it to the "outPtr" value. + // + // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-yaml/main.go + ReadYAML(outPtr interface{}) error // ReadForm binds the formObject with the form data // it supports any kind of type, including custom structs. // It will return nothing if request data are empty. @@ -781,6 +785,49 @@ type Context interface { Markdown(markdownB []byte, options ...Markdown) (int, error) // YAML parses the "v" using the yaml parser and renders its result to the client. YAML(v interface{}) (int, error) + + // +-----------------------------------------------------------------------+ + // | Content Νegotiation | + // | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | | + // +-----------------------------------------------------------------------+ + + // Negotiation creates once and returns the negotiation builder + // to build server-side available content for specific mime type(s) + // and charset(s). + // + // See `Negotiate` method too. + Negotiation() *NegotiationBuilder + // Negotiate used for serving different representations of a resource at the same URI. + // + // The "v" can be a single `N` struct value. + // The "v" can be any value completes the `ContentSelector` interface. + // The "v" can be any value completes the `ContentNegotiator` interface. + // The "v" can be any value of struct(JSON, JSONP, XML, YAML) or + // string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type. + // + // If the "v" is nil, the `Context.Negotitation()` builder's + // content will be used instead, otherwise "v" overrides builder's content + // (server mime types are still retrieved by its registered, supported, mime list) + // + // Set mime type priorities by `Negotiation().JSON().XML().HTML()...`. + // Set charset priorities by `Negotiation().Charset(...)`. + // Set encoding algorithm priorities by `Negotiation().Encoding(...)`. + // Modify the accepted by + // `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`. + // + // It returns `ErrContentNotSupported` when not matched mime type(s). + // + // Resources: + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding + // + // Supports the above without quality values. + // + // Read more at: https://github.com/kataras/iris/wiki/Content-negotiation + Negotiate(v interface{}) (int, error) + // +------------------------------------------------------------+ // | Serve files | // +------------------------------------------------------------+ @@ -1750,7 +1797,9 @@ func (ctx *context) contentTypeOnce(cType string, charset string) { charset = ctx.Application().ConfigurationReadOnly().GetCharset() } - cType += "; charset=" + charset + if cType != ContentBinaryHeaderValue { + cType += "; charset=" + charset + } ctx.Values().Set(contentTypeContextKey, cType) ctx.writer.Header().Set(ContentTypeHeaderKey, cType) @@ -2396,19 +2445,26 @@ func (ctx *context) shouldOptimize() bool { // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-json/main.go -func (ctx *context) ReadJSON(jsonObject interface{}) error { +func (ctx *context) ReadJSON(outPtr interface{}) error { var unmarshaler = json.Unmarshal if ctx.shouldOptimize() { unmarshaler = jsoniter.Unmarshal } - return ctx.UnmarshalBody(jsonObject, UnmarshalerFunc(unmarshaler)) + return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(unmarshaler)) } // ReadXML reads XML from request's body and binds it to a value of any xml-valid type. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go -func (ctx *context) ReadXML(xmlObject interface{}) error { - return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal)) +func (ctx *context) ReadXML(outPtr interface{}) error { + return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(xml.Unmarshal)) +} + +// ReadYAML reads YAML from request's body and binds it to the "outPtr" value. +// +// Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-yaml/main.go +func (ctx *context) ReadYAML(outPtr interface{}) error { + return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(yaml.Unmarshal)) } // IsErrPath can be used at `context#ReadForm` and `context#ReadQuery`. @@ -2893,126 +2949,6 @@ const ( ContentFormMultipartHeaderValue = "multipart/form-data" ) -// TODO: -// const negotitationContextKey = "_iris_accept_negotitation_builder" - -// func (ctx *context) Accept() *Negotitation { -// if n := ctx.Values().Get(negotitationContextKey); n != nil { -// return n.(*Negotitation) -// } - -// n := new(Negotitation) -// n.accept = parseHeader(ctx.GetHeader("Accept")) -// n.charset = parseHeader(ctx.GetHeader("Accept-Charset")) - -// ctx.Values().Set(negotitationContextKey, n) -// return n -// } - -// func parseHeader(headerValue string) []string { -// in := strings.Split(headerValue, ",") -// out := make([]string, 0, len(in)) - -// for _, value := range in { -// // remove any spaces and quality values such as ;q=0.8. -// // */* or * means accept everything. -// v := strings.TrimSpace(strings.Split(value, ";")[0]) -// if v != "" { -// out = append(out, v) -// } -// } - -// return out -// } - -// // Negotitation builds the accepted mime types and charset -// // -// // and "Accept-Charset" headers respectfully. -// // The default values are set by the client side, server can append or override those. -// // The end result will be challenged with runtime preffered set of content types and charsets. -// // -// // See `Negotitate`. -// type Negotitation struct { -// // initialized with "Accept" header values. -// accept []string -// // initialized with "Accept-Charset" and if was empty then the -// // application's default (which defaults to utf-8). -// charset []string - -// // To support override in request life cycle. -// // We need slice when data is the same format -// // for one or more mime types, -// // i.e text/xml and obselete application/xml. -// lastAccept []string -// lastCharset []string -// } - -// func (n *Negotitation) Override() *Negotitation { -// // when called first. -// n.accept = n.accept[0:0] -// n.charset = n.charset[0:0] - -// // when called after. -// if len(n.lastAccept) > 0 { -// n.accept = append(n.accept, n.lastAccept...) -// n.lastAccept = n.lastAccept[0:0] -// } - -// if len(n.lastCharset) > 0 { -// n.charset = append(n.charset, n.lastCharset...) -// n.lastCharset = n.lastCharset[0:0] -// } - -// return n -// } - -// func (n *Negotitation) MIME(mimeType ...string) *Negotitation { -// n.lastAccept = mimeType -// n.accept = append(n.accept, mimeType...) -// return n -// } - -// func (n *Negotitation) JSON() *Negotitation { -// return n.MIME(ContentJSONHeaderValue) -// } - -// func (n *Negotitation) XML() *Negotitation { -// return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue) -// } - -// func (n *Negotitation) HTML() *Negotitation { -// return n.MIME(ContentHTMLHeaderValue) -// } - -// func (n *Negotitation) Charset(charset ...string) *Negotitation { -// n.lastCharset = charset -// n.charset = append(n.charset, charset...) - -// return n -// } - -// func (n *Negotitation) build(preferences []string) (contentType, charset string) { -// return -// } - -// // https://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html -// // https://developer.mozilla.org/en-US/docs/tag/Content%20Negotiation -// // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept -// // https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation/List_of_default_Accept_values -// func (ctx *context) Negotiate(v interface{}, preferences ...string) (int, error) { -// contentType, charset := ctx.Accept().build(preferences) - -// // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset -// // If the server cannot serve any matching character set, -// // it can theoretically send back a 406 (Not Acceptable) error code. -// ctx.contentTypeOnce(contentType, charset) - -// switch contentType { - -// } -// return -1, nil -// } - // Binary writes out the raw bytes as binary data. func (ctx *context) Binary(data []byte) (int, error) { ctx.ContentType(ContentBinaryHeaderValue) @@ -3319,6 +3255,627 @@ func (ctx *context) YAML(v interface{}) (int, error) { return ctx.Write(out) } +// +-----------------------------------------------------------------------+ +// | Content Νegotiation | +// | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | | +// +-----------------------------------------------------------------------+ + +// ErrContentNotSupported returns from the `Negotiate` method +// when server responds with 406. +var ErrContentNotSupported = errors.New("unsupported content") + +// ContentSelector is the interface which structs can implement +// to manually choose a content based on the negotiated mime (content type). +// It can be passed to the `Context.Negotiate` method. +// +// See the `N` struct too. +type ContentSelector interface { + SelectContent(mime string) interface{} +} + +// ContentNegotiator is the interface which structs can implement +// to override the `Context.Negotiate` default implementation and +// manually respond to the client based on a manuall call of `Context.Negotiation().Build()` +// to get the final negotiated mime and charset. +// It can be passed to the `Context.Negotiate` method. +type ContentNegotiator interface { + // mime and charset can be retrieved by: + // mime, charset := Context.Negotiation().Build() + // Pass this method to `Context.Negotiate` method + // to write custom content. + // Overriding the existing behavior of Context.Negotiate for selecting values based on + // content types, although it can accept any custom mime type with []byte. + // Content type is already set. + // Use it with caution, 99.9% you don't need this but it's here for extreme cases. + Negotiate(ctx Context) (int, error) +} + +// N is a struct which can be passed on the `Context.Negotiate` method. +// It contains fields which should be filled based on the `Context.Negotiation()` +// server side values. If no matched mime then its "Other" field will be sent, +// which should be a string or []byte. +// It completes the `ContentSelector` interface. +type N struct { + Text, HTML string + Markdown []byte + Binary []byte + + JSON interface{} + JSONP interface{} + XML interface{} + YAML interface{} + + Other []byte // custom content types. +} + +// SelectContent returns a content based on the matched negotiated "mime". +func (n N) SelectContent(mime string) interface{} { + switch mime { + case ContentTextHeaderValue: + return n.Text + case ContentHTMLHeaderValue: + return n.HTML + case ContentMarkdownHeaderValue: + return n.Markdown + case ContentBinaryHeaderValue: + return n.Binary + case ContentJSONHeaderValue: + return n.JSON + case ContentJavascriptHeaderValue: + return n.JSONP + case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue: + return n.XML + case ContentYAMLHeaderValue: + return n.YAML + default: + return n.Other + } +} + +const negotiationContextKey = "_iris_negotiation_builder" + +// Negotiation creates once and returns the negotiation builder +// to build server-side available prioritized content +// for specific content type(s), charset(s) and encoding algorithm(s). +// +// See `Negotiate` method too. +func (ctx *context) Negotiation() *NegotiationBuilder { + if n := ctx.Values().Get(negotiationContextKey); n != nil { + return n.(*NegotiationBuilder) + } + + acceptBuilder := NegotiationAcceptBuilder{} + acceptBuilder.accept = parseHeader(ctx.GetHeader("Accept")) + acceptBuilder.charset = parseHeader(ctx.GetHeader("Accept-Charset")) + + n := &NegotiationBuilder{Accept: acceptBuilder} + + ctx.Values().Set(negotiationContextKey, n) + + return n +} + +func parseHeader(headerValue string) []string { + in := strings.Split(headerValue, ",") + out := make([]string, 0, len(in)) + + for _, value := range in { + // remove any spaces and quality values such as ;q=0.8. + v := strings.TrimSpace(strings.Split(value, ";")[0]) + if v != "" { + out = append(out, v) + } + } + + return out +} + +// Negotiate used for serving different representations of a resource at the same URI. +// +// The "v" can be a single `N` struct value. +// The "v" can be any value completes the `ContentSelector` interface. +// The "v" can be any value completes the `ContentNegotiator` interface. +// The "v" can be any value of struct(JSON, JSONP, XML, YAML) or +// string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type. +// +// If the "v" is nil, the `Context.Negotitation()` builder's +// content will be used instead, otherwise "v" overrides builder's content +// (server mime types are still retrieved by its registered, supported, mime list) +// +// Set mime type priorities by `Negotiation().JSON().XML().HTML()...`. +// Set charset priorities by `Negotiation().Charset(...)`. +// Set encoding algorithm priorities by `Negotiation().Encoding(...)`. +// Modify the accepted by +// `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`. +// +// It returns `ErrContentNotSupported` when not matched mime type(s). +// +// Resources: +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding +// +// Supports the above without quality values. +// +// Read more at: https://github.com/kataras/iris/wiki/Content-negotiation +func (ctx *context) Negotiate(v interface{}) (int, error) { + contentType, charset, encoding, content := ctx.Negotiation().Build() + if v == nil { + v = content + } + + if contentType == "" { + // If the server cannot serve any matching set, + // it can send back a 406 (Not Acceptable) error code. + ctx.StatusCode(http.StatusNotAcceptable) + return -1, ErrContentNotSupported + } + + if charset == "" { + charset = ctx.Application().ConfigurationReadOnly().GetCharset() + } + + if encoding == "gzip" { + ctx.Gzip(true) + } + + ctx.contentTypeOnce(contentType, charset) + + if n, ok := v.(ContentNegotiator); ok { + return n.Negotiate(ctx) + } + + if s, ok := v.(ContentSelector); ok { + v = s.SelectContent(contentType) + } + + // switch v := value.(type) { + // case []byte: + // if contentType == ContentMarkdownHeaderValue { + // return ctx.Markdown(v) + // } + + // return ctx.Write(v) + // case string: + // return ctx.WriteString(v) + // default: + // make it switch by content-type only, but we lose custom mime types capability that way: + // ^ solved with []byte on default case and + // ^ N.Other and + // ^ ContentSelector and ContentNegotiator interfaces. + + switch contentType { + case ContentTextHeaderValue, ContentHTMLHeaderValue: + return ctx.WriteString(v.(string)) + case ContentMarkdownHeaderValue: + return ctx.Markdown(v.([]byte)) + case ContentJSONHeaderValue: + return ctx.JSON(v) + case ContentJavascriptHeaderValue: + return ctx.JSONP(v) + case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue: + return ctx.XML(v) + case ContentYAMLHeaderValue: + return ctx.YAML(v) + default: + // maybe "Other" or v is []byte or string but not a built-in framework mime, + // for custom content types, + // panic if not correct usage. + switch vv := v.(type) { + case []byte: + return ctx.Write(vv) + case string: + return ctx.WriteString(vv) + default: + ctx.StatusCode(http.StatusNotAcceptable) + return -1, ErrContentNotSupported + } + + } +} + +// NegotiationBuilder returns from the `Context.Negotitation` +// and can be used inside chain of handlers to build server-side +// mime type(s), charset(s) and encoding algorithm(s) +// that should match with the client's +// Accept, Accept-Charset and Accept-Encoding headers (by-default). +// To modify the client's accept use its "Accept" field +// which it's the `NegotitationAcceptBuilder`. +// +// See the `Negotiate` method too. +type NegotiationBuilder struct { + Accept NegotiationAcceptBuilder + + mime []string // we need order. + contents map[string]interface{} // map to the "mime" and content should be rendered if that mime requested. + charset []string + encoding []string +} + +// MIME registers a mime type and optionally the value that should be rendered +// through `Context.Negotiate` when this mime type is accepted by client. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) MIME(mime string, content interface{}) *NegotiationBuilder { + mimes := parseHeader(mime) // if contains more than one sep by commas ",". + if content == nil { + n.mime = append(n.mime, mimes...) + return n + } + + if n.contents == nil { + n.contents = make(map[string]interface{}) + } + + for _, m := range mimes { + n.mime = append(n.mime, m) + n.contents[m] = content + } + + return n +} + +// Text registers the "text/plain" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "text/plain" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Text(v ...string) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentTextHeaderValue, content) +} + +// HTML registers the "text/html" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "text/html" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) HTML(v ...string) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentHTMLHeaderValue, content) +} + +// Markdown registers the "text/markdown" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "text/markdown" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Markdown(v ...[]byte) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v + } + return n.MIME(ContentMarkdownHeaderValue, content) +} + +// Binary registers the "application/octet-stream" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "application/octet-stream" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Binary(v ...[]byte) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentBinaryHeaderValue, content) +} + +// JSON registers the "application/json" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "application/json" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentJSONHeaderValue, content) +} + +// JSONP registers the "application/javascript" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "application/javascript" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) JSONP(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentJavascriptHeaderValue, content) +} + +// XML registers the "text/xml" and "application/xml" content types and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts one of the "text/xml" or "application/xml" content types. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) XML(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentXMLHeaderValue+","+ContentXMLUnreadableHeaderValue, content) +} + +// YAML registers the "application/x-yaml" content type and, optionally, +// a value that `Context.Negotiate` will render +// when a client accepts the "application/x-yaml" content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) YAML(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME(ContentYAMLHeaderValue, content) +} + +// Any registers a wildcard that can match any client's accept content type. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Any(v ...interface{}) *NegotiationBuilder { + var content interface{} + if len(v) > 0 { + content = v[0] + } + return n.MIME("*", content) +} + +// Charset overrides the application's config's charset (which defaults to "utf-8") +// that a client should match for +// (through Accept-Charset header or custom through `NegotitationBuilder.Accept.Override().Charset(...)` call). +// Do not set it if you don't know what you're doing. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Charset(charset ...string) *NegotiationBuilder { + n.charset = append(n.charset, charset...) + return n +} + +// Encoding registers one or more encoding algorithms by name, i.e gzip, deflate. +// that a client should match for (through Accept-Encoding header). +// +// Only the "gzip" can be handlded automatically as it's the only builtin encoding algorithm +// to serve resources. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) Encoding(encoding ...string) *NegotiationBuilder { + n.encoding = append(n.encoding, encoding...) + return n +} + +// EncodingGzip registers the "gzip" encoding algorithm +// that a client should match for (through Accept-Encoding header or call of Accept.Encoding(enc)). +// +// It will make resources to served by "gzip" if Accept-Encoding contains the "gzip" as well. +// +// Returns itself for recursive calls. +func (n *NegotiationBuilder) EncodingGzip() *NegotiationBuilder { + return n.Encoding(GzipHeaderValue) +} + +// Build calculates the client's and server's mime type(s), charset(s) and encoding +// and returns the final content type, charset and encoding that server should render +// to the client. It does not clear the fields, use the `Clear` method if neeeded. +// +// The returned "content" can be nil if the matched "contentType" does not provide any value, +// in that case the `Context.Negotiate(v)` must be called with a non-nil value. +func (n *NegotiationBuilder) Build() (contentType, charset, encoding string, content interface{}) { + contentType = negotiationMatch(n.Accept.accept, n.mime) + charset = negotiationMatch(n.Accept.charset, n.charset) + encoding = negotiationMatch(n.Accept.encoding, n.encoding) + + if n.contents != nil { + if data, ok := n.contents[contentType]; ok { + content = data + } + } + + return +} + +// Clear clears the prioritized mime type(s), charset(s) and any contents +// relative to those mime type(s). +// The "Accept" field is stay as it is, use its `Override` method +// to clear out the client's accepted mime type(s) and charset(s). +func (n *NegotiationBuilder) Clear() *NegotiationBuilder { + n.mime = n.mime[0:0] + n.contents = nil + n.charset = n.charset[0:0] + return n +} + +func negotiationMatch(in []string, priorities []string) string { + // e.g. + // match json: + // in: text/html, application/json + // prioritities: application/json + // not match: + // in: text/html, application/json + // prioritities: text/xml + // match html: + // in: text/html, application/json + // prioritities: */* + // not match: + // in: application/json + // prioritities: text/xml + // match json: + // in: text/html, application/* + // prioritities: application/json + + if len(priorities) == 0 { + return "" + } + + if len(in) == 0 { + return priorities[0] + } + + for _, accepted := range in { + for _, p := range priorities { + // wildcard is */* or text/* and etc. + // so loop through each char. + for i, n := 0, len(accepted); i < n; i++ { + if accepted[i] != p[i] { + break + } + + if accepted[i] == '*' || p[i] == '*' { + return p + } + + if i == n-1 { + return p + } + } + } + } + + return "" +} + +// NegotiationAcceptBuilder builds the accepted mime types and charset +// +// and "Accept-Charset" headers respectfully. +// The default values are set by the client side, server can append or override those. +// The end result will be challenged with runtime preffered set of content types and charsets. +// +// See the `Negotiate` method too. +type NegotiationAcceptBuilder struct { + // initialized with "Accept" request header values. + accept []string + // initialized with "Accept-Encoding" request header. and if was empty then the + // application's default (which defaults to utf-8). + charset []string + // initialized with "Accept-Encoding" request header values. + encoding []string + + // To support override in request life cycle. + // We need slice when data is the same format + // for one or more mime types, + // i.e text/xml and obselete application/xml. + lastAccept []string + lastCharset []string + lastEncoding []string +} + +// Override clears the default values for accept and accept charset. +// Returns itself. +func (n *NegotiationAcceptBuilder) Override() *NegotiationAcceptBuilder { + // when called first. + n.accept = n.accept[0:0] + n.charset = n.charset[0:0] + n.encoding = n.encoding[0:0] + + // when called after. + if len(n.lastAccept) > 0 { + n.accept = append(n.accept, n.lastAccept...) + n.lastAccept = n.lastAccept[0:0] + } + + if len(n.lastCharset) > 0 { + n.charset = append(n.charset, n.lastCharset...) + n.lastCharset = n.lastCharset[0:0] + } + + if len(n.lastEncoding) > 0 { + n.encoding = append(n.encoding, n.lastEncoding...) + n.lastEncoding = n.lastEncoding[0:0] + } + + return n +} + +// MIME adds accepted client's mime type(s). +// Returns itself. +func (n *NegotiationAcceptBuilder) MIME(mimeType ...string) *NegotiationAcceptBuilder { + n.lastAccept = mimeType + n.accept = append(n.accept, mimeType...) + return n +} + +// Text adds the "text/plain" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) Text() *NegotiationAcceptBuilder { + return n.MIME(ContentTextHeaderValue) +} + +// HTML adds the "text/html" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) HTML() *NegotiationAcceptBuilder { + return n.MIME(ContentHTMLHeaderValue) +} + +// Markdown adds the "text/markdown" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) Markdown() *NegotiationAcceptBuilder { + return n.MIME(ContentMarkdownHeaderValue) +} + +// Binary adds the "application/octet-stream" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) Binary() *NegotiationAcceptBuilder { + return n.MIME(ContentBinaryHeaderValue) +} + +// JSON adds the "application/json" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder { + return n.MIME(ContentJSONHeaderValue) +} + +// JSONP adds the "application/javascript" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) JSONP() *NegotiationAcceptBuilder { + return n.MIME(ContentJavascriptHeaderValue) +} + +// XML adds the "text/xml" and "application/xml" as accepted client content types. +// Returns itself. +func (n *NegotiationAcceptBuilder) XML() *NegotiationAcceptBuilder { + return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue) +} + +// YAML adds the "application/x-yaml" as accepted client content type. +// Returns itself. +func (n *NegotiationAcceptBuilder) YAML() *NegotiationAcceptBuilder { + return n.MIME(ContentYAMLHeaderValue) +} + +// Charset adds one or more client accepted charsets. +// Returns itself. +func (n *NegotiationAcceptBuilder) Charset(charset ...string) *NegotiationAcceptBuilder { + n.lastCharset = charset + n.charset = append(n.charset, charset...) + + return n +} + +// Encoding adds one or more client accepted encoding algorithms. +// Returns itself. +func (n *NegotiationAcceptBuilder) Encoding(encoding ...string) *NegotiationAcceptBuilder { + n.lastEncoding = encoding + n.encoding = append(n.encoding, encoding...) + + return n +} + +// EncodingGzip adds the "gzip" as accepted encoding. +// Returns itself. +func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder { + return n.Encoding(GzipHeaderValue) +} + // +------------------------------------------------------------+ // | Serve files | // +------------------------------------------------------------+ diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 35005f8732..b5aee7a729 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -10,6 +10,7 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/macro" + macroHandler "github.com/kataras/iris/macro/handler" ) // MethodNone is a Virtual method @@ -109,6 +110,20 @@ func (repo *repository) get(routeName string) *Route { return nil } +func (repo *repository) getRelative(r *Route) *Route { + if r.tmpl.IsTrailing() || !macroHandler.CanMakeHandler(r.tmpl) { + return nil + } + + for _, route := range repo.routes { + if r.Subdomain == route.Subdomain && r.Method == route.Method && r.FormattedPath == route.FormattedPath && !route.tmpl.IsTrailing() { + return route + } + } + + return nil +} + func (repo *repository) getByPath(tmplPath string) *Route { if repo.pos != nil { if idx, ok := repo.pos[tmplPath]; ok { @@ -345,6 +360,8 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co var route *Route // the last one is returned. for _, route = range routes { // global + + route.topLink = api.routes.getRelative(route) api.routes.register(route) } diff --git a/core/router/handler.go b/core/router/handler.go index 4091e24154..b63d0979b9 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -5,11 +5,12 @@ import ( "sort" "strings" - "github.com/kataras/golog" - "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/netutil" + macroHandler "github.com/kataras/iris/macro/handler" + + "github.com/kataras/golog" ) // RequestHandler the middle man between acquiring a context and releasing it. @@ -84,6 +85,13 @@ func (h *routerHandler) Build(provider RoutesProvider) error { rp := errors.NewReporter() registeredRoutes := provider.GetRoutes() + // before sort. + for _, r := range registeredRoutes { + if r.topLink != nil { + bindMultiParamTypesHandler(r.topLink, r) + } + } + // sort, subdomains go first. sort.Slice(registeredRoutes, func(i, j int) bool { first, second := registeredRoutes[i], registeredRoutes[j] @@ -116,29 +124,57 @@ func (h *routerHandler) Build(provider RoutesProvider) error { }) for _, r := range registeredRoutes { - // build the r.Handlers based on begin and done handlers, if any. - r.BuildHandlers() - if r.Subdomain != "" { h.hosts = true } - // the only "bad" with this is if the user made an error - // on route, it will be stacked shown in this build state - // and no in the lines of the user's action, they should read - // the docs better. Or TODO: add a link here in order to help new users. - if err := h.addRoute(r); err != nil { - // node errors: - rp.Add("%v -> %s", err, r.String()) - continue + if r.topLink == nil { + // build the r.Handlers based on begin and done handlers, if any. + r.BuildHandlers() + + // the only "bad" with this is if the user made an error + // on route, it will be stacked shown in this build state + // and no in the lines of the user's action, they should read + // the docs better. Or TODO: add a link here in order to help new users. + if err := h.addRoute(r); err != nil { + // node errors: + rp.Add("%v -> %s", err, r.String()) + continue + } } - golog.Debugf(r.Trace()) + golog.Debugf(r.Trace()) // keep log different parameter types in the same path as different routes. } return rp.Return() } +func bindMultiParamTypesHandler(top *Route, r *Route) { + r.BuildHandlers() + + h := r.Handlers[1:] // remove the macro evaluator handler as we manually check below. + f := macroHandler.MakeFilter(r.tmpl) + if f == nil { + return // should never happen, previous checks made to set the top link. + } + + decisionHandler := func(ctx context.Context) { + currentRouteName := ctx.RouteName() + if f(ctx) { + ctx.SetCurrentRouteName(r.Name) + ctx.HandlerIndex(0) + ctx.Do(h) + return + } + + ctx.SetCurrentRouteName(currentRouteName) + ctx.StatusCode(http.StatusOK) + ctx.Next() + } + + r.topLink.beginHandlers = append(context.Handlers{decisionHandler}, r.topLink.beginHandlers...) +} + func (h *routerHandler) HandleRequest(ctx context.Context) { method := ctx.Method() path := ctx.Path() diff --git a/core/router/route.go b/core/router/route.go index a7f53a09e5..9746e4231c 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -40,6 +40,8 @@ type Route struct { // route, manually or automatic by the framework, // get the route by `Application#GetRouteByPath(staticSite.RequestPath)`. StaticSites []context.StaticSite `json:"staticSites"` + + topLink *Route } // NewRoute returns a new route based on its method, diff --git a/doc.go b/doc.go index fca949ed2e..f9593e8025 100644 --- a/doc.go +++ b/doc.go @@ -38,13 +38,13 @@ Source code and other details for the project are available at GitHub: Current Version -11.2.2 +11.2.3 Installation The only requirement is the Go Programming Language, at least version 1.12. - $ go get github.com/kataras/iris@v11.2.2 + $ go get github.com/kataras/iris@master Wiki: diff --git a/go.mod b/go.mod index 313be63d3f..e54413743b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/kataras/iris go 1.12 require ( + github.com/kataras/neffos v0.0.9 github.com/BurntSushi/toml v0.3.1 github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible // indirect github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7 // indirect @@ -20,7 +21,6 @@ require ( github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0 github.com/json-iterator/go v1.1.6 github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40 - github.com/kataras/neffos v0.0.8 github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect github.com/mediocregopher/radix/v3 v3.3.0 github.com/microcosm-cc/bluemonday v1.0.2 diff --git a/go.sum b/go.sum index 829f1b6a13..d2b849840e 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/kataras/neffos v0.0.9 h1:P9xJ78Q3/BGEfUJe85L1keVm258ZzaiR5tREPZzGzUY= +github.com/kataras/neffos v0.0.9/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CloudyKit/jet v2.1.3-0.20180809161101-62edd43e4f88+incompatible/go.mod h1:HPYO+50pSWkPoj9Q/eq0aRGByCL6ScRlUmiEX5Zgm+w= github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM= @@ -15,8 +17,6 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/ github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/kataras/golog v0.0.0-20190624001437-99c81de45f40/go.mod h1:PcaEvfvhGsqwXZ6S3CgCbmjcp+4UDUh2MIfF2ZEul8M= -github.com/kataras/neffos v0.0.8 h1:mOv06sotaVbwCszRaRVerbDRP1embTR7y5J5dhnjjYs= -github.com/kataras/neffos v0.0.8/go.mod h1:ZYmJC07hQPW67eKuzlfY7SO3bC0mw83A3j6im82hfqw= github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= github.com/mediocregopher/radix/v3 v3.3.0/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= diff --git a/go19.go b/go19.go index 79cb6b7613..d98299c56d 100644 --- a/go19.go +++ b/go19.go @@ -90,4 +90,12 @@ type ( // // An alias for the `context/Context#CookieOption`. CookieOption = context.CookieOption + // N is a struct which can be passed on the `Context.Negotiate` method. + // It contains fields which should be filled based on the `Context.Negotiation()` + // server side values. If no matched mime then its "Other" field will be sent, + // which should be a string or []byte. + // It completes the `context/context.ContentSelector` interface. + // + // An alias for the `context/Context#N`. + N = context.N ) diff --git a/iris.go b/iris.go index a5aae22961..8cb0f36c4e 100644 --- a/iris.go +++ b/iris.go @@ -37,7 +37,7 @@ import ( var ( // Version is the current version number of the Iris Web Framework. - Version = "11.2.2" + Version = "11.2.3" ) // HTTP status codes as registered with IANA. diff --git a/macro/handler/handler.go b/macro/handler/handler.go index ea36c7d566..1cf7445000 100644 --- a/macro/handler/handler.go +++ b/macro/handler/handler.go @@ -34,23 +34,54 @@ func CanMakeHandler(tmpl macro.Template) (needsMacroHandler bool) { // If the template does not contain any dynamic attributes and a special handler is NOT required // then it returns a nil handler. func MakeHandler(tmpl macro.Template) context.Handler { + filter := MakeFilter(tmpl) + + return func(ctx context.Context) { + if !filter(ctx) { + ctx.StopExecution() + return + } + + // if all passed, just continue. + ctx.Next() + } +} + +// MakeFilter returns a Filter which reports whether a specific macro template +// and its parameters pass the serve-time validation. +func MakeFilter(tmpl macro.Template) context.Filter { if !CanMakeHandler(tmpl) { return nil } - return func(ctx context.Context) { + return func(ctx context.Context) bool { for _, p := range tmpl.Params { if !p.CanEval() { continue // allow. } - if !p.Eval(ctx.Params().Get(p.Name), &ctx.Params().Store) { + // 07-29-2019 + // changed to retrieve by param index in order to support + // different parameter names for routes with + // different param types (and probably different param names i.e {name:string}, {id:uint64}) + // in the exact same path pattern. + // + // Same parameter names are not allowed, different param types in the same path + // should have different name e.g. {name} {id:uint64}; + // something like {name} and {name:uint64} + // is bad API design and we do NOT allow it by-design. + entry, found := ctx.Params().Store.GetEntryAt(p.Index) + if !found { + // should never happen. + return false + } + + if !p.Eval(entry.String(), &ctx.Params().Store) { ctx.StatusCode(p.ErrCode) - ctx.StopExecution() - return + return false } } - // if all passed, just continue. - ctx.Next() + + return true } } diff --git a/macro/template.go b/macro/template.go index 8e766cb5dd..fcdb0b096a 100644 --- a/macro/template.go +++ b/macro/template.go @@ -20,6 +20,11 @@ type Template struct { Params []TemplateParam `json:"params"` } +// IsTrailing reports whether this Template is a traling one. +func (t *Template) IsTrailing() bool { + return len(t.Params) > 0 && ast.IsTrailing(t.Params[len(t.Params)-1].Type) +} + // TemplateParam is the parsed macro parameter's template // they are being used to describe the param's syntax result. type TemplateParam struct { diff --git a/websocket/websocket.go b/websocket/websocket.go index e1389d6ab6..c39e4c7d1a 100644 --- a/websocket/websocket.go +++ b/websocket/websocket.go @@ -1,6 +1,8 @@ package websocket import ( + "net/http" + "github.com/kataras/iris/context" "github.com/kataras/neffos" @@ -138,6 +140,14 @@ func SetDefaultUnmarshaler(fn func(data []byte, v interface{}) error) { // IDGenerator is an iris-specific IDGenerator for new connections. type IDGenerator func(context.Context) string +func wrapIDGenerator(idGen IDGenerator) func(ctx context.Context) neffos.IDGenerator { + return func(ctx context.Context) neffos.IDGenerator { + return func(w http.ResponseWriter, r *http.Request) string { + return idGen(ctx) + } + } +} + // Handler returns an Iris handler to be served in a route of an Iris application. // Accepts the neffos websocket server as its first input argument // and optionally an Iris-specific `IDGenerator` as its second one. @@ -151,19 +161,19 @@ func Handler(s *neffos.Server, IDGenerator ...IDGenerator) context.Handler { if ctx.IsStopped() { return } - Upgrade(ctx, idGen(ctx), s) + Upgrade(ctx, idGen, s) } } // Upgrade upgrades the request and returns a new websocket Conn. // Use `Handler` for higher-level implementation instead. -func Upgrade(ctx context.Context, customID string, s *neffos.Server) *neffos.Conn { +func Upgrade(ctx context.Context, idGen IDGenerator, s *neffos.Server) *neffos.Conn { conn, _ := s.Upgrade(ctx.ResponseWriter(), ctx.Request(), func(socket neffos.Socket) neffos.Socket { return &socketWrapper{ Socket: socket, ctx: ctx, } - }, customID) + }, wrapIDGenerator(idGen)(ctx)) return conn } diff --git a/websocket/websocket_go19.go b/websocket/websocket_go19.go index e5cccb3beb..fa611adedc 100644 --- a/websocket/websocket_go19.go +++ b/websocket/websocket_go19.go @@ -17,6 +17,9 @@ type ( GorillaDialerOptions = gorilla.Options // GobwasDialerOptions is just an alias for the `gorilla/websocket.Dialer` struct type. GobwasDialerOptions = gobwas.Options + // GobwasHeader is an alias to the adapter that allows the use of `http.Header` as + // handshake headers. + GobwasHeader = gobwas.Header // Conn describes the main websocket connection's functionality. // Its `Connection` will return a new `NSConn` instance.