This commit is contained in:
2025-07-21 06:30:42 +02:00
parent ddf5fcbbcc
commit 2127fb2ec0
35 changed files with 29 additions and 3539 deletions

View File

@@ -1,15 +0,0 @@
for @lib/circles/mcc
generate openapi 3.1 spec
do it as one file called openapi.yaml and put in the dir as mentioned above
based on the models and db implementation
implement well chosen examples in the openapi spec
note: in OpenAPI 3.1.0, the example property is deprecated in favor of examples
do this for the models & methods as defined below
do it also for the custom and generic methods, don't forget any

View File

@@ -1,197 +0,0 @@
in @lib/circles/mcc
generate openapi 3.1 spec
based on the models and db implementation
implement well chosen examples in the openapi spec
note: in OpenAPI 3.1.0, the example property is deprecated in favor of examples.
do this for the models & methods as defined below
do it for custom and generic methods, don't forget any
```v
// CalendarEvent represents a calendar event with all its properties
pub struct CalendarEvent {
pub mut:
id u32 // Unique identifier
title string // Event title
description string // Event details
location string // Event location
start_time ourtime.OurTime
end_time ourtime.OurTime // End time
all_day bool // True if it's an all-day event
recurrence string // RFC 5545 Recurrence Rule (e.g., "FREQ=DAILY;COUNT=10")
attendees []string // List of emails or user IDs
organizer string // Organizer email
status string // "CONFIRMED", "CANCELLED", "TENTATIVE"
caldav_uid string // CalDAV UID for syncing
sync_token string // Sync token for tracking changes
etag string // ETag for caching
color string // User-friendly color categorization
}
// Email represents an email message with all its metadata and content
pub struct Email {
pub mut:
// Database ID
id u32 // Database ID (assigned by DBHandler)
// Content fields
uid u32 // Unique identifier of the message (in the circle)
seq_num u32 // IMAP sequence number (in the mailbox)
mailbox string // The mailbox this email belongs to
message string // The email body content
attachments []Attachment // Any file attachments
// IMAP specific fields
flags []string // IMAP flags like \Seen, \Deleted, etc.
internal_date i64 // Unix timestamp when the email was received
size u32 // Size of the message in bytes
envelope ?Envelope // IMAP envelope information (contains From, To, Subject, etc.)
}
// Attachment represents an email attachment
pub struct Attachment {
pub mut:
filename string
content_type string
data string // Base64 encoded binary data
}
// Envelope represents an IMAP envelope structure
pub struct Envelope {
pub mut:
date i64
subject string
from []string
sender []string
reply_to []string
to []string
cc []string
bcc []string
in_reply_to string
message_id string
}
```
methods
```v
pub fn (mut m MailDB) new() Email {
}
// set adds or updates an email
pub fn (mut m MailDB) set(email Email) !Email {
}
// get retrieves an email by its ID
pub fn (mut m MailDB) get(id u32) !Email {
}
// list returns all email IDs
pub fn (mut m MailDB) list() ![]u32 {
}
pub fn (mut m MailDB) getall() ![]Email {
}
// delete removes an email by its ID
pub fn (mut m MailDB) delete(id u32) ! {
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_uid retrieves an email by its UID
pub fn (mut m MailDB) get_by_uid(uid u32) !Email {
}
// get_by_mailbox retrieves all emails in a specific mailbox
pub fn (mut m MailDB) get_by_mailbox(mailbox string) ![]Email {
}
// delete_by_uid removes an email by its UID
pub fn (mut m MailDB) delete_by_uid(uid u32) ! {
}
// delete_by_mailbox removes all emails in a specific mailbox
pub fn (mut m MailDB) delete_by_mailbox(mailbox string) ! {
}
// update_flags updates the flags of an email
pub fn (mut m MailDB) update_flags(uid u32, flags []string) !Email {
}
// search_by_subject searches for emails with a specific subject substring
pub fn (mut m MailDB) search_by_subject(subject string) ![]Email {
}
// search_by_address searches for emails with a specific email address in from, to, cc, or bcc fields
pub fn (mut m MailDB) search_by_address(address string) ![]Email {
}
pub fn (mut c CalendarDB) new() CalendarEvent {
CalendarEvent {}
}
// set adds or updates a calendar event
pub fn (mut c CalendarDB) set(event CalendarEvent) CalendarEvent {
CalendarEvent {}
}
// get retrieves a calendar event by its ID
pub fn (mut c CalendarDB) get(id u32) CalendarEvent {
CalendarEvent {}
}
// list returns all calendar event IDs
pub fn (mut c CalendarDB) list() []u32 {
[]
}
pub fn (mut c CalendarDB) getall() []CalendarEvent {
[]
}
// delete removes a calendar event by its ID
pub fn (mut c CalendarDB) delete(id u32) {
}
//////////////////CUSTOM METHODS//////////////////////////////////
// get_by_caldav_uid retrieves a calendar event by its CalDAV UID
pub fn (mut c CalendarDB) get_by_caldav_uid(caldav_uid String) CalendarEvent {
CalendarEvent {}
}
// get_events_by_date retrieves all events that occur on a specific date
pub fn (mut c CalendarDB) get_events_by_date(date String) []CalendarEvent {
[]
}
// get_events_by_organizer retrieves all events organized by a specific person
pub fn (mut c CalendarDB) get_events_by_organizer(organizer String) []CalendarEvent {
[]
}
// get_events_by_attendee retrieves all events that a specific person is attending
pub fn (mut c CalendarDB) get_events_by_attendee(attendee String) []CalendarEvent {
[]
}
// search_events_by_title searches for events with a specific title substring
pub fn (mut c CalendarDB) search_events_by_title(title String) []CalendarEvent {
[]
}
// update_status updates the status of an event
pub fn (mut c CalendarDB) update_status(id u32, status String) CalendarEvent {
CalendarEvent {}
}
// delete_by_caldav_uid removes an event by its CalDAV UID
pub fn (mut c CalendarDB) delete_by_caldav_uid(caldav_uid String) {
}
```

