This is a discussion of #2 and #4.
Current Architecture
Earlier I illustrated the architecture of Revel Framework along with my
misunderstanding of what "convention" means. To summarize what was said:
- Revel Framework consists of two parts:
revel/cmd
package implements a command line tool for:
- bootstrapping your applications;
- recompilation and hot reloading them;
- generation of
main.go
file;
- generation of reverse routes;
- starting integration tests;
- building applications;
- packaging them.
revel
package is responsible for everything else:
- parsing of configuration files;
- parsing of routes (on every start of the app);
- execution of templates;
- allocation and start of http.Server;
- allocation and initialization of all kinds of parameters for:
- Session
- Cache
- Flash
- Validation
- value binding;
Drawbacks
- User app is tightly coupled with the
revel
package and if they don't like something
about its implementation aspects they have to ask about making changes to the core code base.
- With this approach it is not possible to:
- Stop embedding
revel.Controller
;
- Do not use
revel.Result
;
- Use custom configuration format of choice;
- Use alternative implementation of router;
- Replace template package;
- Slow releases: large code base that cannot be broken without releasing the next version, the next version cannot be released till enough changes are made;
- A lot of unnecessary for a user dependencies;
- The framework is difficult to maintain: monolithic codebase, expected behaviour of its components is not defined:
- when user embeds
revel.Controller
instead of *revel.Controller
, it works but shows Error 500, we have to guess whether it is by design or a bug.
Mechanisms of Extendability
- Startup hooks - implemented as a global
revel.OnAppStart
variable of type []func()
(now a bit more complex than that to support ordering);
- Interceptors - similar to Startup Hooks but for special Actions that can be started
Before
, ..., After
regular Actions.
- Filters - similar to Startup Hooks but work on a level of HTTP Handler function.
Drawbacks
- Too many ways to define middleware: interceptor functions vs. interceptor methods vs. filters;
- No way for a middleware to share something with controllers, for
Session
, Cache
, Flash
, Validation
this is
solved by manually adding respective fields
to the revel.Controller{}
struct.
- I.e. for some new middleware
CSRF
that means that in order to share CSRFToken
with actions of controllers
there are a few options:
- Add one more field to the
revel.Controller{}
struct;
- Pass the value using some field that already exists:
c.Controller.Session
, c.Controller.Flash
(not always possible, e.g. I may want to pass int
type or some struct
; and requires an allocation of a map
even if I don't need a session but just a single variable of scalar type).
Default Layout
app/
controllers/
- imports revel
package;
routes/
- imports revel
package. Automatically generated by revel/cmd
and cannot be commit
ed due to .gitignore
;
tmp/
- entry point of the application,
imports: revel
, revel/testing
, revel/modules/*
, controllers
, tests
.
Automatically generated by revel/cmd
and cannot be commit
ed due to .gitignore
;
views/
conf/
app.conf
- INI configuration file;
routes
- a list of routes in a custom format inspired by Play Framework;
messages/
public/
tests/
- integration tests that can be run by revel/cmd
.
Drawbacks
app/
directory is a rudiment of Play Framework 1 that was written in Java where it is a regular
practice to have a lot of nested directories. Go projects may have a more flat structure;
app/tmp/
and app/routes/
are not part of user app (they are in .gitignore
), thus builds are not reproducable and revel/cmd
is required to build/start an app;
conf/routes
is not type safe, validated at start time, changes at the stage when the project has already been compiled
lead to unexpected result (Solutions TBD);
tests/
cannot be run without revel/cmd
.
Sample Code
package controllers
import (
"github.com/revel/revel"
)
func init() {
revel.Filters = []revel.Filter{
...
MyCustomFilter(*revel.Controller, fc []revel.Filter) {
// Do something.
...
fc[0](c, fc[1:])
}
}
}
package controllers
import (
"github.com/revel/revel"
)
type App struct {
*revel.Controller
}
func (c *App) Before() revel.Result {
return nil
}
func (c *App) Index() revel.Result {
return c.Render()
}
func (c *App) After() revel.Result {
return nil
}
func init() {
revel.InterceptMethod((&App{}).Before, revel.BEFORE)
revel.InterceptMethod((&App{}).After, revel.AFTER)
revel.OnAppStart(func() {
// Initialize the app at start-up time.
})
}
Proposal
Tools
Implement everything that Revel provides as a set of independent tools.
Every single one should be usable on its own:
harness/
- hot reloads user applications;
main.go
- entry point of the tool so it can be used independently;
runner/
- package that implements some Handler
interface;
bootstrap/
- generates a new application from a specified skeleton;
main.go
- entry point of the tool so it can be used independently;
creator/
- package that implements some Handler
interface;
handler/
- generates standard handler functions from Revel-like controllers;
routes/
- generates type safe reverse routes;
- TBD
Turn revel
package into a simple command that imports the tool packages and starts their Handler
functions.
Generated Code
Generated code should not depend on revel
or any other projects if it's not possible to replace
the dependency easily. The standard library should be relied on as much as possible.
New Definition of Action
Any method that returns a type implementing standard http.Handler
interface
as a first argument is an Action.
func (c *App) Index(...) http.Handler {
return c.Render()
}
New Definition of Controller
Any struct type that has actions is a controller.
// App is a controller.
type App struct {
}
func (c *App) Index(...) http.Handler {
return c.Render()
}
// Smth isn't.
type Smth struct {
}
Extendability
Special Actions
Before
is a special action that will be executed before every regular action.
After
is an equivalent of Before
but with a finally semantics meaning that it will be guaranteed
to be started after every regular action.
Binding
Actions can bind not only built-in types (int
, uint
, string
, float32
, etc.) but also http.Request
,
types implementing http.ResponseWriter
interface, and any other types that have a special
Bind(url.Values)
method (TBD).
That would allow us to use the Special Actions as Filters
(in Revel's terminology):
func (c *App) Before(w http.ResponseWriter, r *http.Request) http.Handler {
http.SetCookie(w, c.cookieByRequest(r))
return nil
}
Embedding of Controllers
- Proposed
controllers/init.go
type Controller struct {
... // Third party controllers are here.
*MyCustomController
}
type MyCustomController struct {
}
func (c *MyCustomController) Before(page int) http.Handler {
// Do something.
}
- Proposed
controllers/app.go
type App struct {
*Controller
}
func (c *App) Before() http.Handler {
...
}
func (c *App) Index() http.Handler {
return c.Render()
}
func (c *App) After() http.Handler {
...
}
Drawbacks
- Use of anonymous embedding requires every Controller to have a unique name. Solutions TBD.
- Controller auto allocation rules TBD.
Startup Hooks
Functions for running some controller's code after init()
but before the app is started
are still needed. One of the options is to have a special Init
function reserved for that:
Or alternatively, auto start any function that is:
- exported;
- returns an
error
.
Details TBD.
func SomeFunctionInControllersPkg() error {
return nil
}
New Layout
assets/
- autogenerated assets;
handlers/
- handler functions generated from controllers;
routes/
- reverse routes;
config/
controllers/
- controllers are splitted into independent packages (TBD);
account/
profile/
smthelse/
routes/
- imports assets/handlers
(TBD);
static/
views/
main.go
- entry point of the application, imports net/http
, routes
, and starts web-server;
revel.ini
- configuration of the project: how to build it, run, and package. If not presented,
default settings (by convention) will be used.
Motivation
go run
is everything that is needed for start of the project;
- User does not depend on
revel
package, only on the standard library;
- Every single aspect of the app is customizable;
Other ideas
More value for the end users
- Bring support of Meteor.js style rpc-publish-subscribe and data synchronization out of the box;
- WebAssembly is coming. Think over how Revel can be used for isomorphic
development in Go (i.e. the same language for both client and server side).