...
This commit is contained in:
@@ -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
|
|
||||||
|
|
||||||
@@ -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) {
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
24
aiprompts/herolib_core/cmdline_argument_parsing_example.vsh
Normal file
24
aiprompts/herolib_core/cmdline_argument_parsing_example.vsh
Normal 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)
|
||||||
|
}
|
||||||
@@ -3,13 +3,10 @@
|
|||||||
this is how we want example scripts to be, see the first line
|
this is how we want example scripts to be, see the first line
|
||||||
|
|
||||||
```vlang
|
```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
|
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
|
## important instructions
|
||||||
|
|
||||||
- never use fn main() in a .vsh script
|
- never use fn main() in a .vsh script
|
||||||
|
- always use the top line as in example above
|
||||||
@@ -11,43 +11,13 @@
|
|||||||
when I generate vlang scripts I will always use .vsh extension and use following as first line:
|
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 ...
|
- 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
|
- 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
|
- 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
|
## 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
|
- use ~ so it works over all machines
|
||||||
|
- don't use 'v test', we have vtest as alternative
|
||||||
@@ -1 +0,0 @@
|
|||||||
../lib/core/httpconnection/readme.md
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../lib/osal/readme.md
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../lib/data/ourdb/README.md
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../lib/data/ourtime/readme.md
|
|
||||||
@@ -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.
|
|
||||||
|
|
||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user