From 21d9730cde24fd8d22a0a7c4fef069d150482bc7 Mon Sep 17 00:00:00 2001 From: Casper Kuethe <43839798+Casper64@users.noreply.github.com> Date: Tue, 27 Jun 2023 00:25:45 +0200 Subject: [PATCH] context, vweb: add ability to set and get values on vweb.Context (#18564) --- vlib/context/context.v | 31 +++++++++- vlib/context/empty_test.v | 4 +- vlib/vweb/README.md | 75 ++++++++++++++++++++++++ vlib/vweb/tests/middleware_test.v | 7 +++ vlib/vweb/tests/middleware_test_server.v | 12 ++++ vlib/vweb/vweb.v | 27 +++++++++ 6 files changed, 153 insertions(+), 3 deletions(-) diff --git a/vlib/context/context.v b/vlib/context/context.v index ad33a43ea4..ba783e6ede 100644 --- a/vlib/context/context.v +++ b/vlib/context/context.v @@ -65,12 +65,41 @@ pub interface Any {} pub interface Context { deadline() ?time.Time value(key Key) ?Any - str() string mut: done() chan int err() IError } +// str returns the `str` method of the corresponding Context struct +pub fn (ctx &Context) str() string { + // since `Context` is an interface we have to manually match every possible + // type that implements `Context` if we want to use a `Context` as a field in a struct + // since the `Context` interface has to implement its own `str` method. + match ctx { + BackgroundContext { + return ctx.str() + } + EmptyContext { + return ctx.str() + } + TodoContext { + return ctx.str() + } + CancelContext { + return ctx.str() + } + TimerContext { + return ctx.str() + } + ValueContext { + return ctx.str() + } + else { + return context_name(ctx) + } + } +} + fn context_name(ctx Context) string { return typeof(ctx) } diff --git a/vlib/context/empty_test.v b/vlib/context/empty_test.v index 433d8c8ad0..b752d66bba 100644 --- a/vlib/context/empty_test.v +++ b/vlib/context/empty_test.v @@ -2,7 +2,7 @@ module context fn test_background() { ctx := background() - assert 'context.Background' == ctx.str() + assert '&context.Background' == ctx.str() if _ := ctx.value('') { panic('This should never happen') } @@ -10,7 +10,7 @@ fn test_background() { fn test_todo() { ctx := todo() - assert 'context.TODO' == ctx.str() + assert '&context.TODO' == ctx.str() if _ := ctx.value('') { panic('This should never happen') } diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index c7a14d4ca1..9c3fbbd02e 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -377,6 +377,81 @@ If any function of step 2 or 3 returns `false` the middleware functions that wou come after it are not executed and the app handler will also not be executed. You can think of it as a chain. +### Context values + +You can store a value pair in vweb's context. It is especially usefull for passing variables +from a middleware function to the route handler. + +**Example**: +```v oksyntax +module main + +import vweb + +struct App { + vweb.Context + middlewares map[string][]vweb.Middleware +} + +pub fn (mut app App) index() vweb.Result { + // get the user or return HTTP 401 + user := app.get_value[User]('user') or { + app.set_status(401, '') + return app.text('HTTP 401: Unauthorized') + } + + return app.text('welcome ${user.name}') +} + +fn main() { + vweb.run(&App{ + middlewares: { + '/': [get_session] + } + }, 8080) +} + +struct User { + session_id string + name string +} + +fn get_session(mut ctx vweb.Context) bool { + // impelement your own logic to get the user + user := User{ + session_id: '123456' + name: 'Vweb' + } + + // set the user + ctx.set_value('user', user) + return true +} +``` + +When you visit the index page the middleware function `get_session` will run first +This function sets a `User` value to a key `'user'`. +We get this key in `index` and display it to the user if the `'user'` key exists. + +#### Changing Context values + +By default context values are immutable when retrieved with `get_value`. If you want to +change the value later you have to set it again with `set_value`. + +**Example:** +```v ignore +fn change_user(mut ctx vweb.Context) bool { + user := User{ + session_id: '654321' + name: 'tester' + } + + // set the user + ctx.set_value('user', user) + return true +} +``` + ### Redirect Used when you want be redirected to an url diff --git a/vlib/vweb/tests/middleware_test.v b/vlib/vweb/tests/middleware_test.v index 78c2a6d753..5184ea8d66 100644 --- a/vlib/vweb/tests/middleware_test.v +++ b/vlib/vweb/tests/middleware_test.v @@ -239,6 +239,13 @@ fn test_redirect_middleware() { assert received.ends_with('302 Found') } +// Context's + +fn test_middleware_with_context() { + x := http.get('http://${localserver}/with-context') or { panic(err) } + assert x.body == 'b' +} + fn testsuite_end() { // This test is guaranteed to be called last. // It sends a request to the server to shutdown. diff --git a/vlib/vweb/tests/middleware_test_server.v b/vlib/vweb/tests/middleware_test_server.v index 479be3986a..cb36202f32 100644 --- a/vlib/vweb/tests/middleware_test_server.v +++ b/vlib/vweb/tests/middleware_test_server.v @@ -46,6 +46,7 @@ fn main() { '/admin/': [middleware1] '/other/': [middleware1, middleware2] '/redirect': [middleware_redirect] + '/with-context': [context1] } } eprintln('>> webserver: pid: ${os.getpid()}, started on http://localhost:${http_port}/ , with maximum runtime of ${app.timeout} milliseconds.') @@ -220,6 +221,12 @@ pub fn (mut app App) redirect_route() vweb.Result { return app.text('${result}should_never_reach!') } +['/with-context'] +pub fn (mut app App) with_context() vweb.Result { + a := app.get_value[string]('a') or { 'none' } + return app.text(a) +} + // middleware functions: pub fn (mut app App) before_request() { @@ -256,6 +263,11 @@ fn middleware_redirect(mut ctx vweb.Context) bool { return false } +fn context1(mut ctx vweb.Context) bool { + ctx.set_value('a', 'b') + return true +} + // utility functions: pub fn (mut app App) shutdown() vweb.Result { diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index 9e0bd002f7..f41a184cac 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -12,6 +12,7 @@ import net.urllib import time import json import encoding.html +import context // A type which don't get filtered inside templates pub type RawHtml = string @@ -144,6 +145,7 @@ pub struct Context { mut: content_type string = 'text/plain' status string = '200 OK' + ctx context.Context = context.EmptyContext{} pub: // HTTP Request req http.Request @@ -377,6 +379,30 @@ pub fn (ctx &Context) get_header(key string) string { return ctx.req.header.get_custom(key) or { '' } } +// set_value sets a value on the context +pub fn (mut ctx Context) set_value(key context.Key, value context.Any) { + ctx.ctx = context.with_value(ctx.ctx, key, value) +} + +// get_value gets a value from the context +pub fn (ctx &Context) get_value[T](key context.Key) ?T { + if val := ctx.ctx.value(key) { + match val { + T { + // `context.value()` always returns a reference + // if we send back `val` the returntype becomes `?&T` and this can be problematic + // for end users since they won't be able to do something like + // `app.get_value[string]('a') or { '' } + // since V expects the value in the or block to be of type `&string`. + // And if a reference was allowed it would enable mutating the context directly + return *val + } + else {} + } + } + return none +} + pub type DatabasePool[T] = fn (tid int) T interface DbPoolInterface { @@ -642,6 +668,7 @@ fn handle_conn[T](mut conn net.TcpConn, global_app &T, routes &map[string]Route, // Create Context with request data ctx := Context{ + ctx: context.background() req: req page_gen_start: page_gen_start conn: conn