View File

@@ -1,26 +0,0 @@
create a module vfs_mail in @lib/vfs
check the interface as defined in @lib/vfs/interface.v and @metadata.v
see example how a vfs is made in @lib/vfs/vfs_local
create the vfs to represent mail objects in @lib/circles/dbs/core/mail_db.v
mailbox propery on the Email object defines the path in the vfs
this mailbox property can be e.g. Draft/something/somethingelse
in that dir show a subdir /id:
- which show the Email as a json underneith the ${email.id}.json
in that dir show subdir /subject:
- which show the Email as a json underneith the name_fix(${email.envelope.subject}.json
so basically we have 2 representations of the same mail in the vfs, both have the. json as content of the file

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import os
import flag
mut fp := flag.new_flag_parser(os.args)
fp.application('compile.vsh')
fp.version('v0.1.0')
fp.description('Compile hero binary in debug or production mode')
fp.skip_executable()
prod_mode := fp.bool('prod', `p`, false, 'Build production version (optimized)')
help_requested := fp.bool('help', `h`, false, 'Show help message')
if help_requested {
println(fp.usage())
exit(0)
}
additional_args := fp.finalize() or {
eprintln(err)
println(fp.usage())
exit(1)
}

View File

@@ -3,13 +3,10 @@
this is how we want example scripts to be, see the first line
```vlang
#!/usr/bin/env -S v -gc none -cc tcc -d use_openssl -enable-globals run
#!/usr/bin/env -S v -cg -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.installers.sysadmintools.daguserver
import freeflowuniverse.herolib...
mut ds := daguserver.get()!
println(ds)
```
the files are in ~/code/github/freeflowuniverse/herolib/examples for herolib
@@ -17,3 +14,4 @@ the files are in ~/code/github/freeflowuniverse/herolib/examples for herolib
## important instructions
- never use fn main() in a .vsh script
- always use the top line as in example above

View File

@@ -11,43 +11,13 @@
when I generate vlang scripts I will always use .vsh extension and use following as first line:
```
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
```
- a .vsh is a v shell script and can be executed as is, no need to use v ...
- in .vsh file there is no need for a main() function
- these scripts can be used for examples or instruction scripts e.g. an installs script
## to do argument parsing use following examples
```v
#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run
import os
import flag
mut fp := flag.new_flag_parser(os.args)
fp.application('compile.vsh')
fp.version('v0.1.0')
fp.description('Compile hero binary in debug or production mode')
fp.skip_executable()
prod_mode := fp.bool('prod', `p`, false, 'Build production version (optimized)')
help_requested := fp.bool('help', `h`, false, 'Show help message')
if help_requested {
println(fp.usage())
exit(0)
}
additional_args := fp.finalize() or {
eprintln(err)
println(fp.usage())
exit(1)
}
```
## when creating a test script
@@ -58,3 +28,4 @@ vtest ~/code/github/freeflowuniverse/herolib/lib/osal/package_test.v
```
- use ~ so it works over all machines
- don't use 'v test', we have vtest as alternative

View File

@@ -1 +0,0 @@
../lib/core/httpconnection/readme.md

View File

@@ -1 +0,0 @@
../lib/osal/readme.md

View File

@@ -1 +0,0 @@
../lib/data/ourdb/README.md

View File

@@ -1 +0,0 @@
../lib/data/ourtime/readme.md

View File

@@ -1,78 +0,0 @@
# HeroScript
## Overview
HeroScript is a simple, declarative scripting language designed to define workflows and execute commands in a structured manner. It follows a straightforward syntax where each action is prefixed with `!!`, indicating the actor and action name.
## Example
A basic HeroScript script for virtual machine management looks like this:
```heroscript
!!vm.define name:'test_vm' cpu:4
memory: '8GB'
storage: '100GB'
description: '
A virtual machine configuration
with specific resources.
'
!!vm.start name:'test_vm'
!!vm.disk_add
name: 'test_vm'
size: '50GB'
type: 'SSD'
!!vm.delete
name: 'test_vm'
force: true
```
### Key Features
- Every action starts with `!!`.
- The first part after `!!` is the actor (e.g., `vm`).
- The second part is the action name (e.g., `define`, `start`, `delete`).
- Multi-line values are supported (e.g., the `description` field).
- Lists are comma-separated where applicable and inside ''.
- If items one 1 line, then no space between name & argument e.g. name:'test_vm'
## Parsing HeroScript
Internally, HeroScript gets parsed into an action object with parameters. Each parameter follows a `key: value` format.
### Parsing Example
```heroscript
!!actor.action
id:a1 name6:aaaaa
name:'need to do something 1'
description:
'
## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
name2: test
name3: hi
name10:'this is with space' name11:aaa11
name4: 'aaa'
//somecomment
name5: 'aab'
```
### Parsing Details
- Each parameter follows a `key: value` format.
- Multi-line values (such as descriptions) support Markdown formatting.
- Comments can be added using `//`.
- Keys and values can have spaces, and values can be enclosed in single quotes.

View File

@@ -1,267 +0,0 @@
## how to process heroscript in Vlang
- heroscript can be converted to a struct,
- the methods available to get the params are in 'params' section further in this doc
```vlang
fn test_play_dagu() ! {
mut plbook := playbook.new(text: thetext_from_above)!
play_dagu(mut plbook)! //see below in vlang block there it all happens
}
pub fn play_dagu(mut plbook playbook.PlayBook) ! {
//find all actions are !!$actor.$actionname. in this case above the actor is !!dagu, we check with the fitler if it exists, if not we return
dagu_actions := plbook.find(filter: 'dagu.')!
if dagu_actions.len == 0 {
return
}
play_dagu_basic(mut plbook)!
}
pub struct DaguScript {
pub mut:
name string
homedir string
title string
reset bool
start bool
colors []string
}
// play_dagu plays the dagu play commands
pub fn play_dagu_basic(mut plbook playbook.PlayBook) ! {
//now find the specific ones for dagu.script_define
mut actions := plbook.find(filter: 'dagu.script_define')!
if actions.len > 0 {
for myaction in actions {
mut p := myaction.params //get the params object from the action object, this can then be processed using the param getters
mut obj := DaguScript{
//INFO: all details about the get methods can be found in 'params get methods' section
name : p.get('name')! //will give error if not exist
homedir : p.get('homedir')!
title : p.get_default('title', 'My Hero DAG')! //uses a default if not set
reset : p.get_default_false('reset')
start : p.get_default_true('start')
colors : p.get_list('colors')
description : p.get_default('description','')!
}
...
}
}
//there can be more actions which will have other filter
}
```
## params get methods (param getters)
```vlang
fn (params &Params) exists(key_ string) bool
//check if arg exist (arg is just a value in the string e.g. red, not value:something)
fn (params &Params) exists_arg(key_ string) bool
//see if the kwarg with the key exists if yes return as string trimmed
fn (params &Params) get(key_ string) !string
//return the arg with nr, 0 is the first
fn (params &Params) get_arg(nr int) !string
//return arg, if the nr is larger than amount of args, will return the defval
fn (params &Params) get_arg_default(nr int, defval string) !string
fn (params &Params) get_default(key string, defval string) !string
fn (params &Params) get_default_false(key string) bool
fn (params &Params) get_default_true(key string) bool
fn (params &Params) get_float(key string) !f64
fn (params &Params) get_float_default(key string, defval f64) !f64
fn (params &Params) get_from_hashmap(key_ string, defval string, hashmap map[string]string) !string
fn (params &Params) get_int(key string) !int
fn (params &Params) get_int_default(key string, defval int) !int
//Looks for a list of strings in the parameters. ',' are used as deliminator to list
fn (params &Params) get_list(key string) ![]string
fn (params &Params) get_list_default(key string, def []string) ![]string
fn (params &Params) get_list_f32(key string) ![]f32
fn (params &Params) get_list_f32_default(key string, def []f32) []f32
fn (params &Params) get_list_f64(key string) ![]f64
fn (params &Params) get_list_f64_default(key string, def []f64) []f64
fn (params &Params) get_list_i16(key string) ![]i16
fn (params &Params) get_list_i16_default(key string, def []i16) []i16
fn (params &Params) get_list_i64(key string) ![]i64
fn (params &Params) get_list_i64_default(key string, def []i64) []i64
fn (params &Params) get_list_i8(key string) ![]i8
fn (params &Params) get_list_i8_default(key string, def []i8) []i8
fn (params &Params) get_list_int(key string) ![]int
fn (params &Params) get_list_int_default(key string, def []int) []int
fn (params &Params) get_list_namefix(key string) ![]string
fn (params &Params) get_list_namefix_default(key string, def []string) ![]string
fn (params &Params) get_list_u16(key string) ![]u16
fn (params &Params) get_list_u16_default(key string, def []u16) []u16
fn (params &Params) get_list_u32(key string) ![]u32
fn (params &Params) get_list_u32_default(key string, def []u32) []u32
fn (params &Params) get_list_u64(key string) ![]u64
fn (params &Params) get_list_u64_default(key string, def []u64) []u64
fn (params &Params) get_list_u8(key string) ![]u8
fn (params &Params) get_list_u8_default(key string, def []u8) []u8
fn (params &Params) get_map() map[string]string
fn (params &Params) get_path(key string) !string
fn (params &Params) get_path_create(key string) !string
fn (params &Params) get_percentage(key string) !f64
fn (params &Params) get_percentage_default(key string, defval string) !f64
//convert GB, MB, KB to bytes e.g. 10 GB becomes bytes in u64
fn (params &Params) get_storagecapacity_in_bytes(key string) !u64
fn (params &Params) get_storagecapacity_in_bytes_default(key string, defval u64) !u64
fn (params &Params) get_storagecapacity_in_gigabytes(key string) !u64
//Get Expiration object from time string input input can be either relative or absolute## Relative time
fn (params &Params) get_time(key string) !ourtime.OurTime
fn (params &Params) get_time_default(key string, defval ourtime.OurTime) !ourtime.OurTime
fn (params &Params) get_time_interval(key string) !Duration
fn (params &Params) get_timestamp(key string) !Duration
fn (params &Params) get_timestamp_default(key string, defval Duration) !Duration
fn (params &Params) get_u32(key string) !u32
fn (params &Params) get_u32_default(key string, defval u32) !u32
fn (params &Params) get_u64(key string) !u64
fn (params &Params) get_u64_default(key string, defval u64) !u64
fn (params &Params) get_u8(key string) !u8
fn (params &Params) get_u8_default(key string, defval u8) !u8
```
## how internally a heroscript gets parsed for params
- example to show how a heroscript gets parsed in action with params
- params are part of action object
```heroscript
example text to parse (heroscript)
id:a1 name6:aaaaa
name:'need to do something 1'
description:
'
## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
name2: test
name3: hi
name10:'this is with space' name11:aaa11
name4: 'aaa'
//somecomment
name5: 'aab'
```
the params are part of the action and are represented as follow for the above:
```vlang
Params{
params: [Param{
key: 'id'
value: 'a1'
}, Param{
key: 'name6'
value: 'aaaaa'
}, Param{
key: 'name'
value: 'need to do something 1'
}, Param{
key: 'description'
value: '## markdown works in it
description can be multiline
lets see what happens
- a
- something else
### subtitle
'
}, Param{
key: 'name2'
value: 'test'
}, Param{
key: 'name3'
value: 'hi'
}, Param{
key: 'name10'
value: 'this is with space'
}, Param{
key: 'name11'
value: 'aaa11'
}, Param{
key: 'name4'
value: 'aaa'
}, Param{
key: 'name5'
value: 'aab'
}]
}
```

File diff suppressed because it is too large Load Diff

View File

@@ -1,907 +0,0 @@
# veb - the V Web Server
A simple yet powerful web server with built-in routing, parameter handling, templating, and other
features.
## Quick Start
Run your veb app with a live reload via `v -d veb_livereload watch run .`
Now modifying any file in your web app (whether it's a .v file with the backend logic
or a compiled .html template file) will
result in an instant refresh of your app
in the browser. No need to quit the app, rebuild it, and refresh the page in the browser!
## Deploying veb apps
All the code, including HTML templates, is in one binary file. That's all you need to deploy.
Use the `-prod` flag when building for production.
## Getting Started
To start, you must import the module `veb` and define a structure which will
represent your app and a structure which will represent the context of a request.
These structures must be declared with the `pub` keyword.
**Example:**
```v
module main
import veb
pub struct User {
pub mut:
name string
id int
}
// Our context struct must embed `veb.Context`!
pub struct Context {
veb.Context
pub mut:
// In the context struct we store data that could be different
// for each request. Like a User struct or a session id
user User
session_id string
}
pub struct App {
pub:
// In the app struct we store data that should be accessible by all endpoints.
// For example, a database or configuration values.
secret_key string
}
// This is how endpoints are defined in veb. This is the index route
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('Hello V! The secret key is "${app.secret_key}"')
}
fn main() {
mut app := &App{
secret_key: 'secret'
}
// Pass the App and context type and start the web server on port 8080
veb.run[App, Context](mut app, 8080)
}
```
You can use the `App` struct for data you want to keep during the lifetime of your program,
or for data that you want to share between different routes.
A new `Context` struct is created every time a request is received,
so it can contain different data for each request.
## Defining endpoints
To add endpoints to your web server, you must extend the `App` struct.
For routing you can either use auto-mapping of function names or specify the path as an attribute.
The function expects a parameter of your Context type and a response of the type `veb.Result`.
**Example:**
```v ignore
// This endpoint can be accessed via http://server:port/hello
pub fn (app &App) hello(mut ctx Context) veb.Result {
return ctx.text('Hello')
}
// This endpoint can be accessed via http://server:port/foo
@['/foo']
pub fn (app &App) world(mut ctx Context) veb.Result {
return ctx.text('World')
}
```
### 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:**
```v ignore
// only GET requests to http://server:port/world are handled by this method
@[get]
pub fn (app &App) world(mut ctx Context) veb.Result {
return ctx.text('World')
}
// only POST requests to http://server:port/product/create are handled by this method
@['/product/create'; post]
pub fn (app &App) create_product(mut ctx Context) veb.Result {
return ctx.text('product')
}
```
By default, endpoints are marked as GET requests only. It is also possible to
add multiple HTTP verbs per endpoint.
**Example:**
```v ignore
// only GET and POST requests to http://server:port/login are handled by this method
@['/login'; get; post]
pub fn (app &App) login(mut ctx Context) veb.Result {
if ctx.req.method == .get {
// show the login page on a GET request
return ctx.html('<h1>Login page</h1><p>todo: make form</p>')
} else {
// request method is POST
password := ctx.form['password']
// validate password length
if password.len < 12 {
return ctx.text('password is too weak!')
} else {
// we receive a POST request, so we want to explicitly tell the browser
// to send a GET request to the profile page.
return ctx.redirect('/profile')
}
}
}
```
### Routes with Parameters
Parameters are passed directly to an endpoint route using the colon sign `:`. The route
parameters are passed as arguments. V will cast the parameter to any of V's primitive types
(`string`, `int` etc,).
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
// V will pass the parameter 'user' as a string
vvvv
@['/hello/:user'] vvvv
pub fn (app &App) hello_user(mut ctx Context, user string) veb.Result {
return ctx.text('Hello ${user}')
}
// V will pass the parameter 'id' as an int
vv
@['/document/:id'] vv
pub fn (app &App) get_document(mut ctx Context, id int) veb.Result {
return ctx.text('Hello ${user}')
}
```
If we visit http://localhost:port/hello/vaesel we would see the text `Hello vaesel`.
### Routes with Parameter Arrays
If you want multiple parameters in your route and if you want to parse the parameters
yourself, or you want a wildcard route, you can add `...` after the `:` and name,
e.g. `@['/:path...']`.
This will match all routes after `'/'`. For example, the url `/path/to/test` would give
`path = '/path/to/test'`.
```v ignore
vvv
@['/:path...'] vvvv
pub fn (app &App) wildcard(mut ctx Context, path string) veb.Result {
return ctx.text('URL path = "${path}"')
}
```
### Query, Form and Files
You have direct access to query values by accessing the `query` field on your context struct.
You are also able to access any formdata or files that were sent
with the request with the fields `.form` and `.files` respectively.
In the following example, visiting http://localhost:port/user?name=veb we
will see the text `Hello veb!`. And if we access the route without the `name` parameter,
http://localhost:port/user, we will see the text `no user was found`,
**Example:**
```v ignore
@['/user'; get]
pub fn (app &App) get_user_by_id(mut ctx Context) veb.Result {
user_name := ctx.query['name'] or {
// we can exit early and send a different response if no `name` parameter was passed
return ctx.text('no user was found')
}
return ctx.text('Hello ${user_name}!')
}
```
### Host
To restrict an endpoint to a specific host, you can use the `host` attribute
followed by a colon `:` and the host name. You can test the Host feature locally
by adding a host to the "hosts" file of your device.
**Example:**
```v ignore
@['/'; host: 'example.com']
pub fn (app &App) hello_web(mut ctx Context) veb.Result {
return app.text('Hello World')
}
@['/'; host: 'api.example.org']
pub fn (app &App) hello_api(mut ctx Context) veb.Result {
return ctx.text('Hello API')
}
// define the handler without a host attribute last if you have conflicting paths.
@['/']
pub fn (app &App) hello_others(mut ctx Context) veb.Result {
return ctx.text('Hello Others')
}
```
You can also [create a controller](#controller-with-hostname) to handle all requests from a specific
host in one app struct.
### Route Matching Order
veb will match routes in the order that you define endpoints.
**Example:**
```v ignore
@['/:path']
pub fn (app &App) with_parameter(mut ctx Context, path string) veb.Result {
return ctx.text('from with_parameter, path: "${path}"')
}
@['/normal']
pub fn (app &App) normal(mut ctx Context) veb.Result {
return ctx.text('from normal')
}
```
In this example we defined an endpoint with a parameter first. If we access our app
on the url http://localhost:port/normal we will not see `from normal`, but
`from with_parameter, path: "normal"`.
### Custom not found page
You can implement a `not_found` endpoint that is called when a request is made, and no
matching route is found to replace the default HTTP 404 not found page. This route
has to be defined on our Context struct.
**Example:**
```v ignore
pub fn (mut ctx Context) not_found() veb.Result {
// set HTTP status 404
ctx.res.set_status(.not_found)
return ctx.html('<h1>Page not found!</h1>')
}
```
## Static files and website
veb also provides a way of handling static files. We can mount a folder at the root
of our web app, or at a custom route. To start using static files we have to embed
`veb.StaticHandler` on our app struct.
**Example:**
Let's say you have the following file structure:
```
.
├── static/
│ ├── css/
│ │ └── main.css
│ └── js/
│ └── main.js
└── main.v
```
If we want all the documents inside the `static` sub-directory to be publicly accessible, we can
use `handle_static`.
> **Note:**
> veb will recursively search the folder you mount; all the files inside that folder
> will be publicly available.
_main.v_
```v
module main
import veb
pub struct Context {
veb.Context
}
pub struct App {
veb.StaticHandler
}
fn main() {
mut app := &App{}
app.handle_static('static', false)!
veb.run[App, Context](mut app, 8080)
}
```
If we start the app with `v run main.v` we can access our `main.css` file at
http://localhost:8080/static/css/main.css
### Mounting folders at specific locations
In the previous example the folder `static` was mounted at `/static`. We could also choose
to mount the static folder at the root of our app: everything inside the `static` folder
is available at `/`.
**Example:**
```v ignore
// change the second argument to `true` to mount a folder at the app root
app.handle_static('static', true)!
```
We can now access `main.css` directly at http://localhost:8080/css/main.css.
If a request is made to the root of a static folder, veb will look for an
`index.html` or `ìndex.htm` file and serve it if available.
Thus, it's also a good way to host a complete website.
An example is available [here](/examples/veb/static_website).
It is also possible to mount the `static` folder at a custom path.
**Example:**
```v ignore
// mount the folder 'static' at path '/public', the path has to start with '/'
app.mount_static_folder_at('static', '/public')
```
If we run our app the `main.css` file is available at http://localhost:8080/public/main.css
### Adding a single static asset
If you don't want to mount an entire folder, but only a single file, you can use `serve_static`.
**Example:**
```v ignore
// serve the `main.css` file at '/path/main.css'
app.serve_static('/path/main.css', 'static/css/main.css')!
```
### Dealing with MIME types
By default, veb will map the extension of a file to a MIME type. If any of your static file's
extensions do not have a default MIME type in veb, veb will throw an error and you
have to add your MIME type to `.static_mime_types` yourself.
**Example:**
Given the following file structure:
```
.
├── static/
│ └── file.what
└── main.v
```
```v ignore
app.handle_static('static', true)!
```
This code will throw an error, because veb has no default MIME type for a `.what` file extension.
```
unknown MIME type for file extension ".what"
```
To fix this we have to provide a MIME type for the `.what` file extension:
```v ignore
app.static_mime_types['.what'] = 'txt/plain'
app.handle_static('static', true)!
```
## Middleware
Middleware in web development is (loosely defined) a hidden layer that sits between
what a user requests (the HTTP Request) and what a user sees (the HTTP Response).
We can use this middleware layer to provide "hidden" functionality to our apps endpoints.
To use veb's middleware we have to embed `veb.Middleware` on our app struct and provide
the type of which context struct should be used.
**Example:**
```v ignore
pub struct App {
veb.Middleware[Context]
}
```
### Use case
We could, for example, get the cookies for an HTTP request and check if the user has already
accepted our cookie policy. Let's modify our Context struct to store whether the user has
accepted our policy or not.
**Example:**
```v ignore
pub struct Context {
veb.Context
pub mut:
has_accepted_cookies bool
}
```
In veb middleware functions take a `mut` parameter with the type of your context struct
and must return `bool`. We have full access to modify our Context struct!
The return value indicates to veb whether it can continue or has to stop. If we send a
response to the client in a middleware function veb has to stop, so we return `false`.
**Example:**
```v ignore
pub fn check_cookie_policy(mut ctx Context) bool {
// get the cookie
cookie_value := ctx.get_cookie('accepted_cookies') or { '' }
// check if the cookie has been set
if cookie_value == 'true' {
ctx.has_accepted_cookies = true
}
// we don't send a response, so we must return true
return true
}
```
We can check this value in an endpoint and return a different response.
**Example:**
```v ignore
@['/only-cookies']
pub fn (app &App) only_cookie_route(mut ctx Context) veb.Result {
if ctx.has_accepted_cookies {
return ctx.text('Welcome!')
} else {
return ctx.text('You must accept the cookie policy!')
}
}
```
There is one thing left for our middleware to work: we have to register our `only_cookie_route`
function as middleware for our app. We must do this after the app is created and before the
app is started.
**Example:**
```v ignore
fn main() {
mut app := &App{}
// register middleware for all routes
app.use(handler: check_cookie_policy)
// Pass the App and context type and start the web server on port 8080
veb.run[App, Context](mut app, 8080)
}
```
### Types of middleware
In the previous example we used so called "global" middleware. This type of middleware
applies to every endpoint defined on our app struct; global. It is also possible
to register middleware for only a certain route(s).
**Example:**
```v ignore
// register middleware only for the route '/auth'
app.route_use('/auth', handler: auth_middleware)
// register middleware only for the route '/documents/' with a parameter
// e.g. '/documents/5'
app.route_use('/documents/:id')
// register middleware with a parameter array. The middleware will be registered
// for all routes that start with '/user/' e.g. '/user/profile/update'
app.route_use('/user/:path...')
```
### Evaluation moment
By default, the registered middleware functions are executed *before* a method on your
app struct is called. You can also change this behaviour to execute middleware functions
*after* a method on your app struct is called, but before the response is sent!
**Example:**
```v ignore
pub fn modify_headers(mut ctx Context) bool {
// add Content-Language: 'en-US' header to each response
ctx.res.header.add(.content_language, 'en-US')
return true
}
```
```v ignore
app.use(handler: modify_headers, after: true)
```
#### When to use which type
You could use "before" middleware to check and modify the HTTP request and you could use
"after" middleware to validate the HTTP response that will be sent or do some cleanup.
Anything you can do in "before" middleware, you can do in "after" middleware.
### Evaluation order
veb will handle requests in the following order:
1. Execute global "before" middleware
2. Execute "before" middleware that matches the requested route
3. Execute the endpoint handler on your app struct
4. Execute global "after" middleware
5. Execute "after" middleware that matches the requested route
In each step, except for step `3`, veb will evaluate the middleware in the order that
they are registered; when you call `app.use` or `app.route_use`.
### Early exit
If any middleware sends a response (and thus must return `false`) veb will not execute any
other middleware, or the endpoint method, and immediately send the response.
**Example:**
```v ignore
pub fn early_exit(mut ctx Context) bool {
ctx.text('early exit')
// we send a response from middleware, so we have to return false
return false
}
pub fn logger(mut ctx Context) bool {
println('received request for "${ctx.req.url}"')
return true
}
```
```v ignore
app.use(handler: early_exit)
app.use(handler: logger)
```
Because we register `early_exit` before `logger` our logging middleware will never be executed!
## Controllers
Controllers can be used to split up your app logic so you are able to have one struct
per "route group". E.g. a struct `Admin` for urls starting with `'/admin'` and a struct `Foo`
for urls starting with `'/foo'`.
To use controllers we have to embed `veb.Controller` on
our app struct and when we register a controller we also have to specify
what the type of the context struct will be. That means that it is possible
to have a different context struct for each controller and the main app struct.
**Example:**
```v
module main
import veb
pub struct Context {
veb.Context
}
pub struct App {
veb.Controller
}
// this endpoint will be available at '/'
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('from app')
}
pub struct Admin {}
// this endpoint will be available at '/admin/'
pub fn (app &Admin) index(mut ctx Context) veb.Result {
return ctx.text('from admin')
}
pub struct Foo {}
// this endpoint will be available at '/foo/'
pub fn (app &Foo) index(mut ctx Context) veb.Result {
return ctx.text('from foo')
}
fn main() {
mut app := &App{}
// register the controllers the same way as how we start a veb app
mut admin_app := &Admin{}
app.register_controller[Admin, Context]('/admin', mut admin_app)!
mut foo_app := &Foo{}
app.register_controller[Foo, Context]('/foo', mut foo_app)!
veb.run[App, Context](mut app, 8080)
}
```
You can do everything with a controller struct as with a regular `App` struct.
Register middleware, add static files and you can even register other controllers!
### Routing
Any route inside a controller struct is treated as a relative route to its controller namespace.
```v ignore
@['/path']
pub fn (app &Admin) path(mut ctx Context) veb.Result {
return ctx.text('Admin')
}
```
When we registered the controller with
`app.register_controller[Admin, Context]('/admin', mut admin_app)!`
we told veb that the namespace of that controller is `'/admin'` so in this example we would
see the text "Admin" if we navigate to the url `'/admin/path'`.
veb doesn't support duplicate routes, so if we add the following
route to the example the code will produce an error.
```v ignore
@['/admin/path']
pub fn (app &App) admin_path(mut ctx Context) veb.Result {
return ctx.text('Admin overwrite')
}
```
There will be an error, because the controller `Admin` handles all routes starting with
`'/admin'`: the endpoint `admin_path` is unreachable.
### Controller with hostname
You can also set a host for a controller. All requests coming to that host will be handled
by the controller.
**Example:**
```v ignore
struct Example {}
// You can only access this route at example.com: http://example.com/
pub fn (app &Example) index(mut ctx Context) veb.Result {
return ctx.text('Example')
}
```
```v ignore
mut example_app := &Example{}
// set the controllers hostname to 'example.com' and handle all routes starting with '/',
// we handle requests with any route to 'example.com'
app.register_controller[Example, Context]('example.com', '/', mut example_app)!
```
## Context Methods
veb has a number of utility methods that make it easier to handle requests and send responses.
These methods are available on `veb.Context` and directly on your own context struct if you
embed `veb.Context`. Below are some of the most used methods, look at the
[standard library documentation](https://modules.vlang.io/) to see them all.
### Request methods
You can directly access the HTTP request on the `.req` field.
#### Get request headers
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
content_length := ctx.get_header(.content_length) or { '0' }
// get custom header
custom_header := ctx.get_custom_header('X-HEADER') or { '' }
// ...
}
```
#### Get a cookie
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
cookie_val := ctx.get_cookie('token') or { '' }
// ...
}
```
### Response methods
You can directly modify the HTTP response by changing the `res` field,
which is of the type `http.Response`.
#### Send response with different MIME types
```v ignore
// send response HTTP_OK with content-type `text/html`
ctx.html('<h1>Hello world!</h1>')
// send response HTTP_OK with content-type `text/plain`
ctx.text('Hello world!')
// stringify the object and send response HTTP_OK with content-type `application/json`
ctx.json(User{
name: 'test'
age: 20
})
```
#### Sending files
**Example:**
```v ignore
pub fn (app &App) file_response(mut ctx Context) veb.Result {
// send the file 'image.png' in folder 'data' to the user
return ctx.file('data/image.png')
}
```
#### Set response headers
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
ctx.set_header(.accept, 'text/html')
// set custom header
ctx.set_custom_header('X-HEADER', 'my-value')!
// ...
}
```
#### Set a cookie
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
ctx.set_cookie(http.Cookie{
name: 'token'
value: 'true'
path: '/'
secure: true
http_only: true
})
// ...
}
```
#### Redirect
You must pass the type of redirect to veb:
- `moved_permanently` HTTP code 301
- `found` HTTP code 302
- `see_other` HTTP code 303
- `temporary_redirect` HTTP code 307
- `permanent_redirect` HTTP code 308
**Common use cases:**
If you want to change the request method, for example when you receive a post request and
want to redirect to another page via a GET request, you should use `see_other`. If you want
the HTTP method to stay the same, you should use `found` generally speaking.
**Example:**
```v ignore
pub fn (app &App) index(mut ctx Context) veb.Result {
token := ctx.get_cookie('token') or { '' }
if token == '' {
// redirect the user to '/login' if the 'token' cookie is not set
// we explicitly tell the browser to send a GET request
return ctx.redirect('/login', typ: .see_other)
} else {
return ctx.text('Welcome!')
}
}
```
#### Sending error responses
**Example:**
```v ignore
pub fn (app &App) login(mut ctx Context) veb.Result {
if username := ctx.form['username'] {
return ctx.text('Hello "${username}"')
} else {
// send an HTTP 400 Bad Request response with a message
return ctx.request_error('missing form value "username"')
}
}
```
You can also use `ctx.server_error(msg string)` to send an HTTP 500 internal server
error with a message.
## Advanced usage
If you need more control over the TCP connection with a client, for example when
you want to keep the connection open. You can call `ctx.takeover_conn`.
When this function is called you are free to do anything you want with the TCP
connection and veb will not interfere. This means that we are responsible for
sending a response over the connection and closing it.
### Empty Result
Sometimes you want to send the response in another thread, for example when using
[Server Sent Events](sse/README.md). When you are sure that a response will be sent
over the TCP connection you can return `veb.no_result()`. This function does nothing
and returns an empty `veb.Result` struct, letting veb know that we sent a response ourselves.
> **Note:**
> It is important to call `ctx.takeover_conn` before you spawn a thread
**Example:**
```v
module main
import net
import time
import veb
pub struct Context {
veb.Context
}
pub struct App {}
pub fn (app &App) index(mut ctx Context) veb.Result {
return ctx.text('hello!')
}
@['/long']
pub fn (app &App) long_response(mut ctx Context) veb.Result {
// let veb know that the connection should not be closed
ctx.takeover_conn()
// use spawn to handle the connection in another thread
// if we don't the whole web server will block for 10 seconds,
// since veb is singlethreaded
spawn handle_connection(mut ctx.conn)
// we will send a custom response ourselves, so we can safely return an empty result
return veb.no_result()
}
fn handle_connection(mut conn net.TcpConn) {
defer {
conn.close() or {}
}
// block for 10 second
time.sleep(time.second * 10)
conn.write_string('HTTP/1.1 200 OK\r\nContent-type: text/html\r\nContent-length: 15\r\n\r\nHello takeover!') or {}
}
fn main() {
mut app := &App{}
veb.run[App, Context](mut app, 8080)
}
```