various improvements and new 'UseOnce' method - read HISTORY.md

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-08-06 03:35:58 +03:00
parent 5d480dc801
commit 46a3a99adf
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
20 changed files with 147 additions and 221 deletions

View File

@ -359,6 +359,10 @@ Response:
Other Improvements:
- `*versioning.Group` type is a full `Party` now.
- `Party.UseOnce` - either inserts a middleware, or on the basis of the middleware already existing, replace that existing middleware instead.
- Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully.
- Add [Blocks](_examples/view/template_blocks_0) template engine. <!-- Reminder for @kataras: follow https://github.com/flosch/pongo2/pull/236#issuecomment-668950566 discussion so we can get back on using the original pongo2 repository as they fixed the issue about an incompatible 3rd party package (although they need more fixes, that's why I commented there) -->
@ -527,6 +531,8 @@ New Context Methods:
Breaking Changes:
- `versioning.NewGroup(string)` now accepts a `Party` as its first input argument: `NewGroup(Party, string)`.
- `versioning.RegisterGroups` is **removed** as it is no longer necessary.
- `Configuration.RemoteAddrHeaders` from `map[string]bool` to `[]string`. If you used `With(out)RemoteAddrHeader` then you are ready to proceed without any code changes for that one.
- `ctx.Gzip(boolean)` replaced with `ctx.CompressWriter(boolean) error`.
- `ctx.GzipReader(boolean) error` replaced with `ctx.CompressReader(boolean) error`.

View File

@ -101,6 +101,7 @@
* [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)
* [Inject Engine Between Handlers](view/context-view-engine/main.go)
* [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go)
* [Write to a custom `io.Writer`](view/write-to)
* [Blocks](view/template_blocks_0)

View File

@ -31,7 +31,7 @@ func newApp() *iris.Application {
m.Handle(new(v1Controller), mvc.Version("1"), mvc.Deprecated(opts)) // 1 or 1.0, 1.0.0 ...
m.Handle(new(v2Controller), mvc.Version("2.3")) // 2.3 or 2.3.0
m.Handle(new(v3Controller), mvc.Version(">=3, <4")) // 3, 3.x, 3.x.x ...
m.Handle(new(noVersionController))
m.Handle(new(noVersionController)) // or if missing it will respond with 501 version not found.
}
return app

View File

