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
V haven't a well defined middleware.
For now, you can use `before_request()`. This method called before every request.
Probably you can use it for check user session cookie or add header
Vweb has different kinds of middleware.
The `before_request()` method is always called before every request before any
other middleware is processed. You could use it to check user session cookies or to add a header.
**Example:**
```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
Used when you want be redirected to an url
**Examples:**
```v ignore
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
['/articles'; get]
pub fn (mut app App) articles() vweb.Result {
if !app.token {
app.redirect('/login')
}
return app.text("patatoes")
if !app.token {
app.redirect('/login')
}
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
// 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 {
return [http.Method.get], '/${name}'
return [http.Method.get], '/${name}', ''
}
mut x := attrs.clone()
mut methods := []http.Method{}
mut middleware := ''
mut path := ''
for i := 0; i < x.len; {
@ -30,6 +31,11 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) {
x.delete(i)
continue
}
if attr.starts_with('middleware:') {
middleware = attr.all_after('middleware:').trim_space()
x.delete(i)
continue
}
i++
}
if x.len > 0 {
@ -44,7 +50,7 @@ fn parse_attrs(name string, attrs []string) !([]http.Method, string) {
path = '/${name}'
}
// 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 {

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 (
sport = 12380
localserver = 'localhost:${sport}'
localserver = '127.0.0.1:${sport}'
exit_after_time = 12000 // milliseconds
vexe = os.getenv('VEXE')
vweb_logfile = os.getenv('VWEB_LOGFILE')

View File

@ -179,8 +179,9 @@ pub:
}
struct Route {
methods []http.Method
path string
methods []http.Method
path string
middleware string
}
// Defining this method is optional.
@ -383,6 +384,12 @@ interface DbInterface {
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`
pub fn run[T](global_app &T, port int) {
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
mut routes := map[string]Route{}
$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}')
}
routes[method.name] = Route{
methods: http_methods
path: route_path
middleware: middleware
}
}
host := if params.host == '' { 'localhost' } else { params.host }
@ -428,6 +436,11 @@ pub fn run_at[T](global_app &T, params RunParams) ! {
for {
// Create a new app object for each connection, copy global data like db connections
mut request_app := &T{}
$if T is MiddlewareInterface {
request_app = &T{
middlewares: global_app.middlewares.clone()
}
}
$if T is DbInterface {
request_app.db = global_app.db
} $else {
@ -550,21 +563,45 @@ fn handle_conn[T](mut conn net.TcpConn, mut app T, routes map[string]Route) {
// should be called first.
if !route.path.contains('/:') && url_words == route_words {
// 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 {
// Populate method args with form values
mut args := []string{cap: method.args.len}
for param in method.args {
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 {
app.$method()
if route.middleware == '' {
app.$method()
} else if validate_app_middleware(mut app, route.middleware, method.name) {
app.$method()
}
}
return
}
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
}
@ -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 {
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
}
}
@ -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 {}
}
// 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 {
// URL path should be at least as long as the route path
// except for the catchall route (`/:path...`)