1
0
mirror of https://github.com/vlang/v.git synced 2023-08-10 21:13:21 +03:00

vweb: middleware implementation (#17730)

This commit is contained in:
Casper Kuethe 2023-03-26 00:57:42 +01:00 committed by GitHub
parent 713c95fcc8
commit 1fe5aca782
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 968 additions and 19 deletions

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@title</title>
</head>
<body>
<nav>
<a href="/">Home</a>
<a href="/admin/secrets">"/admin/secrets"</a>
<a href="/admin/dynamic">"/admin/dynamic"</a>
<a href="/early">Early exit</a>
</nav>
<main>
@content
</main>
<footer></footer>
</body>
</html>

View File

@ -0,0 +1 @@
<p>Early exit</p>

View File

@ -0,0 +1,3 @@
<h1>Hello </h1>

View File

@ -0,0 +1 @@
<p>Super secret stuff</p>

View File

@ -0,0 +1,109 @@
module main
import vweb
// for another example see vlib/vweb/tests/middleware_test_server.v
const (
http_port = 8080
)
struct App {
vweb.Context
middlewares map[string][]vweb.Middleware
mut:
is_authenticated bool
}
fn main() {
mut app := new_app()
vweb.run(app, http_port)
}
fn new_app() &App {
mut app := &App{
middlewares: {
// chaining is allowed, middleware will be evaluated in order
'/admin/': [other_func1, other_func2]
'/early': [middleware_early]
}
}
// do stuff with app
// ...
return app
}
['/']
pub fn (mut app App) index() vweb.Result {
println('Index page')
title := 'Home Page'
content := $tmpl('templates/index.html')
base := $tmpl('templates/base.html')
return app.html(base)
}
[middleware: check_auth]
['/admin/secrets']
pub fn (mut app App) secrets() vweb.Result {
println('Secrets page')
title := 'Secret Admin Page'
content := $tmpl('templates/secret.html')
base := $tmpl('templates/base.html')
return app.html(base)
}
['/admin/:sub']
pub fn (mut app App) dynamic(sub string) vweb.Result {
println('Dynamic page')
title := 'Secret dynamic'
content := sub
base := $tmpl('templates/base.html')
return app.html(base)
}
['/early']
pub fn (mut app App) early() vweb.Result {
println('Early page')
title := 'Early Exit'
content := $tmpl('templates/early.html')
base := $tmpl('templates/base.html')
return app.html(base)
}
// is always executed first!
pub fn (mut app App) before_request() {
app.is_authenticated = false
println('0')
}
pub fn (mut app App) check_auth() bool {
println('3')
if app.is_authenticated == false {
app.redirect('/')
}
return app.is_authenticated
}
fn other_func1(mut ctx vweb.Context) bool {
println('1')
return true
}
fn other_func2(mut ctx vweb.Context) bool {
println('2')
// ...
return true
}
fn middleware_early(mut ctx vweb.Context) bool {
println('4')
ctx.text(':(')
// returns false, so the middleware propogation is stopped and the user will see the text ":("
return false
}

View File

@ -252,9 +252,10 @@ pub fn (mut app App) controller_get_user_by_id() vweb.Result {
``` ```
### Middleware ### Middleware
V haven't a well defined middleware. Vweb has different kinds of middleware.
For now, you can use `before_request()`. This method called before every request. The `before_request()` method is always called before every request before any
Probably you can use it for check user session cookie or add header other middleware is processed. You could use it to check user session cookies or to add a header.
**Example:** **Example:**
```v ignore ```v ignore
@ -263,24 +264,117 @@ pub fn (mut app App) before_request() {
} }
``` ```
Middleware functions can be passed directly when creating an App instance and is
executed when the url starts with the defined key.
In the following example, if a user navigates to `/path/to/test` the middleware
is executed in the following order: `middleware_func`, `other_func`, `global_middleware`.
The middleware is executed in the same order as they are defined and if any function in
the chain returns `false` the propogation is stopped.
**Example:**
```v
module main
import vweb
struct App {
vweb.Context
middlewares map[string][]vweb.Middleware
}
fn new_app() &App {
mut app := &App{
middlewares: {
// chaining is allowed, middleware will be evaluated in order
'/path/to/': [middleware_func, other_func]
'/': [global_middleware]
}
}
// do stuff with app
// ...
return app
}
fn middleware_func(mut ctx vweb.Context) bool {
// ...
return true
}
fn other_func(mut ctx vweb.Context) bool {
// ...
return true
}
fn global_middleware(mut ctx vweb.Context) bool {
// ...
return true
}
```
Middleware functions will be of type `vweb.Middleware` and are not methods of App,
so they could also be imported from other modules.
```v ignore
pub type Middleware = fn (mut Context) bool
```
Middleware can also be added to route specific functions via attributes.
**Example:**
```v ignore
[middleware: check_auth]
['/admin/data']
pub fn (mut app App) admin() vweb.Result {
// ...
}
// check_auth is a method of App, so we don't need to pass the context as parameter.
pub fn (mut app App) check_auth () bool {
// ...
return true
}
```
For now you can only add 1 middleware to a route specific function via attributes.
### Redirect ### Redirect
Used when you want be redirected to an url Used when you want be redirected to an url
**Examples:** **Examples:**
```v ignore ```v ignore
pub fn (mut app App) before_request() { pub fn (mut app App) before_request() {
app.user_id = app.get_cookie('id') or { app.redirect('/') } app.user_id = app.get_cookie('id') or { app.redirect('/') }
} }
``` ```
```v ignore ```v ignore
['/articles'; get] ['/articles'; get]
pub fn (mut app App) articles() vweb.Result { pub fn (mut app App) articles() vweb.Result {
if !app.token { if !app.token {
app.redirect('/login') app.redirect('/login')
} }
return app.text("patatoes") return app.text('patatoes')
}
```
You can also combine middleware and redirect.
**Example:**
```v ignore
[middleware: with_auth]
['/admin/secret']
pub fn (mut app App) admin_secret() vweb.Result {
// this code should never be reached
return app.text('secret')
}
['/redirect']
pub fn (mut app App) with_auth() bool {
app.redirect('/auth/login')
return false
} }
``` ```

View File

@ -4,13 +4,14 @@ import net.urllib
import net.http import net.http
// Parsing function attributes for methods and path. // Parsing function attributes for methods and path.
fn parse_attrs(name string, attrs []string) !([]http.Method, string) { fn parse_attrs(name string, attrs []string) !([]http.Method, string, string) {
if attrs.len == 0 { if attrs.len == 0 {
return [http.Method.get], '/${name}' return [http.Method.get], '/${name}', ''
} }
mut x := attrs.clone() mut x := attrs.clone()
mut methods := []http.Method{} mut methods := []http.Method{}
mut middleware := ''
mut path := '' mut path := ''
for i := 0; i < x.len; { for i := 0; i < x.len; {
@ -30,6 +31,11 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) {
x.delete(i) x.delete(i)
continue continue
} }
if attr.starts_with('middleware:') {
middleware = attr.all_after('middleware:').trim_space()
x.delete(i)
continue
}
i++ i++
} }
if x.len > 0 { if x.len > 0 {
@ -44,7 +50,7 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) {
path = '/${name}' path = '/${name}'
} }
// Make path lowercase for case-insensitive comparisons // Make path lowercase for case-insensitive comparisons
return methods, path.to_lower() return methods, path.to_lower(), middleware
} }
fn parse_query_from_url(url urllib.URL) map[string]string { fn parse_query_from_url(url urllib.URL) map[string]string {

View File

@ -0,0 +1,350 @@
import os
import time
import json
import net
import net.http
import io
const (
sport = 12381
localserver = '127.0.0.1:${sport}'
exit_after_time = 12000 // milliseconds
vexe = os.getenv('VEXE')
vweb_logfile = os.getenv('VWEB_LOGFILE')
vroot = os.dir(vexe)
serverexe = os.join_path(os.cache_dir(), 'middleware_test_server.exe')
tcp_r_timeout = 30 * time.second
tcp_w_timeout = 30 * time.second
)
// setup of vweb webserver
fn testsuite_begin() {
os.chdir(vroot) or {}
if os.exists(serverexe) {
os.rm(serverexe) or {}
}
}
fn test_middleware_vweb_app_can_be_compiled() {
// did_server_compile := os.system('${os.quoted_path(vexe)} -g -o ${os.quoted_path(serverexe)} vlib/vweb/tests/middleware_test_server.vv')
// TODO: find out why it does not compile with -usecache and -g
did_server_compile := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(serverexe)} vlib/vweb/tests/middleware_test_server.v')
assert did_server_compile == 0
assert os.exists(serverexe)
}
fn test_middleware_vweb_app_runs_in_the_background() {
mut suffix := ''
$if !windows {
suffix = ' > /dev/null &'
}
if vweb_logfile != '' {
suffix = ' 2>> ${os.quoted_path(vweb_logfile)} >> ${os.quoted_path(vweb_logfile)} &'
}
server_exec_cmd := '${os.quoted_path(serverexe)} ${sport} ${exit_after_time} ${suffix}'
$if debug_net_socket_client ? {
eprintln('running:\n${server_exec_cmd}')
}
$if windows {
spawn os.system(server_exec_cmd)
} $else {
res := os.system(server_exec_cmd)
assert res == 0
}
$if macos {
time.sleep(1000 * time.millisecond)
} $else {
time.sleep(100 * time.millisecond)
}
}
// normal routes:
fn test_app_middleware() {
x := http.get('http://${localserver}/') or { panic(err) }
assert x.body == '0app_middlewareindex'
}
fn test_single_middleware() {
received := simple_tcp_client(path: '/single') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1HTTP/')
assert received.ends_with('0single')
}
fn test_multiple_middleware() {
received := simple_tcp_client(path: '/multiple') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1m2HTTP/')
assert received.ends_with('0multiple')
}
fn test_combined_middleware() {
received := simple_tcp_client(path: '/combined') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1m2HTTP/')
assert received.ends_with('0app_middlewarecombined')
}
fn test_nested_middleware() {
received := simple_tcp_client(path: '/admin/nested') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1HTTP/')
assert received.ends_with('0nested')
}
// above routes + post
struct Post {
msg string
}
fn test_app_post_middleware() {
test_object := Post{
msg: 'HI'
}
json_test := json.encode(test_object)
mut x := http.post_json('http://${localserver}/index_post', json_test) or { panic(err) }
assert x.body == '0app_middlewareindex_post:${json_test}'
}
fn test_single_post_middleware() {
test_object := Post{
msg: 'HI'
}
json_test := json.encode(test_object)
received := simple_tcp_client_post_json(
path: '/single_post'
headers: 'Content-Length: ${json_test.len}\r\n'
content: json_test
) or {
assert err.msg() == ''
return
}
assert received.starts_with('m1')
assert received.ends_with('0single_post:${json_test}')
}
fn test_multiple_post_middleware() {
test_object := Post{
msg: 'HI'
}
json_test := json.encode(test_object)
received := simple_tcp_client_post_json(
path: '/multiple_post'
headers: 'Content-Length: ${json_test.len}\r\n'
content: json_test
) or {
assert err.msg() == ''
return
}
assert received.starts_with('m1m2')
assert received.ends_with('0multiple_post:${json_test}')
}
fn test_combined_post_middleware() {
test_object := Post{
msg: 'HI'
}
json_test := json.encode(test_object)
received := simple_tcp_client_post_json(
path: '/combined_post'
headers: 'Content-Length: ${json_test.len}\r\n'
content: json_test
) or {
assert err.msg() == ''
return
}
assert received.starts_with('m1m2')
assert received.ends_with('0app_middlewarecombined_post:${json_test}')
}
fn test_nested_post_middleware() {
test_object := Post{
msg: 'HI'
}
json_test := json.encode(test_object)
received := simple_tcp_client_post_json(
path: '/admin/nested_post'
headers: 'Content-Length: ${json_test.len}\r\n'
content: json_test
) or {
assert err.msg() == ''
return
}
assert received.starts_with('m1')
assert received.ends_with('0nested_post:${json_test}')
}
// dynamic routes:
fn test_dynamic_middleware() {
dynamic_path := 'test'
received := simple_tcp_client(path: '/admin/${dynamic_path}') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1HTTP/')
assert received.ends_with('0admin_dynamic:${dynamic_path}')
}
fn test_combined_dynamic_middleware() {
dynamic_path := 'test'
received := simple_tcp_client(path: '/other/${dynamic_path}') or {
assert err.msg() == ''
return
}
assert received.starts_with('m1m2HTTP/')
assert received.ends_with('0app_middlewarecombined_dynamic:${dynamic_path}')
}
// redirect routes:
fn test_app_redirect_middleware() {
x := http.get('http://${localserver}/app_redirect') or { panic(err) }
x_home := http.get('http://${localserver}/') or { panic(err) }
assert x.body == x_home.body
received := simple_tcp_client(path: '/app_redirect') or {
assert err.msg() == ''
return
}
assert received.starts_with('HTTP/1.1 302 Found')
assert received.ends_with('302 Found')
}
fn test_redirect_middleware() {
received := simple_tcp_client(path: '/redirect') or {
assert err.msg() == ''
return
}
println(received)
assert received.starts_with('m_redirect')
assert received.contains('HTTP/1.1 302 Found')
assert received.ends_with('302 Found')
}
fn testsuite_end() {
// This test is guaranteed to be called last.
// It sends a request to the server to shutdown.
x := http.fetch(
url: 'http://${localserver}/shutdown'
method: .get
cookies: {
'skey': 'superman'
}
) or {
assert err.msg() == ''
return
}
assert x.status() == .ok
assert x.body == 'good bye'
}
// utility code:
struct SimpleTcpClientConfig {
retries int = 20
host string = 'static.dev'
path string = '/'
agent string = 'v/net.tcp.v'
headers string = '\r\n'
content string
}
fn simple_tcp_client(config SimpleTcpClientConfig) !string {
mut client := &net.TcpConn(0)
mut tries := 0
for tries < config.retries {
tries++
eprintln('> client retries: ${tries}')
client = net.dial_tcp(localserver) or {
if tries > config.retries {
return err
}
time.sleep(100 * time.millisecond)
continue
}
break
}
if client == unsafe { nil } {
eprintln('coult not create a tcp client connection to ${localserver} after ${config.retries} retries')
exit(1)
}
client.set_read_timeout(tcp_r_timeout)
client.set_write_timeout(tcp_w_timeout)
defer {
client.close() or {}
}
message := 'GET ${config.path} HTTP/1.1
Host: ${config.host}
User-Agent: ${config.agent}
Accept: */*
${config.headers}
${config.content}'
$if debug_net_socket_client ? {
eprintln('sending:\n${message}')
}
client.write(message.bytes())!
read := io.read_all(reader: client)!
$if debug_net_socket_client ? {
eprintln('received:\n${read}')
}
return read.bytestr()
}
fn simple_tcp_client_post_json(config SimpleTcpClientConfig) !string {
mut client := &net.TcpConn(0)
mut tries := 0
for tries < config.retries {
tries++
eprintln('> client retries: ${tries}')
client = net.dial_tcp(localserver) or {
if tries > config.retries {
return err
}
time.sleep(100 * time.millisecond)
continue
}
break
}
if client == unsafe { nil } {
eprintln('coult not create a tcp client connection to ${localserver} after ${config.retries} retries')
exit(1)
}
client.set_read_timeout(tcp_r_timeout)
client.set_write_timeout(tcp_w_timeout)
defer {
client.close() or {}
}
message := 'POST ${config.path} HTTP/1.1
Host: ${config.host}
User-Agent: ${config.agent}
Accept: */*
Content-Type: application/json
${config.headers}
${config.content}'
$if debug_net_socket_client ? {
eprintln('sending:\n${message}')
}
client.write(message.bytes())!
read := io.read_all(reader: client)!
$if debug_net_socket_client ? {
eprintln('received:\n${read}')
}
return read.bytestr()
}

View File

@ -0,0 +1,274 @@
module main
import vweb
import time
import os
struct App {
vweb.Context
timeout int
global_config shared Config
middlewares map[string][]vweb.Middleware
}
struct Config {
pub mut:
middleware_text string
}
fn exit_after_timeout(timeout_in_ms int) {
time.sleep(timeout_in_ms * time.millisecond)
println('>> webserver: pid: ${os.getpid()}, exiting ...')
exit(0)
}
fn main() {
if os.args.len != 3 {
panic('Usage: `vweb_test_server.exe PORT TIMEOUT_IN_MILLISECONDS`')
}
http_port := os.args[1].int()
assert http_port > 0
timeout := os.args[2].int()
spawn exit_after_timeout(timeout)
shared config := &Config{}
app := &App{
timeout: timeout
global_config: config
middlewares: {
'/single': [middleware1]
'/single_post': [middleware1]
'/multiple': [middleware1, middleware2]
'/multiple_post': [middleware1, middleware2]
'/combined': [middleware1, middleware2]
'/combined_post': [middleware1, middleware2]
'/admin/': [middleware1]
'/other/': [middleware1, middleware2]
'/redirect': [middleware_redirect]
}
}
eprintln('>> webserver: pid: ${os.getpid()}, started on http://localhost:${http_port}/ , with maximum runtime of ${app.timeout} milliseconds.')
vweb.run_at(app, host: 'localhost', port: http_port, family: .ip)!
}
// normal routes:
[middleware: app_middleware]
['/']
pub fn (mut app App) index() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}index')
}
['/single']
pub fn (mut app App) single() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}single')
}
['/multiple']
pub fn (mut app App) multiple() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}multiple')
}
[middleware: app_middleware]
['/combined']
pub fn (mut app App) combined() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}combined')
}
['/admin/nested']
pub fn (mut app App) nested() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}nested')
}
// above routes + post
[middleware: app_middleware]
['/index_post'; post]
pub fn (mut app App) index_post() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}index_post:${app.req.data}')
}
['/single_post'; post]
pub fn (mut app App) single_post() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}single_post:${app.req.data}')
}
['/multiple_post'; post]
pub fn (mut app App) multiple_post() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}multiple_post:${app.req.data}')
}
[middleware: app_middleware]
['/combined_post'; post]
pub fn (mut app App) combined_post() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}combined_post:${app.req.data}')
}
['/admin/nested_post'; post]
pub fn (mut app App) nested_post() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}nested_post:${app.req.data}')
}
// dynamic routes
['/admin/:dynamic']
pub fn (mut app App) admin_dynamic(dynamic string) vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}admin_dynamic:${dynamic}')
}
[middleware: app_middleware]
['/other/:dynamic']
pub fn (mut app App) combined_dynamic(dynamic string) vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}combined_dynamic:${dynamic}')
}
// redirect routes:
[middleware: app_redirect]
['/app_redirect']
pub fn (mut app App) app_redirect_route() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}should_never_reach!')
}
['/redirect']
pub fn (mut app App) redirect_route() vweb.Result {
mut result := ''
rlock app.global_config {
result = app.global_config.middleware_text
}
return app.text('${result}should_never_reach!')
}
// middleware functions:
pub fn (mut app App) before_request() {
lock app.global_config {
app.global_config.middleware_text = '0'
}
}
pub fn (mut app App) app_middleware() bool {
lock app.global_config {
app.global_config.middleware_text += 'app_middleware'
}
return true
}
pub fn (mut app App) app_redirect() bool {
app.redirect('/')
return false
}
fn middleware1(mut ctx vweb.Context) bool {
ctx.conn.write_string('m1') or { panic(err) }
return true
}
fn middleware2(mut ctx vweb.Context) bool {
ctx.conn.write_string('m2') or { panic(err) }
return true
}
fn middleware_redirect(mut ctx vweb.Context) bool {
ctx.conn.write_string('m_redirect') or { panic(err) }
ctx.redirect('/')
return false
}
// utility functions:
pub fn (mut app App) shutdown() vweb.Result {
session_key := app.get_cookie('skey') or { return app.not_found() }
if session_key != 'superman' {
return app.not_found()
}
spawn app.gracefull_exit()
return app.ok('good bye')
}
fn (mut app App) gracefull_exit() {
eprintln('>> webserver: gracefull_exit')
time.sleep(100 * time.millisecond)
exit(0)
}