@ -42,9 +42,16 @@ func examplePerRoute(app *iris.Application) {
// Headers[1] = Accept-Version: "2"
func examplePerParty(app *iris.Application) {
usersAPI := app.Party("/api/users")
// You can customize the way a version is extracting
// via middleware, for example:
// version url parameter, and, if it's missing we default it to "1".
usersAPI.Use(func(ctx iris.Context) {
versioning.SetVersion(ctx, ctx.URLParamDefault("version", "1"))
ctx.Next()
})
// version 1.
usersAPIV1 := versioning.NewGroup(">= 1, < 2")
usersAPIV1 := versioning.NewGroup(usersAPI, ">= 1, < 2")
usersAPIV1.Get("/", func(ctx iris.Context) {
ctx.Writef("v1 resource: /api/users handler")
})
@ -53,15 +60,13 @@ func examplePerParty(app *iris.Application) {
})
// version 2.
usersAPIV2 := versioning.NewGroup(">= 2, < 3")
usersAPIV2 := versioning.NewGroup(usersAPI, ">= 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) {

View File

@ -303,7 +303,7 @@ var acquireGoroutines = func() interface{} {
}
func (ctx *Context) Go(fn func(cancelCtx stdContext.Context)) (running int) {
g := ctx.Values().GetOrSet(goroutinesContextKey, acquireGoroutines).(*goroutines)
g := ctx.values.GetOrSet(goroutinesContextKey, acquireGoroutines).(*goroutines)
if fn != nil {
g.wg.Add(1)
@ -613,6 +613,18 @@ func (ctx *Context) StopWithError(statusCode int, err error) {
ctx.StopWithText(statusCode, err.Error())
}
// StopWithPlainError like `StopWithError` but it does NOT
// write anything to the response writer, it stores the error
// so any error handler matching the given "statusCode" can handle it by its own.
func (ctx *Context) StopWithPlainError(statusCode int, err error) {
if err == nil {
return
}
ctx.SetErr(err)
ctx.StopWithStatus(statusCode)
}
// StopWithJSON stops the handlers chain, writes the status code
// and sends a JSON response.
//
@ -4459,7 +4471,7 @@ func (ctx *Context) Exec(method string, path string) {
// backup the request path information
backupPath := req.URL.Path
backupMethod := req.Method
// don't backupValues := ctx.Values().ReadOnly()
// don't backupValues := ctx.values.ReadOnly()
// set the request to be align with the 'againstRequestPath'
req.RequestURI = path
req.URL.Path = path
@ -4548,7 +4560,7 @@ func (ctx *Context) RegisterDependency(v interface{}) {
val = reflect.ValueOf(v)
}
cv := ctx.Values().Get(DependenciesContextKey)
cv := ctx.values.Get(DependenciesContextKey)
if cv != nil {
m, ok := cv.(DependenciesMap)
if !ok {
@ -4559,7 +4571,7 @@ func (ctx *Context) RegisterDependency(v interface{}) {
return
}
ctx.Values().Set(DependenciesContextKey, DependenciesMap{
ctx.values.Set(DependenciesContextKey, DependenciesMap{
val.Type(): val,
})
}
@ -4567,7 +4579,7 @@ func (ctx *Context) RegisterDependency(v interface{}) {
// UnregisterDependency removes a dependency based on its type.
// Reports whether a dependency with that type was found and removed successfully.
func (ctx *Context) UnregisterDependency(typ reflect.Type) bool {
cv := ctx.Values().Get(DependenciesContextKey)
cv := ctx.values.Get(DependenciesContextKey)
if cv != nil {
m, ok := cv.(DependenciesMap)
if ok {
@ -4594,18 +4606,25 @@ const errorContextKey = "iris.context.error"
// as a context value, it does nothing more.
// Also, by-default this error's value is written to the client
// on failures when no registered error handler is available (see `Party.On(Any)ErrorCode`).
// See `GetError` to retrieve it back.
// See `GetErr` to retrieve it back.
//
// To remove an error simply pass nil.
//
// Note that, if you want to stop the chain
// with an error see the `StopWithError` instead.
// with an error see the `StopWithError/StopWithPlainError` instead.
func (ctx *Context) SetErr(err error) {
ctx.Values().Set(errorContextKey, err)
if err == nil {
ctx.values.Remove(errorContextKey)
return
}
ctx.values.Set(errorContextKey, err)
}
// GetErr is a helper which retrieves
// the error value stored by `SetErr`.
func (ctx *Context) GetErr() error {
if v := ctx.Values().Get(errorContextKey); v != nil {
if v := ctx.values.Get(errorContextKey); v != nil {
if err, ok := v.(error); ok {
return err
}

View File

@ -138,6 +138,8 @@ func overlapRoute(r *Route, next *Route) {
return
}
ctx.SetErr(nil) // clear any stored error.
// Set the route to the next one and execute it.
ctx.SetCurrentRoute(next.ReadOnly)
ctx.HandlerIndex(0)
ctx.Do(nextHandlers)
@ -768,6 +770,25 @@ func (api *APIBuilder) Use(handlers ...context.Handler) {
api.middleware = append(api.middleware, handlers...)
}
// UseOnce either inserts a middleware,
// or on the basis of the middleware already existing,
// replace that existing middleware instead.
func (api *APIBuilder) UseOnce(handlers ...context.Handler) {
reg:
for _, handler := range handlers {
name := context.HandlerName(handler)
for i, registeredHandler := range api.middleware {
registeredName := context.HandlerName(registeredHandler)
if name == registeredName {
api.middleware[i] = handler // replace this handler with the new one.
continue reg // break and continue to the next handler.
}
}
api.middleware = append(api.middleware, handler) // or just insert it.
}
}
// UseGlobal registers handlers that should run at the very beginning.
// It prepends those handler(s) to all routes,
// including all parties, subdomains.

View File

@ -46,7 +46,7 @@ type Attachments struct {
type DirCacheOptions struct {
// Enable or disable cache.
Enable bool
// Minimium content size for compression in bytes.
// Minimum content size for compression in bytes.
CompressMinSize int64
// Ignore compress files that match this pattern.
CompressIgnore *regexp.Regexp

View File

@ -76,9 +76,14 @@ type Party interface {
// Use appends Handler(s) to the current Party's routes and child routes.
// If the current Party is the root, then it registers the middleware to all child Parties' routes too.
Use(middleware ...context.Handler)
// UseOnce either inserts a middleware,
// or on the basis of the middleware already existing,
// replace that existing middleware instead.
UseOnce(handlers ...context.Handler)
// Done appends to the very end, Handler(s) to the current Party's routes and child routes.
// The difference from .Use is that this/or these Handler(s) are being always running last.
Done(handlers ...context.Handler)
// Reset removes all the begin and done handlers that may derived from the parent party via `Use` & `Done`,
// and the execution rules.
// Note that the `Reset` will not reset the handlers that are registered via `UseGlobal` & `DoneGlobal`.

View File

@ -25,15 +25,8 @@ import (
func Version(version string) OptionFunc {
return func(c *ControllerActivator) {
c.Router().SetRegisterRule(router.RouteOverlap) // required for this feature.
c.Use(func(ctx *context.Context) {
if !versioning.Match(ctx, version) {
ctx.StopExecution()
return
}
ctx.Next()
})
// Note: Do not use a group, we need c.Use for the specific controller's routes.
c.Use(versioning.Handler(version))
}
}

View File

@ -1,185 +1,46 @@
package versioning
import (
"net/http"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/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.
type Group struct {
router.Party
// 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
}
)
// Information not currently in-use.
version string
deprecation DeprecationOptions
}
// NewGroup returns a ptr to Group based on the given "version".
// It sets the API Version for the "r" Party.
//
// See `Handle` and `RegisterGroups` for more.
func NewGroup(version string) *Group {
func NewGroup(r router.Party, version string) *Group {
// Note that this feature alters the RouteRegisterRule to RouteOverlap
// the RouteOverlap rule does not contain any performance downside
// but it's good to know that if you registered other mode, this wanna change it.
r.SetRegisterRule(router.RouteOverlap)
r.UseOnce(Handler(version)) // this is required in order to not populate this middleware to the next group.
return &Group{
Party: r,
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.
// store it for future use, e.g. collect all deprecated APIs and notify the developer.
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},
g.Party.UseOnce(func(ctx *context.Context) {
WriteDeprecated(ctx, options)
ctx.Next()
})
}
// 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
return g
}

View File

@ -1,6 +1,7 @@
package versioning
import (
"errors"
"strings"
"github.com/kataras/iris/v12/context"
@ -31,6 +32,10 @@ const (
NotFound = "iris.api.version.notfound"
)
// ErrNotFound reports whether a requested version
// does not match with any of the server's implemented ones.
var ErrNotFound = errors.New("version not found")
// 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) {
@ -46,8 +51,7 @@ var NotFoundHandler = func(ctx *context.Context) {
recognize the request method and is not capable of supporting it for any resource.
*/
ctx.StatusCode(501)
ctx.WriteString("version not found")
ctx.StopWithPlainError(501, ErrNotFound)
}
// GetVersion returns the current request version.

View File

@ -44,6 +44,22 @@ func Match(ctx *context.Context, expectedVersion string) bool {
return true
}
// Handler returns a handler which stop the execution
// when the given "version" does not match with the requested one.
func Handler(version string) context.Handler {
return func(ctx *context.Context) {
if !Match(ctx, version) {
// Any overlapped handler
// can just clear the status code
// and the error to ignore this (see `NewGroup`).
NotFoundHandler(ctx)
return
}
ctx.Next()
}
}
// 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

View File

@ -79,7 +79,7 @@ func TestNewGroup(t *testing.T) {
userAPI := app.Party("/api/user")
// [... static serving, middlewares and etc goes here].
userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)
userAPIV10 := versioning.NewGroup(userAPI, "1.0").Deprecated(versioning.DefaultDeprecationOptions)
// V10middlewareResponse := "m1"
// userAPIV10.Use(func(ctx iris.Context) {
// println("exec userAPIV10.Use - midl1")
@ -97,7 +97,7 @@ func TestNewGroup(t *testing.T) {
// })
userAPIV10.Get("/", sendHandler(v10Response))
userAPIV2 := versioning.NewGroup(">= 2, < 3")
userAPIV2 := versioning.NewGroup(userAPI, ">= 2, < 3")
// V2middlewareResponse := "m2"
// userAPIV2.Use(func(ctx iris.Context) {
// println("exec userAPIV2.Use - midl1")
@ -113,8 +113,6 @@ func TestNewGroup(t *testing.T) {
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()

View File

@ -17,25 +17,7 @@ Parse using embedded assets, Layouts and Party-specific layout, Template Funcs,
| 7 | Jet | [CloudyKit/jet](https://github.com/CloudyKit/jet) |
| 8 | Ace | [yosssi/ace](https://github.com/yosssi/ace) |
## Examples
- [Overview](https://github.com/kataras/iris/blob/master/_examples/view/overview/main.go)
- [Hi](https://github.com/kataras/iris/blob/master/_examples/view/template_html_0/main.go)
- [A simple Layout](https://github.com/kataras/iris/blob/master/_examples/view/template_html_1/main.go)
- [Layouts: `yield` and `render` tmpl funcs](https://github.com/kataras/iris/blob/master/_examples/view/template_html_2/main.go)
- [The `urlpath` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_3/main.go)
- [The `url` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_4/main.go)
- [Inject Data Between Handlers](https://github.com/kataras/iris/blob/master/_examples/view/context-view-data/main.go)
- [Embedding Templates Into App Executable File](https://github.com/kataras/iris/blob/master/_examples/view/embedding-templates-into-app/main.go)
- [Blocks](https://github.com/kataras/iris/blob/master/_examples/view/template_blocks_0)
- [Blocks Embedded](https://github.com/kataras/iris/blob/master/_examples/view/template_blocks_1_embedded)
- [Greeting with Pug (Jade)`](view/template_pug_0)
- [Pug (Jade) Actions`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_1)
- [Pug (Jade) Includes`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_2)
- [Pug (Jade) Extends`](https://github.com/kataras/iris/blob/master/_examples/view/template_pug_3)
- [Jet](https://github.com/kataras/iris/blob/master/_examples/view/template_jet_0)
- [Jet Embedded](https://github.com/kataras/iris/blob/master/_examples/view/template_jet_1_embedded)
- [Ace](https://github.com/kataras/iris/blob/master/_examples/view/template_ace_0)
[List of Examples](https://github.com/kataras/iris/tree/master/_examples/view).
You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `Context.ResponseWriter`, take a look at the [iris/_examples/view/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/view/quicktemplate) example.

View File

@ -26,7 +26,10 @@ type AmberEngine struct {
templateCache map[string]*template.Template
}
var _ Engine = (*AmberEngine)(nil)
var (
_ Engine = (*AmberEngine)(nil)
_ EngineFuncer = (*AmberEngine)(nil)
)
// Amber creates and returns a new amber view engine.
// The given "extension" MUST begin with a dot.

View File

@ -24,7 +24,10 @@ type BlocksEngine struct {
Engine *blocks.Blocks
}
var _ Engine = (*BlocksEngine)(nil)
var (
_ Engine = (*BlocksEngine)(nil)
_ EngineFuncer = (*BlocksEngine)(nil)
)
// WrapBlocks wraps an initialized blocks engine and returns its Iris adapter.
// See `Blocks` package-level function too.
@ -53,9 +56,8 @@ func (s *BlocksEngine) Ext() string {
// - url func(routeName string, args ...string) string
// - urlpath func(routeName string, args ...string) string
// - tr func(lang, key string, args ...interface{}) string
func (s *BlocksEngine) AddFunc(funcName string, funcBody interface{}) *BlocksEngine {
func (s *BlocksEngine) AddFunc(funcName string, funcBody interface{}) {
s.Engine.Funcs(template.FuncMap{funcName: funcBody})
return s
}
// AddLayoutFunc adds a template function for templates that are marked as layouts.

View File

@ -106,7 +106,10 @@ type DjangoEngine struct {
templateCache map[string]*pongo2.Template
}
var _ Engine = (*DjangoEngine)(nil)
var (
_ Engine = (*DjangoEngine)(nil)
_ EngineFuncer = (*DjangoEngine)(nil)
)
// Django creates and returns a new django view engine.
// The given "extension" MUST begin with a dot.

View File

@ -27,7 +27,10 @@ type HandlebarsEngine struct {
templateCache map[string]*raymond.Template
}
var _ Engine = (*HandlebarsEngine)(nil)
var (
_ Engine = (*HandlebarsEngine)(nil)
_ EngineFuncer = (*HandlebarsEngine)(nil)
)
// Handlebars creates and returns a new handlebars view engine.
// The given "extension" MUST begin with a dot.

View File

@ -35,7 +35,10 @@ type HTMLEngine struct {
//
}
var _ Engine = (*HTMLEngine)(nil)
var (
_ Engine = (*HTMLEngine)(nil)
_ EngineFuncer = (*HTMLEngine)(nil)
)
var emptyFuncs = template.FuncMap{
"yield": func(binding interface{}) (string, error) {
@ -175,12 +178,10 @@ func (s *HTMLEngine) AddLayoutFunc(funcName string, funcBody interface{}) *HTMLE
// - urlpath func(routeName string, args ...string) string
// - render func(fullPartialName string) (template.HTML, error).
// - tr func(lang, key string, args ...interface{}) string
func (s *HTMLEngine) AddFunc(funcName string, funcBody interface{}) *HTMLEngine {
func (s *HTMLEngine) AddFunc(funcName string, funcBody interface{}) {
s.rmu.Lock()
s.funcs[funcName] = funcBody
s.rmu.Unlock()
return s
}
// SetFuncs overrides the template funcs with the given "funcMap".

View File

@ -36,7 +36,10 @@ type JetEngine struct {
jetDataContextKey string
}
var _ Engine = (*JetEngine)(nil)
var (
_ Engine = (*JetEngine)(nil)
_ EngineFuncer = (*JetEngine)(nil)
)
// jet library does not export or give us any option to modify them via Set
// (unless we parse the files by ourselves but this is not a smart choice).