From eafbf335cfa19e38c560210569a0ee7ab29780c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hitalo=20de=20Jesus=20do=20Ros=C3=A1rio=20Souza?= <63821277+enghitalo@users.noreply.github.com> Date: Sat, 20 Aug 2022 06:06:24 -0300 Subject: [PATCH] docs: improve vlib/vweb/README.md (#15146) --- vlib/vweb/README.md | 530 +++++++++++++++++++++++++++++++++++++++----- vlib/vweb/vweb.v | 12 +- 2 files changed, 487 insertions(+), 55 deletions(-) diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index 850048c40..cc39f735c 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -1,78 +1,140 @@ -# vweb - the V Web Server # +# vweb - the V Web Server -A simple yet powerful web server with built-in routing, parameter handling, -templating, and other features. +A simple yet powerful web server with built-in routing, parameter handling, templating, and other +features. +The [gitly](https://gitly.org/) site is based on vweb. -## Alpha level software ## +**_Some features may not be complete, and have some bugs._** -Some features may not be complete, and there may still be bugs. However, it is -still a very useful tool. The [gitly](https://gitly.org/) site is based on vweb. - -## Features ## +## Features - **Very fast** performance of C on the web. - **Small binary** hello world website is <100 KB. -- **Easy to deploy** just one binary file that also includes all templates. - No need to install any dependencies. -- **Templates are precompiled** all errors are visible at compilation time, - not at runtime. +- **Easy to deploy** just one binary file that also includes all templates. No need to install any + dependencies. +- **Templates are precompiled** all errors are visible at compilation time, not at runtime. + +### Examples -There is no formal documentation yet - here is a simple -[example](https://github.com/vlang/v/tree/master/examples/vweb/vweb_example.v) +There are some examples +that can be explored [here](https://github.com/vlang/v/tree/master/examples/vweb). -There's also the V forum, [vorum](https://github.com/vlang/vorum) +And others like: -`vorum.v` contains all GET and POST actions. +- [vweb_orm_jwt](https://github.com/vlang/v/tree/master/examples/vweb_orm_jwt) (back-end) +- [vorum](https://github.com/vlang/vorum) (front-end) +- [gitly](https://github.com/vlang/gitly) (full-stack) + +**Front-end getting start example** +`src/main.v` ```v ignore -pub fn (app mut App) index() { - posts := app.find_all_posts() - $vweb.html() +module main + +import vweb +import os + +struct App { + vweb.Context } -// TODO ['/post/:id/:title'] -// TODO `fn (app App) post(id int)` -pub fn (app App) post() { - id := app.get_post_id() - post := app.retrieve_post(id) or { - app.redirect('/') - return - } - comments := app.find_comments(id) - show_form := true - $vweb.html() +struct Object { + title string + description string +} + +fn main() { + vweb.run_at(new_app(), vweb.RunParams{ + port: 8081 + }) or { panic(err) } +} + +fn new_app() &App { + mut app := &App{} + // makes all static files available. + app.mount_static_folder_at(os.resource_abs_path('.'), '/') + return app } + +['/'] +pub fn (mut app App) page_home() vweb.Result { + // all this constants can be accessed by src/templates/page/home.html file. + page_title := 'V is the new V' + v_url := 'https://github.com/vlang/v' + + list_of_object := [ + Object{ + title: 'One good title' + description: 'this is the first' + }, + Object{ + title: 'Other good title' + description: 'more one' + }, + ] + // $vweb.html() in `_ vweb.Result ()` like this + // render the `.html` in folder `./templates/` + return $vweb.html() +} + ``` -`index.html` is an example of the V template language: +`$vweb.html()` compiles an HTML template into V during compilation, and embeds the resulting code +into the current action. + +That means that the template automatically has access to that action's entire environment. + +`src/templates/page/home.html` ```html -@for post in posts -
- @post.title - - @post.nr_comments - @post.time -
-@end + +
+ ${page_title} + @css 'src/templates/page/home.css' +
+ +

Hello, Vs.

+ @for var in list_of_object +
+ ${var.title} + ${var.description} +
+ @end +
@include 'component.html'
+ + ``` -`$vweb.html()` compiles an HTML template into V during compilation, -and embeds the resulting code into the current action. +`src/templates/page/component.html` -That means that the template automatically has access to that action's entire environment. +```html +
This is a component
+``` -## Deploying vweb apps ## +`src/templates/page/home.css` -Everything, including HTML templates, is in one binary file. That's all you need to deploy. +```css +h1.title { + font-family: Arial, Helvetica, sans-serif; + color: #3b7bbf; +} +``` -## Getting Started ## +V suport some [Template directives](/vlib/v/TEMPLATES.md) like +`@css`, `@js` for static files in \ +`@if`, `@for` for conditional and loop +and +`@include` to include html components. -To start with vweb, you have to import the module `vweb`. After the import, -define a struct to hold vweb.Context (and any other variables your program will -need). +## Deploying vweb apps -The web server can be started by calling `vweb.run(&App{}, port)`. +Everything, including HTML templates, is in one binary file. That's all you need to deploy. + +## Getting Started + +To start with vweb, you have to import the module `vweb` and define a struct to hold vweb.Context +(and any other variables your program will need). +The web server can be started by calling `vweb.run(&App{}, port)` or `vweb.run(&App{}, RunParams)` **Example:** @@ -85,10 +147,16 @@ struct App { fn main() { vweb.run(&App{}, 8080) + // // or + // vweb.run_at(new_app(), vweb.RunParams{ + // host: 'localhost' + // port: 8099 + // family: .ip + // }) or { panic(err) } } ``` -### Defining endpoints ### +### Defining endpoints To add endpoints to your web server, you have to extend the `App` struct. For routing you can either use auto-mapping of function names or specify the path as an attribute. @@ -109,7 +177,11 @@ fn (mut app App) world() vweb.Result { } ``` -To create an HTTP POST endpoint, you simply add a `[post]` attribute before the function definition. +#### - HTTP verbs + +To use any HTTP verbs (or methods, as they are properly called), +such as `[post]`, `[get]`, `[put]`, `[patch]` or `[delete]` +you can simply add the attribute before the function definition. **Example:** @@ -118,16 +190,26 @@ To create an HTTP POST endpoint, you simply add a `[post]` attribute before the fn (mut app App) world() vweb.Result { return app.text('World') } + +['/product/create'; post] +fn (mut app App) create_product() vweb.Result { + return app.text('product') +} ``` -To pass a parameter to an endpoint, you simply define it inside -an attribute, e. g. `['/hello/:user]`. +#### - Parameters + +Parameters are passed direcly in endpoint route using colon sign `:` and received using the same +name at function +To pass a parameter to an endpoint, you simply define it inside an attribute, e. g. +`['/hello/:user]`. After it is defined in the attribute, you have to add it as a function parameter. **Example:** ```v ignore -['/hello/:user'] + vvvv +['/hello/:user'] vvvv fn (mut app App) hello_user(user string) vweb.Result { return app.text('Hello $user') } @@ -139,3 +221,343 @@ If you want to read the request body, you can do that by calling `app.req.data`. To read the request headers, you just call `app.req.header` and access the header you want, e.g. `app.req.header.get(.content_type)`. See `struct Header` for all available methods (`v doc net.http Header`). + +### 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 +**Example:** + +```v ignore +pub fn (mut app App) before_request() { + app.user_id = app.get_cookie('id') or { '0' } +} +``` + +### 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('/') } +} +``` + +```v ignore +['/articles'; get] +pub fn (mut app App) articles() vweb.Result { + if !app.token { + app.redirect('/login') + } + return app.text("patatoes") +} +``` + +### Responses + +#### - set_status + +Sets the response status +**Example:** + +```v ignore +['/user/get_all'; get] +pub fn (mut app App) controller_get_all_user() vweb.Result { + token := app.get_header('token') + + if !token { + app.set_status(401, '') + return app.text('Not valid token') + } + + response := app.service_get_all_user() or { + app.set_status(400, '') + return app.text('$err') + } + return app.json(response) +} +``` + +#### - html + +Response HTTP_OK with payload with content-type `text/html` +**Example:** + +```v ignore +pub fn (mut app App) html_page() vweb.Result { + return app.html('

ok

') +} +``` + +#### - text + +Response HTTP_OK with payload with content-type `text/plain` +**Example:** + +```v ignore +pub fn (mut app App) simple() vweb.Result { + return app.text('A simple result') +} +``` + +#### - json + +Response HTTP_OK with payload with content-type `application/json` +**Examples:** + +```v ignore +['/articles'; get] +pub fn (mut app App) articles() vweb.Result { + articles := app.find_all_articles() + json_result := json.encode(articles) + return app.json(json_result) +} +``` + +```v ignore +['/user/create'; post] +pub fn (mut app App) controller_create_user() vweb.Result { + body := json.decode(User, app.req.data) or { + app.set_status(400, '') + return app.text('Failed to decode json, error: $err') + } + + response := app.service_add_user(body.username, body.password) or { + app.set_status(400, '') + return app.text('error: $err') + } + + return app.json(response) +} +``` + +#### - json_pretty + +Response HTTP_OK with a pretty-printed JSON result +**Example:** + +```v ignore +fn (mut app App) time_json_pretty() { + app.json_pretty({ + 'time': time.now().format() + }) +} +``` + +#### - file + +Response HTTP_OK with file as payload + +#### - ok + +Response HTTP_OK with payload +**Example:** + +```v ignore +['/form_echo'; post] +pub fn (mut app App) form_echo() vweb.Result { + app.set_content_type(app.req.header.get(.content_type) or { '' }) + return app.ok(app.form['foo']) +} +``` + +#### - server_error + +Response a server error +**Example:** + +```v ignore +fn (mut app App) sse() vweb.Result { + return app.server_error(501) +} +``` + +#### - not_found + +Response HTTP_NOT_FOUND with payload +**Example:** + +```v ignore +['/:user/:repo/settings'] +pub fn (mut app App) user_repo_settings(username string, repository string) vweb.Result { + if username !in known_users { + return app.not_found() + } + return app.html('username: $username | repository: $repository') +} +``` + +### Requests + +#### - get_header + +Returns the header data from the key +**Example:** + +```v ignore +['/user/get_all'; get] +pub fn (mut app App) controller_get_all_user() vweb.Result { + token := app.get_header('token') + return app.text(token) +} +``` + +#### - get_cookie + +Sets a cookie +**Example:** + +```v ignore +pub fn (mut app App) before_request() { + app.user_id = app.get_cookie('id') or { '0' } +} +``` + +#### - add_header + +Adds an header to the response with key and val +**Example:** + +```v ignore +['/upload'; post] +pub fn (mut app App) upload() vweb.Result { + fdata := app.files['upfile'] + + data_rows := fdata[0].data.split('\n') + + mut output_data := '' + + for elem in data_rows { + delim_row := elem.split('\t') + output_data += '${delim_row[0]}\t${delim_row[1]}\t' + output_data += '${delim_row[0].int() + delim_row[1].int()}\n' + } + + output_data = output_data.all_before_last('\n') + + app.add_header('Content-Disposition', 'attachment; filename=results.txt') + app.send_response_to_client('application/octet-stream', output_data) + + return $vweb.html() +} +``` + +#### - set_cookie + +Sets a cookie +**Example:** + +```v ignore +pub fn (mut app App) cookie() vweb.Result { + app.set_cookie(name: 'cookie', value: 'test') + return app.text('Response Headers\n$app.header') +} +``` + +#### - set_cookie_with_expire_date + +Sets a cookie with a `expire_data` +**Example:** + +```v ignore +pub fn (mut app App) cookie() vweb.Result { + key := 'cookie' + value := 'test' + duration := time.Duration(2 * time.minute ) // add 2 minutes + expire_date := time.now().add(duration) + + app.set_cookie_with_expire_date(key, value, expire_date) + return app.text('Response Headers\n$app.header') +} +``` + +#### - set_content_type + +Sets the response content type +**Example:** + +```v ignore +['/form_echo'; post] +pub fn (mut app App) form_echo() vweb.Result { + app.set_content_type(app.req.header.get(.content_type) or { '' }) + return app.ok(app.form['foo']) +} +``` + +### Template + +#### -handle_static + +handle_static is used to mark a folder (relative to the current working folder) as one that +contains only static resources (css files, images etc). + +If `root` is set the mount path for the dir will be in '/' + +**Example:** + +```v ignore +fn main() { + mut app := &App{} + app.serve_static('/favicon.ico', 'favicon.ico') + // Automatically make available known static mime types found in given directory. + os.chdir(os.dir(os.executable()))? + app.handle_static('assets', true) + vweb.run(app, port) +} +``` + +#### -mount_static_folder_at + +makes all static files in `directory_path` and inside it, available at http://server/mount_path. + +For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'), +and you have a file /var/share/myassets/main.css . +=> That file will be available at URL: http://server/assets/main.css . + +#### -serve_static + +Serves a file static. +`url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the +file type + +**Example:** + +```v ignore +fn main() { + mut app := &App{} + app.serve_static('/favicon.ico', 'favicon.ico') + app.mount_static_folder_at(os.resource_abs_path('.'), '/') + vweb.run(app, 8081) +} +``` + +### Others + +#### -ip + +Returns the ip address from the current user + +**Example:** + +```v ignore +pub fn (mut app App) ip() vweb.Result { + ip := app.ip() + return app.text('ip: $ip') +} +``` + +#### -error + +Set a string to the form error + +**Example:** + +```v ignore +pub fn (mut app App) error() vweb.Result { + app.error('here as an error') + println(app.form_error) //'vweb error: here as an error' +} +``` diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index b9b61c89d..d45113a8e 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -193,6 +193,7 @@ pub fn (ctx Context) init_server() { // Probably you can use it for check user session cookie or add header. pub fn (ctx Context) before_request() {} +// TODO - test // vweb intern function [manualfree] pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bool { @@ -243,6 +244,7 @@ pub fn (mut ctx Context) json_pretty(j T) Result { return Result{} } +// TODO - test // Response HTTP_OK with file as payload pub fn (mut ctx Context) file(f_path string) Result { ext := os.file_ext(f_path) @@ -267,6 +269,7 @@ pub fn (mut ctx Context) ok(s string) Result { return Result{} } +// TODO - test // Response a server error pub fn (mut ctx Context) server_error(ecode int) Result { $if debug { @@ -302,6 +305,7 @@ pub fn (mut ctx Context) not_found() Result { return Result{} } +// TODO - test // Sets a cookie pub fn (mut ctx Context) set_cookie(cookie http.Cookie) { mut cookie_data := []string{} @@ -320,6 +324,7 @@ pub fn (mut ctx Context) set_content_type(typ string) { ctx.content_type = typ } +// TODO - test // Sets a cookie with a `expire_data` pub fn (mut ctx Context) set_cookie_with_expire_date(key string, val string, expire_date time.Time) { ctx.add_header('Set-Cookie', '$key=$val; Secure; HttpOnly; expires=$expire_date.utc_string()') @@ -345,6 +350,7 @@ pub fn (ctx &Context) get_cookie(key string) ?string { // TODO refactor return error('Cookie not found') } +// TODO - test // Sets the response status pub fn (mut ctx Context) set_status(code int, desc string) { if code < 100 || code > 599 { @@ -354,11 +360,13 @@ pub fn (mut ctx Context) set_status(code int, desc string) { } } +// TODO - test // Adds an header to the response with key and val pub fn (mut ctx Context) add_header(key string, val string) { ctx.header.add_custom(key, val) or {} } +// TODO - test // Returns the header data from the key pub fn (ctx &Context) get_header(key string) string { return ctx.req.header.get_custom(key) or { '' } @@ -381,7 +389,7 @@ pub struct RunParams { } // run_at - start a new VWeb server, listening only on a specific address `host`, at the specified `port` -// Example: vweb.run_at(app, 'localhost', 8099) +// Example: vweb.run_at(new_app(), vweb.RunParams{ host: 'localhost' port: 8099 family: .ip }) or { panic(err) } [manualfree] pub fn run_at(global_app &T, params RunParams) ? { if params.port <= 0 || params.port > 65535 { @@ -653,6 +661,7 @@ pub fn (mut ctx Context) handle_static(directory_path string, root bool) bool { return true } +// TODO - test // mount_static_folder_at - makes all static files in `directory_path` and inside it, available at http://server/mount_path // For example: suppose you have called .mount_static_folder_at('/var/share/myassets', '/assets'), // and you have a file /var/share/myassets/main.css . @@ -668,6 +677,7 @@ pub fn (mut ctx Context) mount_static_folder_at(directory_path string, mount_pat return true } +// TODO - test // Serves a file static // `url` is the access path on the site, `file_path` is the real path to the file, `mime_type` is the file type pub fn (mut ctx Context) serve_static(url string, file_path string) { -- 2.30.2