View File

@ -7,7 +7,7 @@ import io
const ( const (
sport = 12380 sport = 12380
localserver = 'localhost:${sport}' localserver = '127.0.0.1:${sport}'
exit_after_time = 12000 // milliseconds exit_after_time = 12000 // milliseconds
vexe = os.getenv('VEXE') vexe = os.getenv('VEXE')
vweb_logfile = os.getenv('VWEB_LOGFILE') vweb_logfile = os.getenv('VWEB_LOGFILE')

View File

@ -179,8 +179,9 @@ pub:
} }
struct Route { struct Route {
methods []http.Method methods []http.Method
path string path string
middleware string
} }
// Defining this method is optional. // Defining this method is optional.
@ -383,6 +384,12 @@ interface DbInterface {
db voidptr db voidptr
} }
pub type Middleware = fn (mut Context) bool
interface MiddlewareInterface {
middlewares map[string][]Middleware
}
// run - start a new VWeb server, listening to all available addresses, at the specified `port` // run - start a new VWeb server, listening to all available addresses, at the specified `port`
pub fn run[T](global_app &T, port int) { pub fn run[T](global_app &T, port int) {
run_at[T](global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) } run_at[T](global_app, host: '', port: port, family: .ip6) or { panic(err.msg()) }
@ -411,13 +418,14 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
// Parsing methods attributes // Parsing methods attributes
mut routes := map[string]Route{} mut routes := map[string]Route{}
$for method in T.methods { $for method in T.methods {
http_methods, route_path := parse_attrs(method.name, method.attrs) or { http_methods, route_path, middleware := parse_attrs(method.name, method.attrs) or {
return error('error parsing method attributes: ${err}') return error('error parsing method attributes: ${err}')
} }
routes[method.name] = Route{ routes[method.name] = Route{
methods: http_methods methods: http_methods
path: route_path path: route_path
middleware: middleware
} }
} }
host := if params.host == '' { 'localhost' } else { params.host } host := if params.host == '' { 'localhost' } else { params.host }
@ -428,6 +436,11 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
for { for {
// Create a new app object for each connection, copy global data like db connections // Create a new app object for each connection, copy global data like db connections
mut request_app := &T{} mut request_app := &T{}
$if T is MiddlewareInterface {
request_app = &T{
middlewares: global_app.middlewares.clone()
}
}
$if T is DbInterface { $if T is DbInterface {
request_app.db = global_app.db request_app.db = global_app.db
} $else { } $else {
@ -550,21 +563,45 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) {
// should be called first. // should be called first.
if !route.path.contains('/:') && url_words == route_words { if !route.path.contains('/:') && url_words == route_words {
// We found a match // We found a match
$if T is MiddlewareInterface {
if validate_middleware(mut app, url.path) == false {
return
}
}
if req.method == .post && method.args.len > 0 { if req.method == .post && method.args.len > 0 {
// Populate method args with form values // Populate method args with form values
mut args := []string{cap: method.args.len} mut args := []string{cap: method.args.len}
for param in method.args { for param in method.args {
args << form[param.name] args << form[param.name]
} }
app.$method(args)
if route.middleware == '' {
app.$method(args)
} else if validate_app_middleware(mut app, route.middleware, method.name) {
app.$method(args)
}
} else { } else {
app.$method() if route.middleware == '' {
app.$method()
} else if validate_app_middleware(mut app, route.middleware, method.name) {
app.$method()
}
} }
return return
} }
if url_words.len == 0 && route_words == ['index'] && method.name == 'index' { if url_words.len == 0 && route_words == ['index'] && method.name == 'index' {
app.$method() $if T is MiddlewareInterface {
if validate_middleware(mut app, url.path) == false {
return
}
}
if route.middleware == '' {
app.$method()
} else if validate_app_middleware(mut app, route.middleware, method.name) {
app.$method()
}
return return
} }
@ -573,7 +610,17 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) {
if method_args.len != method.args.len { if method_args.len != method.args.len {
eprintln('warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})') eprintln('warning: uneven parameters count (${method.args.len}) in `${method.name}`, compared to the vweb route `${method.attrs}` (${method_args.len})')
} }
app.$method(method_args)
$if T is MiddlewareInterface {
if validate_middleware(mut app, url.path) == false {
return
}
}
if route.middleware == '' {
app.$method(method_args)
} else if validate_app_middleware(mut app, route.middleware, method.name) {
app.$method(method_args)
}
return return
} }
} }
@ -583,6 +630,49 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) {
conn.write(vweb.http_404.bytes()) or {} conn.write(vweb.http_404.bytes()) or {}
} }
// validate_middleware validates and fires all middlewares that are defined in the global app instance
fn validate_middleware[T](mut app T, full_path string) bool {
for path, middleware_chain in app.middlewares {
// only execute middleware if route.path starts with `path`
if full_path.len >= path.len && full_path.starts_with(path) {
// there is middleware for this route
for func in middleware_chain {
if func(mut app.Context) == false {
return false
}
}
}
}
// passed all middleware checks
return true
}
// validate_app_middleware validates all middlewares as a method of `app`
fn validate_app_middleware[T](mut app T, middleware string, method_name string) bool {
// then the middleware that is defined for this route specifically
valid := fire_app_middleware(mut app, middleware) or {
eprintln('warning: middleware `${middleware}` for the `${method_name}` are not found')
true
}
return valid
}
// fire_app_middleware fires all middlewares that are defined as a method of `app`
fn fire_app_middleware[T](mut app T, method_name string) ?bool {
$for method in T.methods {
if method_name == method.name {
$if method.return_type is bool {
return app.$method()
} $else {
eprintln('error in `${method.name}, middleware functions must return bool')
return none
}
}
}
// no middleware function found
return none
}
fn route_matches(url_words []string, route_words []string) ?[]string { fn route_matches(url_words []string, route_words []string) ?[]string {
// URL path should be at least as long as the route path // URL path should be at least as long as the route path
// except for the catchall route (`/:path...`) // except for the catchall route (`/:path...`)