-
Notifications
You must be signed in to change notification settings - Fork 5
/
controller.go
332 lines (291 loc) · 9.28 KB
/
controller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
package fir
import (
"embed"
"fmt"
"html/template"
"net/http"
"reflect"
"strings"
"sync"
"time"
"github.com/go-playground/validator/v10"
"github.com/gorilla/schema"
"github.com/gorilla/securecookie"
"github.com/gorilla/websocket"
"github.com/lithammer/shortuuid/v4"
"github.com/livefir/fir/pubsub"
servertiming "github.com/mitchellh/go-server-timing"
"github.com/patrickmn/go-cache"
)
// Controller is an interface which encapsulates a group of views. It routes requests to the appropriate view.
// It routes events to the appropriate view. It also provides a way to register views.
type Controller interface {
Route(route Route) http.HandlerFunc
RouteFunc(options RouteFunc) http.HandlerFunc
}
type opt struct {
onSocketConnect func(userOrSessionID string) error
onSocketDisconnect func(userOrSessionID string)
channelFunc func(r *http.Request, viewID string) *string
pathParamsFunc func(r *http.Request) PathParams
websocketUpgrader websocket.Upgrader
disableTemplateCache bool
disableWebsocket bool
debugLog bool
enableWatch bool
watchExts []string
publicDir string
developmentMode bool
embedfs *embed.FS
readFile readFileFunc
existFile existFileFunc
pubsub pubsub.Adapter
appName string
formDecoder *schema.Decoder
cookieName string
secureCookie *securecookie.SecureCookie
cache *cache.Cache
funcMap template.FuncMap
dropDuplicateInterval time.Duration
}
// ControllerOption is an option for the controller.
type ControllerOption func(*opt)
func WithFuncMap(funcMap template.FuncMap) ControllerOption {
return func(opt *opt) {
mergedFuncMap := make(template.FuncMap)
for k, v := range opt.funcMap {
mergedFuncMap[k] = v
}
for k, v := range funcMap {
mergedFuncMap[k] = v
}
opt.funcMap = mergedFuncMap
}
}
// WithSessionSecrets is an option to set the session secrets for the controller.
// used to sign and encrypt the session cookie.
func WithSessionSecrets(hashKey []byte, blockKey []byte) ControllerOption {
return func(o *opt) {
o.secureCookie = securecookie.New(hashKey, blockKey)
}
}
// WithSessionName is an option to set the session name/cookie name for the controller.
func WithSessionName(name string) ControllerOption {
return func(o *opt) {
o.cookieName = name
}
}
// WithChannelFunc is an option to set a function to construct the channel name for the controller's views.
func WithChannelFunc(f func(r *http.Request, viewID string) *string) ControllerOption {
return func(o *opt) {
o.channelFunc = f
}
}
// WithPathParamsFunc is an option to set a function to construct the path params for the controller's views.
func WithPathParamsFunc(f func(r *http.Request) PathParams) ControllerOption {
return func(o *opt) {
o.pathParamsFunc = f
}
}
// WithPubsubAdapter is an option to set a pubsub adapter for the controller's views.
func WithPubsubAdapter(pubsub pubsub.Adapter) ControllerOption {
return func(o *opt) {
o.pubsub = pubsub
}
}
// WithWebsocketUpgrader is an option to set the websocket upgrader for the controller
func WithWebsocketUpgrader(upgrader websocket.Upgrader) ControllerOption {
return func(o *opt) {
o.websocketUpgrader = upgrader
}
}
// WithEmbedFS is an option to set the embed.FS for the controller.
func WithEmbedFS(fs embed.FS) ControllerOption {
return func(o *opt) {
o.embedfs = &fs
}
}
// WithPublicDir is the path to directory containing the public html template files.
func WithPublicDir(path string) ControllerOption {
return func(o *opt) {
o.publicDir = path
}
}
// WithFormDecoder is an option to set the form decoder(gorilla/schema) for the controller.
func WithFormDecoder(decoder *schema.Decoder) ControllerOption {
return func(o *opt) {
o.formDecoder = decoder
}
}
// WithDisableWebsocket is an option to disable websocket.
func WithDisableWebsocket() ControllerOption {
return func(o *opt) {
o.disableWebsocket = true
}
}
// WithDropDuplicateInterval is an option to set the interval to drop duplicate events received by the websocket.
func WithDropDuplicateInterval(interval time.Duration) ControllerOption {
return func(o *opt) {
o.dropDuplicateInterval = interval
}
}
// WithOnSocketConnect takes a function that is called when a new websocket connection is established.
// The function should return an error if the connection should be rejected.
// The user or fir's browser session id is passed to the function.
// user must be set in request.Context with the key UserKey by a developer supplied authentication mechanism.
// It can be used to track user connections and disconnections.
// It can be be used to reject connections based on user or session id.
// It can be used to refresh the page data when a user re-connects.
func WithOnSocketConnect(f func(userOrSessionID string) error) ControllerOption {
return func(o *opt) {
o.onSocketConnect = f
}
}
// WithOnSocketDisconnect takes a function that is called when a websocket connection is disconnected.
func WithOnSocketDisconnect(f func(userOrSessionID string)) ControllerOption {
return func(o *opt) {
o.onSocketDisconnect = f
}
}
// DisableTemplateCache is an option to disable template caching. This is useful for development.
func DisableTemplateCache() ControllerOption {
return func(o *opt) {
o.disableTemplateCache = true
}
}
// EnableDebugLog is an option to enable debug logging.
func EnableDebugLog() ControllerOption {
return func(o *opt) {
o.debugLog = true
}
}
// EnableWatch is an option to enable watching template files for changes.
func EnableWatch(rootDir string, extensions ...string) ControllerOption {
return func(o *opt) {
o.enableWatch = true
if len(extensions) > 0 {
o.publicDir = rootDir
o.watchExts = append(o.watchExts, extensions...)
}
}
}
// DevelopmentMode is an option to enable development mode. It enables debug logging, template watching, and disables template caching.
func DevelopmentMode(enable bool) ControllerOption {
return func(o *opt) {
o.developmentMode = enable
}
}
// NewController creates a new controller.
func NewController(name string, options ...ControllerOption) Controller {
if name == "" {
panic("controller name is required")
}
formDecoder := schema.NewDecoder()
formDecoder.IgnoreUnknownKeys(true)
formDecoder.SetAliasTag("json")
validate := validator.New()
// register function to get tag name from json tags.
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
o := &opt{
websocketUpgrader: websocket.Upgrader{
// disabled compression since its too noisy: https://github.com/gorilla/websocket/issues/859
// EnableCompression: true,
// ReadBufferSize: 4096,
// WriteBufferSize: 4096,
// WriteBufferPool: &sync.Pool{},
},
watchExts: defaultWatchExtensions,
pubsub: pubsub.NewInmem(),
appName: name,
formDecoder: formDecoder,
cookieName: "_fir_session_",
secureCookie: securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
),
cache: cache.New(5*time.Minute, 10*time.Minute),
funcMap: defaultFuncMap(),
dropDuplicateInterval: 250 * time.Millisecond,
publicDir: ".",
}
for _, option := range options {
option(o)
}
c := &controller{
opt: *o,
name: name,
routes: make(map[string]*route),
}
if c.developmentMode {
fmt.Println("controller starting in developer mode")
c.debugLog = true
c.enableWatch = true
c.disableTemplateCache = true
}
if c.enableWatch {
go watchTemplates(c)
}
if c.embedfs != nil {
c.readFile = readFileFS(*c.embedfs)
c.existFile = existFileFS(*c.embedfs)
} else {
c.readFile = readFileOS
c.existFile = existFileOS
}
md := markdown(c.readFile, c.existFile)
c.funcMap["markdown"] = md
c.funcMap["md"] = md
c.opt.channelFunc = c.defaultChannelFunc
return c
}
type controller struct {
name string
routes map[string]*route
opt
}
func (c *controller) defaults() *routeOpt {
defaultRouteOpt := &routeOpt{
id: shortuuid.New(),
content: "Hello Fir App!",
layoutContentName: "content",
partials: []string{"./routes/partials"},
funcMap: c.opt.funcMap,
extensions: []string{".gohtml", ".gotmpl", ".html", ".tmpl"},
eventSender: make(chan Event),
onLoad: func(ctx RouteContext) error {
return nil
},
funcMapMutex: &sync.RWMutex{},
}
return defaultRouteOpt
}
// Route returns an http.HandlerFunc that renders the route
func (c *controller) Route(route Route) http.HandlerFunc {
defaultRouteOpt := c.defaults()
for _, option := range route.Options() {
option(defaultRouteOpt)
}
// create new route
r := newRoute(c, defaultRouteOpt)
// register route in the controller
c.routes[r.id] = r
return servertiming.Middleware(r, nil).ServeHTTP
}
// RouteFunc returns an http.HandlerFunc that renders the route
func (c *controller) RouteFunc(opts RouteFunc) http.HandlerFunc {
defaultRouteOpt := c.defaults()
for _, option := range opts() {
option(defaultRouteOpt)
}
// create new route
r := newRoute(c, defaultRouteOpt)
// register route in the controller
c.routes[r.id] = r
return servertiming.Middleware(r, nil).ServeHTTP
}