forked from alash3al/olive-go
/
olive.go
297 lines (261 loc) · 9.18 KB
/
olive.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
// Olive (v2.0) A tiny http framework perfect for building web services, created by Mohammed Al Ashaal (http://alash3al.xyz) under MIT License .
/**
package main
import "net/http"
import "github.com/alash3al/olive-go"
func main() {
olive.New().GET("/", func(ctx *olive.Context) bool {
ctx.SetBody("index")
// return false = "don't run the next matched route with the same method and pattern if any"
// this feature allows you to run multiple routes with the same properties
return false
}).ANY("/page/?(.*?)", func(ctx *olive.Context) bool {
ctx.SetBody("i'm the parent \n")
return true
}).GET("/page", func(ctx *olive.Context) bool {
ctx.SetBody([]byte("hi !"))
return false
}).POST("/page/([^/]+)/and/([^/]+)", func(ctx *olive.Context) bool {
var input map[string]string
ctx.GetBody(&input, 512) // parse the request body into {input} and returns error if any
ctx.SetBody(ctx.Params)
return false
}).GroupBy("path", "/api/v1", func(ApiV1 *olive.App){
ApiV1.GET("/ok", func(ctx *olive.Context) bool {
ctx.Res.Write([]byte("api/v1/ok"))
return false
}).GET("/page/([^/]+)/and/([^/]+)", func(ctx *olive.Context) bool {
ctx.Res.Write([]byte("api/v1/ " + ctx.Params[0] + " " + ctx.Params[1]))
return false
})
}).ANY("?.*?", olive.Handler(http.NotFoundHandler(), false)).Listen(":80")
}
*/
package olive
// we only need these package
import("io"; "regexp"; "strings"; "net/url"; "net/http"; "encoding/json"; "html/template")
// a route callback
type handler func(*Context)bool
// a group function
type grouper func(*App)
// a route property
type route struct {
method, path, vhost string
cb handler
}
// ---------------------
// A request context and properties
type Context struct {
Req *http.Request
Res http.ResponseWriter
Params []string
Vars map[string]interface{}
}
// Set the status code
func (self Context) SetStatus(code int) {
self.Res.WriteHeader(code)
}
// Set a http header field
func (self Context) SetHeader(k, v string) {
self.Res.Header().Set(k, v)
}
// Add a header field
func (self Context) AddHeader(k, v string) {
self.Res.Header().Add(k, v)
}
// Get the specified header field "from the request"
func (self Context) GetHeader(k string) string {
return self.Req.Header.Get(k)
}
// Delete the specified header field
func (self Context) DelHeader(k string) {
self.Res.Header().Del(k)
}
// Return the url query vars
func (self Context) GetQuery() url.Values {
return self.Req.URL.Query()
}
// Read the request body into the provided variable, it will read the body as json
// if the specified "v" isn't ([]byte, io.Writer) .
func (self Context) GetBody(v interface{}, max int64) (err error) {
switch v.(type) {
case []byte:
_, err = http.MaxBytesReader(self.Res, self.Req.Body, max).Read(v.([]byte))
case io.Writer:
_, err = io.Copy(v.(io.Writer), http.MaxBytesReader(self.Res, self.Req.Body, max))
default:
err = json.NewDecoder(http.MaxBytesReader(self.Res, self.Req.Body, max)).Decode(v)
}
return err
}
// Write to the client, this function will detect the type of the data,
// it will send the data as json if the specified input isn't (string, []byte, *template.Template and io.Reader),
// it execute the input as html template if the first argument is *template.Template and second argument is template's data .
func (self Context) SetBody(d ... interface{}) {
if len(d) == 0 {
panic("Olive:Context Calling Send() without any arguments !")
}
switch d[0].(type) {
case []byte:
self.Res.Write(d[0].([]byte))
case string:
self.Res.Write([]byte(d[0].(string)))
case io.Reader:
io.Copy(self.Res, d[0].(io.Reader))
case *template.Template:
self.SetHeader("Content-Type", "text/html; charset=utf-8")
if len(d) < 2 {
err := (d[0].(*template.Template)).Execute(self.Res, nil)
if err != nil {
panic(err)
}
} else {
err := (d[0].(*template.Template)).Execute(self.Res, d[1])
if err != nil {
panic(err)
}
}
default:
self.SetHeader("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(self.Res).Encode(d[0])
}
}
// ---------------------
// Our main structure
type App struct {
routes []route
parent string
vhost string
}
// Create a new instance
func New() *App {
return &App{
routes: []route{},
parent: "/",
vhost: "(.*?)",
}
}
// Group multiple routes by the specified section and its pattern with its grouper,
// olive currently supports by "host" or "path" only .
func (self *App) GroupBy(by, pattern string, fn grouper) *App {
by = strings.ToLower(strings.TrimSpace(by))
if by == "host" {
old := self.vhost
self.vhost = strings.TrimSpace(strings.ToLower(pattern))
fn(self)
self.vhost = old
} else if by == "path" {
old := self.parent
self.parent = regexp.MustCompile(`/+`).ReplaceAllString("/" + self.parent + "/" + strings.TrimSpace(pattern) + "/", "/")
fn(self)
self.parent = old
}
return self
}
// Handle the specified custom method for the specified path;
// NOTE: method could be a "regexp string";
// NOTE: path could be a "regexp string";
func (self *App) METHOD(method, path string, cb handler) *App {
self.routes = append(self.routes, route{
method: strings.ToUpper(strings.TrimSpace(method)),
path: regexp.MustCompile(`/+`).ReplaceAllString("/" + self.parent + "/" + strings.TrimSpace(path) + "/", "/"),
vhost: self.vhost,
cb: cb,
})
return self
}
// Handle ANY request method for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) ANY(path string, cb handler) *App {
return self.METHOD("(.*?)", path, cb)
}
// Handle GET request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) GET(path string, cb handler) *App {
return self.METHOD("GET", path, cb)
}
// Handle POST request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) POST(path string, cb handler) *App {
return self.METHOD("POST", path, cb)
}
// Handle PUT request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) PUT(path string, cb handler) *App {
return self.METHOD("PUT", path, cb)
}
// Handle PATCH request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) PATCH(path string, cb handler) *App {
return self.METHOD("PATCH", path, cb)
}
// Handle HEAD request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) HEAD(path string, cb handler) *App {
return self.METHOD("HEAD", path, cb)
}
// Handle DELETE request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) DELETE(path string, cb handler) *App {
return self.METHOD("DELETE", path, cb)
}
// Handle OPTIONS request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) OPTIONS(path string, cb handler) *App {
return self.METHOD("OPTIONS", path, cb)
}
// Handle TRACE request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) TRACE(path string, cb handler) *App {
return self.METHOD("TRACE", path, cb)
}
// Handle CONNECT request for the specified path with the specified callback;
// NOTE: this is based on "func(self *App) METHOD()"
func (self *App) CONNECT(path string, cb handler) *App {
return self.METHOD("CONNECT", path, cb)
}
// Dispatch all registered routes
func (self App) ServeHTTP(res http.ResponseWriter, req *http.Request) {
ctx := &Context{Req: req, Res: res}
current_path := regexp.MustCompile(`/+`).ReplaceAllString("/" + strings.TrimSpace(req.URL.Path) + "/", "/")
current_method := req.Method
current_host := strings.TrimSpace(strings.ToLower(strings.SplitN(req.Host, ":", 2)[0]))
rlen := len(self.routes)
var _route route
var re_vhost, re_path *regexp.Regexp
var i int
for i = 0; i < rlen; i ++ {
_route = self.routes[i]
if ! regexp.MustCompile("^" + _route.method + "$").MatchString(current_method) {
continue
}
re_vhost = regexp.MustCompile("^" + _route.vhost + "$")
if ! re_vhost.MatchString(current_host) {
continue
}
re_path = regexp.MustCompile("^" + _route.path + "$")
if ! re_path.MatchString(current_path) {
continue
}
ctx.Params = append(re_vhost.FindAllStringSubmatch(current_host, -1)[0][1:], re_path.FindAllStringSubmatch(current_path, -1)[0][1:] ...)
if ! _route.cb(ctx) {
break
}
}
}
// An Alias to http.ListenAndServe
func (self App) Listen(addr string) error {
return http.ListenAndServe(addr, self)
}
// An Alias to http.ListenAndServeTLS
func (self App) ListenTLS(addr string, certFile string, keyFile string) error {
return http.ListenAndServeTLS(addr, certFile, keyFile, self)
}
// Convert any http.Handler compatible handler to an internal handler,
// and rtrn is what will be returned for "[don't]run the next matched route with the same method and patttern"
func Handler(h http.Handler, rtrn bool) handler {
return func(ctx *Context) bool {
h.ServeHTTP(ctx.Res, ctx.Req)
return rtrn
}
}