MVC improvements: add HandleWebsocket that now registers events automatically based on the struct's methods(!) and fix a bug when more than one value of the same type is registered to a static field of a controller

Former-commit-id: e369d1426ac1a6b58314930a18362670317da3c1
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-07-09 12:16:19 +03:00
parent 85666da682
commit 450f20902d
18 changed files with 383 additions and 183 deletions

View File

@ -363,9 +363,9 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her
- [Basic Authentication](authentication/basicauth/main.go) - [Basic Authentication](authentication/basicauth/main.go)
- [OAUth2](authentication/oauth2/main.go) - [OAUth2](authentication/oauth2/main.go)
- [JWT](experimental-handlers/jwt/main.go) - [Request Auth](request/main.go) **NEW**
- [Request Auth(JWT)](experimental-handlers/jwt/main.go)
- [Sessions](#sessions) - [Sessions](#sessions)
- [Request Authentication](authentication/request/main.go) **NEW**
### File Server ### File Server

View File

@ -2,5 +2,6 @@
- [Basic Authentication](basicauth/main.go) - [Basic Authentication](basicauth/main.go)
- [OAUth2](oauth2/main.go) - [OAUth2](oauth2/main.go)
- [JWT](https://github.com/kataras/iris/blob/master/_examples/experimental-handlers/jwt/main.go) - [Request Auth](request/main.go)
- [Request Auth(JWT)](https://github.com/kataras/iris/blob/master/_examples/experimental-handlers/jwt/main.go)
- [Sessions](https://github.com/kataras/iris/tree/master/_examples/#sessions) - [Sessions](https://github.com/kataras/iris/tree/master/_examples/#sessions)

View File

@ -134,3 +134,5 @@ func main() {
// You can read more examples and run testable code // You can read more examples and run testable code
// at the `iris/crypto`, `iris/crypto/sign` // at the `iris/crypto`, `iris/crypto/sign`
// and `iris/crypto/gcm` packages themselves. // and `iris/crypto/gcm` packages themselves.
//
// See experimental-handlers/jwt example to use JWT for request authentication instead.

View File

@ -23,7 +23,7 @@ func myHandler(ctx iris.Context) {
ctx.Writef("This is an authenticated request\n") ctx.Writef("This is an authenticated request\n")
ctx.Writef("Claim content:\n") ctx.Writef("Claim content:\n")
ctx.Writef("%s", user.Signature) ctx.Writef("%#+v\n", user.Claims)
} }
func main() { func main() {
@ -42,5 +42,12 @@ func main() {
app.Use(jwtHandler.Serve) app.Use(jwtHandler.Serve)
app.Get("/ping", myHandler) app.Get("/ping", myHandler)
// Example request:
// curl -X GET -H\
// "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjozMjEzMjF9.8waEX7-vPKACa-Soi1pQvW3Rl8QY-SUFcHKTLZI4mvU"\
// http://localhost:3001/ping
//
//Read more at: https://jwt.io/
app.Run(iris.Addr("localhost:3001")) app.Run(iris.Addr("localhost:3001"))
} // don't forget to look ../jwt_test.go to see how to set your own custom claims }

View File

@ -0,0 +1,106 @@
<html>
<head>
<title>Online visitors MVC example</title>
<style>
body {
margin: 0;
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
color: #212121;
font-size: 1.0em;
line-height: 1.6;
}
.container {
max-width: 750px;
margin: auto;
padding: 15px;
}
#online_visitors {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<span id="online_visitors">1 online visitor</span>
</div>
<!-- the message's input -->
<input id="input" type="text" />
<!-- when clicked then a websocket event will be sent to the server, at this example we registered the 'chat' -->
<button id="sendBtn" disabled>Send</button>
<!-- the messages will be shown here -->
<pre id="output"></pre>
<!-- import the iris client-side library for browser from a CDN or locally.
However, `neffos.(min.)js` is a NPM package too so alternatively,
you can use it as dependency on your package.json and all nodejs-npm tooling become available:
see the "browserify" example for more-->
<script src="https://cdn.jsdelivr.net/npm/neffos.js@latest/dist/neffos.min.js"></script>
<script type="text/javascript">
const wsURL = "ws://localhost:8080/websocket"
var outputTxt = document.getElementById("output");
function addMessage(msg) {
outputTxt.innerHTML += msg + "\n";
}
async function runExample() {
try {
const conn = await neffos.dial(wsURL, {
default: { // "default" namespace.
_OnNamespaceConnected: function (nsConn, msg) {
if (nsConn.conn.wasReconnected()) {
addMessage("re-connected after " + nsConn.conn.reconnectTries.toString() + " trie(s)");
}
let inputTxt = document.getElementById("input");
let sendBtn = document.getElementById("sendBtn");
sendBtn.disabled = false;
sendBtn.onclick = function () {
const input = inputTxt.value;
inputTxt.value = "";
nsConn.emit("OnChat", input);
addMessage("Me: " + input);
};
addMessage("connected to namespace: " + msg.Namespace);
},
_OnNamespaceDisconnect: function (nsConn, msg) {
addMessage("disconnected from namespace: " + msg.Namespace);
},
OnChat: function (nsConn, msg) { // "OnChat" event.
console.log(msg);
addMessage(msg.Body);
},
OnVisit: function (nsConn, msg) {
const newCount = Number(msg.Body); // or parseInt.
console.log("visit websocket event with newCount of: ", newCount);
var text = "1 online visitor";
if (newCount > 1) {
text = newCount + " online visitors";
}
document.getElementById("online_visitors").innerHTML = text;
},
}
});
conn.connect("default");
} catch (err) {
console.log(err)
}
}
runExample();
</script>
</body>
</html>

View File

@ -1,41 +1,45 @@
package main package main
import ( import (
"fmt"
"sync/atomic" "sync/atomic"
"github.com/kataras/iris" "github.com/kataras/iris"
"github.com/kataras/iris/mvc" "github.com/kataras/iris/mvc"
"github.com/kataras/iris/websocket" "github.com/kataras/iris/websocket"
"github.com/kataras/neffos"
) )
func main() { func main() {
app := iris.New() app := iris.New()
app.Logger().SetLevel("debug")
// optionally enable debug messages to the neffos real-time framework
// and print them through the iris' logger.
neffos.EnableDebug(app.Logger())
// load templates. // load templates.
app.RegisterView(iris.HTML("./views", ".html")) app.RegisterView(iris.HTML("./views", ".html"))
// render the ./views/index.html. // render the ./browser/index.html.
app.Get("/", func(ctx iris.Context) { app.HandleDir("/", "./browser")
ctx.View("index.html")
})
mvc.Configure(app.Party("/websocket"), configureMVC) websocketAPI := app.Party("/websocket")
// Or mvc.New(app.Party(...)).Configure(configureMVC)
m := mvc.New(websocketAPI)
m.Register(
&prefixedLogger{prefix: "DEV"},
)
m.HandleWebsocket(&websocketController{Namespace: "default", Age: 42, Otherstring: "other string"})
websocketServer := neffos.New(websocket.DefaultGorillaUpgrader, m)
websocketAPI.Get("/", websocket.Handler(websocketServer))
// http://localhost:8080 // http://localhost:8080
app.Run(iris.Addr(":8080")) app.Run(iris.Addr(":8080"))
} }
func configureMVC(m *mvc.Application) {
ws := websocket.New(websocket.Config{})
// http://localhost:8080/websocket/iris-ws.js
m.Router.Any("/iris-ws.js", websocket.ClientHandler())
// This will bind the result of ws.Upgrade which is a websocket.Connection
// to the controller(s) served by the `m.Handle`.
m.Register(ws.Upgrade)
m.Handle(new(websocketController))
}
var visits uint64 var visits uint64
func increment() uint64 { func increment() uint64 {
@ -47,36 +51,74 @@ func decrement() uint64 {
} }
type websocketController struct { type websocketController struct {
// Note that you could use an anonymous field as well, it doesn't matter, binder will find it. *neffos.NSConn `stateless:"true"`
// Namespace string
// This is the current websocket connection, each client has its own instance of the *websocketController. Age int
Conn websocket.Connection Otherstring string
Logger LoggerService
} }
func (c *websocketController) onLeave(roomName string) { // or
// func (c *websocketController) Namespace() string {
// return "default"
// }
func (c *websocketController) OnNamespaceDisconnect(msg neffos.Message) error {
c.Logger.Log("Disconnected")
// visits-- // visits--
newCount := decrement() newCount := decrement()
// This will call the "visit" event on all clients, except the current one, // This will call the "OnVisit" event on all clients, except the current one,
// (it can't because it's left but for any case use this type of design) // (it can't because it's left but for any case use this type of design)
c.Conn.To(websocket.Broadcast).Emit("visit", newCount) c.Conn.Server().Broadcast(nil, neffos.Message{
Namespace: msg.Namespace,
Event: "OnVisit",
Body: []byte(fmt.Sprintf("%d", newCount)),
})
return nil
} }
func (c *websocketController) update() { func (c *websocketController) OnNamespaceConnected(msg neffos.Message) error {
// println("Broadcast prefix is: " + c.BroadcastPrefix)
c.Logger.Log("Connected")
// visits++ // visits++
newCount := increment() newCount := increment()
// This will call the "visit" event on all clients, including the current // This will call the "OnVisit" event on all clients, including the current
// with the 'newCount' variable. // with the 'newCount' variable.
// //
// There are many ways that u can do it and faster, for example u can just send a new visitor // There are many ways that u can do it and faster, for example u can just send a new visitor
// and client can increment itself, but here we are just "showcasing" the websocket controller. // and client can increment itself, but here we are just "showcasing" the websocket controller.
c.Conn.To(websocket.All).Emit("visit", newCount) c.Conn.Server().Broadcast(c, neffos.Message{
Namespace: msg.Namespace,
Event: "OnVisit",
Body: []byte(fmt.Sprintf("%d", newCount)),
})
return nil
} }
func (c *websocketController) Get( /* websocket.Connection could be lived here as well, it doesn't matter */ ) { func (c *websocketController) OnChat(msg neffos.Message) error {
c.Conn.OnLeave(c.onLeave) ctx := websocket.GetContext(c.Conn)
c.Conn.On("visit", c.update)
// call it after all event callbacks registration. ctx.Application().Logger().Infof("[IP: %s] [ID: %s] broadcast to other clients the message [%s]",
c.Conn.Wait() ctx.RemoteAddr(), c, string(msg.Body))
c.Conn.Server().Broadcast(c, msg)
return nil
}
type LoggerService interface {
Log(string)
}
type prefixedLogger struct {
prefix string
}
func (s *prefixedLogger) Log(msg string) {
fmt.Printf("%s: %s\n", s.prefix, msg)
} }

View File

@ -1,63 +0,0 @@
<html>
<head>
<title>Online visitors MVC example</title>
<style>
body {
margin: 0;
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
color: #212121;
font-size: 1.0em;
line-height: 1.6;
}
.container {
max-width: 750px;
margin: auto;
padding: 15px;
}
#online_visitors {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<span id="online_visitors">1 online visitor</span>
</div>
<script src="/websocket/iris-ws.js"></script>
<script type="text/javascript">
(function () {
var socket = new Ws("ws://localhost:8080/websocket");
socket.OnConnect(function(){
// update the rest of connected clients, including "myself" when "my" connection is 100% ready.
socket.Emit("visit");
});
socket.On("visit", function (newCount) {
console.log("visit websocket event with newCount of: ", newCount);
var text = "1 online visitor";
if (newCount > 1) {
text = newCount + " online visitors";
}
document.getElementById("online_visitors").innerHTML = text;
});
socket.OnDisconnect(function () {
document.getElementById("online_visitors").innerHTML = "you've been disconnected";
});
})();
</script>
</body>
</html>

View File

@ -68,7 +68,7 @@ func main() {
// serves the endpoint of ws://localhost:8080/echo // serves the endpoint of ws://localhost:8080/echo
websocketRoute := app.Get("/echo", websocket.Handler(websocketServer)) websocketRoute := app.Get("/echo", websocket.Handler(websocketServer))
j.Get()
if enableJWT { if enableJWT {
// Register the jwt middleware (on handshake): // Register the jwt middleware (on handshake):
websocketRoute.Use(j.Serve) websocketRoute.Use(j.Serve)
@ -76,20 +76,25 @@ func main() {
// //
// Check for token through the jwt middleware // Check for token through the jwt middleware
// on websocket connection or on any event: // on websocket connection or on any event:
/* /* websocketServer.OnConnect = func(c *neffos.Conn) error {
websocketServer.OnConnect = func(c *neffos.Conn) error { ctx := websocket.GetContext(c)
ctx := websocket.GetContext(c) if err := j.CheckJWT(ctx); err != nil {
if err := j.CheckJWT(ctx); err != nil { // will send the above error on the client
// will send the above error on the client // and will not allow it to connect to the websocket server at all.
// and will not allow it to connect to the websocket server at all. return err
return err }
}
log.Printf("[%s] connected to the server", c.ID()) user := ctx.Values().Get("jwt").(*jwt.Token)
// or just: user := j.Get(ctx)
return nil log.Printf("This is an authenticated request\n")
} log.Printf("Claim content:")
*/ log.Printf("%#+v\n", user.Claims)
log.Printf("[%s] connected to the server", c.ID())
return nil
} */
} }
// serves the browser-based websocket client. // serves the browser-based websocket client.

View File

@ -401,7 +401,7 @@ type Configuration struct {
// DisablePathCorrectionRedirection works whenever configuration.DisablePathCorrection is set to false // 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 // 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. // the trailing slash ("/") instead of send a redirection status.
// //
// Defaults to false. // Defaults to false.
DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"` DisablePathCorrectionRedirection bool `json:"disablePathCorrectionRedirection,omitempty" yaml:"DisablePathCorrectionRedirection" toml:"DisablePathCorrectionRedirection"`

2
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/iris-contrib/go.uuid v2.0.0+incompatible github.com/iris-contrib/go.uuid v2.0.0+incompatible
github.com/json-iterator/go v1.1.6 // vendor removed. github.com/json-iterator/go v1.1.6 // vendor removed.
github.com/kataras/golog v0.0.0-20180321173939-03be10146386 github.com/kataras/golog v0.0.0-20180321173939-03be10146386
github.com/kataras/neffos v0.0.3 github.com/kataras/neffos v0.0.5
github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect github.com/kataras/pio v0.0.0-20190103105442-ea782b38602d // indirect
github.com/microcosm-cc/bluemonday v1.0.2 github.com/microcosm-cc/bluemonday v1.0.2
github.com/ryanuber/columnize v2.1.0+incompatible github.com/ryanuber/columnize v2.1.0+incompatible

View File

@ -1,6 +1,8 @@
package di package di
import "reflect" import (
"reflect"
)
// EmptyIn is just an empty slice of reflect.Value. // EmptyIn is just an empty slice of reflect.Value.
var EmptyIn = []reflect.Value{} var EmptyIn = []reflect.Value{}
@ -164,6 +166,13 @@ func structFieldIgnored(f reflect.StructField) bool {
return s == "true" // if has an ignore tag then ignore it. return s == "true" // if has an ignore tag then ignore it.
} }
// for controller's fields only. Explicit set a stateless to a field
// in order to make the controller a Stateless one even if no other dynamic dependencies exist.
func structFieldStateless(f reflect.StructField) bool {
s := f.Tag.Get("stateless")
return s == "true"
}
type field struct { type field struct {
Type reflect.Type Type reflect.Type
Name string // the actual name. Name string // the actual name.
@ -190,7 +199,7 @@ func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int)
for i, n := 0, elemTyp.NumField(); i < n; i++ { for i, n := 0, elemTyp.NumField(); i < n; i++ {
f := elemTyp.Field(i) f := elemTyp.Field(i)
if IndirectType(f.Type).Kind() == reflect.Struct && if IndirectType(f.Type).Kind() == reflect.Struct &&
!structFieldIgnored(f) { !structFieldIgnored(f) && !structFieldStateless(f) {
fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...) fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...)
continue continue
} }
@ -230,7 +239,8 @@ func LookupNonZeroFieldsValues(v reflect.Value, skipUnexported bool) (bindValues
for _, f := range fields { for _, f := range fields {
if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/ if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/
!IsZero(fieldVal) { goodVal(fieldVal) && !IsZero(fieldVal) {
// fmt.Printf("[%d][field index = %d] append to bindValues: %s = %s\n", i, f.Index[0], f.Name, fieldVal.String())
bindValues = append(bindValues, fieldVal) bindValues = append(bindValues, fieldVal)
} }
} }

View File

@ -20,6 +20,22 @@ const (
Singleton Singleton
) )
// read-only on runtime.
var scopeNames = map[Scope]string{
Stateless: "Stateless",
Singleton: "Singleton",
}
// Return "Stateless" for 0 or "Singleton" for 1.
func (scope Scope) String() string {
name, ok := scopeNames[scope]
if !ok {
return "Unknown"
}
return name
}
type ( type (
targetStructField struct { targetStructField struct {
Object *BindObject Object *BindObject
@ -65,6 +81,20 @@ func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker,
elemType: IndirectType(v.Type()), elemType: IndirectType(v.Type()),
} }
// Optionally check and keep good values only here,
// but not required because they are already checked by users of this function.
//
// for i, v := range values {
// if !goodVal(v) || IsZero(v) {
// if last := len(values) - 1; last > i {
// values = append(values[:i], values[i+1:]...)
// } else {
// values = values[0:last]
// }
// }
// }
visited := make(map[int]struct{}, 0) // add a visited to not add twice a single value (09-Jul-2019).
fields := lookupFields(s.elemType, true, nil) fields := lookupFields(s.elemType, true, nil)
for _, f := range fields { for _, f := range fields {
if hijack != nil { if hijack != nil {
@ -78,15 +108,19 @@ func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker,
} }
} }
for _, val := range values { for idx, val := range values {
if _, alreadySet := visited[idx]; alreadySet {
continue
}
// the binded values to the struct's fields. // the binded values to the struct's fields.
b, err := MakeBindObject(val, goodFunc) b, err := MakeBindObject(val, goodFunc)
if err != nil { if err != nil {
return s // if error stop here. return s // if error stop here.
} }
if b.IsAssignable(f.Type) { if b.IsAssignable(f.Type) {
visited[idx] = struct{}{}
// fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String()) // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String())
s.fields = append(s.fields, &targetStructField{ s.fields = append(s.fields, &targetStructField{
FieldIndex: f.Index, FieldIndex: f.Index,
@ -126,13 +160,14 @@ func (s *StructInjector) setState() {
// if so then set the temp staticBindingsFieldsLength to that number, so for example: // if so then set the temp staticBindingsFieldsLength to that number, so for example:
// if static binding length is 0 // if static binding length is 0
// but an unexported field is set-ed then act that as singleton. // but an unexported field is set-ed then act that as singleton.
if allStructFieldsLength > staticBindingsFieldsLength { if allStructFieldsLength > staticBindingsFieldsLength {
structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false) structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false)
staticBindingsFieldsLength = len(structFieldsUnexportedNonZero) staticBindingsFieldsLength = len(structFieldsUnexportedNonZero)
} }
// println("staticBindingsFieldsLength: ", staticBindingsFieldsLength)
// println("allStructFieldsLength: ", allStructFieldsLength) // println("allStructFieldsLength: ", allStructFieldsLength)
// println("staticBindingsFieldsLength: ", staticBindingsFieldsLength)
// if the number of static values binded is equal to the // if the number of static values binded is equal to the
// total struct's fields(including unexported fields this time) then set as singleton. // total struct's fields(including unexported fields this time) then set as singleton.
@ -169,9 +204,23 @@ func (s *StructInjector) fillStruct() {
func (s *StructInjector) String() (trace string) { func (s *StructInjector) String() (trace string) {
for i, f := range s.fields { for i, f := range s.fields {
elemField := s.elemType.FieldByIndex(f.FieldIndex) elemField := s.elemType.FieldByIndex(f.FieldIndex)
trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n",
i+1, bindTypeString(f.Object.BindType), f.Object.Type.String(), format := "\t[%d] %s binding: %#+v for field '%s %s'"
elemField.Name, elemField.Type.String()) if len(s.fields) > i+1 {
format += "\n"
}
if !f.Object.Value.IsValid() {
continue // probably a Context.
}
valuePresent := f.Object.Value.Interface()
if f.Object.BindType == Dynamic {
valuePresent = f.Object.Type.String()
}
trace += fmt.Sprintf(format, i+1, bindTypeString(f.Object.BindType), valuePresent, elemField.Name, elemField.Type.String())
} }
return return

View File

@ -29,7 +29,22 @@ func (bv Values) CloneWithFieldsOf(s interface{}) Values {
// add the manual filled fields to the dependencies. // add the manual filled fields to the dependencies.
filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true) filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true)
for i, filled := range filledFieldValues {
for _, v := range values {
// do NOT keep duplicate equal values (09-Jul-2019).
if reflect.DeepEqual(v, filled) {
if last := len(filledFieldValues) - 1; last > i {
filledFieldValues = append(filledFieldValues[:i], filledFieldValues[i+1:]...)
} else {
filledFieldValues = filledFieldValues[0:last]
}
break
}
}
}
values = append(values, filledFieldValues...) values = append(values, filledFieldValues...)
return values return values
} }

View File

@ -87,8 +87,11 @@ type ControllerActivator struct {
errorHandler hero.ErrorHandler errorHandler hero.ErrorHandler
// initialized on the first `Handle`. // initialized on the first `Handle` or immediately when "servesWebsocket" is true.
injector *di.StructInjector injector *di.StructInjector
// true if this controller listens and serves to websocket events.
servesWebsocket bool
} }
// NameOf returns the package name + the struct type's name, // NameOf returns the package name + the struct type's name,
@ -145,6 +148,11 @@ func whatReservedMethods(typ reflect.Type) map[string]*router.Route {
return routes return routes
} }
func (c *ControllerActivator) markAsWebsocket() {
c.servesWebsocket = true
c.attachInjector()
}
// Dependencies returns the write and read access of the dependencies that are // Dependencies returns the write and read access of the dependencies that are
// came from the parent MVC Application, with this you can customize // came from the parent MVC Application, with this you can customize
// the dependencies per controller, used at the `BeforeActivation`. // the dependencies per controller, used at the `BeforeActivation`.
@ -325,6 +333,21 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
var emptyIn = []reflect.Value{} var emptyIn = []reflect.Value{}
func (c *ControllerActivator) attachInjector() {
if c.injector == nil {
c.injector = di.Struct(c.Value, c.dependencies...)
if !c.servesWebsocket {
golog.Debugf("MVC Controller [%s] [Scope=%s]", c.fullName, c.injector.Scope)
} else {
golog.Debugf("MVC Websocket Controller [%s]", c.fullName)
}
if c.injector.Has {
golog.Debugf("Dependencies:\n%s", c.injector.String())
}
}
}
func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler { func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler {
// Remember: // Remember:
// The `Handle->handlerOf` can be called from `BeforeActivation` event // The `Handle->handlerOf` can be called from `BeforeActivation` event
@ -333,12 +356,7 @@ func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []ref
// To solve this we're doing a check on the FIRST `Handle`, // To solve this we're doing a check on the FIRST `Handle`,
// if c.injector is nil, then set it with the current bindings, // if c.injector is nil, then set it with the current bindings,
// these bindings can change after, so first add dependencies and after register routes. // these bindings can change after, so first add dependencies and after register routes.
if c.injector == nil { c.attachInjector()
c.injector = di.Struct(c.Value, c.dependencies...)
if c.injector.Has {
golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String())
}
}
// fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies)

View File

@ -1,12 +1,14 @@
package mvc package mvc
import ( import (
"reflect"
"strings" "strings"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/core/router" "github.com/kataras/iris/core/router"
"github.com/kataras/iris/hero" "github.com/kataras/iris/hero"
"github.com/kataras/iris/hero/di" "github.com/kataras/iris/hero/di"
"github.com/kataras/iris/websocket"
"github.com/kataras/golog" "github.com/kataras/golog"
) )
@ -172,6 +174,52 @@ Set the Logger's Level to "debug" to view the active dependencies per controller
// //
// Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc // Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc
func (app *Application) Handle(controller interface{}) *Application { func (app *Application) Handle(controller interface{}) *Application {
app.handle(controller)
return app
}
// HandleWebsocket handles a websocket specific controller.
// Its exported methods are the events.
// If a "Namespace" field or method exists then namespace is set, otherwise empty namespace.
// Note that a websocket controller is registered and ran under a specific connection connected to a namespace
// and it cannot send HTTP responses on that state.
// However all static and dynamic dependency injection features are working, as expected, like any regular MVC Controller.
func (app *Application) HandleWebsocket(controller interface{}) {
c := app.handle(controller)
c.markAsWebsocket()
}
var _ websocket.ConnHandler = (*Application)(nil)
// GetNamespaces completes the websocket ConnHandler interface.
// It returns a collection of namespace and events that
// were registered through `HandleWebsocket` controllers.
func (app *Application) GetNamespaces() websocket.Namespaces {
makeInjector := func(injector *di.StructInjector) websocket.StructInjector {
return func(_ reflect.Type, nsConn *websocket.NSConn) reflect.Value {
v := injector.Acquire()
if injector.CanInject {
injector.InjectElem(v.Elem(), reflect.ValueOf(websocket.GetContext(nsConn.Conn)))
}
return v
}
}
var websocketControllers []websocket.ConnHandler
for _, c := range app.Controllers {
if c.servesWebsocket {
wsInjector := makeInjector(c.injector)
s := websocket.NewStruct(c.Value).SetInjector(wsInjector)
websocketControllers = append(websocketControllers, s)
}
}
return websocket.JoinConnHandlers(websocketControllers...).GetNamespaces()
}
func (app *Application) handle(controller interface{}) *ControllerActivator {
// initialize the controller's activator, nothing too magical so far. // initialize the controller's activator, nothing too magical so far.
c := newControllerActivator(app.Router, controller, app.Dependencies, app.ErrorHandler) c := newControllerActivator(app.Router, controller, app.Dependencies, app.ErrorHandler)
@ -193,7 +241,7 @@ func (app *Application) Handle(controller interface{}) *Application {
} }
app.Controllers = append(app.Controllers, c) app.Controllers = append(app.Controllers, c)
return app return c
} }
// HandleError registers a `hero.ErrorHandlerFunc` which will be fired when // HandleError registers a `hero.ErrorHandlerFunc` which will be fired when

View File

@ -1,48 +0,0 @@
package mvc
import (
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
)
var defaultSessionManager = sessions.New(sessions.Config{})
// SessionController is a simple `Controller` implementation
// which requires a binded session manager in order to give
// direct access to the current client's session via its `Session` field.
//
// SessionController is deprecated please use the new dependency injection's methods instead,
// i.e `mvcApp.Register(sessions.New(sessions.Config{}).Start)`.
// It's more controlled by you,
// also *sessions.Session type can now `Destroy` itself without the need of the manager, embrace it.
type SessionController struct {
Manager *sessions.Sessions
Session *sessions.Session
}
// BeforeActivation called, once per application lifecycle NOT request,
// every single time the dev registers a specific SessionController-based controller.
// It makes sure that its "Manager" field is filled
// even if the caller didn't provide any sessions manager via the MVC's Application's `Handle` function.
func (s *SessionController) BeforeActivation(b BeforeActivation) {
if didntBindManually := b.Dependencies().AddOnce(defaultSessionManager); didntBindManually {
b.Router().GetReporter().Add(
`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
therefore this controller is using the default sessions manager instead.
Please refer to the documentation to learn how you can provide the session manager`)
}
}
// BeginRequest initializes the current user's Session.
func (s *SessionController) BeginRequest(ctx context.Context) {
if s.Manager == nil {
ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug
because the SessionController should predict this on its activation state and use a default one automatically`)
return
}
s.Session = s.Manager.Start(ctx)
}
// EndRequest is here to complete the `BaseController`.
func (s *SessionController) EndRequest(ctx context.Context) {}

View File

@ -53,13 +53,15 @@ var (
// //
// See examples for more. // See examples for more.
Dial = neffos.Dial Dial = neffos.Dial
// IsTryingToReconnect reports whether the returning "err" from the `Server#Upgrade` // IsTryingToReconnect reports whether the returning "err" from the `Server#Upgrade`
// is from a client that was trying to reconnect to the websocket server. // is from a client that was trying to reconnect to the websocket server.
// //
// Look the `Conn#WasReconnected` and `Conn#ReconnectTries` too. // Look the `Conn#WasReconnected` and `Conn#ReconnectTries` too.
IsTryingToReconnect = neffos.IsTryingToReconnect IsTryingToReconnect = neffos.IsTryingToReconnect
// NewStruct returns the `Struct` Conn Handler based on ptr value.
NewStruct = neffos.NewStruct
// JoinConnHandlers combines two or more ConnHandlers as one.
JoinConnHandlers = neffos.JoinConnHandlers
// OnNamespaceConnect is the event name which its callback is fired right before namespace connect, // OnNamespaceConnect is the event name which its callback is fired right before namespace connect,
// if non-nil error then the remote connection's `Conn.Connect` will fail and send that error text. // if non-nil error then the remote connection's `Conn.Connect` will fail and send that error text.
// Connection is not ready to emit data to the namespace. // Connection is not ready to emit data to the namespace.

View File

@ -39,7 +39,8 @@ type (
// i.e on `OnNamespaceConnect` it will abort a remote namespace connection. // i.e on `OnNamespaceConnect` it will abort a remote namespace connection.
// See examples for more. // See examples for more.
MessageHandlerFunc = neffos.MessageHandlerFunc MessageHandlerFunc = neffos.MessageHandlerFunc
// ConnHandler is the interface which namespaces and events can be retrieved through.
ConnHandler = neffos.ConnHandler
// Events completes the `ConnHandler` interface. // Events completes the `ConnHandler` interface.
// It is a map which its key is the event name // It is a map which its key is the event name
// and its value the event's callback. // and its value the event's callback.
@ -63,6 +64,11 @@ type (
// //
// See `New` and `Dial`. // See `New` and `Dial`.
WithTimeout = neffos.WithTimeout WithTimeout = neffos.WithTimeout
// Struct completes the `ConnHandler` interface.
// It uses a structure to register a specific namespace and its events.
Struct = neffos.Struct
// StructInjector can be used to customize the value creation that can is used on serving events.
StructInjector = neffos.StructInjector
// The Message is the structure which describes the incoming and outcoming data. // The Message is the structure which describes the incoming and outcoming data.
// Emitter's "body" argument is the `Message.Body` field. // Emitter's "body" argument is the `Message.Body` field.
// Emitter's return non-nil error is the `Message.Err` field. // Emitter's return non-nil error is the `Message.Err` field.