diff --git a/.travis.yml b/.travis.yml index 6e6ada55f5..a1912a5077 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,14 @@ os: - linux - osx go: - - "go1.9" - - "go1.10" + - 1.9.x + - 1.10.x + - 1.11.x go_import_path: github.com/kataras/iris # we disable test caching via GOCACHE=off -env: - global: - - GOCACHE=off +# env: +# global: +# - GOCACHE=off install: - go get ./... # for iris-contrib/httpexpect, kataras/golog script: diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 54e3725a70..0000000000 --- a/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM irisgo/cloud-native-go:latest - -ENV APPSOURCES /go/src/github.com/iris-contrib/cloud-native-go - -RUN ${APPSOURCES}/cloud-native-go \ No newline at end of file diff --git a/Dockerfile.build b/Dockerfile.build deleted file mode 100644 index 6c127b0e21..0000000000 --- a/Dockerfile.build +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.9.3-alpine - -RUN apk update && apk upgrade && apk add --no-cache bash git -RUN go get github.com/iris-contrib/cloud-native-go - -ENV SOURCES /go/src/github.com/iris-contrib/cloud-native-go -# COPY . ${SOURCES} - -RUN cd ${SOURCES} $$ CGO_ENABLED=0 go build - -ENTRYPOINT cloud-native-go -EXPOSE 8080 \ No newline at end of file diff --git a/FAQ.md b/FAQ.md index f5c8110d6d..15a7b2d2cd 100644 --- a/FAQ.md +++ b/FAQ.md @@ -28,9 +28,10 @@ go get -u github.com/kataras/iris More than 100 practical examples, tutorials and articles at: +- https://github.com/kataras/iris#iris-starter-kits - https://github.com/kataras/iris/tree/master/_examples - https://github.com/iris-contrib/examples -- https://iris-go.com/v10/recipe +- https://iris-go.com/v11/recipe - https://docs.iris-go.com (in-progress) - https://godoc.org/github.com/kataras/iris diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 944aae4d64..0000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,285 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - name = "github.com/BurntSushi/toml" - packages = ["."] - revision = "b26d9c308763d68093482582cea63d69be07a0f0" - version = "v0.3.0" - -[[projects]] - branch = "master" - name = "github.com/Joker/jade" - packages = ["."] - revision = "35b3f5bdbcc920cd31f4870536dbc63be8530541" - -[[projects]] - branch = "master" - name = "github.com/iris-contrib/i18n" - packages = ["."] - revision = "976ad7f3463c8f0b8fd949cf5e78a73a1828c96d" - -[[projects]] - name = "github.com/ajg/form" - packages = ["."] - revision = "cc2954064ec9ea8d93917f0f87456e11d7b881ad" - version = "v1.5" - -[[projects]] - branch = "master" - name = "github.com/aymerick/raymond" - packages = [".","ast","lexer","parser"] - revision = "72acac2207479d21dd45898c2a4264246c818148" - -[[projects]] - name = "github.com/davecgh/go-spew" - packages = ["spew"] - revision = "346938d642f2ec3594ed81d874461961cd0faa76" - version = "v1.1.0" - -[[projects]] - branch = "master" - name = "github.com/eknkc/amber" - packages = [".","parser"] - revision = "b8bd8b03e4f747e33f092617225e9fa8076c0448" - -[[projects]] - name = "github.com/fatih/structs" - packages = ["."] - revision = "a720dfa8df582c51dee1b36feabb906bde1588bd" - version = "v1.0" - -[[projects]] - branch = "master" - name = "github.com/flosch/pongo2" - packages = ["."] - revision = "58f1f3387f7c57843ff829d92a759d08abe5a63f" - -[[projects]] - branch = "master" - name = "github.com/gavv/monotime" - packages = ["."] - revision = "47d58efa69556a936a3c15eb2ed42706d968ab01" - -[[projects]] - branch = "master" - name = "github.com/golang/snappy" - packages = ["."] - revision = "553a641470496b2327abcac10b36396bd98e45c9" - -[[projects]] - branch = "master" - name = "github.com/google/go-querystring" - packages = ["query"] - revision = "53e6ce116135b80d037921a7fdd5138cf32d7a8a" - -[[projects]] - name = "github.com/imkira/go-interpol" - packages = ["."] - revision = "5accad8134979a6ac504d456a6c7f1c53da237ca" - version = "v1.1.0" - -[[projects]] - branch = "master" - name = "github.com/iris-contrib/formBinder" - packages = ["."] - revision = "ad9fb86c356f971f30319c40ddbdcf72129a2791" - -[[projects]] - branch = "master" - name = "github.com/iris-contrib/httpexpect" - packages = ["."] - revision = "ebe99fcebbcedf6e7916320cce24c3e1832766ac" - -[[projects]] - branch = "master" - name = "github.com/json-iterator/go" - packages = ["."] - revision = "13f86432b882000a51c6e610c620974462691a97" - -[[projects]] - branch = "master" - name = "github.com/juju/errors" - packages = ["."] - revision = "c7d06af17c68cd34c835053720b21f6549d9b0ee" - -[[projects]] - branch = "master" - name = "github.com/kataras/golog" - packages = ["."] - revision = "dd676348ce75fa471fbbcd1bbbd131d00179756a" - -[[projects]] - branch = "master" - name = "github.com/kataras/pio" - packages = [".","terminal"] - revision = "825e39f34365e7db2c9fbc3692c16220e3bd7418" - -[[projects]] - branch = "v2" - name = "github.com/kataras/survey" - packages = [".","terminal","core"] - revision = "00934ae069eda15df26fa427ac393e67e239380c" - -[[projects]] - name = "github.com/klauspost/compress" - packages = ["flate","gzip"] - revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf" - version = "v1.2.1" - -[[projects]] - name = "github.com/klauspost/cpuid" - packages = ["."] - revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da" - version = "v1.1" - -[[projects]] - name = "github.com/klauspost/crc32" - packages = ["."] - revision = "cb6bfca970f6908083f26f39a79009d608efd5cd" - version = "v1.1" - -[[projects]] - branch = "master" - name = "github.com/microcosm-cc/bluemonday" - packages = ["."] - revision = "1c44c0b45a213b21569d6dbf440c47191652566f" - -[[projects]] - branch = "master" - name = "github.com/moul/http2curl" - packages = ["."] - revision = "4e24498b31dba4683efb9d35c1c8a91e2eda28c8" - -[[projects]] - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - name = "gopkg.in/russross/blackfriday.v2" - packages = ["."] - revision = "cadec560ec52d93835bf2f15bd794700d3a2473b" - version = "v2.0.0" - -[[projects]] - branch = "master" - name = "github.com/ryanuber/columnize" - packages = ["."] - revision = "abc90934186a77966e2beeac62ed966aac0561d5" - -[[projects]] - branch = "v2" - name = "github.com/iris-contrib/go.uuid" - packages = ["."] - revision = "36e9d2ebbde5e3f13ab2e25625fd453271d6522e" - -[[projects]] - branch = "master" - name = "github.com/sergi/go-diff" - packages = ["diffmatchpatch"] - revision = "feef008d51ad2b3778f85d387ccf91735543008d" - -[[projects]] - branch = "master" - name = "github.com/shurcooL/sanitized_anchor_name" - packages = ["."] - revision = "541ff5ee47f1dddf6a5281af78307d921524bcb5" - -[[projects]] - name = "github.com/stretchr/testify" - packages = ["assert","require"] - revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" - version = "v1.1.4" - -[[projects]] - branch = "master" - name = "github.com/valyala/bytebufferpool" - packages = ["."] - revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7" - -[[projects]] - branch = "master" - name = "github.com/xeipuuv/gojsonpointer" - packages = ["."] - revision = "6fe8760cad3569743d51ddbb243b26f8456742dc" - -[[projects]] - branch = "master" - name = "github.com/xeipuuv/gojsonreference" - packages = ["."] - revision = "e02fc20de94c78484cd5ffb007f8af96be030a45" - -[[projects]] - branch = "master" - name = "github.com/xeipuuv/gojsonschema" - packages = ["."] - revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70" - -[[projects]] - branch = "master" - name = "github.com/yalp/jsonpath" - packages = ["."] - revision = "31a79c7593bb93eb10b163650d4a3e6ca190e4dc" - -[[projects]] - name = "github.com/yudai/gojsondiff" - packages = [".","formatter"] - revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" - version = "1.0.0" - -[[projects]] - branch = "master" - name = "github.com/yudai/golcs" - packages = ["."] - revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68" - -[[projects]] - branch = "master" - name = "golang.org/x/crypto" - packages = ["acme","acme/autocert"] - revision = "13931e22f9e72ea58bb73048bc752b48c6d4d4ac" - -[[projects]] - branch = "master" - name = "golang.org/x/net" - packages = ["html","html/atom","idna","publicsuffix"] - revision = "66aacef3dd8a676686c7ae3716979581e8b03c47" - -[[projects]] - branch = "master" - name = "golang.org/x/sys" - packages = ["unix"] - revision = "c28acc882ebcbfbe8ce9f0f14b9ac26ee138dd51" - -[[projects]] - branch = "master" - name = "golang.org/x/text" - packages = ["internal/gen","internal/triegen","internal/ucd","secure/bidirule","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable"] - revision = "de984aaade05b6b868d6ad3a063e045ed3609f20" - -[[projects]] - name = "gopkg.in/ini.v1" - packages = ["."] - revision = "20b96f641a5ea98f2f8619ff4f3e061cff4833bd" - version = "v1.28.2" - -[[projects]] - name = "gopkg.in/yaml.v2" - packages = ["."] - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "2.2.1" - -[[projects]] - branch = "master" - name = "github.com/Shopify/goreferrer" - packages = ["."] - revision = "aad8439df3bf67adb025382ee2e5f46a72fc9456" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - inputs-digest = "9669ba685c3ec1b0c93329b8457e8567e67cd9c11fd5ca1ecc1cc6e145775f60" - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index 0226fcefa0..0000000000 --- a/Gopkg.toml +++ /dev/null @@ -1,83 +0,0 @@ -[[constraint]] - name = "github.com/BurntSushi/toml" - version = "0.3.0" - -[[constraint]] - branch = "master" - name = "github.com/Joker/jade" - -[[constraint]] - branch = "master" - name = "github.com/iris-contrib/i18n" - -[[constraint]] - branch = "master" - name = "github.com/aymerick/raymond" - -[[constraint]] - branch = "master" - name = "github.com/eknkc/amber" - -[[constraint]] - name = "github.com/fatih/structs" - version = "1.0.0" - -[[constraint]] - branch = "master" - name = "github.com/flosch/pongo2" - -[[constraint]] - branch = "master" - name = "github.com/iris-contrib/formBinder" - -[[constraint]] - branch = "master" - name = "github.com/iris-contrib/httpexpect" - -[[constraint]] - branch = "master" - name = "github.com/json-iterator/go" - -[[constraint]] - branch = "master" - name = "github.com/kataras/golog" - -[[constraint]] - branch = "v2" - name = "github.com/kataras/survey" - -[[constraint]] - name = "github.com/klauspost/compress" - version = "1.2.1" - -[[constraint]] - branch = "master" - name = "github.com/microcosm-cc/bluemonday" - -[[constraint]] - branch = "v2" - name = "gopkg.in/russross/blackfriday.v2" - -[[constraint]] - branch = "master" - name = "github.com/ryanuber/columnize" - -[[constraint]] - branch = "v2" - name = "github.com/iris-contrib/go.uuid" - -[[constraint]] - branch = "master" - name = "github.com/valyala/bytebufferpool" - -[[constraint]] - branch = "master" - name = "golang.org/x/crypto" - -[[constraint]] - name = "gopkg.in/yaml.v2" - version = "2.2.1" - -[[constraint]] - branch = "master" - name = "github.com/Shopify/goreferrer" \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index b042fd7cf1..587e156268 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,443 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. +# Fr, 11 January 2019 | v11.1.1 + +Happy new year! This is a minor release, contains mostly bug fixes. + +Strange that we don't have major features in this release, right? Don't worry, I am not out of ideas (at least not yet!). +I have some features in-mind but lately I do not have the time to humanize those ideas for you due to my new position in [Netdata Inc.](https://github.com/netdata/netdata), so be patient and [stay-tuned](https://github.com/kataras/iris/stargazers). Read the current changelog below: + +- session/redis: fix unused service config var. IdleTimeout witch was replaced by default values. [#1140](https://github.com/kataras/iris/pull/1140) ([@d7561985](https://github.com/d7561985)) + +- fix [#1141](https://github.com/kataras/iris/issues/1141) and [#1142](https://github.com/kataras/iris/issues/1142). [2bd7a8e88777766d1f4cac7562feec304112d2b1](https://github.com/kataras/iris/commit/2bd7a8e88777766d1f4cac7562feec304112d2b1) (@kataras) + +- fix cache corruption due to recorder reuse. [#1146](https://github.com/kataras/iris/pull/1146) ([@Slamper](https://github.com/Slamper)) + +- add `StatusTooEarly`, compatible with: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425#Browser_compatibility. [31b2913447aa9e41e16a3eb33eb0019427e15cea](https://github.com/kataras/iris/commit/31b2913447aa9e41e16a3eb33eb0019427e15cea) (@kataras) + +- fix [#1164](https://github.com/kataras/iris/issues/1164). [701e8e46c20395f87fa34bf9fabd145074c7b78c](https://github.com/kataras/iris/commit/701e8e46c20395f87fa34bf9fabd145074c7b78c) (@kataras) + +- `context#ReadForm` can skip unkown fields by `IsErrPath(err)`, fixes: [#1157](https://github.com/kataras/iris/issues/1157). [1607bb5113568af6a34142f23bfa44903205b314](https://github.com/kataras/iris/commit/1607bb5113568af6a34142f23bfa44903205b314) (@kataras) + + +Doc updates: + +- fix grammar and misspell. [5069e9afd8700d20dfd04cdc008efd671b5d0b40](https://github.com/kataras/iris/commit/5069e9afd8700d20dfd04cdc008efd671b5d0b40) (@kataras) + +- fix link for httpexpect in README. [#1148](https://github.com/kataras/iris/pull/1148) ([@drenel18](https://github.com/drenel18)) + +- translate _examples/README.md into Chinese. [#1156](https://github.com/kataras/iris/pull/1156) ([@fduxiao](https://github.com/fduxiao)) + +- add https://github.com/snowlyg/IrisApiProject to starter kits (Chinese). [ea12533871253afc34e40e36ba658b51955ea82d](https://github.com/kataras/iris/commit/ea12533871253afc34e40e36ba658b51955ea82d) + +- add https://github.com/yz124/superstar to starter kits (Chinese). [0e734ff8445f07482c28881347c1e564dc5aab9c](https://github.com/kataras/iris/commit/0e734ff8445f07482c28881347c1e564dc5aab9c) + +# Su, 18 November 2018 | v11.1.0 + +PR: https://github.com/kataras/iris/pull/1130 + +This release contains a new feature for versioning your Iris APIs. The initial motivation and feature request came by https://github.com/kataras/iris/issues/1129. + +The [versioning](https://github.com/kataras/iris/tree/master/versioning) package provides [semver](https://semver.org/) versioning for your APIs. It implements all the suggestions written at [api-guidelines](https://github.com/byrondover/api-guidelines/blob/master/Guidelines.md#versioning) and more. + + +The version comparison is done by the [go-version](https://github.com/hashicorp/go-version) package. It supports matching over patterns like `">= 1.0, < 3"` and etc. + +## Features + +- per route version matching, a normal iris handler with "switch" cases via Map for version => handler +- per group versioned routes and deprecation API +- version matching like ">= 1.0, < 2.0" or just "2.0.1" and etc. +- version not found handler (can be customized by simply adding the versioning.NotFound: customNotMatchVersionHandler on the Map) +- version is retrieved from the "Accept" and "Accept-Version" headers (can be customized via middleware) +- respond with "X-API-Version" header, if version found. +- deprecation options with customizable "X-API-Warn", "X-API-Deprecation-Date", "X-API-Deprecation-Info" headers via `Deprecated` wrapper. + +## Get version + +Current request version is retrieved by `versioning.GetVersion(ctx)`. + +By default the `GetVersion` will try to read from: +- `Accept` header, i.e `Accept: "application/json; version=1.0"` +- `Accept-Version` header, i.e `Accept-Version: "1.0"` + +You can also set a custom version for a handler via a middleware by using the context's store values. +For example: +```go +func(ctx iris.Context) { + ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1.0")) + ctx.Next() +} +``` + +## Match version to handler + +The `versioning.NewMatcher(versioning.Map) iris.Handler` creates a single handler which decides what handler need to be executed based on the requested version. + +```go +app := iris.New() + +// middleware for all versions. +myMiddleware := func(ctx iris.Context) { + // [...] + ctx.Next() +} + +myCustomNotVersionFound := func(ctx iris.Context) { + ctx.StatusCode(404) + ctx.Writef("%s version not found", versioning.GetVersion(ctx)) +} + +userAPI := app.Party("/api/user") +userAPI.Get("/", myMiddleware, versioning.NewMatcher(versioning.Map{ + "1.0": sendHandler(v10Response), + ">= 2, < 3": sendHandler(v2Response), + versioning.NotFound: myCustomNotVersionFound, +})) +``` + +### Deprecation + +Using the `versioning.Deprecated(handler iris.Handler, options versioning.DeprecationOptions) iris.Handler` function you can mark a specific handler version as deprecated. + + +```go +v10Handler := versioning.Deprecated(sendHandler(v10Response), versioning.DeprecationOptions{ + // if empty defaults to: "WARNING! You are using a deprecated version of this API." + WarnMessage string + DeprecationDate time.Time + DeprecationInfo string +}) + +userAPI.Get("/", versioning.NewMatcher(versioning.Map{ + "1.0": v10Handler, + // [...] +})) +``` + +This will make the handler to send these headers to the client: + +- `"X-API-Warn": options.WarnMessage` +- `"X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))` +- `"X-API-Deprecation-Info": options.DeprecationInfo` + +> versioning.DefaultDeprecationOptions can be passed instead if you don't care about Date and Info. + +## Grouping routes by version + +Grouping routes by version is possible as well. + +Using the `versioning.NewGroup(version string) *versioning.Group` function you can create a group to register your versioned routes. +The `versioning.RegisterGroups(r iris.Party, versionNotFoundHandler iris.Handler, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`. + +```go +app := iris.New() + +userAPI := app.Party("/api/user") +// [... static serving, middlewares and etc goes here]. + +userAPIV10 := versioning.NewGroup("1.0") +userAPIV10.Get("/", sendHandler(v10Response)) + +userAPIV2 := versioning.NewGroup(">= 2, < 3") +userAPIV2.Get("/", sendHandler(v2Response)) +userAPIV2.Post("/", sendHandler(v2Response)) +userAPIV2.Put("/other", sendHandler(v2Response)) + +versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2) +``` + +> A middleware can be registered to the actual `iris.Party` only, using the methods we learnt above, i.e by using the `versioning.Match` in order to detect what code/handler you want to be executed when "x" or no version is requested. + +### Deprecation for Group + +Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want to notify your API consumers that this specific version is deprecated. + +```go +userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions) +``` + +## Compare version manually from inside your handlers + +```go +// reports if the "version" is matching to the "is". +// the "is" can be a constraint like ">= 1, < 3". +If(version string, is string) bool +``` + +```go +// same as `If` but expects a Context to read the requested version. +Match(ctx iris.Context, expectedVersion string) bool +``` + +```go +app.Get("/api/user", func(ctx iris.Context) { + if versioning.Match(ctx, ">= 2.2.3") { + // [logic for >= 2.2.3 version of your handler goes here] + return + } +}) +``` + +Example can be found [here](_examples/versioning/main.go). + +# Fr, 09 November 2018 | v11.0.4 + +Add `Configuration.DisablePathCorrectionRedirection` - `iris.WithoutPathCorrectionRedirection` to support +direct handler execution of the matching route without the last `'/'` instead of sending a redirect response when `DisablePathCorrection` is set to false(default behavior). + +Usage: + +For example, CORS needs the allow origin headers in redirect response as well, +however is not possible from the router to know what headers a route's handler will send to the client. +So the best option we have is to just execute the handler itself instead of sending a redirect response. +Add the `app.Run(..., iris.WithoutPathCorrectionRedirection)` on the server side if you wish +to directly fire the handler instead of redirection (which is the default behavior) +on request paths like `"$yourdomain/v1/mailer/"` when `"/v1/mailer"` route handler is registered. + +Example Code: + +```go +package main + +import "github.com/kataras/iris" + + +func main() { + app := iris.New() + + crs := func(ctx iris.Context) { + ctx.Header("Access-Control-Allow-Origin", "*") + ctx.Header("Access-Control-Allow-Credentials", "true") + ctx.Header("Access-Control-Allow-Headers", + "Access-Control-Allow-Origin,Content-Type") + ctx.Next() + } + + v1 := app.Party("/api/v1", crs).AllowMethods(iris.MethodOptions) + { + v1.Post("/mailer", func(ctx iris.Context) { + var any iris.Map + err := ctx.ReadJSON(&any) + if err != nil { + ctx.WriteString(err.Error()) + ctx.StatusCode(iris.StatusBadRequest) + return + } + ctx.Application().Logger().Infof("received %#+v", any) + }) + } + + // HERE: + app.Run(iris.Addr(":80"), iris.WithoutPathCorrectionRedirection) +} +``` + +# Tu, 06 November 2018 | v11.0.3 + +- add "part" html view engine's tmpl function: [15bb55d](https://github.com/kataras/iris/commit/15bb55d85eac378bbe0c98c10ffea938cc05fe4d) + +- update pug engine's vendor: [c20bc3b](https://github.com/kataras/iris/commit/c20bc3bceef158ef99931e609123fa0aca2a918c) + +# Tu, 30 October 2018 | v11.0.2 + +Fix [memstore](core/memstore/memstore.go) overflows when build 32 bit app, reported and fixed by [@bouroo](https://github.com/bouroo) at: https://github.com/kataras/iris/issues/1118 + +# Su, 28 October 2018 | v11.0.1 + +- Update benchmarks: https://github.com/kataras/iris/commit/d1b47b1ec65ae77a2ca7485e510386f4a5456ac4 +- Add link for third-party source benchmarks: https://github.com/kataras/iris/commit/64e80a7ee5c23ed938ddc8b68d181a25420c7653 +- Add optionally custom low-level websocket message data prefix as requested at: https://github.com/kataras/iris/issues/1113 by [@jjhesk](https://github.com/jjhesk). Example: + +```go +app := iris.New() + +// [...] +wsServer := websocket.New(websocket.Config{ + // [...] + EvtMessagePrefix: []byte("my-custom-prefix:"), +}) + +// [...] + +// serve the javascript built'n client-side library, +// see websockets.html script tags, this path is used. +app.Any("/iris-ws.js", func(ctx iris.Context) { + ctx.Write(wsServer.ClientSource) +}) + +// [...] +``` + +# Su, 21 October 2018 | v11.0.0 + +For the craziest of us, click [here](https://github.com/kataras/iris/compare/v10.7.0...v11) 🔥 to find out the commits and the code changes since our previous release. + +## Breaking changes + +- Remove the "Configurator" `WithoutVersionChecker` and the configuration field `DisableVersionChecker` +- `:int` parameter type **can accept negative numbers now**. +- `app.Macros().String/Int/Uint64/Path...RegisterFunc` should be replaced to: `app.Macros().Get("string" or "int" or "uint64" or "path" when "path" is the ":path" parameter type).RegisterFunc`, because you can now add custom macros and parameter types as well, see [here](_examples/routing/macros). +- `RegisterFunc("min", func(paramValue string) bool {...})` should be replaced to `RegisterFunc("min", func(paramValue ) bool {...})`, the `paramValue` argument is now stored in the exact type the macro's type evaluator inits it, i.e `uint64` or `int` and so on, therefore you don't have to convert the parameter value each time (this should make your handlers with macro functions activated even faster now) +- The `Context#ReadForm` will no longer return an error if it has no value to read from the request, we let those checks to the caller and validators as requested at: https://github.com/kataras/iris/issues/1095 by [@haritsfahreza](https://github.com/haritsfahreza) + +## Routing + +I wrote a [new router implementation](https://github.com/kataras/muxie#philosophy) for our Iris internal(low-level) routing mechanism, it is good to know that this was the second time we have updated the router internals without a single breaking change after the v6, thanks to the very well-written and designed-first code we have for the high-level path syntax component called [macro interpreter](macro/interpreter). + +The new router supports things like **closest wildcard resolution**. + +> If the name doesn't sound good to you it is because I named that feature myself, I don't know any other framework or router that supports a thing like that so be gentle:) + +Previously you couldn't register routes like: `/{myparam:path}` and `/static` and `/{myparam:string}` and `/{myparam:string}/static` and `/static/{myparam:string}` all in one path prefix without a "decision handler". And generally if you had a wildcard it was possible to add (a single) static part and (a single) named parameter but not without performance cost and limits, why only one? (one is better than nothing: look the Iris' alternatives) We struggle to overcome our own selves, now you **can definitely do it without a bit of performance cost**, and surely we hand't imagine the wildcard to **catch all if nothing else found** without huge routing performance cost, the wildcard(`:path`) meant ONLY: "accept one or more path segments and put them into the declared parameter" so if you had register a dynamic single-path-segment named parameter like `:string, :int, :uint, :alphabetical...` in between those path segments it wouldn't work. The **closest wildcard resolution** offers you the opportunity to design your APIs even better via custom handlers and error handlers like `404 not found` to path prefixes for your API's groups, now you can do it without any custom code for path resolution inside a "decision handler" or a middleware. + +Code worths 1000 words, now it is possible to define your routes like this without any issues: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/context" +) + +func main() { + app := iris.New() + + // matches everyhing if nothing else found, + // so you can use it for custom 404 root-level/main pages! + app.Get("/{p:path}", func(ctx context.Context) { + path := ctx.Params().Get("p") + // gives the path without the first "/". + ctx.Writef("Site Custom 404 Error Message\nPage of: '%s' not found", path) + }) + + app.Get("/", indexHandler) + + // request: http://localhost:8080/profile + // response: "Profile Index" + app.Get("/profile", func(ctx context.Context) { + ctx.Writef("Profile Index") + }) + + // request: http://localhost:8080/profile/kataras + // response: "Profile of username: 'kataras'" + app.Get("/profile/{username}", func(ctx context.Context) { + username := ctx.Params().Get("username") + ctx.Writef("Profile of username: '%s'", username) + }) + + // request: http://localhost:8080/profile/settings + // response: "Profile personal settings" + app.Get("/profile/settings", func(ctx context.Context) { + ctx.Writef("Profile personal settings") + }) + + // request: http://localhost:8080/profile/settings/security + // response: "Profile personal security settings" + app.Get("/profile/settings/security", func(ctx context.Context) { + ctx.Writef("Profile personal security settings") + }) + + // matches everyhing /profile/*somethng_here* + // if no other route matches the path semgnet after the + // /profile or /profile/ + // + // So, you can use it for custom 404 profile pages + // side-by-side to your root wildcard without issues! + // For example: + // request: http://localhost:8080/profile/kataras/what + // response: + // Profile Page Custom 404 Error Message + // Profile Page of: 'kataras/what' was unable to be found + app.Get("/profile/{p:path}", func(ctx context.Context) { + path := ctx.Params().Get("p") + ctx.Writef("Profile Page Custom 404 Error Message\nProfile Page of: '%s' not found", path) + }) + + app.Run(iris.Addr(":8080")) +} + +func indexHandler(ctx context.Context) { + ctx.HTML("This is the index page") +} + +``` + +The `github.com/kataras/iris/core/router.AllMethods` is now a variable that can be altered by end-developers, so things like `app.Any` can register to custom methods as well, as requested at: https://github.com/kataras/iris/issues/1102. For example, import that package and do `router.AllMethods = append(router.AllMethods, "LINK")` in your `main` or `init` function. + +The old `github.com/kataras/iris/core/router/macro` package was moved to `guthub.com/kataras/iris/macro` to allow end-developers to add custom parameter types and macros, it supports all go standard types by default as you will see below. + +- `:int` parameter type as an alias to the old `:int` which can accept any numeric path segment now, both negative and positive numbers +- Add `:int8` parameter type and `ctx.Params().GetInt8` +- Add `:int16` parameter type and `ctx.Params().GetInt16` +- Add `:int32` parameter type and `ctx.Params().GetInt32` +- Add `:int64` parameter type and `ctx.Params().GetInt64` +- Add `:uint` parameter type and `ctx.Params().GetUint` +- Add `:uint8` parameter type and `ctx.Params().GetUint8` +- Add `:uint16` parameter type and `ctx.Params().GetUint16` +- Add `:uint32` parameter type and `ctx.Params().GetUint32` +- Add `:uint64` parameter type and `ctx.Params().GetUint64` +- Add alias `:bool` for the `:boolean` parameter type + +Here is the full list of the built'n parameter types that we support now, including their validations/path segment rules. + +| Param Type | Go Type | Validation | Retrieve Helper | +| -----------------|------|-------------|------| +| `:string` | string | the default if param type is missing, anything (single path segment) | `Params().Get` | +| `:int` | int | -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch | `Params().GetInt` | +| `:int8` | int8 | -128 to 127 | `Params().GetInt8` | +| `:int16` | int16 | -32768 to 32767 | `Params().GetInt16` | +| `:int32` | int32 | -2147483648 to 2147483647 | `Params().GetInt32` | +| `:int64` | int64 | -9223372036854775808 to 9223372036854775807 | `Params().GetInt64` | +| `:uint` | uint | 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32), depends on the host arch | `Params().GetUint` | +| `:uint8` | uint8 | 0 to 255 | `Params().GetUint8` | +| `:uint16` | uint16 | 0 to 65535 | `Params().GetUint16` | +| `:uint32` | uint32 | 0 to 4294967295 | `Params().GetUint32` | +| `:uint64` | uint64 | 0 to 18446744073709551615 | `Params().GetUint64` | +| `:bool` | bool | "1" or "t" or "T" or "TRUE" or "true" or "True" or "0" or "f" or "F" or "FALSE" or "false" or "False" | `Params().GetBool` | +| `:alphabetical` | string | lowercase or uppercase letters | `Params().Get` | +| `:file` | string | lowercase or uppercase letters, numbers, underscore (_), dash (-), point (.) and no spaces or other special characters that are not valid for filenames | `Params().Get` | +| `:path` | string | anything, can be separated by slashes (path segments) but should be the last part of the route path | `Params().Get` | + +**Usage**: + +```go +app.Get("/users/{id:uint64}", func(ctx iris.Context){ + id, _ := ctx.Params().GetUint64("id") + // [...] +}) +``` + +| Built'n Func | Param Types | +| -----------|---------------| +| `regexp`(expr string) | :string | +| `prefix`(prefix string) | :string | +| `suffix`(suffix string) | :string | +| `contains`(s string) | :string | +| `min`(minValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | +| `max`(maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | +| `range`(minValue, maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | + +**Usage**: + +```go +app.Get("/profile/{name:alphabetical max(255)}", func(ctx iris.Context){ + name := ctx.Params().Get("name") + // len(name) <=255 otherwise this route will fire 404 Not Found + // and this handler will not be executed at all. +}) +``` + +## Vendoring + +- Rename the vendor `sessions/sessiondb/vendor/...bbolt` from `coreos/bbolt` to `etcd-io/bbolt` and update to v1.3.1, based on [that](https://github.com/etcd-io/bbolt/releases/tag/v1.3.1-etcd.7) +- Update the vendor `sessions/sessiondb/vendor/...badger` to v1.5.3 + +I believe it is soon to adapt the new [go modules](https://github.com/golang/go/wiki/Modules#table-of-contents) inside Iris, the new `go mod` command may change until go 1.12, it is still an experimental feature. +The [vendor](https://github.com/kataras/iris/tree/master/vendor) folder will be kept until the majority of Go developers get acquainted with the new `go modules`. The `go.mod` and `go.sum` files will come at `iris v12` (or `go 1.12`), we could do that on this version as well but I don't want to have half-things, versioning should be passed on import path as well and that is a large breaking change to go with it right now, so it will probably have a new path such as `github.com/kataras/iris/v12` based on a `git tag` like every Iris release (we are lucky here because we used semantic versioning from day zero). No folder re-structure inside the root git repository to split versions will ever happen, so backwards-compatibility for older go versions(before go 1.9.3) and iris versions will be not enabled by-default although it's easy for anyone to grab any version from older [releases](https://github.com/kataras/iris/releases) or branch and target that. + # Sat, 11 August 2018 | v10.7.0 I am overjoyed to announce stage 1 of the the Iris Web framework **10.7 stable release is now available**. @@ -228,7 +665,7 @@ For example: at [_examples/mvc/basic/main.go line 100](_examples/mvc/basic/main. - fix `APIBuilder, Party#StaticWeb` and `APIBuilder, Party#StaticEmbedded` wrong strip prefix inside children parties - keep the `iris, core/router#StaticEmbeddedHandler` and remove the `core/router/APIBuilder#StaticEmbeddedHandler`, (note the `Handler` suffix) it's global and has nothing to do with the `Party` or the `APIBuilder` -- fix high path cleaning between `{}` (we already escape those contents at the [interpreter](core/router/macro/interpreter) level but some symbols are still removed by the higher-level api builder) , i.e `\\` from the string's macro function `regex` contents as reported at [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) +- fix high path cleaning between `{}` (we already escape those contents at the [interpreter](macro/interpreter) level but some symbols are still removed by the higher-level api builder) , i.e `\\` from the string's macro function `regex` contents as reported at [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) - sync the `golang.org/x/sys/unix` vendor ## The most important diff --git a/HISTORY_GR.md b/HISTORY_GR.md index 30f1674517..34f6a5f2cc 100644 --- a/HISTORY_GR.md +++ b/HISTORY_GR.md @@ -17,9 +17,33 @@ **Πώς να αναβαθμίσετε**: Ανοίξτε την γραμμή εντολών σας και εκτελέστε αυτήν την εντολή: `go get -u github.com/kataras/iris` ή αφήστε το αυτόματο updater να το κάνει αυτό για σας. +# Fr, 11 January 2019 | v11.1.1 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111) για να διαβάσετε στα αγγλικά. + +# Su, 18 November 2018 | v11.1.0 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) για να διαβάσετε στα αγγλικά για το νέο "versioning" feature. + +# Fr, 09 November 2018 | v11.0.4 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) για να διαβάσετε στα αγγλικά τις αλλαγές που φέρνει το τελευταίο patch για την έκδοση 11. + +# Tu, 30 October 2018 | v11.0.2 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-30-october-2018--v1102) για να διαβάσετε στα αγγλικά μια σημαντική διόρθωση για τα x32 machines που φέρνει το τελευταίο patch για την έκδοση 11. + +# Su, 28 October 2018 | v11.0.1 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#su-28-october-2018--v1101) για να διαβάσετε στα αγγλικά τις αλλαγές και το νέο feature που φέρνει το τελευταίο patch για την έκδοση 11. + +# Su, 21 October 2018 | v11.0.0 + +Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#su-21-october-2018--v1100) για να διαβάσετε στα αγγλικά τις αλλαγές και τα νέα features που φέρνει νεότερη έκδοση του Iris, version 11. + # Sat, 11 August 2018 | v10.7.0 -Είμαι στην πραγματικά ευχάριστη θέση να σας ανακοινώσω το πρώτο στάδιο της σταθερής κυκλοφορίας της έκδοσης **10.7** του Iris Web Framework. +Είμαι στην, πραγματικά, ευχάριστη θέση να σας ανακοινώσω το πρώτο στάδιο της σταθερής κυκλοφορίας της έκδοσης **10.7** του Iris Web Framework. Η έκδοση 10.7.0 είναι μέρος των [επίσημων διανομών μας](https://github.com/kataras/iris/releases). diff --git a/HISTORY_ID.md b/HISTORY_ID.md index 001bdab1d0..f69ea630b6 100644 --- a/HISTORY_ID.md +++ b/HISTORY_ID.md @@ -17,6 +17,30 @@ Developers tidak diwajibkan untuk melakukan upgrade apabila mereka tidak membutu **Cara Upgrade**: Bukan command-line anda dan eksekuis perintah ini: `go get -u github.com/kataras/iris` atau biarkan updater otomatis melakukannya untuk anda. +# Fr, 11 January 2019 | v11.1.1 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111) instead. + +# Su, 18 November 2018 | v11.1.0 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) instead. + +# Fr, 09 November 2018 | v11.0.4 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) instead. + +# Tu, 30 October 2018 | v11.0.2 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-30-october-2018--v1102) instead. + +# Su, 28 October 2018 | v11.0.1 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-28-october-2018--v1101) instead. + +# Su, 21 October 2018 | v11.0.0 + +This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-21-october-2018--v1100) instead. + # Sat, 11 August 2018 | v10.7.0 This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#sat-11-august-2018--v1070) instead. @@ -83,7 +107,7 @@ For example: at [_examples/mvc/basic/main.go line 100](_examples/mvc/basic/main. - fix `APIBuilder, Party#StaticWeb` and `APIBuilder, Party#StaticEmbedded` wrong strip prefix inside children parties - keep the `iris, core/router#StaticEmbeddedHandler` and remove the `core/router/APIBuilder#StaticEmbeddedHandler`, (note the `Handler` suffix) it's global and has nothing to do with the `Party` or the `APIBuilder` -- fix high path cleaning between `{}` (we already escape those contents at the [interpreter](core/router/macro/interpreter) level but some symbols are still removed by the higher-level api builder) , i.e `\\` from the string's macro function `regex` contents as reported at [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) +- fix high path cleaning between `{}` (we already escape those contents at the [interpreter](macro/interpreter) level but some symbols are still removed by the higher-level api builder) , i.e `\\` from the string's macro function `regex` contents as reported at [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) - sync the `golang.org/x/sys/unix` vendor ## The most important diff --git a/HISTORY_ZH.md b/HISTORY_ZH.md index 674ede1a38..d66a4fcccc 100644 --- a/HISTORY_ZH.md +++ b/HISTORY_ZH.md @@ -17,6 +17,30 @@ **如何升级**: 打开命令行执行以下命令: `go get -u github.com/kataras/iris` 或者等待自动更新。 +# Fr, 11 January 2019 | v11.1.1 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111) instead. + +# Su, 18 November 2018 | v11.1.0 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) instead. + +# Fr, 09 November 2018 | v11.0.4 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) instead. + +# Tu, 30 October 2018 | v11.0.2 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-30-october-2018--v1102) instead. + +# Su, 28 October 2018 | v11.0.1 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-28-october-2018--v1101) instead. + +# Su, 21 October 2018 | v11.0.0 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-21-october-2018--v1100) instead. + # Sat, 11 August 2018 | v10.7.0 This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#sat-11-august-2018--v1070) instead. @@ -79,7 +103,7 @@ This history entry is not translated yet to the Chinese language yet, please ref - 修正 `APIBuilder, Party#StaticWeb` 和 `APIBuilder, Party#StaticEmbedded` 子分组内的前缀错误 - 保留 `iris, core/router#StaticEmbeddedHandler` 并移除 `core/router/APIBuilder#StaticEmbeddedHandler`, (`Handler` 后缀) 这是全局性的,与 `Party` `APIBuilder` 无关。 -- 修正 路径 `{}` 中的路径清理 (我们已经在 [解释器](core/router/macro/interpreter) 级别转义了这些字符, 但是一些符号仍然被更高级别的API构建器删除) , 例如 `\\` 字符串的宏函数正则表达式内容 [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) +- 修正 路径 `{}` 中的路径清理 (我们已经在 [解释器](macro/interpreter) 级别转义了这些字符, 但是一些符号仍然被更高级别的API构建器删除) , 例如 `\\` 字符串的宏函数正则表达式内容 [927](https://github.com/kataras/iris/issues/927) by [commit e85b113476eeefffbc7823297cc63cd152ebddfd](https://github.com/kataras/iris/commit/e85b113476eeefffbc7823297cc63cd152ebddfd) - 同步 `golang.org/x/sys/unix` ## 重要变更 diff --git a/README.md b/README.md index f801ffee8b..c5f97e9ae8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ +# ⚡️ Update: community-driven version 11.1.0 + +Click [here](HISTORY.md#su-18-november-2018--v1110) to read about the versioning API that the most recent version of Iris brings to you. + # Iris Web Framework -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/routing%20by-example-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris is a fast, simple yet fully featured and very efficient web framework for Go. @@ -22,17 +26,6 @@ $ go get -u github.com/kataras/iris Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. -[![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new-gray.png)](_benchmarks/README_UNIX.md) - -_Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ - -
-Benchmarks from third-party source over the rest web frameworks - -![Comparison with other frameworks](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) - -
-
Known issues for code editors and IDEs at general @@ -52,6 +45,22 @@ import (
+## Benchmarks + +### Iris vs .NET Core vs Expressjs + +[![Iris vs .NET Core(C#) vs Node.js (Express)](_benchmarks/benchmarks_graph_22_october_2018_gray.png)](_benchmarks/README.md) + +_Updated at: [Monday, 22 October 2018](_benchmarks/README.md)_ + +### Iris vs the rest Go web frameworks and routers vs any other alternative + +[![](_benchmarks/benchmarks_third_party_source_snapshot_go_23_october_2018.png)](https://github.com/the-benchmarker/web-frameworks#full-table) + +As shown in the benchmarks (from a [third-party source](https://github.com/the-benchmarker)), Iris is the fastest open-source Go web framework in the planet. The net/http 100% compatible router [muxie](https://github.com/kataras/muxie) I've created some weeks ago is also trending there with amazing results, fastest net/http router ever created as well. View the results at: + +https://github.com/the-benchmarker/web-frameworks#full-table + ## Philosophy The Iris philosophy is to provide robust tooling for HTTP, making it a great solution for single page applications, web sites, hybrids, or public HTTP APIs. Keep note that, so far, iris is the fastest web framework ever created in terms of performance. @@ -87,6 +96,35 @@ func main() { $ go run example.go ``` +## Iris starter kits + + + +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) + +> Did you build something similar? Let us [know](https://github.com/kataras/iris/pulls)! + ## API Examples ### Using Get, Post, Put, Patch, Delete and Options @@ -113,10 +151,18 @@ func main() { | Param Type | Go Type | Validation | Retrieve Helper | | -----------------|------|-------------|------| -| `:string` | string | anything | `Params().Get` | -| `:int` | uint, uint8, uint16, uint32, uint64, int, int8, int32, int64 | positive number, no digits limit | `Params().GetInt/Int64`...| -| `:long` | int64 | -9223372036854775808 to 9223372036854775807 | `Params().GetInt64` | -| `:boolean` | bool | "1" or "t" or "T" or "TRUE" or "true" or "True" or "0" or "f" or "F" or "FALSE" or "false" or "False" | `Params().GetBool` | +| `:string` | string | anything (single path segment) | `Params().Get` | +| `:int` | int | -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch | `Params().GetInt` | +| `:int8` | int8 | -128 to 127 | `Params().GetInt8` | +| `:int16` | int16 | -32768 to 32767 | `Params().GetInt16` | +| `:int32` | int32 | -2147483648 to 2147483647 | `Params().GetInt32` | +| `:int64` | int64 | -9223372036854775808 to 9223372036854775807 | `Params().GetInt64` | +| `:uint` | uint | 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32), depends on the host arch | `Params().GetUint` | +| `:uint8` | uint8 | 0 to 255 | `Params().GetUint8` | +| `:uint16` | uint16 | 0 to 65535 | `Params().GetUint16` | +| `:uint32` | uint32 | 0 to 4294967295 | `Params().GetUint32` | +| `:uint64` | uint64 | 0 to 18446744073709551615 | `Params().GetUint64` | +| `:bool` | bool | "1" or "t" or "T" or "TRUE" or "true" or "True" or "0" or "f" or "F" or "FALSE" or "false" or "False" | `Params().GetBool` | | `:alphabetical` | string | lowercase or uppercase letters | `Params().Get` | | `:file` | string | lowercase or uppercase letters, numbers, underscore (_), dash (-), point (.) and no spaces or other special characters that are not valid for filenames | `Params().Get` | | `:path` | string | anything, can be separated by slashes (path segments) but should be the last part of the route path | `Params().Get` | @@ -124,8 +170,8 @@ func main() { **Usage**: ```go -app.Get("/users/{id:int64}", func(ctx iris.Context){ - id, _ := ctx.Params().GetInt64("id") +app.Get("/users/{id:uint64}", func(ctx iris.Context){ + id := ctx.Params().GetUint64Default("id", 0) // [...] }) ``` @@ -136,9 +182,9 @@ app.Get("/users/{id:int64}", func(ctx iris.Context){ | `prefix`(prefix string) | :string | | `suffix`(suffix string) | :string | | `contains`(s string) | :string | -| `min`(minValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int64 | -| `max`(maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int64 | -| `range`(minValue, maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :int, :int64 | +| `min`(minValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | +| `max`(maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :string(char length), :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | +| `range`(minValue, maxValue int or int8 or int16 or int32 or int64 or uint8 or uint16 or uint32 or uint64 or float32 or float64) | :int, :int8, :int16, :int32, :int64, :uint, :uint8, :uint16, :uint32, :uint64 | **Usage**: @@ -162,7 +208,7 @@ latLonRegex, _ := regexp.Compile(latLonExpr) // Register your custom argument-less macro function to the :string param type. // MatchString is a type of func(string) bool, so we use it as it is. -app.Macros().String.RegisterFunc("coordinate", latLonRegex.MatchString) +app.Macros().Get("string").RegisterFunc("coordinate", latLonRegex.MatchString) app.Get("/coordinates/{lat:string coordinate()}/{lon:string coordinate()}", func(ctx iris.Context) { ctx.Writef("Lat: %s | Lon: %s", ctx.Params().Get("lat"), ctx.Params().Get("lon")) @@ -173,7 +219,7 @@ Register your custom macro function which accepts two int arguments. ```go -app.Macros().String.RegisterFunc("range", func(minLength, maxLength int) func(string) bool { +app.Macros().Get("string").RegisterFunc("range", func(minLength, maxLength int) func(string) bool { return func(paramValue string) bool { return len(paramValue) >= minLength && len(paramValue) <= maxLength } @@ -189,7 +235,7 @@ app.Get("/limitchar/{name:string range(1,200) else 400}", func(ctx iris.Context) Register your custom macro function which accepts a slice of strings `[...,...]`. ```go -app.Macros().String.RegisterFunc("has", func(validNames []string) func(string) bool { +app.Macros().Get("string").RegisterFunc("has", func(validNames []string) func(string) bool { return func(paramValue string) bool { for _, validName := range validNames { if validName == paramValue { @@ -201,7 +247,7 @@ app.Macros().String.RegisterFunc("has", func(validNames []string) func(string) b } }) -app.Get("/static_validation/{name:string has([kataras,gerasimos,maropoulos]}", func(ctx iris.Context) { +app.Get("/static_validation/{name:string has([kataras,gerasimos,maropoulos])}", func(ctx iris.Context) { name := ctx.Params().Get("name") ctx.Writef(`Hello %s | the name should be "kataras" or "gerasimos" or "maropoulos" otherwise this handler will not be executed`, name) @@ -212,32 +258,32 @@ app.Get("/static_validation/{name:string has([kataras,gerasimos,maropoulos]}", f ```go func main() { - app := iris.Default() + app := iris.Default() - // This handler will match /user/john but will not match neither /user/ or /user. - app.Get("/user/{name}", func(ctx iris.Context) { - name := ctx.Params().Get("name") - ctx.Writef("Hello %s", name) - }) - - // This handler will match /users/42 - // but will not match - // neither /users or /users/. - app.Get("/users/{id:long}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt64("id") - ctx.Writef("User with ID: %d", id) - }) - - // This handler will match /user/john/send - // but will not match /user/john/ - app.Post("/user/{name:string}/{action:path}", func(ctx iris.Context) { - name := ctx.Params().Get("name") - action := ctx.Params().Get("action") - message := name + " is " + action - ctx.WriteString(message) - }) + // This handler will match /user/john but will not match neither /user/ or /user. + app.Get("/user/{name}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + ctx.Writef("Hello %s", name) + }) - app.Run(iris.Addr(":8080")) + // This handler will match /users/42 + // but will not match /users/-1 because uint should be bigger than zero + // neither /users or /users/. + app.Get("/users/{id:uint64}", func(ctx iris.Context) { + id := ctx.Params().GetUint64Default("id", 0) + ctx.Writef("User with ID: %d", id) + }) + + // However, this one will match /user/john/send and also /user/john/everything/else/here + // but will not match /user/john neither /user/john/. + app.Post("/user/{name:string}/{action:path}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + action := ctx.Params().Get("action") + message := name + " is " + action + ctx.WriteString(message) + }) + + app.Run(iris.Addr(":8080")) } ``` @@ -245,6 +291,7 @@ func main() { > Learn more about path parameter's types by navigating [here](_examples/routing/dynamic-path/main.go#L31). + ### Dependency Injection The package [hero](hero) contains features for binding any object or functions that `handlers` can use, these are called dependencies. @@ -586,7 +633,6 @@ func main() { app.Run( iris.Addr(":8080"), iris.WithoutBanner, - iris.WithoutVersionChecker, iris.WithoutServerError(iris.ErrServerClosed), ) } @@ -636,7 +682,7 @@ func main() { // Register validation for 'User' // NOTE: only have to register a non-pointer type for 'User', validator - // interanlly dereferences during it's type checks. + // internally dereferences during it's type checks. validate.RegisterStructValidation(UserStructLevelValidation, User{}) app := iris.New() @@ -938,32 +984,7 @@ First of all, the most correct way to begin with a web framework is to learn the - Navigate through **100+1** **[examples](_examples)** and some [iris starter kits](#iris-starter-kits) we crafted for you - Read the [godocs](https://godoc.org/github.com/kataras/iris) for any details - Prepare a cup of coffee or tea, whatever pleases you the most, and read some [articles](#articles) we found for you - -### Iris starter kits - - - -1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) -5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) - -> Did you build something similar? Let us [know](https://github.com/kataras/iris/pulls)! +- Run some of our [starter kits](#iris-starter-kits) ### Middleware @@ -973,6 +994,7 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma ### Articles +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) @@ -998,7 +1020,7 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma ## Support -- [HISTORY](HISTORY.md#sat-11-august-2018--v1070) file is your best friend, it contains information about the latest features and changes +- [HISTORY](HISTORY.md#fr-11-january-2019--v1111) file is your best friend, it contains information about the latest features and changes - Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues) - Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com) - Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/README_GR.md b/README_GR.md index 8fb8f43acd..19399496c4 100644 --- a/README_GR.md +++ b/README_GR.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go. @@ -108,7 +108,7 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο ## Υποστήριξη -- To [HISTORY](HISTORY_GR.md#sat-11-august-2018--v1070) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές +- To [HISTORY](HISTORY_GR.md#fr-11-january-2019--v1111) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές - Μήπως τυχαίνει να βρήκατε κάποιο bug; Δημοσιεύστε το στα [github issues](https://github.com/kataras/iris/issues) - Έχετε οποιεσδήποτε ερωτήσεις ή πρέπει να μιλήσετε με κάποιον έμπειρο για την επίλυση ενός προβλήματος σε πραγματικό χρόνο; Ελάτε μαζί μας στην [συνομιλία κοινότητας](https://chat.iris-go.com) - Συμπληρώστε την αναφορά εμπειρίας χρήστη κάνοντας κλικ [εδώ](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) @@ -182,14 +182,17 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο | Quickstart for Iris with Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) -5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > Έχετε χτίσει κάτι παρόμοιο; [Ενημέρωσέ μας](https://github.com/kataras/iris/pulls)! @@ -201,6 +204,7 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο ### Articles +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) diff --git a/README_ID.md b/README_ID.md index 1839c15c94..687b42604f 100644 --- a/README_ID.md +++ b/README_ID.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris adalah web framework yang cepat, sederhana namun berfitur lengkap dan sangat efisien untuk Go. @@ -106,7 +106,7 @@ _Diperbarui pada: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ ## Dukungan -- File [HISTORY](HISTORY_ID.md#sat-11-august-2018--v1070) adalah sahabat anda, file tersebut memiliki informasi terkait fitur dan perubahan terbaru +- File [HISTORY](HISTORY_ID.md#fr-11-january-2019--v1111) adalah sahabat anda, file tersebut memiliki informasi terkait fitur dan perubahan terbaru - Apakah anda menemukan bug? Laporkan itu melalui [github issues](https://github.com/kataras/iris/issues) - Apakah anda memiliki pertanyaan atau butuh untuk bicara kepada seseorang yang sudah berpengalaman untuk menyelesaikan masalah secara langsung? Gabung bersama kami di [community chat](https://chat.iris-go.com) - Lengkapi laporan user-experience berbasis formulir kami dengan tekan [disini](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) @@ -180,14 +180,17 @@ Pertama - tama, cara yang paling tepat untuk memulai dengan web framework adalah | Quickstart for Iris with Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) -5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > Apakah anda membuat hal yang serupa? [Beritahu kami](https://github.com/kataras/iris/pulls)! @@ -199,6 +202,7 @@ Iris, tidak seperti yang lain, 100% compatible dengan standards dan maka dari it ### Articles +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) diff --git a/README_JPN.md b/README_JPN.md index e2da517d16..60cd9ff2f2 100644 --- a/README_JPN.md +++ b/README_JPN.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Irisはシンプルで高速、それにも関わらず充実した機能を有する効率的なGo言語のウェブフレームワークです。 @@ -106,7 +106,7 @@ _Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ ## 支援 -- [HISTORY](HISTORY.md#sat-11-august-2018--v1070)ファイルはあなたの友人です。このファイルには、機能に関する最新の情報や変更点が記載されています。 +- [HISTORY](HISTORY.md#fr-11-january-2019--v1111)ファイルはあなたの友人です。このファイルには、機能に関する最新の情報や変更点が記載されています。 - バグを発見しましたか?[github issues](https://github.com/kataras/iris/issues)に投稿をお願い致します。 - 質問がありますか?または問題を即時に解決するため、熟練者に相談する必要がありますか?[community chat](https://chat.iris-go.com)に参加しましょう。 - [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)をクリックしてユーザーとしての体験を報告しましょう。 @@ -180,14 +180,17 @@ Irisプロジェクトに貢献して頂ける方は、[CONTRIBUTING.md](CONTRIB | Quickstart for Iris with Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) -5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > 似たようなものを開発しましたか? [私たちにも教えてください!](https://github.com/kataras/iris/pulls) @@ -199,6 +202,7 @@ Irisは他のフレームワークと異なり、標準パッケージと10 ### 記事 +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) diff --git a/README_PT_BR.md b/README_PT_BR.md index cdaaf03c1c..e0d34b8a90 100644 --- a/README_PT_BR.md +++ b/README_PT_BR.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris é um framework rápido, simples porém completo e muito eficiente para a linguagem Go. @@ -106,7 +106,7 @@ _Atualizado em : [Terça, 21 de Novembro de 2017](_benchmarks/README_UNIX.md)_ ## Apoie -- [HISTORY](HISTORY.md#sat-11-august-2018--v1070) o arquivo HISTORY é o seu melhor amigo, ele contém informações sobre as últimas features e mudanças. +- [HISTORY](HISTORY.md#fr-11-january-2019--v1111) o arquivo HISTORY é o seu melhor amigo, ele contém informações sobre as últimas features e mudanças. - Econtrou algum bug ? Poste-o nas [issues](https://github.com/kataras/iris/issues) - Possui alguma dúvida ou gostaria de falar com alguém experiente para resolver seu problema em tempo real ? Junte-se ao [chat da nossa comunidade](https://chat.iris-go.com). - Complete nosso formulário de experiência do usuário clicando [aqui](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) @@ -185,14 +185,17 @@ padrão. Feito isso, você pode seguir as seguintes diretrizes: | Quickstart do Iris com Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [Web app básico utilizando o Iris](https://github.com/gauravtiwari/go_iris_app) -2. [Uma mini rede social criada com o incrível Iris💖💖] (https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Projeto demo usando react com typescript e Iris](https://github.com/ionutvilie/react-ts) -5. [Plataforma de Gerenciamento de Localização auto hospedada criada com Iris e Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker e Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart do Iris com Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [Um projeto Hasura para iniciantes pronto para o deply com um app Golang hello-world utilizando o IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: Web app básico utilizando o Iris](https://github.com/gauravtiwari/go_iris_app) +5. [Uma mini rede social criada com o incrível Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Projeto demo usando react com typescript e Iris](https://github.com/ionutvilie/react-ts) +8. [Plataforma de Gerenciamento de Localização auto hospedada criada com Iris e Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker e Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart do Iris com Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: Um projeto Hasura para iniciantes pronto para o deply com um app Golang hello-world utilizando o IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > Voce criou algo parecido ? [Informe-nos](https://github.com/kataras/iris/pulls)! @@ -204,6 +207,7 @@ Iris, ao contrário dos demais, é 100% compatível com os padrões e esse é o ### Artigos +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [Um aplicação Todo utilizando Iris e Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [Um projeto Hasura para iniciantes pronto para o deply com um app Golang hello-world utilizando o IRIS](https://bit.ly/2lmKaAZ) * [Top 6 frameworks web do Go em 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) diff --git a/README_RU.md b/README_RU.md index 201e2d9824..8b1baccc22 100644 --- a/README_RU.md +++ b/README_RU.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go. @@ -106,7 +106,7 @@ _Обновлено: [Вторник, 21 ноября 2017 г.](_benchmarks/READ ## Поддержка -- Файл [HISTORY](HISTORY.md#sat-11-august-2018--v1070) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях +- Файл [HISTORY](HISTORY.md#fr-11-january-2019--v1111) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях - Вы случайно обнаружили ошибку? Опубликуйте ее на [Github вопросы](https://github.com/kataras/iris/issues) - У Вас есть какие-либо вопросы или Вам нужно поговорить с кем-то, кто бы смог решить Вашу проблему в режиме реального времени? Присоединяйтесь к нам в [чате сообщества](https://chat.iris-go.com) - Заполните наш отчет о пользовательском опыте на основе формы, нажав [здесь](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) @@ -180,14 +180,17 @@ _Обновлено: [Вторник, 21 ноября 2017 г.](_benchmarks/READ | Quickstart for Iris with Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [Основное веб-приложение, созданное в Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [Мини-социальная сеть, созданная с удивительным Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Демо-проект с реакцией на использованием машинописного текста и Iris](https://github.com/ionutvilie/react-ts) -5. [Самостоятельная платформа управления локализацией, построенная с помощью Iris и Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Быстрый запуск для Iris с Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [Cтартовый проект Hasura с готовностью применять веб-приложение Golang hello-world с IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > Вы построили что-то подобное? Дайте нам [знать](https://github.com/kataras/iris/pulls)! @@ -199,6 +202,7 @@ Iris, в отличие от других, на 100% совместим со с ### Статьи +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [Приложение Todo MVC с использованием Iris и Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [Стартовый проект Hasura с готовностью применять веб-приложение Golang hello-world с IRIS](bit.ly/2lmKaAZ) * [Топ-6 веб-фреймворков для Go на 2017 год diff --git a/README_ZH.md b/README_ZH.md index 95721dd592..17cbdd624d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,7 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://iris-go.com/v10/recipe) [![release](https://img.shields.io/badge/release%20-v10.7-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。 @@ -102,7 +102,7 @@ _更新于: [2017年11月21日星期二](_benchmarks/README_UNIX.md)_ ## 支持 -- [更新记录](HISTORY_ZH.md#sat-11-august-2018--v1070) 是您最好的朋友,它包含有关最新功能和更改的信息 +- [更新记录](HISTORY_ZH.md#fr-11-january-2019--v1111) 是您最好的朋友,它包含有关最新功能和更改的信息 - 你碰巧找到了一个错误? 请提交 [github issues](https://github.com/kataras/iris/issues) - 您是否有任何疑问或需要与有经验的人士交谈以实时解决问题? [加入我们的聊天](https://chat.iris-go.com) - [点击这里完成我们基于表单的用户体验报告](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) @@ -175,14 +175,17 @@ _更新于: [2017年11月21日星期二](_benchmarks/README_UNIX.md)_ | Quickstart for Iris with Nanobox | https://guides.nanobox.io/golang/iris/from-scratch | --> -1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) -2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) -3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) -4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) -5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) -6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) -7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) -8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) +1. [snowlyg/IrisApiProject: Iris + gorm + jwt + sqlite3](https://github.com/snowlyg/IrisApiProject) **NEW-Chinese** +2. [yz124/superstar: Iris + xorm to implement the star library](https://github.com/yz124/superstar) **NEW-Chinese** +3. [jebzmos4/Iris-golang: A basic CRUD API in golang with Iris](https://github.com/jebzmos4/Iris-golang) +4. [gauravtiwari/go_iris_app: A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +5. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +6. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +7. [ionutvilie/react-ts: Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +8. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +9. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +10. [nanobox.io: Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +11. [hasura.io: A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) > 如果你有类似的使用经验吗 [请提交给我们](https://github.com/kataras/iris/pulls)! @@ -192,6 +195,7 @@ Iris 拥有大量的中间件 [[1]](middleware/)[[2]](https://github.com/iris-co ### 相关文章(英文) +* [CRUD REST API in Iris (a framework for golang)](https://medium.com/@jebzmos4/crud-rest-api-in-iris-a-framework-for-golang-a5d33652401e) * [A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) * [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) diff --git a/VERSION b/VERSION index 729c3531e6..27271c123f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.7.0:https://github.com/kataras/iris/blob/master/HISTORY.md#sat-11-august-2018--v1070 \ No newline at end of file +11.1.1:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-11-january-2019--v1111 \ No newline at end of file diff --git a/_benchmarks/README.md b/_benchmarks/README.md index 04ee836fc8..d595c98bd2 100644 --- a/_benchmarks/README.md +++ b/_benchmarks/README.md @@ -1,14 +1,55 @@ +# Benchmarks: Monday, 22 October 2018 + +![](benchmarks_graph_22_october_2018.png) + ## Hardware -* Processor: Intel(R) Core(TM) **i7-4710HQ** CPU @ 2.50GHz 2.50GHz -* RAM: **8.00 GB** +![hardware screen](screens/hardware_win.png) ## Software -* OS: Microsoft **Windows** [Version **10**.0.15063], power plan is "High performance" -* HTTP Benchmark Tool: https://github.com/codesenberg/bombardier, latest version **1.1** -* **.NET Core**: https://www.microsoft.com/net/core, latest version **2.0** -* **Iris**: https://github.com/kataras/iris, latest version **8.3** built with [go1.8.3](https://golang.org) +* OS: Microsoft **Windows 10** [Version 1803 (OS Build 17134.345)] +* HTTP Benchmark Tool: https://github.com/codesenberg/bombardier, latest version **1.2.0** +* **Iris [Go]**: https://github.com/kataras/iris, latest version **11.0.0** built with [go1.11.1](https://golang.org) +* **.NET Core (both Kestrel & MVC) [C#]**: https://www.microsoft.com/net/core, latest version **2.1.5** +* **Node.js (express + throng) [Javascript]**: https://nodejs.org/, latest version **10.12.0**, express: https://github.com/expressjs/express latest version **4.16.4** and [throng](https://www.npmjs.com/package/throng) latest version **4.0.0** + +## Results + +* Throughput - `how much data transferred per second`. +* Reqs/sec (Requests Per Second in Average) - `the highest the better, important`. +* Latency - `the smallest the better, important`. +* Time To Complete - `the smallest the better, important`. + +| Name | Throughput | Reqs/sec | Latency | Time To Complete | Total Requests | +|-------|:-----------|:--------|:-------------|---------|------| +| Iris | 24.41MB/s | **131268** | **0.95ms** | **7s** | 1000000 | +| .NET Core (Kestrel) | 19.95MB/s | 110150 | 1.13ms | 9s | 1000000 | +| Expressjs| 18.60MB/s | 70352 | 1.80ms | 14s | 1000000 | +| Iris with Sessions | 25.32MB/s | **82477** | **1.53ms** | **1m1s** | 5000000 | +| .NET Core (Kestrel) with Sessions | 28.45MB/s | 61946 | 2.06ms | 1m1s | 5000000 | +| Expressjs with Sessions | 12.81MB/s | 46962 | 2.53mss | 1m42s | 5000000 | +| Iris MVC | 21.27MB/s | **114378** | **1.09ms** | **43s** | 5000000 | +| .Net Core MVC | 18.19MB/s | 82608 | 1.51ms | 1m | 5000000 | +| Expressjs MVC | - | - | - | - | - | +| Iris MVC with Templates | 306.60MB/s | **42501** | **2.94ms** | **23s** | 1000000 | +| .Net Core MVC with Templates | 188.01MB/s | 26051 | 4.78ms | 38s | 1000000 | +| Expressjs MVC with Templates | - | - | - | - | - | + +Go ahead and read the rest of the page to learn how you can reproduce the benchmarks. Don't be afraid! It's actually very easy, you can do things like that as well! + +## Old results (August of 2017) + + **Go vs .NET Core in terms of HTTP performance (Sa, 19 August 2017)** + +- https://medium.com/@kataras/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8 +- https://dev.to/kataras/go-vsnet-core-in-terms-of-http-performance + +**Iris Go vs .NET Core Kestrel in terms of HTTP performance (Mo, 21 August 2017)** + +- https://medium.com/@kataras/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5 + +**Thank you all** for the 100% green feedback, have fun! # .NET Core MVC vs Iris MVC @@ -22,7 +63,7 @@ We will compare two identical things here, in terms of application, the expected ```bash $ cd netcore-mvc -$ dotnet run -c Release +$ dotnet run --urls=http://localhost:5000 -c Release Hosting environment: Production Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc Now listening on: http://localhost:5000 @@ -31,16 +72,16 @@ Application started. Press Ctrl+C to shut down. ```bash $ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5 -Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections - 5000000 / 5000000 [=====================================================================================] 100.00% 2m3s +Bombarding http://localhost:5000/api/values/5 with 5000000 request(s) using 125 connection(s) + 5000000 / 5000000 [=====================================================================================] 100.00% 1m0s Done! Statistics Avg Stdev Max - Reqs/sec 40226.03 8724.30 161919 - Latency 3.09ms 1.40ms 169.12ms + Reqs/sec 82608.44 4072.64 96896.66 + Latency 1.51ms 255.49us 235.36ms HTTP codes: 1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 8.91MB/s + Throughput: 18.19MB/s ``` ### Iris MVC @@ -54,16 +95,16 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 5000000 http://localhost:5000/api/values/5 -Bombarding http://localhost:5000/api/values/5 with 5000000 requests using 125 connections - 5000000 / 5000000 [======================================================================================] 100.00% 47s +Bombarding http://localhost:5000/api/values/5 with 5000000 request(s) using 125 connection(s) + 5000000 / 5000000 [======================================================================================] 100.00% 43s Done! Statistics Avg Stdev Max - Reqs/sec 105643.81 7687.79 122564 - Latency 1.18ms 366.55us 22.01ms + Reqs/sec 114378.40 5080.77 135410.87 + Latency 1.09ms 26.25us 19.92ms HTTP codes: 1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 19.65MB/s + Throughput: 21.27MB/s ``` Click [here](screens) to navigate to the screenshots. @@ -77,17 +118,15 @@ Click [here](screens) to navigate to the screenshots. * Memory usage - smaller is better. * LOC (Lines Of Code) - smaller is better. -.NET Core MVC Application, written using 86 lines of code, ran for **2 minutes and 3 seconds** serving **40226.03** requests per second within **3.09ms** latency in average and **169.12ms** max, the memory usage of all these was ~123MB (without the dotnet host). +.NET Core MVC Application, written using 86 lines of code, ran for **1 minute** serving **82608.44** requests per second within **1.51ms** latency in average and **235.36ms** max, the memory usage of all these was ~123MB (without the dotnet host). -Iris MVC Application, written using 27 lines of code, ran for **47 seconds** serving **105643.71** requests per second within **1.18ms** latency in average and **22.01ms** max, the memory usage of all these was ~12MB. +Iris MVC Application, written using 27 lines of code, ran for **43 seconds** serving **114378.40** requests per second within **1.09ms** latency in average and **19.92ms** max, the memory usage of all these was ~12MB. -#### Update: 20 August 2017 +#### Update: 20 August 2017 and benchmarks re-ran at 22 October 2018 As [Josh Clark](https://twitter.com/clarkis117) and [Scott Hanselman‏](https://twitter.com/shanselman)‏ pointed out [on this status](https://twitter.com/shanselman/status/899005786826788865), on .NET Core MVC `Startup.cs` file the line with `services.AddMvc();` can be replaced with `services.AddMvcCore();`. I followed their helpful instructions and re-run the benchmarks. The article now contains the latest benchmark output for the .NET Core application with the change both Josh and Scott noted. -The twitter conversion: https://twitter.com/MakisMaropoulos/status/899113215895982080 - -For those who want to compare with the standard services.AddMvc(); you can see the old output by pressing [here](screens/5m_requests_netcore-mvc.png). +The twitter conversation: https://twitter.com/MakisMaropoulos/status/899113215895982080 ## MVC + Templates @@ -97,7 +136,7 @@ Let’s run one more benchmark, spawn `1000000 requests` but this time we expect ```bash $ cd netcore-mvc-templates -$ dotnet run -c Release +$ dotnet run --urls=http://localhost:5000 -c Release Hosting environment: Production Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-mvc-templates Now listening on: http://localhost:5000 @@ -106,16 +145,16 @@ Application started. Press Ctrl+C to shut down. ```bash $ bombardier -c 125 -n 1000000 http://localhost:5000 -Bombarding http://localhost:5000 with 1000000 requests using 125 connections - 1000000 / 1000000 [=====================================================================================] 100.00% 1m20s +Bombarding http://localhost:5000 with 1000000 request(s) using 125 connection(s) + 1000000 / 1000000 [======================================================================================] 100.00% 38s Done! Statistics Avg Stdev Max - Reqs/sec 11738.60 7741.36 125887 - Latency 10.10ms 22.10ms 1.97s + Reqs/sec 26051.53 3256.67 42363.32 + Latency 4.78ms 0.93ms 417.39ms HTTP codes: 1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 89.03MB/s + Throughput: 188.01MB/s ``` ### Iris MVC with Templates @@ -129,34 +168,26 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 1000000 http://localhost:5000 -Bombarding http://localhost:5000 with 1000000 requests using 125 connections - 1000000 / 1000000 [======================================================================================] 100.00% 37s +Bombarding http://localhost:5000 with 1000000 request(s) using 125 connection(s) + 1000000 / 1000000 [======================================================================================] 100.00% 23s Done! Statistics Avg Stdev Max - Reqs/sec 26656.76 1944.73 31188 - Latency 4.69ms 1.20ms 22.52ms + Reqs/sec 42501.30 1604.82 46023.80 + Latency 2.94ms 81.93us 24.93ms HTTP codes: 1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 192.51MB/s + Throughput: 306.60MB/s ``` ### Summary -* Time to complete the `1000000 requests` - smaller is better. -* Reqs/sec - bigger is better. -* Latency - smaller is better -* Memory usage - smaller is better. -* Throughput - bigger is better. - -.NET Core MVC with Templates Application ran for **1 minute and 20 seconds** serving **11738.60** requests per second with **89.03MB/s** within **10.10ms** latency in average and **1.97s** max, the memory usage of all these was ~193MB (without the dotnet host). +.NET Core MVC with Templates Application ran for **38 seconds** serving **26051.53** requests per second with **188.01MB/s** within **4.78ms** latency in average and **417.39ms** max, the memory usage of all these was ~193MB (without the dotnet host). -Iris MVC with Templates Application ran for **37 seconds** serving **26656.76** requests per second with **192.51MB/s** within **1.18ms** latency in average and **22.52ms** max, the memory usage of all these was ~17MB. +Iris MVC with Templates Application ran for **23 seconds** serving **42501.30** requests per second with **306.60MB/s** within **2.94ms** latency in average and **24.93ms** max, the memory usage of all these was ~17MB. # .NET Core (Kestrel) vs Iris -_Monday, 21 August 2017_ - This time we will compare the speed of the “low-level” .NET Core’s server implementation named Kestrel and Iris’ “low-level” handlers, we will test two simple applications, the first will be the same as our previous application but written using handlers and the second test will contain a single route which sets and gets a session value(string) based on a key(string). ## Simple @@ -167,7 +198,7 @@ Spawn `1000000 requests` with 125 different "threads", targeting to a dynamic re ```bash $ cd netcore -$ dotnet run -c Release +$ dotnet run --urls=http://localhost:5000 -c Release Hosting environment: Production Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore Now listening on: http://localhost:5000 @@ -176,16 +207,16 @@ Application started. Press Ctrl+C to shut down. ```bash $ bombardier -c 125 -n 1000000 http://localhost:5000/api/values/5 -Bombarding http://localhost:5000/api/values/5 with 1000000 requests using 125 connections - 1000000 / 1000000 [======================================================================================] 100.00% 10s +Bombarding http://localhost:5000/api/values/5 with 1000000 request(s) using 125 connection(s) + 1000000 / 1000000 [=======================================================================================] 100.00% 9s Done! Statistics Avg Stdev Max - Reqs/sec 97884.57 8699.94 110509 - Latency 1.28ms 682.63us 61.04ms + Reqs/sec 110150.12 8195.73 122486.03 + Latency 1.13ms 178.98us 81.78ms HTTP codes: 1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 17.73MB/s + Throughput: 19.95MB/s ``` ### Iris @@ -199,22 +230,22 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 1000000 http://localhost:5000/api/values/5 -Bombarding http://localhost:5000/api/values/5 with 1000000 requests using 125 connections - 1000000 / 1000000 [=======================================================================================] 100.00% 8s +Bombarding http://localhost:5000/api/values/5 with 1000000 request(s) using 125 connection(s) + 1000000 / 1000000 [=======================================================================================] 100.00% 7s Done! Statistics Avg Stdev Max - Reqs/sec 117917.79 4437.04 125614 - Latency 1.06ms 278.12us 19.03ms + Reqs/sec 131268.51 5757.43 141530.72 + Latency 0.95ms 62.10us 19.92ms HTTP codes: 1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 21.93MB/s + Throughput: 24.41MB/s ``` ### Node.js (Express) ```bash -$ cd expressjs +$ cd expressjs-throng $ npm install $ node app.js Now listening on: http://localhost:5000 @@ -223,31 +254,25 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 1000000 http://localhost:5000/api/values/5 -Bombarding http://localhost:5000/api/values/5 with 1000000 requests using 125 connections - 1000000 / 1000000 [=======================================================================================] 100.00% 1m25s +Bombarding http://localhost:5000/api/values/5 with 1000000 request(s) using 125 connection(s) + 1000000 / 1000000 [======================================================================================] 100.00% 14s Done! Statistics Avg Stdev Max - Reqs/sec 11665.30 628.41 21978 - Latency 10.72ms 1.45ms 112.10ms + Reqs/sec 70352.00 10947.26 115334.47 + Latency 1.80ms 1.39ms 206.45ms HTTP codes: 1xx - 0, 2xx - 1000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 3.14MB/s + Throughput: 18.60MB/s ``` ### Summary -* Time to complete the `1000000 requests` - smaller is better. -* Reqs/sec - bigger is better. -* Latency - smaller is better -* Throughput - bigger is better. -* LOC (Lines Of Code) - smaller is better. - -.NET Core (Kestrel) Application written using **63 code of lines** ran for **10 seconds** serving **97884.57** requests per second with **17.73MB/s** within **1.28ms** latency in average and **61.04ms** max. +.NET Core (Kestrel) Application written using **63 code of lines** ran for **9 seconds** serving **110150.12** requests per second with **19.95MB/s** within **1.13ms** latency in average and **81.78ms** max. -Iris Application written using **14 code of lines** ran for **8 seconds** serving **117917.79** requests per second with **21.93MB/s** within **1.06ms** latency in average and **19.03ms** max. +Iris Application written using **14 code of lines** ran for **7 seconds** serving **131268.51** requests per second with **24.41MB/s** within **0.95ms** latency in average and **19.92ms** max. -Node.js (Express) Application written using **12 code of lines** ran for **1 minute and 25 seconds** serving **11665.30** requests per second with **3.14MB/s** within **10.72ms** latency in average and **112.10ms** max. +Node.js (Express) Application written using **12 code of lines** ran for **14 seconds** serving **70352** requests per second with **18.60MB/s** within **1.80ms** latency in average and **206.45ms** max. ## Sessions @@ -257,7 +282,7 @@ Spawn `5000000 requests` with 125 different "threads" targeting a static request ```bash $ cd netcore-sessions -$ dotnet run -c Release +$ dotnet run --urls=http://localhost:5000 -c Release Hosting environment: Production Content root path: C:\mygopath\src\github.com\kataras\iris\_benchmarks\netcore-sessions Now listening on: http://localhost:5000 @@ -266,16 +291,16 @@ Application started. Press Ctrl+C to shut down. ```bash $ bombardier -c 125 -n 5000000 http://localhost:5000/setget -Bombarding http://localhost:5000/setget with 5000000 requests using 125 connections - 5000000 / 5000000 [====================================================================================] 100.00% 2m40s +Bombarding http://localhost:5000/setget with 5000000 request(s) using 125 connection(s) + 5000000 / 5000000 [=====================================================================================] 100.00% 1m1s Done! Statistics Avg Stdev Max - Reqs/sec 31844.77 13856.19 253746 - Latency 4.02ms 15.57ms 0.96s + Reqs/sec 61946.59 13164.30 100166.94 + Latency 2.06ms 2.42ms 781.91ms HTTP codes: 1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 14.51MB/s + Throughput: 28.45MB/s ``` ### Iris with Sessions @@ -289,22 +314,22 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 5000000 http://localhost:5000/setget -Bombarding http://localhost:5000/setget with 5000000 requests using 125 connections - 5000000 / 5000000 [====================================================================================] 100.00% 1m15s +Bombarding http://localhost:5000/setget with 5000000 request(s) using 125 connection(s) + 5000000 / 5000000 [=====================================================================================] 100.00% 1m1s Done! Statistics Avg Stdev Max - Reqs/sec 66749.70 32110.67 110445 - Latency 1.88ms 9.13ms 1.94s + Reqs/sec 82477.76 29886.37 166399.36 + Latency 1.53ms 462.79us 286.23ms HTTP codes: 1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 20.65MB/s + Throughput: 25.32MB/s ``` ### Node.js (Express) with Sessions ```bash -$ cd expressjs-sessions +$ cd expressjs-throng-sessions $ npm install $ node app.js Now listening on: http://localhost:5000 @@ -313,42 +338,24 @@ Application started. Press CTRL+C to shut down. ```bash $ bombardier -c 125 -n 5000000 http://localhost:5000/setget -Bombarding http://localhost:5000/setget with 5000000 requests using 125 connections - 5000000 / 5000000 [====================================================================================] 100.00% 15m47s +Bombarding http://localhost:5000/setget with 5000000 request(s) using 125 connection(s) + 5000000 / 5000000 [====================================================================================] 100.00% 1m42s Done! Statistics Avg Stdev Max - Reqs/sec 5634.27 2317.30 9945 - Latency 22.17ms 8.19ms 119.08ms + Reqs/sec 46962.17 10028.21 104302.48 + Latency 2.53ms 1.45ms 368.01ms HTTP codes: 1xx - 0, 2xx - 5000000, 3xx - 0, 4xx - 0, 5xx - 0 others - 0 - Throughput: 1.48MB/s + Throughput: 12.81MB/s ``` ### Summary -* Time to complete the `5000000 requests` - smaller is better. -* Reqs/sec - bigger is better. -* Latency - smaller is better -* Throughput - bigger is better. - -.NET Core with Sessions Application ran for **2 minutes and 40 seconds** serving **31844.77** requests per second with **14.51MB/s** within **4.02ms** latency in average and **0.96s** max. - -Iris with Sessions Application ran for **1 minute and 15 seconds** serving **66749.70** requests per second with **20.65MB/s** within **1.88ms** latency in average and **1.94s** max. +.NET Core with Sessions Application ran for **1 minute and 1 second** serving **61946.59** requests per second with **28.45MB/s** within **2.06ms** latency in average and **781.91ms** max. -Node.js (Express) with Sessions Application ran for **15 minutes and 47 seconds** serving **5634.27** requests per second with **1.48MB/s** within **22.17ms** latency in average and **119.08ms** max. +Iris with Sessions Application ran for **1 minute and 1 second** serving **82477.76** requests per second with **25.32MB/s** within **1.53ms** latency in average and **286.23ms** max. -> Click [here](screens) to navigate to the screenshots. - -### Articles - - **Go vs .NET Core in terms of HTTP performance (Sa, 19 August 2017)** - -- https://medium.com/@kataras/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8 -- https://dev.to/kataras/go-vsnet-core-in-terms-of-http-performance - -**Iris Go vs .NET Core Kestrel in terms of HTTP performance (Mo, 21 August 2017)** - -- https://medium.com/@kataras/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5 +Node.js (Express) with Sessions Application ran for **1 minute and 42 seconds** serving **46962.17** requests per second with **12.81MB/s** within **2.53ms** latency in average and **368.01ms** max. -**Thank you all** for the 100% green feedback, have fun! \ No newline at end of file +> Click [here](screens) to navigate to the screenshots. \ No newline at end of file diff --git a/_benchmarks/README_UNIX.md b/_benchmarks/README_UNIX.md deleted file mode 100644 index a7e87d2cb1..0000000000 --- a/_benchmarks/README_UNIX.md +++ /dev/null @@ -1,61 +0,0 @@ -![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new.png)] - -## Hardware - -* [Processor](screens/unix/system_info_cpu.png): Intel(R) Core(TM) **i7-4710HQ** CPU @ 2.50GHz -* [RAM](screens/unix/system_info_ram.png): **8.00 GB** - -## Software - -* OS: Linux **Ubuntu** [Version **17.10**] with latest kernel version **4.14.0-041400-generic x86_64 GNU/Linux** -* HTTP Benchmark Tool: https://github.com/codesenberg/bombardier, latest version **1.1** -* **Iris [Go]**: https://github.com/kataras/iris, latest version **8.5.7** built with [go1.9.2](https://golang.org) -* **.NET Core [C#]**: https://www.microsoft.com/net/core, latest version **2.0.2** -* **Node.js (express + throng) [Javascript]**: https://nodejs.org/, latest version **9.2.0**, express: https://github.com/expressjs/express latest version **4.16.0** and [throng](https://www.npmjs.com/package/throng) latest version **4.0.0** - -Go ahead to the [README.md](README.md) and read how you can reproduce the benchmarks. Don't be scary it's actually very easy, you can do these things as well! - -## Results - -* Throughput - `bigger is better`. -* Reqs/sec (Requests Per Second in Average) - `bigger is better`. -* Latency - `smaller is better`. -* Time To Complete - `smaller is better`. -* Total Requests in this fortune are all 1 million, in order to be easier to do the graph later on. - -### Native - -| Name | Throughput | Reqs/sec | Latency | Time To Complete | Total Requests | -|-------|:-----------|:--------|:-------------|---------|------| -| Iris | **29.31MB/s** | 157628 | 791.58us | 6s | 1000000 | -| Kestrel | **25.28MB/s** | 139642 | 0.89ms | 7s | 1000000 | -| Node.js | **13.69MB/s** | 50907 | 2.45ms | 19s | 1000000 | -| Iris with Sessions | **22.37MB/s** | 71922 | 1.74ms | 14s | 1000000 | -| Kestrel with Sessions | **14.51MB/s** | 31102 | 4.02ms | 32s | 1000000 | -| Node.js with Sessions | **5.08MB/s** | 19358 | 6.48ms | 51s | 1000000 | - -> each test has its own screenshot, click [here](screens/unix) to explore - -### MVC (Model View Controller) - -| Name | Throughput | Reqs/sec | Latency | Time To Complete | Total Requests | -|-------|:-----------|:--------|:-------------|---------|------| -| Iris MVC | **26.39MB/s** | 141868 | 0.88ms | 7s | 1000000 | -| .Net Core MVC | **11.99MB/s** | 54418 | 2.30ms | 18s | 1000000 | -| - | - | - | - | - | - | -| Iris MVC with Templates | **136.58MB/s** | 18933 | 6.60ms | 52s | 1000000 | -| .Net Core MVC with Templates | **88.95MB/s** | 12347 | 10.12ms | 1m21s | 1000000 | -| - | - | - | - | - | - | - -> nodejs express does not contain any MVC features - -### Updates - -- 21 November 2017: initial run and publish - -## Articles (ms windows OS) - -- https://hackernoon.com/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8 -- https://hackernoon.com/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5 - -**Thank you all** for the 100% green feedback, have fun! \ No newline at end of file diff --git a/_benchmarks/benchmarks_graph_22_october_2018.png b/_benchmarks/benchmarks_graph_22_october_2018.png new file mode 100644 index 0000000000..3a4c9f1eb6 Binary files /dev/null and b/_benchmarks/benchmarks_graph_22_october_2018.png differ diff --git a/_benchmarks/benchmarks_graph_22_october_2018_gray.png b/_benchmarks/benchmarks_graph_22_october_2018_gray.png new file mode 100644 index 0000000000..c202816813 Binary files /dev/null and b/_benchmarks/benchmarks_graph_22_october_2018_gray.png differ diff --git a/_benchmarks/benchmarks_third_party_source_snapshot_go_23_october_2018.png b/_benchmarks/benchmarks_third_party_source_snapshot_go_23_october_2018.png new file mode 100644 index 0000000000..fd723256a8 Binary files /dev/null and b/_benchmarks/benchmarks_third_party_source_snapshot_go_23_october_2018.png differ diff --git a/_benchmarks/expressjs-throng-sessions/package-lock.json b/_benchmarks/expressjs-throng-sessions/package-lock.json index cec4bda590..1de893ae76 100644 --- a/_benchmarks/expressjs-throng-sessions/package-lock.json +++ b/_benchmarks/expressjs-throng-sessions/package-lock.json @@ -1,15 +1,15 @@ { "name": "expressjs-app", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.18", "negotiator": "0.6.1" } }, @@ -19,20 +19,20 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" } }, "bytes": { @@ -41,9 +41,9 @@ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" }, "clone": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.3.tgz", - "integrity": "sha1-KY1+IjFmD0DAA8LtMUDezz9TCF8=" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" }, "content-disposition": { "version": "0.5.2", @@ -79,9 +79,9 @@ } }, "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "destroy": { "version": "1.0.4", @@ -94,9 +94,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "escape-html": { "version": "1.0.3", @@ -109,40 +109,40 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "express": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.0.tgz", - "integrity": "sha1-tRljjk61jnF4yBtJjvIveYyy4lU=", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.5", "array-flatten": "1.1.1", - "body-parser": "1.18.2", + "body-parser": "1.18.3", "content-disposition": "0.5.2", - "content-type": "1.0.4", + "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.0", - "serve-static": "1.13.0", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", + "statuses": "~1.4.0", + "type-is": "~1.6.16", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" } }, "express-session": { @@ -154,25 +154,25 @@ "cookie-signature": "1.0.6", "crc": "3.4.4", "debug": "2.6.9", - "depd": "1.1.1", - "on-headers": "1.0.1", - "parseurl": "1.3.2", - "uid-safe": "2.1.5", + "depd": "~1.1.1", + "on-headers": "~1.0.1", + "parseurl": "~1.3.2", + "uid-safe": "~2.1.5", "utils-merge": "1.0.1" } }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "requires": { "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" } }, "forwarded": { @@ -186,27 +186,23 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { - "depd": "1.1.1", + "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" } }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "inherits": { "version": "2.0.3", @@ -214,13 +210,13 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" }, "lodash": { "version": "2.4.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=" }, "lodash.defaults": { @@ -230,7 +226,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "merge-descriptors": { @@ -249,16 +245,16 @@ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" }, "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" }, "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "requires": { - "mime-db": "1.30.0" + "mime-db": "~1.37.0" } }, "ms": { @@ -276,14 +272,14 @@ "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-3.2.1.tgz", "integrity": "sha1-p5WNMqikLZEZziWYZWfqLF+WZ3M=", "requires": { - "clone": "1.0.3", - "lodash": "4.17.4" + "clone": "1.0.x", + "lodash": "4.x" }, "dependencies": { "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" } } }, @@ -311,18 +307,18 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" } }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "random-bytes": { "version": "1.0.0", @@ -335,50 +331,55 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", "requires": { "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.0.tgz", - "integrity": "sha1-FjONu5ou3krVe0hCDsO4LY6ApXs=", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "requires": { "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.6.2", + "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" } }, "serve-static": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.0.tgz", - "integrity": "sha1-gQyR24AOlLoofq5rTgbKq5/cFvE=", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" } }, "session-memory-store": { @@ -386,8 +387,8 @@ "resolved": "https://registry.npmjs.org/session-memory-store/-/session-memory-store-0.2.2.tgz", "integrity": "sha1-1GQ48oTPjg7RVE9v1Zk+4Ud8dlY=", "requires": { - "lodash": "2.4.2", - "node-cache": "3.2.1" + "lodash": "^2.4.2", + "node-cache": "^3.1.0" } }, "setprototypeof": { @@ -396,25 +397,25 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, "throng": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/throng/-/throng-4.0.0.tgz", "integrity": "sha1-mDxroZk7WOroWZmKpof/6I34TBc=", "requires": { - "lodash.defaults": "4.2.0" + "lodash.defaults": "^4.0.1" } }, "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.18" } }, "uid-safe": { @@ -422,7 +423,7 @@ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", "requires": { - "random-bytes": "1.0.0" + "random-bytes": "~1.0.0" } }, "unpipe": { diff --git a/_benchmarks/expressjs-throng-sessions/package.json b/_benchmarks/expressjs-throng-sessions/package.json index b8e257bc89..d99d23cc0d 100644 --- a/_benchmarks/expressjs-throng-sessions/package.json +++ b/_benchmarks/expressjs-throng-sessions/package.json @@ -1,6 +1,6 @@ { "name": "expressjs-app", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "app.js", "scripts": { @@ -9,7 +9,7 @@ "author": "Gerasimos (Makis) Maropoulos ", "license": "ISC", "dependencies": { - "express": "^4.16.0", + "express": "^4.16.4", "express-session": "^1.15.6", "session-memory-store": "^0.2.2", "throng": "4.0.0" diff --git a/_benchmarks/expressjs-throng/package-lock.json b/_benchmarks/expressjs-throng/package-lock.json index 6747dc5e40..ca2ae8b029 100644 --- a/_benchmarks/expressjs-throng/package-lock.json +++ b/_benchmarks/expressjs-throng/package-lock.json @@ -1,15 +1,15 @@ { "name": "expressjs-app", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { "accepts": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", - "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", "requires": { - "mime-types": "2.1.17", + "mime-types": "~2.1.18", "negotiator": "0.6.1" } }, @@ -19,20 +19,20 @@ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" }, "body-parser": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", - "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", "requires": { "bytes": "3.0.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.1", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", - "on-finished": "2.3.0", - "qs": "6.5.1", - "raw-body": "2.3.2", - "type-is": "1.6.15" + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" } }, "bytes": { @@ -69,9 +69,9 @@ } }, "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "destroy": { "version": "1.0.4", @@ -84,9 +84,9 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "encodeurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", - "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "escape-html": { "version": "1.0.3", @@ -99,54 +99,54 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "express": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.0.tgz", - "integrity": "sha1-tRljjk61jnF4yBtJjvIveYyy4lU=", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", "requires": { - "accepts": "1.3.4", + "accepts": "~1.3.5", "array-flatten": "1.1.1", - "body-parser": "1.18.2", + "body-parser": "1.18.3", "content-disposition": "0.5.2", - "content-type": "1.0.4", + "content-type": "~1.0.4", "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.1", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.0", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", "fresh": "0.5.2", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.2", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.2", - "qs": "6.5.1", - "range-parser": "1.2.0", - "safe-buffer": "5.1.1", - "send": "0.16.0", - "serve-static": "1.13.0", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", "setprototypeof": "1.1.0", - "statuses": "1.3.1", - "type-is": "1.6.15", + "statuses": "~1.4.0", + "type-is": "~1.6.16", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" } }, "finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "requires": { "debug": "2.6.9", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.2", - "statuses": "1.3.1", - "unpipe": "1.0.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" } }, "forwarded": { @@ -160,27 +160,23 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", + "version": "1.6.3", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "requires": { - "depd": "1.1.1", + "depd": "~1.1.2", "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": "1.3.1" - }, - "dependencies": { - "setprototypeof": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", - "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" - } + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" } }, "iconv-lite": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", - "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } }, "inherits": { "version": "2.0.3", @@ -188,9 +184,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ipaddr.js": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", - "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" }, "lodash.defaults": { "version": "4.2.0", @@ -199,7 +195,7 @@ }, "media-typer": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "resolved": "http://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "merge-descriptors": { @@ -218,16 +214,16 @@ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" }, "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" }, "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "requires": { - "mime-db": "1.30.0" + "mime-db": "~1.37.0" } }, "ms": { @@ -259,18 +255,18 @@ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, "proxy-addr": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", - "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", "requires": { - "forwarded": "0.1.2", - "ipaddr.js": "1.5.2" + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" } }, "qs": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", - "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, "range-parser": { "version": "1.2.0", @@ -278,50 +274,55 @@ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" }, "raw-body": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", - "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", "requires": { "bytes": "3.0.0", - "http-errors": "1.6.2", - "iconv-lite": "0.4.19", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", "unpipe": "1.0.0" } }, "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "send": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.0.tgz", - "integrity": "sha1-FjONu5ou3krVe0hCDsO4LY6ApXs=", + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", "requires": { "debug": "2.6.9", - "depd": "1.1.1", - "destroy": "1.0.4", - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.6.2", + "http-errors": "~1.6.2", "mime": "1.4.1", "ms": "2.0.0", - "on-finished": "2.3.0", - "range-parser": "1.2.0", - "statuses": "1.3.1" + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" } }, "serve-static": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.0.tgz", - "integrity": "sha1-gQyR24AOlLoofq5rTgbKq5/cFvE=", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", "requires": { - "encodeurl": "1.0.1", - "escape-html": "1.0.3", - "parseurl": "1.3.2", - "send": "0.16.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" } }, "setprototypeof": { @@ -330,25 +331,25 @@ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" }, "statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" }, "throng": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/throng/-/throng-4.0.0.tgz", "integrity": "sha1-mDxroZk7WOroWZmKpof/6I34TBc=", "requires": { - "lodash.defaults": "4.2.0" + "lodash.defaults": "^4.0.1" } }, "type-is": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", - "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.17" + "mime-types": "~2.1.18" } }, "unpipe": { diff --git a/_benchmarks/expressjs-throng/package.json b/_benchmarks/expressjs-throng/package.json index c5f53166c8..b1d2430a13 100644 --- a/_benchmarks/expressjs-throng/package.json +++ b/_benchmarks/expressjs-throng/package.json @@ -1,6 +1,6 @@ { "name": "expressjs-app", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "app.js", "scripts": { @@ -9,7 +9,7 @@ "author": "Gerasimos (Makis) Maropoulos ", "license": "ISC", "dependencies": { - "express": "^4.16.0", + "express": "^4.16.4", "throng": "4.0.0" } } diff --git a/_benchmarks/iris-mvc-templates/main.go b/_benchmarks/iris-mvc-templates/main.go index 5601ad146e..cfd46558fd 100644 --- a/_benchmarks/iris-mvc-templates/main.go +++ b/_benchmarks/iris-mvc-templates/main.go @@ -24,7 +24,7 @@ func main() { mvc.New(app).Handle(new(controllers.HomeController)) - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":5000")) } type err struct { diff --git a/_benchmarks/iris-mvc/main.go b/_benchmarks/iris-mvc/main.go index 6ea755de06..4b0e0a1859 100644 --- a/_benchmarks/iris-mvc/main.go +++ b/_benchmarks/iris-mvc/main.go @@ -16,7 +16,7 @@ func main() { mvc.New(app.Party("/api/values/{id}")). Handle(new(controllers.ValuesController)) - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":5000")) } // +2MB/s faster than the previous implementation, 0.4MB/s difference from the raw handlers. diff --git a/_benchmarks/iris-sessions/main.go b/_benchmarks/iris-sessions/main.go index 956cad0d3b..d5a340629b 100644 --- a/_benchmarks/iris-sessions/main.go +++ b/_benchmarks/iris-sessions/main.go @@ -24,9 +24,7 @@ func main() { app.Delete("/del", delHandler) */ - // 24 August 2017: Iris has a built'n version updater but we don't need it - // when benchmarking... - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":5000")) } // Set and Get diff --git a/_benchmarks/iris/main.go b/_benchmarks/iris/main.go index 7c1ee6e29d..d9885e884f 100644 --- a/_benchmarks/iris/main.go +++ b/_benchmarks/iris/main.go @@ -8,5 +8,5 @@ func main() { ctx.WriteString("value") }) - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":5000")) } diff --git a/_benchmarks/netcore-mvc-templates/netcore-mvc-templates.csproj b/_benchmarks/netcore-mvc-templates/netcore-mvc-templates.csproj index 74c7d770c5..dde6ee07ec 100644 --- a/_benchmarks/netcore-mvc-templates/netcore-mvc-templates.csproj +++ b/_benchmarks/netcore-mvc-templates/netcore-mvc-templates.csproj @@ -1,15 +1,15 @@ - netcoreapp2.0 + netcoreapp2.1 - + - + diff --git a/_benchmarks/netcore-mvc/netcore-mvc.csproj b/_benchmarks/netcore-mvc/netcore-mvc.csproj index e071cdd521..0e7f839d79 100644 --- a/_benchmarks/netcore-mvc/netcore-mvc.csproj +++ b/_benchmarks/netcore-mvc/netcore-mvc.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 @@ -9,11 +9,12 @@ - + + - + - + diff --git a/_benchmarks/netcore-sessions/netcore-sessions.csproj b/_benchmarks/netcore-sessions/netcore-sessions.csproj index 74c7d770c5..dde6ee07ec 100644 --- a/_benchmarks/netcore-sessions/netcore-sessions.csproj +++ b/_benchmarks/netcore-sessions/netcore-sessions.csproj @@ -1,15 +1,15 @@ - netcoreapp2.0 + netcoreapp2.1 - + - + diff --git a/_benchmarks/netcore/netcore.csproj b/_benchmarks/netcore/netcore.csproj index 74c7d770c5..dde6ee07ec 100644 --- a/_benchmarks/netcore/netcore.csproj +++ b/_benchmarks/netcore/netcore.csproj @@ -1,15 +1,15 @@ - netcoreapp2.0 + netcoreapp2.1 - + - + diff --git a/_benchmarks/screens/1m_requests_expressjs.png b/_benchmarks/screens/1m_requests_expressjs.png index 7d0603e041..941e38f8f6 100644 Binary files a/_benchmarks/screens/1m_requests_expressjs.png and b/_benchmarks/screens/1m_requests_expressjs.png differ diff --git a/_benchmarks/screens/1m_requests_iris-mvc-templates.png b/_benchmarks/screens/1m_requests_iris-mvc-templates.png index 28a23cbf1a..e94a174cbb 100644 Binary files a/_benchmarks/screens/1m_requests_iris-mvc-templates.png and b/_benchmarks/screens/1m_requests_iris-mvc-templates.png differ diff --git a/_benchmarks/screens/1m_requests_iris.png b/_benchmarks/screens/1m_requests_iris.png index 78617a9a8b..b4931f72d6 100644 Binary files a/_benchmarks/screens/1m_requests_iris.png and b/_benchmarks/screens/1m_requests_iris.png differ diff --git a/_benchmarks/screens/1m_requests_netcore-mvc-templates.png b/_benchmarks/screens/1m_requests_netcore-mvc-templates.png index 46c48672a0..324c20d27d 100644 Binary files a/_benchmarks/screens/1m_requests_netcore-mvc-templates.png and b/_benchmarks/screens/1m_requests_netcore-mvc-templates.png differ diff --git a/_benchmarks/screens/1m_requests_netcore.png b/_benchmarks/screens/1m_requests_netcore.png index 7e36b7ef94..697c2d6a46 100644 Binary files a/_benchmarks/screens/1m_requests_netcore.png and b/_benchmarks/screens/1m_requests_netcore.png differ diff --git a/_benchmarks/screens/5m_requests_express-sessions.png b/_benchmarks/screens/5m_requests_express-sessions.png new file mode 100644 index 0000000000..22886a156a Binary files /dev/null and b/_benchmarks/screens/5m_requests_express-sessions.png differ diff --git a/_benchmarks/screens/5m_requests_iris-mvc.png b/_benchmarks/screens/5m_requests_iris-mvc.png index f2c6f8b800..af94071e46 100644 Binary files a/_benchmarks/screens/5m_requests_iris-mvc.png and b/_benchmarks/screens/5m_requests_iris-mvc.png differ diff --git a/_benchmarks/screens/5m_requests_iris-sessions.png b/_benchmarks/screens/5m_requests_iris-sessions.png index 8da528a57c..a96f0ac3b8 100644 Binary files a/_benchmarks/screens/5m_requests_iris-sessions.png and b/_benchmarks/screens/5m_requests_iris-sessions.png differ diff --git a/_benchmarks/screens/5m_requests_iris.png b/_benchmarks/screens/5m_requests_iris.png deleted file mode 100644 index e82f42fe0a..0000000000 Binary files a/_benchmarks/screens/5m_requests_iris.png and /dev/null differ diff --git a/_benchmarks/screens/5m_requests_netcore-mvc.png b/_benchmarks/screens/5m_requests_netcore-mvc.png index 6e10b9a887..71e5a07006 100644 Binary files a/_benchmarks/screens/5m_requests_netcore-mvc.png and b/_benchmarks/screens/5m_requests_netcore-mvc.png differ diff --git a/_benchmarks/screens/5m_requests_netcore-mvc_addmvccore.png b/_benchmarks/screens/5m_requests_netcore-mvc_addmvccore.png deleted file mode 100644 index a752d9c4e7..0000000000 Binary files a/_benchmarks/screens/5m_requests_netcore-mvc_addmvccore.png and /dev/null differ diff --git a/_benchmarks/screens/5m_requests_netcore-sessions.png b/_benchmarks/screens/5m_requests_netcore-sessions.png index 6baeebb39e..3df3c06363 100644 Binary files a/_benchmarks/screens/5m_requests_netcore-sessions.png and b/_benchmarks/screens/5m_requests_netcore-sessions.png differ diff --git a/_benchmarks/screens/hardware_win.png b/_benchmarks/screens/hardware_win.png new file mode 100644 index 0000000000..f035a1263f Binary files /dev/null and b/_benchmarks/screens/hardware_win.png differ diff --git a/_benchmarks/screens/unix/system_info_cpu.png b/_benchmarks/screens/unix/system_info_cpu.png deleted file mode 100644 index df91e815db..0000000000 Binary files a/_benchmarks/screens/unix/system_info_cpu.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng-sessions.png b/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng-sessions.png deleted file mode 100644 index 28d925c1ff..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng.png b/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng.png deleted file mode 100644 index ee0d32625a..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_expressjs-throng.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_iris-mvc-templates.png b/_benchmarks/screens/unix/unix_1m_requests_iris-mvc-templates.png deleted file mode 100644 index 94b5e00047..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_iris-mvc-templates.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_iris-mvc.png b/_benchmarks/screens/unix/unix_1m_requests_iris-mvc.png deleted file mode 100644 index b4573e0775..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_iris-mvc.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_iris-sessions.png b/_benchmarks/screens/unix/unix_1m_requests_iris-sessions.png deleted file mode 100644 index 66e15cf65a..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_iris-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_iris.png b/_benchmarks/screens/unix/unix_1m_requests_iris.png deleted file mode 100644 index 4fcd203eab..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_iris.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc-templates.png b/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc-templates.png deleted file mode 100644 index 1b27bc7dcd..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc-templates.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc.png b/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc.png deleted file mode 100644 index 8066799a9f..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_netcore-mvc.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_netcore-sessions.png b/_benchmarks/screens/unix/unix_1m_requests_netcore-sessions.png deleted file mode 100644 index 88fd2944ec..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_netcore-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_1m_requests_netcore.png b/_benchmarks/screens/unix/unix_1m_requests_netcore.png deleted file mode 100644 index a3c017cfc0..0000000000 Binary files a/_benchmarks/screens/unix/unix_1m_requests_netcore.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_5m_requests_expressjs-throng-sessions.png b/_benchmarks/screens/unix/unix_5m_requests_expressjs-throng-sessions.png deleted file mode 100644 index 15a72bcee9..0000000000 Binary files a/_benchmarks/screens/unix/unix_5m_requests_expressjs-throng-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_5m_requests_iris-mvc.png b/_benchmarks/screens/unix/unix_5m_requests_iris-mvc.png deleted file mode 100644 index 3f6a0a5468..0000000000 Binary files a/_benchmarks/screens/unix/unix_5m_requests_iris-mvc.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_5m_requests_iris-sessions.png b/_benchmarks/screens/unix/unix_5m_requests_iris-sessions.png deleted file mode 100644 index f57a6b9c4a..0000000000 Binary files a/_benchmarks/screens/unix/unix_5m_requests_iris-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_5m_requests_netcore-mvc.png b/_benchmarks/screens/unix/unix_5m_requests_netcore-mvc.png deleted file mode 100644 index 9378a6772b..0000000000 Binary files a/_benchmarks/screens/unix/unix_5m_requests_netcore-mvc.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_5m_requests_netcore-sessions.png b/_benchmarks/screens/unix/unix_5m_requests_netcore-sessions.png deleted file mode 100644 index 6fb0614ae3..0000000000 Binary files a/_benchmarks/screens/unix/unix_5m_requests_netcore-sessions.png and /dev/null differ diff --git a/_benchmarks/screens/unix/unix_system_info_ram.png b/_benchmarks/screens/unix/unix_system_info_ram.png deleted file mode 100644 index fedfd7bdad..0000000000 Binary files a/_benchmarks/screens/unix/unix_system_info_ram.png and /dev/null differ diff --git a/_examples/README.md b/_examples/README.md index 26530a1931..38a3faa2b2 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -146,8 +146,10 @@ Navigate through examples for a better understanding. - [Custom HTTP Errors](routing/http-errors/main.go) - [Dynamic Path](routing/dynamic-path/main.go) * [root level wildcard path](routing/dynamic-path/root-wildcard/main.go) +- [Write your own custom parameter types](routing/macros/main.go) **NEW** - [Reverse routing](routing/reverse/main.go) -- [Custom wrapper](routing/custom-wrapper/main.go) +- [Custom Router (high-level)](routing/custom-high-level-router/main.go) **NEW** +- [Custom Wrapper](routing/custom-wrapper/main.go) - Custom Context * [method overriding](routing/custom-context/method-overriding/main.go) * [new implementation](routing/custom-context/new-implementation/main.go) @@ -156,6 +158,11 @@ Navigate through examples for a better understanding. * [per-route](routing/writing-a-middleware/per-route/main.go) * [globally](routing/writing-a-middleware/globally/main.go) +### Versioning + +- [How it works](https://github.com/kataras/iris/blob/master/versioning/README.md) +- [Example](versioning/main.go) + ### hero - [Basic](hero/basic/main.go) diff --git a/_examples/README_ZH.md b/_examples/README_ZH.md index 3958a5d4a0..53e2a75508 100644 --- a/_examples/README_ZH.md +++ b/_examples/README_ZH.md @@ -13,15 +13,15 @@ - [Hello WebAssemply!](webassembly/basic/main.go) **NEW** - [基础](overview/main.go) - [教程: 在线人数](tutorial/online-visitors/main.go) -- [教程: A Todo MVC Application using Iris and Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) +- [教程: 一个“待完成”MVC Application基于Iris和Vue.js](https://hackernoon.com/a-todo-mvc-application-using-iris-and-vue-js-5019ff870064) - [教程: 结合 BoltDB 生成短网址](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) - [教程: 用安卓设备搭建服务器 (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988) -- [POC: Convert the medium-sized project "Parrot" from native to Iris](https://github.com/iris-contrib/parrot) -- [POC: Isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/kataras/iris-starter-kit) +- [POC: 把中等项目"Parrot"从原生转换到Iris](https://github.com/iris-contrib/parrot) +- [POC: 同构react/hot reloadable/redux/css-modules的起始工具包](https://github.com/kataras/iris-starter-kit) - [教程: DropzoneJS 上传](tutorial/dropzonejs) - [教程: Caddy 服务器使用](tutorial/caddy) - [教程: Iris + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) -- [教程: API for Apache Kafka](tutorial/api-for-apache-kafka) **NEW** +- [教程: Apache Kafka的API](tutorial/api-for-apache-kafka) **NEW** ### 目录结构 @@ -38,16 +38,16 @@ Iris 是个底层框架, 对 MVC 模式有很好的支持,但不限制文件 - [基础用法](http-listening/listen-addr/main.go)    * [忽略错误信息](http-listening/listen-addr/omit-server-errors/main.go) -- [UNIX socket file](http-listening/listen-unix/main.go) +- [UNIX socket文件](http-listening/listen-unix/main.go) - [TLS](http-listening/listen-tls/main.go) - [Letsencrypt (自动认证)](http-listening/listen-letsencrypt/main.go) - [进程关闭通知](http-listening/notify-on-shutdown/main.go) - 自定义 TCP 监听器    * [通用 net.Listener](http-listening/custom-listener/main.go) - * [SO_REUSEPORT for unix systems](http-listening/custom-listener/unix-reuseport/main.go) -- 自定义 HTTP 服务 -    * [easy way](http-listening/custom-httpserver/easy-way/main.go) - * [std way](http-listening/custom-httpserver/std-way/main.go) + * [unix系统的SO_REUSEPORT](http-listening/custom-listener/unix-reuseport/main.go) +- 自定义 HTTP 服务 +    * [简单方式](http-listening/custom-httpserver/easy-way/main.go) + * [标准方式](http-listening/custom-httpserver/std-way/main.go)    * [多个服务示例](http-listening/custom-httpserver/multi/main.go) - 优雅关闭    * [使用 `RegisterOnInterrupt`](http-listening/graceful-shutdown/default-notifier/main.go) @@ -105,7 +105,9 @@ app.Get("{root:path}", rootWildcardHandler) - [自定义 HTTP 错误](routing/http-errors/main.go) - [动态路径](routing/dynamic-path/main.go) * [根级通配符路径](routing/dynamic-path/root-wildcard/main.go) +- [编写你自己的参数类型](routing/macros/main.go) **NEW** - [反向路由](routing/reverse/main.go) +- [自定义路由(高层级)](routing/custom-high-level-router/main.go) **NEW** - [自定义包装](routing/custom-wrapper/main.go) - 自定义上下文    * [方法重写](routing/custom-context/method-overriding/main.go) @@ -120,7 +122,7 @@ app.Get("{root:path}", rootWildcardHandler) - [基础](hero/basic/main.go) - [概览](hero/overview) - [Sessions](hero/sessions) **NEW** -- [Yet another dependency injection example and good practises at general](hero/smart-contract/main.go) **NEW** +- [另一种依赖注入的例子和通常的较好实践](hero/smart-contract/main.go) **NEW** ### MVC 模式 @@ -132,11 +134,10 @@ Iris 支持快速的请求数据,模型,持久性数据和绑定。 **特点** -All HTTP Methods are supported, for example if want to serve `GET` -then the controller should have a function named `Get()`, -you can define more than one method function to serve in the same Controller. +支持所有的HTTP访问方式,例如,如果想要处理`GET`请求,那么控制器应该有一个叫做`Get()`的函数 +你可以在同一个控制器上面定义不止一个请求处理函数(method function). -Serve custom controller's struct's methods as handlers with custom paths(even with regex parametermized path) via the `BeforeActivation` custom event callback, per-controller. Example: +通过`BeforeActivation`对每个控制器自定义事件回调,使自定义控制器的结构方法(struct's methods)处理自定义路径(甚至是包含参数是正则表达式的路径),例如: ```go import ( @@ -160,12 +161,12 @@ type MyController struct {} func (m *MyController) BeforeActivation(b mvc.BeforeActivation) { // b.Dependencies().Add/Remove - // b.Router().Use/UseGlobal/Done // and any standard API call you already know + // b.Router().Use/UseGlobal/Done // 以及任何你已经知道的标准API调用 - // 1-> Method - // 2-> Path - // 3-> The controller's function name to be parsed as handler - // 4-> Any handlers that should run before the MyCustomHandler + // 1-> 方法 + // 2-> 路径 + // 3-> 被解释成handler的控制器函数名称 + // 4-> 在MyCustomHandler之前需要执行的其他handler b.Handle("GET", "/something/{id:long}", "MyCustomHandler", anyMiddleware...) } @@ -176,37 +177,35 @@ func (m *MyController) Get() string { return "Hey" } func (m *MyController) MyCustomHandler(id int64) string { return "MyCustomHandler says Hey" } ``` -Persistence data inside your Controller struct (share data between requests) -by defining services to the Dependencies or have a `Singleton` controller scope. +通过定义依赖项服务或者有一个`单一(Singleton)`控制器范畴来持久化你控制器结构中的数据(多次请求间共享的数据) + +在控制器间共享依赖或者把他们注册到上层MVC应用,以及 +在一个控制器内部可以修改每一个控制器在`BeforeActivation`可选事件回调上的依赖项的能力。 +即 `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`. -Share the dependencies between controllers or register them on a parent MVC Application, and ability -to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, -i.e `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`. +访问`Context`作为一个控制器的域(field)(不需要手动绑定)即`Ctx iris.Context`,或者通过一个方法的输入参数,即`func(ctx iris.Context, otherArguments...)`。 -Access to the `Context` as a controller's field(no manual binding is neede) i.e `Ctx iris.Context` or via a method's input argument, i.e `func(ctx iris.Context, otherArguments...)`. +你控制器结构中的模型(在模型函数中设置并且由视图来渲染) +你可以通过一个控制器的方法来返回模型或者设置一个请求生命周期的域 -Models inside your Controller struct (set-ed at the Method function and rendered by the View). -You can return models from a controller's method or set a field in the request lifecycle -and return that field to another method, in the same request lifecycle. +并且在同一个生命周期中把这个域返回给另一个方法。 -Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. -`Controllers` can be registered to any `Party`, including Subdomains, the Party's begin and done handlers work as expected. +正如你之前所熟知的流程,mvc应用程序有它自己的以标准iris API `iris/router.Party`作为类型的`路由(router)`。 +`控制器`可以被注册到任意`集合(party)`,包括子域名,这个集合会像所预料那样开始完成处理器工作。 -Optional `BeginRequest(ctx)` function to perform any initialization before the method execution, -useful to call middlewares or when many methods use the same collection of data. +可额外调用`BeginRequest(ctx)`在任何方法被执行前进行初始化,对于调用中间层(middlewares)或者使用同一组数据的多个方法而言十分有效 -Optional `EndRequest(ctx)` function to perform any finalization after any method executed. +同样可调用`EndRequest(ctx)`在任何方法执行后做完成工作(finalization) -Inheritance, recursively, see for example our `mvc.SessionController`, it has the `Session *sessions.Session` and `Manager *sessions.Sessions` as embedded fields -which are filled by its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go). -This is just an example, you could use the `sessions.Session` which returned from the manager's `Start` as a dynamic dependency to the MVC Application, i.e -`mvcApp.Register(sessions.New(sessions.Config{Cookie: "iris_session_id"}).Start)`. +递归继承参考我们`mvc.SessionController`的例子,它以`Session *sessions.Session`和`Manager *sessions.Sessions`作为嵌入域,由它的`BeginRequest`来传递,看[这里](https://github.com/kataras/iris/blob/master/mvc/session_controller.go) -Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. -When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods -with the `By` word, uppercase is a new sub path. Example: +这只是一个例子,你可以使用`sessions.Session`,它作为一个MVC应用的动态依赖从管理者的`Start`返回,即 +`mvcApp.Register(sessions.New(sessions.Config{Cookie: "iris_session_id"}).Start)`. -If `mvc.New(app.Party("/user")).Handle(new(user.Controller))` +通过控制器方法的输入参数来访问动态路径参数,不需要绑定。 +当你需要使用Iris的默认语法从一个控制器里来解析一个handler,你需要在这个方法前加上`By`, 大写表示一个新的子路径。例如: + +如果是 `mvc.New(app.Party("/user")).Handle(new(user.Controller))` - `func(*Controller) Get()` - `GET:/user`. - `func(*Controller) Post()` - `POST:/user`. @@ -217,17 +216,17 @@ If `mvc.New(app.Party("/user")).Handle(new(user.Controller))` - `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}` - `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}` -If `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` +如果是 `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` - `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` -If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` +如果是 `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` - `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` - Supported types for method functions receivers: int, int64, bool and string. + 支持的函数接收者类型是:int, int64, bool and string。 -Response via output arguments, optionally, i.e +可以通过输出参数来响应,即 ```go func(c *ExampleController) Get() string | @@ -246,13 +245,13 @@ func(c *ExampleController) Get() string | mvc.Result or (mvc.Result, error) ``` -where [mvc.Result](https://github.com/kataras/iris/blob/master/mvc/func_result.go) is an interface which contains only that function: `Dispatch(ctx iris.Context)`. +其中[mvc.Result](https://github.com/kataras/iris/blob/master/mvc/func_result.go)是一个仅包含`Dispatch(ctx iris.Context)`的接口 ## Iris MVC 模式代码复用 -By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same (or similar) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user. +通过创建互相独立的组建,开发者可以简单快捷地在别的应用里面复用组建。 一个应用同样的(或相似的)视图使用不同的数据可以被重构给另一个应用,因为视图仅仅处理数据怎么展示给用户。 -If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). +如果你是一个新的web后端开发者,请先阅读MVC架构模式,一个不错的入门是[wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). 参考下面的示例 @@ -289,21 +288,23 @@ If you're new to back-end web development read about the MVC architectural patte | amber | `iris.Amber(...)` | | pug(jade) | `iris.Pug(...)` | -- [Overview](view/overview/main.go) +- [Overview概览](view/overview/main.go) - [Hi](view/template_html_0/main.go) -- [A simple Layout](view/template_html_1/main.go) -- [Layouts: `yield` and `render` tmpl funcs](view/template_html_2/main.go) -- [The `urlpath` tmpl func](view/template_html_3/main.go) -- [The `url` tmpl func](view/template_html_4/main.go) -- [Inject Data Between Handlers](view/context-view-data/main.go) -- [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go) -- [Write to a custom `io.Writer`](view/write-to) -- [Greeting with Pug (Jade)`](view/template_pug_0) -- [Pug (Jade) Actions`](view/template_pug_1) -- [Pug (Jade) Includes`](view/template_pug_2) -- [Pug (Jade) Extends`](view/template_pug_3) +- [A simple Layout简单层](view/template_html_1/main.go) +- [Layouts: `yield` and `render` tmpl funcs 视图层`生产`以及`渲染`模版函数](view/template_html_2/main.go) +- [The `urlpath` tmpl func`urlpath`模版函数](view/template_html_3/main.go) +- [The `url` tmpl func`url`模版函数](view/template_html_4/main.go) +- [Inject Data Between Handlers在处理器间注入数据](view/context-view-data/main.go) +- [Embedding Templates Into App Executable File在应用程序可执行文件中嵌入模版](view/embedding-templates-into-app/main.go) +- [Write to a custom `io.Writer`自定义`io.Writer`](view/write-to) +- [Greeting with `Pug (Jade)`使用`Pug (Jade)`](view/template_pug_0) +- [`Pug (Jade) Actions`](view/template_pug_1) +- [`Pug (Jade) Includes`](view/template_pug_2) +- [`Pug (Jade) Extends`](view/template_pug_3) You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples. +只要使用`context#ResponseWriter`,你可以服务[quicktemplate](https://github.com/valyala/quicktemplate)和[hero templates](https://github.com/shiyanhui/hero/hero)文件。 +看这the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate)和[http_responsewriter/herotemplate](http_responsewriter/herotemplate)的例子 ### 认证 @@ -315,53 +316,56 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### 文件服务器 - [Favicon](file-server/favicon/main.go) -- [Basic](file-server/basic/main.go) -- [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) -- [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go) **NEW** -- [Send/Force-Download Files](file-server/send-files/main.go) -- Single Page Applications - * [single Page Application](file-server/single-page-application/basic/main.go) - * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) - * [embedded Single Page Application with other routes](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) - -### How to Read from `context.Request() *http.Request` - -- [Read JSON](http_request/read-json/main.go) -- [Read XML](http_request/read-xml/main.go) -- [Read Form](http_request/read-form/main.go) -- [Read Custom per type](http_request/read-custom-per-type/main.go) -- [Read Custom via Unmarshaler](http_request/read-custom-via-unmarshaler/main.go) -- [Upload/Read File](http_request/upload-file/main.go) -- [Upload multiple files with an easy way](http_request/upload-files/main.go) +- [基础操作](file-server/basic/main.go) +- [把文件嵌入应用的可执行文件](file-server/embedding-files-into-app/main.go) +- [嵌入Gzip压缩的文件到可咨询文件](file-server/embedding-gziped-files-into-app/main.go) **NEW** +- [上传/(强制)下载文件](file-server/send-files/main.go) +- 单页面应用(Single Page Applications) + * [单页面应用](file-server/single-page-application/basic/main.go) + * [嵌入式(embedded)单页面应用](file-server/single-page-application/embedded-single-page-application/main.go) + * [使用额外路由的嵌入式单页面应用](file-server/single-page-application/embedded-single-page-application-with-other-routes/main.go) + +### 如何读取`context.Request() *http.Request` + +- [读取JSON](http_request/read-json/main.go) +- [读取XML](http_request/read-xml/main.go) +- [读取Form](http_request/read-form/main.go) +- [读取每个类型的自定义结果Custom per type](http_request/read-custom-per-type/main.go) +- [通过Unmarshaler读取Custom](http_request/read-custom-via-unmarshaler/main.go) +- [上传/读取文件Upload/Read File](http_request/upload-file/main.go) +- [简单上传多个文件Upload multiple files with an easy way](http_request/upload-files/main.go) > The `context.Request()` returns the same *http.Request you already know, these examples show some places where the Context uses this object. Besides that you can use it as you did before iris. +> `context.Request()`返回你已知的同一*http.Request, 这些例子给出了Context使用这个对象的地方。 除此以外你可以在使用iris之前那样子使用它 -### How to Write to `context.ResponseWriter() http.ResponseWriter` +### 如何写入`context.ResponseWriter() http.ResponseWriter` -- [Write `valyala/quicktemplate` templates](http_responsewriter/quicktemplate) -- [Write `shiyanhui/hero` templates](http_responsewriter/herotemplate) +- [`valyala/quicktemplate`模版](http_responsewriter/quicktemplate) +- [`shiyanhui/hero`模版](http_responsewriter/herotemplate) - [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go) -- [Write Gzip](http_responsewriter/write-gzip/main.go) -- [Stream Writer](http_responsewriter/stream-writer/main.go) -- [Transactions](http_responsewriter/transactions/main.go) +- [写入Gzip压缩](http_responsewriter/write-gzip/main.go) +- [流输出Stream Writer](http_responsewriter/stream-writer/main.go) +- [数据传递Transactions](http_responsewriter/transactions/main.go) - [SSE](http_responsewriter/sse/main.go) **NEW** -- [SSE (third-party package usage for server sent events)](http_responsewriter/sse-third-party/main.go) +- [SSE (third-party package usage for server sent events第三方库SSE)](http_responsewriter/sse-third-party/main.go) > The `context/context#ResponseWriter()` returns an enchament version of a http.ResponseWriter, these examples show some places where the Context uses this object. Besides that you can use it as you did before iris. +> `context.Request()`返回了一个http.ResponseWriter的迷醉(魔幻)版本, 这些例子给出了Context使用这个对象的地方。 除此以外你可以在使用iris之前那样子使用它 + ### ORM -- [Using xorm(Mysql, MyMysql, Postgres, Tidb, **SQLite**, MsSql, MsSql, Oracle)](orm/xorm/main.go) +- [使用 xorm(Mysql, MyMysql, Postgres, Tidb, **SQLite**, MsSql, MsSql, Oracle)](orm/xorm/main.go) ### 其他 -- [Request Logger](http_request/request-logger/main.go) - * [log requests to a file](http_request/request-logger/request-logger-file/main.go) -- [Localization and Internationalization](miscellaneous/i18n/main.go) -- [Recovery](miscellaneous/recover/main.go) -- [Profiling (pprof)](miscellaneous/pprof/main.go) -- [Internal Application File Logger](miscellaneous/file-logger/main.go) -- [Google reCAPTCHA](miscellaneous/recaptcha/main.go) +- [请求记录器](http_request/request-logger/main.go) + * [将请求记录到文件](http_request/request-logger/request-logger-file/main.go) +- [本地化和多语言支持](miscellaneous/i18n/main.go) +- [恢复](miscellaneous/recover/main.go) +- [性能报告Profiling (pprof)](miscellaneous/pprof/main.go) +- [内部文件记录Internal Application File Logger](miscellaneous/file-logger/main.go) +- [Google验证码Google reCAPTCHA](miscellaneous/recaptcha/main.go) ### 试验性质处理器 @@ -372,9 +376,9 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [JWT](experimental-handlers/jwt/main.go) - [Newrelic](experimental-handlers/newrelic/simple/main.go) - [Prometheus](experimental-handlers/prometheus/simple/main.go) -- [Secure](experimental-handlers/secure/simple/main.go) +- [安全](experimental-handlers/secure/simple/main.go) - [Tollboothic](experimental-handlers/tollboothic/limit-handler/main.go) -- [Cross-Site Request Forgery Protection](experimental-handlers/csrf/main.go) +- [跨站点伪造请求(CSRF)防护](experimental-handlers/csrf/main.go) #### 更多 @@ -387,8 +391,9 @@ https://github.com/kataras/iris/tree/master/middleware#third-party-handlers ### 测试 The `httptest` package is your way for end-to-end HTTP testing, it uses the httpexpect library created by our friend, [gavv](https://github.com/gavv). +`httptest`包是你用于端对端HTTP测试的,它使用我们朋友[gavv](https://github.com/gavv)创建的httpexpect库 -[Example](testing/httptest/main_test.go) +[例子](testing/httptest/main_test.go) ### 缓存 @@ -401,18 +406,18 @@ Iris 独立缓存包 [package](https://github.com/kataras/iris/tree/master/cache ### Cookies -- [Basic](cookies/basic/main.go) -- [Encode/Decode (securecookie)](cookies/securecookie/main.go) +- [基础](cookies/basic/main.go) +- [加密/解密 (安全cookie)](cookies/securecookie/main.go) ### Sessions Iris session 管理独立包 [package](https://github.com/kataras/iris/tree/master/sessions). -- [Overview](sessions/overview/main.go) -- [Standalone](sessions/standalone/main.go) -- [Secure Cookie](sessions/securecookie/main.go) -- [Flash Messages](sessions/flash-messages/main.go) -- [Databases](sessions/database) +- [概览](sessions/overview/main.go) +- [独立使用](sessions/standalone/main.go) +- [安全cookie](sessions/securecookie/main.go) +- [临时消息](sessions/flash-messages/main.go) +- [数据库](sessions/database) * [Badger](sessions/database/badger/main.go) * [Redis](sessions/database/redis/main.go) @@ -420,16 +425,17 @@ Iris session 管理独立包 [package](https://github.com/kataras/iris/tree/mast ### Websockets -iris websocket library lives on its own [package](https://github.com/kataras/iris/tree/master/websocket). +iris websocket库依赖于它自己的[包](https://github.com/kataras/iris/tree/master/websocket). -The package is designed to work with raw websockets although its API is similar to the famous [socket.io](https://socket.io). I have read an article recently and I felt very contented about my decision to design a **fast** websocket-**only** package for Iris and not a backwards socket.io-like package. You can read that article by following this link: https://medium.com/@ivanderbyl/why-you-don-t-need-socket-io-6848f1c871cd. +设计这个包的目的是处理原始websockets,虽然它的API和著名的[socket.io](https://socket.io)很像。我最近读了一片文章,并且对我 +决定给iris设计一个**快速的**websocket**限定**包并且不是一个向后传递类socket.io的包。你可以阅读这个链接里的文章https://medium.com/@ivanderbyl/why-you-don-t-need-socket-io-6848f1c871cd。 -- [Chat](websocket/chat/main.go) -- [Native Messages](websocket/native-messages/main.go) -- [Connection List](websocket/connectionlist/main.go) -- [TLS Enabled](websocket/secure/main.go) -- [Custom Raw Go Client](websocket/custom-go-client/main.go) -- [Third-Party socket.io](websocket/third-party-socketio/main.go) +- [聊天](websocket/chat/main.go) +- [原生消息](websocket/native-messages/main.go) +- [连接列表](websocket/connectionlist/main.go) +- [TLS支持](websocket/secure/main.go) +- [自定义原始Go客户端](websocket/custom-go-client/main.go) +- [第三方socket.io](websocket/third-party-socketio/main.go) > 如果你愿意,你可以自由使用你自己喜欢的websockets包。 diff --git a/_examples/experimental-handlers/cors/simple/client/main.go b/_examples/experimental-handlers/cors/simple/client/main.go new file mode 100644 index 0000000000..9163f29a2a --- /dev/null +++ b/_examples/experimental-handlers/cors/simple/client/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "github.com/kataras/iris" +) + +// NOTE: THIS IS OPTIONALLY. +// It is just an example of communication between cors/simple/main.go and your app +// based on issues that beginners had with it. +// You should use your own favourite library for HTTP requests (any programming language ofc). +// +// Replace the '8fc93b1c.ngrok.io' with a domain which +// exposes the cors/simple/main.go server side. +const url = "http://8fc93b1c.ngrok.io/api/v1/mailer" + +var clientSide = []byte(``) + +func main() { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.Write(clientSide) + }) + + // Start and navigate to http://localhost:8080 + // and go to the previous terminal of your running cors/simple/main.go server + // and see the logs. + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/experimental-handlers/cors/simple/main.go b/_examples/experimental-handlers/cors/simple/main.go index ea8f4e70ed..5003329bac 100644 --- a/_examples/experimental-handlers/cors/simple/main.go +++ b/_examples/experimental-handlers/cors/simple/main.go @@ -1,23 +1,32 @@ package main -// go get -u github.com/iris-contrib/middleware/... - import ( "github.com/kataras/iris" - - "github.com/iris-contrib/middleware/cors" ) func main() { app := iris.New() - crs := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, // allows everything, use that to change the hosts. - AllowCredentials: true, - }) + crs := func(ctx iris.Context) { + ctx.Header("Access-Control-Allow-Origin", "*") + ctx.Header("Access-Control-Allow-Credentials", "true") + ctx.Header("Access-Control-Allow-Headers", "Access-Control-Allow-Origin,Content-Type") + ctx.Next() + } // or "github.com/iris-contrib/middleware/cors" v1 := app.Party("/api/v1", crs).AllowMethods(iris.MethodOptions) // <- important for the preflight. { + v1.Post("/mailer", func(ctx iris.Context) { + var any iris.Map + err := ctx.ReadJSON(&any) + if err != nil { + ctx.WriteString(err.Error()) + ctx.StatusCode(iris.StatusBadRequest) + return + } + ctx.Application().Logger().Infof("received %#+v", any) + }) + v1.Get("/home", func(ctx iris.Context) { ctx.WriteString("Hello from /home") }) @@ -35,5 +44,11 @@ func main() { }) } - app.Run(iris.Addr("localhost:8080")) + // iris.WithoutPathCorrectionRedirection | iris#Configuration.DisablePathCorrectionRedirection: + // CORS needs the allow origin headers in the redirect response as well, we have a solution for this: + // If you use iris >= v11.0.4 then add the `app.Run(..., iris.WithoutPathCorrectionRedirection)` + // on the server side if you wish + // to directly fire the handler instead of redirection (which is the default behavior) + // on request paths like "/v1/mailer/" when "/v1/mailer" route handler is registered. + app.Run(iris.Addr(":80"), iris.WithoutPathCorrectionRedirection) } diff --git a/_examples/hero/overview/main.go b/_examples/hero/overview/main.go index ac7221072b..efb188847f 100644 --- a/_examples/hero/overview/main.go +++ b/_examples/hero/overview/main.go @@ -50,8 +50,6 @@ func main() { app.Run( // Start the web server at localhost:8080 iris.Addr("localhost:8080"), - // disables updates: - iris.WithoutVersionChecker, // skip err server closed when CTRL/CMD+C pressed: iris.WithoutServerError(iris.ErrServerClosed), // enables faster json serialization and more: diff --git a/_examples/hero/sessions/main.go b/_examples/hero/sessions/main.go index aa1f67b3eb..7603453253 100644 --- a/_examples/hero/sessions/main.go +++ b/_examples/hero/sessions/main.go @@ -34,7 +34,6 @@ func main() { app.Run( iris.Addr(":8080"), - iris.WithoutVersionChecker, iris.WithoutServerError(iris.ErrServerClosed), ) } diff --git a/_examples/http-listening/iris-configurator-and-host-configurator/README.md b/_examples/http-listening/iris-configurator-and-host-configurator/README.md deleted file mode 100644 index 89ecb89c15..0000000000 --- a/_examples/http-listening/iris-configurator-and-host-configurator/README.md +++ /dev/null @@ -1,2 +0,0 @@ -A silly example for this issue: https://github.com/kataras/iris/issues/688#issuecomment-318828259. -However it seems useful and therefore is being included in the examples for everyone else. \ No newline at end of file diff --git a/_examples/http-listening/iris-configurator-and-host-configurator/counter/configurator.go b/_examples/http-listening/iris-configurator-and-host-configurator/counter/configurator.go deleted file mode 100644 index 7298830347..0000000000 --- a/_examples/http-listening/iris-configurator-and-host-configurator/counter/configurator.go +++ /dev/null @@ -1,30 +0,0 @@ -package counter - -import ( - "time" - - "github.com/kataras/iris" - "github.com/kataras/iris/core/host" -) - -func Configurator(app *iris.Application) { - counterValue := 0 - - go func() { - ticker := time.NewTicker(time.Second) - - for range ticker.C { - counterValue++ - } - - app.ConfigureHost(func(h *host.Supervisor) { // <- HERE: IMPORTANT - h.RegisterOnShutdown(func() { - ticker.Stop() - }) - }) // or put the ticker outside of the gofunc and put the configurator before or after the app.Get, outside of this gofunc - }() - - app.Get("/counter", func(ctx iris.Context) { - ctx.Writef("Counter value = %d", counterValue) - }) -} diff --git a/_examples/http-listening/iris-configurator-and-host-configurator/main.go b/_examples/http-listening/iris-configurator-and-host-configurator/main.go index a82d974f9e..f1d7ad85b9 100644 --- a/_examples/http-listening/iris-configurator-and-host-configurator/main.go +++ b/_examples/http-listening/iris-configurator-and-host-configurator/main.go @@ -1,14 +1,28 @@ package main import ( - "github.com/kataras/iris/_examples/http-listening/iris-configurator-and-host-configurator/counter" - "github.com/kataras/iris" ) func main() { app := iris.New() - app.Configure(counter.Configurator) - app.Run(iris.Addr(":8080")) + app.ConfigureHost(func(host *iris.Supervisor) { // <- HERE: IMPORTANT + // You can control the flow or defer something using some of the host's methods: + // host.RegisterOnError + // host.RegisterOnServe + host.RegisterOnShutdown(func() { + app.Logger().Infof("Application shutdown on signal") + }) + }) + + app.Get("/", func(ctx iris.Context) { + ctx.HTML("

Hello

\n") + }) + + app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) + + /* There are more easy ways to notify for global shutdown using the `iris.RegisterOnInterrupt` for default signal interrupt events. + You can even go it even further by looking at the: "graceful-shutdown" example. + */ } diff --git a/_examples/http-listening/notify-on-shutdown/main.go b/_examples/http-listening/notify-on-shutdown/main.go index 67400aa864..9a6e37091a 100644 --- a/_examples/http-listening/notify-on-shutdown/main.go +++ b/_examples/http-listening/notify-on-shutdown/main.go @@ -1,11 +1,10 @@ package main import ( - stdContext "context" + "context" "time" "github.com/kataras/iris" - "github.com/kataras/iris/core/host" ) func main() { @@ -15,20 +14,20 @@ func main() { ctx.HTML("

Hello, try to refresh the page after ~10 secs

") }) - // app.ConfigureHost(configureHost) -> or pass "configureHost" as `app.Addr` argument, same result. - app.Logger().Info("Wait 10 seconds and check your terminal again") // simulate a shutdown action here... go func() { <-time.After(10 * time.Second) timeout := 5 * time.Second - ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // close all hosts, this will notify the callback we had register // inside the `configureHost` func. app.Shutdown(ctx) }() + // app.ConfigureHost(configureHost) -> or pass "configureHost" as `app.Addr` argument, same result. + // start the server as usual, the only difference is that // we're adding a second (optional) function // to configure the just-created host supervisor. @@ -37,9 +36,14 @@ func main() { // wait 10 seconds and check your terminal. app.Run(iris.Addr(":8080", configureHost), iris.WithoutServerError(iris.ErrServerClosed)) + /* + Or for simple cases you can just use the: + iris.RegisterOnInterrupt for global catch of the CTRL/CMD+C and OS events. + Look at the "graceful-shutdown" example for more. + */ } -func configureHost(su *host.Supervisor) { +func configureHost(su *iris.Supervisor) { // here we have full access to the host that will be created // inside the `app.Run` function or `NewHost`. // diff --git a/_examples/http_request/extract-referer/main.go b/_examples/http_request/extract-referer/main.go index 8c14e54ea1..3e2436c562 100644 --- a/_examples/http_request/extract-referer/main.go +++ b/_examples/http_request/extract-referer/main.go @@ -25,5 +25,5 @@ func main() { // http://localhost:8080?referer=https://twitter.com/Xinterio/status/1023566830974251008 // http://localhost:8080?referer=https://www.google.com/search?q=Top+6+golang+web+frameworks&oq=Top+6+golang+web+frameworks - app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) } diff --git a/_examples/http_request/read-form/main.go b/_examples/http_request/read-form/main.go index 7b619a7b47..148bb19576 100644 --- a/_examples/http_request/read-form/main.go +++ b/_examples/http_request/read-form/main.go @@ -27,7 +27,7 @@ func main() { app.Post("/form_action", func(ctx iris.Context) { visitor := Visitor{} err := ctx.ReadForm(&visitor) - if err != nil { + if err != nil && !iris.IsErrPath(err) /* see: https://github.com/kataras/iris/issues/1157 */ { ctx.StatusCode(iris.StatusInternalServerError) ctx.WriteString(err.Error()) } diff --git a/_examples/http_request/read-json-struct-validation/main.go b/_examples/http_request/read-json-struct-validation/main.go index f7beedc0c4..16dab51166 100644 --- a/_examples/http_request/read-json-struct-validation/main.go +++ b/_examples/http_request/read-json-struct-validation/main.go @@ -36,7 +36,7 @@ func main() { // Register validation for 'User' // NOTE: only have to register a non-pointer type for 'User', validator - // interanlly dereferences during it's type checks. + // internally dereferences during it's type checks. validate.RegisterStructValidation(UserStructLevelValidation, User{}) app := iris.New() diff --git a/_examples/http_request/request-logger/request-logger-file-json/main.go b/_examples/http_request/request-logger/request-logger-file-json/main.go new file mode 100644 index 0000000000..2387de4882 --- /dev/null +++ b/_examples/http_request/request-logger/request-logger-file-json/main.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "os" + "runtime" + "strings" + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/middleware/logger" + + "github.com/kataras/golog" +) + +const deleteFileOnExit = false + +func main() { + app := iris.New() + + logFile := newLogFile() + defer func() { + logFile.Close() + if deleteFileOnExit { + os.Remove(logFile.Name()) + } + }() + + // Handle the logs by yourself using the `app.Logger#Handle` method. + // Return true if that handled, otherwise will print to the screen. + // You can also use the `app.Logger#SetOutput/AddOutput` to change or add + // multi (io.Writer) outputs if you just want to print the message + // somewhere else than the terminal screen. + app.Logger().Handle(func(l *golog.Log) bool { + _, fn, line, _ := runtime.Caller(5) + + var ( + // formatted date string based on the `golog#TimeFormat`, which can be customized. + // Or use the golog.Log#Time field to get the exact time.Time instance. + datetime = l.FormatTime() + // the log's message level. + level = golog.GetTextForLevel(l.Level, false) + // the log's message. + message = l.Message + // the source code line of where it is called, + // this can differ on your app, see runtime.Caller(%d). + source = fmt.Sprintf("%s#%d", fn, line) + ) + + // You can always use a custom json structure and json.Marshal and logFile.Write(its result) + // but it is faster to just build your JSON string by yourself as we do below. + jsonStr := fmt.Sprintf(`{"datetime":"%s","level":"%s","message":"%s","source":"%s"}`, datetime, level, message, source) + fmt.Fprintln(logFile, jsonStr) + + /* Example output: + {"datetime":"2018/10/31 13:13","level":"[INFO]","message":"My server started","source":"c:/mygopath/src/github.com/kataras/iris/_examples/http_request/request-logger/request-logger-file-json/main.go#71"} + */ + return true + }) + + r := newRequestLogger() + + app.Use(r) + app.OnAnyErrorCode(r, func(ctx iris.Context) { + ctx.HTML("

Error: Please try this instead.

") + }) + + h := func(ctx iris.Context) { + ctx.Writef("Hello from %s", ctx.Path()) + } + + app.Get("/", h) + + app.Get("/1", h) + + app.Get("/2", h) + + app.Logger().Info("My server started") + // http://localhost:8080 + // http://localhost:8080/1 + // http://localhost:8080/2 + // http://lcoalhost:8080/notfoundhere + app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +} + +var excludeExtensions = [...]string{ + ".js", + ".css", + ".jpg", + ".png", + ".ico", + ".svg", +} + +func newRequestLogger() iris.Handler { + c := logger.Config{ + Status: true, + IP: true, + Method: true, + Path: true, + } + + // we don't want to use the logger + // to log requests to assets and etc + c.AddSkipper(func(ctx iris.Context) bool { + path := ctx.Path() + for _, ext := range excludeExtensions { + if strings.HasSuffix(path, ext) { + return true + } + } + return false + }) + + return logger.New(c) +} + +// get a filename based on the date, file logs works that way the most times +// but these are just a sugar. +func todayFilename() string { + today := time.Now().Format("Jan 02 2006") + return today + ".json" +} + +func newLogFile() *os.File { + filename := todayFilename() + // open an output file, this will append to the today's file if server restarted. + f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + + return f +} diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go index 83bd350fc7..3d77e08ab2 100644 --- a/_examples/mvc/login/main.go +++ b/_examples/mvc/login/main.go @@ -79,8 +79,6 @@ func main() { app.Run( // Starts the web server at localhost:8080 iris.Addr("localhost:8080"), - // Disables the updater. - iris.WithoutVersionChecker, // Ignores err server closed log when CTRL/CMD+C pressed. iris.WithoutServerError(iris.ErrServerClosed), // Enables faster json serialization and more. diff --git a/_examples/mvc/overview/main.go b/_examples/mvc/overview/main.go index 9d7d9cf6d7..2201332524 100644 --- a/_examples/mvc/overview/main.go +++ b/_examples/mvc/overview/main.go @@ -33,8 +33,6 @@ func main() { app.Run( // Start the web server at localhost:8080 iris.Addr("localhost:8080"), - // disables updates: - iris.WithoutVersionChecker, // skip err server closed when CTRL/CMD+C pressed: iris.WithoutServerError(iris.ErrServerClosed), // enables faster json serialization and more: diff --git a/_examples/mvc/session-controller/main.go b/_examples/mvc/session-controller/main.go index 8774d448aa..aa3ea0514a 100644 --- a/_examples/mvc/session-controller/main.go +++ b/_examples/mvc/session-controller/main.go @@ -68,5 +68,5 @@ func main() { // 3. refresh the page some times // 4. close the browser // 5. re-open the browser and re-play 2. - app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080")) } diff --git a/_examples/overview/main.go b/_examples/overview/main.go index 29e52560e3..d416f29ee3 100644 --- a/_examples/overview/main.go +++ b/_examples/overview/main.go @@ -74,7 +74,7 @@ func main() { } // Listen for incoming HTTP/1.x & HTTP/2 clients on localhost port 8080. - app.Run(iris.Addr(":8080"), iris.WithCharset("UTF-8"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080"), iris.WithCharset("UTF-8")) } func logThisMiddleware(ctx iris.Context) { diff --git a/_examples/routing/README.md b/_examples/routing/README.md index 62cde33e32..88c40db999 100644 --- a/_examples/routing/README.md +++ b/_examples/routing/README.md @@ -110,9 +110,9 @@ app := iris.New() users := app.Party("/users", myAuthMiddlewareHandler) // http://localhost:8080/users/42/profile -users.Get("/{id:int}/profile", userProfileHandler) +users.Get("/{id:uint64}/profile", userProfileHandler) // http://localhost:8080/users/messages/1 -users.Get("/inbox/{id:int}", userMessageHandler) +users.Get("/inbox/{id:uint64}", userMessageHandler) ``` The same could be also written using a function which accepts the child router(the Party). @@ -124,13 +124,13 @@ app.PartyFunc("/users", func(users iris.Party) { users.Use(myAuthMiddlewareHandler) // http://localhost:8080/users/42/profile - users.Get("/{id:int}/profile", userProfileHandler) + users.Get("/{id:uint64}/profile", userProfileHandler) // http://localhost:8080/users/messages/1 - users.Get("/inbox/{id:int}", userMessageHandler) + users.Get("/inbox/{id:uint64}", userMessageHandler) }) ``` -> `id:int` is a (typed) dynamic path parameter, learn more by scrolling down. +> `id:uint` is a (typed) dynamic path parameter, learn more by scrolling down. # Dynamic Path Parameters @@ -152,23 +152,71 @@ Standard macro types for route path parameters | {param:string} | +------------------------+ string type -anything +anything (single path segmnent) + ++-------------------------------+ +| {param:int} | ++-------------------------------+ +int type +-9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch +------------------------+ -| {param:int} | +| {param:int8} | +------------------------+ -int type -only numbers (0-9) +int8 type +-128 to 127 + ++------------------------+ +| {param:int16} | ++------------------------+ +int16 type +-32768 to 32767 + ++------------------------+ +| {param:int32} | ++------------------------+ +int32 type +-2147483648 to 2147483647 +------------------------+ -| {param:long} | +| {param:int64} | +------------------------+ int64 type -only numbers (0-9) +-9223372036854775808 to 9223372036854775807 + ++------------------------+ +| {param:uint} | ++------------------------+ +uint type +0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32) + ++------------------------+ +| {param:uint8} | ++------------------------+ +uint8 type +0 to 255 + ++------------------------+ +| {param:uint16} | ++------------------------+ +uint16 type +0 to 65535 +------------------------+ -| {param:boolean} | +| {param:uint32} | +------------------------+ +uint32 type +0 to 4294967295 + ++------------------------+ +| {param:uint64} | ++------------------------+ +uint64 type +0 to 18446744073709551615 + ++---------------------------------+ +| {param:bool} or {param:boolean} | ++---------------------------------+ bool type only "1" or "t" or "T" or "TRUE" or "true" or "True" or "0" or "f" or "F" or "FALSE" or "false" or "False" @@ -194,8 +242,8 @@ no spaces ! or other character | {param:path} | +------------------------+ path type -anything, should be the last part, more than one path segment, -i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3" +anything, should be the last part, can be more than one path segment, +i.e: "/test/*param" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3" ``` If type is missing then parameter's type is defaulted to string, so @@ -209,18 +257,24 @@ you are able to register your own too!. Register a named path parameter function ```go -app.Macros().Int.RegisterFunc("min", func(argument int) func(paramValue string) bool { +app.Macros().Get("int").RegisterFunc("min", func(argument int) func(paramValue int) bool { // [...] - return true - // -> true means valid, false means invalid fire 404 or if "else 500" is appended to the macro syntax then internal server error. + return func(paramValue int) bool { + // -> true means valid, false means invalid fire 404 + // or if "else 500" is appended to the macro syntax then internal server error. + return true + } }) ``` -At the `func(argument ...)` you can have any standard type, it will be validated before the server starts so don't care about any performance cost there, the only thing it runs at serve time is the returning `func(paramValue string) bool`. +At the `func(argument ...)` you can have any standard type, it will be validated before the server starts so don't care about any performance cost there, the only thing it runs at serve time is the returning `func(paramValue ) bool`. ```go -{param:string equal(iris)} , "iris" will be the argument here: -app.Macros().String.RegisterFunc("equal", func(argument string) func(paramValue string) bool { +{param:string equal(iris)} +``` +The "iris" will be the argument here: +```go +app.Macros().Get("string").RegisterFunc("equal", func(argument string) func(paramValue string) bool { return func(paramValue string){ return argument == paramValue } }) ``` @@ -237,38 +291,34 @@ app.Get("/username/{name}", func(ctx iris.Context) { // Let's register our first macro attached to int macro type. // "min" = the function // "minValue" = the argument of the function -// func(string) bool = the macro's path parameter evaluator, this executes in serve time when +// func() bool = the macro's path parameter evaluator, this executes in serve time when // a user requests a path which contains the :int macro type with the min(...) macro parameter function. -app.Macros().Int.RegisterFunc("min", func(minValue int) func(string) bool { +app.Macros().Get("int").RegisterFunc("min", func(minValue int) func(int) bool { // do anything before serve here [...] // at this case we don't need to do anything - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - return n >= minValue + return func(paramValue int) bool { + return paramValue >= minValue } }) // http://localhost:8080/profile/id>=1 // this will throw 404 even if it's found as route on : /profile/0, /profile/blabla, /profile/-1 // macro parameter functions are optional of course. -app.Get("/profile/{id:int min(1)}", func(ctx iris.Context) { +app.Get("/profile/{id:uint64 min(1)}", func(ctx iris.Context) { // second parameter is the error but it will always nil because we use macros, // the validaton already happened. - id, _ := ctx.Params().GetInt("id") + id, _ := ctx.Params().GetUint64("id") ctx.Writef("Hello id: %d", id) }) // to change the error code per route's macro evaluator: -app.Get("/profile/{id:int min(1)}/friends/{friendid:int min(1) else 504}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") - friendid, _ := ctx.Params().GetInt("friendid") +app.Get("/profile/{id:uint64 min(1)}/friends/{friendid:uint64 min(1) else 504}", func(ctx iris.Context) { + id, _ := ctx.Params().GetUint64("id") + friendid, _ := ctx.Params().GetUint64("friendid") ctx.Writef("Hello id: %d looking for friend id: ", id, friendid) }) // this will throw e 504 error code instead of 404 if all route's macros not passed. -// http://localhost:8080/game/a-zA-Z/level/0-9 +// http://localhost:8080/game/a-zA-Z/level/42 // remember, alphabetical is lowercase or uppercase letters only. app.Get("/game/{name:alphabetical}/level/{level:int}", func(ctx iris.Context) { ctx.Writef("name: %s | level: %s", ctx.Params().Get("name"), ctx.Params().Get("level")) @@ -297,12 +347,10 @@ app.Run(iris.Addr(":8080")) } ``` -A **path parameter name should contain only alphabetical letters. Symbols like '_' and numbers are NOT allowed**. - +A path parameter name should contain only alphabetical letters or digits. Symbols like '_' are NOT allowed. Last, do not confuse `ctx.Params()` with `ctx.Values()`. -Path parameter's values goes to `ctx.Params()` and context's local storage -that can be used to communicate between handlers and middleware(s) goes to -`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()`. # Routing and reverse lookups @@ -776,6 +824,32 @@ type Context interface { // IsStopped checks and returns true if the current position of the Context is 255, // means that the StopExecution() was called. IsStopped() bool + // OnConnectionClose registers the "cb" function which will fire (on its own goroutine, no need to be registered goroutine by the end-dev) + // when the underlying connection has gone away. + // + // This mechanism can be used to cancel long operations on the server + // if the client has disconnected before the response is ready. + // + // It depends on the `http#CloseNotify`. + // CloseNotify may wait to notify until Request.Body has been + // fully read. + // + // After the main Handler has returned, there is no guarantee + // that the channel receives a value. + // + // Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported). + // The "cb" will not fire for sure if the output value is false. + // + // Note that you can register only one callback for the entire request handler chain/per route. + // + // Look the `ResponseWriter#CloseNotifier` for more. + OnConnectionClose(fnGoroutine func()) bool + // OnClose registers the callback function "cb" to the underline connection closing event using the `Context#OnConnectionClose` + // and also in the end of the request handler using the `ResponseWriter#SetBeforeFlush`. + // Note that you can register only one callback for the entire request handler chain/per route. + // + // Look the `Context#OnConnectionClose` and `ResponseWriter#SetBeforeFlush` for more. + OnClose(cb func()) // +------------------------------------------------------------+ // | Current "user/request" storage | @@ -855,8 +929,12 @@ type Context interface { // // Keep note that this checks the "User-Agent" request header. IsMobile() bool + // GetReferrer extracts and returns the information from the "Referer" header as specified + // in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + // or by the URL query parameter "referer". + GetReferrer() Referrer // +------------------------------------------------------------+ - // | Response Headers helpers | + // | Headers helpers | // +------------------------------------------------------------+ // Header adds a header to the response writer. @@ -867,16 +945,18 @@ type Context interface { // GetContentType returns the response writer's header value of "Content-Type" // which may, setted before with the 'ContentType'. GetContentType() string + // GetContentType returns the request's header value of "Content-Type". + GetContentTypeRequested() string // GetContentLength returns the request's header value of "Content-Length". // Returns 0 if header was unable to be found or its value was not a valid number. GetContentLength() int64 // StatusCode sets the status code header to the response. - // Look .GetStatusCode too. + // Look .`GetStatusCode` too. StatusCode(statusCode int) // GetStatusCode returns the current status code of the response. - // Look StatusCode too. + // Look `StatusCode` too. GetStatusCode() int // Redirect sends a redirect response to the client @@ -911,6 +991,9 @@ type Context interface { // URLParamIntDefault returns the url query parameter as int value from a request, // if not found or parse failed then "def" is returned. URLParamIntDefault(name string, def int) int + // URLParamInt32Default returns the url query parameter as int32 value from a request, + // if not found or parse failed then "def" is returned. + URLParamInt32Default(name string, def int32) int32 // URLParamInt64 returns the url query parameter as int64 value from a request, // returns -1 and an error if parse failed. URLParamInt64(name string) (int64, error) @@ -1058,6 +1141,10 @@ type Context interface { // Examples of usage: context.ReadJSON, context.ReadXML. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-custom-via-unmarshaler/main.go + // + // UnmarshalBody does not check about gzipped data. + // Do not rely on compressed data incoming to your server. The main reason is: https://en.wikipedia.org/wiki/Zip_bomb + // However you are still free to read the `ctx.Request().Body io.Reader` manually. UnmarshalBody(outPtr interface{}, unmarshaler Unmarshaler) error // ReadJSON reads JSON from request's body and binds it to a pointer of a value of any json-valid type. // @@ -1068,7 +1155,8 @@ type Context interface { // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go ReadXML(xmlObjectPtr interface{}) error // ReadForm binds the formObject with the form data - // it supports any kind of struct. + // it supports any kind of type, including custom structs. + // It will return nothing if request data are empty. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go ReadForm(formObjectPtr interface{}) error diff --git a/_examples/routing/basic/main.go b/_examples/routing/basic/main.go index d694b20cff..d03ea46c0d 100644 --- a/_examples/routing/basic/main.go +++ b/_examples/routing/basic/main.go @@ -29,12 +29,12 @@ func main() { app.Get("/donate", donateHandler, donateFinishHandler) // Pssst, don't forget dynamic-path example for more "magic"! - app.Get("/api/users/{userid:int min(1)}", func(ctx iris.Context) { - userID, err := ctx.Params().GetInt("userid") + app.Get("/api/users/{userid:uint64 min(1)}", func(ctx iris.Context) { + userID, err := ctx.Params().GetUint64("userid") if err != nil { ctx.Writef("error while trying to parse userid parameter," + - "this will never happen if :int is being used because if it's not integer it will fire Not Found automatically.") + "this will never happen if :uint64 is being used because if it's not a valid uint64 it will fire Not Found automatically.") ctx.StatusCode(iris.StatusBadRequest) return } diff --git a/_examples/routing/custom-high-level-router/main.go b/_examples/routing/custom-high-level-router/main.go new file mode 100644 index 0000000000..77830024c5 --- /dev/null +++ b/_examples/routing/custom-high-level-router/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "strings" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" +) + +/* A Router should contain all three of the following methods: + - HandleRequest should handle the request based on the Context. + HandleRequest(ctx context.Context) + - Build should builds the handler, it's being called on router's BuildRouter. + Build(provider router.RoutesProvider) error + - RouteExists reports whether a particular route exists. + RouteExists(ctx context.Context, method, path string) bool + +For a more detailed, complete and useful example +you can take a look at the iris' router itself which is located at: +https://github.com/kataras/iris/tree/master/core/router/handler.go +which completes this exact interface, the `router#RequestHandler`. +*/ +type customRouter struct { + // a copy of routes (safer because you will not be able to alter a route on serve-time without a `app.RefreshRouter` call): + // []router.Route + // or just expect the whole routes provider: + provider router.RoutesProvider +} + +// HandleRequest a silly example which finds routes based only on the first part of the requested path +// which must be a static one as well, the rest goes to fill the parameters. +func (r *customRouter) HandleRequest(ctx context.Context) { + path := ctx.Path() + ctx.Application().Logger().Infof("Requested resource path: %s", path) + + parts := strings.Split(path, "/")[1:] + staticPath := "/" + parts[0] + for _, route := range r.provider.GetRoutes() { + if strings.HasPrefix(route.Path, staticPath) && route.Method == ctx.Method() { + paramParts := parts[1:] + for _, paramValue := range paramParts { + for _, p := range route.Tmpl().Params { + ctx.Params().Set(p.Name, paramValue) + } + } + + ctx.SetCurrentRouteName(route.Name) + ctx.Do(route.Handlers) + return + } + } + + // if nothing found... + ctx.StatusCode(iris.StatusNotFound) +} + +func (r *customRouter) Build(provider router.RoutesProvider) error { + for _, route := range provider.GetRoutes() { + // do any necessary validation or conversations based on your custom logic here + // but always run the "BuildHandlers" for each registered route. + route.BuildHandlers() + // [...] r.routes = append(r.routes, *route) + } + + r.provider = provider + return nil +} + +func (r *customRouter) RouteExists(ctx context.Context, method, path string) bool { + // [...] + return false +} + +func main() { + app := iris.New() + + // In case you are wondering, the parameter types and macros like "{param:string $func()}" still work inside + // your custom router if you fetch by the Route's Handler + // because they are middlewares under the hood, so you don't have to implement the logic of handling them manually, + // though you have to match what requested path is what route and fill the ctx.Params(), this is the work of your custom router. + app.Get("/hello/{name}", func(ctx context.Context) { + name := ctx.Params().Get("name") + ctx.Writef("Hello %s\n", name) + }) + + app.Get("/cs/{num:uint64 min(10) else 400}", func(ctx context.Context) { + num := ctx.Params().GetUint64Default("num", 0) + ctx.Writef("num is: %d\n", num) + }) + + // To replace the existing router with a customized one by using the iris/context.Context + // you have to use the `app.BuildRouter` method before `app.Run` and after the routes registered. + // You should pass your custom router's instance as the second input arg, which must completes the `router#RequestHandler` + // interface as shown above. + // + // To see how you can build something even more low-level without direct iris' context support (you can do that manually as well) + // navigate to the "custom-wrapper" example instead. + myCustomRouter := new(customRouter) + app.BuildRouter(app.ContextPool, myCustomRouter, app.APIBuilder, true) + + app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +} diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go index 5a3f0b5751..b1ac1510e2 100644 --- a/_examples/routing/dynamic-path/main.go +++ b/_examples/routing/dynamic-path/main.go @@ -2,7 +2,6 @@ package main import ( "regexp" - "strconv" "github.com/kataras/iris" ) @@ -14,15 +13,14 @@ func main() { // we've seen static routes, group of routes, subdomains, wildcard subdomains, a small example of parameterized path // with a single known paramete and custom http errors, now it's time to see wildcard parameters and macros. - // iris, like net/http std package registers route's handlers + // Iris, like net/http std package registers route's handlers // by a Handler, the iris' type of handler is just a func(ctx iris.Context) // where context comes from github.com/kataras/iris/context. - // Until go 1.9 you will have to import that package too, after go 1.9 this will be not be necessary. // - // iris has the easiest and the most powerful routing process you have ever meet. + // Iris has the easiest and the most powerful routing process you have ever meet. // // At the same time, - // iris has its own interpeter(yes like a programming language) + // Iris has its own interpeter(yes like a programming language) // for route's path syntax and their dynamic path parameters parsing and evaluation, // We call them "macros" for shortcut. // How? It calculates its needs and if not any special regexp needed then it just @@ -30,40 +28,88 @@ func main() { // otherwise it pre-compiles the regexp and adds the necessary middleware(s). // // Standard macro types for parameters: - // +------------------------+ - // | {param:string} | - // +------------------------+ + // +------------------------+ + // | {param:string} | + // +------------------------+ // string type - // anything + // anything (single path segmnent) // - // +------------------------+ - // | {param:int} | - // +------------------------+ + // +-------------------------------+ + // | {param:int} | + // +-------------------------------+ // int type - // only numbers (0-9) + // -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch + // + // +------------------------+ + // | {param:int8} | + // +------------------------+ + // int8 type + // -128 to 127 + // + // +------------------------+ + // | {param:int16} | + // +------------------------+ + // int16 type + // -32768 to 32767 + // + // +------------------------+ + // | {param:int32} | + // +------------------------+ + // int32 type + // -2147483648 to 2147483647 // // +------------------------+ - // | {param:long} | + // | {param:int64} | // +------------------------+ // int64 type - // only numbers (0-9) + // -9223372036854775808 to 9223372036854775807 + // + // +------------------------+ + // | {param:uint} | + // +------------------------+ + // uint type + // 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32) // // +------------------------+ - // | {param:boolean} | + // | {param:uint8} | // +------------------------+ + // uint8 type + // 0 to 255 + // + // +------------------------+ + // | {param:uint16} | + // +------------------------+ + // uint16 type + // 0 to 65535 + // + // +------------------------+ + // | {param:uint32} | + // +------------------------+ + // uint32 type + // 0 to 4294967295 + // + // +------------------------+ + // | {param:uint64} | + // +------------------------+ + // uint64 type + // 0 to 18446744073709551615 + // + // +---------------------------------+ + // | {param:bool} or {param:boolean} | + // +---------------------------------+ // bool type // only "1" or "t" or "T" or "TRUE" or "true" or "True" // or "0" or "f" or "F" or "FALSE" or "false" or "False" // - // +------------------------+ - // | {param:alphabetical} | - // +------------------------+ + // +------------------------+ + // | {param:alphabetical} | + // +------------------------+ // alphabetical/letter type // letters only (upper or lowercase) // - // +------------------------+ - // | {param:file} | - // +------------------------+ + // +------------------------+ + // | {param:file} | + // +------------------------+ // file type // letters (upper or lowercase) // numbers (0-9) @@ -72,12 +118,12 @@ func main() { // point (.) // no spaces ! or other character // - // +------------------------+ - // | {param:path} | - // +------------------------+ + // +------------------------+ + // | {param:path} | + // +------------------------+ // path type - // anything, should be the last part, more than one path segment, - // i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3" + // anything, should be the last part, can be more than one path segment, + // i.e: "/test/{param:path}" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3" // // if type is missing then parameter's type is defaulted to string, so // {param} == {param:string}. @@ -89,7 +135,7 @@ func main() { // you are able to register your own too!. // // Register a named path parameter function: - // app.Macros().Int.RegisterFunc("min", func(argument int) func(paramValue string) bool { + // app.Macros().Number.RegisterFunc("min", func(argument int) func(paramValue string) bool { // [...] // return true/false -> true means valid. // }) @@ -107,51 +153,52 @@ func main() { ctx.Writef("Hello %s", ctx.Params().Get("name")) }) // type is missing = {name:string} - // Let's register our first macro attached to int macro type. + // Let's register our first macro attached to uint64 macro type. // "min" = the function // "minValue" = the argument of the function - // func(string) bool = the macro's path parameter evaluator, this executes in serve time when - // a user requests a path which contains the :int macro type with the min(...) macro parameter function. - app.Macros().Int.RegisterFunc("min", func(minValue int) func(string) bool { - // do anything before serve here [...] - // at this case we don't need to do anything - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - return n >= minValue + // func(uint64) bool = our func's evaluator, this executes in serve time when + // a user requests a path which contains the :uint64 macro parameter type with the min(...) macro parameter function. + app.Macros().Get("uint64").RegisterFunc("min", func(minValue uint64) func(uint64) bool { + // type of "paramValue" should match the type of the internal macro's evaluator function, which in this case is "uint64". + return func(paramValue uint64) bool { + return paramValue >= minValue } }) - // http://localhost:8080/profile/id>=1 + // http://localhost:8080/profile/id>=20 // this will throw 404 even if it's found as route on : /profile/0, /profile/blabla, /profile/-1 // macro parameter functions are optional of course. - app.Get("/profile/{id:int min(1)}", func(ctx iris.Context) { + app.Get("/profile/{id:uint64 min(20)}", func(ctx iris.Context) { // second parameter is the error but it will always nil because we use macros, // the validaton already happened. - id, _ := ctx.Params().GetInt("id") + id := ctx.Params().GetUint64Default("id", 0) ctx.Writef("Hello id: %d", id) }) // to change the error code per route's macro evaluator: - app.Get("/profile/{id:int min(1)}/friends/{friendid:int min(1) else 504}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") - friendid, _ := ctx.Params().GetInt("friendid") + app.Get("/profile/{id:uint64 min(1)}/friends/{friendid:uint64 min(1) else 504}", func(ctx iris.Context) { + id := ctx.Params().GetUint64Default("id", 0) + friendid := ctx.Params().GetUint64Default("friendid", 0) ctx.Writef("Hello id: %d looking for friend id: ", id, friendid) }) // this will throw e 504 error code instead of 404 if all route's macros not passed. - // Another example using a custom regexp and any custom logic. + // :uint8 0 to 255. + app.Get("/ages/{age:uint8 else 400}", func(ctx iris.Context) { + age, _ := ctx.Params().GetUint8("age") + ctx.Writef("age selected: %d", age) + }) + + // Another example using a custom regexp or any custom logic. + + // Register your custom argument-less macro function to the :string param type. latLonExpr := "^-?[0-9]{1,3}(?:\\.[0-9]{1,10})?$" latLonRegex, err := regexp.Compile(latLonExpr) if err != nil { panic(err) } - app.Macros().String.RegisterFunc("coordinate", func() func(paramName string) (ok bool) { - // MatchString is a type of func(string) bool, so we can return that as it's. - return latLonRegex.MatchString - }) + // MatchString is a type of func(string) bool, so we use it as it is. + app.Macros().Get("string").RegisterFunc("coordinate", latLonRegex.MatchString) app.Get("/coordinates/{lat:string coordinate() else 502}/{lon:string coordinate() else 502}", func(ctx iris.Context) { ctx.Writef("Lat: %s | Lon: %s", ctx.Params().Get("lat"), ctx.Params().Get("lon")) @@ -159,7 +206,43 @@ func main() { // - // http://localhost:8080/game/a-zA-Z/level/0-9 + // Another one is by using a custom body. + app.Macros().Get("string").RegisterFunc("range", func(minLength, maxLength int) func(string) bool { + return func(paramValue string) bool { + return len(paramValue) >= minLength && len(paramValue) <= maxLength + } + }) + + app.Get("/limitchar/{name:string range(1,200)}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + ctx.Writef(`Hello %s | the name should be between 1 and 200 characters length + otherwise this handler will not be executed`, name) + }) + + // + + // Register your custom macro function which accepts a slice of strings `[...,...]`. + app.Macros().Get("string").RegisterFunc("has", func(validNames []string) func(string) bool { + return func(paramValue string) bool { + for _, validName := range validNames { + if validName == paramValue { + return true + } + } + + return false + } + }) + + app.Get("/static_validation/{name:string has([kataras,gerasimos,maropoulos])}", func(ctx iris.Context) { + name := ctx.Params().Get("name") + ctx.Writef(`Hello %s | the name should be "kataras" or "gerasimos" or "maropoulos" + otherwise this handler will not be executed`, name) + }) + + // + + // http://localhost:8080/game/a-zA-Z/level/42 // remember, alphabetical is lowercase or uppercase letters only. app.Get("/game/{name:alphabetical}/level/{level:int}", func(ctx iris.Context) { ctx.Writef("name: %s | level: %s", ctx.Params().Get("name"), ctx.Params().Get("level")) @@ -197,10 +280,9 @@ func main() { // if "/mypath/{myparam:path}" then the parameter has two names, one is the "*" and the other is the user-defined "myparam". // WARNING: - // A path parameter name should contain only alphabetical letters. Symbols like '_' and numbers are NOT allowed. + // A path parameter name should contain only alphabetical letters or digits. Symbols like '_' are NOT allowed. // Last, do not confuse `ctx.Params()` with `ctx.Values()`. - // Path parameter's values goes to `ctx.Params()` and context's local storage - // that can be used to communicate between handlers and middleware(s) goes to - // `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()`. app.Run(iris.Addr(":8080")) } diff --git a/_examples/routing/macros/main.go b/_examples/routing/macros/main.go new file mode 100644 index 0000000000..3de4e29a80 --- /dev/null +++ b/_examples/routing/macros/main.go @@ -0,0 +1,76 @@ +// Package main shows how you can register a custom parameter type and macro functions that belongs to it. +package main + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/hero" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + app.Macros().Register("slice", "", false, true, func(paramValue string) (interface{}, bool) { + return strings.Split(paramValue, "/"), true + }).RegisterFunc("contains", func(expectedItems []string) func(paramValue []string) bool { + sort.Strings(expectedItems) + return func(paramValue []string) bool { + if len(paramValue) != len(expectedItems) { + return false + } + + sort.Strings(paramValue) + for i := 0; i < len(paramValue); i++ { + if paramValue[i] != expectedItems[i] { + return false + } + } + + return true + } + }) + + // In order to use your new param type inside MVC controller's function input argument or a hero function input argument + // you have to tell the Iris what type it is, the `ValueRaw` of the parameter is the same type + // as you defined it above with the func(paramValue string) (interface{}, bool). + // The new value and its type(from string to your new custom type) it is stored only once now, + // you don't have to do any conversions for simple cases like this. + context.ParamResolvers[reflect.TypeOf([]string{})] = func(paramIndex int) interface{} { + return func(ctx context.Context) []string { + // When you want to retrieve a parameter with a value type that it is not supported by-default, such as ctx.Params().GetInt + // then you can use the `GetEntry` or `GetEntryAt` and cast its underline `ValueRaw` to the desired type. + // The type should be the same as the macro's evaluator function (last argument on the Macros#Register) return value. + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.([]string) + } + } + + /* + http://localhost:8080/test_slice_hero/myvaluei1/myavlue2 -> + myparam's value (a trailing path parameter type) is: []string{"myvaluei1", "myavlue2"} + */ + app.Get("/test_slice_hero/{myparam:slice}", hero.Handler(func(myparam []string) string { + return fmt.Sprintf("myparam's value (a trailing path parameter type) is: %#v\n", myparam) + })) + + /* + http://localhost:8080/test_slice_contains/notcontains1/value2 -> + (404) Not Found + + http://localhost:8080/test_slice_contains/value1/value2 -> + myparam's value (a trailing path parameter type) is: []string{"value1", "value2"} + */ + app.Get("/test_slice_contains/{myparam:slice contains([value1,value2])}", func(ctx context.Context) { + // When it is not a built'n function available to retrieve your value with the type you want, such as ctx.Params().GetInt + // then you can use the `GetEntry.ValueRaw` to get the real value, which is set-ed by your macro above. + myparam := ctx.Params().GetEntry("myparam").ValueRaw.([]string) + ctx.Writef("myparam's value (a trailing path parameter type) is: %#v\n", myparam) + }) + + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/routing/main.go b/_examples/routing/main.go index d9af5f3d34..9f4503274e 100644 --- a/_examples/routing/main.go +++ b/_examples/routing/main.go @@ -35,25 +35,25 @@ func registerGamesRoutes(app *iris.Application) { { // braces are optional of course, it's just a style of code // "GET" method - games.Get("/{gameID:int}/clans", h) - games.Get("/{gameID:int}/clans/clan/{clanPublicID:int}", h) - games.Get("/{gameID:int}/clans/search", h) + games.Get("/{gameID:uint64}/clans", h) + games.Get("/{gameID:uint64}/clans/clan/{clanPublicID:uint64}", h) + games.Get("/{gameID:uint64}/clans/search", h) // "PUT" method - games.Put("/{gameID:int}/players/{clanPublicID:int}", h) - games.Put("/{gameID:int}/clans/clan/{clanPublicID:int}", h) + games.Put("/{gameID:uint64}/players/{clanPublicID:uint64}", h) + games.Put("/{gameID:uint64}/clans/clan/{clanPublicID:uint64}", h) // remember: "clanPublicID" should not be changed to other routes with the same prefix. // "POST" method - games.Post("/{gameID:int}/clans", h) - games.Post("/{gameID:int}/players", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/leave", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/application", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/application/{action}", h) // {action} == {action:string} - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/invitation", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/invitation/{action}", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/delete", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/promote", h) - games.Post("/{gameID:int}/clans/{clanPublicID:int}/memberships/demote", h) + games.Post("/{gameID:uint64}/clans", h) + games.Post("/{gameID:uint64}/players", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/leave", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/application", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/application/{action}", h) // {action} == {action:string} + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/invitation", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/invitation/{action}", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/delete", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/promote", h) + games.Post("/{gameID:uint64}/clans/{clanPublicID:uint64}/memberships/demote", h) gamesCh := games.Party("/challenge") { diff --git a/_examples/routing/overview/main.go b/_examples/routing/overview/main.go index 65c15dc6d7..d2ff717843 100644 --- a/_examples/routing/overview/main.go +++ b/_examples/routing/overview/main.go @@ -68,8 +68,8 @@ func main() { // GET: http://localhost:8080/users/42 // **/users/42 and /users/help works after iris version 7.0.5** - usersRoutes.Get("/{id:int}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") + usersRoutes.Get("/{id:uint64}", func(ctx iris.Context) { + id, _ := ctx.Params().GetUint64("id") ctx.Writef("get user by id: %d", id) }) @@ -80,15 +80,15 @@ func main() { }) // PUT: http://localhost:8080/users - usersRoutes.Put("/{id:int}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") // or .Get to get its string represatantion. + usersRoutes.Put("/{id:uint64}", func(ctx iris.Context) { + id, _ := ctx.Params().GetUint64("id") // or .Get to get its string represatantion. username := ctx.PostValue("username") ctx.Writef("update user for id= %d and new username= %s", id, username) }) // DELETE: http://localhost:8080/users/42 - usersRoutes.Delete("/{id:int}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") + usersRoutes.Delete("/{id:uint64}", func(ctx iris.Context) { + id, _ := ctx.Params().GetUint64("id") ctx.Writef("delete user by id: %d", id) }) diff --git a/_examples/routing/route-state/main.go b/_examples/routing/route-state/main.go index ec4f03dbce..59d0676323 100644 --- a/_examples/routing/route-state/main.go +++ b/_examples/routing/route-state/main.go @@ -31,6 +31,8 @@ func main() { // same as navigating to "http://localhost:8080/invisible/iris" when /change has being invoked and route state changed // from "offline" to "online" ctx.Values().Set("from", "/execute") // values and session can be shared when calling Exec from a "foreign" context. + // ctx.Exec("NONE", "/invisible/iris") + // or after "/change": ctx.Exec("GET", "/invisible/iris") }) diff --git a/_examples/subdomains/www/main.go b/_examples/subdomains/www/main.go index c41e343d52..9ecfc96920 100644 --- a/_examples/subdomains/www/main.go +++ b/_examples/subdomains/www/main.go @@ -13,11 +13,11 @@ func newApp() *iris.Application { app.PartyFunc("/api/users", func(r iris.Party) { r.Get("/", info) - r.Get("/{id:int}", info) + r.Get("/{id:uint64}", info) r.Post("/", info) - r.Put("/{id:int}", info) + r.Put("/{id:uint64}", info) }) /* <- same as: usersAPI := app.Party("/api/users") { // those brackets are just syntactic-sugar things. diff --git a/_examples/tutorial/url-shortener/main.go b/_examples/tutorial/url-shortener/main.go index 1516627c68..7fa9112316 100644 --- a/_examples/tutorial/url-shortener/main.go +++ b/_examples/tutorial/url-shortener/main.go @@ -2,7 +2,7 @@ // // Article: https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7 // -// $ go get github.com/coreos/bbolt +// $ go get github.com/etcd-io/bbolt // $ go get github.com/satori/go.uuid // $ cd $GOPATH/src/github.com/kataras/iris/_examples/tutorial/url-shortener // $ go build diff --git a/_examples/tutorial/url-shortener/store.go b/_examples/tutorial/url-shortener/store.go index 32238b0961..f2b78891f9 100644 --- a/_examples/tutorial/url-shortener/store.go +++ b/_examples/tutorial/url-shortener/store.go @@ -3,7 +3,7 @@ package main import ( "bytes" - "github.com/coreos/bbolt" + bolt "github.com/etcd-io/bbolt" ) // Panic panics, change it if you don't want to panic on critical INITIALIZE-ONLY-ERRORS diff --git a/_examples/tutorial/vuejs-todo-mvc/README.md b/_examples/tutorial/vuejs-todo-mvc/README.md index 00c912fa50..b856373bcc 100644 --- a/_examples/tutorial/vuejs-todo-mvc/README.md +++ b/_examples/tutorial/vuejs-todo-mvc/README.md @@ -27,7 +27,7 @@ Many articles have been written, in the past, that lead developers not to use a You’ll need two dependencies: 1. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/), latest v2. -2. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v10. +2. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v11. > If you have Go already installed then just execute `go get -u github.com/kataras/iris` to install the Iris Web Framework. @@ -530,7 +530,7 @@ func main() { todosApp.Handle(new(controllers.TodoController)) // start the web server at http://localhost:8080 - app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080")) } ``` diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go index c571e42630..34e5ed888d 100644 --- a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -51,5 +51,5 @@ func main() { todosApp.Handle(new(controllers.TodoController)) // start the web server at http://localhost:8080 - app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080")) } diff --git a/_examples/versioning/README.md b/_examples/versioning/README.md new file mode 100644 index 0000000000..d45f42d3d3 --- /dev/null +++ b/_examples/versioning/README.md @@ -0,0 +1 @@ +Head over to the [kataras/iris/versioning/README.md](https://github.com/kataras/iris/blob/master/versioning/README.md) instead. \ No newline at end of file diff --git a/_examples/versioning/main.go b/_examples/versioning/main.go new file mode 100644 index 0000000000..8516b190a0 --- /dev/null +++ b/_examples/versioning/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/versioning" +) + +func main() { + app := iris.New() + + examplePerRoute(app) + examplePerParty(app) + + // Read the README.md before any action. + app.Run(iris.Addr(":8080")) +} + +// How to test: +// Open Postman +// GET: localhost:8080/api/cats +// Headers[1] = Accept-Version: "1" and repeat with +// Headers[1] = Accept-Version: "2.5" +// or even "Accept": "application/json; version=2.5" +func examplePerRoute(app *iris.Application) { + app.Get("/api/cats", versioning.NewMatcher(versioning.Map{ + "1": catsVersionExactly1Handler, + ">= 2, < 3": catsV2Handler, + versioning.NotFound: versioning.NotFoundHandler, + })) +} + +// How to test: +// Open Postman +// GET: localhost:8080/api/users +// Headers[1] = Accept-Version: "1.9.9" and repeat with +// Headers[1] = Accept-Version: "2.5" +// +// POST: localhost:8080/api/users/new +// Headers[1] = Accept-Version: "1.8.3" +// +// POST: localhost:8080/api/users +// Headers[1] = Accept-Version: "2" +func examplePerParty(app *iris.Application) { + usersAPI := app.Party("/api/users") + + // version 1. + usersAPIV1 := versioning.NewGroup(">= 1, < 2") + usersAPIV1.Get("/", func(ctx iris.Context) { + ctx.Writef("v1 resource: /api/users handler") + }) + usersAPIV1.Post("/new", func(ctx iris.Context) { + ctx.Writef("v1 resource: /api/users/new post handler") + }) + + // version 2. + usersAPIV2 := versioning.NewGroup(">= 2, < 3") + usersAPIV2.Get("/", func(ctx iris.Context) { + ctx.Writef("v2 resource: /api/users handler") + }) + usersAPIV2.Post("/", func(ctx iris.Context) { + ctx.Writef("v2 resource: /api/users post handler") + }) + + versioning.RegisterGroups(usersAPI, versioning.NotFoundHandler, usersAPIV1, usersAPIV2) +} + +func catsVersionExactly1Handler(ctx iris.Context) { + ctx.Writef("v1 exactly resource: /api/cats handler") +} + +func catsV2Handler(ctx iris.Context) { + ctx.Writef("v2 resource: /api/cats handler") +} diff --git a/_examples/view/template_html_5/main.go b/_examples/view/template_html_5/main.go new file mode 100644 index 0000000000..9fcc7fdb83 --- /dev/null +++ b/_examples/view/template_html_5/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + app.RegisterView(iris.HTML("./views", ".html").Layout("layout.html")) + // TIP: append .Reload(true) to reload the templates on each request. + + app.Get("/home", func(ctx iris.Context) { + ctx.ViewData("title", "Home page"); + ctx.View("home.html") + // Note that: you can pass "layout" : "otherLayout.html" to bypass the config's Layout property + // or view.NoLayout to disable layout on this render action. + // third is an optional parameter + }) + + app.Get("/about", func(ctx iris.Context) { + ctx.View("about.html") + }) + + app.Get("/user/index", func(ctx iris.Context) { + ctx.View("user/index.html") + }) + + // http://localhost:8080 + app.Run(iris.Addr(":8080")) +} diff --git a/_examples/view/template_html_5/views/about.html b/_examples/view/template_html_5/views/about.html new file mode 100644 index 0000000000..a264299265 --- /dev/null +++ b/_examples/view/template_html_5/views/about.html @@ -0,0 +1,15 @@ +{{ define "about-head"}} + about page + +{{ end }} + +{{ define "about-body"}} + extend body content in layout. +{{ end }} +
+ Hello about page +
\ No newline at end of file diff --git a/_examples/view/template_html_5/views/home.html b/_examples/view/template_html_5/views/home.html new file mode 100644 index 0000000000..365990090f --- /dev/null +++ b/_examples/view/template_html_5/views/home.html @@ -0,0 +1,11 @@ +{{ define "home-head"}} + {{.title}} + +{{ end }} +
+ Hello home page +
\ No newline at end of file diff --git a/_examples/view/template_html_5/views/layout.html b/_examples/view/template_html_5/views/layout.html new file mode 100644 index 0000000000..c374205f7e --- /dev/null +++ b/_examples/view/template_html_5/views/layout.html @@ -0,0 +1,11 @@ + + +{{ part "head" }} + + +

[layout] Body content is below...

+ {{ part "body" }} + + {{ yield }} + + diff --git a/_examples/view/template_html_5/views/user/index.html b/_examples/view/template_html_5/views/user/index.html new file mode 100644 index 0000000000..6c73fb2e4a --- /dev/null +++ b/_examples/view/template_html_5/views/user/index.html @@ -0,0 +1,10 @@ +{{ define "user/index-head"}} + +{{ end }} +
+ Hello user index page +
\ No newline at end of file diff --git a/_examples/webassembly/basic/client/hello_go11beta3.go b/_examples/webassembly/basic/client/hello_go111.go similarity index 70% rename from _examples/webassembly/basic/client/hello_go11beta3.go rename to _examples/webassembly/basic/client/hello_go111.go index 9cbf5060d1..97dda1f577 100644 --- a/_examples/webassembly/basic/client/hello_go11beta3.go +++ b/_examples/webassembly/basic/client/hello_go111.go @@ -1,4 +1,4 @@ -// +build go1.11beta3 +// +build js package main @@ -9,7 +9,7 @@ import ( ) func main() { - // GOARCH=wasm GOOS=js /home/$yourusername/go1.11beta1/bin/go build -o hello.wasm hello_go11beta2.go + // GOARCH=wasm GOOS=js /home/$yourusername/go1.11/bin/go build -o hello.wasm hello_go111.go js.Global().Get("console").Call("log", "Hello WebAssemply!") message := fmt.Sprintf("Hello, the current time is: %s", time.Now().String()) js.Global().Get("document").Call("getElementById", "hello").Set("innerText", message) diff --git a/_examples/webassembly/basic/main.go b/_examples/webassembly/basic/main.go index 7dfe0ad282..4099da53a3 100644 --- a/_examples/webassembly/basic/main.go +++ b/_examples/webassembly/basic/main.go @@ -6,7 +6,7 @@ import ( /* You need to build the hello.wasm first, download the go1.11 and execute the below command: -$ cd client && GOARCH=wasm GOOS=js /home/$yourname/go1.11beta3/bin/go build -o hello.wasm hello_go11beta3.go +$ cd client && GOARCH=wasm GOOS=js /home/$yourname/go1.11/bin/go build -o hello.wasm hello_go111.go */ func main() { diff --git a/_examples/websocket/chat/main.go b/_examples/websocket/chat/main.go index fce0a664bc..e3f18b159f 100644 --- a/_examples/websocket/chat/main.go +++ b/_examples/websocket/chat/main.go @@ -26,8 +26,13 @@ func main() { func setupWebsocket(app *iris.Application) { // create our echo websocket server ws := websocket.New(websocket.Config{ + // These are low-level optionally fields, + // user/client can't see those values. ReadBufferSize: 1024, WriteBufferSize: 1024, + // only javascript client-side code has the same rule, + // which you serve using the ws.ClientSource (see below). + EvtMessagePrefix: []byte("my-custom-prefix:"), }) ws.OnConnection(handleConnection) @@ -38,7 +43,7 @@ func setupWebsocket(app *iris.Application) { // serve the javascript built'n client-side library, // see websockets.html script tags, this path is used. app.Any("/iris-ws.js", func(ctx iris.Context) { - ctx.Write(websocket.ClientSource) + ctx.Write(ws.ClientSource) }) } diff --git a/_examples/websocket/custom-go-client/main.go b/_examples/websocket/custom-go-client/main.go index da199b5ae1..32ff53db41 100644 --- a/_examples/websocket/custom-go-client/main.go +++ b/_examples/websocket/custom-go-client/main.go @@ -89,7 +89,7 @@ func SendMessage(serverID, to, method, message string) error { func SendtBytes(serverID, to, method string, message []byte) error { // look https://github.com/kataras/iris/blob/master/websocket/message.go , client.go and client.js // to understand the buffer line: - buffer := []byte(fmt.Sprintf("iris-websocket-message:%v;0;%v;%v;", method, serverID, to)) + buffer := []byte(fmt.Sprintf("%s%v;0;%v;%v;", websocket.DefaultEvtMessageKey, method, serverID, to)) buffer = append(buffer, message...) _, err := WS.Write(buffer) if err != nil { diff --git a/cache/entry/entry.go b/cache/entry/entry.go index 141d1ede3f..7beab739ee 100644 --- a/cache/entry/entry.go +++ b/cache/entry/entry.go @@ -115,7 +115,8 @@ func (e *Entry) Reset(statusCode int, headers map[string][]string, e.response.headers = newHeaders } - e.response.body = body + e.response.body = make([]byte,len(body)) + copy(e.response.body, body) // check if a given life changer provided // and if it does then execute the change life time if lifeChanger != nil { diff --git a/configuration.go b/configuration.go index 7bbc013939..e31a9c76a4 100644 --- a/configuration.go +++ b/configuration.go @@ -222,14 +222,6 @@ var WithoutInterruptHandler = func(app *Application) { app.config.DisableInterruptHandler = true } -// WithoutVersionChecker will disable the version checker and updater. -// The Iris server will be not -// receive automatic updates if you pass this -// to the `Run` function. Use it only while you're ready for Production environment. -var WithoutVersionChecker = func(app *Application) { - app.config.DisableVersionChecker = true -} - // WithoutPathCorrection disables the PathCorrection setting. // // See `Configuration`. @@ -237,6 +229,14 @@ var WithoutPathCorrection = func(app *Application) { app.config.DisablePathCorrection = true } +// WithoutPathCorrectionRedirection disables the PathCorrectionRedirection setting. +// +// See `Configuration`. +var WithoutPathCorrectionRedirection = func(app *Application) { + app.config.DisablePathCorrection = false + app.config.DisablePathCorrectionRedirection = true +} + // WithoutBodyConsumptionOnUnmarshal disables BodyConsumptionOnUnmarshal setting. // // See `Configuration`. @@ -388,19 +388,24 @@ type Configuration struct { // Defaults to false. DisableInterruptHandler bool `json:"disableInterruptHandler,omitempty" yaml:"DisableInterruptHandler" toml:"DisableInterruptHandler"` - // DisableVersionChecker if true then process will be not be notified for any available updates. - // - // Defaults to false. - DisableVersionChecker bool `json:"disableVersionChecker,omitempty" yaml:"DisableVersionChecker" toml:"DisableVersionChecker"` - - // DisablePathCorrection corrects and redirects the requested path to the registered path + // DisablePathCorrection corrects and redirects or executes directly the handler of + // the requested path to the registered path // for example, if /home/ path is requested but no handler for this Route found, // then the Router checks if /home handler exists, if yes, - // (permant)redirects the client to the correct path /home + // (permant)redirects the client to the correct path /home. + // + // See `DisablePathCorrectionRedirection` to enable direct handler execution instead of redirection. // // Defaults to false. DisablePathCorrection bool `json:"disablePathCorrection,omitempty" yaml:"DisablePathCorrection" toml:"DisablePathCorrection"` + // DisablePathCorrectionRedirection works whenever configuration.DisablePathCorrection is set to false + // and if DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without + // the last slash ("/") instead of send a redirection status. + // + // Defaults to false. + DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"` + // EnablePathEscape when is true then its escapes the path, the named parameters (if any). // Change to false it if you want something like this https://github.com/kataras/iris/issues/135 to work // @@ -540,6 +545,13 @@ func (c Configuration) GetDisablePathCorrection() bool { return c.DisablePathCorrection } +// GetDisablePathCorrectionRedirection returns the Configuration#DisablePathCorrectionRedirection field. +// If DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without +// the last slash ("/") instead of send a redirection status. +func (c Configuration) GetDisablePathCorrectionRedirection() bool { + return c.DisablePathCorrectionRedirection +} + // GetEnablePathEscape is the Configuration#EnablePathEscape, // returns true when its escapes the path, the named parameters (if any). func (c Configuration) GetEnablePathEscape() bool { @@ -677,14 +689,14 @@ func WithConfiguration(c Configuration) Configurator { main.DisableInterruptHandler = v } - if v := c.DisableVersionChecker; v { - main.DisableVersionChecker = v - } - if v := c.DisablePathCorrection; v { main.DisablePathCorrection = v } + if v := c.DisablePathCorrectionRedirection; v { + main.DisablePathCorrectionRedirection = v + } + if v := c.EnablePathEscape; v { main.EnablePathEscape = v } @@ -758,7 +770,6 @@ func DefaultConfiguration() Configuration { return Configuration{ DisableStartupLog: false, DisableInterruptHandler: false, - DisableVersionChecker: false, DisablePathCorrection: false, EnablePathEscape: false, FireMethodNotAllowed: false, diff --git a/configuration_test.go b/configuration_test.go index ab770fbe08..32f5bcf8dd 100644 --- a/configuration_test.go +++ b/configuration_test.go @@ -77,13 +77,12 @@ func TestConfigurationOptions(t *testing.T) { func TestConfigurationOptionsDeep(t *testing.T) { charset := "MYCHARSET" - app := New().Configure(WithCharset(charset), WithoutBodyConsumptionOnUnmarshal, WithoutBanner, WithoutVersionChecker) + app := New().Configure(WithCharset(charset), WithoutBodyConsumptionOnUnmarshal, WithoutBanner) expected := DefaultConfiguration() expected.Charset = charset expected.DisableBodyConsumptionOnUnmarshal = true expected.DisableStartupLog = true - expected.DisableVersionChecker = true has := *app.config @@ -142,8 +141,8 @@ func TestConfigurationYAML(t *testing.T) { }() yamlConfigurationContents := ` -DisableVersionChecker: true DisablePathCorrection: false +DisablePathCorrectionRedirection: true EnablePathEscape: false FireMethodNotAllowed: true EnableOptimizations: true @@ -165,14 +164,14 @@ Other: c := app.config - if expected := true; c.DisableVersionChecker != expected { - t.Fatalf("error on TestConfigurationYAML: Expected DisableVersionChecker %v but got %v", expected, c.DisableVersionChecker) - } - if expected := false; c.DisablePathCorrection != expected { t.Fatalf("error on TestConfigurationYAML: Expected DisablePathCorrection %v but got %v", expected, c.DisablePathCorrection) } + if expected := true; c.DisablePathCorrectionRedirection != expected { + t.Fatalf("error on TestConfigurationYAML: Expected DisablePathCorrectionRedirection %v but got %v", expected, c.DisablePathCorrectionRedirection) + } + if expected := false; c.EnablePathEscape != expected { t.Fatalf("error on TestConfigurationYAML: Expected EnablePathEscape %v but got %v", expected, c.EnablePathEscape) } @@ -241,7 +240,7 @@ func TestConfigurationTOML(t *testing.T) { }() tomlConfigurationContents := ` -DisableVersionChecker = true +DisablePathCorrectionRedirection = true EnablePathEscape = false FireMethodNotAllowed = true EnableOptimizations = true @@ -265,14 +264,14 @@ Charset = "UTF-8" c := app.config - if expected := true; c.DisableVersionChecker != expected { - t.Fatalf("error on TestConfigurationTOML: Expected DisableVersionChecker %v but got %v", expected, c.DisableVersionChecker) - } - if expected := false; c.DisablePathCorrection != expected { t.Fatalf("error on TestConfigurationTOML: Expected DisablePathCorrection %v but got %v", expected, c.DisablePathCorrection) } + if expected := true; c.DisablePathCorrectionRedirection != expected { + t.Fatalf("error on TestConfigurationTOML: Expected DisablePathCorrectionRedirection %v but got %v", expected, c.DisablePathCorrectionRedirection) + } + if expected := false; c.EnablePathEscape != expected { t.Fatalf("error on TestConfigurationTOML: Expected EnablePathEscape %v but got %v", expected, c.EnablePathEscape) } diff --git a/context/configuration.go b/context/configuration.go index 20ff827fb8..95039516f1 100644 --- a/context/configuration.go +++ b/context/configuration.go @@ -20,6 +20,11 @@ type ConfigurationReadOnly interface { // (permant)redirects the client to the correct path /home. GetDisablePathCorrection() bool + // GetDisablePathCorrectionRedirection returns the Configuration#DisablePathCorrectionRedirection field. + // If DisablePathCorrectionRedirection set to true then it will fire the handler of the matching route without + // the last slash ("/") instead of send a redirection status. + GetDisablePathCorrectionRedirection() bool + // GetEnablePathEscape is the configuration.EnablePathEscape, // returns true when its escapes the path, the named parameters (if any). GetEnablePathEscape() bool diff --git a/context/context.go b/context/context.go index b70624284b..f437e44750 100644 --- a/context/context.go +++ b/context/context.go @@ -26,10 +26,10 @@ import ( "github.com/Shopify/goreferrer" "github.com/fatih/structs" + "github.com/iris-contrib/blackfriday" formbinder "github.com/iris-contrib/formBinder" "github.com/json-iterator/go" "github.com/microcosm-cc/bluemonday" - "gopkg.in/russross/blackfriday.v2" "gopkg.in/yaml.v2" ) @@ -76,131 +76,6 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { return u(data, v) } -// RequestParams is a key string - value string storage which -// context's request dynamic path params are being kept. -// Empty if the route is static. -type RequestParams struct { - store memstore.Store -} - -// Set adds a key-value pair to the path parameters values -// it's being called internally so it shouldn't be used as a local storage by the user, use `ctx.Values()` instead. -func (r *RequestParams) Set(key, value string) { - r.store.Set(key, value) -} - -// Visit accepts a visitor which will be filled -// by the key-value params. -func (r *RequestParams) Visit(visitor func(key string, value string)) { - r.store.Visit(func(k string, v interface{}) { - visitor(k, v.(string)) // always string here. - }) -} - -var emptyEntry memstore.Entry - -// GetEntryAt returns the internal Entry of the memstore based on its index, -// the stored index by the router. -// If not found then it returns a zero Entry and false. -func (r RequestParams) GetEntryAt(index int) (memstore.Entry, bool) { - if len(r.store) > index { - return r.store[index], true - } - return emptyEntry, false -} - -// GetEntry returns the internal Entry of the memstore based on its "key". -// If not found then it returns a zero Entry and false. -func (r RequestParams) GetEntry(key string) (memstore.Entry, bool) { - // we don't return the pointer here, we don't want to give the end-developer - // the strength to change the entry that way. - if e := r.store.GetEntry(key); e != nil { - return *e, true - } - return emptyEntry, false -} - -// Get returns a path parameter's value based on its route's dynamic path key. -func (r RequestParams) Get(key string) string { - return r.store.GetString(key) -} - -// GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key. -func (r RequestParams) GetTrim(key string) string { - return strings.TrimSpace(r.Get(key)) -} - -// GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. -func (r RequestParams) GetEscape(key string) string { - return DecodeQuery(DecodeQuery(r.Get(key))) -} - -// GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. -// same as `GetEscape`. -func (r RequestParams) GetDecoded(key string) string { - return r.GetEscape(key) -} - -// GetInt returns the path parameter's value as int, based on its key. -// It checks for all available types of int, including int64, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetInt(key string) (int, error) { - return r.store.GetInt(key) -} - -// GetInt64 returns the path paramete's value as int64, based on its key. -// It checks for all available types of int, including int, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetInt64(key string) (int64, error) { - return r.store.GetInt64(key) -} - -// GetFloat64 returns a path parameter's value based as float64 on its route's dynamic path key. -// It checks for all available types of int, including float64, int, strings etc. -// It will return -1 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetFloat64(key string) (float64, error) { - return r.store.GetFloat64(key) -} - -// GetUint64 returns the path paramete's value as uint64, based on its key. -// It checks for all available types of int, including int, uint64, int64, strings etc. -// It will return 0 and a non-nil error if parameter wasn't found. -func (r RequestParams) GetUint64(key string) (uint64, error) { - return r.store.GetUint64(key) -} - -// GetBool returns the path parameter's value as bool, based on its key. -// a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" -// or "0" or "f" or "F" or "FALSE" or "false" or "False". -// Any other value returns an error. -func (r RequestParams) GetBool(key string) (bool, error) { - return r.store.GetBool(key) -} - -// GetIntUnslashed same as Get but it removes the first slash if found. -// Usage: Get an id from a wildcard path. -// -// Returns -1 with an error if the parameter couldn't be found. -func (r RequestParams) GetIntUnslashed(key string) (int, error) { - v := r.Get(key) - if v != "" { - if len(v) > 1 { - if v[0] == '/' { - v = v[1:] - } - } - return strconv.Atoi(v) - - } - - return -1, fmt.Errorf("unable to find int for '%s'", key) -} - -// Len returns the full length of the parameters. -func (r RequestParams) Len() int { - return r.store.Len() -} - // Context is the midle-man server's "object" for the clients. // // A New context is being acquired from a sync.Pool on each connection. @@ -279,7 +154,7 @@ type Context interface { // HandlerIndex sets the current index of the // current context's handlers chain. // If -1 passed then it just returns the - // current handler index without change the current index.rns that index, useless return value. + // current handler index without change the current index. // // Look Handlers(), Next() and StopExecution() too. HandlerIndex(n int) (currentIndex int) @@ -685,7 +560,8 @@ type Context interface { // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-xml/main.go ReadXML(xmlObjectPtr interface{}) error // ReadForm binds the formObject with the form data - // it supports any kind of struct. + // it supports any kind of type, including custom structs. + // It will return nothing if request data are empty. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go ReadForm(formObjectPtr interface{}) error @@ -1116,7 +992,7 @@ func NewContext(app Application) Context { func (ctx *context) BeginRequest(w http.ResponseWriter, r *http.Request) { ctx.handlers = nil // will be filled by router.Serve/HTTP ctx.values = ctx.values[0:0] // >> >> by context.Values().Set - ctx.params.store = ctx.params.store[0:0] + ctx.params.Store = ctx.params.Store[0:0] ctx.request = r ctx.currentHandlerIndex = 0 ctx.writer = AcquireResponseWriter() @@ -2410,24 +2286,28 @@ func (ctx *context) ReadXML(xmlObject interface{}) error { return ctx.UnmarshalBody(xmlObject, UnmarshalerFunc(xml.Unmarshal)) } -var ( - errReadBody = errors.New("while trying to read %s from the request body. Trace %s") -) +// IsErrPath can be used at `context#ReadForm`. +// It reports whether the incoming error is type of `formbinder.ErrPath`, +// which can be ignored when server allows unknown post values to be sent by the client. +// +// A shortcut for the `formbinder#IsErrPath`. +var IsErrPath = formbinder.IsErrPath // ReadForm binds the formObject with the form data -// it supports any kind of struct. +// it supports any kind of type, including custom structs. +// It will return nothing if request data are empty. // // Example: https://github.com/kataras/iris/blob/master/_examples/http_request/read-form/main.go func (ctx *context) ReadForm(formObject interface{}) error { values := ctx.FormValues() - if values == nil { - return errors.New("An empty form passed on ReadForm") + if len(values) == 0 { + return nil } // or dec := formbinder.NewDecoder(&formbinder.DecoderOptions{TagName: "form"}) // somewhere at the app level. I did change the tagName to "form" // inside its source code, so it's not needed for now. - return errReadBody.With(formbinder.Decode(values, formObject)) + return formbinder.Decode(values, formObject) } // +------------------------------------------------------------+ @@ -2987,12 +2867,10 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) { options = opts[0] } - optimize := ctx.shouldOptimize() - ctx.ContentType(ContentJSONHeaderValue) if options.StreamingJSON { - if optimize { + if ctx.shouldOptimize() { var jsoniterConfig = jsoniter.Config{ EscapeHTML: !options.UnescapeHTML, IndentionStep: 4, @@ -3013,7 +2891,7 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) { return ctx.writer.Written(), err } - n, err = WriteJSON(ctx.writer, v, options, optimize) + n, err = WriteJSON(ctx.writer, v, options, ctx.shouldOptimize()) if err != nil { ctx.StatusCode(http.StatusInternalServerError) return 0, err @@ -3553,7 +3431,7 @@ func (ctx *context) TransactionsSkipped() bool { return false } -// Exec calls the framewrok's ServeCtx +// Exec calls the framewrok's ServeHTTPC // based on this context but with a changed method and path // like it was requested by the user, but it is not. // @@ -3586,41 +3464,36 @@ func (ctx *context) Exec(method string, path string) { } // backup the handlers - backupHandlers := ctx.Handlers()[0:] - backupPos := ctx.HandlerIndex(-1) + backupHandlers := ctx.handlers[0:] + backupPos := ctx.currentHandlerIndex + req := ctx.request // backup the request path information - backupPath := ctx.Path() - backupMethod := ctx.Method() + backupPath := req.URL.Path + backupMethod := req.Method // don't backupValues := ctx.Values().ReadOnly() - - // [values stays] - // reset handlers - ctx.SetHandlers(nil) - - req := ctx.Request() // set the request to be align with the 'againstRequestPath' req.RequestURI = path req.URL.Path = path req.Method = method + // [values stays] + // reset handlers + ctx.handlers = ctx.handlers[0:0] + ctx.currentHandlerIndex = 0 + // execute the route from the (internal) context router // this way we keep the sessions and the values ctx.Application().ServeHTTPC(ctx) - // set back the old handlers and the last known index - ctx.SetHandlers(backupHandlers) - ctx.HandlerIndex(backupPos) // set the request back to its previous state req.RequestURI = backupPath req.URL.Path = backupPath req.Method = backupMethod - // don't fill the values in order to be able to communicate from and to. - // // fill the values as they were before - // backupValues.Visit(func(key string, value interface{}) { - // ctx.Values().Set(key, value) - // }) + // set back the old handlers and the last known index + ctx.handlers = backupHandlers + ctx.currentHandlerIndex = backupPos } // RouteExists reports whether a particular route exists diff --git a/context/request_params.go b/context/request_params.go new file mode 100644 index 0000000000..7757308b97 --- /dev/null +++ b/context/request_params.go @@ -0,0 +1,206 @@ +package context + +import ( + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/kataras/iris/core/memstore" +) + +// RequestParams is a key string - value string storage which +// context's request dynamic path params are being kept. +// Empty if the route is static. +type RequestParams struct { + memstore.Store +} + +// Set inserts a value to the key-value storage. +// +// See `SetImmutable` and `Get` too. +func (r *RequestParams) Set(key, value string) { + r.Store.Set(key, value) +} + +// GetEntryAt will return the parameter's internal store's `Entry` based on the index. +// If not found it will return an emptry `Entry`. +func (r *RequestParams) GetEntryAt(index int) memstore.Entry { + entry, _ := r.Store.GetEntryAt(index) + return entry +} + +// GetEntry will return the parameter's internal store's `Entry` based on its name/key. +// If not found it will return an emptry `Entry`. +func (r *RequestParams) GetEntry(key string) memstore.Entry { + entry, _ := r.Store.GetEntry(key) + return entry +} + +// Visit accepts a visitor which will be filled +// by the key-value params. +func (r *RequestParams) Visit(visitor func(key string, value string)) { + r.Store.Visit(func(k string, v interface{}) { + visitor(k, fmt.Sprintf("%v", v)) // always string here. + }) +} + +// Get returns a path parameter's value based on its route's dynamic path key. +func (r RequestParams) Get(key string) string { + return r.GetString(key) +} + +// GetTrim returns a path parameter's value without trailing spaces based on its route's dynamic path key. +func (r RequestParams) GetTrim(key string) string { + return strings.TrimSpace(r.Get(key)) +} + +// GetEscape returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. +func (r RequestParams) GetEscape(key string) string { + return DecodeQuery(DecodeQuery(r.Get(key))) +} + +// GetDecoded returns a path parameter's double-url-query-escaped value based on its route's dynamic path key. +// same as `GetEscape`. +func (r RequestParams) GetDecoded(key string) string { + return r.GetEscape(key) +} + +// GetIntUnslashed same as Get but it removes the first slash if found. +// Usage: Get an id from a wildcard path. +// +// Returns -1 and false if not path parameter with that "key" found. +func (r RequestParams) GetIntUnslashed(key string) (int, bool) { + v := r.Get(key) + if v != "" { + if len(v) > 1 { + if v[0] == '/' { + v = v[1:] + } + } + + vInt, err := strconv.Atoi(v) + if err != nil { + return -1, false + } + return vInt, true + } + + return -1, false +} + +var ( + // ParamResolvers is the global param resolution for a parameter type for a specific go std or custom type. + // + // Key is the specific type, which should be unique. + // The value is a function which accepts the parameter index + // and it should return the value as the parameter type evaluator expects it. + // i.e [reflect.TypeOf("string")] = func(paramIndex int) interface{} { + // return func(ctx Context) { + // return ctx.Params().GetEntryAt(paramIndex).ValueRaw.() + // } + // } + // + // Read https://github.com/kataras/iris/tree/master/_examples/routing/macros for more details. + ParamResolvers = map[reflect.Type]func(paramIndex int) interface{}{ + reflect.TypeOf(""): func(paramIndex int) interface{} { + return func(ctx Context) string { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(string) + } + }, + reflect.TypeOf(int(1)): func(paramIndex int) interface{} { + return func(ctx Context) int { + // v, _ := ctx.Params().GetEntryAt(paramIndex).IntDefault(0) + // return v + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int) + } + }, + reflect.TypeOf(int8(1)): func(paramIndex int) interface{} { + return func(ctx Context) int8 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int8) + } + }, + reflect.TypeOf(int16(1)): func(paramIndex int) interface{} { + return func(ctx Context) int16 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int16) + } + }, + reflect.TypeOf(int32(1)): func(paramIndex int) interface{} { + return func(ctx Context) int32 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int32) + } + }, + reflect.TypeOf(int64(1)): func(paramIndex int) interface{} { + return func(ctx Context) int64 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(int64) + } + }, + reflect.TypeOf(uint(1)): func(paramIndex int) interface{} { + return func(ctx Context) uint { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint) + } + }, + reflect.TypeOf(uint8(1)): func(paramIndex int) interface{} { + return func(ctx Context) uint8 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint8) + } + }, + reflect.TypeOf(uint16(1)): func(paramIndex int) interface{} { + return func(ctx Context) uint16 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint16) + } + }, + reflect.TypeOf(uint32(1)): func(paramIndex int) interface{} { + return func(ctx Context) uint32 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint32) + } + }, + reflect.TypeOf(uint64(1)): func(paramIndex int) interface{} { + return func(ctx Context) uint64 { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(uint64) + } + }, + reflect.TypeOf(true): func(paramIndex int) interface{} { + return func(ctx Context) bool { + return ctx.Params().GetEntryAt(paramIndex).ValueRaw.(bool) + } + }, + } +) + +// ParamResolverByTypeAndIndex will return a function that can be used to bind path parameter's exact value by its Go std type +// and the parameter's index based on the registered path. +// Usage: nameResolver := ParamResolverByKindAndKey(reflect.TypeOf(""), 0) +// Inside a Handler: nameResolver.Call(ctx)[0] +// it will return the reflect.Value Of the exact type of the parameter(based on the path parameters and macros). +// It is only useful for dynamic binding of the parameter, it is used on "hero" package and it should be modified +// only when Macros are modified in such way that the default selections for the available go std types are not enough. +// +// Returns empty value and false if "k" does not match any valid parameter resolver. +func ParamResolverByTypeAndIndex(typ reflect.Type, paramIndex int) (reflect.Value, bool) { + /* NO: + // This could work but its result is not exact type, so direct binding is not possible. + resolver := m.ParamResolver + fn := func(ctx context.Context) interface{} { + entry, _ := ctx.Params().GetEntry(paramName) + return resolver(entry) + } + // + + // This works but it is slower on serve-time. + paramNameValue := []reflect.Value{reflect.ValueOf(paramName)} + var fnSignature func(context.Context) string + return reflect.MakeFunc(reflect.ValueOf(&fnSignature).Elem().Type(), func(in []reflect.Value) []reflect.Value { + return in[0].MethodByName("Params").Call(emptyIn)[0].MethodByName("Get").Call(paramNameValue) + // return []reflect.Value{reflect.ValueOf(in[0].Interface().(context.Context).Params().Get(paramName))} + }) + // + */ + + r, ok := ParamResolvers[typ] + if !ok || r == nil { + return reflect.Value{}, false + } + + return reflect.ValueOf(r(paramIndex)), true +} diff --git a/context/route.go b/context/route.go index e632f11f0f..7c680e7d95 100644 --- a/context/route.go +++ b/context/route.go @@ -1,6 +1,6 @@ package context -import "github.com/kataras/iris/core/router/macro" +import "github.com/kataras/iris/macro" // RouteReadOnly allows decoupled access to the current route // inside the context. @@ -25,7 +25,7 @@ type RouteReadOnly interface { // StaticPath returns the static part of the original, registered route path. // if /user/{id} it will return /user - // if /user/{id}/friend/{friendid:int} it will return /user too + // if /user/{id}/friend/{friendid:uint64} it will return /user too // if /assets/{filepath:path} it will return /assets. StaticPath() string diff --git a/core/host/proxy_test.go b/core/host/proxy_test.go index b7ad878e07..76561b342f 100644 --- a/core/host/proxy_test.go +++ b/core/host/proxy_test.go @@ -60,7 +60,7 @@ func TestProxy(t *testing.T) { t.Fatalf("%v while creating tcp4 listener for new tls local test listener", err) } // main server - go app.Run(iris.Listener(httptest.NewLocalTLSListener(l)), iris.WithoutVersionChecker, iris.WithoutStartupLog) + go app.Run(iris.Listener(httptest.NewLocalTLSListener(l)), iris.WithoutStartupLog) e := httptest.NewInsecure(t, httptest.URL("http://"+listener.Addr().String())) e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedIndex) diff --git a/core/maintenance/maintenance.go b/core/maintenance/maintenance.go deleted file mode 100644 index 613b67fda6..0000000000 --- a/core/maintenance/maintenance.go +++ /dev/null @@ -1,6 +0,0 @@ -package maintenance - -// Start starts the maintenance process. -func Start() { - CheckForUpdates() -} diff --git a/core/maintenance/version.go b/core/maintenance/version.go deleted file mode 100644 index f3d3113f68..0000000000 --- a/core/maintenance/version.go +++ /dev/null @@ -1,78 +0,0 @@ -package maintenance - -import ( - "fmt" - "os" - "os/exec" - - "github.com/kataras/iris/core/maintenance/version" - - "github.com/kataras/golog" - "github.com/kataras/survey" -) - -const ( - // Version is the string representation of the current local Iris Web Framework version. - Version = "10.7.0" -) - -// CheckForUpdates checks for any available updates -// and asks for the user if want to update now or not. -func CheckForUpdates() { - v := version.Acquire() - updateAvailale := v.Compare(Version) == version.Smaller - - if updateAvailale { - if confirmUpdate(v) { - installVersion() - return - } - } -} - -func confirmUpdate(v version.Version) bool { - // on help? when asking for installing the new update. - ignoreUpdatesMsg := "Would you like to ignore future updates? Disable the version checker via:\napp.Run(..., iris.WithoutVersionChecker)" - - // if update available ask for update action. - shouldUpdateNowMsg := - fmt.Sprintf("A new version is available online[%s > %s]. Type '?' for help.\nRelease notes: %s.\nUpdate now?", - v.String(), Version, v.ChangelogURL) - - var confirmUpdate bool - survey.AskOne(&survey.Confirm{ - Message: shouldUpdateNowMsg, - Help: ignoreUpdatesMsg, - }, &confirmUpdate, nil) - return confirmUpdate // it's true only when update was available and user typed "yes". -} - -func installVersion() { - golog.Infof("Downloading...\n") - repo := "github.com/kataras/iris/..." - cmd := exec.Command("go", "get", "-u", "-v", repo) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - - if err := cmd.Run(); err != nil { - golog.Warnf("unexpected message while trying to go get,\nif you edited the original source code then you've to remove the whole $GOPATH/src/github.com/kataras folder and execute `go get -u github.com/kataras/iris/...` manually\n%v", err) - return - } - - golog.Infof("Update process finished.\nManual rebuild and restart is required to apply the changes...\n") - return -} - -/* Author's note: -We could use github webhooks to automatic notify for updates -when a new update is pushed to the repository -even when server is already started and running but this would expose -a route which dev may don't know about, so let it for now but if -they ask it then I should add an optional configuration field -to "live/realtime update" and implement the idea (which is already implemented in the iris-go server). -*/ - -/* Author's note: -The old remote endpoint for version checker is still available on the server for backwards -compatibility with older clients, it will stay there for a long period of time. -*/ diff --git a/core/maintenance/version/fetch.go b/core/maintenance/version/fetch.go deleted file mode 100644 index 82d3cd75d0..0000000000 --- a/core/maintenance/version/fetch.go +++ /dev/null @@ -1,59 +0,0 @@ -package version - -import ( - "io/ioutil" - "strings" - "time" - - "github.com/hashicorp/go-version" - - "github.com/kataras/golog" - "github.com/kataras/iris/core/netutil" -) - -const ( - versionURL = "https://raw.githubusercontent.com/kataras/iris/master/VERSION" -) - -func fetch() (*version.Version, string) { - client := netutil.Client(time.Duration(30 * time.Second)) - - r, err := client.Get(versionURL) - if err != nil { - golog.Debugf("err: %v\n", err) - return nil, "" - } - defer r.Body.Close() - - if r.StatusCode >= 400 { - golog.Debugf("Internet connection is missing, updater is unable to fetch the latest Iris version\n%v", err) - return nil, "" - } - - b, err := ioutil.ReadAll(r.Body) - - if len(b) == 0 || err != nil { - golog.Debugf("err: %v\n", err) - return nil, "" - } - - var ( - fetchedVersion = string(b) - changelogURL string - ) - // Example output: - // Version(8.5.5) - // 8.5.5:https://github.com/kataras/iris/blob/master/HISTORY.md#tu-02-november-2017--v855 - if idx := strings.IndexByte(fetchedVersion, ':'); idx > 0 { - changelogURL = fetchedVersion[idx+1:] - fetchedVersion = fetchedVersion[0:idx] - } - - latestVersion, err := version.NewVersion(fetchedVersion) - if err != nil { - golog.Debugf("while fetching and parsing latest version from github: %v\n", err) - return nil, "" - } - - return latestVersion, changelogURL -} diff --git a/core/maintenance/version/version.go b/core/maintenance/version/version.go deleted file mode 100644 index 4a33895212..0000000000 --- a/core/maintenance/version/version.go +++ /dev/null @@ -1,64 +0,0 @@ -package version - -import ( - "time" - - "github.com/hashicorp/go-version" -) - -// Version is a version wrapper which -// contains some additional customized properties. -type Version struct { - version.Version - WrittenAt time.Time - ChangelogURL string -} - -// Result is the compare result type. -// Available types are Invalid, Smaller, Equal or Larger. -type Result int32 - -const ( - // Smaller when the compared version is smaller than the latest one. - Smaller Result = -1 - // Equal when the compared version is equal with the latest one. - Equal Result = 0 - // Larger when the compared version is larger than the latest one. - Larger Result = 1 - // Invalid means that an error occurred when comparing the versions. - Invalid Result = -2 -) - -// Compare compares the "versionStr" with the latest Iris version, -// opossite to the version package -// it returns the result of the "versionStr" not the "v" itself. -func (v *Version) Compare(versionStr string) Result { - if len(v.Version.String()) == 0 { - // if version not refreshed, by an internet connection lose, - // then return Invalid. - return Invalid - } - - other, err := version.NewVersion(versionStr) - if err != nil { - return Invalid - } - return Result(other.Compare(&v.Version)) -} - -// Acquire returns the latest version info wrapper. -// It calls the fetch. -func Acquire() (v Version) { - newVersion, changelogURL := fetch() - if newVersion == nil { // if github was down then don't panic, just set it as the smallest version. - newVersion, _ = version.NewVersion("0.0.1") - } - - v = Version{ - Version: *newVersion, - WrittenAt: time.Now(), - ChangelogURL: changelogURL, - } - - return -} diff --git a/core/memstore/memstore.go b/core/memstore/memstore.go index 5b6bbdf2e3..af933c0752 100644 --- a/core/memstore/memstore.go +++ b/core/memstore/memstore.go @@ -6,6 +6,8 @@ package memstore import ( + "fmt" + "math" "reflect" "strconv" "strings" @@ -14,6 +16,11 @@ import ( ) type ( + // ValueSetter is the interface which can be accepted as a generic solution of RequestParams or memstore when Set is the only requirement, + // i.e internally on macro/template/TemplateParam#Eval:paramChanger. + ValueSetter interface { + Set(key string, newValue interface{}) (Entry, bool) + } // Entry is the entry of the context storage Store - .Values() Entry struct { Key string @@ -25,6 +32,8 @@ type ( Store []Entry ) +var _ ValueSetter = (*Store)(nil) + // GetByKindOrNil will try to get this entry's value of "k" kind, // if value is not that kind it will NOT try to convert it the "k", instead // it will return nil, except if boolean; then it will return false @@ -67,11 +76,19 @@ func (e Entry) GetByKindOrNil(k reflect.Kind) interface{} { // If not found returns "def". func (e Entry) StringDefault(def string) string { v := e.ValueRaw + if v == nil { + return def + } if vString, ok := v.(string); ok { return vString } + val := fmt.Sprintf("%v", v) + if val != "" { + return val + } + return def } @@ -94,107 +111,341 @@ func (e Entry) IntDefault(def int) (int, error) { if v == nil { return def, errFindParse.Format("int", e.Key) } - if vint, ok := v.(int); ok { - return vint, nil - } else if vstring, sok := v.(string); sok && vstring != "" { - vint, err := strconv.Atoi(vstring) + + switch vv := v.(type) { + case string: + val, err := strconv.Atoi(vv) if err != nil { return def, err } - - return vint, nil + return val, nil + case int: + return vv, nil + case int8: + return int(vv), nil + case int16: + return int(vv), nil + case int32: + return int(vv), nil + case int64: + return int(vv), nil + case uint: + return int(vv), nil + case uint8: + return int(vv), nil + case uint16: + return int(vv), nil + case uint32: + return int(vv), nil + case uint64: + return int(vv), nil } return def, errFindParse.Format("int", e.Key) } -// Int64Default returns the entry's value as int64. +// Int8Default returns the entry's value as int8. // If not found returns "def" and a non-nil error. -func (e Entry) Int64Default(def int64) (int64, error) { +func (e Entry) Int8Default(def int8) (int8, error) { v := e.ValueRaw if v == nil { - return def, errFindParse.Format("int64", e.Key) + return def, errFindParse.Format("int8", e.Key) } - if vint64, ok := v.(int64); ok { - return vint64, nil + switch vv := v.(type) { + case string: + val, err := strconv.ParseInt(vv, 10, 8) + if err != nil { + return def, err + } + return int8(val), nil + case int: + return int8(vv), nil + case int8: + return vv, nil + case int16: + return int8(vv), nil + case int32: + return int8(vv), nil + case int64: + return int8(vv), nil } - if vint, ok := v.(int); ok { - return int64(vint), nil + return def, errFindParse.Format("int8", e.Key) +} + +// Int16Default returns the entry's value as int16. +// If not found returns "def" and a non-nil error. +func (e Entry) Int16Default(def int16) (int16, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("int16", e.Key) } - if vstring, sok := v.(string); sok { - return strconv.ParseInt(vstring, 10, 64) + switch vv := v.(type) { + case string: + val, err := strconv.ParseInt(vv, 10, 16) + if err != nil { + return def, err + } + return int16(val), nil + case int: + return int16(vv), nil + case int8: + return int16(vv), nil + case int16: + return vv, nil + case int32: + return int16(vv), nil + case int64: + return int16(vv), nil } - return def, errFindParse.Format("int64", e.Key) + return def, errFindParse.Format("int16", e.Key) } -// Float64Default returns the entry's value as float64. +// Int32Default returns the entry's value as int32. // If not found returns "def" and a non-nil error. -func (e Entry) Float64Default(def float64) (float64, error) { +func (e Entry) Int32Default(def int32) (int32, error) { v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("int32", e.Key) + } + switch vv := v.(type) { + case string: + val, err := strconv.ParseInt(vv, 10, 32) + if err != nil { + return def, err + } + return int32(val), nil + case int: + return int32(vv), nil + case int8: + return int32(vv), nil + case int16: + return int32(vv), nil + case int32: + return vv, nil + case int64: + return int32(vv), nil + } + + return def, errFindParse.Format("int32", e.Key) +} + +// Int64Default returns the entry's value as int64. +// If not found returns "def" and a non-nil error. +func (e Entry) Int64Default(def int64) (int64, error) { + v := e.ValueRaw if v == nil { - return def, errFindParse.Format("float64", e.Key) + return def, errFindParse.Format("int64", e.Key) } - if vfloat32, ok := v.(float32); ok { - return float64(vfloat32), nil + switch vv := v.(type) { + case string: + return strconv.ParseInt(vv, 10, 64) + case int64: + return vv, nil + case int32: + return int64(vv), nil + case int8: + return int64(vv), nil + case int: + return int64(vv), nil } - if vfloat64, ok := v.(float64); ok { - return vfloat64, nil + return def, errFindParse.Format("int64", e.Key) +} + +// UintDefault returns the entry's value as uint. +// If not found returns "def" and a non-nil error. +func (e Entry) UintDefault(def uint) (uint, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("uint", e.Key) } - if vint, ok := v.(int); ok { - return float64(vint), nil + x64 := strconv.IntSize == 64 + var maxValue uint64 = math.MaxUint32 + if x64 { + maxValue = math.MaxUint64 } - if vstring, sok := v.(string); sok { - vfloat64, err := strconv.ParseFloat(vstring, 64) + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, strconv.IntSize) if err != nil { return def, err } - - return vfloat64, nil + if val > uint64(maxValue) { + return def, errFindParse.Format("uint", e.Key) + } + return uint(val), nil + case uint: + return vv, nil + case uint8: + return uint(vv), nil + case uint16: + return uint(vv), nil + case uint32: + return uint(vv), nil + case uint64: + if vv > uint64(maxValue) { + return def, errFindParse.Format("uint", e.Key) + } + return uint(vv), nil + case int: + if vv < 0 || vv > int(maxValue) { + return def, errFindParse.Format("uint", e.Key) + } + return uint(vv), nil } - return def, errFindParse.Format("float64", e.Key) + return def, errFindParse.Format("uint", e.Key) } -// Float32Default returns the entry's value as float32. +// Uint8Default returns the entry's value as uint8. // If not found returns "def" and a non-nil error. -func (e Entry) Float32Default(key string, def float32) (float32, error) { +func (e Entry) Uint8Default(def uint8) (uint8, error) { v := e.ValueRaw - if v == nil { - return def, errFindParse.Format("float32", e.Key) + return def, errFindParse.Format("uint8", e.Key) + } + + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, 8) + if err != nil { + return def, err + } + if val > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(val), nil + case uint: + if vv > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil + case uint8: + return vv, nil + case uint16: + if vv > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil + case uint32: + if vv > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil + case uint64: + if vv > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil + case int: + if vv < 0 || vv > math.MaxUint8 { + return def, errFindParse.Format("uint8", e.Key) + } + return uint8(vv), nil } - if vfloat32, ok := v.(float32); ok { - return vfloat32, nil + return def, errFindParse.Format("uint8", e.Key) +} + +// Uint16Default returns the entry's value as uint16. +// If not found returns "def" and a non-nil error. +func (e Entry) Uint16Default(def uint16) (uint16, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("uint16", e.Key) } - if vfloat64, ok := v.(float64); ok { - return float32(vfloat64), nil + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, 16) + if err != nil { + return def, err + } + if val > math.MaxUint16 { + return def, errFindParse.Format("uint16", e.Key) + } + return uint16(val), nil + case uint: + if vv > math.MaxUint16 { + return def, errFindParse.Format("uint16", e.Key) + } + return uint16(vv), nil + case uint8: + return uint16(vv), nil + case uint16: + return vv, nil + case uint32: + if vv > math.MaxUint16 { + return def, errFindParse.Format("uint16", e.Key) + } + return uint16(vv), nil + case uint64: + if vv > math.MaxUint16 { + return def, errFindParse.Format("uint16", e.Key) + } + return uint16(vv), nil + case int: + if vv < 0 || vv > math.MaxUint16 { + return def, errFindParse.Format("uint16", e.Key) + } + return uint16(vv), nil } - if vint, ok := v.(int); ok { - return float32(vint), nil + return def, errFindParse.Format("uint16", e.Key) +} + +// Uint32Default returns the entry's value as uint32. +// If not found returns "def" and a non-nil error. +func (e Entry) Uint32Default(def uint32) (uint32, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("uint32", e.Key) } - if vstring, sok := v.(string); sok { - vfloat32, err := strconv.ParseFloat(vstring, 32) + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, 32) if err != nil { return def, err } - - return float32(vfloat32), nil + if val > math.MaxUint32 { + return def, errFindParse.Format("uint32", e.Key) + } + return uint32(val), nil + case uint: + if vv > math.MaxUint32 { + return def, errFindParse.Format("uint32", e.Key) + } + return uint32(vv), nil + case uint8: + return uint32(vv), nil + case uint16: + return uint32(vv), nil + case uint32: + return vv, nil + case uint64: + if vv > math.MaxUint32 { + return def, errFindParse.Format("uint32", e.Key) + } + return uint32(vv), nil + case int32: + return uint32(vv), nil + case int64: + if vv < 0 || vv > math.MaxUint32 { + return def, errFindParse.Format("uint32", e.Key) + } + return uint32(vv), nil } - return def, errFindParse.Format("float32", e.Key) + return def, errFindParse.Format("uint32", e.Key) } // Uint64Default returns the entry's value as uint64. @@ -205,23 +456,95 @@ func (e Entry) Uint64Default(def uint64) (uint64, error) { return def, errFindParse.Format("uint64", e.Key) } - if vuint64, ok := v.(uint64); ok { - return vuint64, nil + switch vv := v.(type) { + case string: + val, err := strconv.ParseUint(vv, 10, 64) + if err != nil { + return def, err + } + if val > math.MaxUint64 { + return def, errFindParse.Format("uint64", e.Key) + } + return uint64(val), nil + case uint8: + return uint64(vv), nil + case uint16: + return uint64(vv), nil + case uint32: + return uint64(vv), nil + case uint64: + return vv, nil + case int64: + return uint64(vv), nil + case int: + return uint64(vv), nil } - if vint64, ok := v.(int64); ok { - return uint64(vint64), nil + return def, errFindParse.Format("uint64", e.Key) +} + +// Float32Default returns the entry's value as float32. +// If not found returns "def" and a non-nil error. +func (e Entry) Float32Default(key string, def float32) (float32, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("float32", e.Key) } - if vint, ok := v.(int); ok { - return uint64(vint), nil + switch vv := v.(type) { + case string: + val, err := strconv.ParseFloat(vv, 32) + if err != nil { + return def, err + } + if val > math.MaxFloat32 { + return def, errFindParse.Format("float32", e.Key) + } + return float32(val), nil + case float32: + return vv, nil + case float64: + if vv > math.MaxFloat32 { + return def, errFindParse.Format("float32", e.Key) + } + return float32(vv), nil + case int: + return float32(vv), nil + } + + return def, errFindParse.Format("float32", e.Key) +} + +// Float64Default returns the entry's value as float64. +// If not found returns "def" and a non-nil error. +func (e Entry) Float64Default(def float64) (float64, error) { + v := e.ValueRaw + if v == nil { + return def, errFindParse.Format("float64", e.Key) } - if vstring, sok := v.(string); sok { - return strconv.ParseUint(vstring, 10, 64) + switch vv := v.(type) { + case string: + val, err := strconv.ParseFloat(vv, 64) + if err != nil { + return def, err + } + return val, nil + case float32: + return float64(vv), nil + case float64: + return vv, nil + case int: + return float64(vv), nil + case int64: + return float64(vv), nil + case uint: + return float64(vv), nil + case uint64: + return float64(vv), nil } - return def, errFindParse.Format("uint64", e.Key) + return def, errFindParse.Format("float64", e.Key) } // BoolDefault returns the user's value as bool. @@ -236,20 +559,17 @@ func (e Entry) BoolDefault(def bool) (bool, error) { return def, errFindParse.Format("bool", e.Key) } - if vBoolean, ok := v.(bool); ok { - return vBoolean, nil - } - - if vString, ok := v.(string); ok { - b, err := strconv.ParseBool(vString) + switch vv := v.(type) { + case string: + val, err := strconv.ParseBool(vv) if err != nil { return def, err } - return b, nil - } - - if vInt, ok := v.(int); ok { - if vInt == 1 { + return val, nil + case bool: + return vv, nil + case int: + if vv == 1 { return true, nil } return false, nil @@ -361,28 +681,39 @@ func (r *Store) SetImmutable(key string, value interface{}) (Entry, bool) { return r.Save(key, value, true) } +var emptyEntry Entry + // GetEntry returns a pointer to the "Entry" found with the given "key" -// if nothing found then it returns nil, so be careful with that, -// it's not supposed to be used by end-developers. -func (r *Store) GetEntry(key string) *Entry { +// if nothing found then it returns an empty Entry and false. +func (r *Store) GetEntry(key string) (Entry, bool) { args := *r n := len(args) for i := 0; i < n; i++ { - kv := &args[i] - if kv.Key == key { - return kv + if kv := args[i]; kv.Key == key { + return kv, true } } - return nil + return emptyEntry, false +} + +// GetEntryAt returns the internal Entry of the memstore based on its index, +// the stored index by the router. +// If not found then it returns a zero Entry and false. +func (r *Store) GetEntryAt(index int) (Entry, bool) { + args := *r + if len(args) > index { + return args[index], true + } + return emptyEntry, false } // GetDefault returns the entry's value based on its key. // If not found returns "def". // This function checks for immutability as well, the rest don't. func (r *Store) GetDefault(key string, def interface{}) interface{} { - v := r.GetEntry(key) - if v == nil || v.ValueRaw == nil { + v, ok := r.GetEntry(key) + if !ok || v.ValueRaw == nil { return def } vv := v.Value() @@ -411,8 +742,8 @@ func (r *Store) Visit(visitor func(key string, value interface{})) { // GetStringDefault returns the entry's value as string, based on its key. // If not found returns "def". func (r *Store) GetStringDefault(key string, def string) string { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return def } @@ -432,8 +763,8 @@ func (r *Store) GetStringTrim(name string) string { // GetInt returns the entry's value as int, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt(key string) (int, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return 0, errFindParse.Format("int", key) } return v.IntDefault(-1) @@ -449,20 +780,60 @@ func (r *Store) GetIntDefault(key string, def int) int { return def } -// GetUint64 returns the entry's value as uint64, based on its key. -// If not found returns 0 and a non-nil error. -func (r *Store) GetUint64(key string) (uint64, error) { - v := r.GetEntry(key) - if v == nil { - return 0, errFindParse.Format("uint64", key) +// GetInt8 returns the entry's value as int8, based on its key. +// If not found returns -1 and a non-nil error. +func (r *Store) GetInt8(key string) (int8, error) { + v, ok := r.GetEntry(key) + if !ok { + return -1, errFindParse.Format("int8", key) } - return v.Uint64Default(0) + return v.Int8Default(-1) } -// GetUint64Default returns the entry's value as uint64, based on its key. +// GetInt8Default returns the entry's value as int8, based on its key. // If not found returns "def". -func (r *Store) GetUint64Default(key string, def uint64) uint64 { - if v, err := r.GetUint64(key); err == nil { +func (r *Store) GetInt8Default(key string, def int8) int8 { + if v, err := r.GetInt8(key); err == nil { + return v + } + + return def +} + +// GetInt16 returns the entry's value as int16, based on its key. +// If not found returns -1 and a non-nil error. +func (r *Store) GetInt16(key string) (int16, error) { + v, ok := r.GetEntry(key) + if !ok { + return -1, errFindParse.Format("int16", key) + } + return v.Int16Default(-1) +} + +// GetInt16Default returns the entry's value as int16, based on its key. +// If not found returns "def". +func (r *Store) GetInt16Default(key string, def int16) int16 { + if v, err := r.GetInt16(key); err == nil { + return v + } + + return def +} + +// GetInt32 returns the entry's value as int32, based on its key. +// If not found returns -1 and a non-nil error. +func (r *Store) GetInt32(key string) (int32, error) { + v, ok := r.GetEntry(key) + if !ok { + return -1, errFindParse.Format("int32", key) + } + return v.Int32Default(-1) +} + +// GetInt32Default returns the entry's value as int32, based on its key. +// If not found returns "def". +func (r *Store) GetInt32Default(key string, def int32) int32 { + if v, err := r.GetInt32(key); err == nil { return v } @@ -472,8 +843,8 @@ func (r *Store) GetUint64Default(key string, def uint64) uint64 { // GetInt64 returns the entry's value as int64, based on its key. // If not found returns -1 and a non-nil error. func (r *Store) GetInt64(key string) (int64, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return -1, errFindParse.Format("int64", key) } return v.Int64Default(-1) @@ -489,11 +860,111 @@ func (r *Store) GetInt64Default(key string, def int64) int64 { return def } +// GetUint returns the entry's value as uint, based on its key. +// If not found returns 0 and a non-nil error. +func (r *Store) GetUint(key string) (uint, error) { + v, ok := r.GetEntry(key) + if !ok { + return 0, errFindParse.Format("uint", key) + } + return v.UintDefault(0) +} + +// GetUintDefault returns the entry's value as uint, based on its key. +// If not found returns "def". +func (r *Store) GetUintDefault(key string, def uint) uint { + if v, err := r.GetUint(key); err == nil { + return v + } + + return def +} + +// GetUint8 returns the entry's value as uint8, based on its key. +// If not found returns 0 and a non-nil error. +func (r *Store) GetUint8(key string) (uint8, error) { + v, ok := r.GetEntry(key) + if !ok { + return 0, errFindParse.Format("uint8", key) + } + return v.Uint8Default(0) +} + +// GetUint8Default returns the entry's value as uint8, based on its key. +// If not found returns "def". +func (r *Store) GetUint8Default(key string, def uint8) uint8 { + if v, err := r.GetUint8(key); err == nil { + return v + } + + return def +} + +// GetUint16 returns the entry's value as uint16, based on its key. +// If not found returns 0 and a non-nil error. +func (r *Store) GetUint16(key string) (uint16, error) { + v, ok := r.GetEntry(key) + if !ok { + return 0, errFindParse.Format("uint16", key) + } + return v.Uint16Default(0) +} + +// GetUint16Default returns the entry's value as uint16, based on its key. +// If not found returns "def". +func (r *Store) GetUint16Default(key string, def uint16) uint16 { + if v, err := r.GetUint16(key); err == nil { + return v + } + + return def +} + +// GetUint32 returns the entry's value as uint32, based on its key. +// If not found returns 0 and a non-nil error. +func (r *Store) GetUint32(key string) (uint32, error) { + v, ok := r.GetEntry(key) + if !ok { + return 0, errFindParse.Format("uint32", key) + } + return v.Uint32Default(0) +} + +// GetUint32Default returns the entry's value as uint32, based on its key. +// If not found returns "def". +func (r *Store) GetUint32Default(key string, def uint32) uint32 { + if v, err := r.GetUint32(key); err == nil { + return v + } + + return def +} + +// GetUint64 returns the entry's value as uint64, based on its key. +// If not found returns 0 and a non-nil error. +func (r *Store) GetUint64(key string) (uint64, error) { + v, ok := r.GetEntry(key) + if !ok { + return 0, errFindParse.Format("uint64", key) + } + return v.Uint64Default(0) +} + +// GetUint64Default returns the entry's value as uint64, based on its key. +// If not found returns "def". +func (r *Store) GetUint64Default(key string, def uint64) uint64 { + if v, err := r.GetUint64(key); err == nil { + return v + } + + return def +} + // GetFloat64 returns the entry's value as float64, based on its key. // If not found returns -1 and a non nil error. func (r *Store) GetFloat64(key string) (float64, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return -1, errFindParse.Format("float64", key) } return v.Float64Default(-1) @@ -516,8 +987,8 @@ func (r *Store) GetFloat64Default(key string, def float64) float64 { // // If not found returns false and a non-nil error. func (r *Store) GetBool(key string) (bool, error) { - v := r.GetEntry(key) - if v == nil { + v, ok := r.GetEntry(key) + if !ok { return false, errFindParse.Format("bool", key) } diff --git a/core/router/api_builder.go b/core/router/api_builder.go index 242fbadbb3..9aa9a3386e 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -9,20 +9,18 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" - "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/macro" ) -const ( - // MethodNone is a Virtual method - // to store the "offline" routes. - MethodNone = "NONE" -) +// MethodNone is a Virtual method +// to store the "offline" routes. +const MethodNone = "NONE" var ( // AllMethods contains the valid http methods: // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", // "PATCH", "OPTIONS", "TRACE". - AllMethods = [...]string{ + AllMethods = []string{ "GET", "POST", "PUT", @@ -68,7 +66,7 @@ func (r *repository) getAll() []*Route { // and child routers. type APIBuilder struct { // the api builder global macros registry - macros *macro.Map + macros *macro.Macros // the api builder global handlers per status code registry (used for custom http errors) errorCodeHandlers *ErrorCodeHandlers // the api builder global routes repository @@ -116,7 +114,7 @@ var _ RoutesProvider = (*APIBuilder)(nil) // passed to the default request handl // which is responsible to build the API and the router handler. func NewAPIBuilder() *APIBuilder { api := &APIBuilder{ - macros: defaultMacros(), + macros: macro.Defaults, errorCodeHandlers: defaultErrorCodeHandlers(), reporter: errors.NewReporter(), relativePath: "/", @@ -246,7 +244,7 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co ) for _, m := range methods { - route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, api.macros) + route, err = NewRoute(m, subdomain, path, possibleMainHandlerName, routeHandlers, *api.macros) if err != nil { // template path parser errors: api.reporter.Add("%v -> %s:%s:%s", err, method, subdomain, path) return nil // fail on first error. @@ -270,10 +268,10 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co // otherwise use `Party` which can handle many paths with different handlers and middlewares. // // Usage: -// app.HandleMany("GET", "/user /user/{id:int} /user/me", genericUserHandler) +// app.HandleMany("GET", "/user /user/{id:uint64} /user/me", genericUserHandler) // At the other side, with `Handle` we've had to write: // app.Handle("GET", "/user", userHandler) -// app.Handle("GET", "/user/{id:int}", userByIDHandler) +// app.Handle("GET", "/user/{id:uint64}", userByIDHandler) // app.Handle("GET", "/user/me", userMeHandler) // // This method is used behind the scenes at the `Controller` function @@ -411,11 +409,11 @@ func (api *APIBuilder) WildcardSubdomain(middleware ...context.Handler) Party { return api.Subdomain(SubdomainWildcardIndicator, middleware...) } -// Macros returns the macro map which is responsible -// to register custom macro functions for all routes. +// Macros returns the macro collection that is responsible +// to register custom macros with their own parameter types and their macro functions for all routes. // // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path -func (api *APIBuilder) Macros() *macro.Map { +func (api *APIBuilder) Macros() *macro.Macros { return api.macros } diff --git a/core/router/fs.go b/core/router/fs.go index 8aa5728eb3..0c0ca01f9f 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -733,10 +733,10 @@ func checkIfRange(ctx context.Context, etagEmptyOrStrongMatch func(ifRangeValue return condFalse } +const indexPage = "/index.html" + // name is '/'-separated, not filepath.Separator. func serveFile(ctx context.Context, fs http.FileSystem, name string, redirect bool, showList bool, gzip bool) (string, int) { - const indexPage = "/index.html" - // redirect .../index.html to .../ // can't use Redirect() because that would make the path absolute, // which would be a problem running under StripPrefix diff --git a/core/router/handler.go b/core/router/handler.go index 24ecfc5d74..67ba584507 100644 --- a/core/router/handler.go +++ b/core/router/handler.go @@ -11,40 +11,30 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/netutil" - "github.com/kataras/iris/core/router/node" ) // RequestHandler the middle man between acquiring a context and releasing it. // By-default is the router algorithm. type RequestHandler interface { - // HandleRequest is same as context.Handler but its usage is only about routing, - // separate the concept here. + // HandleRequest should handle the request based on the Context. HandleRequest(context.Context) - // Build should builds the handler, it's being called on router's BuildRouter. + // Build should builds the handler, it's being called on router's BuildRouter. Build(provider RoutesProvider) error // RouteExists reports whether a particular route exists. RouteExists(ctx context.Context, method, path string) bool } -type tree struct { - Method string - // subdomain is empty for default-hostname routes, - // ex: mysubdomain. - Subdomain string - Nodes *node.Nodes -} - type routerHandler struct { - trees []*tree + trees []*trie hosts bool // true if at least one route contains a Subdomain. } var _ RequestHandler = &routerHandler{} -func (h *routerHandler) getTree(method, subdomain string) *tree { +func (h *routerHandler) getTree(method, subdomain string) *trie { for i := range h.trees { t := h.trees[i] - if t.Method == method && t.Subdomain == subdomain { + if t.method == method && t.subdomain == subdomain { return t } } @@ -64,12 +54,14 @@ func (h *routerHandler) addRoute(r *Route) error { t := h.getTree(method, subdomain) if t == nil { - n := node.Nodes{} + n := newTrieNode() // first time we register a route to this method with this subdomain - t = &tree{Method: method, Subdomain: subdomain, Nodes: &n} + t = &trie{method: method, subdomain: subdomain, root: n} h.trees = append(h.trees, t) } - return t.Nodes.Add(routeName, path, handlers) + + t.insert(path, routeName, handlers) + return nil } // NewDefaultHandler returns the handler which is responsible @@ -160,40 +152,45 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { r := ctx.Request() // use Trim to ensure there is no open redirect due to two leading slashes path = "/" + strings.Trim(path, "/") - r.URL.Path = path - url := r.URL.String() - // Fixes https://github.com/kataras/iris/issues/921 - // This is caused for security reasons, imagine a payment shop, - // you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7). - if method == http.MethodPost || method == http.MethodPut { - ctx.Redirect(url, http.StatusTemporaryRedirect) - return - } + r.URL.Path = path + if !ctx.Application().ConfigurationReadOnly().GetDisablePathCorrectionRedirection() { + // do redirect, else continue with the modified path without the last "/". + url := r.URL.String() + + // Fixes https://github.com/kataras/iris/issues/921 + // This is caused for security reasons, imagine a payment shop, + // you can't just permantly redirect a POST request, so just 307 (RFC 7231, 6.4.7). + if method == http.MethodPost || method == http.MethodPut { + ctx.Redirect(url, http.StatusTemporaryRedirect) + return + } - ctx.Redirect(url, http.StatusMovedPermanently) + ctx.Redirect(url, http.StatusMovedPermanently) - // RFC2616 recommends that a short note "SHOULD" be included in the - // response because older user agents may not understand 301/307. - // Shouldn't send the response for POST or HEAD; that leaves GET. - if method == http.MethodGet { - note := "Moved Permanently.\n" + // RFC2616 recommends that a short note "SHOULD" be included in the + // response because older user agents may not understand 301/307. + // Shouldn't send the response for POST or HEAD; that leaves GET. + if method == http.MethodGet { + note := "Moved Permanently.\n" - ctx.ResponseWriter().WriteString(note) + ctx.ResponseWriter().WriteString(note) + } + return } - return + } } for i := range h.trees { t := h.trees[i] - if method != t.Method { + if method != t.method { continue } - if h.hosts && t.Subdomain != "" { + if h.hosts && t.subdomain != "" { requestHost := ctx.Host() if netutil.IsLoopbackSubdomain(requestHost) { // this fixes a bug when listening on @@ -202,7 +199,7 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { continue // it's not a subdomain, it's something like 127.0.0.1 probably } // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty - if t.Subdomain == SubdomainWildcardIndicator { + if t.subdomain == SubdomainWildcardIndicator { // mydomain.com -> invalid // localhost -> invalid // sub.mydomain.com -> valid @@ -220,14 +217,14 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { continue } // continue to that, any subdomain is valid. - } else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot. + } else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot. continue } } - routeName, handlers := t.Nodes.Find(path, ctx.Params()) - if len(handlers) > 0 { - ctx.SetCurrentRouteName(routeName) - ctx.Do(handlers) + n := t.search(path, ctx.Params()) + if n != nil { + ctx.SetCurrentRouteName(n.RouteName) + ctx.Do(n.Handlers) // found return } @@ -238,15 +235,12 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { if ctx.Application().ConfigurationReadOnly().GetFireMethodNotAllowed() { for i := range h.trees { t := h.trees[i] - // a bit slower than previous implementation but @kataras let me to apply this change - // because it's more reliable. - // // if `Configuration#FireMethodNotAllowed` is kept as defaulted(false) then this function will not // run, therefore performance kept as before. - if t.Nodes.Exists(path) { + if h.subdomainAndPathAndMethodExists(ctx, t, "", path) { // RCF rfc2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // The response MUST include an Allow header containing a list of valid methods for the requested resource. - ctx.Header("Allow", t.Method) + ctx.Header("Allow", t.method) ctx.StatusCode(http.StatusMethodNotAllowed) return } @@ -256,55 +250,55 @@ func (h *routerHandler) HandleRequest(ctx context.Context) { ctx.StatusCode(http.StatusNotFound) } -// RouteExists reports whether a particular route exists -// It will search from the current subdomain of context's host, if not inside the root domain. -func (h *routerHandler) RouteExists(ctx context.Context, method, path string) bool { - for i := range h.trees { - t := h.trees[i] - if method != t.Method { - continue - } +func (h *routerHandler) subdomainAndPathAndMethodExists(ctx context.Context, t *trie, method, path string) bool { + if method != "" && method != t.method { + return false + } - if h.hosts && t.Subdomain != "" { - requestHost := ctx.Host() - if netutil.IsLoopbackSubdomain(requestHost) { - // this fixes a bug when listening on - // 127.0.0.1:8080 for example - // and have a wildcard subdomain and a route registered to root domain. - continue // it's not a subdomain, it's something like 127.0.0.1 probably + if h.hosts && t.subdomain != "" { + requestHost := ctx.Host() + if netutil.IsLoopbackSubdomain(requestHost) { + // this fixes a bug when listening on + // 127.0.0.1:8080 for example + // and have a wildcard subdomain and a route registered to root domain. + return false // it's not a subdomain, it's something like 127.0.0.1 probably + } + // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty + if t.subdomain == SubdomainWildcardIndicator { + // mydomain.com -> invalid + // localhost -> invalid + // sub.mydomain.com -> valid + // sub.localhost -> valid + serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() + if serverHost == requestHost { + return false // it's not a subdomain, it's a full domain (with .com...) } - // it's a dynamic wildcard subdomain, we have just to check if ctx.subdomain is not empty - if t.Subdomain == SubdomainWildcardIndicator { - // mydomain.com -> invalid - // localhost -> invalid - // sub.mydomain.com -> valid - // sub.localhost -> valid - serverHost := ctx.Application().ConfigurationReadOnly().GetVHost() - if serverHost == requestHost { - continue // it's not a subdomain, it's a full domain (with .com...) - } - dotIdx := strings.IndexByte(requestHost, '.') - slashIdx := strings.IndexByte(requestHost, '/') - if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) { - // if "." was found anywhere but not at the first path segment (host). - } else { - continue - } - // continue to that, any subdomain is valid. - } else if !strings.HasPrefix(requestHost, t.Subdomain) { // t.Subdomain contains the dot. - continue + dotIdx := strings.IndexByte(requestHost, '.') + slashIdx := strings.IndexByte(requestHost, '/') + if dotIdx > 0 && (slashIdx == -1 || slashIdx > dotIdx) { + // if "." was found anywhere but not at the first path segment (host). + } else { + return false } + // continue to that, any subdomain is valid. + } else if !strings.HasPrefix(requestHost, t.subdomain) { // t.subdomain contains the dot. + return false } + } - _, handlers := t.Nodes.Find(path, ctx.Params()) - if len(handlers) > 0 { - // found + n := t.search(path, ctx.Params()) + return n != nil +} + +// RouteExists reports whether a particular route exists +// It will search from the current subdomain of context's host, if not inside the root domain. +func (h *routerHandler) RouteExists(ctx context.Context, method, path string) bool { + for i := range h.trees { + t := h.trees[i] + if h.subdomainAndPathAndMethodExists(ctx, t, method, path) { return true } - - // not found or method not allowed. - break } return false diff --git a/core/router/macro.go b/core/router/macro.go deleted file mode 100644 index ba6969ba30..0000000000 --- a/core/router/macro.go +++ /dev/null @@ -1,253 +0,0 @@ -package router - -import ( - "net/http" - "strconv" - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" - "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/core/router/macro/interpreter/ast" -) - -// defaultMacros returns a new macro map which -// contains the default router's named param types functions. -func defaultMacros() *macro.Map { - macros := macro.NewMap() - // registers the String and Int default macro funcs - // user can add or override of his own funcs later on - // i.e: - // app.Macro.String.RegisterFunc("equal", func(eqWith string) func(string) bool { - // return func(paramValue string) bool { - // return eqWith == paramValue - // }}) - registerBuiltinsMacroFuncs(macros) - - return macros -} - -func registerBuiltinsMacroFuncs(out *macro.Map) { - // register the String which is the default type if not - // parameter type is specified or - // if a given parameter into path given but the func doesn't exist on the - // parameter type's function list. - // - // these can be overridden by the user, later on. - registerStringMacroFuncs(out.String) - registerIntMacroFuncs(out.Int) - registerIntMacroFuncs(out.Long) - registerAlphabeticalMacroFuncs(out.Alphabetical) - registerFileMacroFuncs(out.File) - registerPathMacroFuncs(out.Path) -} - -// String -// anything one part -func registerStringMacroFuncs(out *macro.Macro) { - // this can be used everywhere, it's to help users to define custom regexp expressions - // on all macros - out.RegisterFunc("regexp", func(expr string) macro.EvaluatorFunc { - regexpEvaluator := macro.MustNewEvaluatorFromRegexp(expr) - return regexpEvaluator - }) - - // checks if param value starts with the 'prefix' arg - out.RegisterFunc("prefix", func(prefix string) macro.EvaluatorFunc { - return func(paramValue string) bool { - return strings.HasPrefix(paramValue, prefix) - } - }) - - // checks if param value ends with the 'suffix' arg - out.RegisterFunc("suffix", func(suffix string) macro.EvaluatorFunc { - return func(paramValue string) bool { - return strings.HasSuffix(paramValue, suffix) - } - }) - - // checks if param value contains the 's' arg - out.RegisterFunc("contains", func(s string) macro.EvaluatorFunc { - return func(paramValue string) bool { - return strings.Contains(paramValue, s) - } - }) - - // checks if param value's length is at least 'min' - out.RegisterFunc("min", func(min int) macro.EvaluatorFunc { - return func(paramValue string) bool { - return len(paramValue) >= min - } - }) - // checks if param value's length is not bigger than 'max' - out.RegisterFunc("max", func(max int) macro.EvaluatorFunc { - return func(paramValue string) bool { - return max >= len(paramValue) - } - }) -} - -// Int -// only numbers (0-9) -func registerIntMacroFuncs(out *macro.Macro) { - // checks if the param value's int representation is - // bigger or equal than 'min' - out.RegisterFunc("min", func(min int) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - return n >= min - } - }) - - // checks if the param value's int representation is - // smaller or equal than 'max' - out.RegisterFunc("max", func(max int) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - return n <= max - } - }) - - // checks if the param value's int representation is - // between min and max, including 'min' and 'max' - out.RegisterFunc("range", func(min, max int) macro.EvaluatorFunc { - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - - if n < min || n > max { - return false - } - return true - } - }) -} - -// Alphabetical -// letters only (upper or lowercase) -func registerAlphabeticalMacroFuncs(out *macro.Macro) { - -} - -// File -// letters (upper or lowercase) -// numbers (0-9) -// underscore (_) -// dash (-) -// point (.) -// no spaces! or other character -func registerFileMacroFuncs(out *macro.Macro) { - -} - -// Path -// File+slashes(anywhere) -// should be the latest param, it's the wildcard -func registerPathMacroFuncs(out *macro.Macro) { - -} - -// compileRoutePathAndHandlers receives a route info and returns its parsed/"compiled" path -// and the new handlers (prepend all the macro's handler, if any). -// -// It's not exported for direct use. -func compileRoutePathAndHandlers(handlers context.Handlers, tmpl *macro.Template) (string, context.Handlers, error) { - // parse the path to node's path, now. - path, err := convertTmplToNodePath(tmpl) - if err != nil { - return tmpl.Src, handlers, err - } - // prepend the macro handler to the route, now, - // right before the register to the tree, so routerbuilder.UseGlobal will work as expected. - if len(tmpl.Params) > 0 { - macroEvaluatorHandler := convertTmplToHandler(tmpl) - // may return nil if no really need a macro handler evaluator - if macroEvaluatorHandler != nil { - handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...) - } - } - - return path, handlers, nil -} - -func convertTmplToNodePath(tmpl *macro.Template) (string, error) { - routePath := tmpl.Src - if len(tmpl.Params) > 0 { - if routePath[len(routePath)-1] == '/' { - routePath = routePath[0 : len(routePath)-2] // remove the last "/" if macro syntax instead of underline's - } - } - - // if it has started with {} and it's valid - // then the tmpl.Params will be filled, - // so no any further check needed - for i, p := range tmpl.Params { - if p.Type == ast.ParamTypePath { - if i != len(tmpl.Params)-1 { - return "", errors.New("parameter type \"ParamTypePath\" should be putted to the very last of a path") - } - routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1) - } else { - routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1) - } - } - - return routePath, nil -} - -// note: returns nil if not needed, the caller(router) should be check for that before adding that on route's Middleware -func convertTmplToHandler(tmpl *macro.Template) context.Handler { - - needMacroHandler := false - - // check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params. - // 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used) - // 2. if we don't have any named params then we don't need a handler too. - for _, p := range tmpl.Params { - if len(p.Funcs) == 0 && (p.Type == ast.ParamTypeUnExpected || p.Type == ast.ParamTypeString || p.Type == ast.ParamTypePath) && p.ErrCode == http.StatusNotFound { - } else { - // println("we need handler for: " + tmpl.Src) - needMacroHandler = true - } - } - - if !needMacroHandler { - // println("we don't need handler for: " + tmpl.Src) - return nil - } - - return func(tmpl macro.Template) context.Handler { - return func(ctx context.Context) { - for _, p := range tmpl.Params { - paramValue := ctx.Params().Get(p.Name) - // first, check for type evaluator - if !p.TypeEvaluator(paramValue) { - ctx.StatusCode(p.ErrCode) - ctx.StopExecution() - return - } - - // then check for all of its functions - for _, evalFunc := range p.Funcs { - if !evalFunc(paramValue) { - ctx.StatusCode(p.ErrCode) - ctx.StopExecution() - return - } - } - - } - // if all passed, just continue - ctx.Next() - } - }(*tmpl) - -} diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go deleted file mode 100644 index 23e1723628..0000000000 --- a/core/router/macro/interpreter/ast/ast.go +++ /dev/null @@ -1,215 +0,0 @@ -package ast - -import ( - "fmt" - "reflect" - "strconv" -) - -// ParamType is a specific uint8 type -// which holds the parameter types' type. -type ParamType uint8 - -const ( - // ParamTypeUnExpected is an unexpected parameter type. - ParamTypeUnExpected ParamType = iota - // ParamTypeString is the string type. - // If parameter type is missing then it defaults to String type. - // Allows anything - // Declaration: /mypath/{myparam:string} or /mypath{myparam} - ParamTypeString - // ParamTypeInt is the integer, a number type. - // Allows only positive numbers (0-9) - // Declaration: /mypath/{myparam:int} - ParamTypeInt - // ParamTypeLong is the integer, a number type. - // Allows only positive numbers (0-9) - // Declaration: /mypath/{myparam:long} - ParamTypeLong - // ParamTypeBoolean is the bool type. - // Allows only "1" or "t" or "T" or "TRUE" or "true" or "True" - // or "0" or "f" or "F" or "FALSE" or "false" or "False". - // Declaration: /mypath/{myparam:boolean} - ParamTypeBoolean - // ParamTypeAlphabetical is the alphabetical/letter type type. - // Allows letters only (upper or lowercase) - // Declaration: /mypath/{myparam:alphabetical} - ParamTypeAlphabetical - // ParamTypeFile is the file single path type. - // Allows: - // letters (upper or lowercase) - // numbers (0-9) - // underscore (_) - // dash (-) - // point (.) - // no spaces! or other character - // Declaration: /mypath/{myparam:file} - ParamTypeFile - // ParamTypePath is the multi path (or wildcard) type. - // Allows anything, should be the last part - // Declaration: /mypath/{myparam:path} - ParamTypePath -) - -func (pt ParamType) String() string { - for k, v := range paramTypes { - if v == pt { - return k - } - } - - return "unexpected" -} - -// Not because for a single reason -// a string may be a -// ParamTypeString or a ParamTypeFile -// or a ParamTypePath or ParamTypeAlphabetical. -// -// func ParamTypeFromStd(k reflect.Kind) ParamType { - -// Kind returns the std kind of this param type. -func (pt ParamType) Kind() reflect.Kind { - switch pt { - case ParamTypeAlphabetical: - fallthrough - case ParamTypeFile: - fallthrough - case ParamTypePath: - fallthrough - case ParamTypeString: - return reflect.String - case ParamTypeInt: - return reflect.Int - case ParamTypeLong: - return reflect.Int64 - case ParamTypeBoolean: - return reflect.Bool - } - return reflect.Invalid // 0 -} - -// ValidKind will return true if at least one param type is supported -// for this std kind. -func ValidKind(k reflect.Kind) bool { - switch k { - case reflect.String: - fallthrough - case reflect.Int: - fallthrough - case reflect.Int64: - fallthrough - case reflect.Bool: - return true - default: - return false - } -} - -// Assignable returns true if the "k" standard type -// is assignabled to this ParamType. -func (pt ParamType) Assignable(k reflect.Kind) bool { - return pt.Kind() == k -} - -var paramTypes = map[string]ParamType{ - "string": ParamTypeString, - "int": ParamTypeInt, - "long": ParamTypeLong, - "boolean": ParamTypeBoolean, - "alphabetical": ParamTypeAlphabetical, - "file": ParamTypeFile, - "path": ParamTypePath, - // could be named also: - // "tail": - // "wild" - // "wildcard" - -} - -// LookupParamType accepts the string -// representation of a parameter type. -// Available: -// "string" -// "int" -// "long" -// "alphabetical" -// "file" -// "path" -func LookupParamType(ident string) ParamType { - if typ, ok := paramTypes[ident]; ok { - return typ - } - return ParamTypeUnExpected -} - -// LookupParamTypeFromStd accepts the string representation of a standard go type. -// It returns a ParamType, but it may differs for example -// the alphabetical, file, path and string are all string go types, so -// make sure that caller resolves these types before this call. -// -// string matches to string -// int matches to int -// int64 matches to long -// bool matches to boolean -func LookupParamTypeFromStd(goType string) ParamType { - switch goType { - case "string": - return ParamTypeString - case "int": - return ParamTypeInt - case "int64": - return ParamTypeLong - case "bool": - return ParamTypeBoolean - default: - return ParamTypeUnExpected - } -} - -// ParamStatement is a struct -// which holds all the necessary information about a macro parameter. -// It holds its type (string, int, alphabetical, file, path), -// its source ({param:type}), -// its name ("param"), -// its attached functions by the user (min, max...) -// and the http error code if that parameter -// failed to be evaluated. -type ParamStatement struct { - Src string // the original unparsed source, i.e: {id:int range(1,5) else 404} - Name string // id - Type ParamType // int - Funcs []ParamFunc // range - ErrorCode int // 404 -} - -// ParamFuncArg represents a single parameter function's argument -type ParamFuncArg interface{} - -// ParamFuncArgToInt converts and returns -// any type of "a", to an integer. -func ParamFuncArgToInt(a ParamFuncArg) (int, error) { - switch a.(type) { - case int: - return a.(int), nil - case string: - return strconv.Atoi(a.(string)) - case int64: - return int(a.(int64)), nil - default: - return -1, fmt.Errorf("unexpected function argument type: %q", a) - } -} - -// ParamFunc holds the name of a parameter's function -// and its arguments (values) -// A param func is declared with: -// {param:int range(1,5)}, -// the range is the -// param function name -// the 1 and 5 are the two param function arguments -// range(1,5) -type ParamFunc struct { - Name string // range - Args []ParamFuncArg // [1,5] -} diff --git a/core/router/macro/macro.go b/core/router/macro/macro.go deleted file mode 100644 index e4141c0789..0000000000 --- a/core/router/macro/macro.go +++ /dev/null @@ -1,292 +0,0 @@ -package macro - -import ( - "fmt" - "reflect" - "regexp" - "strconv" - "unicode" - - "github.com/kataras/iris/core/router/macro/interpreter/ast" -) - -// EvaluatorFunc is the signature for both param types and param funcs. -// It should accepts the param's value as string -// and return true if validated otherwise false. -type EvaluatorFunc func(paramValue string) bool - -// NewEvaluatorFromRegexp accepts a regexp "expr" expression -// and returns an EvaluatorFunc based on that regexp. -// the regexp is compiled before return. -// -// Returns a not-nil error on regexp compile failure. -func NewEvaluatorFromRegexp(expr string) (EvaluatorFunc, error) { - if expr == "" { - return nil, fmt.Errorf("empty regex expression") - } - - // add the last $ if missing (and not wildcard(?)) - if i := expr[len(expr)-1]; i != '$' && i != '*' { - expr += "$" - } - - r, err := regexp.Compile(expr) - if err != nil { - return nil, err - } - - return r.MatchString, nil -} - -// MustNewEvaluatorFromRegexp same as NewEvaluatorFromRegexp -// but it panics on the "expr" parse failure. -func MustNewEvaluatorFromRegexp(expr string) EvaluatorFunc { - r, err := NewEvaluatorFromRegexp(expr) - if err != nil { - panic(err) - } - return r -} - -var ( - goodParamFuncReturnType = reflect.TypeOf(func(string) bool { return false }) - goodParamFuncReturnType2 = reflect.TypeOf(EvaluatorFunc(func(string) bool { return false })) -) - -func goodParamFunc(typ reflect.Type) bool { - // should be a func - // which returns a func(string) bool - if typ.Kind() == reflect.Func { - if typ.NumOut() == 1 { - typOut := typ.Out(0) - if typOut == goodParamFuncReturnType || typOut == goodParamFuncReturnType2 { - return true - } - } - } - - return false -} - -// goodParamFuncName reports whether the function name is a valid identifier. -func goodParamFuncName(name string) bool { - if name == "" { - return false - } - // valid names are only letters and _ - for _, r := range name { - switch { - case r == '_': - case !unicode.IsLetter(r): - return false - } - } - return true -} - -// the convertBuilderFunc return value is generating at boot time. -// convertFunc converts an interface to a valid full param function. -func convertBuilderFunc(fn interface{}) ParamEvaluatorBuilder { - - typFn := reflect.TypeOf(fn) - if !goodParamFunc(typFn) { - return nil - } - - numFields := typFn.NumIn() - - return func(args []ast.ParamFuncArg) EvaluatorFunc { - if len(args) != numFields { - // no variadics support, for now. - panic("args should be the same len as numFields") - } - var argValues []reflect.Value - for i := 0; i < numFields; i++ { - field := typFn.In(i) - arg := args[i] - - if field.Kind() != reflect.TypeOf(arg).Kind() { - panic("fields should have the same type") - } - - argValues = append(argValues, reflect.ValueOf(arg)) - } - - evalFn := reflect.ValueOf(fn).Call(argValues)[0].Interface() - - var evaluator EvaluatorFunc - // check for typed and not typed - if _v, ok := evalFn.(EvaluatorFunc); ok { - evaluator = _v - } else if _v, ok = evalFn.(func(string) bool); ok { - evaluator = _v - } - return func(paramValue string) bool { - return evaluator(paramValue) - } - } -} - -type ( - // Macro represents the parsed macro, - // which holds - // the evaluator (param type's evaluator + param functions evaluators) - // and its param functions. - // - // Any type contains its own macro - // instance, so an String type - // contains its type evaluator - // which is the "Evaluator" field - // and it can register param functions - // to that macro which maps to a parameter type. - Macro struct { - Evaluator EvaluatorFunc - funcs []ParamFunc - } - - // ParamEvaluatorBuilder is a func - // which accepts a param function's arguments (values) - // and returns an EvaluatorFunc, its job - // is to make the macros to be registered - // by user at the most generic possible way. - ParamEvaluatorBuilder func([]ast.ParamFuncArg) EvaluatorFunc - - // ParamFunc represents the parsed - // parameter function, it holds - // the parameter's name - // and the function which will build - // the evaluator func. - ParamFunc struct { - Name string - Func ParamEvaluatorBuilder - } -) - -func newMacro(evaluator EvaluatorFunc) *Macro { - return &Macro{Evaluator: evaluator} -} - -// RegisterFunc registers a parameter function -// to that macro. -// Accepts the func name ("range") -// and the function body, which should return an EvaluatorFunc -// a bool (it will be converted to EvaluatorFunc later on), -// i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){}) -func (m *Macro) RegisterFunc(funcName string, fn interface{}) { - fullFn := convertBuilderFunc(fn) - m.registerFunc(funcName, fullFn) -} - -func (m *Macro) registerFunc(funcName string, fullFn ParamEvaluatorBuilder) { - if !goodParamFuncName(funcName) { - return - } - - for _, fn := range m.funcs { - if fn.Name == funcName { - fn.Func = fullFn - return - } - } - - m.funcs = append(m.funcs, ParamFunc{ - Name: funcName, - Func: fullFn, - }) -} - -func (m *Macro) getFunc(funcName string) ParamEvaluatorBuilder { - for _, fn := range m.funcs { - if fn.Name == funcName { - if fn.Func == nil { - continue - } - return fn.Func - } - } - return nil -} - -// Map contains the default macros mapped to their types. -// This is the manager which is used by the caller to register custom -// parameter functions per param-type (String, Int, Long, Boolean, Alphabetical, File, Path). -type Map struct { - // string type - // anything - String *Macro - // uint type - // only positive numbers (+0-9) - // it could be uint/uint32 but we keep int for simplicity - Int *Macro - // long an int64 type - // only positive numbers (+0-9) - // it could be uint64 but we keep int64 for simplicity - Long *Macro - // boolean as bool type - // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" - // or "0" or "f" or "F" or "FALSE" or "false" or "False". - Boolean *Macro - // alphabetical/letter type - // letters only (upper or lowercase) - Alphabetical *Macro - // file type - // letters (upper or lowercase) - // numbers (0-9) - // underscore (_) - // dash (-) - // point (.) - // no spaces! or other character - File *Macro - // path type - // anything, should be the last part - Path *Macro -} - -// NewMap returns a new macro Map with default -// type evaluators. -// -// Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path -func NewMap() *Map { - return &Map{ - // it allows everything, so no need for a regexp here. - String: newMacro(func(string) bool { return true }), - Int: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")), - Long: newMacro(MustNewEvaluatorFromRegexp("^[0-9]+$")), - Boolean: newMacro(func(paramValue string) bool { - // a simple if statement is faster than regex ^(true|false|True|False|t|0|f|FALSE|TRUE)$ - // in this case. - _, err := strconv.ParseBool(paramValue) - return err == nil - }), - Alphabetical: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z ]+$")), - File: newMacro(MustNewEvaluatorFromRegexp("^[a-zA-Z0-9_.-]*$")), - // it allows everything, we have String and Path as different - // types because I want to give the opportunity to the user - // to organise the macro functions based on wildcard or single dynamic named path parameter. - // Should be the last. - Path: newMacro(func(string) bool { return true }), - } -} - -// Lookup returns the specific Macro from the map -// based on the parameter type. -// i.e if ast.ParamTypeInt then it will return the m.Int. -// Returns the m.String if not matched. -func (m *Map) Lookup(typ ast.ParamType) *Macro { - switch typ { - case ast.ParamTypeInt: - return m.Int - case ast.ParamTypeLong: - return m.Long - case ast.ParamTypeBoolean: - return m.Boolean - case ast.ParamTypeAlphabetical: - return m.Alphabetical - case ast.ParamTypeFile: - return m.File - case ast.ParamTypePath: - return m.Path - default: - return m.String - } -} diff --git a/core/router/macro/macro_test.go b/core/router/macro/macro_test.go deleted file mode 100644 index d412da29e5..0000000000 --- a/core/router/macro/macro_test.go +++ /dev/null @@ -1,186 +0,0 @@ -package macro - -import ( - "reflect" - "testing" -) - -// Most important tests to look: -// ../parser/parser_test.go -// ../lexer/lexer_test.go - -func TestGoodParamFunc(t *testing.T) { - good1 := func(min int, max int) func(string) bool { - return func(paramValue string) bool { - return true - } - } - - good2 := func(min int, max int) func(string) bool { - return func(paramValue string) bool { - return true - } - } - - notgood1 := func(min int, max int) bool { - return false - } - - if !goodParamFunc(reflect.TypeOf(good1)) { - t.Fatalf("expected good1 func to be good but it's not") - } - - if !goodParamFunc(reflect.TypeOf(good2)) { - t.Fatalf("expected good2 func to be good but it's not") - } - - if goodParamFunc(reflect.TypeOf(notgood1)) { - t.Fatalf("expected notgood1 func to be the worst") - } -} - -func TestGoodParamFuncName(t *testing.T) { - tests := []struct { - name string - good bool - }{ - {"range", true}, - {"_range", true}, - {"range_", true}, - {"r_ange", true}, - // numbers or other symbols are invalid. - {"range1", false}, - {"2range", false}, - {"r@nge", false}, - {"rang3", false}, - } - for i, tt := range tests { - isGood := goodParamFuncName(tt.name) - if tt.good && !isGood { - t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name) - } else if !tt.good && isGood { - t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name) - } - } -} - -func testEvaluatorRaw(macroEvaluator *Macro, input string, pass bool, i int, t *testing.T) { - if got := macroEvaluator.Evaluator(input); pass != got { - t.Fatalf("tests[%d] - expecting %v but got %v", i, pass, got) - } -} - -func TestStringEvaluatorRaw(t *testing.T) { - f := NewMap() - - tests := []struct { - pass bool - input string - }{ - {true, "astring"}, // 0 - {true, "astringwith_numb3rS_and_symbol$"}, // 1 - {true, "32321"}, // 2 - {true, "main.css"}, // 3 - {true, "/assets/main.css"}, // 4 - // false never - } // 0 - - for i, tt := range tests { - testEvaluatorRaw(f.String, tt.input, tt.pass, i, t) - } -} - -func TestIntEvaluatorRaw(t *testing.T) { - f := NewMap() - - tests := []struct { - pass bool - input string - }{ - {false, "astring"}, // 0 - {false, "astringwith_numb3rS_and_symbol$"}, // 1 - {true, "32321"}, // 2 - {false, "main.css"}, // 3 - {false, "/assets/main.css"}, // 4 - } - - for i, tt := range tests { - testEvaluatorRaw(f.Int, tt.input, tt.pass, i, t) - } -} - -func TestAlphabeticalEvaluatorRaw(t *testing.T) { - f := NewMap() - - tests := []struct { - pass bool - input string - }{ - {true, "astring"}, // 0 - {false, "astringwith_numb3rS_and_symbol$"}, // 1 - {false, "32321"}, // 2 - {false, "main.css"}, // 3 - {false, "/assets/main.css"}, // 4 - } - - for i, tt := range tests { - testEvaluatorRaw(f.Alphabetical, tt.input, tt.pass, i, t) - } -} - -func TestFileEvaluatorRaw(t *testing.T) { - f := NewMap() - - tests := []struct { - pass bool - input string - }{ - {true, "astring"}, // 0 - {false, "astringwith_numb3rS_and_symbol$"}, // 1 - {true, "32321"}, // 2 - {true, "main.css"}, // 3 - {false, "/assets/main.css"}, // 4 - } - - for i, tt := range tests { - testEvaluatorRaw(f.File, tt.input, tt.pass, i, t) - } -} - -func TestPathEvaluatorRaw(t *testing.T) { - f := NewMap() - - pathTests := []struct { - pass bool - input string - }{ - {true, "astring"}, // 0 - {true, "astringwith_numb3rS_and_symbol$"}, // 1 - {true, "32321"}, // 2 - {true, "main.css"}, // 3 - {true, "/assets/main.css"}, // 4 - {true, "disk/assets/main.css"}, // 5 - } - - for i, tt := range pathTests { - testEvaluatorRaw(f.Path, tt.input, tt.pass, i, t) - } -} - -// func TestMapRegisterFunc(t *testing.T) { -// m := NewMap() -// m.String.RegisterFunc("prefix", func(prefix string) EvaluatorFunc { -// return func(paramValue string) bool { -// return strings.HasPrefix(paramValue, prefix) -// } -// }) - -// p, err := Parse("/user/@iris") -// if err != nil { -// t.Fatalf(err) -// } - -// // p.Params = append(p.) - -// testEvaluatorRaw(m.String, p.Src, false, 0, t) -// } diff --git a/core/router/macro/template.go b/core/router/macro/template.go deleted file mode 100644 index f26c1d0619..0000000000 --- a/core/router/macro/template.go +++ /dev/null @@ -1,75 +0,0 @@ -package macro - -import ( - "github.com/kataras/iris/core/router/macro/interpreter/ast" - "github.com/kataras/iris/core/router/macro/interpreter/parser" -) - -// Template contains a route's path full parsed template. -// -// Fields: -// Src is the raw source of the path, i.e /users/{id:int min(1)} -// Params is the list of the Params that are being used to the -// path, i.e the min as param name and 1 as the param argument. -type Template struct { - // Src is the original template given by the client - Src string `json:"src"` - Params []TemplateParam `json:"params"` -} - -// TemplateParam is the parsed macro parameter's template -// they are being used to describe the param's syntax result. -type TemplateParam struct { - Src string `json:"src"` // the unparsed param'false source - // Type is not useful anywhere here but maybe - // it's useful on host to decide how to convert the path template to specific router's syntax - Type ast.ParamType `json:"type"` - Name string `json:"name"` - ErrCode int `json:"errCode"` - TypeEvaluator EvaluatorFunc `json:"-"` - Funcs []EvaluatorFunc `json:"-"` -} - -// Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions) -// and returns a new Template. -// It builds all the parameter functions for that template -// and their evaluators, it's the api call that makes use the interpeter's parser -> lexer. -func Parse(src string, macros *Map) (*Template, error) { - params, err := parser.Parse(src) - if err != nil { - return nil, err - } - t := new(Template) - t.Src = src - - for _, p := range params { - funcMap := macros.Lookup(p.Type) - typEval := funcMap.Evaluator - - tmplParam := TemplateParam{ - Src: p.Src, - Type: p.Type, - Name: p.Name, - ErrCode: p.ErrorCode, - TypeEvaluator: typEval, - } - for _, paramfn := range p.Funcs { - tmplFn := funcMap.getFunc(paramfn.Name) - if tmplFn == nil { // if not find on this type, check for String's which is for global funcs too - tmplFn = macros.String.getFunc(paramfn.Name) - if tmplFn == nil { // if not found then just skip this param - continue - } - } - evalFn := tmplFn(paramfn.Args) - if evalFn == nil { - continue - } - tmplParam.Funcs = append(tmplParam.Funcs, evalFn) - } - - t.Params = append(t.Params, tmplParam) - } - - return t, nil -} diff --git a/core/router/node/node.go b/core/router/node/node.go deleted file mode 100644 index 4a4adb05eb..0000000000 --- a/core/router/node/node.go +++ /dev/null @@ -1,448 +0,0 @@ -package node - -import ( - "sort" - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -// Nodes a conversion type for []*node. -type Nodes []*node - -type node struct { - s string - routeName string - wildcardParamName string // name of the wildcard parameter, only one per whole Node is allowed - paramNames []string // only-names - childrenNodes Nodes - handlers context.Handlers - root bool - rootWildcard bool // if it's a wildcard {path} type on root, it should allow everything but it is not conflicts with - // any other static or dynamic or wildcard paths if exists on other nodes. -} - -// ErrDublicate returnned from `Add` when two or more routes have the same registered path. -var ErrDublicate = errors.New("two or more routes have the same registered path") - -/// TODO: clean up needed until v8.5 - -// Add adds a node to the tree, returns an ErrDublicate error on failure. -func (nodes *Nodes) Add(routeName string, path string, handlers context.Handlers) error { - // println("[Add] adding path: " + path) - // resolve params and if that node should be added as root - var params []string - var paramStart, paramEnd int - for { - paramStart = strings.IndexByte(path[paramEnd:], ':') - if paramStart == -1 { - break - } - paramStart += paramEnd - paramStart++ - paramEnd = strings.IndexByte(path[paramStart:], '/') - - if paramEnd == -1 { - params = append(params, path[paramStart:]) - path = path[:paramStart] - break - } - paramEnd += paramStart - params = append(params, path[paramStart:paramEnd]) - path = path[:paramStart] + path[paramEnd:] - paramEnd -= paramEnd - paramStart - } - - var p []int - for i := 0; i < len(path); i++ { - idx := strings.IndexByte(path[i:], ':') - if idx == -1 { - break - } - p = append(p, idx+i) - i = idx + i - } - - for _, idx := range p { - // print("-2 nodes.Add: path: " + path + " params len: ") - // println(len(params)) - if err := nodes.add(routeName, path[:idx], nil, nil, true); err != nil { - return err - } - // print("-1 nodes.Add: path: " + path + " params len: ") - // println(len(params)) - if nidx := idx + 1; len(path) > nidx { - if err := nodes.add(routeName, path[:nidx], nil, nil, true); err != nil { - return err - } - } - } - - // print("nodes.Add: path: " + path + " params len: ") - // println(len(params)) - if err := nodes.add(routeName, path, params, handlers, true); err != nil { - return err - } - - // prioritize by static path remember, they were already sorted by subdomains too. - nodes.prioritize() - return nil -} - -func (nodes *Nodes) add(routeName, path string, paramNames []string, handlers context.Handlers, root bool) (err error) { - // println("[add] route name: " + routeName) - // println("[add] adding path: " + path) - - // wraia etsi doulevei ara - // na to kanw na exei to node to diko tou wildcard parameter name - // kai sto telos na pernei auto, me vasi to *paramname - // alla edw mesa 9a ginete register vasi tou last / - - // set the wildcard param name to the root and its children. - wildcardIdx := strings.IndexByte(path, '*') - wildcardParamName := "" - if wildcardIdx > 0 && len(paramNames) == 0 { // 27 Oct comment: && len(paramNames) == 0 { - wildcardParamName = path[wildcardIdx+1:] - path = path[0:wildcardIdx-1] + "/" // replace *paramName with single slash - - // if path[len(path)-1] == '/' { - // if root wildcard, then add it as it's and return - rootWildcard := path == "/" - if rootWildcard { - path += "/" // if root wildcard, then do it like "//" instead of simple "/" - } - - n := &node{ - rootWildcard: rootWildcard, - s: path, - routeName: routeName, - wildcardParamName: wildcardParamName, - paramNames: paramNames, - handlers: handlers, - root: root, - } - *nodes = append(*nodes, n) - // println("1. nodes.Add path: " + path) - return - - } - -loop: - for _, n := range *nodes { - if n.rootWildcard { - continue - } - - if len(n.paramNames) == 0 && n.wildcardParamName != "" { - continue - } - - minlen := len(n.s) - if len(path) < minlen { - minlen = len(path) - } - - for i := 0; i < minlen; i++ { - if n.s[i] == path[i] { - continue - } - if i == 0 { - continue loop - } - - *n = node{ - s: n.s[:i], - childrenNodes: Nodes{ - { - s: n.s[i:], - routeName: n.routeName, - wildcardParamName: n.wildcardParamName, // wildcardParamName - paramNames: n.paramNames, - childrenNodes: n.childrenNodes, - handlers: n.handlers, - }, - { - s: path[i:], - routeName: routeName, - wildcardParamName: wildcardParamName, - paramNames: paramNames, - handlers: handlers, - }, - }, - root: n.root, - } - - // println("2. change n and return " + n.s[:i] + " and " + path[i:]) - return - } - - if len(path) < len(n.s) { - // println("3. change n and return | n.s[:len(path)] = " + n.s[:len(path)-1] + " and child: " + n.s[len(path)-1:]) - - *n = node{ - s: n.s[:len(path)], - routeName: routeName, - wildcardParamName: wildcardParamName, - paramNames: paramNames, - childrenNodes: Nodes{ - { - s: n.s[len(path):], - routeName: n.routeName, - wildcardParamName: n.wildcardParamName, // wildcardParamName - paramNames: n.paramNames, - childrenNodes: n.childrenNodes, - handlers: n.handlers, - }, - }, - handlers: handlers, - root: n.root, - } - - return - } - - if len(path) > len(n.s) { - if n.wildcardParamName != "" { - n := &node{ - s: path, - routeName: routeName, - wildcardParamName: wildcardParamName, - paramNames: paramNames, - handlers: handlers, - root: root, - } - // println("3.5. nodes.Add path: " + n.s) - *nodes = append(*nodes, n) - return - } - - pathToAdd := path[len(n.s):] - // println("4. nodes.Add route name: " + routeName) - // println("4. nodes.Add path: " + pathToAdd) - err = n.childrenNodes.add(routeName, pathToAdd, paramNames, handlers, false) - return err - } - - if len(handlers) == 0 { // missing handlers - return nil - } - - if len(n.handlers) > 0 { // n.handlers already setted - return ErrDublicate - } - n.paramNames = paramNames - n.handlers = handlers - n.routeName = routeName - return - } - - // START - // Author's note: - // 27 Oct 2017; fixes s|i|l+static+p - // without breaking the current tests. - if wildcardIdx > 0 { - wildcardParamName = path[wildcardIdx+1:] - path = path[0:wildcardIdx-1] + "/" - } - // END - - n := &node{ - s: path, - routeName: routeName, - wildcardParamName: wildcardParamName, - paramNames: paramNames, - handlers: handlers, - root: root, - } - *nodes = append(*nodes, n) - - // println("5. node add on path: " + path + " n.s: " + n.s + " wildcard param: " + n.wildcardParamName) - return -} - -// Find resolves the path, fills its params -// and returns the registered to the resolved node's handlers. -func (nodes Nodes) Find(path string, params *context.RequestParams) (string, context.Handlers) { - n, paramValues := nodes.findChild(path, nil) - if n != nil { - // map the params, - // n.params are the param names - if len(paramValues) > 0 { - // println("-----------") - // print("param values returned len: ") - // println(len(paramValues)) - // println("first value is: " + paramValues[0]) - // print("n.paramNames len: ") - // println(len(n.paramNames)) - for i, name := range n.paramNames { - // println("setting param name: " + name + " = " + paramValues[i]) - params.Set(name, paramValues[i]) - } - // last is the wildcard, - // if paramValues are exceed from the registered param names. - // Note that n.wildcardParamName can be not empty but that doesn't meaning - // that it contains a wildcard path, so the check is required. - if len(paramValues) > len(n.paramNames) { - // println("len(paramValues) > len(n.paramNames)") - lastWildcardVal := paramValues[len(paramValues)-1] - // println("setting wildcard param name: " + n.wildcardParamName + " = " + lastWildcardVal) - params.Set(n.wildcardParamName, lastWildcardVal) - } - } - - return n.routeName, n.handlers - } - - return "", nil -} - -// Exists returns true if a node with that "path" exists, -// otherise false. -// -// We don't care about parameters here. -func (nodes Nodes) Exists(path string) bool { - n, _ := nodes.findChild(path, nil) - return n != nil && len(n.handlers) > 0 -} - -func (nodes Nodes) findChild(path string, params []string) (*node, []string) { - - for _, n := range nodes { - if n.s == ":" { - paramEnd := strings.IndexByte(path, '/') - if paramEnd == -1 { - if len(n.handlers) == 0 { - return nil, nil - } - return n, append(params, path) - } - return n.childrenNodes.findChild(path[paramEnd:], append(params, path[:paramEnd])) - } - - // println("n.s: " + n.s) - // print("n.childrenNodes len: ") - // println(len(n.childrenNodes)) - // print("n.root: ") - // println(n.root) - - // by runtime check of:, - // if n.s == "//" && n.root && n.wildcardParamName != "" { - // but this will slow down, so we have a static field on the node itself: - if n.rootWildcard { - // println("return from n.rootWildcard") - // single root wildcard - if len(path) < 2 { - // do not remove that, it seems useless but it's not, - // we had an error while production, this fixes that. - path = "/" + path - } - return n, append(params, path[1:]) - } - - // second conditional may be unnecessary - // because of the n.rootWildcard before, but do it. - if n.wildcardParamName != "" && len(path) > 2 { - // println("n has wildcard n.s: " + n.s + " on path: " + path) - // n.s = static/, path = static - - // println(n.s + " vs path: " + path) - - // we could have /other/ as n.s so - // we must do this check, remember: - // now wildcards live on their own nodes - if len(path) == len(n.s)-1 { - // then it's like: - // path = /other2 - // ns = /other2/ - if path == n.s[0:len(n.s)-1] { - return n, params - } - } - - // othwerwise path = /other2/dsadas - // ns= /other2/ - if strings.HasPrefix(path, n.s) { - if len(path) > len(n.s)+1 { - return n, append(params, path[len(n.s):]) // without slash - } - } - - } - - if !strings.HasPrefix(path, n.s) { - // fmt.Printf("---here root: %v, n.s: "+n.s+" and path: "+path+" is dynamic: %v , wildcardParamName: %s, children len: %v \n", n.root, n.isDynamic(), n.wildcardParamName, len(n.childrenNodes)) - // println(path + " n.s: " + n.s + " continue...") - continue - } - - if len(path) == len(n.s) { - if len(n.handlers) == 0 { - return nil, nil - } - return n, params - } - - child, childParamNames := n.childrenNodes.findChild(path[len(n.s):], params) - - // print("childParamNames len: ") - // println(len(childParamNames)) - - // if len(childParamNames) > 0 { - // println("childParamsNames[0] = " + childParamNames[0]) - // } - - if child == nil || len(child.handlers) == 0 { - if n.s[len(n.s)-1] == '/' && !(n.root && (n.s == "/" || len(n.childrenNodes) > 0)) { - if len(n.handlers) == 0 { - return nil, nil - } - - // println("if child == nil.... | n.s = " + n.s) - // print("n.paramNames len: ") - // println(n.paramNames) - // print("n.wildcardParamName is: ") - // println(n.wildcardParamName) - // print("return n, append(params, path[len(n.s) | params: ") - // println(path[len(n.s):]) - return n, append(params, path[len(n.s):]) - } - - continue - } - - return child, childParamNames - } - return nil, nil -} - -// childLen returns all the children's and their children's length. -func (n *node) childLen() (i int) { - for _, n := range n.childrenNodes { - i++ - i += n.childLen() - } - return -} - -func (n *node) isDynamic() bool { - return n.s == ":" || n.wildcardParamName != "" || n.rootWildcard -} - -// prioritize sets the static paths first. -func (nodes Nodes) prioritize() { - sort.Slice(nodes, func(i, j int) bool { - if nodes[i].isDynamic() { - return false - } - if nodes[j].isDynamic() { - return true - } - - return nodes[i].childLen() > nodes[j].childLen() - }) - - for _, n := range nodes { - n.childrenNodes.prioritize() - } -} diff --git a/core/router/party.go b/core/router/party.go index 8e8b56bd83..5d462392f6 100644 --- a/core/router/party.go +++ b/core/router/party.go @@ -3,7 +3,7 @@ package router import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/errors" - "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/macro" ) // Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. @@ -18,11 +18,11 @@ type Party interface { GetRelPath() string // GetReporter returns the reporter for adding errors GetReporter() *errors.Reporter - // Macros returns the macro map which is responsible - // to register custom macro functions for all routes. + // Macros returns the macro collection that is responsible + // to register custom macros with their own parameter types and their macro functions for all routes. // // Learn more at: https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path - Macros() *macro.Map + Macros() *macro.Macros // Party groups routes which may have the same prefix and share same handlers, // returns that new rich subrouter. @@ -110,10 +110,10 @@ type Party interface { // otherwise use `Party` which can handle many paths with different handlers and middlewares. // // Usage: - // app.HandleMany(iris.MethodGet, "/user /user/{id:int} /user/me", userHandler) + // app.HandleMany(iris.MethodGet, "/user /user/{id:uint64} /user/me", userHandler) // At the other side, with `Handle` we've had to write: // app.Handle(iris.MethodGet, "/user", userHandler) - // app.Handle(iris.MethodGet, "/user/{id:int}", userHandler) + // app.Handle(iris.MethodGet, "/user/{id:uint64}", userHandler) // app.Handle(iris.MethodGet, "/user/me", userHandler) // // This method is used behind the scenes at the `Controller` function diff --git a/core/router/path.go b/core/router/path.go index 3a471f9023..0a8b4014b7 100644 --- a/core/router/path.go +++ b/core/router/path.go @@ -7,15 +7,9 @@ import ( "strings" "github.com/kataras/iris/core/netutil" - "github.com/kataras/iris/core/router/macro/interpreter/lexer" -) - -const ( - // ParamStart the character in string representation where the underline router starts its dynamic named parameter. - ParamStart = ":" - // WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard - // path parameter. - WildcardParamStart = "*" + "github.com/kataras/iris/macro" + "github.com/kataras/iris/macro/interpreter/ast" + "github.com/kataras/iris/macro/interpreter/lexer" ) // Param receives a parameter name prefixed with the ParamStart symbol. @@ -31,6 +25,26 @@ func WildcardParam(name string) string { return prefix(name, WildcardParamStart) } +func convertMacroTmplToNodePath(tmpl macro.Template) string { + routePath := tmpl.Src + if len(routePath) > 1 && routePath[len(routePath)-1] == '/' { + routePath = routePath[0 : len(routePath)-1] // remove any last "/" + } + + // if it has started with {} and it's valid + // then the tmpl.Params will be filled, + // so no any further check needed. + for _, p := range tmpl.Params { + if ast.IsTrailing(p.Type) { + routePath = strings.Replace(routePath, p.Src, WildcardParam(p.Name), 1) + } else { + routePath = strings.Replace(routePath, p.Src, Param(p.Name), 1) + } + } + + return routePath +} + func prefix(s string, prefix string) string { if !strings.HasPrefix(s, prefix) { return prefix + s diff --git a/core/router/path_test.go b/core/router/path_test.go index 66ef283bac..2c26c6da54 100644 --- a/core/router/path_test.go +++ b/core/router/path_test.go @@ -27,8 +27,8 @@ func TestCleanPath(t *testing.T) { "/total/{year:string regexp(\\d{4})}/more/{s:string regexp(\\d{7})}"}, {"/single_no_params", "/single_no_params"}, - {"/single/{id:int}", - "/single/{id:int}"}, + {"/single/{id:uint64}", + "/single/{id:uint64}"}, } for i, tt := range tests { @@ -45,8 +45,10 @@ func TestSplitPath(t *testing.T) { }{ {"/v2/stores/{id:string format(uuid)} /v3", []string{"/v2/stores/{id:string format(uuid)}", "/v3"}}, - {"/user/{id:int} /admin/{id:int}", - []string{"/user/{id:int}", "/admin/{id:int}"}}, + {"/user/{id:uint64} /admin/{id:uint64}", + []string{"/user/{id:uint64}", "/admin/{id:uint64}"}}, + {"/users/{id:int} /admins/{id:int64}", + []string{"/users/{id:int}", "/admins/{id:int64}"}}, {"/user /admin", []string{"/user", "/admin"}}, {"/single_no_params", diff --git a/core/router/route.go b/core/router/route.go index 6c4918f157..132eb7c728 100644 --- a/core/router/route.go +++ b/core/router/route.go @@ -5,18 +5,19 @@ import ( "strings" "github.com/kataras/iris/context" - "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/macro" + "github.com/kataras/iris/macro/handler" ) // Route contains the information about a registered Route. // If any of the following fields are changed then the // caller should Refresh the router. type Route struct { - Name string `json:"name"` // "userRoute" - Method string `json:"method"` // "GET" - methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. - Subdomain string `json:"subdomain"` // "admin." - tmpl *macro.Template // Tmpl().Src: "/api/user/{id:int}" + Name string `json:"name"` // "userRoute" + Method string `json:"method"` // "GET" + methodBckp string // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one. + Subdomain string `json:"subdomain"` // "admin." + tmpl macro.Template // Tmpl().Src: "/api/user/{id:uint64}" // temp storage, they're appended to the Handlers on build. // Execution happens before Handlers, can be empty. beginHandlers context.Handlers @@ -39,16 +40,19 @@ type Route struct { // It parses the path based on the "macros", // handlers are being changed to validate the macros at serve time, if needed. func NewRoute(method, subdomain, unparsedPath, mainHandlerName string, - handlers context.Handlers, macros *macro.Map) (*Route, error) { + handlers context.Handlers, macros macro.Macros) (*Route, error) { tmpl, err := macro.Parse(unparsedPath, macros) if err != nil { return nil, err } - path, handlers, err := compileRoutePathAndHandlers(handlers, tmpl) - if err != nil { - return nil, err + path := convertMacroTmplToNodePath(tmpl) + // prepend the macro handler to the route, now, + // right before the register to the tree, so APIBuilder#UseGlobal will work as expected. + if handler.CanMakeHandler(tmpl) { + macroEvaluatorHandler := handler.MakeHandler(tmpl) + handlers = append(context.Handlers{macroEvaluatorHandler}, handlers...) } path = cleanPath(path) // maybe unnecessary here but who cares in this moment @@ -152,7 +156,18 @@ func (r Route) String() string { // via Tmpl().Src, Route.Path is the path // converted to match the underline router's specs. func (r Route) Tmpl() macro.Template { - return *r.tmpl + return r.tmpl +} + +// RegisteredHandlersLen returns the end-developer's registered handlers, all except the macro evaluator handler +// if was required by the build process. +func (r Route) RegisteredHandlersLen() int { + n := len(r.Handlers) + if handler.CanMakeHandler(r.tmpl) { + n-- + } + + return n } // IsOnline returns true if the route is marked as "online" (state). @@ -198,7 +213,7 @@ func formatPath(path string) string { // StaticPath returns the static part of the original, registered route path. // if /user/{id} it will return /user -// if /user/{id}/friend/{friendid:int} it will return /user too +// if /user/{id}/friend/{friendid:uint64} it will return /user too // if /assets/{filepath:path} it will return /assets. func (r Route) StaticPath() string { src := r.tmpl.Src @@ -242,7 +257,8 @@ func (r Route) Trace() string { printfmt += fmt.Sprintf(" %s", r.Subdomain) } printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src) - if l := len(r.Handlers); l > 1 { + + if l := r.RegisteredHandlersLen(); l > 1 { printfmt += fmt.Sprintf("-> %s() and %d more", r.MainHandlerName, l-1) } else { printfmt += fmt.Sprintf("-> %s()", r.MainHandlerName) diff --git a/core/router/router.go b/core/router/router.go index 50526395b5..f4e9840d76 100644 --- a/core/router/router.go +++ b/core/router/router.go @@ -31,7 +31,7 @@ func NewRouter() *Router { return &Router{} } // RefreshRouter re-builds the router. Should be called when a route's state // changed (i.e Method changed at serve-time). func (router *Router) RefreshRouter() error { - return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider) + return router.BuildRouter(router.cPool, router.requestHandler, router.routesProvider, true) } // BuildRouter builds the router based on @@ -41,7 +41,7 @@ func (router *Router) RefreshRouter() error { // its wrapper. // // Use of RefreshRouter to re-build the router if needed. -func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider) error { +func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHandler, routesProvider RoutesProvider, force bool) error { if requestHandler == nil { return errors.New("router: request handler is nil") @@ -60,9 +60,23 @@ func (router *Router) BuildRouter(cPool *context.Pool, requestHandler RequestHan defer router.mu.Unlock() // store these for RefreshRouter's needs. - router.cPool = cPool - router.requestHandler = requestHandler - router.routesProvider = routesProvider + if force { + router.cPool = cPool + router.requestHandler = requestHandler + router.routesProvider = routesProvider + } else { + if router.cPool == nil { + router.cPool = cPool + } + + if router.requestHandler == nil { + router.requestHandler = requestHandler + } + + if router.routesProvider == nil && routesProvider != nil { + router.routesProvider = routesProvider + } + } // the important router.mainHandler = func(w http.ResponseWriter, r *http.Request) { diff --git a/core/router/router_wildcard_root_test.go b/core/router/router_wildcard_root_test.go index 110192ebeb..c3190ce5a2 100644 --- a/core/router/router_wildcard_root_test.go +++ b/core/router/router_wildcard_root_test.go @@ -122,20 +122,25 @@ func TestRouterWildcardRootMany(t *testing.T) { func TestRouterWildcardRootManyAndRootStatic(t *testing.T) { var tt = []testRoute{ - // all routes will be handlded by "h" because we added wildcard to root, + // routes that may return 404 will be handled by the below route ("h" handler) because we added wildcard to root, // this feature is very important and can remove noumerous of previous hacks on our apps. + // + // Static paths and parameters have priority over wildcard, all three types can be registered in the same path prefix. + // + // Remember, all of those routes are registered don't be tricked by the visual appearance of the below test blocks. {"GET", "/{p:path}", h, []testRouteRequest{ {"GET", "", "/other2almost/some", iris.StatusOK, same_as_request_path}, }}, {"GET", "/static/{p:path}", h, []testRouteRequest{ - {"GET", "", "/static", iris.StatusOK, same_as_request_path}, + {"GET", "", "/static", iris.StatusOK, same_as_request_path}, // HERE<- IF NOT FOUND THEN BACKWARDS TO WILDCARD IF THERE IS ONE, HMM. {"GET", "", "/static/something/here", iris.StatusOK, same_as_request_path}, }}, {"GET", "/", h, []testRouteRequest{ {"GET", "", "/", iris.StatusOK, same_as_request_path}, }}, {"GET", "/other/{paramother:path}", h2, []testRouteRequest{ - {"GET", "", "/other", iris.StatusForbidden, same_as_request_path}, + // OK and not h2 because of the root wildcard. + {"GET", "", "/other", iris.StatusOK, same_as_request_path}, {"GET", "", "/other/wildcard", iris.StatusForbidden, same_as_request_path}, {"GET", "", "/other/wildcard/here", iris.StatusForbidden, same_as_request_path}, }}, @@ -145,6 +150,7 @@ func TestRouterWildcardRootManyAndRootStatic(t *testing.T) { }}, {"GET", "/other2/static", h3, []testRouteRequest{ {"GET", "", "/other2/static", iris.StatusOK, prefix_static_path_following_by_request_path}, + // h2(Forbiddenn) instead of h3 OK because it will be handled by the /other2/{paramothersecond:path}'s handler which gives 403. {"GET", "", "/other2/staticed", iris.StatusForbidden, same_as_request_path}, }}, } @@ -165,6 +171,7 @@ func testTheRoutes(t *testing.T, tests []testRoute, debug bool) { // run the tests for _, tt := range tests { for _, req := range tt.requests { + // t.Logf("req: %s:%s\n", tt.method, tt.path) method := req.method if method == "" { method = tt.method diff --git a/core/router/trie.go b/core/router/trie.go new file mode 100644 index 0000000000..b323ac06d1 --- /dev/null +++ b/core/router/trie.go @@ -0,0 +1,281 @@ +package router + +import ( + "strings" + + "github.com/kataras/iris/context" +) + +const ( + // ParamStart the character in string representation where the underline router starts its dynamic named parameter. + ParamStart = ":" + // WildcardParamStart the character in string representation where the underline router starts its dynamic wildcard + // path parameter. + WildcardParamStart = "*" +) + +// An iris-specific identical version of the https://github.com/kataras/muxie version 1.0.0 released at 15 Oct 2018 +type trieNode struct { + parent *trieNode + + children map[string]*trieNode + hasDynamicChild bool // does one of the children contains a parameter or wildcard? + childNamedParameter bool // is the child a named parameter (single segmnet) + childWildcardParameter bool // or it is a wildcard (can be more than one path segments) ? + paramKeys []string // the param keys without : or *. + end bool // it is a complete node, here we stop and we can say that the node is valid. + key string // if end == true then key is filled with the original value of the insertion's key. + // if key != "" && its parent has childWildcardParameter == true, + // we need it to track the static part for the closest-wildcard's parameter storage. + staticKey string + + // insert data. + Handlers context.Handlers + RouteName string +} + +func newTrieNode() *trieNode { + n := new(trieNode) + return n +} + +func (tn *trieNode) hasChild(s string) bool { + return tn.getChild(s) != nil +} + +func (tn *trieNode) getChild(s string) *trieNode { + if tn.children == nil { + return nil + } + + return tn.children[s] +} + +func (tn *trieNode) addChild(s string, n *trieNode) { + if tn.children == nil { + tn.children = make(map[string]*trieNode) + } + + if _, exists := tn.children[s]; exists { + return + } + + n.parent = tn + tn.children[s] = n +} + +func (tn *trieNode) findClosestParentWildcardNode() *trieNode { + tn = tn.parent + for tn != nil { + if tn.childWildcardParameter { + return tn.getChild(WildcardParamStart) + } + + tn = tn.parent + } + + return nil +} + +func (tn *trieNode) String() string { + return tn.key +} + +type trie struct { + root *trieNode + + // if true then it will handle any path if not other parent wildcard exists, + // so even 404 (on http services) is up to it, see trie#insert. + hasRootWildcard bool + hasRootSlash bool + + method string + // subdomain is empty for default-hostname routes, + // ex: mysubdomain. + subdomain string +} + +func newTrie() *trie { + return &trie{ + root: newTrieNode(), + } +} + +const ( + pathSep = "/" + pathSepB = '/' +) + +func slowPathSplit(path string) []string { + if path == "/" { + return []string{"/"} + } + + return strings.Split(path, pathSep)[1:] +} + +func (tr *trie) insert(path, routeName string, handlers context.Handlers) { + input := slowPathSplit(path) + + n := tr.root + if path == pathSep { + tr.hasRootSlash = true + } + + var paramKeys []string + + for _, s := range input { + c := s[0] + + if isParam, isWildcard := c == ParamStart[0], c == WildcardParamStart[0]; isParam || isWildcard { + n.hasDynamicChild = true + paramKeys = append(paramKeys, s[1:]) // without : or *. + + // if node has already a wildcard, don't force a value, check for true only. + if isParam { + n.childNamedParameter = true + s = ParamStart + } + + if isWildcard { + n.childWildcardParameter = true + s = WildcardParamStart + if tr.root == n { + tr.hasRootWildcard = true + } + } + } + + if !n.hasChild(s) { + child := newTrieNode() + n.addChild(s, child) + } + + n = n.getChild(s) + } + + n.RouteName = routeName + n.Handlers = handlers + n.paramKeys = paramKeys + n.key = path + n.end = true + + i := strings.Index(path, ParamStart) + if i == -1 { + i = strings.Index(path, WildcardParamStart) + } + if i == -1 { + i = len(n.key) + } + + n.staticKey = path[:i] +} + +func (tr *trie) search(q string, params *context.RequestParams) *trieNode { + end := len(q) + + if end == 0 || (end == 1 && q[0] == pathSepB) { + // fixes only root wildcard but no / registered at. + if tr.hasRootSlash { + return tr.root.getChild(pathSep) + } else if tr.hasRootWildcard { + // no need to going through setting parameters, this one has not but it is wildcard. + return tr.root.getChild(WildcardParamStart) + } + + return nil + } + + n := tr.root + start := 1 + i := 1 + var paramValues []string + + for { + if i == end || q[i] == pathSepB { + if child := n.getChild(q[start:i]); child != nil { + n = child + } else if n.childNamedParameter { + n = n.getChild(ParamStart) + if ln := len(paramValues); cap(paramValues) > ln { + paramValues = paramValues[:ln+1] + paramValues[ln] = q[start:i] + } else { + paramValues = append(paramValues, q[start:i]) + } + } else if n.childWildcardParameter { + n = n.getChild(WildcardParamStart) + if ln := len(paramValues); cap(paramValues) > ln { + paramValues = paramValues[:ln+1] + paramValues[ln] = q[start:] + } else { + paramValues = append(paramValues, q[start:]) + } + break + } else { + n = n.findClosestParentWildcardNode() + if n != nil { + // means that it has :param/static and *wildcard, we go trhough the :param + // but the next path segment is not the /static, so go back to *wildcard + // instead of not found. + // + // Fixes: + // /hello/*p + // /hello/:p1/static/:p2 + // req: http://localhost:8080/hello/dsadsa/static/dsadsa => found + // req: http://localhost:8080/hello/dsadsa => but not found! + // and + // /second/wild/*p + // /second/wild/static/otherstatic/ + // req: /second/wild/static/otherstatic/random => but not found! + params.Set(n.paramKeys[0], q[len(n.staticKey):]) + return n + } + + return nil + } + + if i == end { + break + } + + i++ + start = i + continue + } + + i++ + } + + if n == nil || !n.end { + if n != nil { // we need it on both places, on last segment (below) or on the first unnknown (above). + if n = n.findClosestParentWildcardNode(); n != nil { + params.Set(n.paramKeys[0], q[len(n.staticKey):]) + return n + } + } + + if tr.hasRootWildcard { + // that's the case for root wildcard, tests are passing + // even without it but stick with it for reference. + // Note ote that something like: + // Routes: /other2/*myparam and /other2/static + // Reqs: /other2/staticed will be handled + // the /other2/*myparam and not the root wildcard, which is what we want. + // + n = tr.root.getChild(WildcardParamStart) + params.Set(n.paramKeys[0], q[1:]) + return n + } + + return nil + } + + for i, paramValue := range paramValues { + if len(n.paramKeys) > i { + params.Set(n.paramKeys[i], paramValue) + } + } + + return n +} diff --git a/deprecated.go b/deprecated.go deleted file mode 100644 index d44707b25f..0000000000 --- a/deprecated.go +++ /dev/null @@ -1 +0,0 @@ -package iris diff --git a/doc.go b/doc.go index 00a781aef7..8f47337342 100644 --- a/doc.go +++ b/doc.go @@ -35,11 +35,11 @@ Source code and other details for the project are available at GitHub: Current Version -10.7.0 +11.1.1 Installation -The only requirement is the Go Programming Language, at least version 1.8 but 1.10 and above is highly recommended. +The only requirement is the Go Programming Language, at least version 1.8 but 1.11.1 and above is highly recommended. $ go get -u github.com/kataras/iris @@ -119,7 +119,7 @@ Example code: usersRoutes := app.Party("/users", logThisMiddleware) { // Method GET: http://localhost:8080/users/42 - usersRoutes.Get("/{id:int min(1)}", getUserByID) + usersRoutes.Get("/{id:uint64 min(1)}", getUserByID) // Method POST: http://localhost:8080/users/create usersRoutes.Post("/create", createUser) } @@ -146,7 +146,7 @@ Example code: } func getUserByID(ctx iris.Context) { - userID := ctx.Params().Get("id") // Or convert directly using: .Values().GetInt/GetInt64 etc... + userID := ctx.Params().Get("id") // Or convert directly using: .Values().GetInt/GetUint64/GetInt64 etc... // your own db fetch here instead of user :=... user := User{Username: "username" + userID} @@ -489,7 +489,7 @@ Example code: users := app.Party("/users", myAuthMiddlewareHandler) // http://myhost.com/users/42/profile - users.Get("/{id:int}/profile", userProfileHandler) + users.Get("/{id:uint64}/profile", userProfileHandler) // http://myhost.com/users/messages/1 users.Get("/inbox/{id:int}", userMessageHandler) @@ -548,8 +548,8 @@ Example code: app.Get("/donate", donateHandler, donateFinishHandler) // Pssst, don't forget dynamic-path example for more "magic"! - app.Get("/api/users/{userid:int min(1)}", func(ctx iris.Context) { - userID, err := ctx.Params().GetInt("userid") + app.Get("/api/users/{userid:uint64 min(1)}", func(ctx iris.Context) { + userID, err := ctx.Params().GetUint64("userid") if err != nil { ctx.Writef("error while trying to parse userid parameter," + @@ -622,8 +622,8 @@ Example code: ctx.Writef("All users") }) // http://v1.localhost:8080/api/users/42 - usersAPI.Get("/{userid:int}", func(ctx iris.Context) { - ctx.Writef("user with id: %s", ctx.Params().Get("userid")) + usersAPI.Get("/{userid:uint64}", func(ctx iris.Context) { + ctx.Writef("user with id: %s", ctx.Params().GetUint64("userid")) }) } } @@ -709,23 +709,71 @@ Standard macro types for parameters: | {param:string} | +------------------------+ string type - anything + anything (single path segmnent) + + +-------------------------------+ + | {param:int} | + +-------------------------------+ + int type + -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch +------------------------+ - | {param:int} | + | {param:int8} | +------------------------+ - int type - only numbers (0-9) + int8 type + -128 to 127 +------------------------+ - | {param:long} | + | {param:int16} | + +------------------------+ + int16 type + -32768 to 32767 + + +------------------------+ + | {param:int32} | + +------------------------+ + int32 type + -2147483648 to 2147483647 + + +------------------------+ + | {param:int64} | +------------------------+ int64 type - only numbers (0-9) + -9223372036854775808 to 9223372036854775807 + + +------------------------+ + | {param:uint} | + +------------------------+ + uint type + 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32) +------------------------+ - | {param:boolean} | + | {param:uint8} | +------------------------+ + uint8 type + 0 to 255 + + +------------------------+ + | {param:uint16} | + +------------------------+ + uint16 type + 0 to 65535 + + +------------------------+ + | {param:uint32} | + +------------------------+ + uint32 type + 0 to 4294967295 + + +------------------------+ + | {param:uint64} | + +------------------------+ + uint64 type + 0 to 18446744073709551615 + + +---------------------------------+ + | {param:bool} or {param:boolean} | + +---------------------------------+ bool type only "1" or "t" or "T" or "TRUE" or "true" or "True" or "0" or "f" or "F" or "FALSE" or "false" or "False" @@ -751,8 +799,8 @@ Standard macro types for parameters: | {param:path} | +------------------------+ path type - anything, should be the last part, more than one path segment, - i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3" + anything, should be the last part, can be more than one path segment, + i.e: "/test/{param:path}" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3" if type is missing then parameter's type is defaulted to string, so {param} == {param:string}. @@ -770,16 +818,18 @@ you are able to register your own too!. Register a named path parameter function: - app.Macros().Int.RegisterFunc("min", func(argument int) func(paramValue string) bool { - [...] - return true/false -> true means valid. + app.Macros().Get("int").RegisterFunc("min", func(argument int) func(paramValue int) bool { + return func(paramValue int) bool { + [...] + return true/false -> true means valid. + } }) at the func(argument ...) you can have any standard type, it will be validated before the server starts so don't care about performance here, the only thing it runs at serve time is the returning func(paramValue string) bool. {param:string equal(iris)} , "iris" will be the argument here: - app.Macros().String.RegisterFunc("equal", func(argument string) func(paramValue string) bool { + app.Macros().Get("string").RegisterFunc("equal", func(argument string) func(paramValue string) bool { return func(paramValue string){ return argument == paramValue } }) @@ -795,38 +845,34 @@ Example Code: // Let's register our first macro attached to int macro type. // "min" = the function // "minValue" = the argument of the function - // func(string) bool = the macro's path parameter evaluator, this executes in serve time when - // a user requests a path which contains the :int macro type with the min(...) macro parameter function. - app.Macros().Int.RegisterFunc("min", func(minValue int) func(string) bool { + // func() bool = the macro's path parameter evaluator, this executes in serve time when + // a user requests a path which contains the int macro type with the min(...) macro parameter function. + app.Macros().Get("int").RegisterFunc("min", func(minValue int) func(int) bool { // do anything before serve here [...] // at this case we don't need to do anything - return func(paramValue string) bool { - n, err := strconv.Atoi(paramValue) - if err != nil { - return false - } - return n >= minValue + return func(paramValue int) bool { + return paramValue >= minValue } }) // http://localhost:8080/profile/id>=1 // this will throw 404 even if it's found as route on : /profile/0, /profile/blabla, /profile/-1 // macro parameter functions are optional of course. - app.Get("/profile/{id:int min(1)}", func(ctx iris.Context) { + app.Get("/profile/{id:uint64 min(1)}", func(ctx iris.Context) { // second parameter is the error but it will always nil because we use macros, // the validaton already happened. - id, _ := ctx.Params().GetInt("id") + id, _ := ctx.Params().GetUint64("id") ctx.Writef("Hello id: %d", id) }) // to change the error code per route's macro evaluator: - app.Get("/profile/{id:int min(1)}/friends/{friendid:int min(1) else 504}", func(ctx iris.Context) { - id, _ := ctx.Params().GetInt("id") - friendid, _ := ctx.Params().GetInt("friendid") + app.Get("/profile/{id:uint64 min(1)}/friends/{friendid:uint64 min(1) else 504}", func(ctx iris.Context) { + id, _ := ctx.Params().GetUint64("id") + friendid, _ := ctx.Params().GetUint64("friendid") ctx.Writef("Hello id: %d looking for friend id: ", id, friendid) }) // this will throw e 504 error code instead of 404 if all route's macros not passed. - // http://localhost:8080/game/a-zA-Z/level/0-9 + // http://localhost:8080/game/a-zA-Z/level/42 // remember, alphabetical is lowercase or uppercase letters only. app.Get("/game/{name:alphabetical}/level/{level:int}", func(ctx iris.Context) { ctx.Writef("name: %s | level: %s", ctx.Params().Get("name"), ctx.Params().Get("level")) @@ -855,11 +901,6 @@ Example Code: } - -A path parameter name should contain only alphabetical letters, symbols, containing '_' and numbers are NOT allowed. -If route failed to be registered, the app will panic without any warnings -if you didn't catch the second return value(error) on .Handle/.Get.... - Last, do not confuse ctx.Values() with ctx.Params(). Path parameter's values goes to ctx.Params() and context's local storage that can be used to communicate between handlers and middleware(s) goes to diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000..a78a558205 --- /dev/null +++ b/go.mod @@ -0,0 +1,57 @@ +module github.com/kataras/iris + +require ( + github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7 // indirect + github.com/BurntSushi/toml v0.3.1 + github.com/Joker/jade v1.0.0 + github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 + github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f // indirect + github.com/aymerick/raymond v2.0.2+incompatible + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/badger v1.5.4 + github.com/dgryski/go-farm v0.0.0-20180109070241-2de33835d102 // indirect + github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 + github.com/etcd-io/bbolt v1.3.0 + github.com/fatih/structs v1.1.0 + github.com/flosch/pongo2 v0.0.0-20180809100617-24195e6d38b0 + github.com/gavv/monotime v0.0.0-20171021193802-6f8212e8d10d // indirect + github.com/golang/protobuf v1.2.0 // indirect + github.com/gomodule/redigo v2.0.0+incompatible + github.com/google/go-querystring v1.0.0 // indirect + github.com/gorilla/websocket v1.4.0 + github.com/hashicorp/go-version v1.0.0 + github.com/imkira/go-interpol v1.1.0 // indirect + github.com/iris-contrib/blackfriday v2.0.0+incompatible + github.com/iris-contrib/formBinder v0.0.0-20190104093907-fbd5963f41e1 // indirect + github.com/iris-contrib/go.uuid v2.0.0+incompatible + github.com/iris-contrib/httpexpect v0.0.0-20180314041918-ebe99fcebbce + github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0 + github.com/json-iterator/go v1.1.5 + github.com/juju/errors v0.0.0-20181012004132-a4583d0a56ea // indirect + github.com/kataras/golog v0.0.0-20180321173939-03be10146386 // indirect + github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect + github.com/klauspost/compress v1.4.1 + github.com/klauspost/cpuid v1.2.0 // indirect + github.com/microcosm-cc/bluemonday v1.0.1 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/moul/http2curl v1.0.0 // indirect + github.com/pkg/errors v0.8.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/ryanuber/columnize v2.1.0+incompatible + github.com/sergi/go-diff v1.0.0 // indirect + github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect + github.com/stretchr/testify v1.2.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56 // indirect + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect + github.com/yudai/gojsondiff v1.0.0 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 + golang.org/x/net v0.0.0-20181114220301-adae6a3d119a // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/ini.v1 v1.39.0 // indirect + gopkg.in/yaml.v2 v2.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000..5c1b160566 --- /dev/null +++ b/go.sum @@ -0,0 +1,82 @@ +github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Joker/hpp v0.0.0-20180418125244-6893e659854a/go.mod h1:MzD2WMdSxvbHw5fM/OXOFily/lipJWRc9C1px0Mt0ZE= +github.com/Joker/jade v1.0.0 h1:lOCEPvTAtWfLpSZYMOv/g44MGQFAolbKh2khHHGu0Kc= +github.com/Joker/jade v1.0.0/go.mod h1:efZIdO0py/LtcJRSa/j2WEklMSAw84WV0zZVMxNToB8= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 h1:WDC6ySpJzbxGWFh4aMxFFC28wwGp5pEuoTtvA4q/qQ4= +github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= +github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/aymerick/raymond v2.0.2+incompatible h1:VEp3GpgdAnv9B2GFyTvqgcKvY+mfKMjPOA3SbKLtnU0= +github.com/aymerick/raymond v2.0.2+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/badger v1.5.4/go.mod h1:VZxzAIRPHRVNRKRo6AXrX9BJegn6il06VMTZVJYCIjQ= +github.com/dgryski/go-farm v0.0.0-20180109070241-2de33835d102/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 h1:clC1lXBpe2kTj2VHdaIu9ajZQe4kcEY9j0NsnDDBZ3o= +github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM= +github.com/etcd-io/bbolt v1.3.0/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2 v0.0.0-20180809100617-24195e6d38b0 h1:ZHx2BEERvWkuwuE7qWN9TuRxucHDH2JrsvneZjVJfo0= +github.com/flosch/pongo2 v0.0.0-20180809100617-24195e6d38b0/go.mod h1:rE0ErqqBaMcp9pzj8JxV1GcfDBpuypXYxlR1c37AUwg= +github.com/gavv/monotime v0.0.0-20171021193802-6f8212e8d10d/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/iris-contrib/blackfriday v2.0.0+incompatible h1:o5sHQHHm0ToHUlAJSTjW9UWicjJSDDauOOQ2AHuIVp4= +github.com/iris-contrib/blackfriday v2.0.0+incompatible/go.mod h1:UzZ2bDEoaSGPbkg6SAB4att1aAwTmVIx/5gCVqeyUdI= +github.com/iris-contrib/formBinder v0.0.0-20190104093907-fbd5963f41e1 h1:7GsNnSLoVceNylMpwcfy5aFNz/S5/TV25crb34I5PEo= +github.com/iris-contrib/formBinder v0.0.0-20190104093907-fbd5963f41e1/go.mod h1:i8kTYUOEstd/S8TG0ChTXQdf4ermA/e8vJX0+QruD9w= +github.com/iris-contrib/go.uuid v2.0.0+incompatible h1:XZubAYg61/JwnJNbZilGjf3b3pB80+OQg2qf6c8BfWE= +github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/+fafWORmlnuysV2EMP8MW+qe0= +github.com/iris-contrib/httpexpect v0.0.0-20180314041918-ebe99fcebbce/go.mod h1:VER17o2JZqquOx41avolD/wMGQSFEFBKWmhag9/RQRY= +github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62A0xJL6I+umB2YTlFRwWXaDFA0jy+5HzGiJjqI= +github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/juju/errors v0.0.0-20181012004132-a4583d0a56ea h1:g2k+8WR7cHch4g0tBDhfiEvAp7fXxTNBiD1oC1Oxj3E= +github.com/juju/errors v0.0.0-20181012004132-a4583d0a56ea/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= +github.com/kataras/golog v0.0.0-20180321173939-03be10146386 h1:VT6AeCHO/mc+VedKBMhoqb5eAK8B1i9F6nZl7EGlHvA= +github.com/kataras/golog v0.0.0-20180321173939-03be10146386/go.mod h1:PcaEvfvhGsqwXZ6S3CgCbmjcp+4UDUh2MIfF2ZEul8M= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d h1:V5Rs9ztEWdp58oayPq/ulmlqJJZeJP6pP79uP3qjcao= +github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d/go.mod h1:NV88laa9UiiDuX9AhMbDPkGYSPugBOV6yTZB1l2K9Z0= +github.com/klauspost/compress v1.4.1 h1:8VMb5+0wMgdBykOV96DwNwKFQ+WTI4pzYURP99CcB9E= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUWQxHW5TkBe7YUl+2s= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869 h1:kkXA53yGe04D0adEYJwEVQjeBppL01Exg+fnMjfUraU= +golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.39.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hero/di.go b/hero/di.go index 4244cfa0a9..d50665145b 100644 --- a/hero/di.go +++ b/hero/di.go @@ -8,6 +8,17 @@ import ( func init() { di.DefaultHijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { + // if IsExpectingStore(fieldOrFuncInput) { + // return &di.BindObject{ + // Type: memstoreTyp, + // BindType: di.Dynamic, + // ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + // // return ctxValue[0].MethodByName("Params").Call(di.EmptyIn)[0] + // return ctxValue[0].MethodByName("Params").Call(di.EmptyIn)[0].Field(0) // the Params' memstore.Store. + // }, + // }, true + // } + if !IsContext(fieldOrFuncInput) { return nil, false } diff --git a/hero/di/func.go b/hero/di/func.go index da81d6f320..3c417d8f0c 100644 --- a/hero/di/func.go +++ b/hero/di/func.go @@ -132,9 +132,8 @@ func (s *FuncInjector) addValue(inputIndex int, value reflect.Value) bool { } if b.IsAssignable(inTyp) { - // println(inTyp.String() + " is assignable to " + val.Type().String()) // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", - // i, b.Type.String(), value.String(), val.Pointer()) + // i, b.Type.String(), inTyp.String(), inTyp.Pointer()) s.inputs = append(s.inputs, &targetFuncInput{ InputIndex: inputIndex, Object: &b, @@ -194,8 +193,8 @@ func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { args := *in for _, input := range s.inputs { input.Object.Assign(ctx, func(v reflect.Value) { - // fmt.Printf("assign input index: %d for value: %v\n", - // input.InputIndex, v.String()) + // fmt.Printf("assign input index: %d for value: %v of type: %s\n", + // input.InputIndex, v.String(), v.Type().Name()) args[input.InputIndex] = v }) diff --git a/hero/di/object.go b/hero/di/object.go index 392abcc587..385162bd87 100644 --- a/hero/di/object.go +++ b/hero/di/object.go @@ -101,6 +101,11 @@ func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Val if !v.IsValid() { return zeroOutVal } + // if v.String() == "" { + // println("di/object.go: " + v.String()) + // // println("di/object.go: because it's interface{} it should be returned as: " + v.Elem().Type().String() + " and its value: " + v.Elem().Interface().(string)) + // return v.Elem() + // } return v } diff --git a/hero/di/reflect.go b/hero/di/reflect.go index 3b91ebf8d5..08fc7f2a7a 100644 --- a/hero/di/reflect.go +++ b/hero/di/reflect.go @@ -54,6 +54,7 @@ func IsZero(v reflect.Value) bool { // if can't interface, i.e return value from unexported field or method then return false return false } + zero := reflect.Zero(v.Type()) return v.Interface() == zero.Interface() } @@ -62,7 +63,10 @@ func IsZero(v reflect.Value) bool { // If "v" is a nil pointer, Indirect returns a zero Value. // If "v" is not a pointer, Indirect returns v. func IndirectValue(v reflect.Value) reflect.Value { - return reflect.Indirect(v) + if k := v.Kind(); k == reflect.Ptr { //|| k == reflect.Interface { + return v.Elem() + } + return v } // ValueOf returns the reflect.Value of "o". @@ -123,6 +127,11 @@ func equalTypes(got reflect.Type, expected reflect.Type) bool { // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) return got.Implements(expected) } + + // if got.String() == "interface {}" { + // return true + // } + return false } @@ -161,7 +170,6 @@ func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) for i, n := 0, elemTyp.NumField(); i < n; i++ { f := elemTyp.Field(i) - if IndirectType(f.Type).Kind() == reflect.Struct && !structFieldIgnored(f) { fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...) diff --git a/hero/handler.go b/hero/handler.go index bb427d772c..f0b4704408 100644 --- a/hero/handler.go +++ b/hero/handler.go @@ -5,13 +5,15 @@ import ( "reflect" "runtime" + "github.com/kataras/iris/context" "github.com/kataras/iris/hero/di" "github.com/kataras/golog" - "github.com/kataras/iris/context" ) -var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() +var ( + contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() +) // IsContext returns true if the "inTyp" is a type of Context. func IsContext(inTyp reflect.Type) bool { @@ -70,7 +72,7 @@ func makeHandler(handler interface{}, values ...reflect.Value) (context.Handler, // is invalid when input len and values are not match // or their types are not match, we will take look at the // second statement, here we will re-try it - // using binders for path parameters: string, int, int64, bool. + // using binders for path parameters: string, int, int64, uint8, uint64, bool and so on. // We don't have access to the path, so neither to the macros here, // but in mvc. So we have to do it here. if valid = funcInjector.Retry(new(params).resolve); !valid { diff --git a/hero/param.go b/hero/param.go index 9a9f028f33..6941d5543c 100644 --- a/hero/param.go +++ b/hero/param.go @@ -19,50 +19,8 @@ type params struct { func (p *params) resolve(index int, typ reflect.Type) (reflect.Value, bool) { currentParamIndex := p.next - v, ok := resolveParam(currentParamIndex, typ) + v, ok := context.ParamResolverByTypeAndIndex(typ, currentParamIndex) p.next = p.next + 1 return v, ok } - -func resolveParam(currentParamIndex int, typ reflect.Type) (reflect.Value, bool) { - var fn interface{} - - switch typ.Kind() { - case reflect.Int: - fn = func(ctx context.Context) int { - // the second "ok/found" check is not necessary, - // because even if the entry didn't found on that "index" - // it will return an empty entry which will return the - // default value passed from the xDefault(def) because its `ValueRaw` is nil. - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.IntDefault(0) - return v - } - case reflect.Int64: - fn = func(ctx context.Context) int64 { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.Int64Default(0) - - return v - } - case reflect.Bool: - fn = func(ctx context.Context) bool { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - v, _ := entry.BoolDefault(false) - return v - } - case reflect.String: - fn = func(ctx context.Context) string { - entry, _ := ctx.Params().GetEntryAt(currentParamIndex) - // print(entry.Key + " with index of: ") - // print(currentParamIndex) - // println(" and value: " + entry.String()) - return entry.String() - } - default: - return reflect.Value{}, false - } - - return reflect.ValueOf(fn), true -} diff --git a/httptest/httptest.go b/httptest/httptest.go index b37f97505c..50253c5396 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -84,8 +84,7 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe setter.Set(conf) } - // set the logger or disable it (default) and disable the updater (for any case). - app.Configure(iris.WithoutVersionChecker) + // set the logger or disable it (default). app.Logger().SetLevel(conf.LogLevel) if err := app.Build(); err != nil { diff --git a/iris.go b/iris.go index f84accea30..d2e3e0df9a 100644 --- a/iris.go +++ b/iris.go @@ -17,7 +17,6 @@ import ( // core packages, needed to build the application "github.com/kataras/iris/core/errors" "github.com/kataras/iris/core/host" - "github.com/kataras/iris/core/maintenance" "github.com/kataras/iris/core/netutil" "github.com/kataras/iris/core/router" // handlerconv conversions @@ -34,7 +33,7 @@ import ( var ( // Version is the current version number of the Iris Web Framework. - Version = maintenance.Version + Version = "11.1.1" ) // HTTP status codes as registered with IANA. @@ -58,13 +57,13 @@ const ( StatusAlreadyReported = 208 // RFC 5842, 7.1 StatusIMUsed = 226 // RFC 3229, 10.4.1 - StatusMultipleChoices = 300 // RFC 7231, 6.4.1 - StatusMovedPermanently = 301 // RFC 7231, 6.4.2 - StatusFound = 302 // RFC 7231, 6.4.3 - StatusSeeOther = 303 // RFC 7231, 6.4.4 - StatusNotModified = 304 // RFC 7232, 4.1 - StatusUseProxy = 305 // RFC 7231, 6.4.5 - _ = 306 // RFC 7231, 6.4.6 (Unused) + StatusMultipleChoices = 300 // RFC 7231, 6.4.1 + StatusMovedPermanently = 301 // RFC 7231, 6.4.2 + StatusFound = 302 // RFC 7231, 6.4.3 + StatusSeeOther = 303 // RFC 7231, 6.4.4 + StatusNotModified = 304 // RFC 7232, 4.1 + StatusUseProxy = 305 // RFC 7231, 6.4.5 + StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 StatusPermanentRedirect = 308 // RFC 7538, 3 @@ -87,9 +86,11 @@ const ( StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 StatusExpectationFailed = 417 // RFC 7231, 6.5.14 StatusTeapot = 418 // RFC 7168, 2.3.3 + StatusMisdirectedRequest = 421 // RFC 7540, 9.1.2 StatusUnprocessableEntity = 422 // RFC 4918, 11.2 StatusLocked = 423 // RFC 4918, 11.3 StatusFailedDependency = 424 // RFC 4918, 11.4 + StatusTooEarly = 425 // RFC 8470, 5.2. StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 StatusPreconditionRequired = 428 // RFC 6585, 3 StatusTooManyRequests = 429 // RFC 6585, 4 @@ -462,6 +463,12 @@ var ( // // A shortcut for the `context#CookieDecode`. CookieDecode = context.CookieDecode + // IsErrPath can be used at `context#ReadForm`. + // It reports whether the incoming error is type of `formbinder.ErrPath`, + // which can be ignored when server allows unknown post values to be sent by the client. + // + // A shortcut for the `context#IsErrPath`. + IsErrPath = context.IsErrPath ) // SPA accepts an "assetHandler" which can be the result of an @@ -761,7 +768,7 @@ func (app *Application) Build() error { // create the request handler, the default routing handler routerHandler := router.NewDefaultHandler() - rp.Describe("router: %v", app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder)) + rp.Describe("router: %v", app.Router.BuildRouter(app.ContextPool, routerHandler, app.APIBuilder, false)) // re-build of the router from outside can be done with; // app.RefreshRouter() } @@ -810,10 +817,6 @@ func (app *Application) Run(serve Runner, withOrWithout ...Configurator) error { app.Configure(withOrWithout...) app.logger.Debugf("Application: running using %d host(s)", len(app.Hosts)+1) - if !app.config.DisableVersionChecker { - go maintenance.Start() - } - // this will block until an error(unless supervisor's DeferFlow called from a Task). err := serve(app) if err != nil { diff --git a/macro/AUTHORS b/macro/AUTHORS new file mode 100644 index 0000000000..0476475022 --- /dev/null +++ b/macro/AUTHORS @@ -0,0 +1,4 @@ +# This is the official list of Iris Macro and Route path interpreter authors for copyright +# purposes. + +Gerasimos Maropoulos diff --git a/macro/LICENSE b/macro/LICENSE new file mode 100644 index 0000000000..c73df4cefa --- /dev/null +++ b/macro/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017-2018 The Iris Macro and Route path interpreter. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Iris nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/macro/handler/handler.go b/macro/handler/handler.go new file mode 100644 index 0000000000..ea36c7d566 --- /dev/null +++ b/macro/handler/handler.go @@ -0,0 +1,56 @@ +// Package handler is the highest level module of the macro package which makes use the rest of the macro package, +// it is mainly used, internally, by the router package. +package handler + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/macro" +) + +// CanMakeHandler reports whether a macro template needs a special macro's evaluator handler to be validated +// before procceed to the next handler(s). +// If the template does not contain any dynamic attributes and a special handler is NOT required +// then it returns false. +func CanMakeHandler(tmpl macro.Template) (needsMacroHandler bool) { + if len(tmpl.Params) == 0 { + return + } + + // check if we have params like: {name:string} or {name} or {anything:path} without else keyword or any functions used inside these params. + // 1. if we don't have, then we don't need to add a handler before the main route's handler (as I said, no performance if macro is not really used) + // 2. if we don't have any named params then we don't need a handler too. + for _, p := range tmpl.Params { + if p.CanEval() { + // if at least one needs it, then create the handler. + needsMacroHandler = true + break + } + } + + return +} + +// MakeHandler creates and returns a handler from a macro template, the handler evaluates each of the parameters if necessary at all. +// 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 { + if !CanMakeHandler(tmpl) { + return nil + } + + return func(ctx context.Context) { + for _, p := range tmpl.Params { + if !p.CanEval() { + continue // allow. + } + + if !p.Eval(ctx.Params().Get(p.Name), &ctx.Params().Store) { + ctx.StatusCode(p.ErrCode) + ctx.StopExecution() + return + } + } + // if all passed, just continue. + ctx.Next() + } +} diff --git a/macro/handler/handler_test.go b/macro/handler/handler_test.go new file mode 100644 index 0000000000..0acc1e830a --- /dev/null +++ b/macro/handler/handler_test.go @@ -0,0 +1,41 @@ +package handler + +import ( + "testing" + + "github.com/kataras/iris/macro" +) + +func TestCanMakeHandler(t *testing.T) { + tests := []struct { + src string + needsHandler bool + }{ + {"/static/static", false}, + {"/{myparam}", false}, + {"/{myparam min(1)}", true}, + {"/{myparam else 500}", true}, + {"/{myparam else 404}", false}, + {"/{myparam:string}/static", false}, + {"/{myparam:int}", true}, + {"/static/{myparam:int}/static", true}, + {"/{myparam:path}", false}, + {"/{myparam:path min(1) else 404}", true}, + } + + availableMacros := *macro.Defaults + for i, tt := range tests { + tmpl, err := macro.Parse(tt.src, availableMacros) + if err != nil { + t.Fatalf("[%d] '%s' failed to be parsed: %v", i, tt.src, err) + } + + if got := CanMakeHandler(tmpl); got != tt.needsHandler { + if tt.needsHandler { + t.Fatalf("[%d] '%s' expected to be able to generate an evaluator handler instead of a nil one", i, tt.src) + } else { + t.Fatalf("[%d] '%s' should not need an evaluator handler", i, tt.src) + } + } + } +} diff --git a/macro/interpreter/ast/ast.go b/macro/interpreter/ast/ast.go new file mode 100644 index 0000000000..a3508373ac --- /dev/null +++ b/macro/interpreter/ast/ast.go @@ -0,0 +1,132 @@ +package ast + +type ( + // ParamType holds the necessary information about a parameter type for the parser to lookup for. + ParamType interface { + // The name of the parameter type. + // Indent should contain the characters for the parser. + Indent() string + } + + // MasterParamType if implemented and its `Master()` returns true then empty type param will be translated to this param type. + // Also its functions will be available to the rest of the macro param type's funcs. + // + // Only one Master is allowed. + MasterParamType interface { + ParamType + Master() bool + } + + // TrailingParamType if implemented and its `Trailing()` returns true + // then it should be declared at the end of a route path and can accept any trailing path segment as one parameter. + TrailingParamType interface { + ParamType + Trailing() bool + } + + // AliasParamType if implemeneted nad its `Alias()` returns a non-empty string + // then the param type can be written with that string literal too. + AliasParamType interface { + ParamType + Alias() string + } +) + +// IsMaster returns true if the "pt" param type is a master one. +func IsMaster(pt ParamType) bool { + p, ok := pt.(MasterParamType) + return ok && p.Master() +} + +// IsTrailing returns true if the "pt" param type is a marked as trailing, +// which should accept more than one path segment when in the end. +func IsTrailing(pt ParamType) bool { + p, ok := pt.(TrailingParamType) + return ok && p.Trailing() +} + +// HasAlias returns any alias of the "pt" param type. +// If alias is empty or not found then it returns false as its second output argument. +func HasAlias(pt ParamType) (string, bool) { + if p, ok := pt.(AliasParamType); ok { + alias := p.Alias() + return alias, len(alias) > 0 + } + + return "", false +} + +// GetMasterParamType accepts a list of ParamType and returns its master. +// If no `Master` specified: +// and len(paramTypes) > 0 then it will return the first one, +// otherwise it returns nil. +func GetMasterParamType(paramTypes ...ParamType) ParamType { + for _, pt := range paramTypes { + if IsMaster(pt) { + return pt + } + } + + if len(paramTypes) > 0 { + return paramTypes[0] + } + + return nil +} + +// LookupParamType accepts the string +// representation of a parameter type. +// Example: +// "string" +// "number" or "int" +// "long" or "int64" +// "uint8" +// "uint64" +// "boolean" or "bool" +// "alphabetical" +// "file" +// "path" +func LookupParamType(indentOrAlias string, paramTypes ...ParamType) (ParamType, bool) { + for _, pt := range paramTypes { + if pt.Indent() == indentOrAlias { + return pt, true + } + + if alias, has := HasAlias(pt); has { + if alias == indentOrAlias { + return pt, true + } + } + } + + return nil, false +} + +// ParamStatement is a struct +// which holds all the necessary information about a macro parameter. +// It holds its type (string, int, alphabetical, file, path), +// its source ({param:type}), +// its name ("param"), +// its attached functions by the user (min, max...) +// and the http error code if that parameter +// failed to be evaluated. +type ParamStatement struct { + Src string // the original unparsed source, i.e: {id:int range(1,5) else 404} + Name string // id + Type ParamType // int + Funcs []ParamFunc // range + ErrorCode int // 404 +} + +// ParamFunc holds the name of a parameter's function +// and its arguments (values) +// A param func is declared with: +// {param:int range(1,5)}, +// the range is the +// param function name +// the 1 and 5 are the two param function arguments +// range(1,5) +type ParamFunc struct { + Name string // range + Args []string // ["1","5"] +} diff --git a/core/router/macro/interpreter/lexer/lexer.go b/macro/interpreter/lexer/lexer.go similarity index 98% rename from core/router/macro/interpreter/lexer/lexer.go rename to macro/interpreter/lexer/lexer.go index 79f7111fc0..6a77260252 100644 --- a/core/router/macro/interpreter/lexer/lexer.go +++ b/macro/interpreter/lexer/lexer.go @@ -1,7 +1,7 @@ package lexer import ( - "github.com/kataras/iris/core/router/macro/interpreter/token" + "github.com/kataras/iris/macro/interpreter/token" ) // Lexer helps us to read/scan characters of a source and resolve their token types. @@ -179,7 +179,7 @@ func (l *Lexer) skipWhitespace() { func (l *Lexer) readIdentifier() string { pos := l.pos - for isLetter(l.ch) { + for isLetter(l.ch) || isDigit(l.ch) { l.readChar() } return l.input[pos:l.pos] diff --git a/core/router/macro/interpreter/lexer/lexer_test.go b/macro/interpreter/lexer/lexer_test.go similarity index 94% rename from core/router/macro/interpreter/lexer/lexer_test.go rename to macro/interpreter/lexer/lexer_test.go index dad919f3f1..848731e0f2 100644 --- a/core/router/macro/interpreter/lexer/lexer_test.go +++ b/macro/interpreter/lexer/lexer_test.go @@ -3,7 +3,7 @@ package lexer import ( "testing" - "github.com/kataras/iris/core/router/macro/interpreter/token" + "github.com/kataras/iris/macro/interpreter/token" ) func TestNextToken(t *testing.T) { diff --git a/core/router/macro/interpreter/parser/parser.go b/macro/interpreter/parser/parser.go similarity index 70% rename from core/router/macro/interpreter/parser/parser.go rename to macro/interpreter/parser/parser.go index 8a352a73f3..1a0b86086e 100644 --- a/core/router/macro/interpreter/parser/parser.go +++ b/macro/interpreter/parser/parser.go @@ -5,15 +5,19 @@ import ( "strconv" "strings" - "github.com/kataras/iris/core/router/macro/interpreter/ast" - "github.com/kataras/iris/core/router/macro/interpreter/lexer" - "github.com/kataras/iris/core/router/macro/interpreter/token" + "github.com/kataras/iris/macro/interpreter/ast" + "github.com/kataras/iris/macro/interpreter/lexer" + "github.com/kataras/iris/macro/interpreter/token" ) // Parse takes a route "fullpath" // and returns its param statements -// and an error on failure. -func Parse(fullpath string) ([]*ast.ParamStatement, error) { +// or an error if failed. +func Parse(fullpath string, paramTypes []ast.ParamType) ([]*ast.ParamStatement, error) { + if len(paramTypes) == 0 { + return nil, fmt.Errorf("empty parameter types") + } + pathParts := strings.SplitN(fullpath, "/", -1) p := new(ParamParser) statements := make([]*ast.ParamStatement, 0) @@ -28,14 +32,14 @@ func Parse(fullpath string) ([]*ast.ParamStatement, error) { } p.Reset(s) - stmt, err := p.Parse() + stmt, err := p.Parse(paramTypes) if err != nil { // exit on first error return nil, err } // if we have param type path but it's not the last path part - if stmt.Type == ast.ParamTypePath && i < len(pathParts)-1 { - return nil, fmt.Errorf("param type 'path' should be lived only inside the last path segment, but was inside: %s", s) + if ast.IsTrailing(stmt.Type) && i < len(pathParts)-1 { + return nil, fmt.Errorf("%s: parameter type \"%s\" should be registered to the very last of a path", s, stmt.Type.Indent()) } statements = append(statements, stmt) @@ -77,15 +81,18 @@ const ( // per-parameter. An error code can be setted via // the "else" keyword inside a route's path. DefaultParamErrorCode = 404 - // DefaultParamType when parameter type is missing use this param type, defaults to string - // and it should be remains unless earth split in two. - DefaultParamType = ast.ParamTypeString ) -func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { - if t.Type == token.INT { - return ast.ParamFuncArgToInt(t.Literal) - } +// func parseParamFuncArg(t token.Token) (a ast.ParamFuncArg, err error) { +// if t.Type == token.INT { +// return ast.ParamFuncArgToInt(t.Literal) +// } +// // act all as strings here, because of int vs int64 vs uint64 and etc. +// return t.Literal, nil +// } + +func parseParamFuncArg(t token.Token) (a string, err error) { + // act all as strings here, because of int vs int64 vs uint64 and etc. return t.Literal, nil } @@ -96,14 +103,14 @@ func (p ParamParser) Error() error { return nil } -// Parse parses the p.src and returns its param statement +// Parse parses the p.src based on the given param types and returns its param statement // and an error on failure. -func (p *ParamParser) Parse() (*ast.ParamStatement, error) { +func (p *ParamParser) Parse(paramTypes []ast.ParamType) (*ast.ParamStatement, error) { l := lexer.New(p.src) stmt := &ast.ParamStatement{ ErrorCode: DefaultParamErrorCode, - Type: DefaultParamType, + Type: ast.GetMasterParamType(paramTypes...), Src: p.src, } @@ -120,14 +127,15 @@ func (p *ParamParser) Parse() (*ast.ParamStatement, error) { switch t.Type { case token.LBRACE: - // name, alphabetical and _, param names are not allowed to contain any number. + // can accept only letter or number only. nextTok := l.NextToken() stmt.Name = nextTok.Literal case token.COLON: - // type + // type can accept both letters and numbers but not symbols ofc. nextTok := l.NextToken() - paramType := ast.LookupParamType(nextTok.Literal) - if paramType == ast.ParamTypeUnExpected { + paramType, found := ast.LookupParamType(nextTok.Literal, paramTypes...) + + if !found { p.appendErr("[%d:%d] unexpected parameter type: %s", t.Start, t.End, nextTok.Literal) } stmt.Type = paramType @@ -143,25 +151,14 @@ func (p *ParamParser) Parse() (*ast.ParamStatement, error) { argValTok := l.NextDynamicToken() // catch anything from "(" and forward, until ")", because we need to // be able to use regex expression as a macro type's func argument too. - argVal, err := parseParamFuncArg(argValTok) - if err != nil { - p.appendErr("[%d:%d] expected param func argument to be a string or number but got %s", t.Start, t.End, argValTok.Literal) - continue - } // fmt.Printf("argValTok: %#v\n", argValTok) // fmt.Printf("argVal: %#v\n", argVal) - lastParamFunc.Args = append(lastParamFunc.Args, argVal) + lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal) case token.COMMA: argValTok := l.NextToken() - argVal, err := parseParamFuncArg(argValTok) - if err != nil { - p.appendErr("[%d:%d] expected param func argument to be a string or number type but got %s", t.Start, t.End, argValTok.Literal) - continue - } - - lastParamFunc.Args = append(lastParamFunc.Args, argVal) + lastParamFunc.Args = append(lastParamFunc.Args, argValTok.Literal) case token.RPAREN: stmt.Funcs = append(stmt.Funcs, lastParamFunc) lastParamFunc = ast.ParamFunc{} // reset diff --git a/core/router/macro/interpreter/parser/parser_test.go b/macro/interpreter/parser/parser_test.go similarity index 56% rename from core/router/macro/interpreter/parser/parser_test.go rename to macro/interpreter/parser/parser_test.go index b1ce0ad85b..5424f67565 100644 --- a/core/router/macro/interpreter/parser/parser_test.go +++ b/macro/interpreter/parser/parser_test.go @@ -6,9 +6,47 @@ import ( "strings" "testing" - "github.com/kataras/iris/core/router/macro/interpreter/ast" + "github.com/kataras/iris/macro/interpreter/ast" ) +type simpleParamType string + +func (pt simpleParamType) Indent() string { return string(pt) } + +type masterParamType simpleParamType + +func (pt masterParamType) Indent() string { return string(pt) } +func (pt masterParamType) Master() bool { return true } + +type wildcardParamType string + +func (pt wildcardParamType) Indent() string { return string(pt) } +func (pt wildcardParamType) Trailing() bool { return true } + +type aliasedParamType []string + +func (pt aliasedParamType) Indent() string { return string(pt[0]) } +func (pt aliasedParamType) Alias() string { return pt[1] } + +var ( + paramTypeString = masterParamType("string") + paramTypeNumber = aliasedParamType{"number", "int"} + paramTypeInt64 = aliasedParamType{"int64", "long"} + paramTypeUint8 = simpleParamType("uint8") + paramTypeUint64 = simpleParamType("uint64") + paramTypeBool = aliasedParamType{"bool", "boolean"} + paramTypeAlphabetical = simpleParamType("alphabetical") + paramTypeFile = simpleParamType("file") + paramTypePath = wildcardParamType("path") +) + +var testParamTypes = []ast.ParamType{ + paramTypeString, + paramTypeNumber, paramTypeInt64, paramTypeUint8, paramTypeUint64, + paramTypeBool, + paramTypeAlphabetical, paramTypeFile, paramTypePath, +} + func TestParseParamError(t *testing.T) { // fail illegalChar := '$' @@ -16,7 +54,7 @@ func TestParseParamError(t *testing.T) { input := "{id" + string(illegalChar) + "int range(1,5) else 404}" p := NewParamParser(input) - _, err := p.Parse() + _, err := p.Parse(testParamTypes) if err == nil { t.Fatalf("expecting not empty error on input '%s'", input) @@ -30,9 +68,9 @@ func TestParseParamError(t *testing.T) { // // success - input2 := "{id:int range(1,5) else 404}" + input2 := "{id:uint64 range(1,5) else 404}" p.Reset(input2) - _, err = p.Parse() + _, err = p.Parse(testParamTypes) if err != nil { t.Fatalf("expecting empty error on input '%s', but got: %s", input2, err.Error()) @@ -40,6 +78,16 @@ func TestParseParamError(t *testing.T) { // } +// mustLookupParamType same as `ast.LookupParamType` but it panics if "indent" does not match with a valid Param Type. +func mustLookupParamType(indent string) ast.ParamType { + pt, found := ast.LookupParamType(indent, testParamTypes...) + if !found { + panic("param type '" + indent + "' is not part of the provided param types") + } + + return pt +} + func TestParseParam(t *testing.T) { tests := []struct { valid bool @@ -49,27 +97,28 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{id:int min(1) max(5) else 404}", Name: "id", - Type: ast.ParamTypeInt, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "min", - Args: []ast.ParamFuncArg{1}}, + Args: []string{"1"}}, { Name: "max", - Args: []ast.ParamFuncArg{5}}, + Args: []string{"5"}}, }, ErrorCode: 404, }}, // 0 {true, ast.ParamStatement{ - Src: "{id:int range(1,5)}", + // test alias of int. + Src: "{id:number range(1,5)}", Name: "id", - Type: ast.ParamTypeInt, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "range", - Args: []ast.ParamFuncArg{1, 5}}, + Args: []string{"1", "5"}}, }, ErrorCode: 404, }}, // 1 @@ -77,11 +126,11 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{file:path contains(.)}", Name: "file", - Type: ast.ParamTypePath, + Type: mustLookupParamType("path"), Funcs: []ast.ParamFunc{ { Name: "contains", - Args: []ast.ParamFuncArg{"."}}, + Args: []string{"."}}, }, ErrorCode: 404, }}, // 2 @@ -89,35 +138,35 @@ func TestParseParam(t *testing.T) { ast.ParamStatement{ Src: "{username:alphabetical}", Name: "username", - Type: ast.ParamTypeAlphabetical, + Type: mustLookupParamType("alphabetical"), ErrorCode: 404, }}, // 3 {true, ast.ParamStatement{ Src: "{myparam}", Name: "myparam", - Type: ast.ParamTypeString, + Type: mustLookupParamType("string"), ErrorCode: 404, }}, // 4 {false, ast.ParamStatement{ Src: "{myparam_:thisianunexpected}", Name: "myparam_", - Type: ast.ParamTypeUnExpected, + Type: nil, ErrorCode: 404, }}, // 5 - {false, // false because it will give an error of unexpeced token type with value 2 + {true, ast.ParamStatement{ Src: "{myparam2}", - Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names. - Type: ast.ParamTypeString, + Name: "myparam2", // we now allow integers to the parameter names. + Type: ast.GetMasterParamType(testParamTypes...), ErrorCode: 404, }}, // 6 {true, ast.ParamStatement{ Src: "{id:int even()}", // test param funcs without any arguments (LPAREN peek for RPAREN) Name: "id", - Type: ast.ParamTypeInt, + Type: mustLookupParamType("number"), Funcs: []ast.ParamFunc{ { Name: "even"}, @@ -126,25 +175,46 @@ func TestParseParam(t *testing.T) { }}, // 7 {true, ast.ParamStatement{ - Src: "{id:long else 404}", + Src: "{id:int64 else 404}", Name: "id", - Type: ast.ParamTypeLong, + Type: mustLookupParamType("int64"), ErrorCode: 404, }}, // 8 {true, ast.ParamStatement{ - Src: "{has:boolean else 404}", - Name: "has", - Type: ast.ParamTypeBoolean, + Src: "{id:long else 404}", // backwards-compatible test. + Name: "id", + Type: mustLookupParamType("int64"), ErrorCode: 404, }}, // 9 + {true, + ast.ParamStatement{ + Src: "{id:long else 404}", + Name: "id", + Type: mustLookupParamType("int64"), // backwards-compatible test of LookupParamType. + ErrorCode: 404, + }}, // 10 + {true, + ast.ParamStatement{ + Src: "{has:bool else 404}", + Name: "has", + Type: mustLookupParamType("bool"), + ErrorCode: 404, + }}, // 11 + {true, + ast.ParamStatement{ + Src: "{has:boolean else 404}", // backwards-compatible test. + Name: "has", + Type: mustLookupParamType("bool"), + ErrorCode: 404, + }}, // 12 } p := new(ParamParser) for i, tt := range tests { p.Reset(tt.expectedStatement.Src) - resultStmt, err := p.Parse() + resultStmt, err := p.Parse(testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) @@ -171,27 +241,27 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{id:int min(1) max(5) else 404}", Name: "id", - Type: ast.ParamTypeInt, + Type: paramTypeNumber, Funcs: []ast.ParamFunc{ { Name: "min", - Args: []ast.ParamFuncArg{1}}, + Args: []string{"1"}}, { Name: "max", - Args: []ast.ParamFuncArg{5}}, + Args: []string{"5"}}, }, ErrorCode: 404, }, }}, // 0 - {"/admin/{id:int range(1,5)}", true, + {"/admin/{id:uint64 range(1,5)}", true, []ast.ParamStatement{{ - Src: "{id:int range(1,5)}", + Src: "{id:uint64 range(1,5)}", Name: "id", - Type: ast.ParamTypeInt, + Type: paramTypeUint64, Funcs: []ast.ParamFunc{ { Name: "range", - Args: []ast.ParamFuncArg{1, 5}}, + Args: []string{"1", "5"}}, }, ErrorCode: 404, }, @@ -200,11 +270,11 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{file:path contains(.)}", Name: "file", - Type: ast.ParamTypePath, + Type: paramTypePath, Funcs: []ast.ParamFunc{ { Name: "contains", - Args: []ast.ParamFuncArg{"."}}, + Args: []string{"."}}, }, ErrorCode: 404, }, @@ -213,7 +283,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{username:alphabetical}", Name: "username", - Type: ast.ParamTypeAlphabetical, + Type: paramTypeAlphabetical, ErrorCode: 404, }, }}, // 3 @@ -221,7 +291,7 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{myparam}", Name: "myparam", - Type: ast.ParamTypeString, + Type: paramTypeString, ErrorCode: 404, }, }}, // 4 @@ -229,15 +299,15 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{myparam_:thisianunexpected}", Name: "myparam_", - Type: ast.ParamTypeUnExpected, + Type: nil, ErrorCode: 404, }, }}, // 5 - {"/p2/{myparam2}", false, // false because it will give an error of unexpeced token type with value 2 + {"/p2/{myparam2}", true, []ast.ParamStatement{{ Src: "{myparam2}", - Name: "myparam", // expected "myparam" because we don't allow integers to the parameter names. - Type: ast.ParamTypeString, + Name: "myparam2", // we now allow integers to the parameter names. + Type: paramTypeString, ErrorCode: 404, }, }}, // 6 @@ -245,13 +315,13 @@ func TestParse(t *testing.T) { []ast.ParamStatement{{ Src: "{file:path}", Name: "file", - Type: ast.ParamTypePath, + Type: paramTypePath, ErrorCode: 404, }, }}, // 7 } for i, tt := range tests { - statements, err := Parse(tt.path) + statements, err := Parse(tt.path, testParamTypes) if tt.valid && err != nil { t.Fatalf("tests[%d] - error %s", i, err.Error()) diff --git a/core/router/macro/interpreter/token/token.go b/macro/interpreter/token/token.go similarity index 96% rename from core/router/macro/interpreter/token/token.go rename to macro/interpreter/token/token.go index 620ad64103..b964db1d70 100644 --- a/core/router/macro/interpreter/token/token.go +++ b/macro/interpreter/token/token.go @@ -13,7 +13,7 @@ type Token struct { // /about/{fullname:alphabetical} // /profile/{anySpecialName:string} -// {id:int range(1,5) else 404} +// {id:uint64 range(1,5) else 404} // /admin/{id:int eq(1) else 402} // /file/{filepath:file else 405} const ( diff --git a/macro/macro.go b/macro/macro.go new file mode 100644 index 0000000000..c4c557043d --- /dev/null +++ b/macro/macro.go @@ -0,0 +1,344 @@ +package macro + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + "unicode" +) + +type ( + // ParamEvaluator is the signature for param type evaluator. + // It accepts the param's value as string and returns + // the value (which its type is used for the input argument of the parameter functions, if any) + // and a true value for passed, otherwise nil and false should be returned. + ParamEvaluator func(paramValue string) (interface{}, bool) +) + +var goodEvaluatorFuncs = []reflect.Type{ + reflect.TypeOf(func(string) (interface{}, bool) { return nil, false }), + reflect.TypeOf(ParamEvaluator(func(string) (interface{}, bool) { return nil, false })), +} + +func goodParamFunc(typ reflect.Type) bool { + if typ.Kind() == reflect.Func { // it should be a func which returns a func (see below check). + if typ.NumOut() == 1 { + typOut := typ.Out(0) + if typOut.Kind() != reflect.Func { + return false + } + + if typOut.NumOut() == 2 { // if it's a type of EvaluatorFunc, used for param evaluator. + for _, fType := range goodEvaluatorFuncs { + if typOut == fType { + return true + } + } + return false + } + + if typOut.NumIn() == 1 && typOut.NumOut() == 1 { // if it's a type of func(paramValue [int,string...]) bool, used for param funcs. + return typOut.Out(0).Kind() == reflect.Bool + } + } + } + + return false +} + +// Regexp accepts a regexp "expr" expression +// and returns its MatchString. +// The regexp is compiled before return. +// +// Returns a not-nil error on regexp compile failure. +func Regexp(expr string) (func(string) bool, error) { + if expr == "" { + return nil, fmt.Errorf("empty regex expression") + } + + // add the last $ if missing (and not wildcard(?)) + if i := expr[len(expr)-1]; i != '$' && i != '*' { + expr += "$" + } + + r, err := regexp.Compile(expr) + if err != nil { + return nil, err + } + + return r.MatchString, nil +} + +// MustRegexp same as Regexp +// but it panics on the "expr" parse failure. +func MustRegexp(expr string) func(string) bool { + r, err := Regexp(expr) + if err != nil { + panic(err) + } + return r +} + +// goodParamFuncName reports whether the function name is a valid identifier. +func goodParamFuncName(name string) bool { + if name == "" { + return false + } + // valid names are only letters and _ + for _, r := range name { + switch { + case r == '_': + case !unicode.IsLetter(r): + return false + } + } + return true +} + +// the convertBuilderFunc return value is generating at boot time. +// convertFunc converts an interface to a valid full param function. +func convertBuilderFunc(fn interface{}) ParamFuncBuilder { + + typFn := reflect.TypeOf(fn) + if !goodParamFunc(typFn) { + return nil + } + + numFields := typFn.NumIn() + + return func(args []string) reflect.Value { + if len(args) != numFields { + // no variadics support, for now. + panic("args should be the same len as numFields") + } + var argValues []reflect.Value + for i := 0; i < numFields; i++ { + field := typFn.In(i) + arg := args[i] + + // try to convert the string literal as we get it from the parser. + var ( + val interface{} + + panicIfErr = func(err error) { + if err != nil { + panic(fmt.Sprintf("on field index: %d: %v", i, err)) + } + } + ) + + // try to get the value based on the expected type. + switch field.Kind() { + case reflect.Int: + v, err := strconv.Atoi(arg) + panicIfErr(err) + val = v + case reflect.Int8: + v, err := strconv.ParseInt(arg, 10, 8) + panicIfErr(err) + val = int8(v) + case reflect.Int16: + v, err := strconv.ParseInt(arg, 10, 16) + panicIfErr(err) + val = int16(v) + case reflect.Int32: + v, err := strconv.ParseInt(arg, 10, 32) + panicIfErr(err) + val = int32(v) + case reflect.Int64: + v, err := strconv.ParseInt(arg, 10, 64) + panicIfErr(err) + val = v + case reflect.Uint: + v, err := strconv.ParseUint(arg, 10, strconv.IntSize) + panicIfErr(err) + val = uint(v) + case reflect.Uint8: + v, err := strconv.ParseUint(arg, 10, 8) + panicIfErr(err) + val = uint8(v) + case reflect.Uint16: + v, err := strconv.ParseUint(arg, 10, 16) + panicIfErr(err) + val = uint16(v) + case reflect.Uint32: + v, err := strconv.ParseUint(arg, 10, 32) + panicIfErr(err) + val = uint32(v) + case reflect.Uint64: + v, err := strconv.ParseUint(arg, 10, 64) + panicIfErr(err) + val = v + case reflect.Float32: + v, err := strconv.ParseFloat(arg, 32) + panicIfErr(err) + val = float32(v) + case reflect.Float64: + v, err := strconv.ParseFloat(arg, 64) + panicIfErr(err) + val = v + case reflect.Bool: + v, err := strconv.ParseBool(arg) + panicIfErr(err) + val = v + case reflect.Slice: + if len(arg) > 1 { + if arg[0] == '[' && arg[len(arg)-1] == ']' { + // it is a single argument but as slice. + val = strings.Split(arg[1:len(arg)-1], ",") // only string slices. + } + } + + default: + val = arg + } + + argValue := reflect.ValueOf(val) + if expected, got := field.Kind(), argValue.Kind(); expected != got { + panic(fmt.Sprintf("func's input arguments should have the same type: [%d] expected %s but got %s", i, expected, got)) + } + + argValues = append(argValues, argValue) + } + + evalFn := reflect.ValueOf(fn).Call(argValues)[0] + + // var evaluator EvaluatorFunc + // // check for typed and not typed + // if _v, ok := evalFn.(EvaluatorFunc); ok { + // evaluator = _v + // } else if _v, ok = evalFn.(func(string) bool); ok { + // evaluator = _v + // } + // return func(paramValue interface{}) bool { + // return evaluator(paramValue) + // } + return evalFn + } +} + +type ( + // Macro represents the parsed macro, + // which holds + // the evaluator (param type's evaluator + param functions evaluators) + // and its param functions. + // + // Any type contains its own macro + // instance, so an String type + // contains its type evaluator + // which is the "Evaluator" field + // and it can register param functions + // to that macro which maps to a parameter type. + Macro struct { + indent string + alias string + master bool + trailing bool + + Evaluator ParamEvaluator + funcs []ParamFunc + } + + // ParamFuncBuilder is a func + // which accepts a param function's arguments (values) + // and returns a function as value, its job + // is to make the macros to be registered + // by user at the most generic possible way. + ParamFuncBuilder func([]string) reflect.Value // the func() bool + + // ParamFunc represents the parsed + // parameter function, it holds + // the parameter's name + // and the function which will build + // the evaluator func. + ParamFunc struct { + Name string + Func ParamFuncBuilder + } +) + +// NewMacro creates and returns a Macro that can be used as a registry for +// a new customized parameter type and its functions. +func NewMacro(indent, alias string, master, trailing bool, evaluator ParamEvaluator) *Macro { + return &Macro{ + indent: indent, + alias: alias, + master: master, + trailing: trailing, + + Evaluator: evaluator, + } +} + +// Indent returns the name of the parameter type. +func (m *Macro) Indent() string { + return m.indent +} + +// Alias returns the alias of the parameter type, if any. +func (m *Macro) Alias() string { + return m.alias +} + +// Master returns true if that macro's parameter type is the +// default one if not :type is followed by a parameter type inside the route path. +func (m *Macro) Master() bool { + return m.master +} + +// Trailing returns true if that macro's parameter type +// is wildcard and can accept one or more path segments as one parameter value. +// A wildcard should be registered in the last path segment only. +func (m *Macro) Trailing() bool { + return m.trailing +} + +// func (m *Macro) SetParamResolver(fn func(memstore.Entry) interface{}) *Macro { +// m.ParamResolver = fn +// return m +// } + +// RegisterFunc registers a parameter function +// to that macro. +// Accepts the func name ("range") +// and the function body, which should return an EvaluatorFunc +// a bool (it will be converted to EvaluatorFunc later on), +// i.e RegisterFunc("min", func(minValue int) func(paramValue string) bool){}) +func (m *Macro) RegisterFunc(funcName string, fn interface{}) *Macro { + fullFn := convertBuilderFunc(fn) + m.registerFunc(funcName, fullFn) + + return m +} + +func (m *Macro) registerFunc(funcName string, fullFn ParamFuncBuilder) { + if !goodParamFuncName(funcName) { + return + } + + for _, fn := range m.funcs { + if fn.Name == funcName { + fn.Func = fullFn + return + } + } + + m.funcs = append(m.funcs, ParamFunc{ + Name: funcName, + Func: fullFn, + }) +} + +func (m *Macro) getFunc(funcName string) ParamFuncBuilder { + for _, fn := range m.funcs { + if fn.Name == funcName { + if fn.Func == nil { + continue + } + return fn.Func + } + } + return nil +} diff --git a/macro/macro_test.go b/macro/macro_test.go new file mode 100644 index 0000000000..ff25c817db --- /dev/null +++ b/macro/macro_test.go @@ -0,0 +1,453 @@ +package macro + +import ( + "reflect" + "strconv" + "testing" +) + +// Most important tests to look: +// ../parser/parser_test.go +// ../lexer/lexer_test.go + +func TestGoodParamFunc(t *testing.T) { + good1 := func(min int, max int) func(string) bool { + return func(paramValue string) bool { + return true + } + } + + good2 := func(min uint64, max uint64) func(string) bool { + return func(paramValue string) bool { + return true + } + } + + notgood1 := func(min int, max int) bool { + return false + } + + if !goodParamFunc(reflect.TypeOf(good1)) { + t.Fatalf("expected good1 func to be good but it's not") + } + + if !goodParamFunc(reflect.TypeOf(good2)) { + t.Fatalf("expected good2 func to be good but it's not") + } + + if goodParamFunc(reflect.TypeOf(notgood1)) { + t.Fatalf("expected notgood1 func to be the worst") + } +} + +func TestGoodParamFuncName(t *testing.T) { + tests := []struct { + name string + good bool + }{ + {"range", true}, + {"_range", true}, + {"range_", true}, + {"r_ange", true}, + // numbers or other symbols are invalid. + {"range1", false}, + {"2range", false}, + {"r@nge", false}, + {"rang3", false}, + } + for i, tt := range tests { + isGood := goodParamFuncName(tt.name) + if tt.good && !isGood { + t.Fatalf("tests[%d] - expecting valid name but got invalid for name %s", i, tt.name) + } else if !tt.good && isGood { + t.Fatalf("tests[%d] - expecting invalid name but got valid for name %s", i, tt.name) + } + } +} + +func testEvaluatorRaw(t *testing.T, macroEvaluator *Macro, input string, expectedType reflect.Kind, pass bool, i int) { + if macroEvaluator.Evaluator == nil && pass { + return // if not evaluator defined then it should allow everything. + } + value, passed := macroEvaluator.Evaluator(input) + if pass != passed { + t.Fatalf("%s - tests[%d] - expecting[pass] %v but got %v", t.Name(), i, pass, passed) + } + + if !passed { + return + } + + if value == nil && expectedType != reflect.Invalid { + t.Fatalf("%s - tests[%d] - expecting[value] to not be nil", t.Name(), i) + } + + if v := reflect.ValueOf(value); v.Kind() != expectedType { + t.Fatalf("%s - tests[%d] - expecting[value.Kind] %v but got %v", t.Name(), i, expectedType, v.Kind()) + } +} + +func TestStringEvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {true, "astring"}, // 0 + {true, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "main.css"}, // 3 + {true, "/assets/main.css"}, // 4 + // false never + } // 0 + + for i, tt := range tests { + testEvaluatorRaw(t, String, tt.input, reflect.String, tt.pass, i) + } +} + +func TestIntEvaluatorRaw(t *testing.T) { + x64 := strconv.IntSize == 64 + + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {x64, "9223372036854775807" /* max int64 */}, // 3 + {x64, "-9223372036854775808" /* min int64 */}, // 4 + {false, "-18446744073709553213213213213213121615"}, // 5 + {false, "42 18446744073709551615"}, // 6 + {false, "--42"}, // 7 + {false, "+42"}, // 8 + {false, "main.css"}, // 9 + {false, "/assets/main.css"}, // 10 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Int, tt.input, reflect.Int, tt.pass, i) + } +} + +func TestInt8EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {false, "32321"}, // 2 + {true, "127" /* max int8 */}, // 3 + {true, "-128" /* min int8 */}, // 4 + {false, "128"}, // 5 + {false, "-129"}, // 6 + {false, "-18446744073709553213213213213213121615"}, // 7 + {false, "42 18446744073709551615"}, // 8 + {false, "--42"}, // 9 + {false, "+42"}, // 10 + {false, "main.css"}, // 11 + {false, "/assets/main.css"}, // 12 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Int8, tt.input, reflect.Int8, tt.pass, i) + } +} + +func TestInt16EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "32767" /* max int16 */}, // 3 + {true, "-32768" /* min int16 */}, // 4 + {false, "-32769"}, // 5 + {false, "32768"}, // 6 + {false, "-18446744073709553213213213213213121615"}, // 7 + {false, "42 18446744073709551615"}, // 8 + {false, "--42"}, // 9 + {false, "+42"}, // 10 + {false, "main.css"}, // 11 + {false, "/assets/main.css"}, // 12 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Int16, tt.input, reflect.Int16, tt.pass, i) + } +} + +func TestInt32EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "1"}, // 3 + {true, "42"}, // 4 + {true, "2147483647" /* max int32 */}, // 5 + {true, "-2147483648" /* min int32 */}, // 6 + {false, "-2147483649"}, // 7 + {false, "2147483648"}, // 8 + {false, "-18446744073709553213213213213213121615"}, // 9 + {false, "42 18446744073709551615"}, // 10 + {false, "--42"}, // 11 + {false, "+42"}, // 12 + {false, "main.css"}, // 13 + {false, "/assets/main.css"}, // 14 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Int32, tt.input, reflect.Int32, tt.pass, i) + } +} + +func TestInt64EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {false, "18446744073709551615"}, // 2 + {false, "92233720368547758079223372036854775807"}, // 3 + {false, "9223372036854775808 9223372036854775808"}, // 4 + {false, "main.css"}, // 5 + {false, "/assets/main.css"}, // 6 + {true, "9223372036854775807"}, // 7 + {true, "-9223372036854775808"}, // 8 + {true, "-0"}, // 9 + {true, "1"}, // 10 + {true, "-042"}, // 11 + {true, "142"}, // 12 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Int64, tt.input, reflect.Int64, tt.pass, i) + } +} + +func TestUintEvaluatorRaw(t *testing.T) { + x64 := strconv.IntSize == 64 + + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "1"}, // 3 + {true, "42"}, // 4 + {x64, "18446744073709551615" /* max uint64 */}, // 5 + {true, "4294967295" /* max uint32 */}, // 6 + {false, "-2147483649"}, // 7 + {true, "2147483648"}, // 8 + {false, "-18446744073709553213213213213213121615"}, // 9 + {false, "42 18446744073709551615"}, // 10 + {false, "--42"}, // 11 + {false, "+42"}, // 12 + {false, "main.css"}, // 13 + {false, "/assets/main.css"}, // 14 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Uint, tt.input, reflect.Uint, tt.pass, i) + } +} + +func TestUint8EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {false, "-9223372036854775808"}, // 2 + {false, "main.css"}, // 3 + {false, "/assets/main.css"}, // 4 + {false, "92233720368547758079223372036854775807"}, // 5 + {false, "9223372036854775808 9223372036854775808"}, // 6 + {false, "-1"}, // 7 + {false, "-0"}, // 8 + {false, "+1"}, // 9 + {false, "18446744073709551615"}, // 10 + {false, "9223372036854775807"}, // 11 + {false, "021"}, // 12 - no leading zeroes are allowed. + {false, "300"}, // 13 + {true, "0"}, // 14 + {true, "255"}, // 15 + {true, "21"}, // 16 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Uint8, tt.input, reflect.Uint8, tt.pass, i) + } +} + +func TestUint16EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "65535" /* max uint16 */}, // 3 + {true, "0" /* min uint16 */}, // 4 + {false, "-32769"}, // 5 + {true, "32768"}, // 6 + {false, "-18446744073709553213213213213213121615"}, // 7 + {false, "42 18446744073709551615"}, // 8 + {false, "--42"}, // 9 + {false, "+42"}, // 10 + {false, "main.css"}, // 11 + {false, "/assets/main.css"}, // 12 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Uint16, tt.input, reflect.Uint16, tt.pass, i) + } +} + +func TestUint32EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "1"}, // 3 + {true, "42"}, // 4 + {true, "4294967295" /* max uint32*/}, // 5 + {true, "0" /* min uint32 */}, // 6 + {false, "-2147483649"}, // 7 + {true, "2147483648"}, // 8 + {false, "-18446744073709553213213213213213121615"}, // 9 + {false, "42 18446744073709551615"}, // 10 + {false, "--42"}, // 11 + {false, "+42"}, // 12 + {false, "main.css"}, // 13 + {false, "/assets/main.css"}, // 14 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Uint32, tt.input, reflect.Uint32, tt.pass, i) + } +} + +func TestUint64EvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {false, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {false, "-9223372036854775808"}, // 2 + {false, "main.css"}, // 3 + {false, "/assets/main.css"}, // 4 + {false, "92233720368547758079223372036854775807"}, // 5 + {false, "9223372036854775808 9223372036854775808"}, // 6 + {false, "-1"}, // 7 + {false, "-0"}, // 8 + {false, "+1"}, // 9 + {true, "18446744073709551615"}, // 10 + {true, "9223372036854775807"}, // 11 + {true, "0"}, // 12 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Uint64, tt.input, reflect.Uint64, tt.pass, i) + } +} + +func TestAlphabeticalEvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {true, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {false, "32321"}, // 2 + {false, "main.css"}, // 3 + {false, "/assets/main.css"}, // 4 + } + + for i, tt := range tests { + testEvaluatorRaw(t, Alphabetical, tt.input, reflect.String, tt.pass, i) + } +} + +func TestFileEvaluatorRaw(t *testing.T) { + tests := []struct { + pass bool + input string + }{ + {true, "astring"}, // 0 + {false, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "main.css"}, // 3 + {false, "/assets/main.css"}, // 4 + } + + for i, tt := range tests { + testEvaluatorRaw(t, File, tt.input, reflect.String, tt.pass, i) + } +} + +func TestPathEvaluatorRaw(t *testing.T) { + pathTests := []struct { + pass bool + input string + }{ + {true, "astring"}, // 0 + {true, "astringwith_numb3rS_and_symbol$"}, // 1 + {true, "32321"}, // 2 + {true, "main.css"}, // 3 + {true, "/assets/main.css"}, // 4 + {true, "disk/assets/main.css"}, // 5 + } + + for i, tt := range pathTests { + testEvaluatorRaw(t, Path, tt.input, reflect.String, tt.pass, i) + } +} + +func TestConvertBuilderFunc(t *testing.T) { + fn := func(min uint64, slice []string) func(string) bool { + return func(paramValue string) bool { + if expected, got := "ok", paramValue; expected != got { + t.Fatalf("paramValue is not the expected one: %s vs %s", expected, got) + } + + if expected, got := uint64(1), min; expected != got { + t.Fatalf("min argument is not the expected one: %d vs %d", expected, got) + } + + if expected, got := []string{"name1", "name2"}, slice; len(expected) == len(got) { + if expected, got := "name1", slice[0]; expected != got { + t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 0, expected, got) + } + + if expected, got := "name2", slice[1]; expected != got { + t.Fatalf("slice argument[%d] does not contain the expected value: %s vs %s", 1, expected, got) + } + } else { + t.Fatalf("slice argument is not the expected one, the length is difference: %d vs %d", len(expected), len(got)) + } + + return true + } + } + + evalFunc := convertBuilderFunc(fn) + if !evalFunc([]string{"1", "[name1,name2]"}).Call([]reflect.Value{reflect.ValueOf("ok")})[0].Interface().(bool) { + t.Fatalf("failed, it should fail already") + } +} diff --git a/macro/macros.go b/macro/macros.go new file mode 100644 index 0000000000..4c6f641617 --- /dev/null +++ b/macro/macros.go @@ -0,0 +1,550 @@ +package macro + +import ( + "strconv" + "strings" + + "github.com/kataras/iris/macro/interpreter/ast" +) + +var ( + // String type + // Allows anything (single path segment, as everything except the `Path`). + // Its functions can be used by the rest of the macros and param types whenever not available function by name is used. + // Because of its "master" boolean value to true (third parameter). + String = NewMacro("string", "", true, false, nil). + RegisterFunc("regexp", MustRegexp). + // checks if param value starts with the 'prefix' arg + RegisterFunc("prefix", func(prefix string) func(string) bool { + return func(paramValue string) bool { + return strings.HasPrefix(paramValue, prefix) + } + }). + // checks if param value ends with the 'suffix' arg + RegisterFunc("suffix", func(suffix string) func(string) bool { + return func(paramValue string) bool { + return strings.HasSuffix(paramValue, suffix) + } + }). + // checks if param value contains the 's' arg + RegisterFunc("contains", func(s string) func(string) bool { + return func(paramValue string) bool { + return strings.Contains(paramValue, s) + } + }). + // checks if param value's length is at least 'min' + RegisterFunc("min", func(min int) func(string) bool { + return func(paramValue string) bool { + return len(paramValue) >= min + } + }). + // checks if param value's length is not bigger than 'max' + RegisterFunc("max", func(max int) func(string) bool { + return func(paramValue string) bool { + return max >= len(paramValue) + } + }) + + simpleNumberEval = MustRegexp("^-?[0-9]+$") + // Int or number type + // both positive and negative numbers, actual value can be min-max int64 or min-max int32 depends on the arch. + // If x64: -9223372036854775808 to 9223372036854775807. + // If x32: -2147483648 to 2147483647 and etc.. + Int = NewMacro("int", "number", false, false, func(paramValue string) (interface{}, bool) { + if !simpleNumberEval(paramValue) { + return nil, false + } + + v, err := strconv.Atoi(paramValue) + if err != nil { + return nil, false + } + + return v, true + }). + // checks if the param value's int representation is + // bigger or equal than 'min' + RegisterFunc("min", func(min int) func(int) bool { + return func(paramValue int) bool { + return paramValue >= min + } + }). + // checks if the param value's int representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max int) func(int) bool { + return func(paramValue int) bool { + return paramValue <= max + } + }). + // checks if the param value's int representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max int) func(int) bool { + return func(paramValue int) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Int8 type + // -128 to 127. + Int8 = NewMacro("int8", "", false, false, func(paramValue string) (interface{}, bool) { + if !simpleNumberEval(paramValue) { + return nil, false + } + + v, err := strconv.ParseInt(paramValue, 10, 8) + if err != nil { + return nil, false + } + return int8(v), true + }). + RegisterFunc("min", func(min int8) func(int8) bool { + return func(paramValue int8) bool { + return paramValue >= min + } + }). + RegisterFunc("max", func(max int8) func(int8) bool { + return func(paramValue int8) bool { + return paramValue <= max + } + }). + RegisterFunc("range", func(min, max int8) func(int8) bool { + return func(paramValue int8) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Int16 type + // -32768 to 32767. + Int16 = NewMacro("int16", "", false, false, func(paramValue string) (interface{}, bool) { + if !simpleNumberEval(paramValue) { + return nil, false + } + + v, err := strconv.ParseInt(paramValue, 10, 16) + if err != nil { + return nil, false + } + return int16(v), true + }). + RegisterFunc("min", func(min int16) func(int16) bool { + return func(paramValue int16) bool { + return paramValue >= min + } + }). + RegisterFunc("max", func(max int16) func(int16) bool { + return func(paramValue int16) bool { + return paramValue <= max + } + }). + RegisterFunc("range", func(min, max int16) func(int16) bool { + return func(paramValue int16) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Int32 type + // -2147483648 to 2147483647. + Int32 = NewMacro("int32", "", false, false, func(paramValue string) (interface{}, bool) { + if !simpleNumberEval(paramValue) { + return nil, false + } + + v, err := strconv.ParseInt(paramValue, 10, 32) + if err != nil { + return nil, false + } + return int32(v), true + }). + RegisterFunc("min", func(min int32) func(int32) bool { + return func(paramValue int32) bool { + return paramValue >= min + } + }). + RegisterFunc("max", func(max int32) func(int32) bool { + return func(paramValue int32) bool { + return paramValue <= max + } + }). + RegisterFunc("range", func(min, max int32) func(int32) bool { + return func(paramValue int32) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Int64 as int64 type + // -9223372036854775808 to 9223372036854775807. + Int64 = NewMacro("int64", "long", false, false, func(paramValue string) (interface{}, bool) { + if !simpleNumberEval(paramValue) { + return nil, false + } + + v, err := strconv.ParseInt(paramValue, 10, 64) + if err != nil { // if err == strconv.ErrRange... + return nil, false + } + return v, true + }). + // checks if the param value's int64 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min int64) func(int64) bool { + return func(paramValue int64) bool { + return paramValue >= min + } + }). + // checks if the param value's int64 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max int64) func(int64) bool { + return func(paramValue int64) bool { + return paramValue <= max + } + }). + // checks if the param value's int64 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max int64) func(int64) bool { + return func(paramValue int64) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Uint as uint type + // actual value can be min-max uint64 or min-max uint32 depends on the arch. + // If x64: 0 to 18446744073709551615. + // If x32: 0 to 4294967295 and etc. + Uint = NewMacro("uint", "", false, false, func(paramValue string) (interface{}, bool) { + v, err := strconv.ParseUint(paramValue, 10, strconv.IntSize) // 32,64... + if err != nil { + return nil, false + } + return uint(v), true + }). + // checks if the param value's int representation is + // bigger or equal than 'min' + RegisterFunc("min", func(min uint) func(uint) bool { + return func(paramValue uint) bool { + return paramValue >= min + } + }). + // checks if the param value's int representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max uint) func(uint) bool { + return func(paramValue uint) bool { + return paramValue <= max + } + }). + // checks if the param value's int representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max uint) func(uint) bool { + return func(paramValue uint) bool { + return !(paramValue < min || paramValue > max) + } + }) + + uint8Eval = MustRegexp("^([0-9]|[1-8][0-9]|9[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") + // Uint8 as uint8 type + // 0 to 255. + Uint8 = NewMacro("uint8", "", false, false, func(paramValue string) (interface{}, bool) { + if !uint8Eval(paramValue) { + return nil, false + } + + v, err := strconv.ParseUint(paramValue, 10, 8) + if err != nil { + return nil, false + } + return uint8(v), true + }). + // checks if the param value's uint8 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min uint8) func(uint8) bool { + return func(paramValue uint8) bool { + return paramValue >= min + } + }). + // checks if the param value's uint8 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max uint8) func(uint8) bool { + return func(paramValue uint8) bool { + return paramValue <= max + } + }). + // checks if the param value's uint8 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max uint8) func(uint8) bool { + return func(paramValue uint8) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Uint16 as uint16 type + // 0 to 65535. + Uint16 = NewMacro("uint16", "", false, false, func(paramValue string) (interface{}, bool) { + v, err := strconv.ParseUint(paramValue, 10, 16) + if err != nil { + return nil, false + } + return uint16(v), true + }). + RegisterFunc("min", func(min uint16) func(uint16) bool { + return func(paramValue uint16) bool { + return paramValue >= min + } + }). + RegisterFunc("max", func(max uint16) func(uint16) bool { + return func(paramValue uint16) bool { + return paramValue <= max + } + }). + RegisterFunc("range", func(min, max uint16) func(uint16) bool { + return func(paramValue uint16) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Uint32 as uint32 type + // 0 to 4294967295. + Uint32 = NewMacro("uint32", "", false, false, func(paramValue string) (interface{}, bool) { + v, err := strconv.ParseUint(paramValue, 10, 32) + if err != nil { + return nil, false + } + return uint32(v), true + }). + RegisterFunc("min", func(min uint32) func(uint32) bool { + return func(paramValue uint32) bool { + return paramValue >= min + } + }). + RegisterFunc("max", func(max uint32) func(uint32) bool { + return func(paramValue uint32) bool { + return paramValue <= max + } + }). + RegisterFunc("range", func(min, max uint32) func(uint32) bool { + return func(paramValue uint32) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Uint64 as uint64 type + // 0 to 18446744073709551615. + Uint64 = NewMacro("uint64", "", false, false, func(paramValue string) (interface{}, bool) { + v, err := strconv.ParseUint(paramValue, 10, 64) + if err != nil { + return nil, false + } + return v, true + }). + // checks if the param value's uint64 representation is + // bigger or equal than 'min'. + RegisterFunc("min", func(min uint64) func(uint64) bool { + return func(paramValue uint64) bool { + return paramValue >= min + } + }). + // checks if the param value's uint64 representation is + // smaller or equal than 'max'. + RegisterFunc("max", func(max uint64) func(uint64) bool { + return func(paramValue uint64) bool { + return paramValue <= max + } + }). + // checks if the param value's uint64 representation is + // between min and max, including 'min' and 'max'. + RegisterFunc("range", func(min, max uint64) func(uint64) bool { + return func(paramValue uint64) bool { + return !(paramValue < min || paramValue > max) + } + }) + + // Bool or boolean as bool type + // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" + // or "0" or "f" or "F" or "FALSE" or "false" or "False". + Bool = NewMacro("bool", "boolean", false, false, func(paramValue string) (interface{}, bool) { + // a simple if statement is faster than regex ^(true|false|True|False|t|0|f|FALSE|TRUE)$ + // in this case. + v, err := strconv.ParseBool(paramValue) + if err != nil { + return nil, false + } + return v, true + }) + + alphabeticalEval = MustRegexp("^[a-zA-Z ]+$") + // Alphabetical letter type + // letters only (upper or lowercase) + Alphabetical = NewMacro("alphabetical", "", false, false, func(paramValue string) (interface{}, bool) { + if !alphabeticalEval(paramValue) { + return nil, false + } + return paramValue, true + }) + + fileEval = MustRegexp("^[a-zA-Z0-9_.-]*$") + // File type + // letters (upper or lowercase) + // numbers (0-9) + // underscore (_) + // dash (-) + // point (.) + // no spaces! or other character + File = NewMacro("file", "", false, false, func(paramValue string) (interface{}, bool) { + if !fileEval(paramValue) { + return nil, false + } + return paramValue, true + }) + // Path type + // anything, should be the last part + // + // It allows everything, we have String and Path as different + // types because I want to give the opportunity to the user + // to organise the macro functions based on wildcard or single dynamic named path parameter. + // Should be living in the latest path segment of a route path. + Path = NewMacro("path", "", false, true, nil) + + // Defaults contains the defaults macro and parameters types for the router. + // + // Read https://github.com/kataras/iris/tree/master/_examples/routing/macros for more details. + Defaults = &Macros{ + String, + Int, + Int8, + Int16, + Int32, + Int64, + Uint, + Uint8, + Uint16, + Uint32, + Uint64, + Bool, + Alphabetical, + Path, + } +) + +// Macros is just a type of a slice of *Macro +// which is responsible to register and search for macros based on the indent(parameter type). +type Macros []*Macro + +// Register registers a custom Macro. +// The "indent" should not be empty and should be unique, it is the parameter type's name, i.e "string". +// The "alias" is optionally and it should be unique, it is the alias of the parameter type. +// "isMaster" and "isTrailing" is for default parameter type and wildcard respectfully. +// The "evaluator" is the function that is converted to an Iris handler which is executed every time +// before the main chain of a route's handlers that contains this macro of the specific parameter type. +// +// Read https://github.com/kataras/iris/tree/master/_examples/routing/macros for more details. +func (ms *Macros) Register(indent, alias string, isMaster, isTrailing bool, evaluator ParamEvaluator) *Macro { + macro := NewMacro(indent, alias, isMaster, isTrailing, evaluator) + if ms.register(macro) { + return macro + } + return nil +} + +func (ms *Macros) register(macro *Macro) bool { + if macro.Indent() == "" { + return false + } + + cp := *ms + + for _, m := range cp { + // can't add more than one with the same ast characteristics. + if macro.Indent() == m.Indent() { + return false + } + + if alias := macro.Alias(); alias != "" { + if alias == m.Alias() || alias == m.Indent() { + return false + } + } + + if macro.Master() && m.Master() { + return false + } + } + + cp = append(cp, macro) + + *ms = cp + return true +} + +// Unregister removes a macro and its parameter type from the list. +func (ms *Macros) Unregister(indent string) bool { + cp := *ms + + for i, m := range cp { + if m.Indent() == indent { + copy(cp[i:], cp[i+1:]) + cp[len(cp)-1] = nil + cp = cp[:len(cp)-1] + + *ms = cp + return true + } + } + + return false +} + +// Lookup returns the responsible macro for a parameter type, it can return nil. +func (ms *Macros) Lookup(pt ast.ParamType) *Macro { + if m := ms.Get(pt.Indent()); m != nil { + return m + } + + if alias, has := ast.HasAlias(pt); has { + if m := ms.Get(alias); m != nil { + return m + } + } + + return nil +} + +// Get returns the responsible macro for a parameter type, it can return nil. +func (ms *Macros) Get(indentOrAlias string) *Macro { + if indentOrAlias == "" { + return nil + } + + for _, m := range *ms { + if m.Indent() == indentOrAlias { + return m + } + + if m.Alias() == indentOrAlias { + return m + } + } + + return nil +} + +// GetMaster returns the default macro and its parameter type, +// by default it will return the `String` macro which is responsible for the "string" parameter type. +func (ms *Macros) GetMaster() *Macro { + for _, m := range *ms { + if m.Master() { + return m + } + } + + return nil +} + +// GetTrailings returns the macros that have support for wildcards parameter types. +// By default it will return the `Path` macro which is responsible for the "path" parameter type. +func (ms *Macros) GetTrailings() (macros []*Macro) { + for _, m := range *ms { + if m.Trailing() { + macros = append(macros, m) + } + } + + return +} diff --git a/macro/template.go b/macro/template.go new file mode 100644 index 0000000000..8e766cb5dd --- /dev/null +++ b/macro/template.go @@ -0,0 +1,155 @@ +package macro + +import ( + "reflect" + + "github.com/kataras/iris/core/memstore" + "github.com/kataras/iris/macro/interpreter/ast" + "github.com/kataras/iris/macro/interpreter/parser" +) + +// Template contains a route's path full parsed template. +// +// Fields: +// Src is the raw source of the path, i.e /users/{id:int min(1)} +// Params is the list of the Params that are being used to the +// path, i.e the min as param name and 1 as the param argument. +type Template struct { + // Src is the original template given by the client + Src string `json:"src"` + Params []TemplateParam `json:"params"` +} + +// TemplateParam is the parsed macro parameter's template +// they are being used to describe the param's syntax result. +type TemplateParam struct { + Src string `json:"src"` // the unparsed param'false source + // Type is not useful anywhere here but maybe + // it's useful on host to decide how to convert the path template to specific router's syntax + Type ast.ParamType `json:"type"` + Name string `json:"name"` + Index int `json:"index"` + ErrCode int `json:"errCode"` + TypeEvaluator ParamEvaluator `json:"-"` + Funcs []reflect.Value `json:"-"` + + stringInFuncs []func(string) bool + canEval bool +} + +func (p TemplateParam) preComputed() TemplateParam { + for _, pfn := range p.Funcs { + if fn, ok := pfn.Interface().(func(string) bool); ok { + p.stringInFuncs = append(p.stringInFuncs, fn) + } + } + + // if true then it should be execute the type parameter or its functions + // else it can be ignored, + // i.e {myparam} or {myparam:string} or {myparam:path} -> + // their type evaluator is nil because they don't do any checks and they don't change + // the default parameter value's type (string) so no need for any work). + p.canEval = p.TypeEvaluator != nil || len(p.Funcs) > 0 || p.ErrCode != parser.DefaultParamErrorCode + + return p +} + +// CanEval returns true if this "p" TemplateParam should be evaluated in serve time. +// It is computed before server ran and it is used to determinate if a route needs to build a macro handler (middleware). +func (p *TemplateParam) CanEval() bool { + return p.canEval +} + +// Eval is the most critical part of the TEmplateParam. +// It is responsible to return "passed:true" or "not passed:false" +// if the "paramValue" is the correct type of the registered parameter type +// and all functions, if any, are passed. +// "paramChanger" is the same form of context's Params().Set +// we could accept a memstore.Store or even context.RequestParams +// but this form has been chosed in order to test easier and fully decoupled from a request when necessary. +// +// It is called from the converted macro handler (middleware) +// from the higher-level component of "kataras/iris/macro/handler#MakeHandler". +func (p *TemplateParam) Eval(paramValue string, paramSetter memstore.ValueSetter) bool { + if p.TypeEvaluator == nil { + for _, fn := range p.stringInFuncs { + if !fn(paramValue) { + return false + } + } + return true + } + + newValue, passed := p.TypeEvaluator(paramValue) + if !passed { + return false + } + + if len(p.Funcs) > 0 { + paramIn := []reflect.Value{reflect.ValueOf(newValue)} + for _, evalFunc := range p.Funcs { + // or make it as func(interface{}) bool and pass directly the "newValue" + // but that would not be as easy for end-developer, so keep that "slower": + if !evalFunc.Call(paramIn)[0].Interface().(bool) { // i.e func(paramValue int) bool + return false + } + } + } + + paramSetter.Set(p.Name, newValue) + return true +} + +// Parse takes a full route path and a macro map (macro map contains the macro types with their registered param functions) +// and returns a new Template. +// It builds all the parameter functions for that template +// and their evaluators, it's the api call that makes use the interpeter's parser -> lexer. +func Parse(src string, macros Macros) (Template, error) { + types := make([]ast.ParamType, len(macros)) + for i, m := range macros { + types[i] = m + } + + tmpl := Template{Src: src} + params, err := parser.Parse(src, types) + if err != nil { + return tmpl, err + } + + for idx, p := range params { + m := macros.Lookup(p.Type) + typEval := m.Evaluator + + tmplParam := TemplateParam{ + Src: p.Src, + Type: p.Type, + Name: p.Name, + Index: idx, + ErrCode: p.ErrorCode, + TypeEvaluator: typEval, + } + + for _, paramfn := range p.Funcs { + tmplFn := m.getFunc(paramfn.Name) + if tmplFn == nil { // if not find on this type, check for Master's which is for global funcs too. + if m := macros.GetMaster(); m != nil { + tmplFn = m.getFunc(paramfn.Name) + } + + if tmplFn == nil { // if not found then just skip this param. + continue + } + } + + evalFn := tmplFn(paramfn.Args) + if evalFn.IsNil() || !evalFn.IsValid() || evalFn.Kind() != reflect.Func { + continue + } + tmplParam.Funcs = append(tmplParam.Funcs, evalFn) + } + + tmpl.Params = append(tmpl.Params, tmplParam.preComputed()) + } + + return tmpl, nil +} diff --git a/middleware/i18n/i18n.go b/middleware/i18n/i18n.go index 4fef4470c1..923398ea6a 100644 --- a/middleware/i18n/i18n.go +++ b/middleware/i18n/i18n.go @@ -19,10 +19,8 @@ type i18nMiddleware struct { func (i *i18nMiddleware) ServeHTTP(ctx context.Context) { wasByCookie := false - language := i.config.Default - langKey := ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey() - language = ctx.Values().GetString(langKey) + language := ctx.Values().GetString(langKey) if language == "" { // try to get by url parameter language = ctx.URLParam(i.config.URLParameter) @@ -50,6 +48,7 @@ func (i *i18nMiddleware) ServeHTTP(ctx context.Context) { if !wasByCookie { ctx.SetCookieKV(langKey, language) } + if language == "" { language = i.config.Default } diff --git a/mvc/controller.go b/mvc/controller.go index 41f37ac200..804b1b004b 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -7,9 +7,9 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/core/router/macro" "github.com/kataras/iris/hero" "github.com/kataras/iris/hero/di" + "github.com/kataras/iris/macro" "github.com/kataras/golog" ) @@ -77,7 +77,7 @@ type ControllerActivator struct { // the already-registered routes, key = the controller's function name. // End-devs can change some properties of the *Route on the `BeforeActivator` by using the - // `GetRoute(functionName)`. It's also protects for duplicatations. + // `GetRoute(functionName)`. It's a shield against duplications as well. routes map[string]*router.Route // the bindings that comes from the Engine and the controller's filled fields if any. @@ -247,7 +247,7 @@ func (c *ControllerActivator) parseMethods() { } func (c *ControllerActivator) parseMethod(m reflect.Method) { - httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + httpMethod, httpPath, err := parseMethod(*c.router.Macros(), m, c.isReservedMethod) if err != nil { if err != errSkip { c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err)) @@ -283,7 +283,7 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware . } // parse a route template which contains the parameters organised. - tmpl, err := macro.Parse(path, c.router.Macros()) + tmpl, err := macro.Parse(path, *c.router.Macros()) if err != nil { c.addErr(fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err)) return nil @@ -338,6 +338,7 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref } // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) + funcInjector := di.Func(m.Func, funcDependencies...) // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) if funcInjector.Has { @@ -396,6 +397,11 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref in := make([]reflect.Value, n, n) in[0] = ctrl funcInjector.Inject(&in, ctxValue) + + // for idxx, inn := range in { + // println("controller.go: execution: in.Value = "+inn.String()+" and in.Type = "+inn.Type().Kind().String()+" of index: ", idxx) + // } + hero.DispatchFuncResult(ctx, call(in)) return } diff --git a/mvc/controller_handle_test.go b/mvc/controller_handle_test.go index 55e200d4c4..9864b3ad6e 100644 --- a/mvc/controller_handle_test.go +++ b/mvc/controller_handle_test.go @@ -37,6 +37,7 @@ type testControllerHandle struct { func (c *testControllerHandle) BeforeActivation(b BeforeActivation) { b.Handle("GET", "/histatic", "HiStatic") b.Handle("GET", "/hiservice", "HiService") + b.Handle("GET", "/hiservice/{ps:string}", "HiServiceBy") b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") } @@ -84,6 +85,10 @@ func (c *testControllerHandle) HiService() string { return c.Service.Say("hi") } +func (c *testControllerHandle) HiServiceBy(v string) string { + return c.Service.Say("hi with param: " + v) +} + func (c *testControllerHandle) HiParamBy(v string) string { return v } @@ -116,7 +121,8 @@ func TestControllerHandle(t *testing.T) { // and can be used in a user-defined, dynamic "mvc handler". e.GET("/hiservice").Expect().Status(httptest.StatusOK). Body().Equal("service: hi") - + e.GET("/hiservice/value").Expect().Status(httptest.StatusOK). + Body().Equal("service: hi with param: value") // this worked with a temporary variadic on the resolvemethodfunc which is not // correct design, I should split the path and params with the rest of implementation // in order a simple template.Src can be given. diff --git a/mvc/controller_method_parser.go b/mvc/controller_method_parser.go index 97c1282edb..1deb40ef46 100644 --- a/mvc/controller_method_parser.go +++ b/mvc/controller_method_parser.go @@ -4,11 +4,12 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "unicode" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/core/router/macro/interpreter/ast" + "github.com/kataras/iris/macro" ) const ( @@ -95,47 +96,25 @@ func (l *methodLexer) peekPrev() (w string) { return w } -var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", - 10: "tenth", - 11: "eleventh", - 12: "twelfth", - 13: "thirteenth", - 14: "fourteenth", - 15: "fifteenth", - 16: "sixteenth", - 17: "seventeenth", - 18: "eighteenth", - 19: "nineteenth", - 20: "twentieth", -} - func genParamKey(argIdx int) string { - return "arg" + posWords[argIdx] // argfirst, argsecond... + return "param" + strconv.Itoa(argIdx) // param0, param1, param2... } type methodParser struct { - lexer *methodLexer - fn reflect.Method + lexer *methodLexer + fn reflect.Method + macros macro.Macros } -func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path string, err error) { +func parseMethod(macros macro.Macros, fn reflect.Method, skipper func(string) bool) (method, path string, err error) { if skipper(fn.Name) { return "", "", errSkip } p := &methodParser{ - fn: fn, - lexer: newMethodLexer(fn.Name), + fn: fn, + lexer: newMethodLexer(fn.Name), + macros: macros, } return p.parse() } @@ -211,34 +190,45 @@ func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (st var ( paramKey = genParamKey(funcArgPos) // argfirst, argsecond... - paramType = ast.ParamTypeString // default string + m = p.macros.GetMaster() // default (String by-default) + trailings = p.macros.GetTrailings() ) // string, int... - goType := typ.In(funcArgPos).Name() + goType := typ.In(funcArgPos).Kind() nextWord := p.lexer.peekNext() if nextWord == tokenWildcard { p.lexer.skip() // skip the Wildcard word. - paramType = ast.ParamTypePath - } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { - // it's not wildcard, so check base on our available macro types. - paramType = pType + if len(trailings) == 0 { + return "", 0, errors.New("no trailing path parameter found") + } + m = trailings[0] } else { - if typ.NumIn() > funcArgPos { - // has more input arguments but we are not in the correct - // index now, maybe the first argument was an `iris/context.Context` - // so retry with the "funcArgPos" incremented. - // - // the "funcArgPos" will be updated to the caller as well - // because we return it among the path and the error. - return p.parsePathParam(path, w, funcArgPos+1) + // validMacros := p.macros.LookupForGoType(goType) + + // instead of mapping with a reflect.Kind which has its limitation, + // we map the param types with a go type as a string, + // so custom structs such as "user" can be mapped to a macro with indent || alias == "user". + m = p.macros.Get(strings.ToLower(goType.String())) + + if m == nil { + if typ.NumIn() > funcArgPos { + // has more input arguments but we are not in the correct + // index now, maybe the first argument was an `iris/context.Context` + // so retry with the "funcArgPos" incremented. + // + // the "funcArgPos" will be updated to the caller as well + // because we return it among the path and the error. + return p.parsePathParam(path, w, funcArgPos+1) + } + + return "", 0, fmt.Errorf("invalid syntax: the standard go type: %s found in controller's function: %s at position: %d does not match any valid macro", goType, p.fn.Name, funcArgPos) } - return "", 0, errors.New("invalid syntax for " + p.fn.Name) } // /{argfirst:path}, /{argfirst:long}... - path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) + path += fmt.Sprintf("/{%s:%s}", paramKey, m.Indent()) if nextWord == "" && typ.NumIn() > funcArgPos+1 { // By is the latest word but func is expected diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 475b79dc5c..7103c9e944 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -365,7 +365,9 @@ func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { } func (c *testControllerRelPathFromFunc) Get() {} -func (c *testControllerRelPathFromFunc) GetBy(int64) {} +func (c *testControllerRelPathFromFunc) GetBy(uint64) {} +func (c *testControllerRelPathFromFunc) GetUint8RatioBy(uint8) {} +func (c *testControllerRelPathFromFunc) GetInt64RatioBy(int64) {} func (c *testControllerRelPathFromFunc) GetAnythingByWildcard(string) {} func (c *testControllerRelPathFromFunc) GetLogin() {} @@ -375,8 +377,10 @@ func (c *testControllerRelPathFromFunc) GetAdminLogin() {} func (c *testControllerRelPathFromFunc) PutSomethingIntoThis() {} -func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} -func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {} +func (c *testControllerRelPathFromFunc) GetSomethingBy(bool) {} + +func (c *testControllerRelPathFromFunc) GetSomethingByBy(string, int) {} + func (c *testControllerRelPathFromFunc) GetSomethingNewBy(string, int) {} // two input arguments, one By which is the latest word. func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} // two input arguments @@ -388,8 +392,13 @@ func TestControllerRelPathFromFunc(t *testing.T) { e.GET("/").Expect().Status(iris.StatusOK). Body().Equal("GET:/") - e.GET("/42").Expect().Status(iris.StatusOK). - Body().Equal("GET:/42") + e.GET("/18446744073709551615").Expect().Status(iris.StatusOK). + Body().Equal("GET:/18446744073709551615") + e.GET("/uint8/ratio/255").Expect().Status(iris.StatusOK). + Body().Equal("GET:/uint8/ratio/255") + e.GET("/uint8/ratio/256").Expect().Status(iris.StatusNotFound) + e.GET("/int64/ratio/-42").Expect().Status(iris.StatusOK). + Body().Equal("GET:/int64/ratio/-42") e.GET("/something/true").Expect().Status(iris.StatusOK). Body().Equal("GET:/something/true") e.GET("/something/false").Expect().Status(iris.StatusOK). diff --git a/mvc/param.go b/mvc/param.go index 8b680aeef6..faa6839626 100644 --- a/mvc/param.go +++ b/mvc/param.go @@ -4,8 +4,7 @@ import ( "reflect" "github.com/kataras/iris/context" - "github.com/kataras/iris/core/router/macro" - "github.com/kataras/iris/core/router/macro/interpreter/ast" + "github.com/kataras/iris/macro" ) func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) { @@ -13,51 +12,40 @@ func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) return } - consumedParams := make(map[int]bool, 0) - for _, in := range funcIn { - for j, p := range params { - if _, consumed := consumedParams[j]; consumed { - continue - } - paramType := p.Type - paramName := p.Name - // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) - if paramType.Assignable(in.Kind()) { - consumedParams[j] = true - // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) - values = append(values, makeFuncParamGetter(paramType, paramName)) - } + // consumedParams := make(map[int]bool, 0) + // for _, in := range funcIn { + // for j, p := range params { + // if _, consumed := consumedParams[j]; consumed { + // continue + // } + + // // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + // if m := macros.Lookup(p.Type); m != nil && m.GoType == in.Kind() { + // consumedParams[j] = true + // // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + // funcDep, ok := context.ParamResolverByKindAndIndex(m.GoType, p.Index) + // // funcDep, ok := context.ParamResolverByKindAndKey(in.Kind(), paramName) + // if !ok { + // // here we can add a logger about invalid parameter type although it should never happen here + // // unless the end-developer modified the macro/macros with a special type but not the context/ParamResolvers. + // continue + // } + // values = append(values, funcDep) + // } + // } + // } + + for i, param := range params { + if len(funcIn) <= i { + return } - } - - return -} - -func makeFuncParamGetter(paramType ast.ParamType, paramName string) reflect.Value { - var fn interface{} - - switch paramType { - case ast.ParamTypeInt: - fn = func(ctx context.Context) int { - v, _ := ctx.Params().GetInt(paramName) - return v - } - case ast.ParamTypeLong: - fn = func(ctx context.Context) int64 { - v, _ := ctx.Params().GetInt64(paramName) - return v - } - case ast.ParamTypeBoolean: - fn = func(ctx context.Context) bool { - v, _ := ctx.Params().GetBool(paramName) - return v - } - default: - // string, path... - fn = func(ctx context.Context) string { - return ctx.Params().Get(paramName) + funcDep, ok := context.ParamResolverByTypeAndIndex(funcIn[i], param.Index) + if !ok { + continue } + + values = append(values, funcDep) } - return reflect.ValueOf(fn) + return } diff --git a/sessions/sessiondb/badger/database.go b/sessions/sessiondb/badger/database.go index 223deafeea..5c990e064b 100644 --- a/sessions/sessiondb/badger/database.go +++ b/sessions/sessiondb/badger/database.go @@ -7,11 +7,11 @@ import ( "sync/atomic" "time" - "github.com/kataras/golog" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/sessions" "github.com/dgraph-io/badger" + "github.com/kataras/golog" ) // DefaultFileMode used as the default database's "fileMode" @@ -144,7 +144,13 @@ func (db *Database) Get(sid string, key string) (value interface{}) { if err != nil { return err } - // item.ValueCopy + + // return item.Value(func(valueBytes []byte) { + // if err := sessions.DefaultTranscoder.Unmarshal(valueBytes, &value); err != nil { + // golog.Error(err) + // } + // }) + valueBytes, err := item.Value() if err != nil { return err @@ -173,13 +179,25 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) { for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() { item := iter.Item() + var value interface{} + + // err := item.Value(func(valueBytes []byte) { + // if err := sessions.DefaultTranscoder.Unmarshal(valueBytes, &value); err != nil { + // golog.Error(err) + // } + // }) + + // if err != nil { + // golog.Error(err) + // continue + // } + valueBytes, err := item.Value() if err != nil { golog.Error(err) continue } - var value interface{} if err = sessions.DefaultTranscoder.Unmarshal(valueBytes, &value); err != nil { golog.Error(err) continue diff --git a/sessions/sessiondb/badger/vendor/badger.txt b/sessions/sessiondb/badger/vendor/badger.txt new file mode 100644 index 0000000000..f890a03872 --- /dev/null +++ b/sessions/sessiondb/badger/vendor/badger.txt @@ -0,0 +1,5 @@ +02 Oct 2018 +----------- +Keep this vendor because go modules uses the v1.5.4 which is the latest published release(~19 Sep 2018) +while the master repo will be used if host's golang version is lower than 1.11 or inside $GOPATH, which +has a breaking change on the `item.Value` input & output values, see the comments inside this `database.go`. \ No newline at end of file diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger.txt b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger.txt deleted file mode 100644 index 12081a43fd..0000000000 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger.txt +++ /dev/null @@ -1,3 +0,0 @@ -27 Dec 2017 - -Commit: 0225b784d8dfc250b3be597fa806f0fd677a6628 \ No newline at end of file diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/backup.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/backup.go index 375b5a3162..4649de25b7 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/backup.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/backup.go @@ -1,9 +1,26 @@ +/* + * Copyright 2017 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package badger import ( "bufio" "encoding/binary" "io" + "log" "sync" "github.com/dgraph-io/badger/y" @@ -36,6 +53,8 @@ func (db *DB) Backup(w io.Writer, since uint64) (uint64, error) { opts := DefaultIteratorOptions opts.AllVersions = true it := txn.NewIterator(opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { item := it.Item() if item.Version() < since { @@ -44,7 +63,8 @@ func (db *DB) Backup(w io.Writer, since uint64) (uint64, error) { } val, err := item.Value() if err != nil { - return err + log.Printf("Key [%x]. Error while fetching value [%v]\n", item.Key(), err) + continue } entry := &protos.KVPair{ @@ -68,7 +88,7 @@ func (db *DB) Backup(w io.Writer, since uint64) (uint64, error) { // Load reads a protobuf-encoded list of all entries from a reader and writes // them to the database. This can be used to restore the database from a backup -// made by calling DB.Dump(). +// made by calling DB.Backup(). // // DB.Load() should be called on a database that is not running any other // concurrent transactions while it is running. @@ -124,12 +144,17 @@ func (db *DB) Load(r io.Reader) error { UserMeta: e.UserMeta[0], ExpiresAt: e.ExpiresAt, }) + // Update nextCommit, memtable stores this timestamp in badger head + // when flushed. + if e.Version >= db.orc.commitTs() { + db.orc.nextCommit = e.Version + 1 + } if len(entries) == 1000 { if err := batchSetAsyncIfNoErr(entries); err != nil { return err } - entries = entries[:0] + entries = make([]*Entry, 0, 1000) } } @@ -145,6 +170,7 @@ func (db *DB) Load(r io.Reader) error { case err := <-errChan: return err default: + db.orc.curRead = db.orc.commitTs() - 1 return nil } } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/compaction.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/compaction.go index 23824ae175..00be8f6efa 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/compaction.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/compaction.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "log" + "math" "sync" "golang.org/x/net/trace" @@ -37,7 +38,7 @@ type keyRange struct { var infRange = keyRange{inf: true} func (r keyRange) String() string { - return fmt.Sprintf("[left=%q, right=%q, inf=%v]", r.left, r.right, r.inf) + return fmt.Sprintf("[left=%x, right=%x, inf=%v]", r.left, r.right, r.inf) } func (r keyRange) equals(dst keyRange) bool { @@ -75,7 +76,10 @@ func getKeyRange(tables []*table.Table) keyRange { biggest = tables[i].Biggest() } } - return keyRange{left: smallest, right: biggest} + return keyRange{ + left: y.KeyWithTs(y.ParseKey(smallest), math.MaxUint64), + right: y.KeyWithTs(y.ParseKey(biggest), 0), + } } type levelCompactStatus struct { diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/db.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/db.go index f03a5caaba..1bd59e031f 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/db.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/db.go @@ -17,8 +17,6 @@ package badger import ( - "bytes" - "container/heap" "encoding/binary" "expvar" "log" @@ -27,6 +25,7 @@ import ( "path/filepath" "strconv" "sync" + "sync/atomic" "time" "github.com/dgraph-io/badger/options" @@ -43,6 +42,7 @@ var ( badgerPrefix = []byte("!badger!") // Prefix for internal keys used by badger. head = []byte("!badger!head") // For storing value offset for replay. txnKey = []byte("!badger!txn") // For indicating end of entries in txn. + badgerMove = []byte("!badger!move") // For key-value pairs which got moved during GC. ) type closers struct { @@ -74,6 +74,8 @@ type DB struct { writeCh chan *request flushChan chan flushTask // For flushing memtables. + blockWrites int32 + orc *oracle } @@ -101,7 +103,7 @@ func replayFunction(out *DB) func(Entry, valuePointer) error { first := true return func(e Entry, vp valuePointer) error { // Function for replaying. if first { - out.elog.Printf("First key=%s\n", e.Key) + out.elog.Printf("First key=%q\n", e.Key) } first = false @@ -168,12 +170,24 @@ func Open(opt Options) (db *DB, err error) { opt.maxBatchSize = (15 * opt.MaxTableSize) / 100 opt.maxBatchCount = opt.maxBatchSize / int64(skl.MaxNodeSize) + if opt.ValueThreshold > math.MaxUint16-16 { + return nil, ErrValueThreshold + } + + if opt.ReadOnly { + // Can't truncate if the DB is read only. + opt.Truncate = false + } + for _, path := range []string{opt.Dir, opt.ValueDir} { dirExists, err := exists(path) if err != nil { return nil, y.Wrapf(err, "Invalid Dir: %q", path) } if !dirExists { + if opt.ReadOnly { + return nil, y.Wrapf(err, "Cannot find Dir for read-only open: %q", path) + } // Try to create the directory err = os.Mkdir(path, 0700) if err != nil { @@ -189,8 +203,8 @@ func Open(opt Options) (db *DB, err error) { if err != nil { return nil, err } - - dirLockGuard, err := acquireDirectoryLock(opt.Dir, lockFile) + var dirLockGuard, valueDirLockGuard *directoryLockGuard + dirLockGuard, err = acquireDirectoryLock(opt.Dir, lockFile, opt.ReadOnly) if err != nil { return nil, err } @@ -199,9 +213,8 @@ func Open(opt Options) (db *DB, err error) { _ = dirLockGuard.release() } }() - var valueDirLockGuard *directoryLockGuard if absValueDir != absDir { - valueDirLockGuard, err = acquireDirectoryLock(opt.ValueDir, lockFile) + valueDirLockGuard, err = acquireDirectoryLock(opt.ValueDir, lockFile, opt.ReadOnly) if err != nil { return nil, err } @@ -218,7 +231,7 @@ func Open(opt Options) (db *DB, err error) { opt.ValueLogLoadingMode == options.MemoryMap) { return nil, ErrInvalidLoadingMode } - manifestFile, manifest, err := openOrCreateManifestFile(opt.Dir) + manifestFile, manifest, err := openOrCreateManifestFile(opt.Dir, opt.ReadOnly) if err != nil { return nil, err } @@ -229,12 +242,12 @@ func Open(opt Options) (db *DB, err error) { }() orc := &oracle{ - isManaged: opt.managedTxns, - nextCommit: 1, - pendingCommits: make(map[uint64]struct{}), - commits: make(map[uint64]uint64), + isManaged: opt.managedTxns, + nextCommit: 1, + commits: make(map[uint64]uint64), + readMark: y.WaterMark{}, } - heap.Init(&orc.commitMark) + orc.readMark.Init() db = &DB{ imm: make([]*skl.Skiplist, 0, opt.NumMemtables), @@ -259,11 +272,13 @@ func Open(opt Options) (db *DB, err error) { return nil, err } - db.closers.compactors = y.NewCloser(1) - db.lc.startCompact(db.closers.compactors) + if !opt.ReadOnly { + db.closers.compactors = y.NewCloser(1) + db.lc.startCompact(db.closers.compactors) - db.closers.memtable = y.NewCloser(1) - go db.flushMemtable(db.closers.memtable) // Need levels controller to be up. + db.closers.memtable = y.NewCloser(1) + go db.flushMemtable(db.closers.memtable) // Need levels controller to be up. + } if err = db.vlog.Open(db, opt); err != nil { return nil, err @@ -319,7 +334,8 @@ func Open(opt Options) (db *DB, err error) { } // Close closes a DB. It's crucial to call it to ensure all the pending updates -// make their way to disk. +// make their way to disk. Calling DB.Close() multiple times is not safe and would +// cause panic. func (db *DB) Close() (err error) { db.elog.Printf("Closing database") // Stop value GC first. @@ -366,11 +382,31 @@ func (db *DB) Close() (err error) { } db.flushChan <- flushTask{nil, valuePointer{}} // Tell flusher to quit. - db.closers.memtable.Wait() - db.elog.Printf("Memtable flushed") + if db.closers.memtable != nil { + db.closers.memtable.Wait() + db.elog.Printf("Memtable flushed") + } + if db.closers.compactors != nil { + db.closers.compactors.SignalAndWait() + db.elog.Printf("Compaction finished") + } - db.closers.compactors.SignalAndWait() - db.elog.Printf("Compaction finished") + // Force Compact L0 + // We don't need to care about cstatus since no parallel compaction is running. + cd := compactDef{ + elog: trace.New("Badger", "Compact"), + thisLevel: db.lc.levels[0], + nextLevel: db.lc.levels[1], + } + cd.elog.SetMaxEvents(100) + defer cd.elog.Finish() + if db.lc.fillTablesL0(&cd) { + if err := db.lc.runCompactDef(0, cd); err != nil { + cd.elog.LazyPrintf("\tLOG Compact FAILED with error: %+v: %+v", err, cd) + } + } else { + cd.elog.LazyPrintf("fillTables failed for level zero. No compaction required") + } if lcErr := db.lc.close(); err == nil { err = errors.Wrap(lcErr, "DB.Close") @@ -380,8 +416,10 @@ func (db *DB) Close() (err error) { db.elog.Finish() - if guardErr := db.dirLockGuard.release(); err == nil { - err = errors.Wrap(guardErr, "DB.Close") + if db.dirLockGuard != nil { + if guardErr := db.dirLockGuard.release(); err == nil { + err = errors.Wrap(guardErr, "DB.Close") + } } if db.valueDirGuard != nil { if guardErr := db.valueDirGuard.release(); err == nil { @@ -451,33 +489,25 @@ func (db *DB) getMemTables() ([]*skl.Skiplist, func()) { // get returns the value in memtable or disk for given key. // Note that value will include meta byte. +// +// IMPORTANT: We should never write an entry with an older timestamp for the same key, We need to +// maintain this invariant to search for the latest value of a key, or else we need to search in all +// tables and find the max version among them. To maintain this invariant, we also need to ensure +// that all versions of a key are always present in the same table from level 1, because compaction +// can push any table down. func (db *DB) get(key []byte) (y.ValueStruct, error) { tables, decr := db.getMemTables() // Lock should be released. defer decr() y.NumGets.Add(1) - version := y.ParseTs(key) - var maxVs y.ValueStruct - // Need to search for values in all tables, with managed db - // latest value needn't be present in the latest table. - // Even without managed db, purging can cause this constraint - // to be violated. - // Search until required version is found or iterate over all - // tables and return max version. for i := 0; i < len(tables); i++ { vs := tables[i].Get(key) y.NumMemtableGets.Add(1) - if vs.Meta == 0 && vs.Value == nil { - continue - } - if vs.Version == version { + if vs.Meta != 0 || vs.Value != nil { return vs, nil } - if maxVs.Version < vs.Version { - maxVs = vs - } } - return db.lc.get(key, maxVs) + return db.lc.get(key) } func (db *DB) updateOffset(ptrs []valuePointer) { @@ -568,8 +598,12 @@ func (db *DB) writeRequests(reqs []*request) error { continue } count += len(b.Entries) - for err := db.ensureRoomForWrite(); err != nil; err = db.ensureRoomForWrite() { - db.elog.Printf("Making room for writes") + var i uint64 + for err := db.ensureRoomForWrite(); err == errNoRoom; err = db.ensureRoomForWrite() { + i++ + if i%100 == 0 { + db.elog.Printf("Making room for writes") + } // We need to poll a bit because both hasRoomForWrite and the flusher need access to s.imm. // When flushChan is full and you are blocked there, and the flusher is trying to update s.imm, // you will get a deadlock. @@ -591,6 +625,9 @@ func (db *DB) writeRequests(reqs []*request) error { } func (db *DB) sendToWriteCh(entries []*Entry) (*request, error) { + if atomic.LoadInt32(&db.blockWrites) == 1 { + return nil, ErrBlockedWrites + } var count, size int64 for _, e := range entries { size += int64(e.estimateSize(db.opt.ValueThreshold)) @@ -681,11 +718,7 @@ func (db *DB) batchSet(entries []*Entry) error { return err } - req.Wg.Wait() - req.Entries = nil - err = req.Err - requestPool.Put(req) - return err + return req.Wait() } // batchSetAsync is the asynchronous version of batchSet. It accepts a callback @@ -700,10 +733,7 @@ func (db *DB) batchSetAsync(entries []*Entry, f func(error)) error { return err } go func() { - req.Wg.Wait() - err := req.Err - req.Entries = nil - requestPool.Put(req) + err := req.Wait() // Write is complete. Let's call the callback function now. f(err) }() @@ -748,7 +778,7 @@ func arenaSize(opt Options) int64 { return opt.MaxTableSize + opt.maxBatchSize + opt.maxBatchCount*int64(skl.MaxNodeSize) } -// WriteLevel0Table flushes memtable. It drops deleteValues. +// WriteLevel0Table flushes memtable. func writeLevel0Table(s *skl.Skiplist, f *os.File) error { iter := s.NewIterator() defer iter.Close() @@ -768,6 +798,8 @@ type flushTask struct { vptr valuePointer } +// TODO: Ensure that this function doesn't return, or is handled by another wrapper function. +// Otherwise, we would have no goroutine which can flush memtables. func (db *DB) flushMemtable(lc *y.Closer) error { defer lc.Done() @@ -824,7 +856,12 @@ func (db *DB) flushMemtable(lc *y.Closer) error { // Update s.imm. Need a lock. db.Lock() - y.AssertTrue(ft.mt == db.imm[0]) //For now, single threaded. + // This is a single-threaded operation. ft.mt corresponds to the head of + // db.imm list. Once we flush it, we advance db.imm. The next ft.mt + // which would arrive here would match db.imm[0], because we acquire a + // lock over DB when pushing to flushChan. + // TODO: This logic is dirty AF. Any change and this could easily break. + y.AssertTrue(ft.mt == db.imm[0]) db.imm = db.imm[1:] ft.mt.DecrRef() // Return memory. db.Unlock() @@ -879,7 +916,6 @@ func (db *DB) calculateSize() { _, vlogSize = totalSize(db.opt.ValueDir) } y.VlogSize.Set(db.opt.Dir, newInt(vlogSize)) - } func (db *DB) updateSize(lc *y.Closer) { @@ -898,141 +934,14 @@ func (db *DB) updateSize(lc *y.Closer) { } } -// PurgeVersionsBelow will delete all versions of a key below the specified version -func (db *DB) PurgeVersionsBelow(key []byte, ts uint64) error { - txn := db.NewTransaction(false) - defer txn.Discard() - return db.purgeVersionsBelow(txn, key, ts) -} - -func (db *DB) purgeVersionsBelow(txn *Txn, key []byte, ts uint64) error { - opts := DefaultIteratorOptions - opts.AllVersions = true - opts.PrefetchValues = false - it := txn.NewIterator(opts) - defer it.Close() - - var entries []*Entry - - for it.Seek(key); it.ValidForPrefix(key); it.Next() { - item := it.Item() - if !bytes.Equal(key, item.Key()) || item.Version() >= ts { - continue - } - if isDeletedOrExpired(item.meta, item.ExpiresAt()) { - continue - } - - // Found an older version. Mark for deletion - entries = append(entries, - &Entry{ - Key: y.KeyWithTs(key, item.version), - meta: bitDelete, - }) - db.vlog.updateGCStats(item) - } - return db.batchSet(entries) -} - -// PurgeOlderVersions deletes older versions of all keys. -// -// This function could be called prior to doing garbage collection to clean up -// older versions that are no longer needed. The caller must make sure that -// there are no long-running read transactions running before this function is -// called, otherwise they will not work as expected. -func (db *DB) PurgeOlderVersions() error { - return db.View(func(txn *Txn) error { - opts := DefaultIteratorOptions - opts.AllVersions = true - opts.PrefetchValues = false - it := txn.NewIterator(opts) - defer it.Close() - - var entries []*Entry - var lastKey []byte - var count, size int - var wg sync.WaitGroup - errChan := make(chan error, 1) - - // func to check for pending error before sending off a batch for writing - batchSetAsyncIfNoErr := func(entries []*Entry) error { - select { - case err := <-errChan: - return err - default: - wg.Add(1) - return txn.db.batchSetAsync(entries, func(err error) { - defer wg.Done() - if err != nil { - select { - case errChan <- err: - default: - } - } - }) - } - } - - for it.Rewind(); it.Valid(); it.Next() { - item := it.Item() - if !bytes.Equal(lastKey, item.Key()) { - lastKey = y.SafeCopy(lastKey, item.Key()) - continue - } - if isDeletedOrExpired(item.meta, item.ExpiresAt()) { - continue - } - // Found an older version. Mark for deletion - e := &Entry{ - Key: y.KeyWithTs(lastKey, item.version), - meta: bitDelete, - } - db.vlog.updateGCStats(item) - curSize := e.estimateSize(db.opt.ValueThreshold) - - // Batch up min(1000, maxBatchCount) entries at a time and write - // Ensure that total batch size doesn't exceed maxBatchSize - if count == 1000 || count+1 >= int(db.opt.maxBatchCount) || - size+curSize >= int(db.opt.maxBatchSize) { - if err := batchSetAsyncIfNoErr(entries); err != nil { - return err - } - count = 0 - size = 0 - entries = []*Entry{} - } - size += curSize - count++ - entries = append(entries, e) - } - - // Write last batch pending deletes - if count > 0 { - if err := batchSetAsyncIfNoErr(entries); err != nil { - return err - } - } - - wg.Wait() - - select { - case err := <-errChan: - return err - default: - return nil - } - }) -} - // RunValueLogGC triggers a value log garbage collection. // // It picks value log files to perform GC based on statistics that are collected -// duing the session, when DB.PurgeOlderVersions() and DB.PurgeVersions() is -// called. If no such statistics are available, then log files are picked in -// random order. The process stops as soon as the first log file is encountered -// which does not result in garbage collection. +// duing compactions. If no such statistics are available, then log files are +// picked in random order. The process stops as soon as the first log file is +// encountered which does not result in garbage collection. // -// When a log file is picked, it is first sampled If the sample shows that we +// When a log file is picked, it is first sampled. If the sample shows that we // can discard at least discardRatio space of that file, it would be rewritten. // // If a call to RunValueLogGC results in no rewrites, then an ErrNoRewrite is @@ -1060,8 +969,7 @@ func (db *DB) RunValueLogGC(discardRatio float64) error { // Find head on disk headKey := y.KeyWithTs(head, math.MaxUint64) // Need to pass with timestamp, lsm get removes the last 8 bytes and compares key - var maxVs y.ValueStruct - val, err := db.lc.get(headKey, maxVs) + val, err := db.lc.get(headKey) if err != nil { return errors.Wrap(err, "Retrieving head from on-disk LSM") } @@ -1179,14 +1087,27 @@ func (db *DB) GetSequence(key []byte, bandwidth uint64) (*Sequence, error) { return seq, err } +func (db *DB) Tables() []TableInfo { + return db.lc.getTableInfo() +} + +// MaxBatchCount returns max possible entries in batch +func (db *DB) MaxBatchCount() int64 { + return db.opt.maxBatchCount +} + +// MaxBatchCount returns max possible batch size +func (db *DB) MaxBatchSize() int64 { + return db.opt.maxBatchSize +} + // MergeOperator represents a Badger merge operator. type MergeOperator struct { sync.RWMutex - f MergeFunc - db *DB - key []byte - skipAtOrBelow uint64 - closer *y.Closer + f MergeFunc + db *DB + key []byte + closer *y.Closer } // MergeFunc accepts two byte slices, one representing an existing value, and @@ -1214,66 +1135,67 @@ func (db *DB) GetMergeOperator(key []byte, return op } -func (op *MergeOperator) iterateAndMerge(txn *Txn) (maxVersion uint64, val []byte, err error) { +var errNoMerge = errors.New("No need for merge") + +func (op *MergeOperator) iterateAndMerge(txn *Txn) (val []byte, err error) { opt := DefaultIteratorOptions opt.AllVersions = true it := txn.NewIterator(opt) - var first bool + defer it.Close() + + var numVersions int for it.Rewind(); it.ValidForPrefix(op.key); it.Next() { item := it.Item() - if item.Version() <= op.skipAtOrBelow { - continue - } - if item.Version() > maxVersion { - maxVersion = item.Version() - } - if !first { - first = true + numVersions++ + if numVersions == 1 { val, err = item.ValueCopy(val) if err != nil { - return 0, nil, err + return nil, err } } else { newVal, err := item.Value() if err != nil { - return 0, nil, err + return nil, err } val = op.f(val, newVal) } + if item.DiscardEarlierVersions() { + break + } } - if !first { - return 0, nil, ErrKeyNotFound + if numVersions == 0 { + return nil, ErrKeyNotFound + } else if numVersions == 1 { + return val, errNoMerge } - return maxVersion, val, nil + return val, nil } func (op *MergeOperator) compact() error { op.Lock() defer op.Unlock() - var maxVersion uint64 err := op.db.Update(func(txn *Txn) error { var ( val []byte err error ) - maxVersion, val, err = op.iterateAndMerge(txn) + val, err = op.iterateAndMerge(txn) if err != nil { return err } // Write value back to db - if maxVersion > op.skipAtOrBelow { - if err := txn.Set(op.key, val); err != nil { - return err - } + if err := txn.SetWithDiscard(op.key, val, 0); err != nil { + return err } return nil }) - if err != nil && err != ErrKeyNotFound { // Ignore ErrKeyNotFound errors during compaction + + if err == ErrKeyNotFound || err == errNoMerge { + // pass. + } else if err != nil { return err } - // Update version - op.skipAtOrBelow = maxVersion return nil } @@ -1287,16 +1209,9 @@ func (op *MergeOperator) runCompactions(dur time.Duration) { stop = true case <-ticker.C: // wait for tick } - oldSkipVersion := op.skipAtOrBelow if err := op.compact(); err != nil { log.Printf("Error while running merge operation: %s", err) } - // Purge older versions if version has updated - if op.skipAtOrBelow > oldSkipVersion { - if err := op.db.PurgeVersionsBelow(op.key, op.skipAtOrBelow+1); err != nil { - log.Printf("Error purging merged keys: %s", err) - } - } if stop { ticker.Stop() break @@ -1315,15 +1230,18 @@ func (op *MergeOperator) Add(val []byte) error { // Get returns the latest value for the merge operator, which is derived by // applying the merge function to all the values added so far. // -// If Add has not been called even once, Get will return ErrKeyNotFound +// If Add has not been called even once, Get will return ErrKeyNotFound. func (op *MergeOperator) Get() ([]byte, error) { op.RLock() defer op.RUnlock() var existing []byte err := op.db.View(func(txn *Txn) (err error) { - _, existing, err = op.iterateAndMerge(txn) + existing, err = op.iterateAndMerge(txn) return err }) + if err == errNoMerge { + return existing, nil + } return existing, err } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_unix.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_unix.go index a058f45438..a5e0fa33c5 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_unix.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_unix.go @@ -35,11 +35,14 @@ type directoryLockGuard struct { f *os.File // The absolute path to our pid file. path string + // Was this a shared lock for a read-only database? + readOnly bool } -// acquireDirectoryLock gets an exclusive lock on the directory (using flock). It writes our pid -// to dirPath/pidFileName for convenience. -func acquireDirectoryLock(dirPath string, pidFileName string) (*directoryLockGuard, error) { +// acquireDirectoryLock gets a lock on the directory (using flock). If +// this is not read-only, it will also write our pid to +// dirPath/pidFileName for convenience. +func acquireDirectoryLock(dirPath string, pidFileName string, readOnly bool) (*directoryLockGuard, error) { // Convert to absolute path so that Release still works even if we do an unbalanced // chdir in the meantime. absPidFilePath, err := filepath.Abs(filepath.Join(dirPath, pidFileName)) @@ -50,7 +53,12 @@ func acquireDirectoryLock(dirPath string, pidFileName string) (*directoryLockGua if err != nil { return nil, errors.Wrapf(err, "cannot open directory %q", dirPath) } - err = unix.Flock(int(f.Fd()), unix.LOCK_EX|unix.LOCK_NB) + opts := unix.LOCK_EX | unix.LOCK_NB + if readOnly { + opts = unix.LOCK_SH | unix.LOCK_NB + } + + err = unix.Flock(int(f.Fd()), opts) if err != nil { f.Close() return nil, errors.Wrapf(err, @@ -58,22 +66,27 @@ func acquireDirectoryLock(dirPath string, pidFileName string) (*directoryLockGua dirPath) } - // Yes, we happily overwrite a pre-existing pid file. We're the only badger process using this - // directory. - err = ioutil.WriteFile(absPidFilePath, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0666) - if err != nil { - f.Close() - return nil, errors.Wrapf(err, - "Cannot write pid file %q", absPidFilePath) + if !readOnly { + // Yes, we happily overwrite a pre-existing pid file. We're the + // only read-write badger process using this directory. + err = ioutil.WriteFile(absPidFilePath, []byte(fmt.Sprintf("%d\n", os.Getpid())), 0666) + if err != nil { + f.Close() + return nil, errors.Wrapf(err, + "Cannot write pid file %q", absPidFilePath) + } } - - return &directoryLockGuard{f, absPidFilePath}, nil + return &directoryLockGuard{f, absPidFilePath, readOnly}, nil } // Release deletes the pid file and releases our lock on the directory. func (guard *directoryLockGuard) release() error { - // It's important that we remove the pid file first. - err := os.Remove(guard.path) + var err error + if !guard.readOnly { + // It's important that we remove the pid file first. + err = os.Remove(guard.path) + } + if closeErr := guard.f.Close(); err == nil { err = closeErr } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_windows.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_windows.go index 36d599fd03..80de84e16f 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_windows.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/dir_windows.go @@ -57,7 +57,11 @@ type directoryLockGuard struct { } // AcquireDirectoryLock acquires exclusive access to a directory. -func acquireDirectoryLock(dirPath string, pidFileName string) (*directoryLockGuard, error) { +func acquireDirectoryLock(dirPath string, pidFileName string, readOnly bool) (*directoryLockGuard, error) { + if readOnly { + return nil, ErrWindowsNotSupported + } + // Convert to absolute path so that Release still works even if we do an unbalanced // chdir in the meantime. absLockFilePath, err := filepath.Abs(filepath.Join(dirPath, pidFileName)) diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/doc.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/doc.go deleted file mode 100644 index 83dc9a28ac..0000000000 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/doc.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Package badger implements an embeddable, simple and fast key-value database, -written in pure Go. It is designed to be highly performant for both reads and -writes simultaneously. Badger uses Multi-Version Concurrency Control (MVCC), and -supports transactions. It runs transactions concurrently, with serializable -snapshot isolation guarantees. - -Badger uses an LSM tree along with a value log to separate keys from values, -hence reducing both write amplification and the size of the LSM tree. This -allows LSM tree to be served entirely from RAM, while the values are served -from SSD. - - -Usage - -Badger has the following main types: DB, Txn, Item and Iterator. DB contains -keys that are associated with values. It must be opened with the appropriate -options before it can be accessed. - -All operations happen inside a Txn. Txn represents a transaction, which can -be read-only or read-write. Read-only transactions can read values for a -given key (which are returned inside an Item), or iterate over a set of -key-value pairs using an Iterator (which are returned as Item type values as -well). Read-write transactions can also update and delete keys from the DB. - -See the examples for more usage details. -*/ -package badger diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/errors.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/errors.go index b03a24f892..1de35826e9 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/errors.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/errors.go @@ -27,6 +27,10 @@ var ( // range. ErrValueLogSize = errors.New("Invalid ValueLogFileSize, must be between 1MB and 2GB") + // ErrValueThreshold is returned when ValueThreshold is set to a value close to or greater than + // uint16. + ErrValueThreshold = errors.New("Invalid ValueThreshold, must be lower than uint16.") + // ErrKeyNotFound is returned when key isn't found on a txn.Get. ErrKeyNotFound = errors.New("Key not found") @@ -81,9 +85,25 @@ var ( // ErrInvalidLoadingMode is returned when opt.ValueLogLoadingMode option is not // within the valid range ErrInvalidLoadingMode = errors.New("Invalid ValueLogLoadingMode, must be FileIO or MemoryMap") + + // ErrReplayNeeded is returned when opt.ReadOnly is set but the + // database requires a value log replay. + ErrReplayNeeded = errors.New("Database was not properly closed, cannot open read-only") + + // ErrWindowsNotSupported is returned when opt.ReadOnly is used on Windows + ErrWindowsNotSupported = errors.New("Read-only mode is not supported on Windows") + + // ErrTruncateNeeded is returned when the value log gets corrupt, and requires truncation of + // corrupt data to allow Badger to run properly. + ErrTruncateNeeded = errors.New("Value log truncate required to run DB. This might result in data loss.") + + // ErrBlockedWrites is returned if the user called DropAll. During the process of dropping all + // data from Badger, we stop accepting new writes, by returning this error. + ErrBlockedWrites = errors.New("Writes are blocked possibly due to DropAll") ) -const maxKeySize = 1 << 16 // Key length can't be more than uint16, as determined by table::header. +// Key length can't be more than uint16, as determined by table::header. +const maxKeySize = 1<<16 - 8 // 8 bytes are for storing timestamp func exceedsMaxKeySizeError(key []byte) error { return errors.Errorf("Key with size %d exceeded %d limit. Key:\n%s", diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/iterator.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/iterator.go index c7ce634655..3a6bec9819 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/iterator.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/iterator.go @@ -20,6 +20,7 @@ import ( "bytes" "fmt" "sync" + "sync/atomic" "time" "github.com/dgraph-io/badger/options" @@ -53,20 +54,32 @@ type Item struct { txn *Txn } -// ToString returns a string representation of Item -func (item *Item) ToString() string { +// String returns a string representation of Item +func (item *Item) String() string { return fmt.Sprintf("key=%q, version=%d, meta=%x", item.Key(), item.Version(), item.meta) +} +// Deprecated +// ToString returns a string representation of Item +func (item *Item) ToString() string { + return item.String() } // Key returns the key. // // Key is only valid as long as item is valid, or transaction is valid. If you need to use it -// outside its validity, please copy it. +// outside its validity, please use KeyCopy func (item *Item) Key() []byte { return item.key } +// KeyCopy returns a copy of the key of the item, writing it to dst slice. +// If nil is passed, or capacity of dst isn't sufficient, a new slice would be allocated and +// returned. +func (item *Item) KeyCopy(dst []byte) []byte { + return y.SafeCopy(dst, item.key) +} + // Version returns the commit timestamp of the item. func (item *Item) Version() uint64 { return item.version @@ -80,7 +93,8 @@ func (item *Item) Version() uint64 { // reused. // // If you need to use a value outside a transaction, please use Item.ValueCopy -// instead, or copy it yourself. +// instead, or copy it yourself. Value might change once discard or commit is called. +// Use ValueCopy if you want to do a Set after Get. func (item *Item) Value() ([]byte, error) { item.wg.Wait() if item.status == prefetched { @@ -117,24 +131,70 @@ func (item *Item) hasValue() bool { return true } +// IsDeletedOrExpired returns true if item contains deleted or expired value. +func (item *Item) IsDeletedOrExpired() bool { + return isDeletedOrExpired(item.meta, item.expiresAt) +} + +func (item *Item) DiscardEarlierVersions() bool { + return item.meta&bitDiscardEarlierVersions > 0 +} + func (item *Item) yieldItemValue() ([]byte, func(), error) { - if !item.hasValue() { - return nil, nil, nil - } + key := item.Key() // No need to copy. + for { + if !item.hasValue() { + return nil, nil, nil + } - if item.slice == nil { - item.slice = new(y.Slice) - } + if item.slice == nil { + item.slice = new(y.Slice) + } - if (item.meta & bitValuePointer) == 0 { - val := item.slice.Resize(len(item.vptr)) - copy(val, item.vptr) - return val, nil, nil - } + if (item.meta & bitValuePointer) == 0 { + val := item.slice.Resize(len(item.vptr)) + copy(val, item.vptr) + return val, nil, nil + } - var vp valuePointer - vp.Decode(item.vptr) - return item.db.vlog.Read(vp, item.slice) + var vp valuePointer + vp.Decode(item.vptr) + result, cb, err := item.db.vlog.Read(vp, item.slice) + if err != ErrRetry { + return result, cb, err + } + if bytes.HasPrefix(key, badgerMove) { + // err == ErrRetry + // Error is retry even after checking the move keyspace. So, let's + // just assume that value is not present. + return nil, cb, nil + } + + // The value pointer is pointing to a deleted value log. Look for the + // move key and read that instead. + runCallback(cb) + // Do not put badgerMove on the left in append. It seems to cause some sort of manipulation. + key = append([]byte{}, badgerMove...) + key = append(key, y.KeyWithTs(item.Key(), item.Version())...) + // Note that we can't set item.key to move key, because that would + // change the key user sees before and after this call. Also, this move + // logic is internal logic and should not impact the external behavior + // of the retrieval. + vs, err := item.db.get(key) + if err != nil { + return nil, nil, err + } + if vs.Version != item.Version() { + return nil, nil, nil + } + // Bug fix: Always copy the vs.Value into vptr here. Otherwise, when item is reused this + // slice gets overwritten. + item.vptr = y.SafeCopy(item.vptr, vs.Value) + item.meta &^= bitValuePointer // Clear the value pointer bit. + if vs.Meta&bitValuePointer > 0 { + item.meta |= bitValuePointer // This meta would only be about value pointer. + } + } } func runCallback(cb func()) { @@ -235,6 +295,8 @@ type IteratorOptions struct { PrefetchSize int Reverse bool // Direction of iteration. False is forward, true is backward. AllVersions bool // Fetch all valid versions of the same key. + + internalAccess bool // Used to allow internal access to badger keys. } // DefaultIteratorOptions contains default options when iterating over Badger key-value stores. @@ -264,6 +326,10 @@ type Iterator struct { // Using prefetch is highly recommended if you're doing a long running iteration. // Avoid long running iterations in update transactions. func (txn *Txn) NewIterator(opt IteratorOptions) *Iterator { + if atomic.AddInt32(&txn.numIterators, 1) > 1 { + panic("Only one iterator can be active at one time.") + } + tables, decr := txn.db.getMemTables() defer decr() txn.db.vlog.incrIteratorCount() @@ -315,8 +381,22 @@ func (it *Iterator) ValidForPrefix(prefix []byte) bool { // Close would close the iterator. It is important to call this when you're done with iteration. func (it *Iterator) Close() { it.iitr.Close() + + // It is important to wait for the fill goroutines to finish. Otherwise, we might leave zombie + // goroutines behind, which are waiting to acquire file read locks after DB has been closed. + waitFor := func(l list) { + item := l.pop() + for item != nil { + item.wg.Wait() + item = l.pop() + } + } + waitFor(it.waste) + waitFor(it.data) + // TODO: We could handle this error. _ = it.txn.db.vlog.decrIteratorCount() + atomic.AddInt32(&it.txn.numIterators, -1) } // Next would advance the iterator by one. Always check it.Valid() after a Next() @@ -367,7 +447,7 @@ func (it *Iterator) parseItem() bool { } // Skip badger keys. - if bytes.HasPrefix(key, badgerPrefix) { + if !it.opt.internalAccess && bytes.HasPrefix(key, badgerPrefix) { mi.Next() return false } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/levels.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/levels.go index 55002b4fa6..31b7fe6bfd 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/levels.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/levels.go @@ -18,6 +18,7 @@ package badger import ( "fmt" + "math" "math/rand" "os" "sort" @@ -104,7 +105,11 @@ func newLevelsController(kv *DB, mf *Manifest) (*levelsController, error) { var maxFileID uint64 for fileID, tableManifest := range mf.Tables { fname := table.NewFilename(fileID, kv.opt.Dir) - fd, err := y.OpenExistingSyncedFile(fname, true) + var flags uint32 = y.Sync + if kv.opt.ReadOnly { + flags |= y.ReadOnly + } + fd, err := y.OpenExistingFile(fname, flags) if err != nil { closeAllTables(tables) return nil, errors.Wrapf(err, "Opening file: %q", fname) @@ -165,6 +170,62 @@ func (s *levelsController) cleanupLevels() error { return firstErr } +// This function picks all tables from all levels, creates a manifest changeset, +// applies it, and then decrements the refs of these tables, which would result +// in their deletion. It spares one table from L0, to keep the badgerHead key +// persisted, so we don't lose where we are w.r.t. value log. +// NOTE: This function in itself isn't sufficient to completely delete all the +// data. After this, one would still need to iterate over the KV pairs and mark +// them as deleted. +func (s *levelsController) deleteLSMTree() (int, error) { + var all []*table.Table + var keepOne *table.Table + for _, l := range s.levels { + l.RLock() + if l.level == 0 && len(l.tables) > 1 { + // Skip the last table. We do this to keep the badgerMove key persisted. + lastIdx := len(l.tables) - 1 + keepOne = l.tables[lastIdx] + all = append(all, l.tables[:lastIdx]...) + } else { + all = append(all, l.tables...) + } + l.RUnlock() + } + if len(all) == 0 { + return 0, nil + } + + // Generate the manifest changes. + changes := []*protos.ManifestChange{} + for _, table := range all { + changes = append(changes, makeTableDeleteChange(table.ID())) + } + changeSet := protos.ManifestChangeSet{Changes: changes} + if err := s.kv.manifest.addChanges(changeSet.Changes); err != nil { + return 0, err + } + + for _, l := range s.levels { + l.Lock() + l.totalSize = 0 + if l.level == 0 && len(l.tables) > 1 { + l.tables = []*table.Table{keepOne} + l.totalSize += keepOne.Size() + } else { + l.tables = l.tables[:0] + } + l.Unlock() + } + // Now allow deletion of tables. + for _, table := range all { + if err := table.DecrRef(); err != nil { + return 0, err + } + } + return len(all), nil +} + func (s *levelsController) startCompact(lc *y.Closer) { n := s.kv.opt.NumCompactors lc.AddRunning(n - 1) @@ -258,6 +319,35 @@ func (s *levelsController) compactBuildTables( topTables := cd.top botTables := cd.bot + var hasOverlap bool + { + kr := getKeyRange(cd.top) + for i, lh := range s.levels { + if i <= l { // Skip upper levels. + continue + } + lh.RLock() + left, right := lh.overlappingTables(levelHandlerRLocked{}, kr) + lh.RUnlock() + if right-left > 0 { + hasOverlap = true + break + } + } + cd.elog.LazyPrintf("Key range overlaps with lower levels: %v", hasOverlap) + } + + // Try to collect stats so that we can inform value log about GC. That would help us find which + // value log file should be GCed. + discardStats := make(map[uint32]int64) + updateStats := func(vs y.ValueStruct) { + if vs.Meta&bitValuePointer > 0 { + var vp valuePointer + vp.Decode(vs.Value) + discardStats[vp.Fid] += int64(vp.Len) + } + } + // Create iterators across all the tables involved first. var iters []y.Iterator if l == 0 { @@ -274,54 +364,111 @@ func (s *levelsController) compactBuildTables( it.Rewind() + // Pick a discard ts, so we can discard versions below this ts. We should + // never discard any versions starting from above this timestamp, because + // that would affect the snapshot view guarantee provided by transactions. + discardTs := s.kv.orc.discardAtOrBelow() + // Start generating new tables. type newTableResult struct { table *table.Table err error } resultCh := make(chan newTableResult) - var i int - for ; it.Valid(); i++ { + var numBuilds, numVersions int + var lastKey, skipKey []byte + for it.Valid() { timeStart := time.Now() builder := table.NewTableBuilder() + var numKeys, numSkips uint64 for ; it.Valid(); it.Next() { - if builder.ReachedCapacity(s.kv.opt.MaxTableSize) { - break + // See if we need to skip this key. + if len(skipKey) > 0 { + if y.SameKey(it.Key(), skipKey) { + numSkips++ + updateStats(it.Value()) + continue + } else { + skipKey = skipKey[:0] + } + } + + if !y.SameKey(it.Key(), lastKey) { + if builder.ReachedCapacity(s.kv.opt.MaxTableSize) { + // Only break if we are on a different key, and have reached capacity. We want + // to ensure that all versions of the key are stored in the same sstable, and + // not divided across multiple tables at the same level. + break + } + lastKey = y.SafeCopy(lastKey, it.Key()) + numVersions = 0 } + + vs := it.Value() + version := y.ParseTs(it.Key()) + if version <= discardTs { + // Keep track of the number of versions encountered for this key. Only consider the + // versions which are below the minReadTs, otherwise, we might end up discarding the + // only valid version for a running transaction. + numVersions++ + lastValidVersion := vs.Meta&bitDiscardEarlierVersions > 0 + if isDeletedOrExpired(vs.Meta, vs.ExpiresAt) || + numVersions > s.kv.opt.NumVersionsToKeep || + lastValidVersion { + // If this version of the key is deleted or expired, skip all the rest of the + // versions. Ensure that we're only removing versions below readTs. + skipKey = y.SafeCopy(skipKey, it.Key()) + + if lastValidVersion { + // Add this key. We have set skipKey, so the following key versions + // would be skipped. + } else if hasOverlap { + // If this key range has overlap with lower levels, then keep the deletion + // marker with the latest version, discarding the rest. We have set skipKey, + // so the following key versions would be skipped. + } else { + // If no overlap, we can skip all the versions, by continuing here. + numSkips++ + updateStats(vs) + continue // Skip adding this key. + } + } + } + numKeys++ y.Check(builder.Add(it.Key(), it.Value())) } // It was true that it.Valid() at least once in the loop above, which means we // called Add() at least once, and builder is not Empty(). - y.AssertTrue(!builder.Empty()) - - cd.elog.LazyPrintf("LOG Compact. Iteration to generate one table took: %v\n", time.Since(timeStart)) - - fileID := s.reserveFileID() - go func(builder *table.Builder) { - defer builder.Close() - - fd, err := y.CreateSyncedFile(table.NewFilename(fileID, s.kv.opt.Dir), true) - if err != nil { - resultCh <- newTableResult{nil, errors.Wrapf(err, "While opening new table: %d", fileID)} - return - } + cd.elog.LazyPrintf("Added %d keys. Skipped %d keys.", numKeys, numSkips) + cd.elog.LazyPrintf("LOG Compact. Iteration took: %v\n", time.Since(timeStart)) + if !builder.Empty() { + numBuilds++ + fileID := s.reserveFileID() + go func(builder *table.Builder) { + defer builder.Close() + + fd, err := y.CreateSyncedFile(table.NewFilename(fileID, s.kv.opt.Dir), true) + if err != nil { + resultCh <- newTableResult{nil, errors.Wrapf(err, "While opening new table: %d", fileID)} + return + } - if _, err := fd.Write(builder.Finish()); err != nil { - resultCh <- newTableResult{nil, errors.Wrapf(err, "Unable to write to file: %d", fileID)} - return - } + if _, err := fd.Write(builder.Finish()); err != nil { + resultCh <- newTableResult{nil, errors.Wrapf(err, "Unable to write to file: %d", fileID)} + return + } - tbl, err := table.OpenTable(fd, s.kv.opt.TableLoadingMode) - // decrRef is added below. - resultCh <- newTableResult{tbl, errors.Wrapf(err, "Unable to open table: %q", fd.Name())} - }(builder) + tbl, err := table.OpenTable(fd, s.kv.opt.TableLoadingMode) + // decrRef is added below. + resultCh <- newTableResult{tbl, errors.Wrapf(err, "Unable to open table: %q", fd.Name())} + }(builder) + } } newTables := make([]*table.Table, 0, 20) - // Wait for all table builders to finish. var firstErr error - for x := 0; x < i; x++ { + for x := 0; x < numBuilds; x++ { res := <-resultCh newTables = append(newTables, res.table) if firstErr == nil { @@ -339,7 +486,7 @@ func (s *levelsController) compactBuildTables( if firstErr != nil { // An error happened. Delete all the newly created table files (by calling DecrRef // -- we're the only holders of a ref). - for j := 0; j < i; j++ { + for j := 0; j < numBuilds; j++ { if newTables[j] != nil { newTables[j].DecrRef() } @@ -351,7 +498,8 @@ func (s *levelsController) compactBuildTables( sort.Slice(newTables, func(i, j int) bool { return y.CompareKeys(newTables[i].Biggest(), newTables[j].Biggest()) < 0 }) - + s.kv.vlog.updateGCStats(discardStats) + cd.elog.LazyPrintf("Discard stats: %v", discardStats) return newTables, func() error { return decrRefs(newTables) }, nil } @@ -442,8 +590,10 @@ func (s *levelsController) fillTables(cd *compactDef) bool { for _, t := range tbls { cd.thisSize = t.Size() cd.thisRange = keyRange{ - left: t.Smallest(), - right: t.Biggest(), + // We pick all the versions of the smallest and the biggest key. + left: y.KeyWithTs(y.ParseKey(t.Smallest()), math.MaxUint64), + // Note that version zero would be the rightmost key. + right: y.KeyWithTs(y.ParseKey(t.Biggest()), 0), } if s.cstatus.overlapsWith(cd.thisLevel.level, cd.thisRange) { continue @@ -482,40 +632,8 @@ func (s *levelsController) runCompactDef(l int, cd compactDef) (err error) { thisLevel := cd.thisLevel nextLevel := cd.nextLevel - if thisLevel.level >= 1 && len(cd.bot) == 0 { - y.AssertTrue(len(cd.top) == 1) - tbl := cd.top[0] - - // We write to the manifest _before_ we delete files (and after we created files). - changes := []*protos.ManifestChange{ - // The order matters here -- you can't temporarily have two copies of the same - // table id when reloading the manifest. - makeTableDeleteChange(tbl.ID()), - makeTableCreateChange(tbl.ID(), nextLevel.level), - } - if err := s.kv.manifest.addChanges(changes); err != nil { - return err - } - - // We have to add to nextLevel before we remove from thisLevel, not after. This way, we - // don't have a bug where reads would see keys missing from both levels. - - // Note: It's critical that we add tables (replace them) in nextLevel before deleting them - // in thisLevel. (We could finagle it atomically somehow.) Also, when reading we must - // read, or at least acquire s.RLock(), in increasing order by level, so that we don't skip - // a compaction. - - if err := nextLevel.replaceTables(cd.top); err != nil { - return err - } - if err := thisLevel.deleteTables(cd.top); err != nil { - return err - } - - cd.elog.LazyPrintf("\tLOG Compact-Move %d->%d smallest:%s biggest:%s took %v\n", - l, l+1, string(tbl.Smallest()), string(tbl.Biggest()), time.Since(timeStart)) - return nil - } + // Table should never be moved directly between levels, always be rewritten to allow discarding + // invalid versions. newTables, decr, err := s.compactBuildTables(l, cd) if err != nil { @@ -557,7 +675,7 @@ func (s *levelsController) doCompact(p compactionPriority) (bool, error) { y.AssertTrue(l+1 < s.kv.opt.MaxLevels) // Sanity check. cd := compactDef{ - elog: trace.New("Badger", "Compact"), + elog: trace.New(fmt.Sprintf("Badger.L%d", l), "Compact"), thisLevel: s.levels[l], nextLevel: s.levels[l+1], } @@ -580,6 +698,7 @@ func (s *levelsController) doCompact(p compactionPriority) (bool, error) { return false, nil } } + defer s.cstatus.delete(cd) // Remove the ranges from compaction status. cd.elog.LazyPrintf("Running for level: %d\n", cd.thisLevel.level) s.cstatus.toLog(cd.elog) @@ -589,8 +708,6 @@ func (s *levelsController) doCompact(p compactionPriority) (bool, error) { return false, err } - // Done with compaction. So, remove the ranges from compaction status. - s.cstatus.delete(cd) s.cstatus.toLog(cd.elog) cd.elog.LazyPrintf("Compaction for level: %d DONE", cd.thisLevel.level) return true, nil @@ -625,7 +742,7 @@ func (s *levelsController) addLevel0Table(t *table.Table) error { // Before we unstall, we need to make sure that level 0 and 1 are healthy. Otherwise, we // will very quickly fill up level 0 again and if the compaction strategy favors level 0, // then level 1 is going to super full. - for { + for i := 0; ; i++ { // Passing 0 for delSize to compactable means we're treating incomplete compactions as // not having finished -- we wait for them to finish. Also, it's crucial this behavior // replicates pickCompactLevels' behavior in computing compactability in order to @@ -634,6 +751,11 @@ func (s *levelsController) addLevel0Table(t *table.Table) error { break } time.Sleep(10 * time.Millisecond) + if i%100 == 0 { + prios := s.pickCompactLevels() + s.elog.Printf("Waiting to add level 0 table. Compaction priorities: %+v\n", prios) + i = 0 + } } { s.elog.Printf("UNSTALLED UNSTALLED UNSTALLED UNSTALLED UNSTALLED UNSTALLED: %v\n", @@ -651,14 +773,12 @@ func (s *levelsController) close() error { } // get returns the found value if any. If not found, we return nil. -func (s *levelsController) get(key []byte, maxVs y.ValueStruct) (y.ValueStruct, error) { +func (s *levelsController) get(key []byte) (y.ValueStruct, error) { // It's important that we iterate the levels from 0 on upward. The reason is, if we iterated // in opposite order, or in parallel (naively calling all the h.RLock() in some order) we could // read level L's tables post-compaction and level L+1's tables pre-compaction. (If we do // parallelize this, we will need to call the h.RLock() function by increasing order of level // number.) - - version := y.ParseTs(key) for _, h := range s.levels { vs, err := h.get(key) // Calls h.RLock() and h.RUnlock(). if err != nil { @@ -667,14 +787,9 @@ func (s *levelsController) get(key []byte, maxVs y.ValueStruct) (y.ValueStruct, if vs.Value == nil && vs.Meta == 0 { continue } - if vs.Version == version { - return vs, nil - } - if maxVs.Version < vs.Version { - maxVs = vs - } + return vs, nil } - return maxVs, nil + return y.ValueStruct{}, nil } func appendIteratorsReversed(out []y.Iterator, th []*table.Table, reversed bool) []y.Iterator { @@ -696,3 +811,31 @@ func (s *levelsController) appendIterators( } return iters } + +type TableInfo struct { + ID uint64 + Level int + Left []byte + Right []byte +} + +func (s *levelsController) getTableInfo() (result []TableInfo) { + for _, l := range s.levels { + for _, t := range l.tables { + info := TableInfo{ + ID: t.ID(), + Level: l.level, + Left: t.Smallest(), + Right: t.Biggest(), + } + result = append(result, info) + } + } + sort.Slice(result, func(i, j int) bool { + if result[i].Level != result[j].Level { + return result[i].Level < result[j].Level + } + return result[i].ID < result[j].ID + }) + return +} diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/managed_db.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/managed_db.go index 3b52c1e784..c3898130e9 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/managed_db.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/managed_db.go @@ -16,6 +16,16 @@ package badger +import ( + "math" + "sync" + "sync/atomic" + "time" + + "github.com/dgraph-io/badger/y" + "github.com/pkg/errors" +) + // ManagedDB allows end users to manage the transactions themselves. Transaction // start and commit timestamps are set by end-user. // @@ -72,15 +82,112 @@ func (txn *Txn) CommitAt(commitTs uint64, callback func(error)) error { return txn.Commit(callback) } -// PurgeVersionsBelow will delete all versions of a key below the specified version -func (db *ManagedDB) PurgeVersionsBelow(key []byte, ts uint64) error { - txn := db.NewTransactionAt(ts, false) - defer txn.Discard() - return db.purgeVersionsBelow(txn, key, ts) -} - // GetSequence is not supported on ManagedDB. Calling this would result // in a panic. func (db *ManagedDB) GetSequence(_ []byte, _ uint64) (*Sequence, error) { panic("Cannot use GetSequence for ManagedDB.") } + +// SetDiscardTs sets a timestamp at or below which, any invalid or deleted +// versions can be discarded from the LSM tree, and thence from the value log to +// reclaim disk space. +func (db *ManagedDB) SetDiscardTs(ts uint64) { + db.orc.setDiscardTs(ts) +} + +var errDone = errors.New("Done deleting keys") + +// DropAll would drop all the data stored in Badger. It does this in the following way. +// - Stop accepting new writes. +// - Pause the compactions. +// - Pick all tables from all levels, create a changeset to delete all these +// tables and apply it to manifest. DO not pick up the latest table from level +// 0, to preserve the (persistent) badgerHead key. +// - Iterate over the KVs in Level 0, and run deletes on them via transactions. +// - The deletions are done at the same timestamp as the latest version of the +// key. Thus, we could write the keys back at the same timestamp as before. +func (db *ManagedDB) DropAll() error { + // Stop accepting new writes. + atomic.StoreInt32(&db.blockWrites, 1) + + // Wait for writeCh to reach size of zero. This is not ideal, but a very + // simple way to allow writeCh to flush out, before we proceed. + tick := time.NewTicker(100 * time.Millisecond) + for range tick.C { + if len(db.writeCh) == 0 { + break + } + } + tick.Stop() + + // Stop the compactions. + if db.closers.compactors != nil { + db.closers.compactors.SignalAndWait() + } + + _, err := db.lc.deleteLSMTree() + // Allow writes so that we can run transactions. Ideally, the user must ensure that they're not + // doing more writes concurrently while this operation is happening. + atomic.StoreInt32(&db.blockWrites, 0) + // Need compactions to happen so deletes below can be flushed out. + if db.closers.compactors != nil { + db.closers.compactors = y.NewCloser(1) + db.lc.startCompact(db.closers.compactors) + } + if err != nil { + return err + } + + type KV struct { + key []byte + version uint64 + } + + var kvs []KV + getKeys := func() error { + txn := db.NewTransactionAt(math.MaxUint64, false) + defer txn.Discard() + + opts := DefaultIteratorOptions + opts.PrefetchValues = false + itr := txn.NewIterator(opts) + defer itr.Close() + + for itr.Rewind(); itr.Valid(); itr.Next() { + item := itr.Item() + kvs = append(kvs, KV{item.KeyCopy(nil), item.Version()}) + } + return nil + } + if err := getKeys(); err != nil { + return err + } + + var wg sync.WaitGroup + errCh := make(chan error, 1) + for _, kv := range kvs { + wg.Add(1) + txn := db.NewTransactionAt(math.MaxUint64, true) + if err := txn.Delete(kv.key); err != nil { + return err + } + if err := txn.CommitAt(kv.version, func(rerr error) { + if rerr != nil { + select { + case errCh <- rerr: + default: + } + } + wg.Done() + }); err != nil { + return err + } + } + wg.Wait() + select { + case err := <-errCh: + return err + default: + return nil + } +} diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/manifest.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/manifest.go index f2e57d9b06..e16bbfb467 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/manifest.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/manifest.go @@ -32,7 +32,7 @@ import ( "github.com/pkg/errors" ) -// Manifest represnts the contents of the MANIFEST file in a Badger store. +// Manifest represents the contents of the MANIFEST file in a Badger store. // // The MANIFEST file describes the startup state of the db -- all LSM files and what level they're // at. @@ -112,17 +112,24 @@ func (m *Manifest) clone() Manifest { // openOrCreateManifestFile opens a Badger manifest file if it exists, or creates on if // one doesn’t. -func openOrCreateManifestFile(dir string) (ret *manifestFile, result Manifest, err error) { - return helpOpenOrCreateManifestFile(dir, manifestDeletionsRewriteThreshold) +func openOrCreateManifestFile(dir string, readOnly bool) (ret *manifestFile, result Manifest, err error) { + return helpOpenOrCreateManifestFile(dir, readOnly, manifestDeletionsRewriteThreshold) } -func helpOpenOrCreateManifestFile(dir string, deletionsThreshold int) (ret *manifestFile, result Manifest, err error) { +func helpOpenOrCreateManifestFile(dir string, readOnly bool, deletionsThreshold int) (ret *manifestFile, result Manifest, err error) { path := filepath.Join(dir, ManifestFilename) - fp, err := y.OpenExistingSyncedFile(path, false) // We explicitly sync in addChanges, outside the lock. + var flags uint32 + if readOnly { + flags |= y.ReadOnly + } + fp, err := y.OpenExistingFile(path, flags) // We explicitly sync in addChanges, outside the lock. if err != nil { if !os.IsNotExist(err) { return nil, Manifest{}, err } + if readOnly { + return nil, Manifest{}, fmt.Errorf("no manifest found, required for read-only db") + } m := createManifest() fp, netCreations, err := helpRewrite(dir, &m) if err != nil { @@ -144,12 +151,13 @@ func helpOpenOrCreateManifestFile(dir string, deletionsThreshold int) (ret *mani return nil, Manifest{}, err } - // Truncate file so we don't have a half-written entry at the end. - if err := fp.Truncate(truncOffset); err != nil { - _ = fp.Close() - return nil, Manifest{}, err + if !readOnly { + // Truncate file so we don't have a half-written entry at the end. + if err := fp.Truncate(truncOffset); err != nil { + _ = fp.Close() + return nil, Manifest{}, err + } } - if _, err = fp.Seek(0, io.SeekEnd); err != nil { _ = fp.Close() return nil, Manifest{}, err @@ -256,7 +264,7 @@ func helpRewrite(dir string, m *Manifest) (*os.File, int, error) { if err := os.Rename(rewritePath, manifestPath); err != nil { return nil, 0, err } - fp, err = y.OpenExistingSyncedFile(manifestPath, false) + fp, err = y.OpenExistingFile(manifestPath, 0) if err != nil { return nil, 0, err } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/options.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/options.go index 9b8bc6ee99..21fc2a5a3a 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/options.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/options.go @@ -46,9 +46,12 @@ type Options struct { // How should LSM tree be accessed. TableLoadingMode options.FileLoadingMode - // How should value log be accessed + // How should value log be accessed. ValueLogLoadingMode options.FileLoadingMode + // How many versions to keep per key. + NumVersionsToKeep int + // 3. Flags that user might want to review // ---------------------------------------- // The following affect all levels of LSM tree. @@ -73,6 +76,10 @@ type Options struct { // Size of single value log file. ValueLogFileSize int64 + // Max number of entries a value log file can hold (approximately). A value log file would be + // determined by the smaller of its file size and max entries. + ValueLogMaxEntries uint32 + // Number of compaction workers to run concurrently. NumCompactors int @@ -86,6 +93,15 @@ type Options struct { maxBatchCount int64 // max entries in batch maxBatchSize int64 // max batch size in bytes + + // Open the DB as read-only. With this set, multiple processes can + // open the same Badger DB. Note: if the DB being opened had crashed + // before and has vlog data to be replayed, ReadOnly will cause Open + // to fail with an appropriate message. + ReadOnly bool + + // Truncate value log to delete corrupt data, if any. Would not truncate if ReadOnly is set. + Truncate bool } // DefaultOptions sets a list of recommended options for good performance. @@ -105,8 +121,27 @@ var DefaultOptions = Options{ NumLevelZeroTablesStall: 10, NumMemtables: 5, SyncWrites: true, + NumVersionsToKeep: 1, // Nothing to read/write value log using standard File I/O // MemoryMap to mmap() the value log files - ValueLogFileSize: 1 << 30, - ValueThreshold: 20, + // (2^30 - 1)*2 when mmapping < 2^31 - 1, max int32. + // -1 so 2*ValueLogFileSize won't overflow on 32-bit systems. + ValueLogFileSize: 1<<30 - 1, + + ValueLogMaxEntries: 1000000, + ValueThreshold: 32, + Truncate: false, +} + +// LSMOnlyOptions follows from DefaultOptions, but sets a higher ValueThreshold so values would +// be colocated with the LSM tree, with value log largely acting as a write-ahead log only. These +// options would reduce the disk usage of value log, and make Badger act like a typical LSM tree. +var LSMOnlyOptions = Options{} + +func init() { + LSMOnlyOptions = DefaultOptions + + LSMOnlyOptions.ValueThreshold = 65500 // Max value length which fits in uint16. + LSMOnlyOptions.ValueLogFileSize = 64 << 20 // Allow easy space reclamation. + LSMOnlyOptions.ValueLogLoadingMode = options.FileIO } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/arena.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/arena.go index 849e5ee564..def550712f 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/arena.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/arena.go @@ -25,7 +25,12 @@ import ( const ( offsetSize = int(unsafe.Sizeof(uint32(0))) - ptrAlign = int(unsafe.Sizeof(uintptr(0))) - 1 + + // Always align nodes on 64-bit boundaries, even on 32-bit architectures, + // so that the node.value field is 64-bit aligned. This is necessary because + // node.getValueOffset uses atomic.LoadUint64, which expects its input + // pointer to be 64-bit aligned. + nodeAlign = int(unsafe.Sizeof(uint64(0))) - 1 ) // Arena should be lock-free. @@ -61,14 +66,14 @@ func (s *Arena) putNode(height int) uint32 { unusedSize := (maxHeight - height) * offsetSize // Pad the allocation with enough bytes to ensure pointer alignment. - l := uint32(MaxNodeSize - unusedSize + ptrAlign) + l := uint32(MaxNodeSize - unusedSize + nodeAlign) n := atomic.AddUint32(&s.n, l) y.AssertTruef(int(n) <= len(s.buf), "Arena too small, toWrite:%d newTotal:%d limit:%d", l, n, len(s.buf)) // Return the aligned offset. - m := (n - l + uint32(ptrAlign)) & ^uint32(ptrAlign) + m := (n - l + uint32(nodeAlign)) & ^uint32(nodeAlign) return m } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/skl.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/skl.go index 7361751a50..b465b09ecc 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/skl.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/skl/skl.go @@ -54,8 +54,7 @@ type node struct { // can be atomically loaded and stored: // value offset: uint32 (bits 0-31) // value size : uint16 (bits 32-47) - // 12 bytes are allocated to ensure 8 byte alignment also on 32bit systems. - value [12]byte + value uint64 // A byte slice is 24 bytes. We are trying to save space here. keyOffset uint32 // Immutable. No need to lock to access key. @@ -109,7 +108,7 @@ func newNode(arena *Arena, key []byte, v y.ValueStruct, height int) *node { node.keyOffset = arena.putKey(key) node.keySize = uint16(len(key)) node.height = uint16(height) - *node.value64BitAlignedPtr() = encodeValue(arena.putVal(v), v.EncodedSize()) + node.value = encodeValue(arena.putVal(v), v.EncodedSize()) return node } @@ -135,15 +134,8 @@ func NewSkiplist(arenaSize int64) *Skiplist { } } -func (s *node) value64BitAlignedPtr() *uint64 { - if uintptr(unsafe.Pointer(&s.value))%8 == 0 { - return (*uint64)(unsafe.Pointer(&s.value)) - } - return (*uint64)(unsafe.Pointer(&s.value[4])) -} - func (s *node) getValueOffset() (uint32, uint16) { - value := atomic.LoadUint64(s.value64BitAlignedPtr()) + value := atomic.LoadUint64(&s.value) return decodeValue(value) } @@ -154,7 +146,7 @@ func (s *node) key(arena *Arena) []byte { func (s *node) setValue(arena *Arena, v y.ValueStruct) { valOffset := arena.putVal(v) value := encodeValue(valOffset, v.EncodedSize()) - atomic.StoreUint64(s.value64BitAlignedPtr(), value) + atomic.StoreUint64(&s.value, value) } func (s *node) getNextOffset(h int) uint32 { diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/builder.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/builder.go index 7ce0d61f9e..43e6562239 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/builder.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/builder.go @@ -212,7 +212,7 @@ func (b *Builder) Finish() []byte { } kl := int(binary.BigEndian.Uint16(klen[:])) if cap(key) < kl { - key = make([]byte, 2*kl) + key = make([]byte, 2*int(kl)) // 2 * uint16 will overflow } key = key[:kl] y.Check2(b.keyBuf.Read(key)) diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/iterator.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/iterator.go index 533fe2a6ed..0eb5ed01a9 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/iterator.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/iterator.go @@ -341,6 +341,7 @@ func (itr *Iterator) next() { } itr.bi = block.NewIterator() itr.bi.SeekToFirst() + itr.err = itr.bi.Error() return } @@ -368,6 +369,7 @@ func (itr *Iterator) prev() { } itr.bi = block.NewIterator() itr.bi.SeekToLast() + itr.err = itr.bi.Error() return } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/table.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/table.go index 52a17b17ab..9804fa1764 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/table.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/table/table.go @@ -22,7 +22,6 @@ import ( "os" "path" "path/filepath" - "sort" "strconv" "strings" "sync" @@ -102,12 +101,6 @@ func (b block) NewIterator() *blockIterator { return &blockIterator{data: b.data} } -type byKey []keyOffset - -func (b byKey) Len() int { return len(b) } -func (b byKey) Swap(i int, j int) { b[i], b[j] = b[j], b[i] } -func (b byKey) Less(i int, j int) bool { return y.CompareKeys(b[i].key, b[j].key) < 0 } - // OpenTable assumes file has only one table and opens it. Takes ownership of fd upon function // entry. Returns a table with one reference count on it (decrementing which may delete the file! // -- consider t.Close() instead). The fd has to writeable because we call Truncate on it before @@ -290,7 +283,6 @@ func (t *Table) readIndex() error { return readError } - sort.Sort(byKey(t.blockIndex)) return nil } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/transaction.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/transaction.go index ac9d1a25b8..32a6a96aec 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/transaction.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/transaction.go @@ -18,8 +18,6 @@ package badger import ( "bytes" - "container/heap" - "fmt" "math" "sort" "strconv" @@ -32,32 +30,20 @@ import ( "github.com/pkg/errors" ) -type uint64Heap []uint64 - -func (u uint64Heap) Len() int { return len(u) } -func (u uint64Heap) Less(i int, j int) bool { return u[i] < u[j] } -func (u uint64Heap) Swap(i int, j int) { u[i], u[j] = u[j], u[i] } -func (u *uint64Heap) Push(x interface{}) { *u = append(*u, x.(uint64)) } -func (u *uint64Heap) Pop() interface{} { - old := *u - n := len(old) - x := old[n-1] - *u = old[0 : n-1] - return x -} - type oracle struct { + // curRead must be at the top for memory alignment. See issue #311. curRead uint64 // Managed by the mutex. refCount int64 isManaged bool // Does not change value, so no locking required. sync.Mutex + writeLock sync.Mutex nextCommit uint64 - // These two structures are used to figure out when a commit is done. The minimum done commit is - // used to update curRead. - commitMark uint64Heap - pendingCommits map[uint64]struct{} + // Either of these is used to determine which versions can be permanently + // discarded during compaction. + discardTs uint64 // Used by ManagedDB. + readMark y.WaterMark // Used by DB. // commits stores a key fingerprint and latest commit counter for it. // refCount is used to clear out commits map to avoid a memory blowup. @@ -70,15 +56,14 @@ func (o *oracle) addRef() { func (o *oracle) decrRef() { if count := atomic.AddInt64(&o.refCount, -1); count == 0 { - // Clear out pendingCommits maps to release memory. + // Clear out commits maps to release memory. o.Lock() - // There could be race here, so check again. - // Checking commitMark is safe since it is protected by mutex. - if len(o.commitMark) > 0 { + // Avoids the race where something new is added to commitsMap + // after we check refCount and before we take Lock. + if atomic.LoadInt64(&o.refCount) != 0 { o.Unlock() return } - y.AssertTrue(len(o.pendingCommits) == 0) if len(o.commits) >= 1000 { // If the map is still small, let it slide. o.commits = make(map[uint64]uint64) } @@ -99,6 +84,23 @@ func (o *oracle) commitTs() uint64 { return o.nextCommit } +// Any deleted or invalid versions at or below ts would be discarded during +// compaction to reclaim disk space in LSM tree and thence value log. +func (o *oracle) setDiscardTs(ts uint64) { + o.Lock() + defer o.Unlock() + o.discardTs = ts +} + +func (o *oracle) discardAtOrBelow() uint64 { + if o.isManaged { + o.Lock() + defer o.Unlock() + return o.discardTs + } + return o.readMark.MinReadTs() +} + // hasConflict must be called while having a lock. func (o *oracle) hasConflict(txn *Txn) bool { if len(txn.reads) == 0 { @@ -134,15 +136,6 @@ func (o *oracle) newCommitTs(txn *Txn) uint64 { for _, w := range txn.writes { o.commits[w] = ts // Update the commitTs. } - if o.isManaged { - // No need to update the heap. - return ts - } - heap.Push(&o.commitMark, ts) - if _, has := o.pendingCommits[ts]; has { - panic(fmt.Sprintf("We shouldn't have the commit ts: %d", ts)) - } - o.pendingCommits[ts] = struct{}{} return ts } @@ -151,29 +144,14 @@ func (o *oracle) doneCommit(cts uint64) { // No need to update anything. return } - o.Lock() - defer o.Unlock() - if _, has := o.pendingCommits[cts]; !has { - panic(fmt.Sprintf("We should already have the commit ts: %d", cts)) - } - delete(o.pendingCommits, cts) - - var min uint64 - for len(o.commitMark) > 0 { - ts := o.commitMark[0] - if _, has := o.pendingCommits[ts]; has { - // Still waiting for a txn to commit. - break + for { + curRead := atomic.LoadUint64(&o.curRead) + if cts <= curRead { + return } - min = ts - heap.Pop(&o.commitMark) + atomic.CompareAndSwapUint64(&o.curRead, curRead, cts) } - if min == 0 { - return - } - atomic.StoreUint64(&o.curRead, min) - // nextCommit must never be reset. } // Txn represents a Badger transaction. @@ -191,8 +169,9 @@ type Txn struct { callbacks []func() discarded bool - size int64 - count int64 + size int64 + count int64 + numIterators int32 } type pendingWritesIterator struct { @@ -285,6 +264,9 @@ func (txn *Txn) checkSize(e *Entry) error { // // It will return ErrReadOnlyTxn if update flag was set to false when creating the // transaction. +// +// The current transaction keeps a reference to the key and val byte slice +// arguments. Users must not modify key and val until the end of the transaction. func (txn *Txn) Set(key, val []byte) error { e := &Entry{ Key: key, @@ -294,36 +276,66 @@ func (txn *Txn) Set(key, val []byte) error { } // SetWithMeta adds a key-value pair to the database, along with a metadata -// byte. This byte is stored alongside the key, and can be used as an aid to +// byte. +// +// This byte is stored alongside the key, and can be used as an aid to // interpret the value or store other contextual bits corresponding to the // key-value pair. +// +// The current transaction keeps a reference to the key and val byte slice +// arguments. Users must not modify key and val until the end of the transaction. func (txn *Txn) SetWithMeta(key, val []byte, meta byte) error { e := &Entry{Key: key, Value: val, UserMeta: meta} return txn.SetEntry(e) } +// SetWithDiscard acts like SetWithMeta, but adds a marker to discard earlier +// versions of the key. +// +// This method is only useful if you have set a higher limit for +// options.NumVersionsToKeep. The default setting is 1, in which case, this +// function doesn't add any more benefit than just calling the normal +// SetWithMeta (or Set) function. If however, you have a higher setting for +// NumVersionsToKeep (in Dgraph, we set it to infinity), you can use this method +// to indicate that all the older versions can be discarded and removed during +// compactions. +// +// The current transaction keeps a reference to the key and val byte slice +// arguments. Users must not modify key and val until the end of the +// transaction. +func (txn *Txn) SetWithDiscard(key, val []byte, meta byte) error { + e := &Entry{ + Key: key, + Value: val, + UserMeta: meta, + meta: bitDiscardEarlierVersions, + } + return txn.SetEntry(e) +} + // SetWithTTL adds a key-value pair to the database, along with a time-to-live -// (TTL) setting. A key stored with with a TTL would automatically expire after -// the time has elapsed , and be eligible for garbage collection. +// (TTL) setting. A key stored with a TTL would automatically expire after the +// time has elapsed , and be eligible for garbage collection. +// +// The current transaction keeps a reference to the key and val byte slice +// arguments. Users must not modify key and val until the end of the +// transaction. func (txn *Txn) SetWithTTL(key, val []byte, dur time.Duration) error { expire := time.Now().Add(dur).Unix() e := &Entry{Key: key, Value: val, ExpiresAt: uint64(expire)} return txn.SetEntry(e) } -// SetEntry takes an Entry struct and adds the key-value pair in the struct, along -// with other metadata to the database. -func (txn *Txn) SetEntry(e *Entry) error { - switch { - case !txn.update: +func (txn *Txn) modify(e *Entry) error { + if !txn.update { return ErrReadOnlyTxn - case txn.discarded: + } else if txn.discarded { return ErrDiscardedTxn - case len(e.Key) == 0: + } else if len(e.Key) == 0 { return ErrEmptyKey - case len(e.Key) > maxKeySize: + } else if len(e.Key) > maxKeySize { return exceedsMaxKeySizeError(e.Key) - case int64(len(e.Value)) > txn.db.opt.ValueLogFileSize: + } else if int64(len(e.Value)) > txn.db.opt.ValueLogFileSize { return exceedsMaxValueSizeError(e.Value, txn.db.opt.ValueLogFileSize) } if err := txn.checkSize(e); err != nil { @@ -336,33 +348,29 @@ func (txn *Txn) SetEntry(e *Entry) error { return nil } -// Delete deletes a key. This is done by adding a delete marker for the key at commit timestamp. -// Any reads happening before this timestamp would be unaffected. Any reads after this commit would -// see the deletion. -func (txn *Txn) Delete(key []byte) error { - if !txn.update { - return ErrReadOnlyTxn - } else if txn.discarded { - return ErrDiscardedTxn - } else if len(key) == 0 { - return ErrEmptyKey - } else if len(key) > maxKeySize { - return exceedsMaxKeySizeError(key) - } +// SetEntry takes an Entry struct and adds the key-value pair in the struct, +// along with other metadata to the database. +// +// The current transaction keeps a reference to the entry passed in argument. +// Users must not modify the entry until the end of the transaction. +func (txn *Txn) SetEntry(e *Entry) error { + return txn.modify(e) +} +// Delete deletes a key. +// +// This is done by adding a delete marker for the key at commit timestamp. Any +// reads happening before this timestamp would be unaffected. Any reads after +// this commit would see the deletion. +// +// The current transaction keeps a reference to the key byte slice argument. +// Users must not modify the key until the end of the transaction. +func (txn *Txn) Delete(key []byte) error { e := &Entry{ Key: key, meta: bitDelete, } - if err := txn.checkSize(e); err != nil { - return err - } - - fp := farm.Fingerprint64(key) // Avoid dealing with byte arrays. - txn.writes = append(txn.writes, fp) - - txn.pendingWrites[string(key)] = e - return nil + return txn.modify(e) } // Get looks for key and returns corresponding Item. @@ -387,6 +395,7 @@ func (txn *Txn) Get(key []byte) (item *Item, rerr error) { item.key = key item.status = prefetched item.version = txn.readTs + item.expiresAt = e.ExpiresAt // We probably don't need to set db on item here. return item, nil } @@ -415,9 +424,17 @@ func (txn *Txn) Get(key []byte) (item *Item, rerr error) { item.db = txn.db item.vptr = vs.Value item.txn = txn + item.expiresAt = vs.ExpiresAt return item, nil } +func (txn *Txn) runCallbacks() { + for _, cb := range txn.callbacks { + cb() + } + txn.callbacks = txn.callbacks[:0] +} + // Discard discards a created transaction. This method is very important and must be called. Commit // method calls this internally, however, calling this multiple times doesn't cause any issues. So, // this can safely be called via a defer right when transaction is created. @@ -427,11 +444,12 @@ func (txn *Txn) Discard() { if txn.discarded { // Avoid a re-run. return } - txn.discarded = true - - for _, cb := range txn.callbacks { - cb() + if atomic.LoadInt32(&txn.numIterators) > 0 { + panic("Unclosed iterator at time of Txn.Discard.") } + txn.discarded = true + txn.db.orc.readMark.Done(txn.readTs) + txn.runCallbacks() if txn.update { txn.db.orc.decrRef() } @@ -468,8 +486,10 @@ func (txn *Txn) Commit(callback func(error)) error { } state := txn.db.orc + state.writeLock.Lock() commitTs := state.newCommitTs(txn) if commitTs == 0 { + state.writeLock.Unlock() return ErrConflict } @@ -488,18 +508,30 @@ func (txn *Txn) Commit(callback func(error)) error { } entries = append(entries, e) + req, err := txn.db.sendToWriteCh(entries) + state.writeLock.Unlock() + if err != nil { + return err + } + + // Need to release all locks or writes can get deadlocked. + txn.runCallbacks() + if callback == nil { // If batchSet failed, LSM would not have been updated. So, no need to rollback anything. // TODO: What if some of the txns successfully make it to value log, but others fail. // Nothing gets updated to LSM, until a restart happens. defer state.doneCommit(commitTs) - return txn.db.batchSet(entries) + return req.Wait() } - return txn.db.batchSetAsync(entries, func(err error) { + go func() { + err := req.Wait() + // Write is complete. Let's call the callback function now. state.doneCommit(commitTs) callback(err) - }) + }() + return nil } // NewTransaction creates a new transaction. Badger supports concurrent execution of transactions, @@ -523,6 +555,11 @@ func (txn *Txn) Commit(callback func(error)) error { // defer txn.Discard() // // Call various APIs. func (db *DB) NewTransaction(update bool) *Txn { + if db.opt.ReadOnly && update { + // DB is read-only, force read-only transaction. + update = false + } + txn := &Txn{ update: update, db: db, @@ -530,6 +567,7 @@ func (db *DB) NewTransaction(update bool) *Txn { count: 1, // One extra entry for BitFin. size: int64(len(txnKey) + 10), // Some buffer for the extra entry. } + db.orc.readMark.Begin(txn.readTs) if update { txn.pendingWrites = make(map[string]*Entry) txn.db.orc.addRef() diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/value.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/value.go index ff78f847a1..6b8de919e1 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/value.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/value.go @@ -45,8 +45,9 @@ import ( // Values have their first byte being byteData or byteDelete. This helps us distinguish between // a key that has never been seen and a key that has been explicitly deleted. const ( - bitDelete byte = 1 << 0 // Set if the key has been deleted. - bitValuePointer byte = 1 << 1 // Set if the value is NOT stored directly next to key. + bitDelete byte = 1 << 0 // Set if the key has been deleted. + bitValuePointer byte = 1 << 1 // Set if the value is NOT stored directly next to key. + bitDiscardEarlierVersions byte = 1 << 2 // Set if earlier versions can be discarded. // The MSB 2 bits are for transactions. bitTxn byte = 1 << 6 // Set if the entry is part of a txn. @@ -175,9 +176,75 @@ func (lf *logFile) sync() error { } var errStop = errors.New("Stop iteration") +var errTruncate = errors.New("Do truncate") type logEntry func(e Entry, vp valuePointer) error +type safeRead struct { + k []byte + v []byte + + recordOffset uint32 +} + +func (r *safeRead) Entry(reader *bufio.Reader) (*Entry, error) { + var hbuf [headerBufSize]byte + var err error + + hash := crc32.New(y.CastagnoliCrcTable) + tee := io.TeeReader(reader, hash) + if _, err = io.ReadFull(tee, hbuf[:]); err != nil { + return nil, err + } + + var h header + h.Decode(hbuf[:]) + if h.klen > maxKeySize { + return nil, errTruncate + } + kl := int(h.klen) + if cap(r.k) < kl { + r.k = make([]byte, 2*kl) + } + vl := int(h.vlen) + if cap(r.v) < vl { + r.v = make([]byte, 2*vl) + } + + e := &Entry{} + e.offset = r.recordOffset + e.Key = r.k[:kl] + e.Value = r.v[:vl] + + if _, err = io.ReadFull(tee, e.Key); err != nil { + if err == io.EOF { + err = errTruncate + } + return nil, err + } + if _, err = io.ReadFull(tee, e.Value); err != nil { + if err == io.EOF { + err = errTruncate + } + return nil, err + } + var crcBuf [4]byte + if _, err = io.ReadFull(reader, crcBuf[:]); err != nil { + if err == io.EOF { + err = errTruncate + } + return nil, err + } + crc := binary.BigEndian.Uint32(crcBuf[:]) + if crc != hash.Sum32() { + return nil, errTruncate + } + e.meta = h.meta + e.UserMeta = h.userMeta + e.ExpiresAt = h.expiresAt + return e, nil +} + // iterate iterates over log file. It doesn't not allocate new memory for every kv pair. // Therefore, the kv pair is only valid for the duration of fn call. func (vlog *valueLog) iterate(lf *logFile, offset uint32, fn logEntry) error { @@ -187,87 +254,69 @@ func (vlog *valueLog) iterate(lf *logFile, offset uint32, fn logEntry) error { } reader := bufio.NewReader(lf.fd) - var hbuf [headerBufSize]byte - var h header - k := make([]byte, 1<<10) - v := make([]byte, 1<<20) + read := &safeRead{ + k: make([]byte, 10), + v: make([]byte, 10), + recordOffset: offset, + } truncate := false - recordOffset := offset + var lastCommit uint64 + var validEndOffset uint32 for { - hash := crc32.New(y.CastagnoliCrcTable) - tee := io.TeeReader(reader, hash) - - // TODO: Move this entry decode into structs.go - if _, err = io.ReadFull(tee, hbuf[:]); err != nil { - if err == io.EOF { - break - } else if err == io.ErrUnexpectedEOF { - truncate = true - break - } - return err - } - - var e Entry - e.offset = recordOffset - h.Decode(hbuf[:]) - if h.klen > maxKeySize { + e, err := read.Entry(reader) + if err == io.EOF { + break + } else if err == io.ErrUnexpectedEOF || err == errTruncate { truncate = true break - } - vl := int(h.vlen) - if cap(v) < vl { - v = make([]byte, 2*vl) + } else if err != nil { + return err + } else if e == nil { + continue } - kl := int(h.klen) - if cap(k) < kl { - k = make([]byte, 2*kl) - } - e.Key = k[:kl] - e.Value = v[:vl] + var vp valuePointer + vp.Len = uint32(headerBufSize + len(e.Key) + len(e.Value) + 4) // len(crcBuf) + read.recordOffset += vp.Len + + vp.Offset = e.offset + vp.Fid = lf.fid - if _, err = io.ReadFull(tee, e.Key); err != nil { - if err == io.EOF || err == io.ErrUnexpectedEOF { + if e.meta&bitTxn > 0 { + txnTs := y.ParseTs(e.Key) + if lastCommit == 0 { + lastCommit = txnTs + } + if lastCommit != txnTs { truncate = true break } - return err - } - if _, err = io.ReadFull(tee, e.Value); err != nil { - if err == io.EOF || err == io.ErrUnexpectedEOF { + + } else if e.meta&bitFinTxn > 0 { + txnTs, err := strconv.ParseUint(string(e.Value), 10, 64) + if err != nil || lastCommit != txnTs { truncate = true break } - return err - } + // Got the end of txn. Now we can store them. + lastCommit = 0 + validEndOffset = read.recordOffset - var crcBuf [4]byte - if _, err = io.ReadFull(reader, crcBuf[:]); err != nil { - if err == io.EOF || err == io.ErrUnexpectedEOF { + } else { + if lastCommit != 0 { + // This is most likely an entry which was moved as part of GC. + // We shouldn't get this entry in the middle of a transaction. truncate = true break } - return err - } - crc := binary.BigEndian.Uint32(crcBuf[:]) - if crc != hash.Sum32() { - truncate = true - break + validEndOffset = read.recordOffset } - e.meta = h.meta - e.UserMeta = h.userMeta - e.ExpiresAt = h.expiresAt - var vp valuePointer - vp.Len = headerBufSize + h.klen + h.vlen + uint32(len(crcBuf)) - recordOffset += vp.Len - - vp.Offset = e.offset - vp.Fid = lf.fid - - if err := fn(e, vp); err != nil { + if vlog.opt.ReadOnly { + return ErrReplayNeeded + } + if err := fn(*e, vp); err != nil { if err == errStop { break } @@ -275,33 +324,32 @@ func (vlog *valueLog) iterate(lf *logFile, offset uint32, fn logEntry) error { } } - if truncate && len(lf.fmap) == 0 { + if vlog.opt.Truncate && truncate && len(lf.fmap) == 0 { // Only truncate if the file isn't mmaped. Otherwise, Windows would puke. - if err := lf.fd.Truncate(int64(recordOffset)); err != nil { + if err := lf.fd.Truncate(int64(validEndOffset)); err != nil { return err } + } else if truncate { + return ErrTruncateNeeded } return nil } -func (vlog *valueLog) rewrite(f *logFile) error { +func (vlog *valueLog) rewrite(f *logFile, tr trace.Trace) error { maxFid := atomic.LoadUint32(&vlog.maxFid) y.AssertTruef(uint32(f.fid) < maxFid, "fid to move: %d. Current max fid: %d", f.fid, maxFid) - - elog := trace.NewEventLog("badger", "vlog-rewrite") - defer elog.Finish() - elog.Printf("Rewriting fid: %d", f.fid) + tr.LazyPrintf("Rewriting fid: %d", f.fid) wb := make([]*Entry, 0, 1000) var size int64 y.AssertTrue(vlog.kv != nil) - var count int + var count, moved int fe := func(e Entry) error { count++ - if count%10000 == 0 { - elog.Printf("Processing entry %d", count) + if count%100000 == 0 { + tr.LazyPrintf("Processing entry %d", count) } vs, err := vlog.kv.get(e.Key) @@ -326,18 +374,28 @@ func (vlog *valueLog) rewrite(f *logFile) error { return nil } if vp.Fid == f.fid && vp.Offset == e.offset { + moved++ // This new entry only contains the key, and a pointer to the value. ne := new(Entry) - ne.meta = 0 // Remove all bits. + ne.meta = 0 // Remove all bits. Different keyspace doesn't need these bits. ne.UserMeta = e.UserMeta - ne.Key = make([]byte, len(e.Key)) - copy(ne.Key, e.Key) - ne.Value = make([]byte, len(e.Value)) - copy(ne.Value, e.Value) + + // Create a new key in a separate keyspace, prefixed by moveKey. We are not + // allowed to rewrite an older version of key in the LSM tree, because then this older + // version would be at the top of the LSM tree. To work correctly, reads expect the + // latest versions to be at the top, and the older versions at the bottom. + if bytes.HasPrefix(e.Key, badgerMove) { + ne.Key = append([]byte{}, e.Key...) + } else { + ne.Key = append([]byte{}, badgerMove...) + ne.Key = append(ne.Key, e.Key...) + } + + ne.Value = append([]byte{}, e.Value...) wb = append(wb, ne) size += int64(e.estimateSize(vlog.opt.ValueThreshold)) if size >= 64*mi { - elog.Printf("request has %d entries, size %d", len(wb), size) + tr.LazyPrintf("request has %d entries, size %d", len(wb), size) if err := vlog.kv.batchSet(wb); err != nil { return err } @@ -357,7 +415,7 @@ func (vlog *valueLog) rewrite(f *logFile) error { return err } - elog.Printf("request has %d entries, size %d", len(wb), size) + tr.LazyPrintf("request has %d entries, size %d", len(wb), size) batchSize := 1024 var loops int for i := 0; i < len(wb); { @@ -374,16 +432,16 @@ func (vlog *valueLog) rewrite(f *logFile) error { if err == ErrTxnTooBig { // Decrease the batch size to half. batchSize = batchSize / 2 - elog.Printf("Dropped batch size to %d", batchSize) + tr.LazyPrintf("Dropped batch size to %d", batchSize) continue } return err } i += batchSize } - elog.Printf("Processed %d entries in %d loops", len(wb), loops) - - elog.Printf("Removing fid: %d", f.fid) + tr.LazyPrintf("Processed %d entries in %d loops", len(wb), loops) + tr.LazyPrintf("Total entries: %d. Moved: %d", count, moved) + tr.LazyPrintf("Removing fid: %d", f.fid) var deleteFileNow bool // Entries written to LSM. Remove the older file now. { @@ -409,6 +467,63 @@ func (vlog *valueLog) rewrite(f *logFile) error { return nil } +func (vlog *valueLog) deleteMoveKeysFor(fid uint32, tr trace.Trace) { + db := vlog.kv + var result []*Entry + var count, pointers uint64 + tr.LazyPrintf("Iterating over move keys to find invalids for fid: %d", fid) + err := db.View(func(txn *Txn) error { + opt := DefaultIteratorOptions + opt.internalAccess = true + opt.PrefetchValues = false + itr := txn.NewIterator(opt) + defer itr.Close() + + for itr.Seek(badgerMove); itr.ValidForPrefix(badgerMove); itr.Next() { + count++ + item := itr.Item() + if item.meta&bitValuePointer == 0 { + continue + } + pointers++ + var vp valuePointer + vp.Decode(item.vptr) + if vp.Fid == fid { + e := &Entry{Key: item.KeyCopy(nil), meta: bitDelete} + result = append(result, e) + } + } + return nil + }) + if err != nil { + tr.LazyPrintf("Got error while iterating move keys: %v", err) + tr.SetError() + return + } + tr.LazyPrintf("Num total move keys: %d. Num pointers: %d", count, pointers) + tr.LazyPrintf("Number of invalid move keys found: %d", len(result)) + batchSize := 10240 + for i := 0; i < len(result); { + end := i + batchSize + if end > len(result) { + end = len(result) + } + if err := db.batchSet(result[i:end]); err != nil { + if err == ErrTxnTooBig { + batchSize /= 2 + tr.LazyPrintf("Dropped batch size to %d", batchSize) + continue + } + tr.LazyPrintf("Error while doing batchSet: %v", err) + tr.SetError() + return + } + i += batchSize + } + tr.LazyPrintf("Move keys deletion done.") + return +} + func (vlog *valueLog) incrIteratorCount() { atomic.AddInt32(&vlog.numActiveIterators, 1) } @@ -470,6 +585,7 @@ type valueLog struct { kv *DB maxFid uint32 writableLogOffset uint32 + numEntriesWritten uint32 opt Options garbageCh chan struct{} @@ -484,7 +600,7 @@ func (vlog *valueLog) fpath(fid uint32) string { return vlogFilePath(vlog.dirPath, fid) } -func (vlog *valueLog) openOrCreateFiles() error { +func (vlog *valueLog) openOrCreateFiles(readOnly bool) error { files, err := ioutil.ReadDir(vlog.dirPath) if err != nil { return errors.Wrapf(err, "Error while opening value log") @@ -519,12 +635,18 @@ func (vlog *valueLog) openOrCreateFiles() error { vlog.maxFid = uint32(maxFid) // Open all previous log files as read only. Open the last log file - // as read write. + // as read write (unless the DB is read only). for fid, lf := range vlog.filesMap { if fid == maxFid { - if lf.fd, err = y.OpenExistingSyncedFile(vlog.fpath(fid), - vlog.opt.SyncWrites); err != nil { - return errors.Wrapf(err, "Unable to open value log file as RDWR") + var flags uint32 + if vlog.opt.SyncWrites { + flags |= y.Sync + } + if readOnly { + flags |= y.ReadOnly + } + if lf.fd, err = y.OpenExistingFile(vlog.fpath(fid), flags); err != nil { + return errors.Wrapf(err, "Unable to open value log file") } } else { if err := lf.openReadOnly(); err != nil { @@ -548,6 +670,7 @@ func (vlog *valueLog) createVlogFile(fid uint32) (*logFile, error) { path := vlog.fpath(fid) lf := &logFile{fid: fid, path: path, loadingMode: vlog.opt.ValueLogLoadingMode} vlog.writableLogOffset = 0 + vlog.numEntriesWritten = 0 var err error if lf.fd, err = y.CreateSyncedFile(path, vlog.opt.SyncWrites); err != nil { @@ -570,7 +693,7 @@ func (vlog *valueLog) Open(kv *DB, opt Options) error { vlog.opt = opt vlog.kv = kv vlog.filesMap = make(map[uint32]*logFile) - if err := vlog.openOrCreateFiles(); err != nil { + if err := vlog.openOrCreateFiles(kv.opt.ReadOnly); err != nil { return errors.Wrapf(err, "Unable to open value log") } @@ -592,7 +715,7 @@ func (vlog *valueLog) Close() error { err = munmapErr } - if id == vlog.maxFid { + if !vlog.opt.ReadOnly && id == vlog.maxFid { // truncate writable log file to correct offset. if truncErr := f.fd.Truncate( int64(vlog.writableLogOffset)); truncErr != nil && err == nil { @@ -631,7 +754,8 @@ func (vlog *valueLog) sortedFids() []uint32 { func (vlog *valueLog) Replay(ptr valuePointer, fn logEntry) error { fid := ptr.Fid offset := ptr.Offset + ptr.Len - vlog.elog.Printf("Seeking at value pointer: %+v\n", ptr) + // @kataras removed: vlog.elog.Printf("Seeking at value pointer: %+v\n", ptr) + // @kataras removed: log.Printf("Replaying from value pointer: %+v\n", ptr) fids := vlog.sortedFids() @@ -644,7 +768,11 @@ func (vlog *valueLog) Replay(ptr valuePointer, fn logEntry) error { of = 0 } f := vlog.filesMap[id] + // @kataras removed: + // log.Printf("Iterating file id: %d", id) + // now := time.Now() err := vlog.iterate(f, of, fn) + // @kataras removed: log.Printf("Iteration took: %s\n", time.Since(now)) if err != nil { return errors.Wrapf(err, "Unable to replay value log: %q", f.path) } @@ -667,6 +795,14 @@ type request struct { Err error } +func (req *request) Wait() error { + req.Wg.Wait() + req.Entries = nil + err := req.Err + requestPool.Put(req) + return err +} + // sync is thread-unsafe and should not be called concurrently with write. func (vlog *valueLog) sync() error { if vlog.opt.SyncWrites { @@ -718,14 +854,15 @@ func (vlog *valueLog) write(reqs []*request) error { atomic.AddUint32(&vlog.writableLogOffset, uint32(n)) vlog.buf.Reset() - if vlog.writableOffset() > uint32(vlog.opt.ValueLogFileSize) { + if vlog.writableOffset() > uint32(vlog.opt.ValueLogFileSize) || + vlog.numEntriesWritten > vlog.opt.ValueLogMaxEntries { var err error if err = curlf.doneWriting(vlog.writableLogOffset); err != nil { return err } newid := atomic.AddUint32(&vlog.maxFid, 1) - y.AssertTruef(newid <= math.MaxUint32, "newid will overflow uint32: %v", newid) + y.AssertTruef(newid > 0, "newid has overflown uint32: %v", newid) newlf, err := vlog.createVlogFile(newid) if err != nil { return err @@ -756,11 +893,16 @@ func (vlog *valueLog) write(reqs []*request) error { } p.Len = uint32(plen) b.Ptrs = append(b.Ptrs, p) - - if p.Offset > uint32(vlog.opt.ValueLogFileSize) { - if err := toDisk(); err != nil { - return err - } + } + vlog.numEntriesWritten += uint32(len(b.Entries)) + // We write to disk here so that all entries that are part of the same transaction are + // written to the same vlog file. + writeNow := + vlog.writableOffset()+uint32(vlog.buf.Len()) > uint32(vlog.opt.ValueLogFileSize) || + vlog.numEntriesWritten > uint32(vlog.opt.ValueLogMaxEntries) + if writeNow { + if err := toDisk(); err != nil { + return err } } } @@ -807,7 +949,7 @@ func (vlog *valueLog) Read(vp valuePointer, s *y.Slice) ([]byte, func(), error) func (vlog *valueLog) readValueBytes(vp valuePointer, s *y.Slice) ([]byte, func(), error) { lf, err := vlog.getFileRLocked(vp.Fid) if err != nil { - return nil, nil, errors.Wrapf(err, "Unable to read from value log: %+v", vp) + return nil, nil, err } buf, err := lf.read(vp, s) @@ -834,11 +976,15 @@ func valueBytesToEntry(buf []byte) (e Entry) { return } -func (vlog *valueLog) pickLog(head valuePointer) *logFile { +func (vlog *valueLog) pickLog(head valuePointer, tr trace.Trace) (files []*logFile) { vlog.filesLock.RLock() defer vlog.filesLock.RUnlock() fids := vlog.sortedFids() - if len(fids) <= 1 || head.Fid == 0 { + if len(fids) <= 1 { + tr.LazyPrintf("Only one or less value log file.") + return nil + } else if head.Fid == 0 { + tr.LazyPrintf("Head pointer is at zero.") return nil } @@ -860,7 +1006,10 @@ func (vlog *valueLog) pickLog(head valuePointer) *logFile { vlog.lfDiscardStats.Unlock() if candidate.fid != math.MaxUint32 { // Found a candidate - return vlog.filesMap[candidate.fid] + tr.LazyPrintf("Found candidate via discard stats: %v", candidate) + files = append(files, vlog.filesMap[candidate.fid]) + } else { + tr.LazyPrintf("Could not find candidate via discard stats. Randomly picking one.") } // Fallback to randomly picking a log file @@ -872,13 +1021,16 @@ func (vlog *valueLog) pickLog(head valuePointer) *logFile { } } if idxHead == 0 { // Not found or first file + tr.LazyPrintf("Could not find any file.") return nil } idx := rand.Intn(idxHead) // Don’t include head.Fid. We pick a random file before it. if idx > 0 { idx = rand.Intn(idx + 1) // Another level of rand to favor smaller fids. } - return vlog.filesMap[fids[idx]] + tr.LazyPrintf("Randomly chose fid: %d", fids[idx]) + files = append(files, vlog.filesMap[fids[idx]]) + return files } func discardEntry(e Entry, vs y.ValueStruct) bool { @@ -900,13 +1052,7 @@ func discardEntry(e Entry, vs y.ValueStruct) bool { return false } -func (vlog *valueLog) doRunGC(gcThreshold float64, head valuePointer) (err error) { - // Pick a log file for GC - lf := vlog.pickLog(head) - if lf == nil { - return ErrNoRewrite - } - +func (vlog *valueLog) doRunGC(lf *logFile, discardRatio float64, tr trace.Trace) (err error) { // Update stats before exiting defer func() { if err == nil { @@ -918,39 +1064,57 @@ func (vlog *valueLog) doRunGC(gcThreshold float64, head valuePointer) (err error type reason struct { total float64 - keep float64 discard float64 + count int } - var r reason - var window = 100.0 - count := 0 + fi, err := lf.fd.Stat() + if err != nil { + tr.LazyPrintf("Error while finding file size: %v", err) + tr.SetError() + return err + } + + // Set up the sampling window sizes. + sizeWindow := float64(fi.Size()) * 0.1 // 10% of the file as window. + countWindow := int(float64(vlog.opt.ValueLogMaxEntries) * 0.01) // 1% of num entries. + tr.LazyPrintf("Size window: %5.2f. Count window: %d.", sizeWindow, countWindow) // Pick a random start point for the log. - skipFirstM := float64(rand.Intn(int(vlog.opt.ValueLogFileSize/mi))) - window + skipFirstM := float64(rand.Int63n(fi.Size())) // Pick a random starting location. + skipFirstM -= sizeWindow // Avoid hitting EOF by moving back by window. + skipFirstM /= float64(mi) // Convert to MBs. + tr.LazyPrintf("Skip first %5.2f MB of file of size: %d MB", skipFirstM, fi.Size()/mi) var skipped float64 + var r reason start := time.Now() y.AssertTrue(vlog.kv != nil) s := new(y.Slice) + var numIterations int err = vlog.iterate(lf, 0, func(e Entry, vp valuePointer) error { - esz := float64(vp.Len) / (1 << 20) // in MBs. +4 for the CAS stuff. - skipped += esz + numIterations++ + esz := float64(vp.Len) / (1 << 20) // in MBs. if skipped < skipFirstM { + skipped += esz return nil } - count++ - if count%100 == 0 { - time.Sleep(time.Millisecond) + // Sample until we reach the window sizes or exceed 10 seconds. + if r.count > countWindow { + tr.LazyPrintf("Stopping sampling after %d entries.", countWindow) + return errStop } - r.total += esz - if r.total > window { + if r.total > sizeWindow { + tr.LazyPrintf("Stopping sampling after reaching window size.") return errStop } if time.Since(start) > 10*time.Second { + tr.LazyPrintf("Stopping sampling after 10 seconds.") return errStop } + r.total += esz + r.count++ vs, err := vlog.kv.get(e.Key) if err != nil { @@ -977,7 +1141,6 @@ func (vlog *valueLog) doRunGC(gcThreshold float64, head valuePointer) (err error } if vp.Fid == lf.fid && vp.Offset == e.offset { // This is still the active entry. This would need to be rewritten. - r.keep += esz } else { vlog.elog.Printf("Reason=%+v\n", r) @@ -998,21 +1161,23 @@ func (vlog *valueLog) doRunGC(gcThreshold float64, head valuePointer) (err error }) if err != nil { - vlog.elog.Errorf("Error while iterating for RunGC: %v", err) + tr.LazyPrintf("Error while iterating for RunGC: %v", err) + tr.SetError() return err } - vlog.elog.Printf("Fid: %d Data status=%+v\n", lf.fid, r) + tr.LazyPrintf("Fid: %d. Skipped: %5.2fMB Num iterations: %d. Data status=%+v\n", + lf.fid, skipped, numIterations, r) - if r.total < 10.0 || r.discard < gcThreshold*r.total { - vlog.elog.Printf("Skipping GC on fid: %d\n\n", lf.fid) + // If we couldn't sample at least a 1000 KV pairs or at least 75% of the window size, + // and what we can discard is below the threshold, we should skip the rewrite. + if (r.count < countWindow && r.total < sizeWindow*0.75) || r.discard < discardRatio*r.total { + tr.LazyPrintf("Skipping GC on fid: %d", lf.fid) return ErrNoRewrite } - - vlog.elog.Printf("REWRITING VLOG %d\n", lf.fid) - if err = vlog.rewrite(lf); err != nil { + if err = vlog.rewrite(lf, tr); err != nil { return err } - vlog.elog.Printf("Done rewriting.") + tr.LazyPrintf("Done rewriting.") return nil } @@ -1026,24 +1191,34 @@ func (vlog *valueLog) waitOnGC(lc *y.Closer) { vlog.garbageCh <- struct{}{} } -func (vlog *valueLog) runGC(gcThreshold float64, head valuePointer) error { +func (vlog *valueLog) runGC(discardRatio float64, head valuePointer) error { select { case vlog.garbageCh <- struct{}{}: - // Run GC - var ( - err error - count int - ) - for { - err = vlog.doRunGC(gcThreshold, head) - if err != nil { - break - } - count++ + // Pick a log file for GC. + tr := trace.New("Badger.ValueLog", "GC") + tr.SetMaxEvents(100) + defer func() { + tr.Finish() + <-vlog.garbageCh + }() + + var err error + files := vlog.pickLog(head, tr) + if len(files) == 0 { + tr.LazyPrintf("PickLog returned zero results.") + return ErrNoRewrite } - <-vlog.garbageCh - if err == ErrNoRewrite && count > 0 { - return nil + tried := make(map[uint32]bool) + for _, lf := range files { + if _, done := tried[lf.fid]; done { + continue + } + tried[lf.fid] = true + err = vlog.doRunGC(lf, discardRatio, tr) + if err == nil { + vlog.deleteMoveKeysFor(lf.fid, tr) + return nil + } } return err default: @@ -1051,12 +1226,10 @@ func (vlog *valueLog) runGC(gcThreshold float64, head valuePointer) error { } } -func (vlog *valueLog) updateGCStats(item *Item) { - if item.meta&bitValuePointer > 0 { - var vp valuePointer - vp.Decode(item.vptr) - vlog.lfDiscardStats.Lock() - vlog.lfDiscardStats.m[vp.Fid] += int64(vp.Len) - vlog.lfDiscardStats.Unlock() +func (vlog *valueLog) updateGCStats(stats map[uint32]int64) { + vlog.lfDiscardStats.Lock() + for fid, sz := range stats { + vlog.lfDiscardStats.m[fid] += sz } + vlog.lfDiscardStats.Unlock() } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/vendor/github.com/golang/protobuf/proto/proto3_test.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/vendor/github.com/golang/protobuf/proto/proto3_test.go deleted file mode 100644 index 73eed6c0b5..0000000000 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/vendor/github.com/golang/protobuf/proto/proto3_test.go +++ /dev/null @@ -1,151 +0,0 @@ -// Go support for Protocol Buffers - Google's data interchange format -// -// Copyright 2014 The Go Authors. All rights reserved. -// https://github.com/golang/protobuf -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package proto_test - -import ( - "bytes" - "testing" - - "github.com/golang/protobuf/proto" - pb "github.com/golang/protobuf/proto/proto3_proto" - tpb "github.com/golang/protobuf/proto/test_proto" -) - -func TestProto3ZeroValues(t *testing.T) { - tests := []struct { - desc string - m proto.Message - }{ - {"zero message", &pb.Message{}}, - {"empty bytes field", &pb.Message{Data: []byte{}}}, - } - for _, test := range tests { - b, err := proto.Marshal(test.m) - if err != nil { - t.Errorf("%s: proto.Marshal: %v", test.desc, err) - continue - } - if len(b) > 0 { - t.Errorf("%s: Encoding is non-empty: %q", test.desc, b) - } - } -} - -func TestRoundTripProto3(t *testing.T) { - m := &pb.Message{ - Name: "David", // (2 | 1<<3): 0x0a 0x05 "David" - Hilarity: pb.Message_PUNS, // (0 | 2<<3): 0x10 0x01 - HeightInCm: 178, // (0 | 3<<3): 0x18 0xb2 0x01 - Data: []byte("roboto"), // (2 | 4<<3): 0x20 0x06 "roboto" - ResultCount: 47, // (0 | 7<<3): 0x38 0x2f - TrueScotsman: true, // (0 | 8<<3): 0x40 0x01 - Score: 8.1, // (5 | 9<<3): 0x4d <8.1> - - Key: []uint64{1, 0xdeadbeef}, - Nested: &pb.Nested{ - Bunny: "Monty", - }, - } - t.Logf(" m: %v", m) - - b, err := proto.Marshal(m) - if err != nil { - t.Fatalf("proto.Marshal: %v", err) - } - t.Logf(" b: %q", b) - - m2 := new(pb.Message) - if err := proto.Unmarshal(b, m2); err != nil { - t.Fatalf("proto.Unmarshal: %v", err) - } - t.Logf("m2: %v", m2) - - if !proto.Equal(m, m2) { - t.Errorf("proto.Equal returned false:\n m: %v\nm2: %v", m, m2) - } -} - -func TestGettersForBasicTypesExist(t *testing.T) { - var m pb.Message - if got := m.GetNested().GetBunny(); got != "" { - t.Errorf("m.GetNested().GetBunny() = %q, want empty string", got) - } - if got := m.GetNested().GetCute(); got { - t.Errorf("m.GetNested().GetCute() = %t, want false", got) - } -} - -func TestProto3SetDefaults(t *testing.T) { - in := &pb.Message{ - Terrain: map[string]*pb.Nested{ - "meadow": new(pb.Nested), - }, - Proto2Field: new(tpb.SubDefaults), - Proto2Value: map[string]*tpb.SubDefaults{ - "badlands": new(tpb.SubDefaults), - }, - } - - got := proto.Clone(in).(*pb.Message) - proto.SetDefaults(got) - - // There are no defaults in proto3. Everything should be the zero value, but - // we need to remember to set defaults for nested proto2 messages. - want := &pb.Message{ - Terrain: map[string]*pb.Nested{ - "meadow": new(pb.Nested), - }, - Proto2Field: &tpb.SubDefaults{N: proto.Int64(7)}, - Proto2Value: map[string]*tpb.SubDefaults{ - "badlands": &tpb.SubDefaults{N: proto.Int64(7)}, - }, - } - - if !proto.Equal(got, want) { - t.Errorf("with in = %v\nproto.SetDefaults(in) =>\ngot %v\nwant %v", in, got, want) - } -} - -func TestUnknownFieldPreservation(t *testing.T) { - b1 := "\x0a\x05David" // Known tag 1 - b2 := "\xc2\x0c\x06Google" // Unknown tag 200 - b := []byte(b1 + b2) - - m := new(pb.Message) - if err := proto.Unmarshal(b, m); err != nil { - t.Fatalf("proto.Unmarshal: %v", err) - } - - if !bytes.Equal(m.XXX_unrecognized, []byte(b2)) { - t.Fatalf("mismatching unknown fields:\ngot %q\nwant %q", m.XXX_unrecognized, b2) - } -} diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_dsync.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_dsync.go index bd25c9e9a3..3f3445e2e9 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_dsync.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_dsync.go @@ -1,4 +1,4 @@ -// +build !dragonfly,!freebsd,!windows,!darwin +// +build !dragonfly,!freebsd,!windows /* * Copyright 2017 Dgraph Labs, Inc. and Contributors @@ -18,8 +18,8 @@ package y -import "syscall" +import "golang.org/x/sys/unix" func init() { - datasyncFileFlag = syscall.O_DSYNC + datasyncFileFlag = unix.O_DSYNC } diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_nodsync.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_nodsync.go index be295f2e02..b68be7ab94 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_nodsync.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/file_nodsync.go @@ -1,4 +1,4 @@ -// +build dragonfly freebsd windows darwin +// +build dragonfly freebsd windows /* * Copyright 2017 Dgraph Labs, Inc. and Contributors diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/watermark.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/watermark.go new file mode 100644 index 0000000000..6fd3c89968 --- /dev/null +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/watermark.go @@ -0,0 +1,130 @@ +/* + * Copyright 2018 Dgraph Labs, Inc. and Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package y + +import ( + "container/heap" + "sync/atomic" + + "golang.org/x/net/trace" +) + +type uint64Heap []uint64 + +func (u uint64Heap) Len() int { return len(u) } +func (u uint64Heap) Less(i int, j int) bool { return u[i] < u[j] } +func (u uint64Heap) Swap(i int, j int) { u[i], u[j] = u[j], u[i] } +func (u *uint64Heap) Push(x interface{}) { *u = append(*u, x.(uint64)) } +func (u *uint64Heap) Pop() interface{} { + old := *u + n := len(old) + x := old[n-1] + *u = old[0 : n-1] + return x +} + +type mark struct { + readTs uint64 + done bool // Set to true if the pending mutation is done. +} +type WaterMark struct { + markCh chan mark + minReadTs uint64 + elog trace.EventLog +} + +// Init initializes a WaterMark struct. MUST be called before using it. +func (w *WaterMark) Init() { + w.markCh = make(chan mark, 1000) + w.elog = trace.NewEventLog("Badger", "MinReadTs") + go w.process() +} + +func (w *WaterMark) Begin(readTs uint64) { + w.markCh <- mark{readTs: readTs, done: false} +} +func (w *WaterMark) Done(readTs uint64) { + w.markCh <- mark{readTs: readTs, done: true} +} + +// DoneUntil returns the maximum index until which all tasks are done. +func (w *WaterMark) MinReadTs() uint64 { + return atomic.LoadUint64(&w.minReadTs) +} + +// process is used to process the Mark channel. This is not thread-safe, +// so only run one goroutine for process. One is sufficient, because +// all ops in the goroutine use only memory and cpu. +func (w *WaterMark) process() { + var reads uint64Heap + // pending maps raft proposal index to the number of pending mutations for this proposal. + pending := make(map[uint64]int) + + heap.Init(&reads) + var loop uint64 + + processOne := func(readTs uint64, done bool) { + // If not already done, then set. Otherwise, don't undo a done entry. + prev, present := pending[readTs] + if !present { + heap.Push(&reads, readTs) + } + + delta := 1 + if done { + delta = -1 + } + pending[readTs] = prev + delta + + loop++ + if len(reads) > 0 && loop%1000 == 0 { + min := reads[0] + w.elog.Printf("ReadTs: %4d. Size: %4d MinReadTs: %-4d Looking for: %-4d. Value: %d\n", + readTs, len(reads), w.MinReadTs(), min, pending[min]) + } + + // Update mark by going through all reads in order; and checking if they have + // been done. Stop at the first readTs, which isn't done. + minReadTs := w.MinReadTs() + // Don't assert that minReadTs < readTs, to avoid any inconsistencies caused by managed + // transactions, or testing where we explicitly set the readTs for transactions like in + // TestTxnVersions. + until := minReadTs + loops := 0 + + for len(reads) > 0 { + min := reads[0] + if done := pending[min]; done != 0 { + break // len(reads) will be > 0. + } + heap.Pop(&reads) + delete(pending, min) + until = min + loops++ + } + if until != minReadTs { + AssertTrue(atomic.CompareAndSwapUint64(&w.minReadTs, minReadTs, until)) + w.elog.Printf("MinReadTs: %d. Loops: %d\n", until, loops) + } + } + + for mark := range w.markCh { + if mark.readTs > 0 { + processOne(mark.readTs, mark.done) + } + } +} diff --git a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/y.go b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/y.go index 20a8ea55e4..5927fd3836 100644 --- a/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/y.go +++ b/sessions/sessiondb/badger/vendor/github.com/dgraph-io/badger/y/y.go @@ -31,6 +31,15 @@ import ( // and encountering the end of slice. var ErrEOF = errors.New("End of mapped region") +const ( + // Sync indicates that O_DSYNC should be set on the underlying file, + // ensuring that data writes do not return until the data is flushed + // to disk. + Sync = 1 << iota + // ReadOnly opens the underlying file on a read-only basis. + ReadOnly +) + var ( // This is O_DSYNC (datasync) on platforms that support it -- see file_unix.go datasyncFileFlag = 0x0 @@ -39,13 +48,17 @@ var ( CastagnoliCrcTable = crc32.MakeTable(crc32.Castagnoli) ) -// OpenExistingSyncedFile opens an existing file, errors if it doesn't exist. -func OpenExistingSyncedFile(filename string, sync bool) (*os.File, error) { - flags := os.O_RDWR - if sync { - flags |= datasyncFileFlag +// OpenExistingFile opens an existing file, errors if it doesn't exist. +func OpenExistingFile(filename string, flags uint32) (*os.File, error) { + openFlags := os.O_RDWR + if flags&ReadOnly != 0 { + openFlags = os.O_RDONLY + } + + if flags&Sync != 0 { + openFlags |= datasyncFileFlag } - return os.OpenFile(filename, flags, 0) + return os.OpenFile(filename, openFlags, 0) } // CreateSyncedFile creates a new file (using O_EXCL), errors if it already existed. diff --git a/sessions/sessiondb/boltdb/database.go b/sessions/sessiondb/boltdb/database.go index fe1c90feb0..b1b2bb64c4 100644 --- a/sessions/sessiondb/boltdb/database.go +++ b/sessions/sessiondb/boltdb/database.go @@ -6,10 +6,11 @@ import ( "runtime" "time" - "github.com/coreos/bbolt" - "github.com/kataras/golog" "github.com/kataras/iris/core/errors" "github.com/kataras/iris/sessions" + + bolt "github.com/etcd-io/bbolt" + "github.com/kataras/golog" ) // DefaultFileMode used as the default database's "fileMode" @@ -173,7 +174,8 @@ func (db *Database) Acquire(sid string, expires time.Duration) (lifetime session return err } - if err := b.Put(expirationKey, timeBytes); err == nil { + err = b.Put(expirationKey, timeBytes) + if err == nil { // create the session bucket now, so the rest of the calls can be easly get the bucket without any further checks. _, err = root.CreateBucket(bsid) } @@ -365,11 +367,7 @@ func (db *Database) Release(sid string) { // try to delete the associated expiration bucket, if exists, ignore error. b.DeleteBucket(getExpirationBucketName(bsid)) - if err := b.DeleteBucket(bsid); err != nil { - return err - } - - return nil + return b.DeleteBucket(bsid) }) } diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/LICENSE b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/LICENSE similarity index 100% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/LICENSE rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/LICENSE diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_386.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_386.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_386.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_386.go index 820d533c15..4d35ee7cf3 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_386.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_386.go @@ -1,4 +1,4 @@ -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0x7FFFFFFF // 2GB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_amd64.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_amd64.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_amd64.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_amd64.go index 98fafdb47d..60a52dad56 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_amd64.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_amd64.go @@ -1,4 +1,4 @@ -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0xFFFFFFFFFFFF // 256TB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm.go similarity index 98% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm.go index 7e5cb4b941..105d27ddb7 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import "unsafe" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm64.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm64.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm64.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm64.go index b26d84f91b..f5aa2a5ee2 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_arm64.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_arm64.go @@ -1,6 +1,6 @@ // +build arm64 -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0xFFFFFFFFFFFF // 256TB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_linux.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_linux.go similarity index 91% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_linux.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_linux.go index 2b67666140..7707bcacf0 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_linux.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_linux.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "syscall" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mips64x.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mips64x.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mips64x.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mips64x.go index 134b578bd4..baeb289fd9 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mips64x.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mips64x.go @@ -1,6 +1,6 @@ // +build mips64 mips64le -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0x8000000000 // 512GB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mipsx.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mipsx.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mipsx.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mipsx.go index d5ecb0597e..2d9b1a91f3 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_mipsx.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_mipsx.go @@ -1,6 +1,6 @@ // +build mips mipsle -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0x40000000 // 1GB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_openbsd.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_openbsd.go similarity index 97% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_openbsd.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_openbsd.go index 7058c3d734..d7f50358ef 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_openbsd.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_openbsd.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "syscall" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc.go index 55cb8a72cc..69804714aa 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc.go @@ -1,6 +1,6 @@ // +build ppc -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0x7FFFFFFF // 2GB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64.go index 9331d9771e..3565908576 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64.go @@ -1,6 +1,6 @@ // +build ppc64 -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0xFFFFFFFFFFFF // 256TB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64le.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64le.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64le.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64le.go index 8c143bc5d1..422c7c69d6 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_ppc64le.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_ppc64le.go @@ -1,6 +1,6 @@ // +build ppc64le -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0xFFFFFFFFFFFF // 256TB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_s390x.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_s390x.go similarity index 95% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_s390x.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_s390x.go index d7c39af925..6d3fcb825d 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_s390x.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_s390x.go @@ -1,6 +1,6 @@ // +build s390x -package bolt +package bbolt // maxMapSize represents the largest mmap size supported by Bolt. const maxMapSize = 0xFFFFFFFFFFFF // 256TB diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix.go index add3bd8823..38e11d4c4d 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix.go @@ -1,6 +1,6 @@ // +build !windows,!plan9,!solaris -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix_solaris.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix_solaris.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix_solaris.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix_solaris.go index fd8335ecc9..492eaf302c 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_unix_solaris.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_unix_solaris.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_windows.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_windows.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_windows.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_windows.go index ca6f9a11c2..4e3f90fb43 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bolt_windows.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bolt_windows.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/boltsync_unix.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/boltsync_unix.go similarity index 91% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/boltsync_unix.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/boltsync_unix.go index f50442523c..9587afefee 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/boltsync_unix.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/boltsync_unix.go @@ -1,6 +1,6 @@ // +build !windows,!plan9,!linux,!openbsd -package bolt +package bbolt // fdatasync flushes written data to a file descriptor. func fdatasync(db *DB) error { diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bucket.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bucket.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bucket.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bucket.go index 44db88b8ab..84bfd4d6a2 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/bucket.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/bucket.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "bytes" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/cursor.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/cursor.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/cursor.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/cursor.go index 1bdda63a2f..3000aced6c 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/cursor.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/cursor.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "bytes" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/db.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/db.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/db.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/db.go index ab97c6014a..4e38ab8c62 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/db.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/db.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "errors" @@ -454,8 +454,8 @@ func (db *DB) Close() error { db.metalock.Lock() defer db.metalock.Unlock() - db.mmaplock.RLock() - defer db.mmaplock.RUnlock() + db.mmaplock.Lock() + defer db.mmaplock.Unlock() return db.close() } diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/errors.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/errors.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/errors.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/errors.go index a3620a3ebb..48758ca577 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/errors.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/errors.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import "errors" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/freelist.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/freelist.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/freelist.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/freelist.go index 266f154294..e4bcb2dcf9 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/freelist.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/freelist.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/node.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/node.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/node.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/node.go index f4ce240edd..6c3fa553ea 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/node.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/node.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "bytes" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/page.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/page.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/page.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/page.go index cde403ae86..bca9615f0f 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/page.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/page.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/tx.go b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/tx.go similarity index 99% rename from sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/tx.go rename to sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/tx.go index 41a9bc619a..f508641427 100644 --- a/sessions/sessiondb/boltdb/vendor/github.com/coreos/bbolt/tx.go +++ b/sessions/sessiondb/boltdb/vendor/github.com/etcd-io/bbolt/tx.go @@ -1,4 +1,4 @@ -package bolt +package bbolt import ( "fmt" diff --git a/sessions/sessiondb/redis/service/service.go b/sessions/sessiondb/redis/service/service.go index dbe58fc70a..c38a5e3920 100644 --- a/sessions/sessiondb/redis/service/service.go +++ b/sessions/sessiondb/redis/service/service.go @@ -3,6 +3,7 @@ package service import ( "fmt" "time" + "github.com/gomodule/redigo/redis" "github.com/kataras/iris/core/errors" ) @@ -94,7 +95,7 @@ func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, found bool // if -1 means the key has unlimited life time. hasExpiration = seconds > -1 // if -2 means key does not exist. - found = ! (c.Err() != nil || seconds == -2) + found = !(c.Err() != nil || seconds == -2) return } @@ -291,7 +292,7 @@ func (r *Service) Connect() { c.Addr = DefaultRedisAddr } - pool := &redis.Pool{IdleTimeout: DefaultRedisIdleTimeout, MaxIdle: c.MaxIdle, MaxActive: c.MaxActive} + pool := &redis.Pool{IdleTimeout: c.IdleTimeout, MaxIdle: c.MaxIdle, MaxActive: c.MaxActive} pool.TestOnBorrow = func(c redis.Conn, t time.Time) error { _, err := c.Do("PING") return err diff --git a/vendor/github.com/Joker/jade/config.go b/vendor/github.com/Joker/jade/config.go index 4daa4059db..c38c0d954f 100644 --- a/vendor/github.com/Joker/jade/config.go +++ b/vendor/github.com/Joker/jade/config.go @@ -9,7 +9,8 @@ var ( tag__bgn = "<%s%s>" tag__end = "" tag__void = "<%s%s/>" - tag__arg = ` %s="{{ print %s }}"` + tag__arg_esc = ` %s="{{ print %s }}"` + tag__arg_une = ` %s="{{ print %s }}"` tag__arg_str = ` %s="%s"` tag__arg_add = `%s " " %s` tag__arg_bgn = "" @@ -37,13 +38,15 @@ var ( text__str = "%s" text__comment = "" - mixin__bgn = "\n%s" - mixin__end = "" - mixin__var_bgn = "{{/* var (" - mixin__var = " %s = %s " - mixin__var_rest = " %s = %#v " - mixin__var_block = " block = `%s` " - mixin__var_end = ") */}}\n" + mixin__bgn = "\n%s" + mixin__end = "" + mixin__var_bgn = "" + mixin__var = "{{ $%s := %s }}" + mixin__var_rest = "{{ $%s := %#v }}" + mixin__var_end = "\n" + mixin__var_block_bgn = "" + mixin__var_block = "" + mixin__var_block_end = "" ) type itemType int8 diff --git a/vendor/github.com/Joker/jade/jade_node.go b/vendor/github.com/Joker/jade/jade_node.go index 6380d1184d..8aabfb720d 100644 --- a/vendor/github.com/Joker/jade/jade_node.go +++ b/vendor/github.com/Joker/jade/jade_node.go @@ -18,13 +18,13 @@ type TagNode struct { Nodes []Node AttrName []string AttrCode []string + AttrUesc []bool TagName string tagType itemType - tab int } func (t *Tree) newTag(pos Pos, name string, tagType itemType) *TagNode { - return &TagNode{tr: t, NodeType: NodeTag, Pos: pos, TagName: name, tagType: tagType, tab: t.tab} + return &TagNode{tr: t, NodeType: NodeTag, Pos: pos, TagName: name, tagType: tagType} } func (l *TagNode) append(n Node) { @@ -35,8 +35,9 @@ func (l *TagNode) tree() *Tree { return l.tr } -func (l *TagNode) attr(a, b string) { +func (l *TagNode) attr(a, b string, c bool) { for k, v := range l.AttrName { + // add to existing attribute if v == a { l.AttrCode[k] = fmt.Sprintf(tag__arg_add, l.AttrCode[k], b) return @@ -45,23 +46,29 @@ func (l *TagNode) attr(a, b string) { l.AttrName = append(l.AttrName, a) l.AttrCode = append(l.AttrCode, b) + l.AttrUesc = append(l.AttrUesc, c) } -func codeStrFmt(a string) (string, bool) { +func (l *TagNode) ifAttrArgBollean() { + for k, v := range l.AttrCode { + if v == "true" { + l.AttrCode[k] = `"` + l.AttrName[k] + `"` + } else if v == "false" { + l.AttrName = append(l.AttrName[:k], l.AttrName[k+1:]...) + l.AttrCode = append(l.AttrCode[:k], l.AttrCode[k+1:]...) + l.AttrUesc = append(l.AttrUesc[:k], l.AttrUesc[k+1:]...) + } + } +} + +func ifAttrArgString(a string, unesc bool) (string, bool) { var ( str = []rune(a) lng = len(str) first = str[0] last = str[lng-1] - unesc = false ) - if first == 'ߐ' { // FIXME temporarily ߐ - [AttrEqualUn] Unescaped flag set in parseAttributes() - str = append(str[:0], str[1:]...) - lng -= 1 - first = str[0] - last = str[lng-1] - unesc = true - } + switch first { case '"', '\'': if first == last { @@ -113,17 +120,31 @@ func (l *TagNode) WriteIn(b io.Writer) { var ( attr = new(bytes.Buffer) ) + l.ifAttrArgBollean() + if len(l.AttrName) > 0 { fmt.Fprint(attr, tag__arg_bgn) for k, name := range l.AttrName { - if arg, ok := codeStrFmt(l.AttrCode[k]); ok { + if arg, ok := ifAttrArgString(l.AttrCode[k], l.AttrUesc[k]); ok { fmt.Fprintf(attr, tag__arg_str, name, arg) + } else if !golang_mode { - fmt.Fprintf(attr, tag__arg, name, l.AttrCode[k]) + fmt.Fprintf(attr, tag__arg_esc, name, l.AttrCode[k]) + } else if _, err := parser.ParseExpr(l.AttrCode[k]); err == nil { - fmt.Fprintf(attr, tag__arg, name, l.Pos, l.AttrCode[k]) + if l.AttrUesc[k] { + fmt.Fprintf(attr, tag__arg_une, name, l.Pos, l.AttrCode[k]) + } else { + fmt.Fprintf(attr, tag__arg_esc, name, l.Pos, l.AttrCode[k]) + } + } else if arg, ok := query(l.AttrCode[k]); ok { - fmt.Fprintf(attr, tag__arg, name, l.Pos, arg) + if l.AttrUesc[k] { + fmt.Fprintf(attr, tag__arg_une, name, l.Pos, arg) + } else { + fmt.Fprintf(attr, tag__arg_esc, name, l.Pos, arg) + } + } else { log.Fatalln("Error tag attribute value ==> ", l.AttrCode[k]) } @@ -149,9 +170,9 @@ func (l *TagNode) CopyTag() *TagNode { return l } n := l.tr.newTag(l.Pos, l.TagName, l.tagType) - n.tab = l.tab n.AttrCode = l.AttrCode n.AttrName = l.AttrName + n.AttrUesc = l.AttrUesc for _, elem := range l.Nodes { n.append(elem.Copy()) } @@ -172,11 +193,10 @@ type CondNode struct { Nodes []Node cond string condType itemType - tab int } func (t *Tree) newCond(pos Pos, cond string, condType itemType) *CondNode { - return &CondNode{tr: t, NodeType: NodeCond, Pos: pos, cond: cond, condType: condType, tab: t.tab} + return &CondNode{tr: t, NodeType: NodeCond, Pos: pos, cond: cond, condType: condType} } func (l *CondNode) append(n Node) { @@ -255,7 +275,6 @@ func (l *CondNode) CopyCond() *CondNode { return l } n := l.tr.newCond(l.Pos, l.cond, l.condType) - n.tab = l.tab for _, elem := range l.Nodes { n.append(elem.Copy()) } @@ -275,11 +294,10 @@ type CodeNode struct { tr *Tree codeType itemType Code []byte // The text; may span newlines. - tab int } func (t *Tree) newCode(pos Pos, text string, codeType itemType) *CodeNode { - return &CodeNode{tr: t, NodeType: NodeCode, Pos: pos, Code: []byte(text), codeType: codeType, tab: t.tab} + return &CodeNode{tr: t, NodeType: NodeCode, Pos: pos, Code: []byte(text), codeType: codeType} } func (t *CodeNode) String() string { @@ -289,18 +307,24 @@ func (t *CodeNode) String() string { } func (t *CodeNode) WriteIn(b io.Writer) { switch t.codeType { - case itemCode: - fmt.Fprintf(b, code__longcode, t.Code) case itemCodeBuffered: if !golang_mode { fmt.Fprintf(b, code__buffered, t.Code) - } else if cb, ok := codeStrFmt(string(t.Code)); ok { - fmt.Fprintf(b, code__buffered, t.Pos, `"`+cb+`"`) + return + } + if code, ok := ifAttrArgString(string(t.Code), false); ok { + fmt.Fprintf(b, code__buffered, t.Pos, `"`+code+`"`) } else { fmt.Fprintf(b, code__buffered, t.Pos, t.Code) } case itemCodeUnescaped: - fmt.Fprintf(b, code__unescaped, t.Code) + if !golang_mode { + fmt.Fprintf(b, code__unescaped, t.Code) + return + } + fmt.Fprintf(b, code__unescaped, t.Pos, t.Code) + case itemCode: + fmt.Fprintf(b, code__longcode, t.Code) case itemElse: fmt.Fprintf(b, code__else) case itemElseIf: @@ -323,7 +347,7 @@ func (t *CodeNode) tree() *Tree { } func (t *CodeNode) Copy() Node { - return &CodeNode{tr: t.tr, NodeType: NodeCode, Pos: t.Pos, codeType: t.codeType, Code: append([]byte{}, t.Code...), tab: t.tab} + return &CodeNode{tr: t.tr, NodeType: NodeCode, Pos: t.Pos, codeType: t.codeType, Code: append([]byte{}, t.Code...)} } // @@ -335,11 +359,10 @@ type BlockNode struct { tr *Tree blockType itemType Name string - tab int } func (t *Tree) newBlock(pos Pos, name string, textType itemType) *BlockNode { - return &BlockNode{tr: t, NodeType: NodeBlock, Pos: pos, Name: name, blockType: textType, tab: t.tab} + return &BlockNode{tr: t, NodeType: NodeBlock, Pos: pos, Name: name, blockType: textType} } func (t *BlockNode) String() string { @@ -368,7 +391,7 @@ func (t *BlockNode) tree() *Tree { } func (t *BlockNode) Copy() Node { - return &BlockNode{tr: t.tr, NodeType: NodeBlock, Pos: t.Pos, blockType: t.blockType, Name: t.Name, tab: t.tab} + return &BlockNode{tr: t.tr, NodeType: NodeBlock, Pos: t.Pos, blockType: t.blockType, Name: t.Name} } // @@ -380,11 +403,10 @@ type TextNode struct { tr *Tree textType itemType Text []byte // The text; may span newlines. - tab int } -func (t *Tree) newText(pos Pos, text string, textType itemType) *TextNode { - return &TextNode{tr: t, NodeType: NodeText, Pos: pos, Text: []byte(text), textType: textType, tab: t.tab} +func (t *Tree) newText(pos Pos, text []byte, textType itemType) *TextNode { + return &TextNode{tr: t, NodeType: NodeText, Pos: pos, Text: text, textType: textType} } func (t *TextNode) String() string { @@ -406,7 +428,7 @@ func (t *TextNode) tree() *Tree { } func (t *TextNode) Copy() Node { - return &TextNode{tr: t.tr, NodeType: NodeText, Pos: t.Pos, textType: t.textType, Text: append([]byte{}, t.Text...), tab: t.tab} + return &TextNode{tr: t.tr, NodeType: NodeText, Pos: t.Pos, textType: t.textType, Text: append([]byte{}, t.Text...)} } // @@ -421,20 +443,22 @@ type MixinNode struct { AttrCode []string AttrRest []string MixinName string - block string + block []Node tagType itemType - tab int } func (t *Tree) newMixin(pos Pos) *MixinNode { - return &MixinNode{tr: t, NodeType: NodeMixin, Pos: pos, tab: t.tab} + return &MixinNode{tr: t, NodeType: NodeMixin, Pos: pos} } func (l *MixinNode) append(n Node) { l.Nodes = append(l.Nodes, n) } +func (l *MixinNode) appendToBlock(n Node) { + l.block = append(l.block, n) +} -func (l *MixinNode) attr(a, b string) { +func (l *MixinNode) attr(a, b string, c bool) { l.AttrName = append(l.AttrName, a) l.AttrCode = append(l.AttrCode, b) } @@ -457,7 +481,6 @@ func (l *MixinNode) WriteIn(b io.Writer) { if an > 0 { fmt.Fprintf(attr, mixin__var_bgn) - fmt.Fprintf(attr, mixin__var_block, l.block) if rest > 0 { fmt.Fprintf(attr, mixin__var_rest, strings.TrimLeft(l.AttrName[an-1], "."), l.AttrRest) l.AttrName = l.AttrName[:an-1] @@ -467,8 +490,18 @@ func (l *MixinNode) WriteIn(b io.Writer) { } fmt.Fprintf(attr, mixin__var_end) } - fmt.Fprintf(b, mixin__bgn, attr) + + if len(l.block) > 0 { + b.Write([]byte(mixin__var_block_bgn)) + for _, n := range l.block { + n.WriteIn(b) + } + b.Write([]byte(mixin__var_block_end)) + } else { + b.Write([]byte(mixin__var_block)) + } + for _, n := range l.Nodes { n.WriteIn(b) } @@ -480,7 +513,6 @@ func (l *MixinNode) CopyMixin() *MixinNode { return l } n := l.tr.newMixin(l.Pos) - n.tab = l.tab for _, elem := range l.Nodes { n.append(elem.Copy()) } @@ -546,7 +578,8 @@ func (d *DoctypeNode) String() string { return fmt.Sprintf(text__str, d.doctype) } func (d *DoctypeNode) WriteIn(b io.Writer) { - b.Write([]byte(d.doctype)) + fmt.Fprintf(b, text__str, d.doctype) + // b.Write([]byte(d.doctype)) } func (d *DoctypeNode) tree() *Tree { return d.tr diff --git a/vendor/github.com/Joker/jade/jade_parse.go b/vendor/github.com/Joker/jade/jade_parse.go index 75d74c617b..9a0a678780 100644 --- a/vendor/github.com/Joker/jade/jade_parse.go +++ b/vendor/github.com/Joker/jade/jade_parse.go @@ -55,7 +55,7 @@ func (t *Tree) hub(token item) (n Node) { case itemTag, itemTagInline, itemTagVoid, itemTagVoidInline: return t.parseTag(token) case itemText, itemComment, itemHTMLTag: - return t.newText(token.pos, token.val, token.typ) + return t.newText(token.pos, []byte(token.val), token.typ) case itemCode, itemCodeBuffered, itemCodeUnescaped, itemMixinBlock: return t.newCode(token.pos, token.val, token.typ) case itemIf, itemUnless: @@ -99,15 +99,13 @@ Loop: if tag.tagType == itemTagVoid || tag.tagType == itemTagVoidInline { break Loop } - t.tab++ tag.append(t.hub(token)) - t.tab-- case token.depth == deep: switch token.typ { case itemClass: - tag.attr("class", `"`+token.val+`"`) + tag.attr("class", `"`+token.val+`"`, false) case itemID: - tag.attr("id", `"`+token.val+`"`) + tag.attr("id", `"`+token.val+`"`, false) case itemAttrStart: t.parseAttributes(tag) case itemTagEnd: @@ -125,7 +123,7 @@ Loop: } type pAttr interface { - attr(string, string) + attr(string, string, bool) } func (t *Tree) parseAttributes(tag pAttr) { @@ -144,26 +142,23 @@ func (t *Tree) parseAttributes(tag pAttr) { case aname == "": aname = token.val case aname != "" && !equal: - tag.attr(aname, `"`+aname+`"`) + tag.attr(aname, `"`+aname+`"`, unesc) aname = token.val case aname != "" && equal: - if unesc { - stack = append(stack, "ߐ"+token.val) - unesc = false - } else { - stack = append(stack, token.val) - } + stack = append(stack, token.val) + } + case itemAttrEqual, itemAttrEqualUn: + if token.typ == itemAttrEqual { + unesc = false + } else { + unesc = true } - case itemAttrEqualUn: - unesc = true - fallthrough - case itemAttrEqual: equal = true switch len_stack := len(stack); { case len_stack == 0 && aname != "": // skip case len_stack > 1 && aname != "": - tag.attr(aname, strings.Join(stack[:len(stack)-1], " ")) + tag.attr(aname, strings.Join(stack[:len(stack)-1], " "), unesc) aname = stack[len(stack)-1] stack = stack[:0] @@ -177,23 +172,23 @@ func (t *Tree) parseAttributes(tag pAttr) { equal = false switch len_stack := len(stack); { case len_stack > 0 && aname != "": - tag.attr(aname, strings.Join(stack, " ")) + tag.attr(aname, strings.Join(stack, " "), unesc) aname = "" stack = stack[:0] case len_stack == 0 && aname != "": - tag.attr(aname, `"`+aname+`"`) + tag.attr(aname, `"`+aname+`"`, unesc) aname = "" } case itemAttrEnd: switch len_stack := len(stack); { case len_stack > 0 && aname != "": - tag.attr(aname, strings.Join(stack, " ")) + tag.attr(aname, strings.Join(stack, " "), unesc) case len_stack > 0 && aname == "": for _, a := range stack { - tag.attr(a, a) + tag.attr(a, a, unesc) } case len_stack == 0 && aname != "": - tag.attr(aname, `"`+aname+`"`) + tag.attr(aname, `"`+aname+`"`, unesc) } return default: @@ -211,9 +206,7 @@ Loop: for { switch token := t.nextNonSpace(); { case token.depth > deep: - t.tab++ cond.append(t.hub(token)) - t.tab-- case token.depth == deep: switch token.typ { case itemElse: @@ -244,9 +237,7 @@ Loop: for { switch token := t.nextNonSpace(); { case token.depth > deep: - t.tab++ cond.append(t.hub(token)) - t.tab-- case token.depth == deep: if token.typ == itemElse { cond.condType = itemForIfNotContain @@ -264,25 +255,23 @@ Loop: func (t *Tree) parseCase(tk item) Node { var ( - deep = tk.depth - _case_ = t.newCond(tk.pos, tk.val, tk.typ) + deep = tk.depth + iCase = t.newCond(tk.pos, tk.val, tk.typ) ) for { if token := t.nextNonSpace(); token.depth > deep { switch token.typ { case itemCaseWhen, itemCaseDefault: - _case_.append(t.newCode(token.pos, token.val, token.typ)) + iCase.append(t.newCode(token.pos, token.val, token.typ)) default: - t.tab++ - _case_.append(t.hub(token)) - t.tab-- + iCase.append(t.hub(token)) } } else { break } } t.backup() - return _case_ + return iCase } func (t *Tree) parseMixin(tk item) *MixinNode { @@ -294,9 +283,7 @@ Loop: for { switch token := t.nextNonSpace(); { case token.depth > deep: - t.tab++ mixin.append(t.hub(token)) - t.tab-- case token.depth == deep: if token.typ == itemAttrStart { t.parseAttributes(mixin) @@ -324,9 +311,7 @@ Loop: for { switch token := t.nextNonSpace(); { case token.depth > deep: - t.tab++ - mixin.append(t.hub(token)) - t.tab-- + mixin.appendToBlock(t.hub(token)) case token.depth == deep: if token.typ == itemAttrStart { t.parseAttributes(mixin) @@ -408,7 +393,6 @@ func (t *Tree) parseInclude(tk item) *ListNode { func (t *Tree) parseSubFile(path string) *ListNode { var incTree = New(path) - incTree.tab = t.tab incTree.block = t.block incTree.mixin = t.mixin _, err := incTree.Parse(t.read(path)) @@ -418,7 +402,7 @@ func (t *Tree) parseSubFile(path string) *ListNode { return incTree.Root } -func (t *Tree) read(path string) string { +func (t *Tree) read(path string) []byte { var ( bb []byte ext string @@ -430,7 +414,7 @@ func (t *Tree) read(path string) string { case "": if _, err = os.Stat(path + ".jade"); os.IsNotExist(err) { if _, err = os.Stat(path + ".pug"); os.IsNotExist(err) { - t.errorf(`".jade" or ".pug" file required`) + t.errorf("try " + path + `.*: ".jade" or ".pug" file required`) } else { ext = ".pug" } @@ -446,5 +430,5 @@ func (t *Tree) read(path string) string { t.errorf(`%s work dir: %s `, err, dir) } - return string(bb) + return bb } diff --git a/vendor/github.com/Joker/jade/lex.go b/vendor/github.com/Joker/jade/lex.go index d4ad69ccb7..13f548fb72 100644 --- a/vendor/github.com/Joker/jade/lex.go +++ b/vendor/github.com/Joker/jade/lex.go @@ -143,10 +143,10 @@ func (l *lexer) drain() { } // lex creates a new scanner for the input string. -func lex(name, input string) *lexer { +func lex(name string, input []byte) *lexer { l := &lexer{ name: name, - input: input, + input: string(input), items: make(chan item), line: 1, } diff --git a/vendor/github.com/Joker/jade/node.go b/vendor/github.com/Joker/jade/node.go index 47d4be26db..5a8b2546b9 100644 --- a/vendor/github.com/Joker/jade/node.go +++ b/vendor/github.com/Joker/jade/node.go @@ -44,11 +44,10 @@ type ListNode struct { Pos tr *Tree Nodes []Node // The element nodes in lexical order. - tab int } func (t *Tree) newList(pos Pos) *ListNode { - return &ListNode{tr: t, NodeType: NodeList, Pos: pos, tab: t.tab} + return &ListNode{tr: t, NodeType: NodeList, Pos: pos} } func (l *ListNode) append(n Node) { diff --git a/vendor/github.com/Joker/jade/parse.go b/vendor/github.com/Joker/jade/parse.go index 8dc15c239e..df14ede5a4 100644 --- a/vendor/github.com/Joker/jade/parse.go +++ b/vendor/github.com/Joker/jade/parse.go @@ -19,7 +19,6 @@ type Tree struct { lex *lexer token [3]item // three-token lookahead for parser. peekCount int - tab int // depth of focus mixin map[string]*MixinNode block map[string]*ListNode @@ -127,10 +126,10 @@ func (t *Tree) recover(errp *error) { } } -func (t *Tree) Parse(text string) (tree *Tree, err error) { +func (t *Tree) Parse(text []byte) (tree *Tree, err error) { defer t.recover(&err) t.lex = lex(t.Name, text) - t.text = text + t.text = string(text) t.topParse() t.lex = nil return t, nil diff --git a/vendor/github.com/Joker/jade/template.go b/vendor/github.com/Joker/jade/template.go index ec8cf45f32..2db14d80d5 100644 --- a/vendor/github.com/Joker/jade/template.go +++ b/vendor/github.com/Joker/jade/template.go @@ -34,7 +34,7 @@ Output:

Hello world!

*/ -func Parse(name, text string) (string, error) { +func Parse(name string, text []byte) (string, error) { outTpl, err := New(name).Parse(text) if err != nil { return "", err @@ -50,7 +50,7 @@ func ParseFile(filename string) (string, error) { if err != nil { return "", err } - return Parse(filepath.Base(filename), string(bs)) + return Parse(filepath.Base(filename), bs) } func (t *Tree) WriteIn(b io.Writer) { diff --git a/vendor/gopkg.in/russross/blackfriday.v2/LICENSE.txt b/vendor/github.com/iris-contrib/blackfriday/LICENSE.txt similarity index 100% rename from vendor/gopkg.in/russross/blackfriday.v2/LICENSE.txt rename to vendor/github.com/iris-contrib/blackfriday/LICENSE.txt diff --git a/vendor/gopkg.in/russross/blackfriday.v2/block.go b/vendor/github.com/iris-contrib/blackfriday/block.go similarity index 93% rename from vendor/gopkg.in/russross/blackfriday.v2/block.go rename to vendor/github.com/iris-contrib/blackfriday/block.go index d7da33f213..b8607474e5 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/block.go +++ b/vendor/github.com/iris-contrib/blackfriday/block.go @@ -17,6 +17,7 @@ import ( "bytes" "html" "regexp" + "strings" "github.com/shurcooL/sanitized_anchor_name" ) @@ -568,8 +569,8 @@ func (*Markdown) isHRule(data []byte) bool { // isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, // and returns the end index if so, or 0 otherwise. It also returns the marker found. -// If syntax is not nil, it gets set to the syntax specified in the fence line. -func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker string) { +// If info is not nil, it gets set to the syntax specified in the fence line. +func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { i, size := 0, 0 // skip up to three spaces @@ -605,9 +606,9 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker } // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the syntax, and discard it if the caller doesn't care. - if syntax != nil { - syn := 0 + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 i = skipChar(data, i, ' ') if i >= len(data) { @@ -617,14 +618,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker return 0, "" } - syntaxStart := i + infoStart := i if data[i] == '{' { i++ - syntaxStart++ + infoStart++ for i < len(data) && data[i] != '}' && data[i] != '\n' { - syn++ + infoLength++ i++ } @@ -634,31 +635,30 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // strip all whitespace at the beginning and the end // of the {} block - for syn > 0 && isspace(data[syntaxStart]) { - syntaxStart++ - syn-- + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- } - for syn > 0 && isspace(data[syntaxStart+syn-1]) { - syn-- + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- } - i++ + i = skipChar(data, i, ' ') } else { - for i < len(data) && !isspace(data[i]) { - syn++ + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ i++ } } - *syntax = string(data[syntaxStart : syntaxStart+syn]) + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) } - i = skipChar(data, i, ' ') - if i >= len(data) || data[i] != '\n' { - if i == len(data) { - return i, marker - } + if i == len(data) { + return i, marker + } + if i > len(data) || data[i] != '\n' { return 0, "" } return i + 1, marker // Take newline into account. @@ -668,14 +668,14 @@ func isFenceLine(data []byte, syntax *string, oldmarker string) (end int, marker // or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. // If doRender is true, a final newline is mandatory to recognize the fenced code block. func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { - var syntax string - beg, marker := isFenceLine(data, &syntax, "") + var info string + beg, marker := isFenceLine(data, &info, "") if beg == 0 || beg >= len(data) { return 0 } var work bytes.Buffer - work.Write([]byte(syntax)) + work.Write([]byte(info)) work.WriteByte('\n') for { @@ -1148,6 +1148,18 @@ func (p *Markdown) list(data []byte, flags ListType) int { return i } +// Returns true if the list item is not the same type as its parent list +func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool { + if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 { + return true + } else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 { + return true + } else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) { + return true + } + return false +} + // Returns true if block ends with a blank line, descending if needed // into lists and sublists. func endsWithBlankLine(block *Node) bool { @@ -1246,6 +1258,7 @@ func (p *Markdown) listItem(data []byte, flags *ListType) int { // process the following lines containsBlankLine := false sublist := 0 + codeBlockMarker := "" gatherlines: for line < len(data) { @@ -1279,6 +1292,27 @@ gatherlines: chunk := data[line+indentIndex : i] + if p.extensions&FencedCode != 0 { + // determine if in or out of codeblock + // if in codeblock, ignore normal list processing + _, marker := isFenceLine(chunk, nil, codeBlockMarker) + if marker != "" { + if codeBlockMarker == "" { + // start of codeblock + codeBlockMarker = marker + } else { + // end of codeblock. + codeBlockMarker = "" + } + } + // we are in a codeblock, write line, and continue + if codeBlockMarker != "" || marker != "" { + raw.Write(data[line+indentIndex : i]) + line = i + continue gatherlines + } + } + // evaluate how this line fits in switch { // is this a nested list item? @@ -1286,16 +1320,23 @@ gatherlines: p.oliPrefix(chunk) > 0 || p.dliPrefix(chunk) > 0: - if containsBlankLine { - *flags |= ListItemContainsBlock - } - // to be a nested list, it must be indented more - // if not, it is the next item in the same list + // if not, it is either a different kind of list + // or the next item in the same list if indent <= itemIndent { + if p.listTypeChanged(chunk, flags) { + *flags |= ListItemEndOfList + } else if containsBlankLine { + *flags |= ListItemContainsBlock + } + break gatherlines } + if containsBlankLine { + *flags |= ListItemContainsBlock + } + // is this the first item in the nested list? if sublist == 0 { sublist = raw.Len() diff --git a/vendor/gopkg.in/russross/blackfriday.v2/esc.go b/vendor/github.com/iris-contrib/blackfriday/esc.go similarity index 100% rename from vendor/gopkg.in/russross/blackfriday.v2/esc.go rename to vendor/github.com/iris-contrib/blackfriday/esc.go diff --git a/vendor/gopkg.in/russross/blackfriday.v2/html.go b/vendor/github.com/iris-contrib/blackfriday/html.go similarity index 97% rename from vendor/gopkg.in/russross/blackfriday.v2/html.go rename to vendor/github.com/iris-contrib/blackfriday/html.go index 25fb185e9d..284c87184f 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/html.go +++ b/vendor/github.com/iris-contrib/blackfriday/html.go @@ -35,6 +35,7 @@ const ( Safelink // Only link to trusted protocols NofollowLinks // Only link with rel="nofollow" NoreferrerLinks // Only link with rel="noreferrer" + NoopenerLinks // Only link with rel="noopener" HrefTargetBlank // Add a blank target CompletePage // Generate a complete HTML page UseXHTML // Generate XHTML output instead of HTML @@ -87,6 +88,10 @@ type HTMLRendererParameters struct { HeadingIDPrefix string // If set, add this text to the back of each Heading ID, to ensure uniqueness. HeadingIDSuffix string + // Increase heading levels: if the offset is 1,

becomes

etc. + // Negative offset is also valid. + // Resulting levels are clipped between 1 and 6. + HeadingLevelOffset int Title string // Document title (used if CompletePage is set) CSS string // Optional CSS file URL (used if CompletePage is set) @@ -282,6 +287,9 @@ func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { if flags&NoreferrerLinks != 0 { val = append(val, "noreferrer") } + if flags&NoopenerLinks != 0 { + val = append(val, "noopener") + } if flags&HrefTargetBlank != 0 { attrs = append(attrs, "target=\"_blank\"") } @@ -331,7 +339,7 @@ func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { func footnoteRef(prefix string, node *Node) []byte { urlFrag := prefix + string(slugify(node.Destination)) - anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) } @@ -460,9 +468,10 @@ var ( ) func headingTagsFromLevel(level int) ([]byte, []byte) { - switch level { - case 1: + if level <= 1 { return h1Tag, h1CloseTag + } + switch level { case 2: return h2Tag, h2CloseTag case 3: @@ -471,9 +480,8 @@ func headingTagsFromLevel(level int) ([]byte, []byte) { return h4Tag, h4CloseTag case 5: return h5Tag, h5CloseTag - default: - return h6Tag, h6CloseTag } + return h6Tag, h6CloseTag } func (r *HTMLRenderer) outHRTag(w io.Writer) { @@ -651,7 +659,8 @@ func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkSt r.out(w, node.Literal) r.cr(w) case Heading: - openTag, closeTag := headingTagsFromLevel(node.Level) + headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level + openTag, closeTag := headingTagsFromLevel(headingLevel) if entering { if node.IsTitleblock { attrs = append(attrs, `class="title"`) diff --git a/vendor/gopkg.in/russross/blackfriday.v2/inline.go b/vendor/github.com/iris-contrib/blackfriday/inline.go similarity index 96% rename from vendor/gopkg.in/russross/blackfriday.v2/inline.go rename to vendor/github.com/iris-contrib/blackfriday/inline.go index 3d633106b5..4ed2907921 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/inline.go +++ b/vendor/github.com/iris-contrib/blackfriday/inline.go @@ -23,8 +23,22 @@ var ( urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) - // TODO: improve this regexp to catch all possible entities: - htmlEntityRe = regexp.MustCompile(`&[a-z]{2,5};`) + // https://www.w3.org/TR/html5/syntax.html#character-references + // highest unicode code point in 17 planes (2^20): 1,114,112d = + // 7 dec digits or 6 hex digits + // named entity references can be 2-31 characters with stuff like < + // at one end and ∳ at the other. There + // are also sometimes numbers at the end, although this isn't inherent + // in the specification; there are never numbers anywhere else in + // current character references, though; see ¾ and ▒, etc. + // https://www.w3.org/TR/html5/syntax.html#named-character-references + // + // entity := "&" (named group | number ref) ";" + // named group := [a-zA-Z]{2,31}[0-9]{0,2} + // number ref := "#" (dec ref | hex ref) + // dec ref := [0-9]{1,7} + // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} + htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) ) // Functions to parse text within a block diff --git a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go b/vendor/github.com/iris-contrib/blackfriday/markdown.go similarity index 95% rename from vendor/gopkg.in/russross/blackfriday.v2/markdown.go rename to vendor/github.com/iris-contrib/blackfriday/markdown.go index ff61cb05e7..58d2e4538c 100644 --- a/vendor/gopkg.in/russross/blackfriday.v2/markdown.go +++ b/vendor/github.com/iris-contrib/blackfriday/markdown.go @@ -93,46 +93,46 @@ const ( // blockTags is a set of tags that are recognized as HTML block tags. // Any of these can be included in markdown text without special escaping. var blockTags = map[string]struct{}{ - "blockquote": struct{}{}, - "del": struct{}{}, - "div": struct{}{}, - "dl": struct{}{}, - "fieldset": struct{}{}, - "form": struct{}{}, - "h1": struct{}{}, - "h2": struct{}{}, - "h3": struct{}{}, - "h4": struct{}{}, - "h5": struct{}{}, - "h6": struct{}{}, - "iframe": struct{}{}, - "ins": struct{}{}, - "math": struct{}{}, - "noscript": struct{}{}, - "ol": struct{}{}, - "pre": struct{}{}, - "p": struct{}{}, - "script": struct{}{}, - "style": struct{}{}, - "table": struct{}{}, - "ul": struct{}{}, + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, // HTML5 - "address": struct{}{}, - "article": struct{}{}, - "aside": struct{}{}, - "canvas": struct{}{}, - "figcaption": struct{}{}, - "figure": struct{}{}, - "footer": struct{}{}, - "header": struct{}{}, - "hgroup": struct{}{}, - "main": struct{}{}, - "nav": struct{}{}, - "output": struct{}{}, - "progress": struct{}{}, - "section": struct{}{}, - "video": struct{}{}, + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, } // Renderer is the rendering interface. This is mostly of interest if you are @@ -480,11 +480,11 @@ func (p *Markdown) parseRefsToAST() { // [^note]: This is the explanation. // // Footnotes should be placed at the end of the document in an ordered list. -// Inline footnotes such as: +// Finally, there are inline footnotes such as: // -// Inline footnotes^[Not supported.] also exist. +// Inline footnotes^[Also supported.] provide a quick inline explanation, +// but are rendered at the bottom of the document. // -// are not yet supported. // reference holds all information necessary for a reference-style links or // footnotes. @@ -813,7 +813,17 @@ func ispunct(c byte) bool { // Test if a character is a whitespace character. func isspace(c byte) bool { - return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' } // Test if a character is letter. diff --git a/vendor/gopkg.in/russross/blackfriday.v2/node.go b/vendor/github.com/iris-contrib/blackfriday/node.go similarity index 100% rename from vendor/gopkg.in/russross/blackfriday.v2/node.go rename to vendor/github.com/iris-contrib/blackfriday/node.go diff --git a/vendor/gopkg.in/russross/blackfriday.v2/smartypants.go b/vendor/github.com/iris-contrib/blackfriday/smartypants.go similarity index 100% rename from vendor/gopkg.in/russross/blackfriday.v2/smartypants.go rename to vendor/github.com/iris-contrib/blackfriday/smartypants.go diff --git a/vendor/github.com/iris-contrib/formBinder/binder.go b/vendor/github.com/iris-contrib/formBinder/binder.go index 51443d46d7..f9b1e1109d 100644 --- a/vendor/github.com/iris-contrib/formBinder/binder.go +++ b/vendor/github.com/iris-contrib/formBinder/binder.go @@ -130,7 +130,7 @@ func NewDecoder(opts *DecoderOptions) *Decoder { func (dec *Decoder) Decode(vs url.Values, dst interface{}) error { main := reflect.ValueOf(dst) if main.Kind() != reflect.Ptr { - return newError(fmt.Errorf("formam: the value passed for decode is not a pointer but a %v", main.Kind())) + return newError(fmt.Errorf("form: the value passed for decode is not a pointer but a %v", main.Kind())) } dec.main = main.Elem() dec.formValues = vs @@ -141,7 +141,7 @@ func (dec *Decoder) Decode(vs url.Values, dst interface{}) error { func Decode(vs url.Values, dst interface{}) error { main := reflect.ValueOf(dst) if main.Kind() != reflect.Ptr { - return newError(fmt.Errorf("formam: the value passed for decode is not a pointer but a %v", main.Kind())) + return newError(fmt.Errorf("form: the value passed for decode is not a pointer but a %v", main.Kind())) } dec := &Decoder{ main: main.Elem(), @@ -303,13 +303,13 @@ func (dec *Decoder) walk() error { case reflect.Array: index, err := strconv.Atoi(dec.bracket) if err != nil { - return newError(fmt.Errorf("formam: the index of array is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: the index of array is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) } dec.curr = dec.curr.Index(index) case reflect.Slice: index, err := strconv.Atoi(dec.bracket) if err != nil { - return newError(fmt.Errorf("formam: the index of slice is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: the index of slice is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) } if dec.curr.Len() <= index { dec.expandSlice(index + 1) @@ -318,7 +318,7 @@ func (dec *Decoder) walk() error { case reflect.Map: dec.walkInMap(dec.bracket) default: - return newError(fmt.Errorf("formam: the field \"%v\" in path \"%v\" has a index for array but it is a %v", dec.field, dec.path, dec.curr.Kind())) + return newError(fmt.Errorf("form: the field \"%v\" in path \"%v\" has a index for array but it is a %v", dec.field, dec.path, dec.curr.Kind())) } } dec.field = "" @@ -388,7 +388,7 @@ func (dec *Decoder) decode() error { // has index, so to decode value by index indicated index, err := strconv.Atoi(dec.bracket) if err != nil { - return newError(fmt.Errorf("formam: the index of array is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: the index of array is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) } dec.curr = dec.curr.Index(index) return dec.decode() @@ -409,7 +409,7 @@ func (dec *Decoder) decode() error { // has index, so to decode value by index indicated index, err := strconv.Atoi(dec.bracket) if err != nil { - return newError(fmt.Errorf("formam: the index of slice is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: the index of slice is not a number in the field \"%v\" of path \"%v\"", dec.field, dec.path)) } if dec.curr.Len() <= index { dec.expandSlice(index + 1) @@ -421,19 +421,19 @@ func (dec *Decoder) decode() error { dec.curr.SetString(dec.value) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: if num, err := strconv.ParseInt(dec.value, 10, 64); err != nil { - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" should be a valid signed integer number", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" should be a valid signed integer number", dec.field, dec.path)) } else { dec.curr.SetInt(num) } case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: if num, err := strconv.ParseUint(dec.value, 10, 64); err != nil { - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" should be a valid unsigned integer number", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" should be a valid unsigned integer number", dec.field, dec.path)) } else { dec.curr.SetUint(num) } case reflect.Float32, reflect.Float64: if num, err := strconv.ParseFloat(dec.value, dec.curr.Type().Bits()); err != nil { - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" should be a valid float number", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" should be a valid float number", dec.field, dec.path)) } else { dec.curr.SetFloat(num) } @@ -444,7 +444,7 @@ func (dec *Decoder) decode() error { case "false", "off", "0": dec.curr.SetBool(false) default: - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" is not a valid boolean", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" is not a valid boolean", dec.field, dec.path)) } case reflect.Interface: dec.curr.Set(reflect.ValueOf(dec.value)) @@ -464,13 +464,13 @@ func (dec *Decoder) decode() error { } } if err != nil { - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" is not a valid datetime", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" is not a valid datetime", dec.field, dec.path)) } dec.curr.Set(reflect.ValueOf(t)) case url.URL: u, err := url.Parse(dec.value) if err != nil { - return newError(fmt.Errorf("formam: the value of field \"%v\" in path \"%v\" is not a valid url", dec.field, dec.path)) + return newError(fmt.Errorf("form: the value of field \"%v\" in path \"%v\" is not a valid url", dec.field, dec.path)) } dec.curr.Set(reflect.ValueOf(*u)) default: @@ -485,17 +485,42 @@ func (dec *Decoder) decode() error { return nil } */ - return newError(fmt.Errorf("formam: not supported type for field \"%v\" in path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: not supported type for field \"%v\" in path \"%v\"", dec.field, dec.path)) } default: - return newError(fmt.Errorf("formam: not supported type for field \"%v\" in path \"%v\"", dec.field, dec.path)) + return newError(fmt.Errorf("form: not supported type for field \"%v\" in path \"%v\"", dec.field, dec.path)) } return nil } +// IsErrPath reports whether the incoming error is type of `ErrPath`, which can be ignored +// when server allows unknown post values to be sent by the client. +func IsErrPath(err error) bool { + if err == nil { + return false + } + + _, ok := err.(ErrPath) + return ok +} + +// ErrPath describes an error that can be ignored if server allows unknown post values to be sent on server. +type ErrPath struct { + field string +} + +func (err ErrPath) Error() string { + return fmt.Sprintf("form: not found the field \"%s\"", err.field) +} + +// Field returns the unknown posted request field. +func (err ErrPath) Field() string { + return err.field +} + // findField finds a field by its name, if it is not found, -// then retry the search examining the tag "formam" of every field of struct +// then retry the search examining the tag "form" of every field of struct func (dec *Decoder) findStructField() error { var anon reflect.Value @@ -520,7 +545,7 @@ func (dec *Decoder) findStructField() error { anon = dec.curr dec.curr = tmp } else if dec.field == field.Tag.Get(dec.opts.TagName) { - // is not found yet, then retry by its tag name "formam" + // is not found yet, then retry by its tag name "form" dec.curr = dec.curr.Field(i) return nil } @@ -530,7 +555,7 @@ func (dec *Decoder) findStructField() error { return nil } - return newError(fmt.Errorf("formam: not found the field \"%v\" in the path \"%v\"", dec.field, dec.path)) + return ErrPath{field: dec.field} } // expandSlice expands the length and capacity of the current slice diff --git a/vendor/github.com/kataras/survey/LICENSE b/vendor/github.com/kataras/survey/LICENSE deleted file mode 100644 index f8ef08748a..0000000000 --- a/vendor/github.com/kataras/survey/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -MIT License - -Copyright (c) 2016 Alec Aivazis -Copyright (c) 2017 Gerasimos Maropoulos - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/kataras/survey/confirm.go b/vendor/github.com/kataras/survey/confirm.go deleted file mode 100644 index d1ef6f3a25..0000000000 --- a/vendor/github.com/kataras/survey/confirm.go +++ /dev/null @@ -1,138 +0,0 @@ -package survey - -import ( - "fmt" - "os" - "regexp" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -// Confirm is a regular text input that accept yes/no answers. Response type is a bool. -type Confirm struct { - core.Renderer - Message string - Default bool - Help string -} - -// data available to the templates when processing -type ConfirmTemplateData struct { - Confirm - Answer string - ShowHelp bool -} - -// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format -var ConfirmQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }} {{color "reset"}} -{{- if .Answer}} - {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} -{{- else }} - {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}} - {{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}} -{{- end}}` - -// the regex for answers -var ( - yesRx = regexp.MustCompile("^(?i:y(?:es)?)$") - noRx = regexp.MustCompile("^(?i:n(?:o)?)$") -) - -func yesNo(t bool) string { - if t { - return "Yes" - } - return "No" -} - -func (c *Confirm) getBool(showHelp bool) (bool, error) { - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - // start waiting for input - for { - line, err := rr.ReadLine(0) - if err != nil { - return false, err - } - // move back up a line to compensate for the \n echoed from terminal - terminal.CursorPreviousLine(1) - val := string(line) - - // get the answer that matches the - var answer bool - switch { - case yesRx.Match([]byte(val)): - answer = true - case noRx.Match([]byte(val)): - answer = false - case val == "": - answer = c.Default - case val == string(core.HelpInputRune) && c.Help != "": - err := c.Render( - ConfirmQuestionTemplate, - ConfirmTemplateData{Confirm: *c, ShowHelp: true}, - ) - if err != nil { - // use the default value and bubble up - return c.Default, err - } - showHelp = true - continue - default: - // we didnt get a valid answer, so print error and prompt again - if err := c.Error(fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil { - return c.Default, err - } - err := c.Render( - ConfirmQuestionTemplate, - ConfirmTemplateData{Confirm: *c, ShowHelp: showHelp}, - ) - if err != nil { - // use the default value and bubble up - return c.Default, err - } - continue - } - return answer, nil - } - // should not get here - return c.Default, nil -} - -/* -Prompt prompts the user with a simple text field and expects a reply followed -by a carriage return. - - likesPie := false - prompt := &survey.Confirm{ Message: "What is your name?" } - survey.AskOne(prompt, &likesPie, nil) -*/ -func (c *Confirm) Prompt() (interface{}, error) { - // render the question template - err := c.Render( - ConfirmQuestionTemplate, - ConfirmTemplateData{Confirm: *c}, - ) - if err != nil { - return "", err - } - - // get input and return - return c.getBool(false) -} - -// Cleanup overwrite the line with the finalized formatted version -func (c *Confirm) Cleanup(val interface{}) error { - // if the value was previously true - ans := yesNo(val.(bool)) - // render the template - return c.Render( - ConfirmQuestionTemplate, - ConfirmTemplateData{Confirm: *c, Answer: ans}, - ) -} diff --git a/vendor/github.com/kataras/survey/core/renderer.go b/vendor/github.com/kataras/survey/core/renderer.go deleted file mode 100644 index 53b457871b..0000000000 --- a/vendor/github.com/kataras/survey/core/renderer.go +++ /dev/null @@ -1,62 +0,0 @@ -package core - -import ( - "strings" - - "github.com/kataras/survey/terminal" -) - -type Renderer struct { - lineCount int - errorLineCount int -} - -var ErrorTemplate = `{{color "red"}}{{ ErrorIcon }} Sorry, your reply was invalid: {{.Error}}{{color "reset"}} -` - -func (r *Renderer) Error(invalid error) error { - // since errors are printed on top we need to reset the prompt - // as well as any previous error print - r.resetPrompt(r.lineCount + r.errorLineCount) - // we just cleared the prompt lines - r.lineCount = 0 - out, err := RunTemplate(ErrorTemplate, invalid) - if err != nil { - return err - } - // keep track of how many lines are printed so we can clean up later - r.errorLineCount = strings.Count(out, "\n") - - // send the message to the user - terminal.Print(out) - return nil -} - -func (r *Renderer) resetPrompt(lines int) { - // clean out current line in case tmpl didnt end in newline - terminal.CursorHorizontalAbsolute(0) - terminal.EraseLine(terminal.ERASE_LINE_ALL) - // clean up what we left behind last time - for i := 0; i < lines; i++ { - terminal.CursorPreviousLine(1) - terminal.EraseLine(terminal.ERASE_LINE_ALL) - } -} - -func (r *Renderer) Render(tmpl string, data interface{}) error { - r.resetPrompt(r.lineCount) - // render the template summarizing the current state - out, err := RunTemplate(tmpl, data) - if err != nil { - return err - } - - // keep track of how many lines are printed so we can clean up later - r.lineCount = strings.Count(out, "\n") - - // print the summary - terminal.Print(out) - - // nothing went wrong - return nil -} diff --git a/vendor/github.com/kataras/survey/core/template.go b/vendor/github.com/kataras/survey/core/template.go deleted file mode 100644 index 9c31cc3069..0000000000 --- a/vendor/github.com/kataras/survey/core/template.go +++ /dev/null @@ -1,83 +0,0 @@ -package core - -import ( - "bytes" - "text/template" - - "github.com/mgutz/ansi" -) - -var DisableColor = false - -var ( - HelpInputRune = '?' - - ErrorIcon = "✘" - HelpIcon = "ⓘ" - QuestionIcon = "?" - - MarkedOptionIcon = "◉" - UnmarkedOptionIcon = "◯" - - SelectFocusIcon = "❯" -) - -var TemplateFuncs = map[string]interface{}{ - // Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format - "color": func(color string) string { - if DisableColor { - return "" - } - return ansi.ColorCode(color) - }, - "HelpInputRune": func() string { - return string(HelpInputRune) - }, - "ErrorIcon": func() string { - return ErrorIcon - }, - "HelpIcon": func() string { - return HelpIcon - }, - "QuestionIcon": func() string { - return QuestionIcon - }, - "MarkedOptionIcon": func() string { - return MarkedOptionIcon - }, - "UnmarkedOptionIcon": func() string { - return UnmarkedOptionIcon - }, - "SelectFocusIcon": func() string { - return SelectFocusIcon - }, -} - -var memoizedGetTemplate = map[string]*template.Template{} - -func getTemplate(tmpl string) (*template.Template, error) { - if t, ok := memoizedGetTemplate[tmpl]; ok { - return t, nil - } - - t, err := template.New("prompt").Funcs(TemplateFuncs).Parse(tmpl) - if err != nil { - return nil, err - } - - memoizedGetTemplate[tmpl] = t - return t, nil -} - -func RunTemplate(tmpl string, data interface{}) (string, error) { - t, err := getTemplate(tmpl) - if err != nil { - return "", err - } - buf := bytes.NewBufferString("") - err = t.Execute(buf, data) - if err != nil { - return "", err - } - return buf.String(), err -} diff --git a/vendor/github.com/kataras/survey/core/write.go b/vendor/github.com/kataras/survey/core/write.go deleted file mode 100644 index becc7b4870..0000000000 --- a/vendor/github.com/kataras/survey/core/write.go +++ /dev/null @@ -1,244 +0,0 @@ -package core - -import ( - "errors" - "fmt" - "reflect" - "strconv" - "strings" -) - -// the tag used to denote the name of the question -const tagName = "survey" - -// add a few interfaces so users can configure how the prompt values are set -type settable interface { - WriteAnswer(field string, value interface{}) error -} - -func WriteAnswer(t interface{}, name string, v interface{}) (err error) { - // if the field is a custom type - if s, ok := t.(settable); ok { - // use the interface method - return s.WriteAnswer(name, v) - } - - // the target to write to - target := reflect.ValueOf(t) - // the value to write from - value := reflect.ValueOf(v) - - // make sure we are writing to a pointer - if target.Kind() != reflect.Ptr { - return errors.New("you must pass a pointer as the target of a Write operation") - } - // the object "inside" of the target pointer - elem := target.Elem() - - // handle the special types - switch elem.Kind() { - // if we are writing to a struct - case reflect.Struct: - // get the name of the field that matches the string we were given - fieldIndex, err := findFieldIndex(elem, name) - // if something went wrong - if err != nil { - // bubble up - return err - } - field := elem.Field(fieldIndex) - // handle references to the settable interface aswell - if s, ok := field.Interface().(settable); ok { - // use the interface method - return s.WriteAnswer(name, v) - } - if field.CanAddr() { - if s, ok := field.Addr().Interface().(settable); ok { - // use the interface method - return s.WriteAnswer(name, v) - } - } - - // copy the value over to the normal struct - return copy(field, value) - case reflect.Map: - mapType := reflect.TypeOf(t).Elem() - if mapType.Key().Kind() != reflect.String || mapType.Elem().Kind() != reflect.Interface { - return errors.New("answer maps must be of type map[string]interface") - } - mt := *t.(*map[string]interface{}) - mt[name] = value.Interface() - return nil - } - // otherwise just copy the value to the target - return copy(elem, value) -} - -func findFieldIndex(s reflect.Value, name string) (int, error) { - // the type of the value - sType := s.Type() - - // first look for matching tags so we can overwrite matching field names - for i := 0; i < sType.NumField(); i++ { - // the field we are current scanning - field := sType.Field(i) - - // the value of the survey tag - tag := field.Tag.Get(tagName) - // if the tag matches the name we are looking for - if tag != "" && tag == name { - // then we found our index - return i, nil - } - } - - // then look for matching names - for i := 0; i < sType.NumField(); i++ { - // the field we are current scanning - field := sType.Field(i) - - // if the name of the field matches what we're looking for - if strings.ToLower(field.Name) == strings.ToLower(name) { - return i, nil - } - } - - // we didn't find the field - return -1, fmt.Errorf("could not find field matching %v", name) -} - -// isList returns true if the element is something we can Len() -func isList(v reflect.Value) bool { - switch v.Type().Kind() { - case reflect.Array, reflect.Slice: - return true - default: - return false - } -} - -// Write takes a value and copies it to the target -func copy(t reflect.Value, v reflect.Value) (err error) { - // if something ends up panicing we need to catch it in a deferred func - defer func() { - if r := recover(); r != nil { - // if we paniced with an error - if _, ok := r.(error); ok { - // cast the result to an error object - err = r.(error) - } else if _, ok := r.(string); ok { - // otherwise we could have paniced with a string so wrap it in an error - err = errors.New(r.(string)) - } - } - }() - - // if we are copying from a string result to something else - if v.Kind() == reflect.String && v.Type() != t.Type() { - var castVal interface{} - var casterr error - vString := v.Interface().(string) - - switch t.Kind() { - case reflect.Bool: - castVal, casterr = strconv.ParseBool(vString) - case reflect.Int: - castVal, casterr = strconv.Atoi(vString) - case reflect.Int8: - var val64 int64 - val64, casterr = strconv.ParseInt(vString, 10, 8) - if casterr == nil { - castVal = int8(val64) - } - case reflect.Int16: - var val64 int64 - val64, casterr = strconv.ParseInt(vString, 10, 16) - if casterr == nil { - castVal = int16(val64) - } - case reflect.Int32: - var val64 int64 - val64, casterr = strconv.ParseInt(vString, 10, 32) - if casterr == nil { - castVal = int32(val64) - } - case reflect.Int64: - castVal, casterr = strconv.ParseInt(vString, 10, 64) - case reflect.Uint: - var val64 uint64 - val64, casterr = strconv.ParseUint(vString, 10, 8) - if casterr == nil { - castVal = uint(val64) - } - case reflect.Uint8: - var val64 uint64 - val64, casterr = strconv.ParseUint(vString, 10, 8) - if casterr == nil { - castVal = uint8(val64) - } - case reflect.Uint16: - var val64 uint64 - val64, casterr = strconv.ParseUint(vString, 10, 16) - if casterr == nil { - castVal = uint16(val64) - } - case reflect.Uint32: - var val64 uint64 - val64, casterr = strconv.ParseUint(vString, 10, 32) - if casterr == nil { - castVal = uint32(val64) - } - case reflect.Uint64: - castVal, casterr = strconv.ParseUint(vString, 10, 64) - case reflect.Float32: - var val64 float64 - val64, casterr = strconv.ParseFloat(vString, 32) - if casterr == nil { - castVal = float32(val64) - } - case reflect.Float64: - castVal, casterr = strconv.ParseFloat(vString, 64) - default: - return fmt.Errorf("Unable to convert from string to type %s", t.Kind()) - } - - if casterr != nil { - return casterr - } - - t.Set(reflect.ValueOf(castVal)) - return - } - - // if we are copying from one slice or array to another - if isList(v) && isList(t) { - // loop over every item in the desired value - for i := 0; i < v.Len(); i++ { - // write to the target given its kind - switch t.Kind() { - // if its a slice - case reflect.Slice: - // an object of the correct type - obj := reflect.Indirect(reflect.New(t.Type().Elem())) - - // write the appropriate value to the obj and catch any errors - if err := copy(obj, v.Index(i)); err != nil { - return err - } - - // just append the value to the end - t.Set(reflect.Append(t, obj)) - // otherwise it could be an array - case reflect.Array: - // set the index to the appropriate value - copy(t.Slice(i, i+1).Index(0), v.Index(i)) - } - } - } else { - // set the value to the target - t.Set(v) - } - - // we're done - return -} diff --git a/vendor/github.com/kataras/survey/core/write_test.go b/vendor/github.com/kataras/survey/core/write_test.go deleted file mode 100644 index 815c4f540e..0000000000 --- a/vendor/github.com/kataras/survey/core/write_test.go +++ /dev/null @@ -1,543 +0,0 @@ -package core - -import ( - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestWrite_returnsErrorIfTargetNotPtr(t *testing.T) { - // try to copy a value to a non-pointer - err := WriteAnswer(true, "hello", true) - // make sure there was an error - if err == nil { - t.Error("Did not encounter error when writing to non-pointer.") - } -} - -func TestWrite_canWriteToBool(t *testing.T) { - // a pointer to hold the boolean value - ptr := true - - // try to copy a false value to the pointer - WriteAnswer(&ptr, "", false) - - // if the value is true - if ptr { - // the test failed - t.Error("Could not write a false bool to a pointer") - } -} - -func TestWrite_canWriteString(t *testing.T) { - // a pointer to hold the boolean value - ptr := "" - - // try to copy a false value to the pointer - err := WriteAnswer(&ptr, "", "hello") - if err != nil { - t.Error(err) - } - - // if the value is not what we wrote - if ptr != "hello" { - t.Error("Could not write a string value to a pointer") - } -} - -func TestWrite_canWriteSlice(t *testing.T) { - // a pointer to hold the value - ptr := []string{} - - // copy in a value - WriteAnswer(&ptr, "", []string{"hello", "world"}) - - // make sure there are two entries - assert.Equal(t, []string{"hello", "world"}, ptr) -} - -func TestWrite_recoversInvalidReflection(t *testing.T) { - // a variable to mutate - ptr := false - - // write a boolean value to the string - err := WriteAnswer(&ptr, "", "hello") - - // if there was no error - if err == nil { - // the test failed - t.Error("Did not encounter error when forced invalid write.") - } -} - -func TestWriteAnswer_handlesNonStructValues(t *testing.T) { - // the value to write to - ptr := "" - - // write a value to the pointer - WriteAnswer(&ptr, "", "world") - - // if we didn't change the value appropriate - if ptr != "world" { - // the test failed - t.Error("Did not write value to primitive pointer") - } -} - -func TestWriteAnswer_canMutateStruct(t *testing.T) { - // the struct to hold the answer - ptr := struct{ Name string }{} - - // write a value to an existing field - err := WriteAnswer(&ptr, "name", "world") - if err != nil { - // the test failed - t.Errorf("Encountered error while writing answer: %v", err.Error()) - // we're done here - return - } - - // make sure we changed the field - if ptr.Name != "world" { - // the test failed - t.Error("Did not mutate struct field when writing answer.") - } -} - -func TestWriteAnswer_canMutateMap(t *testing.T) { - // the map to hold the answer - ptr := make(map[string]interface{}) - - // write a value to an existing field - err := WriteAnswer(&ptr, "name", "world") - if err != nil { - // the test failed - t.Errorf("Encountered error while writing answer: %v", err.Error()) - // we're done here - return - } - - // make sure we changed the field - if ptr["name"] != "world" { - // the test failed - t.Error("Did not mutate map when writing answer.") - } -} - -func TestWrite_returnsErrorIfInvalidMapType(t *testing.T) { - // try to copy a value to a non map[string]interface{} - ptr := make(map[int]string) - - err := WriteAnswer(&ptr, "name", "world") - // make sure there was an error - if err == nil { - t.Error("Did not encounter error when writing to invalid map.") - } -} - -func TestWrite_writesStringSliceToIntSlice(t *testing.T) { - // make a slice of int to write to - target := []int{} - - // write the answer - err := WriteAnswer(&target, "name", []string{"1", "2", "3"}) - - // make sure there was no error - assert.Nil(t, err, "WriteSlice to Int Slice") - // and we got what we wanted - assert.Equal(t, []int{1, 2, 3}, target) -} - -func TestWrite_writesStringArrayToIntArray(t *testing.T) { - // make an array of int to write to - target := [3]int{} - - // write the answer - err := WriteAnswer(&target, "name", [3]string{"1", "2", "3"}) - - // make sure there was no error - assert.Nil(t, err, "WriteArray to Int Array") - // and we got what we wanted - assert.Equal(t, [3]int{1, 2, 3}, target) -} - -func TestWriteAnswer_returnsErrWhenFieldNotFound(t *testing.T) { - // the struct to hold the answer - ptr := struct{ Name string }{} - - // write a value to an existing field - err := WriteAnswer(&ptr, "", "world") - - if err == nil { - // the test failed - t.Error("Did not encountered error while writing answer to non-existing field.") - } -} - -func TestFindFieldIndex_canFindExportedField(t *testing.T) { - // create a reflective wrapper over the struct to look through - val := reflect.ValueOf(struct{ Name string }{}) - - // find the field matching "name" - fieldIndex, err := findFieldIndex(val, "name") - // if something went wrong - if err != nil { - // the test failed - t.Error(err.Error()) - return - } - - // make sure we got the right value - if val.Type().Field(fieldIndex).Name != "Name" { - // the test failed - t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name) - } -} - -func TestFindFieldIndex_canFindTaggedField(t *testing.T) { - // the struct to look through - val := reflect.ValueOf(struct { - Username string `survey:"name"` - }{}) - - // find the field matching "name" - fieldIndex, err := findFieldIndex(val, "name") - // if something went wrong - if err != nil { - // the test failed - t.Error(err.Error()) - return - } - - // make sure we got the right value - if val.Type().Field(fieldIndex).Name != "Username" { - // the test failed - t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name) - } -} - -func TestFindFieldIndex_canHandleCapitalAnswerNames(t *testing.T) { - // create a reflective wrapper over the struct to look through - val := reflect.ValueOf(struct{ Name string }{}) - - // find the field matching "name" - fieldIndex, err := findFieldIndex(val, "Name") - // if something went wrong - if err != nil { - // the test failed - t.Error(err.Error()) - return - } - - // make sure we got the right value - if val.Type().Field(fieldIndex).Name != "Name" { - // the test failed - t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name) - } -} - -func TestFindFieldIndex_tagOverwriteFieldName(t *testing.T) { - // the struct to look through - val := reflect.ValueOf(struct { - Name string - Username string `survey:"name"` - }{}) - - // find the field matching "name" - fieldIndex, err := findFieldIndex(val, "name") - // if something went wrong - if err != nil { - // the test failed - t.Error(err.Error()) - return - } - - // make sure we got the right value - if val.Type().Field(fieldIndex).Name != "Username" { - // the test failed - t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name) - } -} - -type testFieldSettable struct { - Values map[string]string -} - -type testStringSettable struct { - Value string `survey:"string"` -} - -type testTaggedStruct struct { - TaggedValue testStringSettable `survey:"tagged"` -} - -type testPtrTaggedStruct struct { - TaggedValue *testStringSettable `survey:"tagged"` -} - -func (t *testFieldSettable) WriteAnswer(name string, value interface{}) error { - if t.Values == nil { - t.Values = map[string]string{} - } - if v, ok := value.(string); ok { - t.Values[name] = v - return nil - } - return fmt.Errorf("Incompatible type %T", value) -} - -func (t *testStringSettable) WriteAnswer(_ string, value interface{}) error { - t.Value = value.(string) - return nil -} - -func TestWriteWithFieldSettable(t *testing.T) { - testSet1 := testFieldSettable{} - err := WriteAnswer(&testSet1, "values", "stringVal") - assert.Nil(t, err) - assert.Equal(t, map[string]string{"values": "stringVal"}, testSet1.Values) - - testSet2 := testFieldSettable{} - err = WriteAnswer(&testSet2, "values", 123) - assert.Error(t, fmt.Errorf("Incompatible type int64"), err) - assert.Equal(t, map[string]string{}, testSet2.Values) - - testString1 := testStringSettable{} - err = WriteAnswer(&testString1, "", "value1") - assert.Nil(t, err) - assert.Equal(t, testStringSettable{"value1"}, testString1) - - testSetStruct := testTaggedStruct{} - err = WriteAnswer(&testSetStruct, "tagged", "stringVal1") - assert.Nil(t, err) - assert.Equal(t, testTaggedStruct{TaggedValue: testStringSettable{"stringVal1"}}, testSetStruct) - - testPtrSetStruct := testPtrTaggedStruct{&testStringSettable{}} - err = WriteAnswer(&testPtrSetStruct, "tagged", "stringVal1") - assert.Nil(t, err) - assert.Equal(t, testPtrTaggedStruct{TaggedValue: &testStringSettable{"stringVal1"}}, testPtrSetStruct) -} - -// CONVERSION TESTS -func TestWrite_canStringToBool(t *testing.T) { - // a pointer to hold the boolean value - ptr := true - - // try to copy a false value to the pointer - WriteAnswer(&ptr, "", "false") - - // if the value is true - if ptr { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToInt(t *testing.T) { - // a pointer to hold the value - var ptr int = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToInt8(t *testing.T) { - // a pointer to hold the value - var ptr int8 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToInt16(t *testing.T) { - // a pointer to hold the value - var ptr int16 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToInt32(t *testing.T) { - // a pointer to hold the value - var ptr int32 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToInt64(t *testing.T) { - // a pointer to hold the value - var ptr int64 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToUint(t *testing.T) { - // a pointer to hold the value - var ptr uint = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToUint8(t *testing.T) { - // a pointer to hold the value - var ptr uint8 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToUint16(t *testing.T) { - // a pointer to hold the value - var ptr uint16 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToUint32(t *testing.T) { - // a pointer to hold the value - var ptr uint32 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToUint64(t *testing.T) { - // a pointer to hold the value - var ptr uint64 = 1 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2") - - // if the value is true - if ptr != 2 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToFloat32(t *testing.T) { - // a pointer to hold the value - var ptr float32 = 1.0 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2.5") - - // if the value is true - if ptr != 2.5 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canStringToFloat64(t *testing.T) { - // a pointer to hold the value - var ptr float64 = 1.0 - - // try to copy a value to the pointer - WriteAnswer(&ptr, "", "2.5") - - // if the value is true - if ptr != 2.5 { - // the test failed - t.Error("Could not convert string to pointer type") - } -} - -func TestWrite_canConvertStructFieldTypes(t *testing.T) { - // the struct to hold the answer - ptr := struct { - Name string - Age uint - Male bool - Height float64 - }{} - - // write the values as strings - check(t, WriteAnswer(&ptr, "name", "Bob")) - check(t, WriteAnswer(&ptr, "age", "22")) - check(t, WriteAnswer(&ptr, "male", "true")) - check(t, WriteAnswer(&ptr, "height", "6.2")) - - // make sure we changed the fields - if ptr.Name != "Bob" { - t.Error("Did not mutate Name when writing answer.") - } - - if ptr.Age != 22 { - t.Error("Did not mutate Age when writing answer.") - } - - if !ptr.Male { - t.Error("Did not mutate Male when writing answer.") - } - - if ptr.Height != 6.2 { - t.Error("Did not mutate Height when writing answer.") - } -} - -func check(t *testing.T, err error) { - if err != nil { - t.Fatalf("Encountered error while writing answer: %v", err.Error()) - } -} diff --git a/vendor/github.com/kataras/survey/editor.go b/vendor/github.com/kataras/survey/editor.go deleted file mode 100644 index 2241dd3435..0000000000 --- a/vendor/github.com/kataras/survey/editor.go +++ /dev/null @@ -1,168 +0,0 @@ -package survey - -import ( - "bytes" - "io/ioutil" - "os" - "os/exec" - "runtime" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -/* -Editor launches an instance of the users preferred editor on a temporary file. -The editor to use is determined by reading the $VISUAL or $EDITOR environment -variables. If neither of those are present, notepad (on Windows) or vim -(others) is used. -The launch of the editor is triggered by the enter key. Since the response may -be long, it will not be echoed as Input does, instead, it print . -Response type is a string. - - message := "" - prompt := &survey.Editor{ Message: "What is your commit message?" } - survey.AskOne(prompt, &message, nil) -*/ -type Editor struct { - core.Renderer - Message string - Default string - Help string -} - -// data available to the templates when processing -type EditorTemplateData struct { - Editor - Answer string - ShowAnswer bool - ShowHelp bool -} - -// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format -var EditorQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }} {{color "reset"}} -{{- if .ShowAnswer}} - {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} -{{- else }} - {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}} - {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} - {{- color "cyan"}}[Enter to launch editor] {{color "reset"}} -{{- end}}` - -var ( - bom = []byte{0xef, 0xbb, 0xbf} - editor = "vim" -) - -func init() { - if runtime.GOOS == "windows" { - editor = "notepad" - } - if v := os.Getenv("VISUAL"); v != "" { - editor = v - } else if e := os.Getenv("EDITOR"); e != "" { - editor = e - } -} - -func (e *Editor) Prompt() (interface{}, error) { - // render the template - err := e.Render( - EditorQuestionTemplate, - EditorTemplateData{Editor: *e}, - ) - if err != nil { - return "", err - } - - // start reading runes from the standard in - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - - terminal.CursorHide() - defer terminal.CursorShow() - - for { - r, _, err := rr.ReadRune() - if err != nil { - return "", err - } - if r == '\r' || r == '\n' { - break - } - if r == terminal.KeyInterrupt { - return "", terminal.InterruptErr - } - if r == terminal.KeyEndTransmission { - break - } - if r == core.HelpInputRune && e.Help != "" { - err = e.Render( - EditorQuestionTemplate, - EditorTemplateData{Editor: *e, ShowHelp: true}, - ) - if err != nil { - return "", err - } - } - continue - } - - // prepare the temp file - f, err := ioutil.TempFile("", "survey") - if err != nil { - return "", err - } - defer os.Remove(f.Name()) - - // write utf8 BOM header - // The reason why we do this is because notepad.exe on Windows determines the - // encoding of an "empty" text file by the locale, for example, GBK in China, - // while golang string only handles utf8 well. However, a text file with utf8 - // BOM header is not considered "empty" on Windows, and the encoding will then - // be determined utf8 by notepad.exe, instead of GBK or other encodings. - if _, err := f.Write(bom); err != nil { - return "", err - } - // close the fd to prevent the editor unable to save file - if err := f.Close(); err != nil { - return "", err - } - - // open the editor - cmd := exec.Command(editor, f.Name()) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - terminal.CursorShow() - if err := cmd.Run(); err != nil { - return "", err - } - - // raw is a BOM-unstripped UTF8 byte slice - raw, err := ioutil.ReadFile(f.Name()) - if err != nil { - return "", err - } - - // strip BOM header - text := string(bytes.TrimPrefix(raw, bom)) - - // check length, return default value on empty - if len(text) == 0 { - return e.Default, nil - } - - return text, nil -} - -func (e *Editor) Cleanup(val interface{}) error { - return e.Render( - EditorQuestionTemplate, - EditorTemplateData{Editor: *e, Answer: "", ShowAnswer: true}, - ) -} diff --git a/vendor/github.com/kataras/survey/input.go b/vendor/github.com/kataras/survey/input.go deleted file mode 100644 index a774751326..0000000000 --- a/vendor/github.com/kataras/survey/input.go +++ /dev/null @@ -1,98 +0,0 @@ -package survey - -import ( - "os" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -/* -Input is a regular text input that prints each character the user types on the screen -and accepts the input with the enter key. Response type is a string. - - name := "" - prompt := &survey.Input{ Message: "What is your name?" } - survey.AskOne(prompt, &name, nil) -*/ -type Input struct { - core.Renderer - Message string - Default string - Help string -} - -// data available to the templates when processing -type InputTemplateData struct { - Input - Answer string - ShowAnswer bool - ShowHelp bool -} - -// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format -var InputQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }} {{color "reset"}} -{{- if .ShowAnswer}} - {{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}} -{{- else }} - {{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}} - {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} -{{- end}}` - -func (i *Input) Prompt() (interface{}, error) { - // render the template - err := i.Render( - InputQuestionTemplate, - InputTemplateData{Input: *i}, - ) - if err != nil { - return "", err - } - - // start reading runes from the standard in - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - - line := []rune{} - // get the next line - for { - line, err = rr.ReadLine(0) - if err != nil { - return string(line), err - } - // terminal will echo the \n so we need to jump back up one row - terminal.CursorPreviousLine(1) - - if string(line) == string(core.HelpInputRune) && i.Help != "" { - err = i.Render( - InputQuestionTemplate, - InputTemplateData{Input: *i, ShowHelp: true}, - ) - if err != nil { - return "", err - } - continue - } - break - } - - // if the line is empty - if line == nil || len(line) == 0 { - // use the default value - return i.Default, err - } - - // we're done - return string(line), err -} - -func (i *Input) Cleanup(val interface{}) error { - return i.Render( - InputQuestionTemplate, - InputTemplateData{Input: *i, Answer: val.(string), ShowAnswer: true}, - ) -} diff --git a/vendor/github.com/kataras/survey/multiselect.go b/vendor/github.com/kataras/survey/multiselect.go deleted file mode 100644 index 1d72b49ce5..0000000000 --- a/vendor/github.com/kataras/survey/multiselect.go +++ /dev/null @@ -1,203 +0,0 @@ -package survey - -import ( - "errors" - "os" - "strings" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -/* -MultiSelect is a prompt that presents a list of various options to the user -for them to select using the arrow keys and enter. Response type is a slice of strings. - - days := []string{} - prompt := &survey.MultiSelect{ - Message: "What days do you prefer:", - Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, - } - survey.AskOne(prompt, &days, nil) -*/ -type MultiSelect struct { - core.Renderer - Message string - Options []string - Default []string - Help string - PageSize int - selectedIndex int - checked map[string]bool - showingHelp bool -} - -// data available to the templates when processing -type MultiSelectTemplateData struct { - MultiSelect - Answer string - ShowAnswer bool - Checked map[string]bool - SelectedIndex int - ShowHelp bool - PageEntries []string -} - -var MultiSelectQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }}{{color "reset"}} -{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} -{{- else }} - {{- if and .Help (not .ShowHelp)}} {{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}}{{end}} - {{- "\n"}} - {{- range $ix, $option := .PageEntries}} - {{- if eq $ix $.SelectedIndex}}{{color "cyan"}}{{ SelectFocusIcon }}{{color "reset"}}{{else}} {{end}} - {{- if index $.Checked $option}}{{color "green"}} {{ MarkedOptionIcon }} {{else}}{{color "default+hb"}} {{ UnmarkedOptionIcon }} {{end}} - {{- color "reset"}} - {{- " "}}{{$option}}{{"\n"}} - {{- end}} -{{- end}}` - -// OnChange is called on every keypress. -func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { - if key == terminal.KeyArrowUp { - // if we are at the top of the list - if m.selectedIndex == 0 { - // go to the bottom - m.selectedIndex = len(m.Options) - 1 - } else { - // decrement the selected index - m.selectedIndex-- - } - } else if key == terminal.KeyArrowDown { - // if we are at the bottom of the list - if m.selectedIndex == len(m.Options)-1 { - // start at the top - m.selectedIndex = 0 - } else { - // increment the selected index - m.selectedIndex++ - } - // if the user pressed down and there is room to move - } else if key == terminal.KeySpace { - if old, ok := m.checked[m.Options[m.selectedIndex]]; !ok { - // otherwise just invert the current value - m.checked[m.Options[m.selectedIndex]] = true - } else { - // otherwise just invert the current value - m.checked[m.Options[m.selectedIndex]] = !old - } - // only show the help message if we have one to show - } else if key == core.HelpInputRune && m.Help != "" { - m.showingHelp = true - } - - // paginate the options - opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex) - - // render the options - m.Render( - MultiSelectQuestionTemplate, - MultiSelectTemplateData{ - MultiSelect: *m, - SelectedIndex: idx, - Checked: m.checked, - ShowHelp: m.showingHelp, - PageEntries: opts, - }, - ) - - // if we are not pressing ent - return line, 0, true -} - -func (m *MultiSelect) Prompt() (interface{}, error) { - // compute the default state - m.checked = make(map[string]bool) - // if there is a default - if len(m.Default) > 0 { - for _, dflt := range m.Default { - for _, opt := range m.Options { - // if the option correponds to the default - if opt == dflt { - // we found our initial value - m.checked[opt] = true - // stop looking - break - } - } - } - } - - // if there are no options to render - if len(m.Options) == 0 { - // we failed - return "", errors.New("please provide options to select from") - } - - // hide the cursor - terminal.CursorHide() - // show the cursor when we're done - defer terminal.CursorShow() - - // paginate the options - opts, idx := paginate(m.PageSize, m.Options, m.selectedIndex) - - // ask the question - err := m.Render( - MultiSelectQuestionTemplate, - MultiSelectTemplateData{ - MultiSelect: *m, - SelectedIndex: idx, - Checked: m.checked, - PageEntries: opts, - }, - ) - if err != nil { - return "", err - } - - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - - // start waiting for input - for { - r, _, _ := rr.ReadRune() - if r == '\r' || r == '\n' { - break - } - if r == terminal.KeyInterrupt { - return "", terminal.InterruptErr - } - if r == terminal.KeyEndTransmission { - break - } - m.OnChange(nil, 0, r) - } - - answers := []string{} - for _, option := range m.Options { - if val, ok := m.checked[option]; ok && val { - answers = append(answers, option) - } - } - - return answers, nil -} - -// Cleanup removes the options section, and renders the ask like a normal question. -func (m *MultiSelect) Cleanup(val interface{}) error { - // execute the output summary template with the answer - return m.Render( - MultiSelectQuestionTemplate, - MultiSelectTemplateData{ - MultiSelect: *m, - SelectedIndex: m.selectedIndex, - Checked: m.checked, - Answer: strings.Join(val.([]string), ", "), - ShowAnswer: true, - }, - ) -} diff --git a/vendor/github.com/kataras/survey/password.go b/vendor/github.com/kataras/survey/password.go deleted file mode 100644 index 9065fec588..0000000000 --- a/vendor/github.com/kataras/survey/password.go +++ /dev/null @@ -1,84 +0,0 @@ -package survey - -import ( - "os" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -/* -Password is like a normal Input but the text shows up as *'s and there is no default. Response -type is a string. - - password := "" - prompt := &survey.Password{ Message: "Please type your password" } - survey.AskOne(prompt, &password, nil) -*/ -type Password struct { - core.Renderer - Message string - Help string -} - -type PasswordTemplateData struct { - Password - ShowHelp bool -} - -// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format -var PasswordQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }} {{color "reset"}} -{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}} {{end}}` - -func (p *Password) Prompt() (line interface{}, err error) { - // render the question template - out, err := core.RunTemplate( - PasswordQuestionTemplate, - PasswordTemplateData{Password: *p}, - ) - terminal.Print(out) - if err != nil { - return "", err - } - - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - - // no help msg? Just return any response - if p.Help == "" { - line, err := rr.ReadLine('*') - return string(line), err - } - - // process answers looking for help prompt answer - for { - line, err := rr.ReadLine('*') - if err != nil { - return string(line), err - } - - if string(line) == string(core.HelpInputRune) { - // terminal will echo the \n so we need to jump back up one row - terminal.CursorPreviousLine(1) - - err = p.Render( - PasswordQuestionTemplate, - PasswordTemplateData{Password: *p, ShowHelp: true}, - ) - if err != nil { - return "", err - } - continue - } - return string(line), err - } -} - -// Cleanup hides the string with a fixed number of characters. -func (prompt *Password) Cleanup(val interface{}) error { - return nil -} diff --git a/vendor/github.com/kataras/survey/select.go b/vendor/github.com/kataras/survey/select.go deleted file mode 100644 index 12820428a4..0000000000 --- a/vendor/github.com/kataras/survey/select.go +++ /dev/null @@ -1,209 +0,0 @@ -package survey - -import ( - "errors" - "os" - - "github.com/kataras/survey/core" - "github.com/kataras/survey/terminal" -) - -/* -Select is a prompt that presents a list of various options to the user -for them to select using the arrow keys and enter. Response type is a string. - - color := "" - prompt := &survey.Select{ - Message: "Choose a color:", - Options: []string{"red", "blue", "green"}, - } - survey.AskOne(prompt, &color, nil) -*/ -type Select struct { - core.Renderer - Message string - Options []string - Default string - Help string - PageSize int - selectedIndex int - useDefault bool - showingHelp bool -} - -// the data available to the templates when processing -type SelectTemplateData struct { - Select - PageEntries []string - SelectedIndex int - Answer string - ShowAnswer bool - ShowHelp bool -} - -var SelectQuestionTemplate = ` -{{- if .ShowHelp }}{{- color "cyan"}}{{ HelpIcon }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}} -{{- color "green+hb"}}{{ QuestionIcon }} {{color "reset"}} -{{- color "default+hb"}}{{ .Message }}{{color "reset"}} -{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}} -{{- else}} - {{- if and .Help (not .ShowHelp)}} {{color "cyan"}}[{{ HelpInputRune }} for help]{{color "reset"}}{{end}} - {{- "\n"}} - {{- range $ix, $choice := .PageEntries}} - {{- if eq $ix $.SelectedIndex}}{{color "cyan+b"}}{{ SelectFocusIcon }} {{else}}{{color "default+hb"}} {{end}} - {{- $choice}} - {{- color "reset"}}{{"\n"}} - {{- end}} -{{- end}}` - -// OnChange is called on every keypress. -func (s *Select) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { - // if the user pressed the enter key - if key == terminal.KeyEnter { - return []rune(s.Options[s.selectedIndex]), 0, true - // if the user pressed the up arrow - } else if key == terminal.KeyArrowUp { - s.useDefault = false - - // if we are at the top of the list - if s.selectedIndex == 0 { - // start from the button - s.selectedIndex = len(s.Options) - 1 - } else { - // otherwise we are not at the top of the list so decrement the selected index - s.selectedIndex-- - } - // if the user pressed down and there is room to move - } else if key == terminal.KeyArrowDown { - s.useDefault = false - // if we are at the bottom of the list - if s.selectedIndex == len(s.Options)-1 { - // start from the top - s.selectedIndex = 0 - } else { - // increment the selected index - s.selectedIndex++ - } - // only show the help message if we have one - } else if key == core.HelpInputRune && s.Help != "" { - s.showingHelp = true - } - - // figure out the options and index to render - opts, idx := paginate(s.PageSize, s.Options, s.selectedIndex) - - // render the options - s.Render( - SelectQuestionTemplate, - SelectTemplateData{ - Select: *s, - SelectedIndex: idx, - ShowHelp: s.showingHelp, - PageEntries: opts, - }, - ) - - // if we are not pressing ent - return []rune(s.Options[s.selectedIndex]), 0, true -} - -func (s *Select) Prompt() (interface{}, error) { - // if there are no options to render - if len(s.Options) == 0 { - // we failed - return "", errors.New("please provide options to select from") - } - - // start off with the first option selected - sel := 0 - // if there is a default - if s.Default != "" { - // find the choice - for i, opt := range s.Options { - // if the option correponds to the default - if opt == s.Default { - // we found our initial value - sel = i - // stop looking - break - } - } - } - // save the selected index - s.selectedIndex = sel - - // figure out the options and index to render - opts, idx := paginate(s.PageSize, s.Options, sel) - - // ask the question - err := s.Render( - SelectQuestionTemplate, - SelectTemplateData{ - Select: *s, - PageEntries: opts, - SelectedIndex: idx, - }, - ) - if err != nil { - return "", err - } - - // hide the cursor - terminal.CursorHide() - // show the cursor when we're done - defer terminal.CursorShow() - - // by default, use the default value - s.useDefault = true - - rr := terminal.NewRuneReader(os.Stdin) - rr.SetTermMode() - defer rr.RestoreTermMode() - // start waiting for input - for { - r, _, err := rr.ReadRune() - if err != nil { - return "", err - } - if r == '\r' || r == '\n' { - break - } - if r == terminal.KeyInterrupt { - return "", terminal.InterruptErr - } - if r == terminal.KeyEndTransmission { - break - } - s.OnChange(nil, 0, r) - } - - var val string - // if we are supposed to use the default value - if s.useDefault { - // if there is a default value - if s.Default != "" { - // use the default value - val = s.Default - } else { - // there is no default value so use the first - val = s.Options[0] - } - // otherwise the selected index points to the value - } else { - // the - val = s.Options[s.selectedIndex] - } - - return val, err -} - -func (s *Select) Cleanup(val interface{}) error { - return s.Render( - SelectQuestionTemplate, - SelectTemplateData{ - Select: *s, - Answer: val.(string), - ShowAnswer: true, - }, - ) -} diff --git a/vendor/github.com/kataras/survey/survey.go b/vendor/github.com/kataras/survey/survey.go deleted file mode 100644 index 2e5fd40e61..0000000000 --- a/vendor/github.com/kataras/survey/survey.go +++ /dev/null @@ -1,198 +0,0 @@ -package survey - -import ( - "errors" - - "github.com/kataras/survey/core" -) - -// PageSize is the default maximum number of items to show in select/multiselect prompts -var PageSize = 7 - -// Validator is a function passed to a Question after a user has provided a response. -// If the function returns an error, then the user will be prompted again for another -// response. -type Validator func(ans interface{}) error - -// Transformer is a function passed to a Question after a user has provided a response. -// The function can be used to implement a custom logic that will result to return -// a different representation of the given answer. -// -// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more. -type Transformer func(ans interface{}) (newAns interface{}) - -// Question is the core data structure for a survey questionnaire. -type Question struct { - Name string - Prompt Prompt - Validate Validator - Transform Transformer -} - -// Prompt is the primary interface for the objects that can take user input -// and return a response. -type Prompt interface { - Prompt() (interface{}, error) - Cleanup(interface{}) error - Error(error) error -} - -/* -AskOne performs the prompt for a single prompt and asks for validation if required. -Response types should be something that can be casted from the response type designated -in the documentation. For example: - - name := "" - prompt := &survey.Input{ - Message: "name", - } - - survey.AskOne(prompt, &name, nil) - -*/ -func AskOne(p Prompt, response interface{}, v Validator) error { - err := Ask([]*Question{{Prompt: p, Validate: v}}, response) - if err != nil { - return err - } - - return nil -} - -/* -Ask performs the prompt loop, asking for validation when appropriate. The response -type can be one of two options. If a struct is passed, the answer will be written to -the field whose name matches the Name field on the corresponding question. Field types -should be something that can be casted from the response type designated in the -documentation. Note, a survey tag can also be used to identify a Otherwise, a -map[string]interface{} can be passed, responses will be written to the key with the -matching name. For example: - - qs := []*survey.Question{ - { - Name: "name", - Prompt: &survey.Input{Message: "What is your name?"}, - Validate: survey.Required, - Transform: survey.Title, - }, - } - - answers := struct{ Name string }{} - - - err := survey.Ask(qs, &answers) -*/ -func Ask(qs []*Question, response interface{}) error { - - // if we weren't passed a place to record the answers - if response == nil { - // we can't go any further - return errors.New("cannot call Ask() with a nil reference to record the answers") - } - - // go over every question - for _, q := range qs { - // grab the user input and save it - ans, err := q.Prompt.Prompt() - // if there was a problem - if err != nil { - return err - } - - // if there is a validate handler for this question - if q.Validate != nil { - // wait for a valid response - for invalid := q.Validate(ans); invalid != nil; invalid = q.Validate(ans) { - err := q.Prompt.Error(invalid) - // if there was a problem - if err != nil { - return err - } - - // ask for more input - ans, err = q.Prompt.Prompt() - // if there was a problem - if err != nil { - return err - } - } - } - - if q.Transform != nil { - // check if we have a transformer available, if so - // then try to acquire the new representation of the - // answer, if the resulting answer is not nil. - if newAns := q.Transform(ans); newAns != nil { - ans = newAns - } - } - - // tell the prompt to cleanup with the validated value - q.Prompt.Cleanup(ans) - - // if something went wrong - if err != nil { - // stop listening - return err - } - - // add it to the map - err = core.WriteAnswer(response, q.Name, ans) - // if something went wrong - if err != nil { - return err - } - - } - // return the response - return nil -} - -// paginate returns a single page of choices given the page size, the total list of -// possible choices, and the current selected index in the total list. -func paginate(page int, choices []string, sel int) ([]string, int) { - // the number of elements to show in a single page - var pageSize int - // if the select has a specific page size - if page != 0 { - // use the specified one - pageSize = page - // otherwise the select does not have a page size - } else { - // use the package default - pageSize = PageSize - } - - var start, end, cursor int - - if len(choices) < pageSize { - // if we dont have enough options to fill a page - start = 0 - end = len(choices) - cursor = sel - - } else if sel < pageSize/2 { - // if we are in the first half page - start = 0 - end = pageSize - cursor = sel - - } else if len(choices)-sel-1 < pageSize/2 { - // if we are in the last half page - start = len(choices) - pageSize - end = len(choices) - cursor = sel - start - - } else { - // somewhere in the middle - above := pageSize / 2 - below := pageSize - above - - cursor = pageSize / 2 - start = sel - above - end = sel + below - } - - // return the subset we care about and the index - return choices[start:end], cursor -} diff --git a/vendor/github.com/kataras/survey/terminal/cursor.go b/vendor/github.com/kataras/survey/terminal/cursor.go deleted file mode 100644 index 533b75a644..0000000000 --- a/vendor/github.com/kataras/survey/terminal/cursor.go +++ /dev/null @@ -1,134 +0,0 @@ -// +build !windows - -package terminal - -import ( - "bufio" - "fmt" - "os" - "regexp" - "strconv" - "strings" -) - -// CursorUp moves the cursor n cells to up. -func CursorUp(n int) { - fmt.Printf("\x1b[%dA", n) -} - -// CursorDown moves the cursor n cells to down. -func CursorDown(n int) { - fmt.Printf("\x1b[%dB", n) -} - -// CursorForward moves the cursor n cells to right. -func CursorForward(n int) { - fmt.Printf("\x1b[%dC", n) -} - -// CursorBack moves the cursor n cells to left. -func CursorBack(n int) { - fmt.Printf("\x1b[%dD", n) -} - -// CursorNextLine moves cursor to beginning of the line n lines down. -func CursorNextLine(n int) { - fmt.Printf("\x1b[%dE", n) -} - -// CursorPreviousLine moves cursor to beginning of the line n lines up. -func CursorPreviousLine(n int) { - fmt.Printf("\x1b[%dF", n) -} - -// CursorHorizontalAbsolute moves cursor horizontally to x. -func CursorHorizontalAbsolute(x int) { - fmt.Printf("\x1b[%dG", x) -} - -// CursorShow shows the cursor. -func CursorShow() { - fmt.Print("\x1b[?25h") -} - -// CursorHide hide the cursor. -func CursorHide() { - fmt.Print("\x1b[?25l") -} - -// CursorMove moves the cursor to a specific x,y location. -func CursorMove(x int, y int) { - fmt.Printf("\x1b[%d;%df", x, y) -} - -// CursorLocation returns the current location of the cursor in the terminal -func CursorLocation() (*Coord, error) { - // print the escape sequence to receive the position in our stdin - fmt.Print("\x1b[6n") - - // read from stdin to get the response - reader := bufio.NewReader(os.Stdin) - // spec says we read 'til R, so do that - text, err := reader.ReadSlice('R') - if err != nil { - return nil, err - } - - // spec also says they're split by ;, so do that too - if strings.Contains(string(text), ";") { - // a regex to parse the output of the ansi code - re := regexp.MustCompile(`\d+;\d+`) - line := re.FindString(string(text)) - - // find the column and rows embedded in the string - coords := strings.Split(line, ";") - - // try to cast the col number to an int - col, err := strconv.Atoi(coords[1]) - if err != nil { - return nil, err - } - - // try to cast the row number to an int - row, err := strconv.Atoi(coords[0]) - if err != nil { - return nil, err - } - - // return the coordinate object with the col and row we calculated - return &Coord{Short(col), Short(row)}, nil - } - - // it didn't work so return an error - return nil, fmt.Errorf("could not compute the cursor position using ascii escape sequences") -} - -// Size returns the height and width of the terminal. -func Size() (*Coord, error) { - // the general approach here is to move the cursor to the very bottom - // of the terminal, ask for the current location and then move the - // cursor back where we started - - // save the current location of the cursor - origin, err := CursorLocation() - if err != nil { - return nil, err - } - - // move the cursor to the very bottom of the terminal - CursorMove(999, 999) - - // ask for the current location - bottom, err := CursorLocation() - if err != nil { - return nil, err - } - - // move back where we began - CursorUp(int(bottom.Y - origin.Y)) - CursorHorizontalAbsolute(int(origin.X)) - - // sice the bottom was calcuated in the lower right corner, it - // is the dimensions we are looking for - return bottom, nil -} diff --git a/vendor/github.com/kataras/survey/terminal/cursor_windows.go b/vendor/github.com/kataras/survey/terminal/cursor_windows.go deleted file mode 100644 index 9a7d5b4721..0000000000 --- a/vendor/github.com/kataras/survey/terminal/cursor_windows.go +++ /dev/null @@ -1,101 +0,0 @@ -package terminal - -import ( - "os" - "syscall" - "unsafe" -) - -func CursorUp(n int) { - cursorMove(0, n) -} - -func CursorDown(n int) { - cursorMove(0, -1*n) -} - -func CursorForward(n int) { - cursorMove(n, 0) -} - -func CursorBack(n int) { - cursorMove(-1*n, 0) -} - -func cursorMove(x int, y int) { - handle := syscall.Handle(os.Stdout.Fd()) - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - - var cursor Coord - cursor.X = csbi.cursorPosition.X + Short(x) - cursor.Y = csbi.cursorPosition.Y + Short(y) - - procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor)))) -} - -func CursorNextLine(n int) { - CursorUp(n) - CursorHorizontalAbsolute(0) -} - -func CursorPreviousLine(n int) { - CursorDown(n) - CursorHorizontalAbsolute(0) -} - -func CursorHorizontalAbsolute(x int) { - handle := syscall.Handle(os.Stdout.Fd()) - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - - var cursor Coord - cursor.X = Short(x) - cursor.Y = csbi.cursorPosition.Y - - if csbi.size.X < cursor.X { - cursor.X = csbi.size.X - } - - procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor)))) -} - -func CursorShow() { - handle := syscall.Handle(os.Stdout.Fd()) - - var cci consoleCursorInfo - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) - cci.visible = 1 - - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) -} - -func CursorHide() { - handle := syscall.Handle(os.Stdout.Fd()) - - var cci consoleCursorInfo - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) - cci.visible = 0 - - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))) -} - -func CursorLocation() (Coord, error) { - handle := syscall.Handle(os.Stdout.Fd()) - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - - return csbi.cursorPosition, nil -} - -func Size() (Coord, error) { - handle := syscall.Handle(os.Stdout.Fd()) - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - - return csbi.size, nil -} diff --git a/vendor/github.com/kataras/survey/terminal/display.go b/vendor/github.com/kataras/survey/terminal/display.go deleted file mode 100644 index 0f014b1353..0000000000 --- a/vendor/github.com/kataras/survey/terminal/display.go +++ /dev/null @@ -1,9 +0,0 @@ -package terminal - -type EraseLineMode int - -const ( - ERASE_LINE_END EraseLineMode = iota - ERASE_LINE_START - ERASE_LINE_ALL -) diff --git a/vendor/github.com/kataras/survey/terminal/display_posix.go b/vendor/github.com/kataras/survey/terminal/display_posix.go deleted file mode 100644 index 47a765c794..0000000000 --- a/vendor/github.com/kataras/survey/terminal/display_posix.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build !windows - -package terminal - -import ( - "fmt" -) - -func EraseLine(mode EraseLineMode) { - fmt.Printf("\x1b[%dK", mode) -} diff --git a/vendor/github.com/kataras/survey/terminal/display_windows.go b/vendor/github.com/kataras/survey/terminal/display_windows.go deleted file mode 100644 index bb1ef45d7b..0000000000 --- a/vendor/github.com/kataras/survey/terminal/display_windows.go +++ /dev/null @@ -1,28 +0,0 @@ -package terminal - -import ( - "os" - "syscall" - "unsafe" -) - -func EraseLine(mode EraseLineMode) { - handle := syscall.Handle(os.Stdout.Fd()) - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - - var w uint32 - var x Short - cursor := csbi.cursorPosition - switch mode { - case ERASE_LINE_END: - x = csbi.size.X - case ERASE_LINE_START: - x = 0 - case ERASE_LINE_ALL: - cursor.X = 0 - x = csbi.size.X - } - procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w))) -} diff --git a/vendor/github.com/kataras/survey/terminal/error.go b/vendor/github.com/kataras/survey/terminal/error.go deleted file mode 100644 index 710c361406..0000000000 --- a/vendor/github.com/kataras/survey/terminal/error.go +++ /dev/null @@ -1,9 +0,0 @@ -package terminal - -import ( - "errors" -) - -var ( - InterruptErr = errors.New("interrupt") -) diff --git a/vendor/github.com/kataras/survey/terminal/output.go b/vendor/github.com/kataras/survey/terminal/output.go deleted file mode 100644 index fabbb9fcde..0000000000 --- a/vendor/github.com/kataras/survey/terminal/output.go +++ /dev/null @@ -1,20 +0,0 @@ -// +build !windows - -package terminal - -import ( - "io" - "os" -) - -// Returns special stdout, which converts escape sequences to Windows API calls -// on Windows environment. -func NewAnsiStdout() io.Writer { - return os.Stdout -} - -// Returns special stderr, which converts escape sequences to Windows API calls -// on Windows environment. -func NewAnsiStderr() io.Writer { - return os.Stderr -} diff --git a/vendor/github.com/kataras/survey/terminal/output_windows.go b/vendor/github.com/kataras/survey/terminal/output_windows.go deleted file mode 100644 index 7d7f47fb14..0000000000 --- a/vendor/github.com/kataras/survey/terminal/output_windows.go +++ /dev/null @@ -1,228 +0,0 @@ -package terminal - -import ( - "bytes" - "fmt" - "io" - "os" - "strconv" - "strings" - "syscall" - "unsafe" - - "github.com/mattn/go-isatty" -) - -var ( - singleArgFunctions = map[rune]func(int){ - 'A': CursorUp, - 'B': CursorDown, - 'C': CursorForward, - 'D': CursorBack, - 'E': CursorNextLine, - 'F': CursorPreviousLine, - 'G': CursorHorizontalAbsolute, - } -) - -const ( - foregroundBlue = 0x1 - foregroundGreen = 0x2 - foregroundRed = 0x4 - foregroundIntensity = 0x8 - foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) - backgroundBlue = 0x10 - backgroundGreen = 0x20 - backgroundRed = 0x40 - backgroundIntensity = 0x80 - backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) -) - -type Writer struct { - out io.Writer - handle syscall.Handle - orgAttr word -} - -func NewAnsiStdout() io.Writer { - var csbi consoleScreenBufferInfo - out := os.Stdout - if !isatty.IsTerminal(out.Fd()) { - return out - } - handle := syscall.Handle(out.Fd()) - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - return &Writer{out: out, handle: handle, orgAttr: csbi.attributes} -} - -func NewAnsiStderr() io.Writer { - var csbi consoleScreenBufferInfo - out := os.Stderr - if !isatty.IsTerminal(out.Fd()) { - return out - } - handle := syscall.Handle(out.Fd()) - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - return &Writer{out: out, handle: handle, orgAttr: csbi.attributes} -} - -func (w *Writer) Write(data []byte) (n int, err error) { - r := bytes.NewReader(data) - - for { - ch, size, err := r.ReadRune() - if err != nil { - break - } - n += size - - switch ch { - case '\x1b': - size, err = w.handleEscape(r) - n += size - if err != nil { - break - } - default: - fmt.Fprint(w.out, string(ch)) - } - } - return -} - -func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) { - buf := make([]byte, 0, 10) - buf = append(buf, "\x1b"...) - - // Check '[' continues after \x1b - ch, size, err := r.ReadRune() - if err != nil { - fmt.Fprint(w.out, string(buf)) - return - } - n += size - if ch != '[' { - fmt.Fprint(w.out, string(buf)) - return - } - - // Parse escape code - var code rune - argBuf := make([]byte, 0, 10) - for { - ch, size, err = r.ReadRune() - if err != nil { - fmt.Fprint(w.out, string(buf)) - return - } - n += size - if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') { - code = ch - break - } - argBuf = append(argBuf, string(ch)...) - } - - w.applyEscapeCode(buf, string(argBuf), code) - return -} - -func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) { - switch arg + string(code) { - case "?25h": - CursorShow() - return - case "?25l": - CursorHide() - return - } - - if f, ok := singleArgFunctions[code]; ok { - if n, err := strconv.Atoi(arg); err == nil { - f(n) - return - } - } - - switch code { - case 'm': - w.applySelectGraphicRendition(arg) - default: - buf = append(buf, string(code)...) - fmt.Fprint(w.out, string(buf)) - } -} - -// Original implementation: https://github.com/mattn/go-colorable -func (w *Writer) applySelectGraphicRendition(arg string) { - if arg == "" { - procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr)) - return - } - - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - attr := csbi.attributes - - for _, param := range strings.Split(arg, ";") { - n, err := strconv.Atoi(param) - if err != nil { - continue - } - - switch { - case n == 0 || n == 100: - attr = w.orgAttr - case 1 <= n && n <= 5: - attr |= foregroundIntensity - case 30 <= n && n <= 37: - attr = (attr & backgroundMask) - if (n-30)&1 != 0 { - attr |= foregroundRed - } - if (n-30)&2 != 0 { - attr |= foregroundGreen - } - if (n-30)&4 != 0 { - attr |= foregroundBlue - } - case 40 <= n && n <= 47: - attr = (attr & foregroundMask) - if (n-40)&1 != 0 { - attr |= backgroundRed - } - if (n-40)&2 != 0 { - attr |= backgroundGreen - } - if (n-40)&4 != 0 { - attr |= backgroundBlue - } - case 90 <= n && n <= 97: - attr = (attr & backgroundMask) - attr |= foregroundIntensity - if (n-90)&1 != 0 { - attr |= foregroundRed - } - if (n-90)&2 != 0 { - attr |= foregroundGreen - } - if (n-90)&4 != 0 { - attr |= foregroundBlue - } - case 100 <= n && n <= 107: - attr = (attr & foregroundMask) - attr |= backgroundIntensity - if (n-100)&1 != 0 { - attr |= backgroundRed - } - if (n-100)&2 != 0 { - attr |= backgroundGreen - } - if (n-100)&4 != 0 { - attr |= backgroundBlue - } - } - } - - procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr)) -} diff --git a/vendor/github.com/kataras/survey/terminal/print.go b/vendor/github.com/kataras/survey/terminal/print.go deleted file mode 100644 index d7b6f612fc..0000000000 --- a/vendor/github.com/kataras/survey/terminal/print.go +++ /dev/null @@ -1,25 +0,0 @@ -package terminal - -import ( - "fmt" -) - -var ( - Stdout = NewAnsiStdout() -) - -// Print prints given arguments with escape sequence conversion for windows. -func Print(a ...interface{}) (n int, err error) { - return fmt.Fprint(Stdout, a...) -} - -// Printf prints a given format with escape sequence conversion for windows. -func Printf(format string, a ...interface{}) (n int, err error) { - return fmt.Fprintf(Stdout, format, a...) -} - -// Println prints given arguments with newline and escape sequence conversion -// for windows. -func Println(a ...interface{}) (n int, err error) { - return fmt.Fprintln(Stdout, a...) -} diff --git a/vendor/github.com/kataras/survey/terminal/runereader.go b/vendor/github.com/kataras/survey/terminal/runereader.go deleted file mode 100644 index 96226425d5..0000000000 --- a/vendor/github.com/kataras/survey/terminal/runereader.go +++ /dev/null @@ -1,183 +0,0 @@ -package terminal - -import ( - "os" - "unicode" -) - -type RuneReader struct { - Input *os.File - - state runeReaderState -} - -func NewRuneReader(input *os.File) *RuneReader { - return &RuneReader{ - Input: input, - state: newRuneReaderState(input), - } -} - -func (rr *RuneReader) ReadLine(mask rune) ([]rune, error) { - line := []rune{} - - // we only care about horizontal displacements from the origin so start counting at 0 - index := 0 - - for { - // wait for some input - r, _, err := rr.ReadRune() - if err != nil { - return line, err - } - - // if the user pressed enter or some other newline/termination like ctrl+d - if r == '\r' || r == '\n' || r == KeyEndTransmission { - // go to the beginning of the next line - Print("\r\n") - - // we're done processing the input - return line, nil - } - - // if the user interrupts (ie with ctrl+c) - if r == KeyInterrupt { - // go to the beginning of the next line - Print("\r\n") - - // we're done processing the input, and treat interrupt like an error - return line, InterruptErr - } - - // allow for backspace/delete editing of inputs - if r == KeyBackspace || r == KeyDelete { - // and we're not at the beginning of the line - if index > 0 && len(line) > 0 { - // if we are at the end of the word - if index == len(line) { - // just remove the last letter from the internal representation - line = line[:len(line)-1] - - // go back one - CursorBack(1) - - // clear the rest of the line - EraseLine(ERASE_LINE_END) - } else { - // we need to remove a character from the middle of the word - - // remove the current index from the list - line = append(line[:index-1], line[index:]...) - - // go back one space so we can clear the rest - CursorBack(1) - - // clear the rest of the line - EraseLine(ERASE_LINE_END) - - // print what comes after - Print(string(line[index-1:])) - - // leave the cursor where the user left it - CursorBack(len(line) - index + 1) - } - - // decrement the index - index-- - } else { - // otherwise the user pressed backspace while at the beginning of the line - soundBell() - } - - // we're done processing this key - continue - } - - // if the left arrow is pressed - if r == KeyArrowLeft { - // and we have space to the left - if index > 0 { - // move the cursor to the left - CursorBack(1) - // decrement the index - index-- - - } else { - // otherwise we are at the beginning of where we started reading lines - // sound the bell - soundBell() - } - - // we're done processing this key press - continue - } - - // if the right arrow is pressed - if r == KeyArrowRight { - // and we have space to the right of the word - if index < len(line) { - // move the cursor to the right - CursorForward(1) - // increment the index - index++ - - } else { - // otherwise we are at the end of the word and can't go past - // sound the bell - soundBell() - } - - // we're done processing this key press - continue - } - - // if the letter is another escape sequence - if unicode.IsControl(r) { - // ignore it - continue - } - - // the user pressed a regular key - - // if we are at the end of the line - if index == len(line) { - // just append the character at the end of the line - line = append(line, r) - // increment the location counter - index++ - - // if we don't need to mask the input - if mask == 0 { - // just print the character the user pressed - Printf("%c", r) - } else { - // otherwise print the mask we were given - Printf("%c", mask) - } - } else { - // we are in the middle of the word so we need to insert the character the user pressed - line = append(line[:index], append([]rune{r}, line[index:]...)...) - - // visually insert the character by deleting the rest of the line - EraseLine(ERASE_LINE_END) - - // print the rest of the word after - for _, char := range line[index:] { - // if we don't need to mask the input - if mask == 0 { - // just print the character the user pressed - Printf("%c", char) - } else { - // otherwise print the mask we were given - Printf("%c", mask) - } - } - - // leave the cursor where the user left it - CursorBack(len(line) - index - 1) - - // accommodate the new letter in our counter - index++ - } - } -} diff --git a/vendor/github.com/kataras/survey/terminal/runereader_bsd.go b/vendor/github.com/kataras/survey/terminal/runereader_bsd.go deleted file mode 100644 index 6ea340923a..0000000000 --- a/vendor/github.com/kataras/survey/terminal/runereader_bsd.go +++ /dev/null @@ -1,13 +0,0 @@ -// copied from: https://github.com/golang/crypto/blob/master/ssh/terminal/util_bsd.go -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin dragonfly freebsd netbsd openbsd - -package terminal - -import "syscall" - -const ioctlReadTermios = syscall.TIOCGETA -const ioctlWriteTermios = syscall.TIOCSETA diff --git a/vendor/github.com/kataras/survey/terminal/runereader_linux.go b/vendor/github.com/kataras/survey/terminal/runereader_linux.go deleted file mode 100644 index 74e1b697ff..0000000000 --- a/vendor/github.com/kataras/survey/terminal/runereader_linux.go +++ /dev/null @@ -1,12 +0,0 @@ -// copied from https://github.com/golang/crypto/blob/master/ssh/terminal/util_linux.go -// Copyright 2013 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package terminal - -// These constants are declared here, rather than importing -// them from the syscall package as some syscall packages, even -// on linux, for example gccgo, do not declare them. -const ioctlReadTermios = 0x5401 // syscall.TCGETS -const ioctlWriteTermios = 0x5402 // syscall.TCSETS diff --git a/vendor/github.com/kataras/survey/terminal/runereader_posix.go b/vendor/github.com/kataras/survey/terminal/runereader_posix.go deleted file mode 100644 index 1f463fb8cc..0000000000 --- a/vendor/github.com/kataras/survey/terminal/runereader_posix.go +++ /dev/null @@ -1,84 +0,0 @@ -// +build !windows - -// The terminal mode manipulation code is derived heavily from: -// https://github.com/golang/crypto/blob/master/ssh/terminal/util.go: -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package terminal - -import ( - "bufio" - "fmt" - "os" - "syscall" - "unsafe" -) - -type runeReaderState struct { - term syscall.Termios - buf *bufio.Reader -} - -func newRuneReaderState(input *os.File) runeReaderState { - return runeReaderState{ - buf: bufio.NewReader(input), - } -} - -// For reading runes we just want to disable echo. -func (rr *RuneReader) SetTermMode() error { - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlReadTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 { - return err - } - - newState := rr.state.term - newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG - - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&newState)), 0, 0, 0); err != 0 { - return err - } - - return nil -} - -func (rr *RuneReader) RestoreTermMode() error { - if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(rr.Input.Fd()), ioctlWriteTermios, uintptr(unsafe.Pointer(&rr.state.term)), 0, 0, 0); err != 0 { - return err - } - return nil -} - -func (rr *RuneReader) ReadRune() (rune, int, error) { - r, size, err := rr.state.buf.ReadRune() - if err != nil { - return r, size, err - } - // parse ^[ sequences to look for arrow keys - if r == '\033' { - r, size, err = rr.state.buf.ReadRune() - if err != nil { - return r, size, err - } - if r != '[' { - return r, size, fmt.Errorf("Unexpected Escape Sequence: %q", []rune{'\033', r}) - } - r, size, err = rr.state.buf.ReadRune() - if err != nil { - return r, size, err - } - switch r { - case 'D': - return KeyArrowLeft, 1, nil - case 'C': - return KeyArrowRight, 1, nil - case 'A': - return KeyArrowUp, 1, nil - case 'B': - return KeyArrowDown, 1, nil - } - return r, size, fmt.Errorf("Unknown Escape Sequence: %q", []rune{'\033', '[', r}) - } - return r, size, err -} diff --git a/vendor/github.com/kataras/survey/terminal/runereader_windows.go b/vendor/github.com/kataras/survey/terminal/runereader_windows.go deleted file mode 100644 index a0cea826ac..0000000000 --- a/vendor/github.com/kataras/survey/terminal/runereader_windows.go +++ /dev/null @@ -1,130 +0,0 @@ -package terminal - -import ( - "os" - "syscall" - "unsafe" -) - -var ( - dll = syscall.NewLazyDLL("kernel32.dll") - setConsoleMode = dll.NewProc("SetConsoleMode") - getConsoleMode = dll.NewProc("GetConsoleMode") - readConsoleInput = dll.NewProc("ReadConsoleInputW") -) - -const ( - EVENT_KEY = 0x0001 - - // key codes for arrow keys - // https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx - VK_LEFT = 0x25 - VK_UP = 0x26 - VK_RIGHT = 0x27 - VK_DOWN = 0x28 - - RIGHT_CTRL_PRESSED = 0x0004 - LEFT_CTRL_PRESSED = 0x0008 - - ENABLE_ECHO_INPUT uint32 = 0x0004 - ENABLE_LINE_INPUT uint32 = 0x0002 - ENABLE_PROCESSED_INPUT uint32 = 0x0001 -) - -type inputRecord struct { - eventType uint16 - padding uint16 - event [16]byte -} - -type keyEventRecord struct { - bKeyDown int32 - wRepeatCount uint16 - wVirtualKeyCode uint16 - wVirtualScanCode uint16 - unicodeChar uint16 - wdControlKeyState uint32 -} - -type runeReaderState struct { - term uint32 -} - -func newRuneReaderState(input *os.File) runeReaderState { - return runeReaderState{} -} - -func (rr *RuneReader) SetTermMode() error { - r, _, err := getConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(unsafe.Pointer(&rr.state.term))) - // windows return 0 on error - if r == 0 { - return err - } - - newState := rr.state.term - newState &^= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT - r, _, err = setConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(newState)) - // windows return 0 on error - if r == 0 { - return err - } - return nil -} - -func (rr *RuneReader) RestoreTermMode() error { - r, _, err := setConsoleMode.Call(uintptr(rr.Input.Fd()), uintptr(rr.state.term)) - // windows return 0 on error - if r == 0 { - return err - } - return nil -} - -func (rr *RuneReader) ReadRune() (rune, int, error) { - ir := &inputRecord{} - bytesRead := 0 - for { - rv, _, e := readConsoleInput.Call(rr.Input.Fd(), uintptr(unsafe.Pointer(ir)), 1, uintptr(unsafe.Pointer(&bytesRead))) - // windows returns non-zero to indicate success - if rv == 0 && e != nil { - return 0, 0, e - } - - if ir.eventType != EVENT_KEY { - continue - } - - // the event data is really a c struct union, so here we have to do an usafe - // cast to put the data into the keyEventRecord (since we have already verified - // above that this event does correspond to a key event - key := (*keyEventRecord)(unsafe.Pointer(&ir.event[0])) - // we only care about key down events - if key.bKeyDown == 0 { - continue - } - if key.wdControlKeyState&(LEFT_CTRL_PRESSED|RIGHT_CTRL_PRESSED) != 0 && key.unicodeChar == 'C' { - return KeyInterrupt, bytesRead, nil - } - - // not a normal character so look up the input sequence from the - // virtual key code mappings (VK_*) - if key.unicodeChar == 0 { - switch key.wVirtualKeyCode { - case VK_DOWN: - return KeyArrowDown, bytesRead, nil - case VK_LEFT: - return KeyArrowLeft, bytesRead, nil - case VK_RIGHT: - return KeyArrowRight, bytesRead, nil - case VK_UP: - return KeyArrowUp, bytesRead, nil - default: - // not a virtual key that we care about so just continue on to - // the next input key - continue - } - } - r := rune(key.unicodeChar) - return r, bytesRead, nil - } -} diff --git a/vendor/github.com/kataras/survey/terminal/sequences.go b/vendor/github.com/kataras/survey/terminal/sequences.go deleted file mode 100644 index 0fc1396944..0000000000 --- a/vendor/github.com/kataras/survey/terminal/sequences.go +++ /dev/null @@ -1,18 +0,0 @@ -package terminal - -const ( - KeyArrowLeft = '\x02' - KeyArrowRight = '\x06' - KeyArrowUp = '\x10' - KeyArrowDown = '\x0e' - KeySpace = ' ' - KeyEnter = '\r' - KeyBackspace = '\b' - KeyDelete = '\x7f' - KeyInterrupt = '\x03' - KeyEndTransmission = '\x04' -) - -func soundBell() { - Print("\a") -} diff --git a/vendor/github.com/kataras/survey/terminal/syscall_windows.go b/vendor/github.com/kataras/survey/terminal/syscall_windows.go deleted file mode 100644 index 63b85d4c39..0000000000 --- a/vendor/github.com/kataras/survey/terminal/syscall_windows.go +++ /dev/null @@ -1,39 +0,0 @@ -package terminal - -import ( - "syscall" -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") - procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") - procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") - procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") - procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") - procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") -) - -type wchar uint16 -type dword uint32 -type word uint16 - -type smallRect struct { - left Short - top Short - right Short - bottom Short -} - -type consoleScreenBufferInfo struct { - size Coord - cursorPosition Coord - attributes word - window smallRect - maximumWindowSize Coord -} - -type consoleCursorInfo struct { - size dword - visible int32 -} diff --git a/vendor/github.com/kataras/survey/terminal/terminal.go b/vendor/github.com/kataras/survey/terminal/terminal.go deleted file mode 100644 index 4b59062c7d..0000000000 --- a/vendor/github.com/kataras/survey/terminal/terminal.go +++ /dev/null @@ -1,8 +0,0 @@ -package terminal - -type Short int16 - -type Coord struct { - X Short - Y Short -} diff --git a/vendor/github.com/kataras/survey/transform.go b/vendor/github.com/kataras/survey/transform.go deleted file mode 100644 index ccc75e0843..0000000000 --- a/vendor/github.com/kataras/survey/transform.go +++ /dev/null @@ -1,76 +0,0 @@ -package survey - -import ( - "reflect" - "strings" -) - -// TransformString returns a `Transformer` based on the "f" -// function which accepts a string representation of the answer -// and returns a new one, transformed, answer. -// Take for example the functions inside the std `strings` package, -// they can be converted to a compatible `Transformer` by using this function, -// i.e: `TransformString(strings.Title)`, `TransformString(strings.ToUpper)`. -// -// Note that `TransformString` is just a helper, `Transformer` can be used -// to transform any type of answer. -func TransformString(f func(s string) string) Transformer { - return func(ans interface{}) interface{} { - // if the answer value passed in is the zero value of the appropriate type - if isZero(reflect.ValueOf(ans)) { - // skip this `Transformer` by returning a nil value. - // The original answer will be not affected, - // see survey.go#L125. - return nil - } - - // "ans" is never nil here, so we don't have to check that - // see survey.go#L97 for more. - // Make sure that the the answer's value was a typeof string. - s, ok := ans.(string) - if !ok { - return nil - } - - return f(s) - } -} - -// ToLower is a `Transformer`. -// It receives an answer value -// and returns a copy of the "ans" -// with all Unicode letters mapped to their lower case. -// -// Note that if "ans" is not a string then it will -// return a nil value, meaning that the above answer -// will not be affected by this call at all. -func ToLower(ans interface{}) interface{} { - transformer := TransformString(strings.ToLower) - return transformer(ans) -} - -// Title is a `Transformer`. -// It receives an answer value -// and returns a copy of the "ans" -// with all Unicode letters that begin words -// mapped to their title case. -// -// Note that if "ans" is not a string then it will -// return a nil value, meaning that the above answer -// will not be affected by this call at all. -func Title(ans interface{}) interface{} { - transformer := TransformString(strings.Title) - return transformer(ans) -} - -// ComposeTransformers is a variadic function used to create one transformer from many. -func ComposeTransformers(transformers ...Transformer) Transformer { - // return a transformer that calls each one sequentially - return func(ans interface{}) interface{} { - // execute each transformer - for _, t := range transformers { - ans = t(ans) - } - return ans - } -} diff --git a/vendor/github.com/kataras/survey/validate.go b/vendor/github.com/kataras/survey/validate.go deleted file mode 100644 index 50162c2582..0000000000 --- a/vendor/github.com/kataras/survey/validate.go +++ /dev/null @@ -1,92 +0,0 @@ -package survey - -import ( - "errors" - "fmt" - "reflect" -) - -// Required does not allow an empty value -func Required(val interface{}) error { - // if the value passed in is the zero value of the appropriate type - if isZero(reflect.ValueOf(val)) { - return errors.New("Value is required") - } - return nil -} - -// MaxLength requires that the string is no longer than the specified value -func MaxLength(length int) Validator { - // return a validator that checks the length of the string - return func(val interface{}) error { - if str, ok := val.(string); ok { - // if the string is longer than the given value - if len(str) > length { - // yell loudly - return fmt.Errorf("value is too long. Max length is %v", length) - } - } else { - // otherwise we cannot convert the value into a string and cannot enforce length - return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) - } - - // the input is fine - return nil - } -} - -// MinLength requires that the string is longer or equal in length to the specified value -func MinLength(length int) Validator { - // return a validator that checks the length of the string - return func(val interface{}) error { - if str, ok := val.(string); ok { - // if the string is shorter than the given value - if len(str) < length { - // yell loudly - return fmt.Errorf("value is too short. Min length is %v", length) - } - } else { - // otherwise we cannot convert the value into a string and cannot enforce length - return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) - } - - // the input is fine - return nil - } -} - -// ComposeValidators is a variadic function used to create one validator from many. -func ComposeValidators(validators ...Validator) Validator { - // return a validator that calls each one sequentially - return func(val interface{}) error { - // execute each validator - for _, validator := range validators { - // if the answer's value is not valid - if err := validator(val); err != nil { - // return the error - return err - } - } - // we passed all validators, the answer is valid - return nil - } -} - -// isZero returns true if the passed value is the zero object -func isZero(v reflect.Value) bool { - switch v.Kind() { - case reflect.Slice, reflect.Map: - return v.Len() == 0 - // fixes: - // if confirm and `Validate: survey.Required` is used - // and answer is "No" (== false) - // then it shows "Sorry, your reply was invalid: Value is required" - // and it stucks there. - // This happens because 'false' is the zero value of a "bool" type. - case reflect.Bool: - return false - } - - // compare the types directly with more general coverage - return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/LICENSE b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/LICENSE deleted file mode 100644 index 91b5cef30e..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Yasuhiro Matsumoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_appengine.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_appengine.go deleted file mode 100644 index 1f28d773d7..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_appengine.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build appengine - -package colorable - -import ( - "io" - "os" - - _ "github.com/mattn/go-isatty" -) - -// NewColorable return new instance of Writer which handle escape sequence. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return os.Stdout -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return os.Stderr -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_others.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_others.go deleted file mode 100644 index 887f203dc7..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_others.go +++ /dev/null @@ -1,30 +0,0 @@ -// +build !windows -// +build !appengine - -package colorable - -import ( - "io" - "os" - - _ "github.com/mattn/go-isatty" -) - -// NewColorable return new instance of Writer which handle escape sequence. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return os.Stdout -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return os.Stderr -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_windows.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_windows.go deleted file mode 100644 index 15a014fd30..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/colorable_windows.go +++ /dev/null @@ -1,968 +0,0 @@ -// +build windows -// +build !appengine - -package colorable - -import ( - "bytes" - "io" - "math" - "os" - "strconv" - "strings" - "syscall" - "unsafe" - - "github.com/mattn/go-isatty" -) - -const ( - foregroundBlue = 0x1 - foregroundGreen = 0x2 - foregroundRed = 0x4 - foregroundIntensity = 0x8 - foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity) - backgroundBlue = 0x10 - backgroundGreen = 0x20 - backgroundRed = 0x40 - backgroundIntensity = 0x80 - backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) -) - -const ( - genericRead = 0x80000000 - genericWrite = 0x40000000 -) - -const ( - consoleTextmodeBuffer = 0x1 -) - -type wchar uint16 -type short int16 -type dword uint32 -type word uint16 - -type coord struct { - x short - y short -} - -type smallRect struct { - left short - top short - right short - bottom short -} - -type consoleScreenBufferInfo struct { - size coord - cursorPosition coord - attributes word - window smallRect - maximumWindowSize coord -} - -type consoleCursorInfo struct { - size dword - visible int32 -} - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") - procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute") - procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") - procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") - procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") - procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") - procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") - procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") - procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer") -) - -// Writer provide colorable Writer to the console -type Writer struct { - out io.Writer - handle syscall.Handle - althandle syscall.Handle - oldattr word - oldpos coord - rest bytes.Buffer -} - -// NewColorable return new instance of Writer which handle escape sequence from File. -func NewColorable(file *os.File) io.Writer { - if file == nil { - panic("nil passed instead of *os.File to NewColorable()") - } - - if isatty.IsTerminal(file.Fd()) { - var csbi consoleScreenBufferInfo - handle := syscall.Handle(file.Fd()) - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}} - } - return file -} - -// NewColorableStdout return new instance of Writer which handle escape sequence for stdout. -func NewColorableStdout() io.Writer { - return NewColorable(os.Stdout) -} - -// NewColorableStderr return new instance of Writer which handle escape sequence for stderr. -func NewColorableStderr() io.Writer { - return NewColorable(os.Stderr) -} - -var color256 = map[int]int{ - 0: 0x000000, - 1: 0x800000, - 2: 0x008000, - 3: 0x808000, - 4: 0x000080, - 5: 0x800080, - 6: 0x008080, - 7: 0xc0c0c0, - 8: 0x808080, - 9: 0xff0000, - 10: 0x00ff00, - 11: 0xffff00, - 12: 0x0000ff, - 13: 0xff00ff, - 14: 0x00ffff, - 15: 0xffffff, - 16: 0x000000, - 17: 0x00005f, - 18: 0x000087, - 19: 0x0000af, - 20: 0x0000d7, - 21: 0x0000ff, - 22: 0x005f00, - 23: 0x005f5f, - 24: 0x005f87, - 25: 0x005faf, - 26: 0x005fd7, - 27: 0x005fff, - 28: 0x008700, - 29: 0x00875f, - 30: 0x008787, - 31: 0x0087af, - 32: 0x0087d7, - 33: 0x0087ff, - 34: 0x00af00, - 35: 0x00af5f, - 36: 0x00af87, - 37: 0x00afaf, - 38: 0x00afd7, - 39: 0x00afff, - 40: 0x00d700, - 41: 0x00d75f, - 42: 0x00d787, - 43: 0x00d7af, - 44: 0x00d7d7, - 45: 0x00d7ff, - 46: 0x00ff00, - 47: 0x00ff5f, - 48: 0x00ff87, - 49: 0x00ffaf, - 50: 0x00ffd7, - 51: 0x00ffff, - 52: 0x5f0000, - 53: 0x5f005f, - 54: 0x5f0087, - 55: 0x5f00af, - 56: 0x5f00d7, - 57: 0x5f00ff, - 58: 0x5f5f00, - 59: 0x5f5f5f, - 60: 0x5f5f87, - 61: 0x5f5faf, - 62: 0x5f5fd7, - 63: 0x5f5fff, - 64: 0x5f8700, - 65: 0x5f875f, - 66: 0x5f8787, - 67: 0x5f87af, - 68: 0x5f87d7, - 69: 0x5f87ff, - 70: 0x5faf00, - 71: 0x5faf5f, - 72: 0x5faf87, - 73: 0x5fafaf, - 74: 0x5fafd7, - 75: 0x5fafff, - 76: 0x5fd700, - 77: 0x5fd75f, - 78: 0x5fd787, - 79: 0x5fd7af, - 80: 0x5fd7d7, - 81: 0x5fd7ff, - 82: 0x5fff00, - 83: 0x5fff5f, - 84: 0x5fff87, - 85: 0x5fffaf, - 86: 0x5fffd7, - 87: 0x5fffff, - 88: 0x870000, - 89: 0x87005f, - 90: 0x870087, - 91: 0x8700af, - 92: 0x8700d7, - 93: 0x8700ff, - 94: 0x875f00, - 95: 0x875f5f, - 96: 0x875f87, - 97: 0x875faf, - 98: 0x875fd7, - 99: 0x875fff, - 100: 0x878700, - 101: 0x87875f, - 102: 0x878787, - 103: 0x8787af, - 104: 0x8787d7, - 105: 0x8787ff, - 106: 0x87af00, - 107: 0x87af5f, - 108: 0x87af87, - 109: 0x87afaf, - 110: 0x87afd7, - 111: 0x87afff, - 112: 0x87d700, - 113: 0x87d75f, - 114: 0x87d787, - 115: 0x87d7af, - 116: 0x87d7d7, - 117: 0x87d7ff, - 118: 0x87ff00, - 119: 0x87ff5f, - 120: 0x87ff87, - 121: 0x87ffaf, - 122: 0x87ffd7, - 123: 0x87ffff, - 124: 0xaf0000, - 125: 0xaf005f, - 126: 0xaf0087, - 127: 0xaf00af, - 128: 0xaf00d7, - 129: 0xaf00ff, - 130: 0xaf5f00, - 131: 0xaf5f5f, - 132: 0xaf5f87, - 133: 0xaf5faf, - 134: 0xaf5fd7, - 135: 0xaf5fff, - 136: 0xaf8700, - 137: 0xaf875f, - 138: 0xaf8787, - 139: 0xaf87af, - 140: 0xaf87d7, - 141: 0xaf87ff, - 142: 0xafaf00, - 143: 0xafaf5f, - 144: 0xafaf87, - 145: 0xafafaf, - 146: 0xafafd7, - 147: 0xafafff, - 148: 0xafd700, - 149: 0xafd75f, - 150: 0xafd787, - 151: 0xafd7af, - 152: 0xafd7d7, - 153: 0xafd7ff, - 154: 0xafff00, - 155: 0xafff5f, - 156: 0xafff87, - 157: 0xafffaf, - 158: 0xafffd7, - 159: 0xafffff, - 160: 0xd70000, - 161: 0xd7005f, - 162: 0xd70087, - 163: 0xd700af, - 164: 0xd700d7, - 165: 0xd700ff, - 166: 0xd75f00, - 167: 0xd75f5f, - 168: 0xd75f87, - 169: 0xd75faf, - 170: 0xd75fd7, - 171: 0xd75fff, - 172: 0xd78700, - 173: 0xd7875f, - 174: 0xd78787, - 175: 0xd787af, - 176: 0xd787d7, - 177: 0xd787ff, - 178: 0xd7af00, - 179: 0xd7af5f, - 180: 0xd7af87, - 181: 0xd7afaf, - 182: 0xd7afd7, - 183: 0xd7afff, - 184: 0xd7d700, - 185: 0xd7d75f, - 186: 0xd7d787, - 187: 0xd7d7af, - 188: 0xd7d7d7, - 189: 0xd7d7ff, - 190: 0xd7ff00, - 191: 0xd7ff5f, - 192: 0xd7ff87, - 193: 0xd7ffaf, - 194: 0xd7ffd7, - 195: 0xd7ffff, - 196: 0xff0000, - 197: 0xff005f, - 198: 0xff0087, - 199: 0xff00af, - 200: 0xff00d7, - 201: 0xff00ff, - 202: 0xff5f00, - 203: 0xff5f5f, - 204: 0xff5f87, - 205: 0xff5faf, - 206: 0xff5fd7, - 207: 0xff5fff, - 208: 0xff8700, - 209: 0xff875f, - 210: 0xff8787, - 211: 0xff87af, - 212: 0xff87d7, - 213: 0xff87ff, - 214: 0xffaf00, - 215: 0xffaf5f, - 216: 0xffaf87, - 217: 0xffafaf, - 218: 0xffafd7, - 219: 0xffafff, - 220: 0xffd700, - 221: 0xffd75f, - 222: 0xffd787, - 223: 0xffd7af, - 224: 0xffd7d7, - 225: 0xffd7ff, - 226: 0xffff00, - 227: 0xffff5f, - 228: 0xffff87, - 229: 0xffffaf, - 230: 0xffffd7, - 231: 0xffffff, - 232: 0x080808, - 233: 0x121212, - 234: 0x1c1c1c, - 235: 0x262626, - 236: 0x303030, - 237: 0x3a3a3a, - 238: 0x444444, - 239: 0x4e4e4e, - 240: 0x585858, - 241: 0x626262, - 242: 0x6c6c6c, - 243: 0x767676, - 244: 0x808080, - 245: 0x8a8a8a, - 246: 0x949494, - 247: 0x9e9e9e, - 248: 0xa8a8a8, - 249: 0xb2b2b2, - 250: 0xbcbcbc, - 251: 0xc6c6c6, - 252: 0xd0d0d0, - 253: 0xdadada, - 254: 0xe4e4e4, - 255: 0xeeeeee, -} - -// `\033]0;TITLESTR\007` -func doTitleSequence(er *bytes.Reader) error { - var c byte - var err error - - c, err = er.ReadByte() - if err != nil { - return err - } - if c != '0' && c != '2' { - return nil - } - c, err = er.ReadByte() - if err != nil { - return err - } - if c != ';' { - return nil - } - title := make([]byte, 0, 80) - for { - c, err = er.ReadByte() - if err != nil { - return err - } - if c == 0x07 || c == '\n' { - break - } - title = append(title, c) - } - if len(title) > 0 { - title8, err := syscall.UTF16PtrFromString(string(title)) - if err == nil { - procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8))) - } - } - return nil -} - -// Write write data on console -func (w *Writer) Write(data []byte) (n int, err error) { - var csbi consoleScreenBufferInfo - procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) - - handle := w.handle - - var er *bytes.Reader - if w.rest.Len() > 0 { - var rest bytes.Buffer - w.rest.WriteTo(&rest) - w.rest.Reset() - rest.Write(data) - er = bytes.NewReader(rest.Bytes()) - } else { - er = bytes.NewReader(data) - } - var bw [1]byte -loop: - for { - c1, err := er.ReadByte() - if err != nil { - break loop - } - if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) - continue - } - c2, err := er.ReadByte() - if err != nil { - break loop - } - - if c2 == ']' { - w.rest.WriteByte(c1) - w.rest.WriteByte(c2) - er.WriteTo(&w.rest) - if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 { - break loop - } - er = bytes.NewReader(w.rest.Bytes()[2:]) - err := doTitleSequence(er) - if err != nil { - break loop - } - w.rest.Reset() - continue - } - if c2 != 0x5b { - continue - } - - w.rest.WriteByte(c1) - w.rest.WriteByte(c2) - er.WriteTo(&w.rest) - - var buf bytes.Buffer - var m byte - for i, c := range w.rest.Bytes()[2:] { - if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { - m = c - er = bytes.NewReader(w.rest.Bytes()[2+i+1:]) - w.rest.Reset() - break - } - buf.Write([]byte(string(c))) - } - if m == 0 { - break loop - } - - switch m { - case 'A': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.y -= short(n) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'B': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.y += short(n) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'C': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x += short(n) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'D': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x -= short(n) - if csbi.cursorPosition.x < 0 { - csbi.cursorPosition.x = 0 - } - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'E': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = 0 - csbi.cursorPosition.y += short(n) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'F': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = 0 - csbi.cursorPosition.y -= short(n) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'G': - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - csbi.cursorPosition.x = short(n - 1) - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'H', 'f': - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - if buf.Len() > 0 { - token := strings.Split(buf.String(), ";") - switch len(token) { - case 1: - n1, err := strconv.Atoi(token[0]) - if err != nil { - continue - } - csbi.cursorPosition.y = short(n1 - 1) - case 2: - n1, err := strconv.Atoi(token[0]) - if err != nil { - continue - } - n2, err := strconv.Atoi(token[1]) - if err != nil { - continue - } - csbi.cursorPosition.x = short(n2 - 1) - csbi.cursorPosition.y = short(n1 - 1) - } - } else { - csbi.cursorPosition.y = 0 - } - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) - case 'J': - n := 0 - if buf.Len() > 0 { - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - } - var count, written dword - var cursor coord - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - switch n { - case 0: - cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) - case 1: - cursor = coord{x: csbi.window.left, y: csbi.window.top} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.window.top-csbi.cursorPosition.y)*csbi.size.x) - case 2: - cursor = coord{x: csbi.window.left, y: csbi.window.top} - count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) - } - procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - case 'K': - n := 0 - if buf.Len() > 0 { - n, err = strconv.Atoi(buf.String()) - if err != nil { - continue - } - } - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - var cursor coord - var count, written dword - switch n { - case 0: - cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x) - case 1: - cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} - count = dword(csbi.size.x - csbi.cursorPosition.x) - case 2: - cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} - count = dword(csbi.size.x) - } - procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) - case 'm': - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - attr := csbi.attributes - cs := buf.String() - if cs == "" { - procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr)) - continue - } - token := strings.Split(cs, ";") - for i := 0; i < len(token); i++ { - ns := token[i] - if n, err = strconv.Atoi(ns); err == nil { - switch { - case n == 0 || n == 100: - attr = w.oldattr - case 1 <= n && n <= 5: - attr |= foregroundIntensity - case n == 7: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) - case n == 22 || n == 25: - attr |= foregroundIntensity - case n == 27: - attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4) - case 30 <= n && n <= 37: - attr &= backgroundMask - if (n-30)&1 != 0 { - attr |= foregroundRed - } - if (n-30)&2 != 0 { - attr |= foregroundGreen - } - if (n-30)&4 != 0 { - attr |= foregroundBlue - } - case n == 38: // set foreground color. - if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") { - if n256, err := strconv.Atoi(token[i+2]); err == nil { - if n256foreAttr == nil { - n256setup() - } - attr &= backgroundMask - attr |= n256foreAttr[n256] - i += 2 - } - } else if len(token) == 5 && token[i+1] == "2" { - var r, g, b int - r, _ = strconv.Atoi(token[i+2]) - g, _ = strconv.Atoi(token[i+3]) - b, _ = strconv.Atoi(token[i+4]) - i += 4 - if r > 127 { - attr |= foregroundRed - } - if g > 127 { - attr |= foregroundGreen - } - if b > 127 { - attr |= foregroundBlue - } - } else { - attr = attr & (w.oldattr & backgroundMask) - } - case n == 39: // reset foreground color. - attr &= backgroundMask - attr |= w.oldattr & foregroundMask - case 40 <= n && n <= 47: - attr &= foregroundMask - if (n-40)&1 != 0 { - attr |= backgroundRed - } - if (n-40)&2 != 0 { - attr |= backgroundGreen - } - if (n-40)&4 != 0 { - attr |= backgroundBlue - } - case n == 48: // set background color. - if i < len(token)-2 && token[i+1] == "5" { - if n256, err := strconv.Atoi(token[i+2]); err == nil { - if n256backAttr == nil { - n256setup() - } - attr &= foregroundMask - attr |= n256backAttr[n256] - i += 2 - } - } else if len(token) == 5 && token[i+1] == "2" { - var r, g, b int - r, _ = strconv.Atoi(token[i+2]) - g, _ = strconv.Atoi(token[i+3]) - b, _ = strconv.Atoi(token[i+4]) - i += 4 - if r > 127 { - attr |= backgroundRed - } - if g > 127 { - attr |= backgroundGreen - } - if b > 127 { - attr |= backgroundBlue - } - } else { - attr = attr & (w.oldattr & foregroundMask) - } - case n == 49: // reset foreground color. - attr &= foregroundMask - attr |= w.oldattr & backgroundMask - case 90 <= n && n <= 97: - attr = (attr & backgroundMask) - attr |= foregroundIntensity - if (n-90)&1 != 0 { - attr |= foregroundRed - } - if (n-90)&2 != 0 { - attr |= foregroundGreen - } - if (n-90)&4 != 0 { - attr |= foregroundBlue - } - case 100 <= n && n <= 107: - attr = (attr & foregroundMask) - attr |= backgroundIntensity - if (n-100)&1 != 0 { - attr |= backgroundRed - } - if (n-100)&2 != 0 { - attr |= backgroundGreen - } - if (n-100)&4 != 0 { - attr |= backgroundBlue - } - } - procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr)) - } - } - case 'h': - var ci consoleCursorInfo - cs := buf.String() - if cs == "5>" { - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 0 - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?25" { - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 1 - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?1049" { - if w.althandle == 0 { - h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0) - w.althandle = syscall.Handle(h) - if w.althandle != 0 { - handle = w.althandle - } - } - } - case 'l': - var ci consoleCursorInfo - cs := buf.String() - if cs == "5>" { - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 1 - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?25" { - procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - ci.visible = 0 - procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) - } else if cs == "?1049" { - if w.althandle != 0 { - syscall.CloseHandle(w.althandle) - w.althandle = 0 - handle = w.handle - } - } - case 's': - procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) - w.oldpos = csbi.cursorPosition - case 'u': - procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) - } - } - - return len(data), nil -} - -type consoleColor struct { - rgb int - red bool - green bool - blue bool - intensity bool -} - -func (c consoleColor) foregroundAttr() (attr word) { - if c.red { - attr |= foregroundRed - } - if c.green { - attr |= foregroundGreen - } - if c.blue { - attr |= foregroundBlue - } - if c.intensity { - attr |= foregroundIntensity - } - return -} - -func (c consoleColor) backgroundAttr() (attr word) { - if c.red { - attr |= backgroundRed - } - if c.green { - attr |= backgroundGreen - } - if c.blue { - attr |= backgroundBlue - } - if c.intensity { - attr |= backgroundIntensity - } - return -} - -var color16 = []consoleColor{ - {0x000000, false, false, false, false}, - {0x000080, false, false, true, false}, - {0x008000, false, true, false, false}, - {0x008080, false, true, true, false}, - {0x800000, true, false, false, false}, - {0x800080, true, false, true, false}, - {0x808000, true, true, false, false}, - {0xc0c0c0, true, true, true, false}, - {0x808080, false, false, false, true}, - {0x0000ff, false, false, true, true}, - {0x00ff00, false, true, false, true}, - {0x00ffff, false, true, true, true}, - {0xff0000, true, false, false, true}, - {0xff00ff, true, false, true, true}, - {0xffff00, true, true, false, true}, - {0xffffff, true, true, true, true}, -} - -type hsv struct { - h, s, v float32 -} - -func (a hsv) dist(b hsv) float32 { - dh := a.h - b.h - switch { - case dh > 0.5: - dh = 1 - dh - case dh < -0.5: - dh = -1 - dh - } - ds := a.s - b.s - dv := a.v - b.v - return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv))) -} - -func toHSV(rgb int) hsv { - r, g, b := float32((rgb&0xFF0000)>>16)/256.0, - float32((rgb&0x00FF00)>>8)/256.0, - float32(rgb&0x0000FF)/256.0 - min, max := minmax3f(r, g, b) - h := max - min - if h > 0 { - if max == r { - h = (g - b) / h - if h < 0 { - h += 6 - } - } else if max == g { - h = 2 + (b-r)/h - } else { - h = 4 + (r-g)/h - } - } - h /= 6.0 - s := max - min - if max != 0 { - s /= max - } - v := max - return hsv{h: h, s: s, v: v} -} - -type hsvTable []hsv - -func toHSVTable(rgbTable []consoleColor) hsvTable { - t := make(hsvTable, len(rgbTable)) - for i, c := range rgbTable { - t[i] = toHSV(c.rgb) - } - return t -} - -func (t hsvTable) find(rgb int) consoleColor { - hsv := toHSV(rgb) - n := 7 - l := float32(5.0) - for i, p := range t { - d := hsv.dist(p) - if d < l { - l, n = d, i - } - } - return color16[n] -} - -func minmax3f(a, b, c float32) (min, max float32) { - if a < b { - if b < c { - return a, c - } else if a < c { - return a, b - } else { - return c, b - } - } else { - if a < c { - return b, c - } else if b < c { - return b, a - } else { - return c, a - } - } -} - -var n256foreAttr []word -var n256backAttr []word - -func n256setup() { - n256foreAttr = make([]word, 256) - n256backAttr = make([]word, 256) - t := toHSVTable(color16) - for i, rgb := range color256 { - c := t.find(rgb) - n256foreAttr[i] = c.foregroundAttr() - n256backAttr[i] = c.backgroundAttr() - } -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/noncolorable.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/noncolorable.go deleted file mode 100644 index 9721e16f4b..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-colorable/noncolorable.go +++ /dev/null @@ -1,55 +0,0 @@ -package colorable - -import ( - "bytes" - "io" -) - -// NonColorable hold writer but remove escape sequence. -type NonColorable struct { - out io.Writer -} - -// NewNonColorable return new instance of Writer which remove escape sequence from Writer. -func NewNonColorable(w io.Writer) io.Writer { - return &NonColorable{out: w} -} - -// Write write data on console -func (w *NonColorable) Write(data []byte) (n int, err error) { - er := bytes.NewReader(data) - var bw [1]byte -loop: - for { - c1, err := er.ReadByte() - if err != nil { - break loop - } - if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) - continue - } - c2, err := er.ReadByte() - if err != nil { - break loop - } - if c2 != 0x5b { - continue - } - - var buf bytes.Buffer - for { - c, err := er.ReadByte() - if err != nil { - break loop - } - if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { - break - } - buf.Write([]byte(string(c))) - } - } - - return len(data), nil -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/LICENSE b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/LICENSE deleted file mode 100644 index 65dc692b6b..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -Copyright (c) Yasuhiro MATSUMOTO - -MIT License (Expat) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/doc.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/doc.go deleted file mode 100644 index 17d4f90ebc..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package isatty implements interface to isatty -package isatty diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_appengine.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_appengine.go deleted file mode 100644 index 9584a98842..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_appengine.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build appengine - -package isatty - -// IsTerminal returns true if the file descriptor is terminal which -// is always false on on appengine classic which is a sandboxed PaaS. -func IsTerminal(fd uintptr) bool { - return false -} - -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 -// terminal. This is also always false on this environment. -func IsCygwinTerminal(fd uintptr) bool { - return false -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_bsd.go deleted file mode 100644 index 42f2514d13..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_bsd.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build darwin freebsd openbsd netbsd dragonfly -// +build !appengine - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TIOCGETA - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux.go deleted file mode 100644 index 7384cf9916..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux.go +++ /dev/null @@ -1,18 +0,0 @@ -// +build linux -// +build !appengine,!ppc64,!ppc64le - -package isatty - -import ( - "syscall" - "unsafe" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go deleted file mode 100644 index 44e5d21302..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build linux -// +build ppc64 ppc64le - -package isatty - -import ( - "unsafe" - - syscall "golang.org/x/sys/unix" -) - -const ioctlReadTermios = syscall.TCGETS - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var termios syscall.Termios - _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) - return err == 0 -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_others.go deleted file mode 100644 index ff4de3d9a5..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_others.go +++ /dev/null @@ -1,10 +0,0 @@ -// +build !windows -// +build !appengine - -package isatty - -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 -// terminal. This is also always false on this environment. -func IsCygwinTerminal(fd uintptr) bool { - return false -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_solaris.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_solaris.go deleted file mode 100644 index 1f0c6bf53d..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_solaris.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build solaris -// +build !appengine - -package isatty - -import ( - "golang.org/x/sys/unix" -) - -// IsTerminal returns true if the given file descriptor is a terminal. -// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c -func IsTerminal(fd uintptr) bool { - var termio unix.Termio - err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) - return err == nil -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_windows.go b/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_windows.go deleted file mode 100644 index af51cbcaa4..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mattn/go-isatty/isatty_windows.go +++ /dev/null @@ -1,94 +0,0 @@ -// +build windows -// +build !appengine - -package isatty - -import ( - "strings" - "syscall" - "unicode/utf16" - "unsafe" -) - -const ( - fileNameInfo uintptr = 2 - fileTypePipe = 3 -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32.dll") - procGetConsoleMode = kernel32.NewProc("GetConsoleMode") - procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx") - procGetFileType = kernel32.NewProc("GetFileType") -) - -func init() { - // Check if GetFileInformationByHandleEx is available. - if procGetFileInformationByHandleEx.Find() != nil { - procGetFileInformationByHandleEx = nil - } -} - -// IsTerminal return true if the file descriptor is terminal. -func IsTerminal(fd uintptr) bool { - var st uint32 - r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) - return r != 0 && e == 0 -} - -// Check pipe name is used for cygwin/msys2 pty. -// Cygwin/MSYS2 PTY has a name like: -// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master -func isCygwinPipeName(name string) bool { - token := strings.Split(name, "-") - if len(token) < 5 { - return false - } - - if token[0] != `\msys` && token[0] != `\cygwin` { - return false - } - - if token[1] == "" { - return false - } - - if !strings.HasPrefix(token[2], "pty") { - return false - } - - if token[3] != `from` && token[3] != `to` { - return false - } - - if token[4] != "master" { - return false - } - - return true -} - -// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 -// terminal. -func IsCygwinTerminal(fd uintptr) bool { - if procGetFileInformationByHandleEx == nil { - return false - } - - // Cygwin/msys's pty is a pipe. - ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) - if ft != fileTypePipe || e != 0 { - return false - } - - var buf [2 + syscall.MAX_PATH]uint16 - r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), - 4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)), - uintptr(len(buf)*2), 0, 0) - if r == 0 || e != 0 { - return false - } - - l := *(*uint32)(unsafe.Pointer(&buf)) - return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2]))) -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/LICENSE b/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/LICENSE deleted file mode 100644 index 06ce0c3b51..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/LICENSE +++ /dev/null @@ -1,9 +0,0 @@ -The MIT License (MIT) -Copyright (c) 2013 Mario L. Gutierrez - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/ansi.go b/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/ansi.go deleted file mode 100644 index dc0413649e..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/ansi.go +++ /dev/null @@ -1,285 +0,0 @@ -package ansi - -import ( - "bytes" - "fmt" - "strconv" - "strings" -) - -const ( - black = iota - red - green - yellow - blue - magenta - cyan - white - defaultt = 9 - - normalIntensityFG = 30 - highIntensityFG = 90 - normalIntensityBG = 40 - highIntensityBG = 100 - - start = "\033[" - bold = "1;" - blink = "5;" - underline = "4;" - inverse = "7;" - strikethrough = "9;" - - // Reset is the ANSI reset escape sequence - Reset = "\033[0m" - // DefaultBG is the default background - DefaultBG = "\033[49m" - // DefaultFG is the default foreground - DefaultFG = "\033[39m" -) - -// Black FG -var Black string - -// Red FG -var Red string - -// Green FG -var Green string - -// Yellow FG -var Yellow string - -// Blue FG -var Blue string - -// Magenta FG -var Magenta string - -// Cyan FG -var Cyan string - -// White FG -var White string - -// LightBlack FG -var LightBlack string - -// LightRed FG -var LightRed string - -// LightGreen FG -var LightGreen string - -// LightYellow FG -var LightYellow string - -// LightBlue FG -var LightBlue string - -// LightMagenta FG -var LightMagenta string - -// LightCyan FG -var LightCyan string - -// LightWhite FG -var LightWhite string - -var ( - plain = false - // Colors maps common color names to their ANSI color code. - Colors = map[string]int{ - "black": black, - "red": red, - "green": green, - "yellow": yellow, - "blue": blue, - "magenta": magenta, - "cyan": cyan, - "white": white, - "default": defaultt, - } -) - -func init() { - for i := 0; i < 256; i++ { - Colors[strconv.Itoa(i)] = i - } - - Black = ColorCode("black") - Red = ColorCode("red") - Green = ColorCode("green") - Yellow = ColorCode("yellow") - Blue = ColorCode("blue") - Magenta = ColorCode("magenta") - Cyan = ColorCode("cyan") - White = ColorCode("white") - LightBlack = ColorCode("black+h") - LightRed = ColorCode("red+h") - LightGreen = ColorCode("green+h") - LightYellow = ColorCode("yellow+h") - LightBlue = ColorCode("blue+h") - LightMagenta = ColorCode("magenta+h") - LightCyan = ColorCode("cyan+h") - LightWhite = ColorCode("white+h") -} - -// ColorCode returns the ANSI color color code for style. -func ColorCode(style string) string { - return colorCode(style).String() -} - -// Gets the ANSI color code for a style. -func colorCode(style string) *bytes.Buffer { - buf := bytes.NewBufferString("") - if plain || style == "" { - return buf - } - if style == "reset" { - buf.WriteString(Reset) - return buf - } else if style == "off" { - return buf - } - - foregroundBackground := strings.Split(style, ":") - foreground := strings.Split(foregroundBackground[0], "+") - fgKey := foreground[0] - fg := Colors[fgKey] - fgStyle := "" - if len(foreground) > 1 { - fgStyle = foreground[1] - } - - bg, bgStyle := "", "" - - if len(foregroundBackground) > 1 { - background := strings.Split(foregroundBackground[1], "+") - bg = background[0] - if len(background) > 1 { - bgStyle = background[1] - } - } - - buf.WriteString(start) - base := normalIntensityFG - if len(fgStyle) > 0 { - if strings.Contains(fgStyle, "b") { - buf.WriteString(bold) - } - if strings.Contains(fgStyle, "B") { - buf.WriteString(blink) - } - if strings.Contains(fgStyle, "u") { - buf.WriteString(underline) - } - if strings.Contains(fgStyle, "i") { - buf.WriteString(inverse) - } - if strings.Contains(fgStyle, "s") { - buf.WriteString(strikethrough) - } - if strings.Contains(fgStyle, "h") { - base = highIntensityFG - } - } - - // if 256-color - n, err := strconv.Atoi(fgKey) - if err == nil { - fmt.Fprintf(buf, "38;5;%d;", n) - } else { - fmt.Fprintf(buf, "%d;", base+fg) - } - - base = normalIntensityBG - if len(bg) > 0 { - if strings.Contains(bgStyle, "h") { - base = highIntensityBG - } - // if 256-color - n, err := strconv.Atoi(bg) - if err == nil { - fmt.Fprintf(buf, "48;5;%d;", n) - } else { - fmt.Fprintf(buf, "%d;", base+Colors[bg]) - } - } - - // remove last ";" - buf.Truncate(buf.Len() - 1) - buf.WriteRune('m') - return buf -} - -// Color colors a string based on the ANSI color code for style. -func Color(s, style string) string { - if plain || len(style) < 1 { - return s - } - buf := colorCode(style) - buf.WriteString(s) - buf.WriteString(Reset) - return buf.String() -} - -// ColorFunc creates a closure to avoid computation ANSI color code. -func ColorFunc(style string) func(string) string { - if style == "" { - return func(s string) string { - return s - } - } - color := ColorCode(style) - return func(s string) string { - if plain || s == "" { - return s - } - buf := bytes.NewBufferString(color) - buf.WriteString(s) - buf.WriteString(Reset) - result := buf.String() - return result - } -} - -// DisableColors disables ANSI color codes. The default is false (colors are on). -func DisableColors(disable bool) { - plain = disable - if plain { - Black = "" - Red = "" - Green = "" - Yellow = "" - Blue = "" - Magenta = "" - Cyan = "" - White = "" - LightBlack = "" - LightRed = "" - LightGreen = "" - LightYellow = "" - LightBlue = "" - LightMagenta = "" - LightCyan = "" - LightWhite = "" - } else { - Black = ColorCode("black") - Red = ColorCode("red") - Green = ColorCode("green") - Yellow = ColorCode("yellow") - Blue = ColorCode("blue") - Magenta = ColorCode("magenta") - Cyan = ColorCode("cyan") - White = ColorCode("white") - LightBlack = ColorCode("black+h") - LightRed = ColorCode("red+h") - LightGreen = ColorCode("green+h") - LightYellow = ColorCode("yellow+h") - LightBlue = ColorCode("blue+h") - LightMagenta = ColorCode("magenta+h") - LightCyan = ColorCode("cyan+h") - LightWhite = ColorCode("white+h") - } -} diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/doc.go b/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/doc.go deleted file mode 100644 index 43c217e11d..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/doc.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Package ansi is a small, fast library to create ANSI colored strings and codes. - -Installation - - # this installs the color viewer and the package - go get -u github.com/mgutz/ansi/cmd/ansi-mgutz - -Example - - // colorize a string, SLOW - msg := ansi.Color("foo", "red+b:white") - - // create a closure to avoid recalculating ANSI code compilation - phosphorize := ansi.ColorFunc("green+h:black") - msg = phosphorize("Bring back the 80s!") - msg2 := phospohorize("Look, I'm a CRT!") - - // cache escape codes and build strings manually - lime := ansi.ColorCode("green+h:black") - reset := ansi.ColorCode("reset") - - fmt.Println(lime, "Bring back the 80s!", reset) - -Other examples - - Color(s, "red") // red - Color(s, "red+b") // red bold - Color(s, "red+B") // red blinking - Color(s, "red+u") // red underline - Color(s, "red+bh") // red bold bright - Color(s, "red:white") // red on white - Color(s, "red+b:white+h") // red bold on white bright - Color(s, "red+B:white+h") // red blink on white bright - -To view color combinations, from terminal - - ansi-mgutz - -Style format - - "foregroundColor+attributes:backgroundColor+attributes" - -Colors - - black - red - green - yellow - blue - magenta - cyan - white - -Attributes - - b = bold foreground - B = Blink foreground - u = underline foreground - h = high intensity (bright) foreground, background - i = inverse - -Wikipedia ANSI escape codes [Colors](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) -*/ -package ansi diff --git a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/print.go b/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/print.go deleted file mode 100644 index 806f436bb3..0000000000 --- a/vendor/github.com/kataras/survey/vendor/github.com/mgutz/ansi/print.go +++ /dev/null @@ -1,57 +0,0 @@ -package ansi - -import ( - "fmt" - "sort" - - colorable "github.com/mattn/go-colorable" -) - -// PrintStyles prints all style combinations to the terminal. -func PrintStyles() { - // for compatibility with Windows, not needed for *nix - stdout := colorable.NewColorableStdout() - - bgColors := []string{ - "", - ":black", - ":red", - ":green", - ":yellow", - ":blue", - ":magenta", - ":cyan", - ":white", - } - - keys := make([]string, 0, len(Colors)) - for k := range Colors { - keys = append(keys, k) - } - - sort.Sort(sort.StringSlice(keys)) - - for _, fg := range keys { - for _, bg := range bgColors { - fmt.Fprintln(stdout, padColor(fg, []string{"" + bg, "+b" + bg, "+bh" + bg, "+u" + bg})) - fmt.Fprintln(stdout, padColor(fg, []string{"+s" + bg, "+i" + bg})) - fmt.Fprintln(stdout, padColor(fg, []string{"+uh" + bg, "+B" + bg, "+Bb" + bg /* backgrounds */, "" + bg + "+h"})) - fmt.Fprintln(stdout, padColor(fg, []string{"+b" + bg + "+h", "+bh" + bg + "+h", "+u" + bg + "+h", "+uh" + bg + "+h"})) - } - } -} - -func pad(s string, length int) string { - for len(s) < length { - s += " " - } - return s -} - -func padColor(color string, styles []string) string { - buffer := "" - for _, style := range styles { - buffer += Color(pad(color+style, 20), color+style) - } - return buffer -} diff --git a/vendor/github.com/satori/go.uuid/LICENSE b/vendor/github.com/satori/go.uuid/LICENSE deleted file mode 100644 index 926d549870..0000000000 --- a/vendor/github.com/satori/go.uuid/LICENSE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (C) 2013-2018 by Maxim Bublis - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/satori/go.uuid/codec.go b/vendor/github.com/satori/go.uuid/codec.go deleted file mode 100644 index 656892c53e..0000000000 --- a/vendor/github.com/satori/go.uuid/codec.go +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright (C) 2013-2018 by Maxim Bublis -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package uuid - -import ( - "bytes" - "encoding/hex" - "fmt" -) - -// FromBytes returns UUID converted from raw byte slice input. -// It will return error if the slice isn't 16 bytes long. -func FromBytes(input []byte) (u UUID, err error) { - err = u.UnmarshalBinary(input) - return -} - -// FromBytesOrNil returns UUID converted from raw byte slice input. -// Same behavior as FromBytes, but returns a Nil UUID on error. -func FromBytesOrNil(input []byte) UUID { - uuid, err := FromBytes(input) - if err != nil { - return Nil - } - return uuid -} - -// FromString returns UUID parsed from string input. -// Input is expected in a form accepted by UnmarshalText. -func FromString(input string) (u UUID, err error) { - err = u.UnmarshalText([]byte(input)) - return -} - -// FromStringOrNil returns UUID parsed from string input. -// Same behavior as FromString, but returns a Nil UUID on error. -func FromStringOrNil(input string) UUID { - uuid, err := FromString(input) - if err != nil { - return Nil - } - return uuid -} - -// MarshalText implements the encoding.TextMarshaler interface. -// The encoding is the same as returned by String. -func (u UUID) MarshalText() (text []byte, err error) { - text = []byte(u.String()) - return -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface. -// Following formats are supported: -// "6ba7b810-9dad-11d1-80b4-00c04fd430c8", -// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}", -// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" -// "6ba7b8109dad11d180b400c04fd430c8" -// ABNF for supported UUID text representation follows: -// uuid := canonical | hashlike | braced | urn -// plain := canonical | hashlike -// canonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct -// hashlike := 12hexoct -// braced := '{' plain '}' -// urn := URN ':' UUID-NID ':' plain -// URN := 'urn' -// UUID-NID := 'uuid' -// 12hexoct := 6hexoct 6hexoct -// 6hexoct := 4hexoct 2hexoct -// 4hexoct := 2hexoct 2hexoct -// 2hexoct := hexoct hexoct -// hexoct := hexdig hexdig -// hexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | -// 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | -// 'A' | 'B' | 'C' | 'D' | 'E' | 'F' -func (u *UUID) UnmarshalText(text []byte) (err error) { - switch len(text) { - case 32: - return u.decodeHashLike(text) - case 36: - return u.decodeCanonical(text) - case 38: - return u.decodeBraced(text) - case 41: - fallthrough - case 45: - return u.decodeURN(text) - default: - return fmt.Errorf("uuid: incorrect UUID length: %s", text) - } -} - -// decodeCanonical decodes UUID string in format -// "6ba7b810-9dad-11d1-80b4-00c04fd430c8". -func (u *UUID) decodeCanonical(t []byte) (err error) { - if t[8] != '-' || t[13] != '-' || t[18] != '-' || t[23] != '-' { - return fmt.Errorf("uuid: incorrect UUID format %s", t) - } - - src := t[:] - dst := u[:] - - for i, byteGroup := range byteGroups { - if i > 0 { - src = src[1:] // skip dash - } - _, err = hex.Decode(dst[:byteGroup/2], src[:byteGroup]) - if err != nil { - return - } - src = src[byteGroup:] - dst = dst[byteGroup/2:] - } - - return -} - -// decodeHashLike decodes UUID string in format -// "6ba7b8109dad11d180b400c04fd430c8". -func (u *UUID) decodeHashLike(t []byte) (err error) { - src := t[:] - dst := u[:] - - if _, err = hex.Decode(dst, src); err != nil { - return err - } - return -} - -// decodeBraced decodes UUID string in format -// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}" or in format -// "{6ba7b8109dad11d180b400c04fd430c8}". -func (u *UUID) decodeBraced(t []byte) (err error) { - l := len(t) - - if t[0] != '{' || t[l-1] != '}' { - return fmt.Errorf("uuid: incorrect UUID format %s", t) - } - - return u.decodePlain(t[1 : l-1]) -} - -// decodeURN decodes UUID string in format -// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" or in format -// "urn:uuid:6ba7b8109dad11d180b400c04fd430c8". -func (u *UUID) decodeURN(t []byte) (err error) { - total := len(t) - - urn_uuid_prefix := t[:9] - - if !bytes.Equal(urn_uuid_prefix, urnPrefix) { - return fmt.Errorf("uuid: incorrect UUID format: %s", t) - } - - return u.decodePlain(t[9:total]) -} - -// decodePlain decodes UUID string in canonical format -// "6ba7b810-9dad-11d1-80b4-00c04fd430c8" or in hash-like format -// "6ba7b8109dad11d180b400c04fd430c8". -func (u *UUID) decodePlain(t []byte) (err error) { - switch len(t) { - case 32: - return u.decodeHashLike(t) - case 36: - return u.decodeCanonical(t) - default: - return fmt.Errorf("uuid: incorrrect UUID length: %s", t) - } -} - -// MarshalBinary implements the encoding.BinaryMarshaler interface. -func (u UUID) MarshalBinary() (data []byte, err error) { - data = u.Bytes() - return -} - -// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. -// It will return error if the slice isn't 16 bytes long. -func (u *UUID) UnmarshalBinary(data []byte) (err error) { - if len(data) != Size { - err = fmt.Errorf("uuid: UUID must be exactly 16 bytes long, got %d bytes", len(data)) - return - } - copy(u[:], data) - - return -} diff --git a/vendor/github.com/satori/go.uuid/generator.go b/vendor/github.com/satori/go.uuid/generator.go deleted file mode 100644 index 499dc35fb7..0000000000 --- a/vendor/github.com/satori/go.uuid/generator.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (C) 2013-2018 by Maxim Bublis -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package uuid - -import ( - "crypto/md5" - "crypto/rand" - "crypto/sha1" - "encoding/binary" - "fmt" - "hash" - "io" - "net" - "os" - "sync" - "time" -) - -// Difference in 100-nanosecond intervals between -// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). -const epochStart = 122192928000000000 - -type epochFunc func() time.Time -type hwAddrFunc func() (net.HardwareAddr, error) - -var ( - global = newRFC4122Generator() - - posixUID = uint32(os.Getuid()) - posixGID = uint32(os.Getgid()) -) - -// NewV1 returns UUID based on current timestamp and MAC address. -func NewV1() (UUID, error) { - return global.NewV1() -} - -// NewV2 returns DCE Security UUID based on POSIX UID/GID. -func NewV2(domain byte) (UUID, error) { - return global.NewV2(domain) -} - -// NewV3 returns UUID based on MD5 hash of namespace UUID and name. -func NewV3(ns UUID, name string) UUID { - return global.NewV3(ns, name) -} - -// NewV4 returns random generated UUID. -func NewV4() (UUID, error) { - return global.NewV4() -} - -// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. -func NewV5(ns UUID, name string) UUID { - return global.NewV5(ns, name) -} - -// Generator provides interface for generating UUIDs. -type Generator interface { - NewV1() (UUID, error) - NewV2(domain byte) (UUID, error) - NewV3(ns UUID, name string) UUID - NewV4() (UUID, error) - NewV5(ns UUID, name string) UUID -} - -// Default generator implementation. -type rfc4122Generator struct { - clockSequenceOnce sync.Once - hardwareAddrOnce sync.Once - storageMutex sync.Mutex - - rand io.Reader - - epochFunc epochFunc - hwAddrFunc hwAddrFunc - lastTime uint64 - clockSequence uint16 - hardwareAddr [6]byte -} - -func newRFC4122Generator() Generator { - return &rfc4122Generator{ - epochFunc: time.Now, - hwAddrFunc: defaultHWAddrFunc, - rand: rand.Reader, - } -} - -// NewV1 returns UUID based on current timestamp and MAC address. -func (g *rfc4122Generator) NewV1() (UUID, error) { - u := UUID{} - - timeNow, clockSeq, err := g.getClockSequence() - if err != nil { - return Nil, err - } - binary.BigEndian.PutUint32(u[0:], uint32(timeNow)) - binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) - binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) - binary.BigEndian.PutUint16(u[8:], clockSeq) - - hardwareAddr, err := g.getHardwareAddr() - if err != nil { - return Nil, err - } - copy(u[10:], hardwareAddr) - - u.SetVersion(V1) - u.SetVariant(VariantRFC4122) - - return u, nil -} - -// NewV2 returns DCE Security UUID based on POSIX UID/GID. -func (g *rfc4122Generator) NewV2(domain byte) (UUID, error) { - u, err := g.NewV1() - if err != nil { - return Nil, err - } - - switch domain { - case DomainPerson: - binary.BigEndian.PutUint32(u[:], posixUID) - case DomainGroup: - binary.BigEndian.PutUint32(u[:], posixGID) - } - - u[9] = domain - - u.SetVersion(V2) - u.SetVariant(VariantRFC4122) - - return u, nil -} - -// NewV3 returns UUID based on MD5 hash of namespace UUID and name. -func (g *rfc4122Generator) NewV3(ns UUID, name string) UUID { - u := newFromHash(md5.New(), ns, name) - u.SetVersion(V3) - u.SetVariant(VariantRFC4122) - - return u -} - -// NewV4 returns random generated UUID. -func (g *rfc4122Generator) NewV4() (UUID, error) { - u := UUID{} - if _, err := g.rand.Read(u[:]); err != nil { - return Nil, err - } - u.SetVersion(V4) - u.SetVariant(VariantRFC4122) - - return u, nil -} - -// NewV5 returns UUID based on SHA-1 hash of namespace UUID and name. -func (g *rfc4122Generator) NewV5(ns UUID, name string) UUID { - u := newFromHash(sha1.New(), ns, name) - u.SetVersion(V5) - u.SetVariant(VariantRFC4122) - - return u -} - -// Returns epoch and clock sequence. -func (g *rfc4122Generator) getClockSequence() (uint64, uint16, error) { - var err error - g.clockSequenceOnce.Do(func() { - buf := make([]byte, 2) - if _, err = g.rand.Read(buf); err != nil { - return - } - g.clockSequence = binary.BigEndian.Uint16(buf) - }) - if err != nil { - return 0, 0, err - } - - g.storageMutex.Lock() - defer g.storageMutex.Unlock() - - timeNow := g.getEpoch() - // Clock didn't change since last UUID generation. - // Should increase clock sequence. - if timeNow <= g.lastTime { - g.clockSequence++ - } - g.lastTime = timeNow - - return timeNow, g.clockSequence, nil -} - -// Returns hardware address. -func (g *rfc4122Generator) getHardwareAddr() ([]byte, error) { - var err error - g.hardwareAddrOnce.Do(func() { - if hwAddr, err := g.hwAddrFunc(); err == nil { - copy(g.hardwareAddr[:], hwAddr) - return - } - - // Initialize hardwareAddr randomly in case - // of real network interfaces absence. - if _, err = g.rand.Read(g.hardwareAddr[:]); err != nil { - return - } - // Set multicast bit as recommended by RFC 4122 - g.hardwareAddr[0] |= 0x01 - }) - if err != nil { - return []byte{}, err - } - return g.hardwareAddr[:], nil -} - -// Returns difference in 100-nanosecond intervals between -// UUID epoch (October 15, 1582) and current time. -func (g *rfc4122Generator) getEpoch() uint64 { - return epochStart + uint64(g.epochFunc().UnixNano()/100) -} - -// Returns UUID based on hashing of namespace UUID and name. -func newFromHash(h hash.Hash, ns UUID, name string) UUID { - u := UUID{} - h.Write(ns[:]) - h.Write([]byte(name)) - copy(u[:], h.Sum(nil)) - - return u -} - -// Returns hardware address. -func defaultHWAddrFunc() (net.HardwareAddr, error) { - ifaces, err := net.Interfaces() - if err != nil { - return []byte{}, err - } - for _, iface := range ifaces { - if len(iface.HardwareAddr) >= 6 { - return iface.HardwareAddr, nil - } - } - return []byte{}, fmt.Errorf("uuid: no HW address found") -} diff --git a/vendor/github.com/satori/go.uuid/sql.go b/vendor/github.com/satori/go.uuid/sql.go deleted file mode 100644 index 56759d3905..0000000000 --- a/vendor/github.com/satori/go.uuid/sql.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2013-2018 by Maxim Bublis -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package uuid - -import ( - "database/sql/driver" - "fmt" -) - -// Value implements the driver.Valuer interface. -func (u UUID) Value() (driver.Value, error) { - return u.String(), nil -} - -// Scan implements the sql.Scanner interface. -// A 16-byte slice is handled by UnmarshalBinary, while -// a longer byte slice or a string is handled by UnmarshalText. -func (u *UUID) Scan(src interface{}) error { - switch src := src.(type) { - case []byte: - if len(src) == Size { - return u.UnmarshalBinary(src) - } - return u.UnmarshalText(src) - - case string: - return u.UnmarshalText([]byte(src)) - } - - return fmt.Errorf("uuid: cannot convert %T to UUID", src) -} - -// NullUUID can be used with the standard sql package to represent a -// UUID value that can be NULL in the database -type NullUUID struct { - UUID UUID - Valid bool -} - -// Value implements the driver.Valuer interface. -func (u NullUUID) Value() (driver.Value, error) { - if !u.Valid { - return nil, nil - } - // Delegate to UUID Value function - return u.UUID.Value() -} - -// Scan implements the sql.Scanner interface. -func (u *NullUUID) Scan(src interface{}) error { - if src == nil { - u.UUID, u.Valid = Nil, false - return nil - } - - // Delegate to UUID Scan function - u.Valid = true - return u.UUID.Scan(src) -} diff --git a/vendor/github.com/satori/go.uuid/uuid.go b/vendor/github.com/satori/go.uuid/uuid.go deleted file mode 100644 index a2b8e2ca2a..0000000000 --- a/vendor/github.com/satori/go.uuid/uuid.go +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright (C) 2013-2018 by Maxim Bublis -// -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Package uuid provides implementation of Universally Unique Identifier (UUID). -// Supported versions are 1, 3, 4 and 5 (as specified in RFC 4122) and -// version 2 (as specified in DCE 1.1). -package uuid - -import ( - "bytes" - "encoding/hex" -) - -// Size of a UUID in bytes. -const Size = 16 - -// UUID representation compliant with specification -// described in RFC 4122. -type UUID [Size]byte - -// UUID versions -const ( - _ byte = iota - V1 - V2 - V3 - V4 - V5 -) - -// UUID layout variants. -const ( - VariantNCS byte = iota - VariantRFC4122 - VariantMicrosoft - VariantFuture -) - -// UUID DCE domains. -const ( - DomainPerson = iota - DomainGroup - DomainOrg -) - -// String parse helpers. -var ( - urnPrefix = []byte("urn:uuid:") - byteGroups = []int{8, 4, 4, 4, 12} -) - -// Nil is special form of UUID that is specified to have all -// 128 bits set to zero. -var Nil = UUID{} - -// Predefined namespace UUIDs. -var ( - NamespaceDNS = Must(FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) - NamespaceURL = Must(FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) - NamespaceOID = Must(FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) - NamespaceX500 = Must(FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) -) - -// Equal returns true if u1 and u2 equals, otherwise returns false. -func Equal(u1 UUID, u2 UUID) bool { - return bytes.Equal(u1[:], u2[:]) -} - -// Version returns algorithm version used to generate UUID. -func (u UUID) Version() byte { - return u[6] >> 4 -} - -// Variant returns UUID layout variant. -func (u UUID) Variant() byte { - switch { - case (u[8] >> 7) == 0x00: - return VariantNCS - case (u[8] >> 6) == 0x02: - return VariantRFC4122 - case (u[8] >> 5) == 0x06: - return VariantMicrosoft - case (u[8] >> 5) == 0x07: - fallthrough - default: - return VariantFuture - } -} - -// Bytes returns bytes slice representation of UUID. -func (u UUID) Bytes() []byte { - return u[:] -} - -// Returns canonical string representation of UUID: -// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. -func (u UUID) String() string { - buf := make([]byte, 36) - - hex.Encode(buf[0:8], u[0:4]) - buf[8] = '-' - hex.Encode(buf[9:13], u[4:6]) - buf[13] = '-' - hex.Encode(buf[14:18], u[6:8]) - buf[18] = '-' - hex.Encode(buf[19:23], u[8:10]) - buf[23] = '-' - hex.Encode(buf[24:], u[10:]) - - return string(buf) -} - -// SetVersion sets version bits. -func (u *UUID) SetVersion(v byte) { - u[6] = (u[6] & 0x0f) | (v << 4) -} - -// SetVariant sets variant bits. -func (u *UUID) SetVariant(v byte) { - switch v { - case VariantNCS: - u[8] = (u[8]&(0xff>>1) | (0x00 << 7)) - case VariantRFC4122: - u[8] = (u[8]&(0xff>>2) | (0x02 << 6)) - case VariantMicrosoft: - u[8] = (u[8]&(0xff>>3) | (0x06 << 5)) - case VariantFuture: - fallthrough - default: - u[8] = (u[8]&(0xff>>3) | (0x07 << 5)) - } -} - -// Must is a helper that wraps a call to a function returning (UUID, error) -// and panics if the error is non-nil. It is intended for use in variable -// initializations such as -// var packageUUID = uuid.Must(uuid.FromString("123e4567-e89b-12d3-a456-426655440000")); -func Must(u UUID, err error) UUID { - if err != nil { - panic(err) - } - return u -} diff --git a/versioning/AUTHORS b/versioning/AUTHORS new file mode 100644 index 0000000000..848245bb12 --- /dev/null +++ b/versioning/AUTHORS @@ -0,0 +1 @@ +Gerasimos Maropoulos diff --git a/versioning/LICENSE b/versioning/LICENSE new file mode 100644 index 0000000000..469fb44d08 --- /dev/null +++ b/versioning/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Iris nor the names of its +contributor, Gerasimos Maropoulos, may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/versioning/README.md b/versioning/README.md new file mode 100644 index 0000000000..01a629add6 --- /dev/null +++ b/versioning/README.md @@ -0,0 +1,142 @@ +# Versioning + +The [versioning](https://github.com/kataras/iris/tree/master/versioning) package provides [semver](https://semver.org/) versioning for your APIs. It implements all the suggestions written at [api-guidelines](https://github.com/byrondover/api-guidelines/blob/master/Guidelines.md#versioning) and more. + + +The version comparison is done by the [go-version](https://github.com/hashicorp/go-version) package. It supports matching over patterns like `">= 1.0, < 3"` and etc. + +## Features + +- per route version matching, a normal iris handler with "switch" cases via Map for version => handler +- per group versioned routes and deprecation API +- version matching like ">= 1.0, < 2.0" or just "2.0.1" and etc. +- version not found handler (can be customized by simply adding the versioning.NotFound: customNotMatchVersionHandler on the Map) +- version is retrieved from the "Accept" and "Accept-Version" headers (can be customized via middleware) +- respond with "X-API-Version" header, if version found. +- deprecation options with customizable "X-API-Warn", "X-API-Deprecation-Date", "X-API-Deprecation-Info" headers via `Deprecated` wrapper. + +## Get version + +Current request version is retrieved by `versioning.GetVersion(ctx)`. + +By default the `GetVersion` will try to read from: +- `Accept` header, i.e `Accept: "application/json; version=1.0"` +- `Accept-Version` header, i.e `Accept-Version: "1.0"` + +You can also set a custom version for a handler via a middleware by using the context's store values. +For example: +```go +func(ctx iris.Context) { + ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1.0")) + ctx.Next() +} +``` + +## Match version to handler + +The `versioning.NewMatcher(versioning.Map) iris.Handler` creates a single handler which decides what handler need to be executed based on the requested version. + +```go +app := iris.New() + +// middleware for all versions. +myMiddleware := func(ctx iris.Context) { + // [...] + ctx.Next() +} + +myCustomNotVersionFound := func(ctx iris.Context) { + ctx.StatusCode(404) + ctx.Writef("%s version not found", versioning.GetVersion(ctx)) +} + +userAPI := app.Party("/api/user") +userAPI.Get("/", myMiddleware, versioning.NewMatcher(versioning.Map{ + "1.0": sendHandler(v10Response), + ">= 2, < 3": sendHandler(v2Response), + versioning.NotFound: myCustomNotVersionFound, +})) +``` + +### Deprecation + +Using the `versioning.Deprecated(handler iris.Handler, options versioning.DeprecationOptions) iris.Handler` function you can mark a specific handler version as deprecated. + + +```go +v10Handler := versioning.Deprecated(sendHandler(v10Response), versioning.DeprecationOptions{ + // if empty defaults to: "WARNING! You are using a deprecated version of this API." + WarnMessage string + DeprecationDate time.Time + DeprecationInfo string +}) + +userAPI.Get("/", versioning.NewMatcher(versioning.Map{ + "1.0": v10Handler, + // [...] +})) +``` + +This will make the handler to send these headers to the client: + +- `"X-API-Warn": options.WarnMessage` +- `"X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))` +- `"X-API-Deprecation-Info": options.DeprecationInfo` + +> versioning.DefaultDeprecationOptions can be passed instead if you don't care about Date and Info. + +## Grouping routes by version + +Grouping routes by version is possible as well. + +Using the `versioning.NewGroup(version string) *versioning.Group` function you can create a group to register your versioned routes. +The `versioning.RegisterGroups(r iris.Party, versionNotFoundHandler iris.Handler, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`. + +```go +app := iris.New() + +userAPI := app.Party("/api/user") +// [... static serving, middlewares and etc goes here]. + +userAPIV10 := versioning.NewGroup("1.0") +userAPIV10.Get("/", sendHandler(v10Response)) + +userAPIV2 := versioning.NewGroup(">= 2, < 3") +userAPIV2.Get("/", sendHandler(v2Response)) +userAPIV2.Post("/", sendHandler(v2Response)) +userAPIV2.Put("/other", sendHandler(v2Response)) + +versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2) +``` + +> A middleware can be registered to the actual `iris.Party` only, using the methods we learnt above, i.e by using the `versioning.Match` in order to detect what code/handler you want to be executed when "x" or no version is requested. + +### Deprecation for Group + +Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want to notify your API consumers that this specific version is deprecated. + +```go +userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions) +``` + +## Compare version manually from inside your handlers + +```go +// reports if the "version" is matching to the "is". +// the "is" can be a constraint like ">= 1, < 3". +If(version string, is string) bool +``` + +```go +// same as `If` but expects a Context to read the requested version. +Match(ctx iris.Context, expectedVersion string) bool +``` + +```go +app.Get("/api/user", func(ctx iris.Context) { + if versioning.Match(ctx, ">= 2.2.3") { + // [logic for >= 2.2.3 version of your handler goes here] + return + } +}) +``` \ No newline at end of file diff --git a/versioning/deprecation.go b/versioning/deprecation.go new file mode 100644 index 0000000000..c7b8cd06e3 --- /dev/null +++ b/versioning/deprecation.go @@ -0,0 +1,50 @@ +package versioning + +import ( + "time" + + "github.com/kataras/iris/context" +) + +// DeprecationOptions describes the deprecation headers key-values. +// - "X-API-Warn": options.WarnMessage +// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate)) +// - "X-API-Deprecation-Info": options.DeprecationInfo +type DeprecationOptions struct { + WarnMessage string + DeprecationDate time.Time + DeprecationInfo string +} + +// ShouldHandle reports whether the deprecation headers should be present or no. +func (opts DeprecationOptions) ShouldHandle() bool { + return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != "" +} + +// DefaultDeprecationOptions are the default deprecation options, +// it defaults the "X-API-Warn" header to a generic message. +var DefaultDeprecationOptions = DeprecationOptions{ + WarnMessage: "WARNING! You are using a deprecated version of this API.", +} + +// Deprecated marks a specific handler as a deprecated. +// Deprecated can be used to tell the clients that +// a newer version of that specific resource is available instead. +func Deprecated(handler context.Handler, options DeprecationOptions) context.Handler { + if options.WarnMessage == "" { + options.WarnMessage = DefaultDeprecationOptions.WarnMessage + } + + return func(ctx context.Context) { + handler(ctx) + ctx.Header("X-API-Warn", options.WarnMessage) + + if !options.DeprecationDate.IsZero() { + ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate)) + } + + if options.DeprecationInfo != "" { + ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo) + } + } +} diff --git a/versioning/deprecation_test.go b/versioning/deprecation_test.go new file mode 100644 index 0000000000..4cdcf83e78 --- /dev/null +++ b/versioning/deprecation_test.go @@ -0,0 +1,33 @@ +package versioning_test + +import ( + "testing" + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "github.com/kataras/iris/versioning" +) + +func TestDeprecated(t *testing.T) { + app := iris.New() + + writeVesion := func(ctx iris.Context) { + ctx.WriteString(versioning.GetVersion(ctx)) + } + + opts := versioning.DeprecationOptions{ + WarnMessage: "deprecated, see ", + DeprecationDate: time.Now().UTC(), + DeprecationInfo: "a bigger version is available, see for more information", + } + app.Get("/", versioning.Deprecated(writeVesion, opts)) + + e := httptest.New(t, app) + + ex := e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() + ex.Status(iris.StatusOK).Body().Equal("1.0") + ex.Header("X-API-Warn").Equal(opts.WarnMessage) + expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat()) + ex.Header("X-API-Deprecation-Date").Equal(expectedDateStr) +} diff --git a/versioning/group.go b/versioning/group.go new file mode 100644 index 0000000000..3ee4ef4c59 --- /dev/null +++ b/versioning/group.go @@ -0,0 +1,185 @@ +package versioning + +import ( + "net/http" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" +) + +type ( + vroute struct { + method string + path string + versions Map + } + + // Group is a group of version-based routes. + // One version per one or more routes. + Group struct { + version string + extraMethods []string + routes []vroute + + deprecation DeprecationOptions + } +) + +// NewGroup returns a ptr to Group based on the given "version". +// +// See `Handle` and `RegisterGroups` for more. +func NewGroup(version string) *Group { + return &Group{ + version: version, + } +} + +// Deprecated marks this group and all its versioned routes +// as deprecated versions of that endpoint. +// It can be called in the end just before `RegisterGroups` +// or first by `NewGroup(...).Deprecated(...)`. It returns itself. +func (g *Group) Deprecated(options DeprecationOptions) *Group { + // if `Deprecated` is called in the end. + for _, r := range g.routes { + r.versions[g.version] = Deprecated(r.versions[g.version], options) + } + + // store the options if called before registering any versioned routes. + g.deprecation = options + + return g +} + +// AllowMethods can be called before `Handle/Get/Post...` +// to tell the underline router that all routes should be registered +// to these "methods" as well. +func (g *Group) AllowMethods(methods ...string) *Group { + g.extraMethods = append(g.extraMethods, methods...) + return g +} + +func (g *Group) addVRoute(method, path string, handler context.Handler) { + for _, r := range g.routes { // check if route already exists. + if r.method == method && r.path == path { + return + } + } + + g.routes = append(g.routes, vroute{ + method: method, + path: path, + versions: Map{g.version: handler}, + }) +} + +// Handle registers a versioned route to the group. +// A call of `RegisterGroups` is necessary in order to register the actual routes +// when the group is complete. +// +// `RegisterGroups` for more. +func (g *Group) Handle(method string, path string, handler context.Handler) { + if g.deprecation.ShouldHandle() { // if `Deprecated` called first. + handler = Deprecated(handler, g.deprecation) + } + + methods := append(g.extraMethods, method) + + for _, method := range methods { + g.addVRoute(method, path, handler) + } +} + +// None registers an "offline" versioned route +// see `context#ExecRoute(routeName)` and routing examples. +func (g *Group) None(path string, handler context.Handler) { + g.Handle(router.MethodNone, path, handler) +} + +// Get registers a versioned route for the Get http method. +func (g *Group) Get(path string, handler context.Handler) { + g.Handle(http.MethodGet, path, handler) +} + +// Post registers a versioned route for the Post http method. +func (g *Group) Post(path string, handler context.Handler) { + g.Handle(http.MethodPost, path, handler) +} + +// Put registers a versioned route for the Put http method +func (g *Group) Put(path string, handler context.Handler) { + g.Handle(http.MethodPut, path, handler) +} + +// Delete registers a versioned route for the Delete http method. +func (g *Group) Delete(path string, handler context.Handler) { + g.Handle(http.MethodDelete, path, handler) +} + +// Connect registers a versioned route for the Connect http method. +func (g *Group) Connect(path string, handler context.Handler) { + g.Handle(http.MethodConnect, path, handler) +} + +// Head registers a versioned route for the Head http method. +func (g *Group) Head(path string, handler context.Handler) { + g.Handle(http.MethodHead, path, handler) +} + +// Options registers a versioned route for the Options http method. +func (g *Group) Options(path string, handler context.Handler) { + g.Handle(http.MethodOptions, path, handler) +} + +// Patch registers a versioned route for the Patch http method. +func (g *Group) Patch(path string, handler context.Handler) { + g.Handle(http.MethodPatch, path, handler) +} + +// Trace registers a versioned route for the Trace http method. +func (g *Group) Trace(path string, handler context.Handler) { + g.Handle(http.MethodTrace, path, handler) +} + +// Any registers a versioned route for ALL of the http methods +// (Get,Post,Put,Head,Patch,Options,Connect,Delete). +func (g *Group) Any(registeredPath string, handler context.Handler) { + g.Get(registeredPath, handler) + g.Post(registeredPath, handler) + g.Put(registeredPath, handler) + g.Delete(registeredPath, handler) + g.Connect(registeredPath, handler) + g.Head(registeredPath, handler) + g.Options(registeredPath, handler) + g.Patch(registeredPath, handler) + g.Trace(registeredPath, handler) +} + +// RegisterGroups registers one or more groups to an `iris.Party` or to the root router. +// See `NewGroup` and `NotFoundHandler` too. +func RegisterGroups(r router.Party, notFoundHandler context.Handler, groups ...*Group) (actualRoutes []*router.Route) { + var total []vroute + for _, g := range groups { + inner: + for _, r := range g.routes { + for i, tr := range total { + if tr.method == r.method && tr.path == r.path { + total[i].versions[g.version] = r.versions[g.version] + continue inner + } + } + + total = append(total, r) + } + } + + for _, vr := range total { + if notFoundHandler != nil { + vr.versions[NotFound] = notFoundHandler + } + + route := r.Handle(vr.method, vr.path, NewMatcher(vr.versions)) + actualRoutes = append(actualRoutes, route) + } + + return +} diff --git a/vendor/github.com/hashicorp/go-version/LICENSE b/versioning/vendor/github.com/hashicorp/go-version/LICENSE similarity index 100% rename from vendor/github.com/hashicorp/go-version/LICENSE rename to versioning/vendor/github.com/hashicorp/go-version/LICENSE diff --git a/vendor/github.com/hashicorp/go-version/constraint.go b/versioning/vendor/github.com/hashicorp/go-version/constraint.go similarity index 100% rename from vendor/github.com/hashicorp/go-version/constraint.go rename to versioning/vendor/github.com/hashicorp/go-version/constraint.go diff --git a/vendor/github.com/hashicorp/go-version/version.go b/versioning/vendor/github.com/hashicorp/go-version/version.go similarity index 100% rename from vendor/github.com/hashicorp/go-version/version.go rename to versioning/vendor/github.com/hashicorp/go-version/version.go diff --git a/vendor/github.com/hashicorp/go-version/version_collection.go b/versioning/vendor/github.com/hashicorp/go-version/version_collection.go similarity index 100% rename from vendor/github.com/hashicorp/go-version/version_collection.go rename to versioning/vendor/github.com/hashicorp/go-version/version_collection.go diff --git a/versioning/version.go b/versioning/version.go new file mode 100644 index 0000000000..75d814baa5 --- /dev/null +++ b/versioning/version.go @@ -0,0 +1,95 @@ +package versioning + +import ( + "strings" + + "github.com/kataras/iris/context" +) + +const ( + // AcceptVersionHeaderKey is the header key of "Accept-Version". + AcceptVersionHeaderKey = "Accept-Version" + // AcceptHeaderKey is the header key of "Accept". + AcceptHeaderKey = "Accept" + // AcceptHeaderVersionValue is the Accept's header value search term the requested version. + AcceptHeaderVersionValue = "version" + + // Key is the context key of the version, can be used to manually modify the "requested" version. + // Example of how you can change the default behavior to extract a requested version (which is by headers) + // from a "version" url parameter instead: + // func(ctx iris.Context) { // &version=1 + // ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1")) + // ctx.Next() + // } + Key = "iris.api.version" // for use inside the ctx.Values(), not visible by the user. + // NotFound is the key that can be used inside a `Map` or inside `ctx.Values().Set(versioning.Key, versioning.NotFound)` + // to tell that a version wasn't found, therefore the not found handler should handle the request instead. + NotFound = Key + ".notfound" +) + +// NotFoundHandler is the default version not found handler that +// is executed from `NewMatcher` when no version is registered as available to dispatch a resource. +var NotFoundHandler = func(ctx context.Context) { + // 303 is an option too, + // end-dev has the chance to change that behavior by using the NotFound in the map: + // + // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /* + 10.5.2 501 Not Implemented + + The server does not support the functionality required to fulfill the request. + This is the appropriate response when the server does not + recognize the request method and is not capable of supporting it for any resource. + */ + + ctx.StatusCode(501) + ctx.WriteString("version not found") +} + +// GetVersion returns the current request version. +// +// By default the `GetVersion` will try to read from: +// - "Accept" header, i.e Accept: "application/json; version=1.0" +// - "Accept-Version" header, i.e Accept-Version: "1.0" +// +// However, the end developer can also set a custom version for a handler via a middleware by using the context's store key +// for versions (see `Key` for further details on that). +func GetVersion(ctx context.Context) string { + // firstly by context store, if manually set-ed by a middleware. + if version := ctx.Values().GetString(Key); version != "" { + return version + } + + // secondly by the "Accept-Version" header. + if version := ctx.GetHeader(AcceptVersionHeaderKey); version != "" { + return version + } + + // thirdly by the "Accept" header which is like"...; version=1.0" + acceptValue := ctx.GetHeader(AcceptHeaderKey) + if acceptValue != "" { + if idx := strings.Index(acceptValue, AcceptHeaderVersionValue); idx != -1 { + rem := acceptValue[idx:] + startVersion := strings.Index(rem, "=") + if startVersion == -1 || len(rem) < startVersion+1 { + return NotFound + } + + rem = rem[startVersion+1:] + + end := strings.Index(rem, " ") + if end == -1 { + end = strings.Index(rem, ";") + } + if end == -1 { + end = len(rem) + } + + if version := rem[:end]; version != "" { + return version + } + } + } + + return NotFound +} diff --git a/versioning/version_test.go b/versioning/version_test.go new file mode 100644 index 0000000000..059fb31050 --- /dev/null +++ b/versioning/version_test.go @@ -0,0 +1,48 @@ +package versioning_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "github.com/kataras/iris/versioning" +) + +func TestGetVersion(t *testing.T) { + app := iris.New() + + writeVesion := func(ctx iris.Context) { + ctx.WriteString(versioning.GetVersion(ctx)) + } + + app.Get("/", writeVesion) + app.Get("/manual", func(ctx iris.Context) { + ctx.Values().Set(versioning.Key, "11.0.5") + ctx.Next() + }, writeVesion) + + e := httptest.New(t, app) + + e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect(). + Status(iris.StatusOK).Body().Equal("1.0") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect(). + Status(iris.StatusOK).Body().Equal("2.1") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect(). + Status(iris.StatusOK).Body().Equal("2.1") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect(). + Status(iris.StatusOK).Body().Equal("2.1") + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect(). + Status(iris.StatusOK).Body().Equal("1") + + // unknown versions. + e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect(). + Status(iris.StatusOK).Body().Equal(versioning.NotFound) + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=").Expect(). + Status(iris.StatusOK).Body().Equal(versioning.NotFound) + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version= ;other=dsa").Expect(). + Status(iris.StatusOK).Body().Equal(versioning.NotFound) + e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=").Expect(). + Status(iris.StatusOK).Body().Equal(versioning.NotFound) + + e.GET("/manual").Expect().Status(iris.StatusOK).Body().Equal("11.0.5") +} diff --git a/versioning/versioning.go b/versioning/versioning.go new file mode 100644 index 0000000000..ca280d1ef9 --- /dev/null +++ b/versioning/versioning.go @@ -0,0 +1,120 @@ +package versioning + +import ( + "github.com/kataras/iris/context" + + "github.com/hashicorp/go-version" +) + +// If reports whether the "version" is matching to the "is". +// the "is" can be a constraint like ">= 1, < 3". +func If(v string, is string) bool { + ver, err := version.NewVersion(v) + if err != nil { + return false + } + + constraints, err := version.NewConstraint(is) + if err != nil { + return false + } + + return constraints.Check(ver) +} + +// Match acts exactly the same as `If` does but instead it accepts +// a Context, so it can be called by a handler to determinate the requested version. +func Match(ctx context.Context, expectedVersion string) bool { + return If(GetVersion(ctx), expectedVersion) +} + +// Map is a map of versions targets to a handlers, +// a handler per version or constraint, the key can be something like ">1, <=2" or just "1". +type Map map[string]context.Handler + +// NewMatcher creates a single handler which decides what handler +// should be executed based on the requested version. +// +// Use the `NewGroup` if you want to add many routes under a specific version. +// +// See `Map` and `NewGroup` too. +func NewMatcher(versions Map) context.Handler { + constraintsHandlers, notFoundHandler := buildConstraints(versions) + + return func(ctx context.Context) { + versionString := GetVersion(ctx) + if versionString == NotFound { + notFoundHandler(ctx) + return + } + + ver, err := version.NewVersion(versionString) + if err != nil { + notFoundHandler(ctx) + return + } + + for _, ch := range constraintsHandlers { + if ch.constraints.Check(ver) { + ctx.Header("X-API-Version", ver.String()) + ch.handler(ctx) + return + } + } + + // pass the not matched version so the not found handler can have knowedge about it. + // ctx.Values().Set(Key, versionString) + // or let a manual cal of GetVersion(ctx) do that instead. + notFoundHandler(ctx) + } +} + +type constraintsHandler struct { + constraints version.Constraints + handler context.Handler +} + +func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHandler, notfoundHandler context.Handler) { + for v, h := range versionsHandler { + if v == NotFound { + notfoundHandler = h + continue + } + + constraints, err := version.NewConstraint(v) + if err != nil { + panic(err) + } + + constraintsHandlers = append(constraintsHandlers, &constraintsHandler{ + constraints: constraints, + handler: h, + }) + } + + if notfoundHandler == nil { + notfoundHandler = NotFoundHandler + } + + // no sort, the end-dev should declare + // all version constraint, i.e < 4.0 may be catch 1.0 if not something like + // >= 3.0, < 4.0. + // I can make it ordered but I do NOT like the final API of it: + /* + app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem. + V("1.0", vHandler("v1 here")), + V("2.0", vHandler("v2 here")), + V("< 4.0", vHandler("v3.x here")), + )) + instead we have: + + app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem. + "1.0": Deprecated(vHandler("v1 here")), + "2.0": vHandler("v2 here"), + ">= 3.0, < 4.0": vHandler("v3.x here"), + VersionUnknown: customHandlerForNotMatchingVersion, + })) + */ + + return +} diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go new file mode 100644 index 0000000000..4f7d560034 --- /dev/null +++ b/versioning/versioning_test.go @@ -0,0 +1,136 @@ +package versioning_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + "github.com/kataras/iris/versioning" +) + +func notFoundHandler(ctx iris.Context) { + ctx.NotFound() +} + +const ( + v10Response = "v1.0 handler" + v2Response = "v2.x handler" +) + +func sendHandler(contents string) iris.Handler { + return func(ctx iris.Context) { + ctx.WriteString(contents) + } +} + +func TestIf(t *testing.T) { + if expected, got := true, versioning.If("1.0", ">=1"); expected != got { + t.Fatalf("expected %s to be %s", "1.0", ">= 1") + } + if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got { + t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2") + } +} +func TestNewMatcher(t *testing.T) { + app := iris.New() + + userAPI := app.Party("/api/user") + userAPI.Get("/", versioning.NewMatcher(versioning.Map{ + "1.0": sendHandler(v10Response), + ">= 2, < 3": sendHandler(v2Response), + versioning.NotFound: notFoundHandler, + })) + + // middleware as usual. + myMiddleware := func(ctx iris.Context) { + ctx.Header("X-Custom", "something") + ctx.Next() + } + myVersions := versioning.Map{ + "1.0": sendHandler(v10Response), + } + + userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions)) + + e := httptest.New(t, app) + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect(). + Status(iris.StatusOK).Body().Equal(v10Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + + // middleware as usual. + ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect() + ex.Status(iris.StatusOK).Body().Equal(v10Response) + ex.Header("X-Custom").Equal("something") + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). + Status(iris.StatusNotFound).Body().Equal("Not Found") +} + +func TestNewGroup(t *testing.T) { + app := iris.New() + + userAPI := app.Party("/api/user") + // [... static serving, middlewares and etc goes here]. + + userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions) + // V10middlewareResponse := "m1" + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl1") + // sendHandler(V10middlewareResponse)(ctx) + // ctx.Next() + // }) + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl2") + // sendHandler(V10middlewareResponse + "midl2")(ctx) + // ctx.Next() + // }) + // userAPIV10.Use(func(ctx iris.Context) { + // println("exec userAPIV10.Use - midl3") + // ctx.Next() + // }) + + userAPIV10.Get("/", sendHandler(v10Response)) + userAPIV2 := versioning.NewGroup(">= 2, < 3") + // V2middlewareResponse := "m2" + // userAPIV2.Use(func(ctx iris.Context) { + // println("exec userAPIV2.Use - midl1") + // sendHandler(V2middlewareResponse)(ctx) + // ctx.Next() + // }) + // userAPIV2.Use(func(ctx iris.Context) { + // println("exec userAPIV2.Use - midl2") + // ctx.Next() + // }) + + userAPIV2.Get("/", sendHandler(v2Response)) + userAPIV2.Post("/", sendHandler(v2Response)) + userAPIV2.Put("/other", sendHandler(v2Response)) + + versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2) + + e := httptest.New(t, app) + + ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect() + ex.Status(iris.StatusOK).Body().Equal(v10Response) + ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage) + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect(). + Status(iris.StatusOK).Body().Equal(v2Response) + + e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect(). + Status(iris.StatusNotImplemented).Body().Equal("version not found") +} diff --git a/view/html.go b/view/html.go index 24bb20ea8e..8941674e31 100644 --- a/view/html.go +++ b/view/html.go @@ -32,7 +32,7 @@ type ( funcs map[string]interface{} // - middleware func(name string, contents string) (string, error) + middleware func(name string, contents []byte) (string, error) Templates *template.Template // } @@ -44,6 +44,9 @@ var emptyFuncs = template.FuncMap{ "yield": func() (string, error) { return "", fmt.Errorf("yield was called, yet no layout defined") }, + "part": func() (string, error) { + return "", fmt.Errorf("block was called, yet no layout defined") + }, "partial": func() (string, error) { return "", fmt.Errorf("block was called, yet no layout defined") }, @@ -263,7 +266,7 @@ func (s *HTMLEngine) loadDirectory() error { tmpl := s.Templates.New(name) tmpl.Option(s.options...) if s.middleware != nil { - contents, err = s.middleware(name, contents) + contents, err = s.middleware(name, buf) } if err != nil { templateErr = err @@ -361,7 +364,7 @@ func (s *HTMLEngine) loadAssets() error { tmpl.Option(s.options...) if s.middleware != nil { - contents, err = s.middleware(name, contents) + contents, err = s.middleware(name, buf) if err != nil { templateErr = fmt.Errorf("%v for name '%s'", err, name) continue @@ -389,6 +392,15 @@ func (s *HTMLEngine) layoutFuncsFor(name string, binding interface{}) { // Return safe HTML here since we are rendering our own template. return template.HTML(buf.String()), err }, + "part": func(partName string) (template.HTML, error) { + nameTemp := strings.Replace(name, ".html", "", -1) + fullPartName := fmt.Sprintf("%s-%s", nameTemp, partName) + buf, err := s.executeTemplateBuf(fullPartName, binding) + if err != nil { + return "", nil + } + return template.HTML(buf.String()), err + }, "current": func() (string, error) { return name, nil }, diff --git a/websocket/client.go b/websocket/client.go index 771e5eb8d7..2144411a7f 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -7,7 +7,7 @@ import ( ) // ClientHandler is the handler which serves the javascript client-side -// library. It uses a small cache based on the iris/context.StaticCacheDuration. +// library. It uses a small cache based on the iris/context.WriteWithExpiration. func ClientHandler() context.Handler { modNow := time.Now() return func(ctx context.Context) { @@ -20,12 +20,12 @@ func ClientHandler() context.Handler { } } -// ClientSource the client-side javascript raw source code +// ClientSource the client-side javascript raw source code. var ClientSource = []byte(`var websocketStringMessageType = 0; var websocketIntMessageType = 1; var websocketBoolMessageType = 2; var websocketJSONMessageType = 4; -var websocketMessagePrefix = "iris-websocket-message:"; +var websocketMessagePrefix = "` + DefaultEvtMessageKey + `"; var websocketMessageSeparator = ";"; var websocketMessagePrefixLen = websocketMessagePrefix.length; var websocketMessageSeparatorLen = websocketMessageSeparator.length; diff --git a/websocket/config.go b/websocket/config.go index cc0ac3103d..68be4f751e 100644 --- a/websocket/config.go +++ b/websocket/config.go @@ -1,10 +1,13 @@ package websocket import ( + "math/rand" "net/http" "time" "github.com/kataras/iris/context" + + "github.com/iris-contrib/go.uuid" ) const ( @@ -22,15 +25,24 @@ const ( DefaultWebsocketReadBufferSize = 4096 // DefaultWebsocketWriterBufferSize 4096 DefaultWebsocketWriterBufferSize = 4096 - // DefaultClientSourcePath "/iris-ws.js" - DefaultClientSourcePath = "/iris-ws.js" + // DefaultEvtMessageKey is the default prefix of the underline websocket events + // that are being established under the hoods. + // + // Defaults to "iris-websocket-message:". + // Last character of the prefix should be ':'. + DefaultEvtMessageKey = "iris-websocket-message:" ) var ( - // DefaultIDGenerator returns the result of 64 - // random combined characters as the id of a new connection. - // Used when config.IDGenerator is nil - DefaultIDGenerator = func(context.Context) string { return randomString(64) } + // DefaultIDGenerator returns a random unique for a new connection. + // Used when config.IDGenerator is nil. + DefaultIDGenerator = func(context.Context) string { + id, err := uuid.NewV4() + if err != nil { + return randomString(64) + } + return id.String() + } ) // Config the websocket server configuration @@ -38,9 +50,16 @@ var ( type Config struct { // IDGenerator used to create (and later on, set) // an ID for each incoming websocket connections (clients). - // The request is an argument which you can use to generate the ID (from headers for example). + // The request is an input parameter which you can use to generate the ID (from headers for example). // If empty then the ID is generated by DefaultIDGenerator: randomString(64) IDGenerator func(ctx context.Context) string + // EvtMessagePrefix is the prefix of the underline websocket events that are being established under the hoods. + // This prefix is visible only to the javascript side (code) and it has nothing to do + // with the message that the end-user receives. + // Do not change it unless it is absolutely necessary. + // + // If empty then defaults to []byte("iris-websocket-message:"). + EvtMessagePrefix []byte // Error is the function that will be fired if any client couldn't upgrade the HTTP connection // to a websocket connection, a handshake error. Error func(w http.ResponseWriter, r *http.Request, status int, reason error) @@ -135,9 +154,47 @@ func (c Config) Validate() Config { } } + if len(c.EvtMessagePrefix) == 0 { + c.EvtMessagePrefix = []byte(DefaultEvtMessageKey) + } + if c.IDGenerator == nil { c.IDGenerator = DefaultIDGenerator } return c } + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return b +} + +// randomString accepts a number(10 for example) and returns a random string using simple but fairly safe random algorithm +func randomString(n int) string { + return string(random(n)) +} diff --git a/websocket/connection.go b/websocket/connection.go index 748f8067af..11f1e787bb 100644 --- a/websocket/connection.go +++ b/websocket/connection.go @@ -2,6 +2,7 @@ package websocket import ( "bytes" + "errors" "io" "net" "strconv" @@ -234,7 +235,7 @@ type ( broadcast Emitter // pre-defined emitter that sends message to all except this all Emitter // pre-defined emitter which sends message to all clients - // access to the Context, use with causion, you can't use response writer as you imagine. + // access to the Context, use with caution, you can't use response writer as you imagine. ctx context.Context values ConnectionValues server *Server @@ -414,15 +415,15 @@ func (c *connection) startReader() { // messageReceived checks the incoming message and fire the nativeMessage listeners or the event listeners (ws custom message) func (c *connection) messageReceived(data []byte) { - if bytes.HasPrefix(data, websocketMessagePrefixBytes) { - customData := string(data) + if bytes.HasPrefix(data, c.server.config.EvtMessagePrefix) { //it's a custom ws message - receivedEvt := getWebsocketCustomEvent(customData) - listeners := c.onEventListeners[receivedEvt] - if listeners == nil { // if not listeners for this event exit from here - return + receivedEvt := c.server.messageSerializer.getWebsocketCustomEvent(data) + listeners, ok := c.onEventListeners[string(receivedEvt)] + if !ok || len(listeners) == 0 { + return // if not listeners for this event exit from here } - customMessage, err := websocketMessageDeserialize(receivedEvt, customData) + + customMessage, err := c.server.messageSerializer.deserialize(receivedEvt, data) if customMessage == nil || err != nil { return } @@ -580,7 +581,14 @@ func (c *connection) Wait() { c.startReader() } +// ErrAlreadyDisconnected can be reported on the `Connection#Disconnect` function whenever the caller tries to close the +// connection when it is already closed by the client or the caller previously. +var ErrAlreadyDisconnected = errors.New("already disconnected") + func (c *connection) Disconnect() error { + if c == nil || c.disconnected { + return ErrAlreadyDisconnected + } return c.server.Disconnect(c.ID()) } diff --git a/websocket/emitter.go b/websocket/emitter.go index 859bf0ee61..d8ae087d8b 100644 --- a/websocket/emitter.go +++ b/websocket/emitter.go @@ -34,10 +34,10 @@ func (e *emitter) EmitMessage(nativeMessage []byte) error { } func (e *emitter) Emit(event string, data interface{}) error { - message, err := websocketMessageSerialize(event, data) + message, err := e.conn.server.messageSerializer.serialize(event, data) if err != nil { return err } - e.EmitMessage([]byte(message)) + e.EmitMessage(message) return nil } diff --git a/websocket/message.go b/websocket/message.go index f5e4f29e3c..804baa3c24 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -1,179 +1,181 @@ package websocket import ( + "bytes" + "encoding/binary" "encoding/json" - "math/rand" "strconv" - "strings" - "time" "github.com/kataras/iris/core/errors" "github.com/valyala/bytebufferpool" ) -// The same values are exists on client side also -const ( - websocketStringMessageType websocketMessageType = iota - websocketIntMessageType - websocketBoolMessageType - websocketBytesMessageType - websocketJSONMessageType -) - -const ( - websocketMessagePrefix = "iris-websocket-message:" - websocketMessageSeparator = ";" - websocketMessagePrefixLen = len(websocketMessagePrefix) - websocketMessageSeparatorLen = len(websocketMessageSeparator) - websocketMessagePrefixAndSepIdx = websocketMessagePrefixLen + websocketMessageSeparatorLen - 1 - websocketMessagePrefixIdx = websocketMessagePrefixLen - 1 - websocketMessageSeparatorIdx = websocketMessageSeparatorLen - 1 -) - -var ( - websocketMessageSeparatorByte = websocketMessageSeparator[0] - websocketMessageBuffer = bytebufferpool.Pool{} - websocketMessagePrefixBytes = []byte(websocketMessagePrefix) -) - type ( - websocketMessageType uint8 + messageType uint8 ) -func (m websocketMessageType) String() string { +func (m messageType) String() string { return strconv.Itoa(int(m)) } -func (m websocketMessageType) Name() string { - if m == websocketStringMessageType { +func (m messageType) Name() string { + switch m { + case messageTypeString: return "string" - } else if m == websocketIntMessageType { + case messageTypeInt: return "int" - } else if m == websocketBoolMessageType { + case messageTypeBool: return "bool" - } else if m == websocketBytesMessageType { + case messageTypeBytes: return "[]byte" - } else if m == websocketJSONMessageType { + case messageTypeJSON: return "json" + default: + return "Invalid(" + m.String() + ")" } +} + +// The same values are exists on client side too. +const ( + messageTypeString messageType = iota + messageTypeInt + messageTypeBool + messageTypeBytes + messageTypeJSON +) + +const ( + messageSeparator = ";" +) + +var messageSeparatorByte = messageSeparator[0] - return "Invalid(" + m.String() + ")" +type messageSerializer struct { + prefix []byte + prefixLen int + separatorLen int + prefixAndSepIdx int + prefixIdx int + separatorIdx int + + buf *bytebufferpool.Pool +} + +func newMessageSerializer(messagePrefix []byte) *messageSerializer { + return &messageSerializer{ + prefix: messagePrefix, + prefixLen: len(messagePrefix), + separatorLen: len(messageSeparator), + prefixAndSepIdx: len(messagePrefix) + len(messageSeparator) - 1, + prefixIdx: len(messagePrefix) - 1, + separatorIdx: len(messageSeparator) - 1, + + buf: new(bytebufferpool.Pool), + } } +var ( + boolTrueB = []byte("true") + boolFalseB = []byte("false") +) + // websocketMessageSerialize serializes a custom websocket message from websocketServer to be delivered to the client // returns the string form of the message // Supported data types are: string, int, bool, bytes and JSON. -func websocketMessageSerialize(event string, data interface{}) (string, error) { - var msgType websocketMessageType - var dataMessage string - - if s, ok := data.(string); ok { - msgType = websocketStringMessageType - dataMessage = s - } else if i, ok := data.(int); ok { - msgType = websocketIntMessageType - dataMessage = strconv.Itoa(i) - } else if b, ok := data.(bool); ok { - msgType = websocketBoolMessageType - dataMessage = strconv.FormatBool(b) - } else if by, ok := data.([]byte); ok { - msgType = websocketBytesMessageType - dataMessage = string(by) - } else { +func (ms *messageSerializer) serialize(event string, data interface{}) ([]byte, error) { + b := ms.buf.Get() + b.Write(ms.prefix) + b.WriteString(event) + b.WriteByte(messageSeparatorByte) + + switch v := data.(type) { + case string: + b.WriteString(messageTypeString.String()) + b.WriteByte(messageSeparatorByte) + b.WriteString(v) + case int: + b.WriteString(messageTypeInt.String()) + b.WriteByte(messageSeparatorByte) + binary.Write(b, binary.LittleEndian, v) + case bool: + b.WriteString(messageTypeBool.String()) + b.WriteByte(messageSeparatorByte) + if v { + b.Write(boolTrueB) + } else { + b.Write(boolFalseB) + } + case []byte: + b.WriteString(messageTypeBytes.String()) + b.WriteByte(messageSeparatorByte) + b.Write(v) + default: //we suppose is json res, err := json.Marshal(data) if err != nil { - return "", err + return nil, err } - msgType = websocketJSONMessageType - dataMessage = string(res) + b.WriteString(messageTypeJSON.String()) + b.WriteByte(messageSeparatorByte) + b.Write(res) } - b := websocketMessageBuffer.Get() - b.WriteString(websocketMessagePrefix) - b.WriteString(event) - b.WriteString(websocketMessageSeparator) - b.WriteString(msgType.String()) - b.WriteString(websocketMessageSeparator) - b.WriteString(dataMessage) - dataMessage = b.String() - websocketMessageBuffer.Put(b) - - return dataMessage, nil + message := b.Bytes() + ms.buf.Put(b) + return message, nil } var errInvalidTypeMessage = errors.New("Type %s is invalid for message: %s") -// websocketMessageDeserialize deserializes a custom websocket message from the client +// deserialize deserializes a custom websocket message from the client // ex: iris-websocket-message;chat;4;themarshaledstringfromajsonstruct will return 'hello' as string // Supported data types are: string, int, bool, bytes and JSON. -func websocketMessageDeserialize(event string, websocketMessage string) (message interface{}, err error) { - t, formaterr := strconv.Atoi(websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+1 : websocketMessagePrefixAndSepIdx+len(event)+2]) // in order to iris-websocket-message;user;-> 4 - if formaterr != nil { - return nil, formaterr - } - _type := websocketMessageType(t) - _message := websocketMessage[websocketMessagePrefixAndSepIdx+len(event)+3:] // in order to iris-websocket-message;user;4; -> themarshaledstringfromajsonstruct - - if _type == websocketStringMessageType { - message = string(_message) - } else if _type == websocketIntMessageType { - message, err = strconv.Atoi(_message) - } else if _type == websocketBoolMessageType { - message, err = strconv.ParseBool(_message) - } else if _type == websocketBytesMessageType { - message = []byte(_message) - } else if _type == websocketJSONMessageType { - err = json.Unmarshal([]byte(_message), &message) - } else { - return nil, errInvalidTypeMessage.Format(_type.Name(), websocketMessage) +func (ms *messageSerializer) deserialize(event []byte, websocketMessage []byte) (interface{}, error) { + dataStartIdx := ms.prefixAndSepIdx + len(event) + 3 + if len(websocketMessage) <= dataStartIdx { + return nil, errors.New("websocket invalid message: " + string(websocketMessage)) } - return -} - -// getWebsocketCustomEvent return empty string when the websocketMessage is native message -func getWebsocketCustomEvent(websocketMessage string) string { - if len(websocketMessage) < websocketMessagePrefixAndSepIdx { - return "" + typ, err := strconv.Atoi(string(websocketMessage[ms.prefixAndSepIdx+len(event)+1 : ms.prefixAndSepIdx+len(event)+2])) // in order to iris-websocket-message;user;-> 4 + if err != nil { + return nil, err } - s := websocketMessage[websocketMessagePrefixAndSepIdx:] - evt := s[:strings.IndexByte(s, websocketMessageSeparatorByte)] - return evt -} - -const ( - letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1< themarshaledstringfromajsonstruct -// random takes a parameter (int) and returns random slice of byte -// ex: var randomstrbytes []byte; randomstrbytes = utils.Random(32) -func random(n int) []byte { - b := make([]byte, n) - // A src.Int63() generates 63 random bits, enough for letterIdxMax characters! - for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; { - if remain == 0 { - cache, remain = src.Int63(), letterIdxMax + switch messageType(typ) { + case messageTypeString: + return string(data), nil + case messageTypeInt: + msg, err := strconv.Atoi(string(data)) + if err != nil { + return nil, err } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- + return msg, nil + case messageTypeBool: + if bytes.Equal(data, boolTrueB) { + return true, nil } - cache >>= letterIdxBits - remain-- + return false, nil + case messageTypeBytes: + return data, nil + case messageTypeJSON: + var msg interface{} + err := json.Unmarshal(data, &msg) + return msg, err + default: + return nil, errInvalidTypeMessage.Format(messageType(typ).Name(), websocketMessage) } - - return b } -// randomString accepts a number(10 for example) and returns a random string using simple but fairly safe random algorithm -func randomString(n int) string { - return string(random(n)) +// getWebsocketCustomEvent return empty string when the websocketMessage is native message +func (ms *messageSerializer) getWebsocketCustomEvent(websocketMessage []byte) []byte { + if len(websocketMessage) < ms.prefixAndSepIdx { + return nil + } + s := websocketMessage[ms.prefixAndSepIdx:] + evt := s[:bytes.IndexByte(s, messageSeparatorByte)] + return evt } diff --git a/websocket/server.go b/websocket/server.go index f704a05ed1..28a2599215 100644 --- a/websocket/server.go +++ b/websocket/server.go @@ -1,6 +1,7 @@ package websocket import ( + "bytes" "sync" "github.com/kataras/iris/context" @@ -34,7 +35,15 @@ type ( // // To serve the built'n javascript client-side library look the `websocket.ClientHandler`. Server struct { - config Config + config Config + // ClientSource contains the javascript side code + // for the iris websocket communication + // based on the configuration's `EvtMessagePrefix`. + // + // Use a route to serve this file on a specific path, i.e + // app.Any("/iris-ws.js", func(ctx iris.Context) { ctx.Write(mywebsocketServer.ClientSource) }) + ClientSource []byte + messageSerializer *messageSerializer connections sync.Map // key = the Connection ID. rooms map[string][]string // by default a connection is joined to a room which has the connection id as its name mu sync.RWMutex // for rooms. @@ -52,9 +61,11 @@ type ( func New(cfg Config) *Server { cfg = cfg.Validate() return &Server{ - config: cfg, - connections: sync.Map{}, // ready-to-use, this is not necessary. - rooms: make(map[string][]string), + config: cfg, + ClientSource: bytes.Replace(ClientSource, []byte(DefaultEvtMessageKey), cfg.EvtMessagePrefix, -1), + messageSerializer: newMessageSerializer(cfg.EvtMessagePrefix), + connections: sync.Map{}, // ready-to-use, this is not necessary. + rooms: make(map[string][]string), onConnectionListeners: make([]ConnectionFunc, 0), upgrader: websocket.Upgrader{ HandshakeTimeout: cfg.HandshakeTimeout, @@ -352,9 +363,12 @@ func (s *Server) GetConnectionsByRoom(roomName string) []Connection { // let's keep it unexported for the best. func (s *Server) emitMessage(from, to string, data []byte) { if to != All && to != Broadcast { - if s.rooms[to] != nil { + s.mu.RLock() + room := s.rooms[to] + s.mu.RUnlock() + if room != nil { // it suppose to send the message to a specific room/or a user inside its own room - for _, connectionIDInsideRoom := range s.rooms[to] { + for _, connectionIDInsideRoom := range room { if c, ok := s.getConnection(connectionIDInsideRoom); ok { c.writeDefault(data) //send the message to the client(s) } else {