This commit is contained in:
2024-12-25 20:13:02 +01:00
parent eff6338f71
commit 0fcccf93b0
131 changed files with 20710 additions and 106 deletions

View File

@@ -0,0 +1,204 @@
# instructions how to work with heroscript in vlang
## heroscript
Heroscript is our small scripting language which has following structure
an example of a heroscript is
```heroscript
!!mailclient.configure
name: 'myname'
host: 'localhost'
port: 25
secure: 1
reset: 1
description: '
a description can be multiline
like this
'
```
Notice how:
- every action starts with !!
- the first part is the actor, mailclient in this case
- the 2e part is the action name, configure in this case
- multilines are supported see the description field
## 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
//the object which will be configured
pub struct mailclient {
pub mut:
name string
host string
port int
secure bool
description string
}
mut plbook := playbook.new(text: $the_heroscript_from_above)!
play_mailclient(mut plbook)! //see below in vlang block there it all happens
pub fn play_mailclient(mut plbook playbook.PlayBook) ! {
//find all actions are !!$actor.$actionname. in this case above the actor is !!mailclient, we check with the fitler if it exists, if not we return
mailclient_actions := plbook.find(filter: 'mailclient.')!
for action in mailclient_actions {
if action.name == "configure"{
mut p := action.params
mut obj := mailclientScript{
//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','')!
}
}
}
}
}
## params get methods (param getters)
above in the p.get...
below you can find the methods which can be used on the params
```vlang
exists(key_ string) bool
//check if arg exist (arg is just a value in the string e.g. red, not value:something)
exists_arg(key_ string) bool
//see if the kwarg with the key exists if yes return as string trimmed
get(key_ string) !string
//return the arg with nr, 0 is the first
get_arg(nr int) !string
//return arg, if the nr is larger than amount of args, will return the defval
get_arg_default(nr int, defval string) !string
get_default(key string, defval string) !string
get_default_false(key string) bool
get_default_true(key string) bool
get_float(key string) !f64
get_float_default(key string, defval f64) !f64
get_from_hashmap(key_ string, defval string, hashmap map[string]string) !string
get_int(key string) !int
get_int_default(key string, defval int) !int
//Looks for a list of strings in the parameters. ',' are used as deliminator to list
get_list(key string) ![]string
get_list_default(key string, def []string) ![]string
get_list_f32(key string) ![]f32
get_list_f32_default(key string, def []f32) []f32
get_list_f64(key string) ![]f64
get_list_f64_default(key string, def []f64) []f64
get_list_i16(key string) ![]i16
get_list_i16_default(key string, def []i16) []i16
get_list_i64(key string) ![]i64
get_list_i64_default(key string, def []i64) []i64
get_list_i8(key string) ![]i8
get_list_i8_default(key string, def []i8) []i8
get_list_int(key string) ![]int
get_list_int_default(key string, def []int) []int
get_list_namefix(key string) ![]string
get_list_namefix_default(key string, def []string) ![]string
get_list_u16(key string) ![]u16
get_list_u16_default(key string, def []u16) []u16
get_list_u32(key string) ![]u32
get_list_u32_default(key string, def []u32) []u32
get_list_u64(key string) ![]u64
get_list_u64_default(key string, def []u64) []u64
get_list_u8(key string) ![]u8
get_list_u8_default(key string, def []u8) []u8
get_map() map[string]string
get_path(key string) !string
get_path_create(key string) !string
get_percentage(key string) !f64
get_percentage_default(key string, defval string) !f64
//convert GB, MB, KB to bytes e.g. 10 GB becomes bytes in u64
get_storagecapacity_in_bytes(key string) !u64
get_storagecapacity_in_bytes_default(key string, defval u64) !u64
get_storagecapacity_in_gigabytes(key string) !u64
//Get Expiration object from time string input input can be either relative or absolute## Relative time
get_time(key string) !ourtime.OurTime
get_time_default(key string, defval ourtime.OurTime) !ourtime.OurTime
get_time_interval(key string) !Duration
get_timestamp(key string) !Duration
get_timestamp_default(key string, defval Duration) !Duration
get_u32(key string) !u32
get_u32_default(key string, defval u32) !u32
get_u64(key string) !u64
get_u64_default(key string, defval u64) !u64
get_u8(key string) !u8
get_u8_default(key string, defval u8) !u8
```

View File

@@ -0,0 +1,136 @@
# how to use params
works very well in combination with heroscript
## How to get the paramsparser
```v
import freeflowuniverse.crystallib.data.paramsparser
// Create new params from text
params := paramsparser.new("color:red size:'large' priority:1 enable:true")!
// Or create empty params and add later
mut params := paramsparser.new_params()
params.set("color", "red")
```
## Parameter Format
The parser supports several formats:
1. Key-value pairs: `key:value`
2. Quoted values: `key:'value with spaces'`
3. Arguments without keys: `arg1 arg2`
4. Comments: `// this is a comment`
Example:
```v
text := "name:'John Doe' age:30 active:true // user details"
params := paramsparser.new(text)!
```
## Getting Values
The module provides various methods to retrieve values:
```v
// Get string value
name := params.get("name")! // returns "John Doe"
// Get with default value
color := params.get_default("color", "blue")! // returns "blue" if color not set
// Get as integer
age := params.get_int("age")! // returns 30
// Get as boolean (true if value is "1", "true", "y", "yes")
is_active := params.get_default_true("active")
// Get as float
score := params.get_float("score")!
// Get as percentage (converts "80%" to 0.8)
progress := params.get_percentage("progress")!
```
## Type Conversion Methods
The module supports various type conversions:
### Basic Types
- `get_int()`: Convert to int32
- `get_u32()`: Convert to unsigned 32-bit integer
- `get_u64()`: Convert to unsigned 64-bit integer
- `get_u8()`: Convert to unsigned 8-bit integer
- `get_float()`: Convert to 64-bit float
- `get_percentage()`: Convert percentage string to float (e.g., "80%" → 0.8)
### Boolean Values
- `get_default_true()`: Returns true if value is empty, "1", "true", "y", or "yes"
- `get_default_false()`: Returns false if value is empty, "0", "false", "n", or "no"
### Lists
The module provides robust support for parsing and converting lists:
```v
// Basic list parsing
names := params.get_list("users")! // parses ["user1", "user2", "user3"]
// With default value
tags := params.get_list_default("tags", ["default"])!
// Lists with type conversion
numbers := params.get_list_int("ids")! // converts each item to int
amounts := params.get_list_f64("prices")! // converts each item to f64
// Name-fixed lists (normalizes each item)
clean_names := params.get_list_namefix("categories")!
```
Supported list types:
- `get_list()`: String list
- `get_list_u8()`, `get_list_u16()`, `get_list_u32()`, `get_list_u64()`: Unsigned integers
- `get_list_i8()`, `get_list_i16()`, `get_list_int()`, `get_list_i64()`: Signed integers
- `get_list_f32()`, `get_list_f64()`: Floating point numbers
Each list method has a corresponding `_default` version that accepts a default value.
Valid list formats:
```v
users: "john, jane,bob"
ids: "1,2,3,4,5"
```
### Advanced
```v
get_map() map[string]string
get_path(key string) !string
get_path_create(key string) !string //will create path if it doesnt exist yet
get_percentage(key string) !f64
get_percentage_default(key string, defval string) !f64
//convert GB, MB, KB to bytes e.g. 10 GB becomes bytes in u64
get_storagecapacity_in_bytes(key string) !u64
get_storagecapacity_in_bytes_default(key string, defval u64) !u64
get_storagecapacity_in_gigabytes(key string) !u64
//Get Expiration object from time string input input can be either relative or absolute## Relative time
get_time(key string) !ourtime.OurTime
get_time_default(key string, defval ourtime.OurTime) !ourtime.OurTime
get_time_interval(key string) !Duration
get_timestamp(key string) !Duration
get_timestamp_default(key string, defval Duration) !Duration
```

View File

@@ -0,0 +1,340 @@
module datatypes
# datatypes
This module provides implementations of less frequently used, but still common data types.
V's `builtin` module is imported implicitly, and has implementations for arrays, maps and strings. These are good for many applications, but there are a plethora of other useful data structures/containers, like linked lists, priority queues, trees, etc, that allow for algorithms with different time complexities, which may be more suitable for your specific application.
It is implemented using generics, that you have to specialise for the type of your actual elements. For example:
```v
import datatypes
mut stack := datatypes.Stack[int]{}
stack.push(1)
println(stack)
```
## Currently Implemented Datatypes:
- [x] Linked list
- [x] Doubly linked list
- [x] Stack (LIFO)
- [x] Queue (FIFO)
- [x] Min heap (priority queue)
- [x] Set
- [x] Quadtree
- [x] Bloom filter
- [ ] ...
fn new_bloom_filter[T](hash_func fn (T) u32, table_size int, num_functions int) !&BloomFilter[T]
new_bloom_filter creates a new bloom_filter. `table_size` should be greater than 0, and `num_functions` should be 1~16.
fn new_bloom_filter_fast[T](hash_func fn (T) u32) &BloomFilter[T]
new_bloom_filter_fast creates a new bloom_filter. `table_size` is 16384, and `num_functions` is 4.
fn new_ringbuffer[T](s int) RingBuffer[T]
new_ringbuffer creates an empty ring buffer of size `s`.
fn (mut bst BSTree[T]) insert(value T) bool
insert give the possibility to insert an element in the BST.
fn (bst &BSTree[T]) contains(value T) bool
contains checks if an element with a given `value` is inside the BST.
fn (mut bst BSTree[T]) remove(value T) bool
remove removes an element with `value` from the BST.
fn (bst &BSTree[T]) is_empty() bool
is_empty checks if the BST is empty
fn (bst &BSTree[T]) in_order_traversal() []T
in_order_traversal traverses the BST in order, and returns the result as an array.
fn (bst &BSTree[T]) post_order_traversal() []T
post_order_traversal traverses the BST in post order, and returns the result in an array.
fn (bst &BSTree[T]) pre_order_traversal() []T
pre_order_traversal traverses the BST in pre order, and returns the result as an array.
fn (bst &BSTree[T]) to_left(value T) !T
to_left returns the value of the node to the left of the node with `value` specified if it exists, otherwise the a false value is returned.
An example of usage can be the following one
```v
left_value, exist := bst.to_left(10)
```
fn (bst &BSTree[T]) to_right(value T) !T
to_right return the value of the element to the right of the node with `value` specified, if exist otherwise, the boolean value is false An example of usage can be the following one
```v
left_value, exist := bst.to_right(10)
```
fn (bst &BSTree[T]) max() !T
max return the max element inside the BST. Time complexity O(N) if the BST is not balanced
fn (bst &BSTree[T]) min() !T
min return the minimum element in the BST. Time complexity O(N) if the BST is not balanced.
fn (mut b BloomFilter[T]) add(element T)
adds the element to bloom filter.
fn (b &BloomFilter[T]) exists(element T) bool
checks the element is exists.
fn (l &BloomFilter[T]) @union(r &BloomFilter[T]) !&BloomFilter[T]
@union returns the union of the two bloom filters.
fn (l &BloomFilter[T]) intersection(r &BloomFilter[T]) !&BloomFilter[T]
intersection returns the intersection of bloom filters.
fn (list DoublyLinkedList[T]) is_empty() bool
is_empty checks if the linked list is empty
fn (list DoublyLinkedList[T]) len() int
len returns the length of the linked list
fn (list DoublyLinkedList[T]) first() !T
first returns the first element of the linked list
fn (list DoublyLinkedList[T]) last() !T
last returns the last element of the linked list
fn (mut list DoublyLinkedList[T]) push_back(item T)
push_back adds an element to the end of the linked list
fn (mut list DoublyLinkedList[T]) push_front(item T)
push_front adds an element to the beginning of the linked list
fn (mut list DoublyLinkedList[T]) push_many(elements []T, direction Direction)
push_many adds array of elements to the beginning of the linked list
fn (mut list DoublyLinkedList[T]) pop_back() !T
pop_back removes the last element of the linked list
fn (mut list DoublyLinkedList[T]) pop_front() !T
pop_front removes the last element of the linked list
fn (mut list DoublyLinkedList[T]) insert(idx int, item T) !
insert adds an element to the linked list at the given index
fn (list &DoublyLinkedList[T]) index(item T) !int
index searches the linked list for item and returns the forward index or none if not found.
fn (mut list DoublyLinkedList[T]) delete(idx int)
delete removes index idx from the linked list and is safe to call for any idx.
fn (list DoublyLinkedList[T]) str() string
str returns a string representation of the linked list
fn (list DoublyLinkedList[T]) array() []T
array returns a array representation of the linked list
fn (mut list DoublyLinkedList[T]) next() ?T
next implements the iter interface to use DoublyLinkedList with V's `for x in list {` loop syntax.
fn (mut list DoublyLinkedList[T]) iterator() DoublyListIter[T]
iterator returns a new iterator instance for the `list`.
fn (mut list DoublyLinkedList[T]) back_iterator() DoublyListIterBack[T]
back_iterator returns a new backwards iterator instance for the `list`.
fn (mut iter DoublyListIterBack[T]) next() ?T
next returns *the previous* element of the list, or `none` when the start of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (mut iter DoublyListIter[T]) next() ?T
next returns *the next* element of the list, or `none` when the end of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (list LinkedList[T]) is_empty() bool
is_empty checks if the linked list is empty
fn (list LinkedList[T]) len() int
len returns the length of the linked list
fn (list LinkedList[T]) first() !T
first returns the first element of the linked list
fn (list LinkedList[T]) last() !T
last returns the last element of the linked list
fn (list LinkedList[T]) index(idx int) !T
index returns the element at the given index of the linked list
fn (mut list LinkedList[T]) push(item T)
push adds an element to the end of the linked list
fn (mut list LinkedList[T]) push_many(elements []T)
push adds an array of elements to the end of the linked list
fn (mut list LinkedList[T]) pop() !T
pop removes the last element of the linked list
fn (mut list LinkedList[T]) shift() !T
shift removes the first element of the linked list
fn (mut list LinkedList[T]) insert(idx int, item T) !
insert adds an element to the linked list at the given index
fn (mut list LinkedList[T]) prepend(item T)
prepend adds an element to the beginning of the linked list (equivalent to insert(0, item))
fn (list LinkedList[T]) str() string
str returns a string representation of the linked list
fn (list LinkedList[T]) array() []T
array returns a array representation of the linked list
fn (mut list LinkedList[T]) next() ?T
next implements the iteration interface to use LinkedList with V's `for` loop syntax.
fn (mut list LinkedList[T]) iterator() ListIter[T]
iterator returns a new iterator instance for the `list`.
fn (mut iter ListIter[T]) next() ?T
next returns the next element of the list, or `none` when the end of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (mut heap MinHeap[T]) insert(item T)
insert adds an element to the heap.
fn (mut heap MinHeap[T]) insert_many(elements []T)
insert array of elements to the heap.
fn (mut heap MinHeap[T]) pop() !T
pop removes the top-most element from the heap.
fn (heap MinHeap[T]) peek() !T
peek gets the top-most element from the heap without removing it.
fn (heap MinHeap[T]) len() int
len returns the number of elements in the heap.
fn (queue Queue[T]) is_empty() bool
is_empty checks if the queue is empty
fn (queue Queue[T]) len() int
len returns the length of the queue
fn (queue Queue[T]) peek() !T
peek returns the head of the queue (first element added)
fn (queue Queue[T]) last() !T
last returns the tail of the queue (last element added)
fn (queue Queue[T]) index(idx int) !T
index returns the element at the given index of the queue
fn (mut queue Queue[T]) push(item T)
push adds an element to the tail of the queue
fn (mut queue Queue[T]) pop() !T
pop removes the element at the head of the queue and returns it
fn (queue Queue[T]) str() string
str returns a string representation of the queue
fn (queue Queue[T]) array() []T
array returns a array representation of the queue
fn (mut rb RingBuffer[T]) push(element T) !
push adds an element to the ring buffer.
fn (mut rb RingBuffer[T]) pop() !T
pop returns the oldest element in the buffer.
fn (mut rb RingBuffer[T]) push_many(elements []T) !
push_many pushes an array to the buffer.
fn (mut rb RingBuffer[T]) pop_many(n u64) ![]T
pop_many returns `n` elements of the buffer starting with the oldest one.
fn (rb RingBuffer[T]) is_empty() bool
is_empty returns `true` if the ring buffer is empty, `false` otherwise.
fn (rb RingBuffer[T]) is_full() bool
is_full returns `true` if the ring buffer is full, `false` otherwise.
fn (rb RingBuffer[T]) capacity() int
capacity returns the capacity of the ring buffer.
fn (mut rb RingBuffer[T]) clear()
clear empties the ring buffer and all pushed elements.
fn (rb RingBuffer[T]) occupied() int
occupied returns the occupied capacity of the buffer.
fn (rb RingBuffer[T]) remaining() int
remaining returns the remaining capacity of the buffer.
fn (set Set[T]) exists(element T) bool
checks the element is exists.
fn (mut set Set[T]) add(element T)
adds the element to set, if it is not present already.
fn (mut set Set[T]) remove(element T)
removes the element from set.
fn (set Set[T]) pick() !T
pick returns an arbitrary element of set, if set is not empty.
fn (mut set Set[T]) rest() ![]T
rest returns the set consisting of all elements except for the arbitrary element.
fn (mut set Set[T]) pop() !T
pop returns an arbitrary element and deleting it from set.
fn (mut set Set[T]) clear()
delete all elements of set.
fn (l Set[T]) == (r Set[T]) bool
== checks whether the two given sets are equal (i.e. contain all and only the same elements).
fn (set Set[T]) is_empty() bool
is_empty checks whether the set is empty or not.
fn (set Set[T]) size() int
size returns the number of elements in the set.
fn (set Set[T]) copy() Set[T]
copy returns a copy of all the elements in the set.
fn (mut set Set[T]) add_all(elements []T)
add_all adds the whole `elements` array to the set
fn (l Set[T]) @union(r Set[T]) Set[T]
@union returns the union of the two sets.
fn (l Set[T]) intersection(r Set[T]) Set[T]
intersection returns the intersection of sets.
fn (l Set[T]) - (r Set[T]) Set[T]
- returns the difference of sets.
fn (l Set[T]) subset(r Set[T]) bool
subset returns true if the set `r` is a subset of the set `l`.
fn (stack Stack[T]) is_empty() bool
is_empty checks if the stack is empty
fn (stack Stack[T]) len() int
len returns the length of the stack
fn (stack Stack[T]) peek() !T
peek returns the top of the stack
fn (mut stack Stack[T]) push(item T)
push adds an element to the top of the stack
fn (mut stack Stack[T]) pop() !T
pop removes the element at the top of the stack and returns it
fn (stack Stack[T]) str() string
str returns a string representation of the stack
fn (stack Stack[T]) array() []T
array returns a array representation of the stack
enum Direction {
front
back
}
struct AABB {
pub mut:
x f64
y f64
width f64
height f64
}
struct BSTree[T] {
mut:
root &BSTreeNode[T] = unsafe { 0 }
}
Pure Binary Seach Tree implementation
Pure V implementation of the Binary Search Tree Time complexity of main operation O(log N) Space complexity O(N)
struct DoublyLinkedList[T] {
mut:
head &DoublyListNode[T] = unsafe { 0 }
tail &DoublyListNode[T] = unsafe { 0 }
// Internal iter pointer for allowing safe modification
// of the list while iterating. TODO: use an option
// instead of a pointer to determine it is initialized.
iter &DoublyListIter[T] = unsafe { 0 }
len int
}
DoublyLinkedList[T] represents a generic doubly linked list of elements, each of type T.
struct DoublyListIter[T] {
mut:
node &DoublyListNode[T] = unsafe { 0 }
}
DoublyListIter[T] is an iterator for DoublyLinkedList. It starts from *the start* and moves forwards to *the end* of the list. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. A DoublyListIter iterator instance always traverses the list from *start to finish*.
struct DoublyListIterBack[T] {
mut:
node &DoublyListNode[T] = unsafe { 0 }
}
DoublyListIterBack[T] is an iterator for DoublyLinkedList. It starts from *the end* and moves backwards to *the start* of the list. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. A DoublyListIterBack iterator instance always traverses the list from *finish to start*.
struct LinkedList[T] {
mut:
head &ListNode[T] = unsafe { 0 }
len int
// Internal iter pointer for allowing safe modification
// of the list while iterating. TODO: use an option
// instead of a pointer to determine if it is initialized.
iter &ListIter[T] = unsafe { 0 }
}
struct ListIter[T] {
mut:
node &ListNode[T] = unsafe { 0 }
}
ListIter[T] is an iterator for LinkedList. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. An iterator instance always traverses the list from start to finish.
struct ListNode[T] {
mut:
data T
next &ListNode[T] = unsafe { 0 }
}
struct MinHeap[T] {
mut:
data []T
}
MinHeap is a binary minimum heap data structure.
struct Quadtree {
pub mut:
perimeter AABB
capacity int
depth int
level int
particles []AABB
nodes []Quadtree
}
fn (mut q Quadtree) create(x f64, y f64, width f64, height f64, capacity int, depth int, level int) Quadtree
create returns a new configurable root node for the tree.
fn (mut q Quadtree) insert(p AABB)
insert recursively adds a particle in the correct index of the tree.
fn (mut q Quadtree) retrieve(p AABB) []AABB
retrieve recursively checks if a particle is in a specific index of the tree.
fn (mut q Quadtree) clear()
clear flushes out nodes and particles from the tree.
fn (q Quadtree) get_nodes() []Quadtree
get_nodes recursively returns the subdivisions the tree has.
struct Queue[T] {
mut:
elements LinkedList[T]
}
struct RingBuffer[T] {
mut:
reader int // index of the tail where data is going to be read
writer int // index of the head where data is going to be written
content []T
}
RingBuffer represents a ring buffer also known as a circular buffer.
struct Set[T] {
mut:
elements map[T]u8
}
struct Stack[T] {
mut:
elements []T
}

View File

@@ -0,0 +1,309 @@
# how to work with heroscript in vlang
## heroscript
Heroscript is our small scripting language which has following structure
an example of a heroscript is
```heroscript
!!dagu.script_define
name: 'test_dag'
homedir:''
title:'a title'
reset:1
start:true //trie or 1 is same
colors: 'green,red,purple' //lists are comma separated
description: '
a description can be multiline
like this
'
!!dagu.add_step
dag: 'test_dag'
name: 'hello_world'
command: 'echo hello world'
!!dagu.add_step
dag: 'test_dag'
name: 'last_step'
command: 'echo last step'
```
Notice how:
- every action starts with !!
- the first part is the actor e.g. dagu in this case
- the 2e part is the action name
- multilines are supported see the description field
## 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'
}]
}
```

View File

@@ -0,0 +1,440 @@
# module osal
import as
```vlang
import freeflowuniverse.crystallib.osal
osal.ping...
```
## ping
```go
assert ping(address:"338.8.8.8")==.unknownhost
assert ping(address:"8.8.8.8")==.ok
assert ping(address:"18.8.8.8")==.timeout
```
will do a panic if its not one of them, an unknown error
## platform
```go
if platform()==.osx{
//do something
}
pub enum PlatformType {
unknown
osx
ubuntu
alpine
}
pub enum CPUType {
unknown
intel
arm
intel32
arm32
}
```
## process
### execute jobs
```v
mut job2:=osal.exec(cmd:"ls /")?
println(job2)
//wont die, the result can be found in /tmp/execscripts
mut job:=osal.exec(cmd:"ls dsds",ignore_error:true)?
//this one has an error
println(job)
```
All scripts are executed from a file from /tmp/execscripts
If the script executes well then its removed, so no leftovers, if it fails the script stays in the dir
### check process logs
```
mut pm:=process.processmap_get()?
```
info returns like:
```json
}, freeflowuniverse.crystallib.process.ProcessInfo{
cpu_perc: 0
mem_perc: 0
cmd: 'mc'
pid: 84455
ppid: 84467
rss: 3168
}, freeflowuniverse.crystallib.process.ProcessInfo{
cpu_perc: 0
mem_perc: 0
cmd: 'zsh -Z -g'
pid: 84467
ppid: 84469
rss: 1360
}]
```
## other commands
fn bin_path() !string
fn cmd_add(args_ CmdAddArgs) !
copy a binary to the right location on the local computer . e.g. is /usr/local/bin on linux . e.g. is ~/hero/bin on osx . will also add the bin location to the path of .zprofile and .zshrc (different per platform)
fn cmd_exists(cmd string) bool
fn cmd_exists_profile(cmd string) bool
fn cmd_path(cmd string) !string
is same as executing which in OS returns path or error
fn cmd_to_script_path(cmd Command) !string
will return temporary path which then can be executed, is a helper function for making script out of command
fn cputype() CPUType
fn cputype_enum_from_string(cpytype string) CPUType
Returns the enum value that matches the provided string for CPUType
fn dir_delete(path string) !
remove all if it exists
fn dir_ensure(path string) !
remove all if it exists
fn dir_reset(path string) !
remove all if it exists and then (re-)create
fn done_delete(key string) !
fn done_exists(key string) bool
fn done_get(key string) ?string
fn done_get_int(key string) int
fn done_get_str(key string) string
fn done_print() !
fn done_reset() !
fn done_set(key string, val string) !
fn download(args_ DownloadArgs) !pathlib.Path
if name is not specified, then will be the filename part if the last ends in an extension like .md .txt .log .text ... the file will be downloaded
fn env_get(key string) !string
Returns the requested environment variable if it exists or throws an error if it does not
fn env_get_all() map[string]string
Returns all existing environment variables
fn env_get_default(key string, def string) string
Returns the requested environment variable if it exists or returns the provided default value if it does not
fn env_set(args EnvSet)
Sets an environment if it was not set before, it overwrites the enviroment variable if it exists and if overwrite was set to true (default)
fn env_set_all(args EnvSetAll)
Allows to set multiple enviroment variables in one go, if clear_before_set is true all existing environment variables will be unset before the operation, if overwrite_if_exists is set to true it will overwrite all existing enviromnent variables
fn env_unset(key string)
Unsets an environment variable
fn env_unset_all()
Unsets all environment variables
fn exec(cmd Command) !Job
cmd is the cmd to execute can use ' ' and spaces . if \n in cmd it will write it to ext and then execute with bash . if die==false then will just return returncode,out but not return error . if stdout will show stderr and stdout . . if cmd starts with find or ls, will give to bash -c so it can execute . if cmd has no path, path will be found . . Command argument: .
```
name string // to give a name to your command, good to see logs...
cmd string
description string
timeout int = 3600 // timeout in sec
stdout bool = true
stdout_log bool = true
raise_error bool = true // if false, will not raise an error but still error report
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
work_folder string // location where cmd will be executed
environment map[string]string // env variables
ignore_error_codes []int
scriptpath string // is the path where the script will be put which is executed
scriptkeep bool // means we don't remove the script
debug bool // if debug will put +ex in the script which is being executed and will make sure script stays
shell bool // means we will execute it in a shell interactive
retry int
interactive bool = true // make sure we run on non interactive way
async bool
runtime RunTime (.bash, .python)
returns Job:
start time.Time
end time.Time
cmd Command
output []string
error []string
exit_code int
status JobStatus
process os.Process
```
return Job .
fn exec_string(cmd Command) !string
cmd is the cmd to execute can use ' ' and spaces if \n in cmd it will write it to ext and then execute with bash if die==false then will just return returncode,out but not return error if stdout will show stderr and stdout
if cmd starts with find or ls, will give to bash -c so it can execute if cmd has no path, path will be found $... are remplaced by environment arguments TODO:implement
Command argument: cmd string timeout int = 600 stdout bool = true die bool = true debug bool
return what needs to be executed can give it to bash -c ...
fn execute_debug(cmd string) !string
fn execute_interactive(cmd string) !
shortcut to execute a job interactive means in shell
fn execute_ok(cmd string) bool
executes a cmd, if not error return true
fn execute_silent(cmd string) !string
shortcut to execute a job silent
fn execute_stdout(cmd string) !string
shortcut to execute a job to stdout
fn file_read(path string) !string
fn file_write(path string, text string) !
fn get_logger() log.Logger
Returns a logger object and allows you to specify via environment argument OSAL_LOG_LEVEL the debug level
fn hero_path() !string
fn hostname() !string
fn initname() !string
e.g. systemd, bash, zinit
fn ipaddr_pub_get() !string
Returns the ipaddress as known on the public side is using resolver4.opendns.com
fn is_linux() bool
fn is_linux_arm() bool
fn is_linux_intel() bool
fn is_osx() bool
fn is_osx_arm() bool
fn is_osx_intel() bool
fn is_ubuntu() bool
fn load_env_file(file_path string) !
fn memdb_exists(key string) bool
fn memdb_get(key string) string
fn memdb_set(key string, val string)
fn package_install(name_ string) !
install a package will use right commands per platform
fn package_refresh() !
update the package list
fn ping(args PingArgs) PingResult
if reached in timout result will be True address is e.g. 8.8.8.8 ping means we check if the destination responds
fn platform() PlatformType
fn platform_enum_from_string(platform string) PlatformType
fn process_exists(pid int) bool
fn process_exists_byname(name string) !bool
fn process_kill_recursive(args ProcessKillArgs) !
kill process and all the ones underneith
fn processinfo_children(pid int) !ProcessMap
get all children of 1 process
fn processinfo_get(pid int) !ProcessInfo
get process info from 1 specific process returns
```
pub struct ProcessInfo {
pub mut:
cpu_perc f32
mem_perc f32
cmd string
pid int
ppid int
//resident memory
rss int
}
```
fn processinfo_get_byname(name string) ![]ProcessInfo
fn processinfo_with_children(pid int) !ProcessMap
return the process and its children
fn processmap_get() !ProcessMap
make sure to use new first, so that the connection has been initted then you can get it everywhere
fn profile_path() string
fn profile_path_add(args ProfilePathAddArgs) !
add the following path to a profile
fn profile_path_add_hero() !string
fn profile_path_source() string
return the source statement if the profile exists
fn profile_path_source_and() string
return source $path && . or empty if it doesn't exist
fn sleep(duration int)
sleep in seconds
fn tcp_port_test(args TcpPortTestArgs) bool
test if a tcp port answers
```
address string //192.168.8.8
port int = 22
timeout u16 = 2000 // total time in milliseconds to keep on trying
```
fn user_add(args UserArgs) !int
add's a user if the user does not exist yet
fn user_exists(username string) bool
fn user_id_get(username string) !int
fn usr_local_path() !string
/usr/local on linux, ${os.home_dir()}/hero on osx
fn whoami() !string
fn write_flags[T](options T) string
enum CPUType {
unknown
intel
arm
intel32
arm32
}
enum ErrorType {
exec
timeout
args
}
enum JobStatus {
init
running
error_exec
error_timeout
error_args
done
}
enum PMState {
init
ok
old
}
enum PingResult {
ok
timeout // timeout from ping
unknownhost // means we don't know the hostname its a dns issue
}
enum PlatformType {
unknown
osx
ubuntu
alpine
arch
suse
}
enum RunTime {
bash
python
heroscript
herocmd
v
}
struct CmdAddArgs {
pub mut:
cmdname string
source string @[required] // path where the binary is
symlink bool // if rather than copy do a symlink
reset bool // if existing cmd will delete
// bin_repo_url string = 'https://github.com/freeflowuniverse/freeflow_binary' // binary where we put the results
}
struct Command {
pub mut:
name string // to give a name to your command, good to see logs...
cmd string
description string
timeout int = 3600 // timeout in sec
stdout bool = true
stdout_log bool = true
raise_error bool = true // if false, will not raise an error but still error report
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
work_folder string // location where cmd will be executed
environment map[string]string // env variables
ignore_error_codes []int
scriptpath string // is the path where the script will be put which is executed
scriptkeep bool // means we don't remove the script
debug bool // if debug will put +ex in the script which is being executed and will make sure script stays
shell bool // means we will execute it in a shell interactive
retry int
interactive bool = true
async bool
runtime RunTime
}
struct DownloadArgs {
pub mut:
name string // optional (otherwise derived out of filename)
url string
reset bool // will remove
hash string // if hash is known, will verify what hash is
dest string // if specified will copy to that destination
timeout int = 180
retry int = 3
minsize_kb u32 = 10 // is always in kb
maxsize_kb u32
expand_dir string
expand_file string
}
struct EnvSet {
pub mut:
key string @[required]
value string @[required]
overwrite bool = true
}
struct EnvSetAll {
pub mut:
env map[string]string
clear_before_set bool
overwrite_if_exists bool = true
}
struct Job {
pub mut:
start time.Time
end time.Time
cmd Command
output string
error string
exit_code int
status JobStatus
process ?&os.Process @[skip; str: skip]
runnr int // nr of time it runs, is for retry
}
fn (mut job Job) execute_retry() !
execute the job and wait on result will retry as specified
fn (mut job Job) execute() !
execute the job, start process, process will not be closed . important you need to close the process later by job.close()! otherwise we get zombie processes
fn (mut job Job) wait() !
wait till the job finishes or goes in error
fn (mut job Job) process() !
process (read std.err and std.out of process)
fn (mut job Job) close() !
will wait & close
struct JobError {
Error
pub mut:
job Job
error_type ErrorType
}
struct PingArgs {
pub mut:
address string @[required]
count u8 = 1 // the ping is successful if it got count amount of replies from the other side
timeout u16 = 1 // the time in which the other side should respond in seconds
retry u8
}
struct ProcessInfo {
pub mut:
cpu_perc f32
mem_perc f32
cmd string
pid int
ppid int // parentpid
// resident memory
rss int
}
fn (mut p ProcessInfo) str() string
struct ProcessKillArgs {
pub mut:
name string
pid int
}
struct ProcessMap {
pub mut:
processes []ProcessInfo
lastscan time.Time
state PMState
pids []int
}
struct ProfilePathAddArgs {
pub mut:
path string @[required]
todelete string // see which one to remove
}
struct TcpPortTestArgs {
pub mut:
address string @[required] // 192.168.8.8
port int = 22
timeout u16 = 2000 // total time in milliseconds to keep on trying
}
struct UserArgs {
pub mut:
name string @[required]
}
*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1
aiprompts/binencoder.md Symbolic link
View File

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

1
aiprompts/currency.md Symbolic link
View File

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

340
aiprompts/datatypes.md Normal file
View File

@@ -0,0 +1,340 @@
module datatypes
# datatypes
This module provides implementations of less frequently used, but still common data types.
V's `builtin` module is imported implicitly, and has implementations for arrays, maps and strings. These are good for many applications, but there are a plethora of other useful data structures/containers, like linked lists, priority queues, trees, etc, that allow for algorithms with different time complexities, which may be more suitable for your specific application.
It is implemented using generics, that you have to specialise for the type of your actual elements. For example:
```v
import datatypes
mut stack := datatypes.Stack[int]{}
stack.push(1)
println(stack)
```
## Currently Implemented Datatypes:
- [x] Linked list
- [x] Doubly linked list
- [x] Stack (LIFO)
- [x] Queue (FIFO)
- [x] Min heap (priority queue)
- [x] Set
- [x] Quadtree
- [x] Bloom filter
- [ ] ...
fn new_bloom_filter[T](hash_func fn (T) u32, table_size int, num_functions int) !&BloomFilter[T]
new_bloom_filter creates a new bloom_filter. `table_size` should be greater than 0, and `num_functions` should be 1~16.
fn new_bloom_filter_fast[T](hash_func fn (T) u32) &BloomFilter[T]
new_bloom_filter_fast creates a new bloom_filter. `table_size` is 16384, and `num_functions` is 4.
fn new_ringbuffer[T](s int) RingBuffer[T]
new_ringbuffer creates an empty ring buffer of size `s`.
fn (mut bst BSTree[T]) insert(value T) bool
insert give the possibility to insert an element in the BST.
fn (bst &BSTree[T]) contains(value T) bool
contains checks if an element with a given `value` is inside the BST.
fn (mut bst BSTree[T]) remove(value T) bool
remove removes an element with `value` from the BST.
fn (bst &BSTree[T]) is_empty() bool
is_empty checks if the BST is empty
fn (bst &BSTree[T]) in_order_traversal() []T
in_order_traversal traverses the BST in order, and returns the result as an array.
fn (bst &BSTree[T]) post_order_traversal() []T
post_order_traversal traverses the BST in post order, and returns the result in an array.
fn (bst &BSTree[T]) pre_order_traversal() []T
pre_order_traversal traverses the BST in pre order, and returns the result as an array.
fn (bst &BSTree[T]) to_left(value T) !T
to_left returns the value of the node to the left of the node with `value` specified if it exists, otherwise the a false value is returned.
An example of usage can be the following one
```v
left_value, exist := bst.to_left(10)
```
fn (bst &BSTree[T]) to_right(value T) !T
to_right return the value of the element to the right of the node with `value` specified, if exist otherwise, the boolean value is false An example of usage can be the following one
```v
left_value, exist := bst.to_right(10)
```
fn (bst &BSTree[T]) max() !T
max return the max element inside the BST. Time complexity O(N) if the BST is not balanced
fn (bst &BSTree[T]) min() !T
min return the minimum element in the BST. Time complexity O(N) if the BST is not balanced.
fn (mut b BloomFilter[T]) add(element T)
adds the element to bloom filter.
fn (b &BloomFilter[T]) exists(element T) bool
checks the element is exists.
fn (l &BloomFilter[T]) @union(r &BloomFilter[T]) !&BloomFilter[T]
@union returns the union of the two bloom filters.
fn (l &BloomFilter[T]) intersection(r &BloomFilter[T]) !&BloomFilter[T]
intersection returns the intersection of bloom filters.
fn (list DoublyLinkedList[T]) is_empty() bool
is_empty checks if the linked list is empty
fn (list DoublyLinkedList[T]) len() int
len returns the length of the linked list
fn (list DoublyLinkedList[T]) first() !T
first returns the first element of the linked list
fn (list DoublyLinkedList[T]) last() !T
last returns the last element of the linked list
fn (mut list DoublyLinkedList[T]) push_back(item T)
push_back adds an element to the end of the linked list
fn (mut list DoublyLinkedList[T]) push_front(item T)
push_front adds an element to the beginning of the linked list
fn (mut list DoublyLinkedList[T]) push_many(elements []T, direction Direction)
push_many adds array of elements to the beginning of the linked list
fn (mut list DoublyLinkedList[T]) pop_back() !T
pop_back removes the last element of the linked list
fn (mut list DoublyLinkedList[T]) pop_front() !T
pop_front removes the last element of the linked list
fn (mut list DoublyLinkedList[T]) insert(idx int, item T) !
insert adds an element to the linked list at the given index
fn (list &DoublyLinkedList[T]) index(item T) !int
index searches the linked list for item and returns the forward index or none if not found.
fn (mut list DoublyLinkedList[T]) delete(idx int)
delete removes index idx from the linked list and is safe to call for any idx.
fn (list DoublyLinkedList[T]) str() string
str returns a string representation of the linked list
fn (list DoublyLinkedList[T]) array() []T
array returns a array representation of the linked list
fn (mut list DoublyLinkedList[T]) next() ?T
next implements the iter interface to use DoublyLinkedList with V's `for x in list {` loop syntax.
fn (mut list DoublyLinkedList[T]) iterator() DoublyListIter[T]
iterator returns a new iterator instance for the `list`.
fn (mut list DoublyLinkedList[T]) back_iterator() DoublyListIterBack[T]
back_iterator returns a new backwards iterator instance for the `list`.
fn (mut iter DoublyListIterBack[T]) next() ?T
next returns *the previous* element of the list, or `none` when the start of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (mut iter DoublyListIter[T]) next() ?T
next returns *the next* element of the list, or `none` when the end of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (list LinkedList[T]) is_empty() bool
is_empty checks if the linked list is empty
fn (list LinkedList[T]) len() int
len returns the length of the linked list
fn (list LinkedList[T]) first() !T
first returns the first element of the linked list
fn (list LinkedList[T]) last() !T
last returns the last element of the linked list
fn (list LinkedList[T]) index(idx int) !T
index returns the element at the given index of the linked list
fn (mut list LinkedList[T]) push(item T)
push adds an element to the end of the linked list
fn (mut list LinkedList[T]) push_many(elements []T)
push adds an array of elements to the end of the linked list
fn (mut list LinkedList[T]) pop() !T
pop removes the last element of the linked list
fn (mut list LinkedList[T]) shift() !T
shift removes the first element of the linked list
fn (mut list LinkedList[T]) insert(idx int, item T) !
insert adds an element to the linked list at the given index
fn (mut list LinkedList[T]) prepend(item T)
prepend adds an element to the beginning of the linked list (equivalent to insert(0, item))
fn (list LinkedList[T]) str() string
str returns a string representation of the linked list
fn (list LinkedList[T]) array() []T
array returns a array representation of the linked list
fn (mut list LinkedList[T]) next() ?T
next implements the iteration interface to use LinkedList with V's `for` loop syntax.
fn (mut list LinkedList[T]) iterator() ListIter[T]
iterator returns a new iterator instance for the `list`.
fn (mut iter ListIter[T]) next() ?T
next returns the next element of the list, or `none` when the end of the list is reached. It is called by V's `for x in iter{` on each iteration.
fn (mut heap MinHeap[T]) insert(item T)
insert adds an element to the heap.
fn (mut heap MinHeap[T]) insert_many(elements []T)
insert array of elements to the heap.
fn (mut heap MinHeap[T]) pop() !T
pop removes the top-most element from the heap.
fn (heap MinHeap[T]) peek() !T
peek gets the top-most element from the heap without removing it.
fn (heap MinHeap[T]) len() int
len returns the number of elements in the heap.
fn (queue Queue[T]) is_empty() bool
is_empty checks if the queue is empty
fn (queue Queue[T]) len() int
len returns the length of the queue
fn (queue Queue[T]) peek() !T
peek returns the head of the queue (first element added)
fn (queue Queue[T]) last() !T
last returns the tail of the queue (last element added)
fn (queue Queue[T]) index(idx int) !T
index returns the element at the given index of the queue
fn (mut queue Queue[T]) push(item T)
push adds an element to the tail of the queue
fn (mut queue Queue[T]) pop() !T
pop removes the element at the head of the queue and returns it
fn (queue Queue[T]) str() string
str returns a string representation of the queue
fn (queue Queue[T]) array() []T
array returns a array representation of the queue
fn (mut rb RingBuffer[T]) push(element T) !
push adds an element to the ring buffer.
fn (mut rb RingBuffer[T]) pop() !T
pop returns the oldest element in the buffer.
fn (mut rb RingBuffer[T]) push_many(elements []T) !
push_many pushes an array to the buffer.
fn (mut rb RingBuffer[T]) pop_many(n u64) ![]T
pop_many returns `n` elements of the buffer starting with the oldest one.
fn (rb RingBuffer[T]) is_empty() bool
is_empty returns `true` if the ring buffer is empty, `false` otherwise.
fn (rb RingBuffer[T]) is_full() bool
is_full returns `true` if the ring buffer is full, `false` otherwise.
fn (rb RingBuffer[T]) capacity() int
capacity returns the capacity of the ring buffer.
fn (mut rb RingBuffer[T]) clear()
clear empties the ring buffer and all pushed elements.
fn (rb RingBuffer[T]) occupied() int
occupied returns the occupied capacity of the buffer.
fn (rb RingBuffer[T]) remaining() int
remaining returns the remaining capacity of the buffer.
fn (set Set[T]) exists(element T) bool
checks the element is exists.
fn (mut set Set[T]) add(element T)
adds the element to set, if it is not present already.
fn (mut set Set[T]) remove(element T)
removes the element from set.
fn (set Set[T]) pick() !T
pick returns an arbitrary element of set, if set is not empty.
fn (mut set Set[T]) rest() ![]T
rest returns the set consisting of all elements except for the arbitrary element.
fn (mut set Set[T]) pop() !T
pop returns an arbitrary element and deleting it from set.
fn (mut set Set[T]) clear()
delete all elements of set.
fn (l Set[T]) == (r Set[T]) bool
== checks whether the two given sets are equal (i.e. contain all and only the same elements).
fn (set Set[T]) is_empty() bool
is_empty checks whether the set is empty or not.
fn (set Set[T]) size() int
size returns the number of elements in the set.
fn (set Set[T]) copy() Set[T]
copy returns a copy of all the elements in the set.
fn (mut set Set[T]) add_all(elements []T)
add_all adds the whole `elements` array to the set
fn (l Set[T]) @union(r Set[T]) Set[T]
@union returns the union of the two sets.
fn (l Set[T]) intersection(r Set[T]) Set[T]
intersection returns the intersection of sets.
fn (l Set[T]) - (r Set[T]) Set[T]
- returns the difference of sets.
fn (l Set[T]) subset(r Set[T]) bool
subset returns true if the set `r` is a subset of the set `l`.
fn (stack Stack[T]) is_empty() bool
is_empty checks if the stack is empty
fn (stack Stack[T]) len() int
len returns the length of the stack
fn (stack Stack[T]) peek() !T
peek returns the top of the stack
fn (mut stack Stack[T]) push(item T)
push adds an element to the top of the stack
fn (mut stack Stack[T]) pop() !T
pop removes the element at the top of the stack and returns it
fn (stack Stack[T]) str() string
str returns a string representation of the stack
fn (stack Stack[T]) array() []T
array returns a array representation of the stack
enum Direction {
front
back
}
struct AABB {
pub mut:
x f64
y f64
width f64
height f64
}
struct BSTree[T] {
mut:
root &BSTreeNode[T] = unsafe { 0 }
}
Pure Binary Seach Tree implementation
Pure V implementation of the Binary Search Tree Time complexity of main operation O(log N) Space complexity O(N)
struct DoublyLinkedList[T] {
mut:
head &DoublyListNode[T] = unsafe { 0 }
tail &DoublyListNode[T] = unsafe { 0 }
// Internal iter pointer for allowing safe modification
// of the list while iterating. TODO: use an option
// instead of a pointer to determine it is initialized.
iter &DoublyListIter[T] = unsafe { 0 }
len int
}
DoublyLinkedList[T] represents a generic doubly linked list of elements, each of type T.
struct DoublyListIter[T] {
mut:
node &DoublyListNode[T] = unsafe { 0 }
}
DoublyListIter[T] is an iterator for DoublyLinkedList. It starts from *the start* and moves forwards to *the end* of the list. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. A DoublyListIter iterator instance always traverses the list from *start to finish*.
struct DoublyListIterBack[T] {
mut:
node &DoublyListNode[T] = unsafe { 0 }
}
DoublyListIterBack[T] is an iterator for DoublyLinkedList. It starts from *the end* and moves backwards to *the start* of the list. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. A DoublyListIterBack iterator instance always traverses the list from *finish to start*.
struct LinkedList[T] {
mut:
head &ListNode[T] = unsafe { 0 }
len int
// Internal iter pointer for allowing safe modification
// of the list while iterating. TODO: use an option
// instead of a pointer to determine if it is initialized.
iter &ListIter[T] = unsafe { 0 }
}
struct ListIter[T] {
mut:
node &ListNode[T] = unsafe { 0 }
}
ListIter[T] is an iterator for LinkedList. It can be used with V's `for x in iter {` construct. One list can have multiple independent iterators, pointing to different positions/places in the list. An iterator instance always traverses the list from start to finish.
struct ListNode[T] {
mut:
data T
next &ListNode[T] = unsafe { 0 }
}
struct MinHeap[T] {
mut:
data []T
}
MinHeap is a binary minimum heap data structure.
struct Quadtree {
pub mut:
perimeter AABB
capacity int
depth int
level int
particles []AABB
nodes []Quadtree
}
fn (mut q Quadtree) create(x f64, y f64, width f64, height f64, capacity int, depth int, level int) Quadtree
create returns a new configurable root node for the tree.
fn (mut q Quadtree) insert(p AABB)
insert recursively adds a particle in the correct index of the tree.
fn (mut q Quadtree) retrieve(p AABB) []AABB
retrieve recursively checks if a particle is in a specific index of the tree.
fn (mut q Quadtree) clear()
clear flushes out nodes and particles from the tree.
fn (q Quadtree) get_nodes() []Quadtree
get_nodes recursively returns the subdivisions the tree has.
struct Queue[T] {
mut:
elements LinkedList[T]
}
struct RingBuffer[T] {
mut:
reader int // index of the tail where data is going to be read
writer int // index of the head where data is going to be written
content []T
}
RingBuffer represents a ring buffer also known as a circular buffer.
struct Set[T] {
mut:
elements map[T]u8
}
struct Stack[T] {
mut:
elements []T
}

View File

@@ -0,0 +1,79 @@
## 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'
}]
}
```

1
aiprompts/docker.md Symbolic link
View File

@@ -0,0 +1 @@
../crystallib/virt/docker/readme.md

1
aiprompts/gittools.md Symbolic link
View File

@@ -0,0 +1 @@
../lib/develop/gittools/README.md

124
aiprompts/html_parser.md Normal file
View File

@@ -0,0 +1,124 @@
module net.html
net/html is an **HTML Parser** written in pure V.
## Usage
```v
import net.html
fn main() {
doc := html.parse('<html><body><h1 class="title">Hello world!</h1></body></html>')
tag := doc.get_tags(name: 'h1')[0] // <h1>Hello world!</h1>
println(tag.name) // h1
println(tag.content) // Hello world!
println(tag.attributes) // {'class':'title'}
println(tag.str()) // <h1 class="title">Hello world!</h1>
}
```
More examples found on [`parser_test.v`](parser_test.v) and [`html_test.v`](html_test.v)
fn parse(text string) DocumentObjectModel
parse parses and returns the DOM from the given text.
Note: this function converts tags to lowercase. E.g. <MyTag>content</MyTag> is parsed as <mytag>content</mytag>.
fn parse_file(filename string) DocumentObjectModel
parse_file parses and returns the DOM from the contents of a file.
Note: this function converts tags to lowercase. E.g. <MyTag>content</MyTag> is parsed as <mytag>content</mytag>.
enum CloseTagType {
in_name
new_tag
}
struct DocumentObjectModel {
mut:
root &Tag = unsafe { nil }
constructed bool
btree BTree
all_tags []&Tag
all_attributes map[string][]&Tag
close_tags map[string]bool // add a counter to see count how many times is closed and parse correctly
attributes map[string][]string
tag_attributes map[string][][]&Tag
tag_type map[string][]&Tag
debug_file os.File
}
The W3C Document Object Model (DOM) is a platform and language-neutral interface that allows programs and scripts to dynamically access and update the content, structure, and style of a document.
https://www.w3.org/TR/WD-DOM/introduction.html
fn (dom DocumentObjectModel) get_root() &Tag
get_root returns the root of the document.
fn (dom DocumentObjectModel) get_tag(name string) []&Tag
get_tag retrieves all tags in the document that have the given tag name.
fn (dom DocumentObjectModel) get_tags(options GetTagsOptions) []&Tag
get_tags returns all tags stored in the document.
fn (dom DocumentObjectModel) get_tags_by_class_name(names ...string) []&Tag
get_tags_by_class_name retrieves all tags recursively in the document root that have the given class name(s).
fn (dom DocumentObjectModel) get_tag_by_attribute(name string) []&Tag
get_tag_by_attribute retrieves all tags in the document that have the given attribute name.
fn (dom DocumentObjectModel) get_tags_by_attribute(name string) []&Tag
get_tags_by_attribute retrieves all tags in the document that have the given attribute name.
fn (mut dom DocumentObjectModel) get_tags_by_attribute_value(name string, value string) []&Tag
get_tags_by_attribute_value retrieves all tags in the document that have the given attribute name and value.
fn (mut dom DocumentObjectModel) get_tag_by_attribute_value(name string, value string) []&Tag
get_tag_by_attribute_value retrieves all tags in the document that have the given attribute name and value.
struct GetTagsOptions {
pub:
name string
}
struct Parser {
mut:
dom DocumentObjectModel
lexical_attributes LexicalAttributes = LexicalAttributes{
current_tag: &Tag{}
}
filename string = 'direct-parse'
initialized bool
tags []&Tag
debug_file os.File
}
Parser is responsible for reading the HTML strings and converting them into a `DocumentObjectModel`.
fn (mut parser Parser) add_code_tag(name string)
This function is used to add a tag for the parser ignore it's content. For example, if you have an html or XML with a custom tag, like `<script>`, using this function, like `add_code_tag('script')` will make all `script` tags content be jumped, so you still have its content, but will not confuse the parser with it's `>` or `<`.
fn (mut parser Parser) split_parse(data string)
split_parse parses the HTML fragment
fn (mut parser Parser) parse_html(data string)
parse_html parses the given HTML string
fn (mut parser Parser) finalize()
finalize finishes the parsing stage .
fn (mut parser Parser) get_dom() DocumentObjectModel
get_dom returns the parser's current DOM representation.
struct Tag {
pub mut:
name string
content string
children []&Tag
attributes map[string]string // attributes will be like map[name]value
last_attribute string
class_set datatypes.Set[string]
parent &Tag = unsafe { nil }
position_in_parent int
closed bool
close_type CloseTagType = .in_name
}
Tag holds the information of an HTML tag.
fn (tag Tag) text() string
text returns the text contents of the tag.
fn (tag &Tag) str() string
fn (tag &Tag) get_tag(name string) ?&Tag
get_tag retrieves the first found child tag in the tag that has the given tag name.
fn (tag &Tag) get_tags(name string) []&Tag
get_tags retrieves all child tags recursively in the tag that have the given tag name.
fn (tag &Tag) get_tag_by_attribute(name string) ?&Tag
get_tag_by_attribute retrieves the first found child tag in the tag that has the given attribute name.
fn (tag &Tag) get_tags_by_attribute(name string) []&Tag
get_tags_by_attribute retrieves all child tags recursively in the tag that have the given attribute name.
fn (tag &Tag) get_tag_by_attribute_value(name string, value string) ?&Tag
get_tag_by_attribute_value retrieves the first found child tag in the tag that has the given attribute name and value.
fn (tag &Tag) get_tags_by_attribute_value(name string, value string) []&Tag
get_tags_by_attribute_value retrieves all child tags recursively in the tag that have the given attribute name and value.
fn (tag &Tag) get_tag_by_class_name(names ...string) ?&Tag
get_tag_by_class_name retrieves the first found child tag in the tag that has the given class name(s).
fn (tag &Tag) get_tags_by_class_name(names ...string) []&Tag
get_tags_by_class_name retrieves all child tags recursively in the tag that have the given class name(s).

1
aiprompts/httpconnection.md Symbolic link
View File

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

105
aiprompts/io.md Normal file
View File

@@ -0,0 +1,105 @@
module io
## Description
`io` provides common interfaces for buffered reading/writing of data.
const read_all_len = 10 * 1024
const read_all_grow_len = 1024
fn cp(mut src Reader, mut dst Writer) !
cp copies from `src` to `dst` by allocating a maximum of 1024 bytes buffer for reading until either EOF is reached on `src` or an error occurs. An error is returned if an error is encountered during write.
fn make_readerwriter(r Reader, w Writer) ReaderWriterImpl
make_readerwriter takes a rstream and a wstream and makes an rwstream with them.
fn new_buffered_reader(o BufferedReaderConfig) &BufferedReader
new_buffered_reader creates a new BufferedReader.
fn new_multi_writer(writers ...Writer) Writer
new_multi_writer returns a Writer that writes to all writers. The write function of the returned Writer writes to all writers of the MultiWriter, returns the length of bytes written, and if any writer fails to write the full length an error is returned and writing to other writers stops, and if any writer returns an error the error is returned immediately and writing to other writers stops.
fn read_all(config ReadAllConfig) ![]u8
read_all reads all bytes from a reader until either a 0 length read or if read_to_end_of_stream is true then the end of the stream (`none`).
fn read_any(mut r Reader) ![]u8
read_any reads any available bytes from a reader (until the reader returns a read of 0 length).
interface RandomReader {
read_from(pos u64, mut buf []u8) !int
}
RandomReader represents a stream of readable data from at a random location.
interface RandomWriter {
write_to(pos u64, buf []u8) !int
}
RandomWriter is the interface that wraps the `write_to` method, which writes `buf.len` bytes to the underlying data stream at a random `pos`.
interface Reader {
// read reads up to buf.len bytes and places
// them into buf.
// A type that implements this should return
// `io.Eof` on end of stream (EOF) instead of just returning 0
mut:
read(mut buf []u8) !int
}
Reader represents a stream of data that can be read.
interface ReaderWriter {
Reader
Writer
}
ReaderWriter represents a stream that can be read and written.
interface Writer {
mut:
write(buf []u8) !int
}
Writer is the interface that wraps the `write` method, which writes `buf.len` bytes to the underlying data stream.
fn (mut r ReaderWriterImpl) read(mut buf []u8) !int
read reads up to `buf.len` bytes into `buf`. It returns the number of bytes read or any error encountered.
fn (mut r ReaderWriterImpl) write(buf []u8) !int
write writes `buf.len` bytes from `buf` to the underlying data stream. It returns the number of bytes written or any error encountered.
struct BufferedReadLineConfig {
pub:
delim u8 = `\n` // line delimiter
}
BufferedReadLineConfig are options that can be given to the read_line() function.
struct BufferedReader {
mut:
reader Reader
buf []u8
offset int // current offset in the buffer
len int
fails int // how many times fill_buffer has read 0 bytes in a row
mfails int // maximum fails, after which we can assume that the stream has ended
pub mut:
end_of_stream bool // whether we reached the end of the upstream reader
total_read int // total number of bytes read
}
BufferedReader provides a buffered interface for a reader.
fn (mut r BufferedReader) read(mut buf []u8) !int
read fufills the Reader interface.
fn (mut r BufferedReader) free()
free deallocates the memory for a buffered reader's internal buffer.
fn (r BufferedReader) end_of_stream() bool
end_of_stream returns whether the end of the stream was reached.
fn (mut r BufferedReader) read_line(config BufferedReadLineConfig) !string
read_line attempts to read a line from the buffered reader. It will read until it finds the specified line delimiter such as (\n, the default or \0) or the end of stream.
struct BufferedReaderConfig {
pub:
reader Reader
cap int = 128 * 1024 // large for fast reading of big(ish) files
retries int = 2 // how many times to retry before assuming the stream ended
}
BufferedReaderConfig are options that can be given to a buffered reader.
struct Eof {
Error
}
/ Eof error means that we reach the end of the stream.
struct MultiWriter {
pub mut:
writers []Writer
}
MultiWriter writes to all its writers.
fn (mut m MultiWriter) write(buf []u8) !int
write writes to all writers of the MultiWriter. Returns the length of bytes written. If any writer fails to write the full length an error is returned and writing to other writers stops. If any writer returns an error the error is returned immediately and writing to other writers stops.
struct NotExpected {
cause string
code int
}
NotExpected is a generic error that means that we receave a not expected error.
struct ReadAllConfig {
pub:
read_to_end_of_stream bool
reader Reader
}
ReadAllConfig allows options to be passed for the behaviour of read_all.

354
aiprompts/net.md Normal file
View File

@@ -0,0 +1,354 @@
module net
## Description
`net` provides networking functions. It is mostly a wrapper to BSD sockets, so you can listen on a port, connect to remote TCP/UDP services, and communicate with them.
const msg_nosignal = 0x4000
const err_connection_refused = error_with_code('net: connection refused', errors_base + 10)
const err_option_wrong_type = error_with_code('net: set_option_xxx option wrong type',
errors_base + 3)
const opts_can_set = [
SocketOption.broadcast,
.debug,
.dont_route,
.keep_alive,
.linger,
.oob_inline,
.receive_buf_size,
.receive_low_size,
.receive_timeout,
.send_buf_size,
.send_low_size,
.send_timeout,
.ipv6_only,
]
const error_eagain = C.EAGAIN
const err_port_out_of_range = error_with_code('net: port out of range', errors_base + 5)
const opts_bool = [SocketOption.broadcast, .debug, .dont_route, .error, .keep_alive, .oob_inline]
const err_connect_failed = error_with_code('net: connect failed', errors_base + 7)
const errors_base = 0
Well defined errors that are returned from socket functions
const opts_int = [
SocketOption.receive_buf_size,
.receive_low_size,
.receive_timeout,
.send_buf_size,
.send_low_size,
.send_timeout,
]
const error_eintr = C.EINTR
const error_ewouldblock = C.EWOULDBLOCK
const err_no_udp_remote = error_with_code('net: no udp remote', errors_base + 6)
const error_einprogress = C.EINPROGRESS
const err_timed_out_code = errors_base + 9
const err_connect_timed_out = error_with_code('net: connect timed out', errors_base + 8)
const err_new_socket_failed = error_with_code('net: new_socket failed to create socket',
errors_base + 1)
const msg_dontwait = C.MSG_DONTWAIT
const infinite_timeout = time.infinite
infinite_timeout should be given to functions when an infinite_timeout is wanted (i.e. functions only ever return with data)
const no_timeout = time.Duration(0)
no_timeout should be given to functions when no timeout is wanted (i.e. all functions return instantly)
const err_timed_out = error_with_code('net: op timed out', errors_base + 9)
const tcp_default_read_timeout = 30 * time.second
const err_option_not_settable = error_with_code('net: set_option_xxx option not settable',
errors_base + 2)
const tcp_default_write_timeout = 30 * time.second
fn addr_from_socket_handle(handle int) Addr
addr_from_socket_handle returns an address, based on the given integer socket `handle`
fn close(handle int) !
close a socket, given its file descriptor `handle`. In non-blocking mode, if `close()` does not succeed immediately, it causes an error to be propagated to `TcpSocket.close()`, which is not intended. Therefore, `select` is used just like `connect()`.
fn default_tcp_dialer() Dialer
default_tcp_dialer will give you an instance of Dialer, that is suitable for making new tcp connections.
fn dial_tcp(oaddress string) !&TcpConn
dial_tcp will try to create a new TcpConn to the given address.
fn dial_tcp_with_bind(saddr string, laddr string) !&TcpConn
dial_tcp_with_bind will bind the given local address `laddr` and dial.
fn dial_udp(raddr string) !&UdpConn
fn error_code() int
fn listen_tcp(family AddrFamily, saddr string, options ListenOptions) !&TcpListener
fn listen_udp(laddr string) !&UdpConn
fn new_ip(port u16, addr [4]u8) Addr
new_ip creates a new Addr from the IPv4 address family, based on the given port and addr
fn new_ip6(port u16, addr [16]u8) Addr
new_ip6 creates a new Addr from the IP6 address family, based on the given port and addr
fn peer_addr_from_socket_handle(handle int) !Addr
peer_addr_from_socket_handle retrieves the ip address and port number, given a socket handle
fn resolve_addrs(addr string, family AddrFamily, @type SocketType) ![]Addr
resolve_addrs converts the given `addr`, `family` and `@type` to a list of addresses
fn resolve_addrs_fuzzy(addr string, @type SocketType) ![]Addr
resolve_addrs converts the given `addr` and `@type` to a list of addresses
fn resolve_ipaddrs(addr string, family AddrFamily, typ SocketType) ![]Addr
resolve_ipaddrs converts the given `addr`, `family` and `typ` to a list of addresses
fn set_blocking(handle int, state bool) !
set_blocking will change the state of the socket to either blocking, when state is true, or non blocking (false).
fn shutdown(handle int, config ShutdownConfig) int
shutdown shutsdown a socket, given its file descriptor `handle`. By default it shuts it down in both directions, both for reading and for writing. You can change that using `net.shutdown(handle, how: .read)` or `net.shutdown(handle, how: .write)` In non-blocking mode, `shutdown()` may not succeed immediately, so `select` is also used to make sure that the function doesn't return an incorrect result.
fn socket_error(potential_code int) !int
fn socket_error_message(potential_code int, s string) !int
fn split_address(addr string) !(string, u16)
split_address splits an address into its host name and its port
fn tcp_socket_from_handle_raw(sockfd int) TcpSocket
tcp_socket_from_handle_raw is similar to tcp_socket_from_handle, but it does not modify any socket options
fn validate_port(port int) !u16
validate_port checks whether a port is valid and returns the port or an error
fn wrap_error(error_code int) !
interface Connection {
addr() !Addr
peer_addr() !Addr
mut:
read(mut []u8) !int
write([]u8) !int
close() !
}
Connection provides a generic SOCK_STREAM style interface that protocols can use as a base connection object to support TCP, UNIX Domain Sockets and various proxying solutions.
interface Dialer {
dial(address string) !Connection
}
Dialer is an abstract dialer interface for producing connections to adresses.
fn (mut s TcpSocket) set_option_bool(opt SocketOption, value bool) !
fn (mut s TcpSocket) set_option_int(opt SocketOption, value int) !
fn (mut s TcpSocket) set_dualstack(on bool) !
fn (mut s TcpSocket) bind(addr string) !
bind a local rddress for TcpSocket
fn (mut s UdpSocket) set_option_bool(opt SocketOption, value bool) !
fn (mut s UdpSocket) set_dualstack(on bool) !
enum AddrFamily {
unix = C.AF_UNIX
ip = C.AF_INET
ip6 = C.AF_INET6
unspec = C.AF_UNSPEC
}
AddrFamily are the available address families
enum ShutdownDirection {
read
write
read_and_write
}
ShutdownDirection is used by `net.shutdown`, for specifying the direction for which the communication will be cut.
enum SocketOption {
// TODO: SO_ACCEPT_CONN is not here because windows doesn't support it
// and there is no easy way to define it
broadcast = C.SO_BROADCAST
debug = C.SO_DEBUG
dont_route = C.SO_DONTROUTE
error = C.SO_ERROR
keep_alive = C.SO_KEEPALIVE
linger = C.SO_LINGER
oob_inline = C.SO_OOBINLINE
reuse_addr = C.SO_REUSEADDR
receive_buf_size = C.SO_RCVBUF
receive_low_size = C.SO_RCVLOWAT
receive_timeout = C.SO_RCVTIMEO
send_buf_size = C.SO_SNDBUF
send_low_size = C.SO_SNDLOWAT
send_timeout = C.SO_SNDTIMEO
socket_type = C.SO_TYPE
ipv6_only = C.IPV6_V6ONLY
}
enum SocketType {
udp = C.SOCK_DGRAM
tcp = C.SOCK_STREAM
seqpacket = C.SOCK_SEQPACKET
}
SocketType are the available sockets
struct Addr {
pub:
len u8
f u8
addr AddrData
}
fn (a Addr) family() AddrFamily
family returns the family/kind of the given address `a`
fn (a Addr) len() u32
len returns the length in bytes of the address `a`, depending on its family
fn (a Addr) port() !u16
port returns the ip or ip6 port of the given address `a`
fn (a Addr) str() string
str returns a string representation of the address `a`
struct C.addrinfo {
mut:
ai_family int
ai_socktype int
ai_flags int
ai_protocol int
ai_addrlen int
ai_addr voidptr
ai_canonname voidptr
ai_next voidptr
}
struct C.fd_set {}
struct C.sockaddr_in {
mut:
sin_len u8
sin_family u8
sin_port u16
sin_addr u32
sin_zero [8]char
}
struct C.sockaddr_in6 {
mut:
// 1 + 1 + 2 + 4 + 16 + 4 = 28;
sin6_len u8 // 1
sin6_family u8 // 1
sin6_port u16 // 2
sin6_flowinfo u32 // 4
sin6_addr [16]u8 // 16
sin6_scope_id u32 // 4
}
struct C.sockaddr_un {
mut:
sun_len u8
sun_family u8
sun_path [max_unix_path]char
}
struct Ip {
port u16
addr [4]u8
// Pad to size so that socket functions
// dont complain to us (see in.h and bind())
// TODO(emily): I would really like to use
// some constant calculations here
// so that this doesnt have to be hardcoded
sin_pad [8]u8
}
fn (a Ip) str() string
str returns a string representation of `a`
struct Ip6 {
port u16
flow_info u32
addr [16]u8
scope_id u32
}
fn (a Ip6) str() string
str returns a string representation of `a`
struct ListenOptions {
pub:
dualstack bool = true
backlog int = 128
}
struct ShutdownConfig {
pub:
how ShutdownDirection = .read_and_write
}
struct Socket {
pub:
handle int
}
fn (s &Socket) address() !Addr
address gets the address of a socket
struct TCPDialer {}
TCPDialer is a concrete instance of the Dialer interface, for creating tcp connections.
fn (t TCPDialer) dial(address string) !Connection
dial will try to create a new abstract connection to the given address. It will return an error, if that is not possible.
struct TcpConn {
pub mut:
sock TcpSocket
handle int
write_deadline time.Time
read_deadline time.Time
read_timeout time.Duration
write_timeout time.Duration
is_blocking bool = true
}
fn (c &TcpConn) addr() !Addr
fn (mut c TcpConn) close() !
close closes the tcp connection
fn (mut con TcpConn) get_blocking() bool
get_blocking returns whether the connection is in a blocking state, that is calls to .read_line, C.recv etc will block till there is new data arrived, instead of returning immediately.
fn (c &TcpConn) peer_addr() !Addr
peer_addr retrieves the ip address and port number used by the peer
fn (c &TcpConn) peer_ip() !string
peer_ip retrieves the ip address used by the peer, and returns it as a string
fn (c TcpConn) read(mut buf []u8) !int
read reads data from the tcp connection into the mutable buffer `buf`. The number of bytes read is limited to the length of the buffer `buf.len`. The returned value is the number of read bytes (between 0 and `buf.len`).
fn (mut c TcpConn) read_deadline() !time.Time
fn (mut con TcpConn) read_line() string
read_line is a *simple*, *non customizable*, blocking line reader. It will return a line, ending with LF, or just '', on EOF.
Note: if you want more control over the buffer, please use a buffered IO reader instead: `io.new_buffered_reader({reader: io.make_reader(con)})`
fn (mut con TcpConn) read_line_max(max_line_len int) string
read_line_max is a *simple*, *non customizable*, blocking line reader. It will return a line, ending with LF, '' on EOF. It stops reading, when the result line length exceeds max_line_len.
fn (c TcpConn) read_ptr(buf_ptr &u8, len int) !int
read_ptr reads data from the tcp connection to the given buffer. It reads at most `len` bytes. It returns the number of actually read bytes, which can vary between 0 to `len`.
fn (c &TcpConn) read_timeout() time.Duration
fn (mut con TcpConn) set_blocking(state bool) !
set_blocking will change the state of the connection to either blocking, when state is true, or non blocking (false). The default for `net` tcp connections is the blocking mode. Calling .read_line will set the connection to blocking mode. In general, changing the blocking mode after a successful connection may cause unexpected surprises, so this function is not recommended to be called anywhere but for this file.
fn (mut c TcpConn) set_read_deadline(deadline time.Time)
fn (mut c TcpConn) set_read_timeout(t time.Duration)
fn (mut c TcpConn) set_sock() !
set_sock initialises the c.sock field. It should be called after `.accept_only()!`.
Note: just use `.accept()!`. In most cases it is simpler, and calls `.set_sock()!` for you.
fn (mut c TcpConn) set_write_deadline(deadline time.Time)
fn (mut c TcpConn) set_write_timeout(t time.Duration)
fn (c TcpConn) str() string
fn (c TcpConn) wait_for_read() !
fn (mut c TcpConn) wait_for_write() !
fn (mut c TcpConn) write(bytes []u8) !int
write blocks and attempts to write all data
fn (mut c TcpConn) write_deadline() !time.Time
fn (mut c TcpConn) write_ptr(b &u8, len int) !int
write_ptr blocks and attempts to write all data
fn (mut c TcpConn) write_string(s string) !int
write_string blocks and attempts to write all data
fn (c &TcpConn) write_timeout() time.Duration
struct TcpListener {
pub mut:
sock TcpSocket
accept_timeout time.Duration
accept_deadline time.Time
is_blocking bool = true
}
fn (mut l TcpListener) accept() !&TcpConn
accept a tcp connection from an external source to the listener `l`.
fn (mut l TcpListener) accept_only() !&TcpConn
accept_only accepts a tcp connection from an external source to the listener `l`. Unlike `accept`, `accept_only` *will not call* `.set_sock()!` on the result, and is thus faster.
Note: you *need* to call `.set_sock()!` manually, before using theconnection after calling `.accept_only()!`, but that does not have to happen in the same thread that called `.accept_only()!`. The intention of this API, is to have a more efficient way to accept connections, that are later processed by a thread pool, while the main thread remains active, so that it can accept other connections. See also vlib/vweb/vweb.v .
If you do not need that, just call `.accept()!` instead, which will call `.set_sock()!` for you.
fn (c &TcpListener) accept_deadline() !time.Time
fn (mut c TcpListener) set_accept_deadline(deadline time.Time)
fn (c &TcpListener) accept_timeout() time.Duration
fn (mut c TcpListener) set_accept_timeout(t time.Duration)
fn (mut c TcpListener) wait_for_accept() !
fn (mut c TcpListener) close() !
fn (c &TcpListener) addr() !Addr
struct UdpConn {
pub mut:
sock UdpSocket
mut:
write_deadline time.Time
read_deadline time.Time
read_timeout time.Duration
write_timeout time.Duration
}
fn (mut c UdpConn) write_ptr(b &u8, len int) !int
sock := UdpSocket{ handle: sbase.handle l: local r: resolve_wrapper(raddr) } }
fn (mut c UdpConn) write(buf []u8) !int
fn (mut c UdpConn) write_string(s string) !int
fn (mut c UdpConn) write_to_ptr(addr Addr, b &u8, len int) !int
fn (mut c UdpConn) write_to(addr Addr, buf []u8) !int
write_to blocks and writes the buf to the remote addr specified
fn (mut c UdpConn) write_to_string(addr Addr, s string) !int
write_to_string blocks and writes the buf to the remote addr specified
fn (mut c UdpConn) read(mut buf []u8) !(int, Addr)
read reads from the socket into buf up to buf.len returning the number of bytes read
fn (c &UdpConn) read_deadline() !time.Time
fn (mut c UdpConn) set_read_deadline(deadline time.Time)
fn (c &UdpConn) write_deadline() !time.Time
fn (mut c UdpConn) set_write_deadline(deadline time.Time)
fn (c &UdpConn) read_timeout() time.Duration
fn (mut c UdpConn) set_read_timeout(t time.Duration)
fn (c &UdpConn) write_timeout() time.Duration
fn (mut c UdpConn) set_write_timeout(t time.Duration)
fn (mut c UdpConn) wait_for_read() !
fn (mut c UdpConn) wait_for_write() !
fn (c &UdpConn) str() string
fn (mut c UdpConn) close() !
struct Unix {
path [max_unix_path]char
}

1
aiprompts/osal.md Symbolic link
View File

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

1
aiprompts/ourdb.md Symbolic link
View File

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

1
aiprompts/ourtime.md Symbolic link
View File

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

1
aiprompts/paramsparser.md Symbolic link
View File

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

6
aiprompts/readme.md Normal file
View File

@@ -0,0 +1,6 @@
to generate relevant instructions
v doc -f text -m . -o stdout -readme -comments | code -

516
aiprompts/regex.md Normal file
View File

@@ -0,0 +1,516 @@
# Regex Library
#### Differences with PCRE:
> regex is not PCRE compatible.
- The basic element is the token not the sequence of symbols, and the mostsimple token, is a single character.
| the OR operator acts on tokens, for example abc|ebc is notabc OR ebc. Instead it is evaluated like ab, followed by c OR e, followed by bc, because the token is the base element, not the sequence of symbols.
- Two char classes with an OR in the middle is a syntax error.
- The match operation stops at the end of the string. It does NOT stopat new line characters.
- The tokens are the atomic units, used by this regex engine. They can be one of the following:
- Simple char, This token is a simple single character like a or b etc.
- Match positional delimiters
- ^ Matches the start of the string.
- $ Matches the end of the string.
#### Char class (cc)
- The character classes match all the chars specified inside. Use square brackets [ ] to enclose them.
- The sequence of the chars in the character class, is evaluated with an OR op.
- For example, the cc [abc], matches any character, that is a or b or c, but it doesn't match C or z.
- Inside a cc, it is possible to specify a "range" of characters, for example [ad-h] is equivalent to writing [adefgh].
- A cc can have different ranges at the same time, for example [a-zA-Z0-9] matches all the latin lowercase, uppercase and numeric characters.
- It is possible to negate the meaning of a cc, using the caret char at the start of the cc like this: [^abc] . That matches every char that is NOT a or b or c.
- A cc can contain meta-chars like: [a-z\d], that match all the lowercase latin chars a-z and all the digits \d.
- It is possible to mix all the properties of the char class together.
- Note > In order to match the - (minus) char, it must be preceded by > a backslash in the cc, for example [\-_\d\a] will match: > - - minus, > - _ underscore, > - \d numeric chars, > - \a lower case chars.
#### Meta-chars
- A meta-char is specified by a backslash, before a character. For example \w is the meta-char w.
- A meta-char can match different types of characters.
- \w matches a word char [a-zA-Z0-9_]
- \W matches a non word char
- \d matches a digit [0-9]
- \D matches a non digit
- \s matches a space char, one of [' ','\t','\n','\r','\v','\f']
- \S matches a non space char
- \a matches only a lowercase char [a-z]
- \A matches only an uppercase char [A-Z]
- \x41 match a byte of value 0x41, A in ascii code
- \X414C match two consecutive bytes of value 0x414c, AL in ascii code
#### Quantifiers
Each token can have a quantifier, that specifies how many times the character must be matched.
Short quantifiers
- ? matches 0 or 1 time, a?b matches both ab or b
- + matches at least 1 time, for example, a+ matches both aaa or a
- * matches 0 or more times, for example, a*b matches aaab, ab or b
Long quantifiers
- {x} matches exactly x times, a{2} matches aa, but not aaa or a
- {min,} matches at least min times, a{2,} matches aaa or aa, not a
- {,max} matches at least 0 times and at maximum max times,for example, a{,2} matches a and aa, but doesn't match aaa- {min,max} matches from min times, to max times, for examplea{2,3} matches aa and aaa, but doesn't match a or aaaa
- A long quantifier, may have a greedy off flag, that is the ? character after the brackets. {2,4}? means to match the minimum number of possible tokens, in this case 2.
#### dot char
The dot is a particular meta-char, that matches "any char".
input:
'''abccc ddeef'''
The following table shows the query strings and the result of parsing source string.
| query string | result |
|--------------|---------|
| `.*c` | `abc` |
| `.*dd` | `abcc dd` |
| `ab.*e` | `abccc dde`|
| `ab.{3} .*e` | `abccc dde`|
- The dot matches any character, until the next token match is satisfied.
- Important Note: Consecutive dots, for example ..., are not allowed. > This will cause a syntax error. Use a quantifier instead.
OR token
The token |, means a logic OR operation between two consecutive tokens, i.e. a|b matches a character that is a or b.
The OR token can work in a "chained way": a|(b)|cd means test first a, if the char is not a, then test the group (b), and if the group doesn't match too, finally test the token c.
Note > Unlike in PCRE, the OR operation works at token level! > It doesn't work at concatenation level!
Note > Two char classes with an OR in the middle is a syntax error.
That also means, that a query string like abc|bde is not equal to (abc)|(bde), but instead to ab(c|b)de. The OR operation works only for c|b, not at char concatenation level.
#### Groups
Groups are a method to create complex patterns with repetitions of blocks of tokens.
The groups are delimited by round brackets `( )`.
Groups can be nested. Like all other tokens, groups can have a quantifier too.
- `c(pa)+z` match `cpapaz` or `cpaz` or `cpapapaz`.
- `(c(pa)+z ?)+` matches `cpaz cpapaz cpapapaz` or `cpapaz`
Let's analyze this last case, first we have the group `#0`, that is the most outer round brackets `(...)+`. This group has a quantifier `+`, that says to match its content at least one time.
Then we have a simple char token `c`, and a second group `#1`: `(pa)+`. This group also tries to match the sequence `pa`, at least one time, as specified by the `+` quantifier.
Then, we have another simple token `z` and another simple token `?`, i.e. the space char (ascii code 32) followed by the `?` quantifier, which means that the preceding space should be matched 0 or 1 time.
This explains why the `(c(pa)+z ?)+` query string, can match `cpaz cpapaz cpapapaz`.
In this implementation the groups are "capture groups". This means that the last temporal result for each group, can be retrieved from the `RE` struct.
The "capture groups" are stored as indexes in the field `groups`, that is an `[]int` inside the `RE` struct.
example
```v
import regex
text := 'cpaz cpapaz cpapapaz'
query := r'(c(pa)+z ?)+'
mut re := regex.regex_opt(query) or { panic(err) }
println(re.get_query())
// #0(c#1(pa)+z ?)+
// #0 and #1 are the ids of the groups, are shown if re.debug is 1 or 2
start, end := re.match_string(text)
// [start=0, end=20] match => [cpaz cpapaz cpapapaz]
mut gi := 0
for gi < re.groups.len {
if re.groups[gi] >= 0 {
println('${gi / 2} :[${text[re.groups[gi]..re.groups[gi + 1]]}]')
}
gi += 2
}
// groups captured
// 0 :[cpapapaz]
// 1 :[pa]
```
Note > To show the group id number in the result of the get_query() > the flag debug of the RE object must be 1 or 2
In order to simplify the use of the captured groups, it is possible to use the utility function: get_group_list.
This function returns a list of groups using this support struct:
```v
pub struct Re_group {
pub:
start int = -1
end int = -1
}
```
Groups example:
This simple function converts an HTML RGB value with 3 or 6 hex digits to an u32 value, this function is not optimized and it is only for didatical purpose. Example: #A0B0CC #A9F
```v
import regex
fn convert_html_rgb(in_col string) u32 {
mut n_digit := if in_col.len == 4 { 1 } else { 2 }
mut col_mul := if in_col.len == 4 { 4 } else { 0 }
// this is the regex query, it uses the V string interpolation to customize the regex query
// Note: If you want to use escaped code you must use the r"" (raw) strings,
// *** please remember that the V interpoaltion doesn't work on raw strings. ***
query :='#([a-fA-F0-9]{${n_digit}})([a-fA-F0-9]{${n_digit}})([a-fA-F0-9]{${n_digit}})'
mut re := regex.regex_opt(query) or { panic(err) }
start, end := re.match_string(in_col)
println('start: ${start}, end: ${end}')
mut res := u32(0)
if start >= 0 {
group_list := re.get_group_list() // this is the utility function
r := ('0x' + in_col[group_list[0].start..group_list[0].end]).int() << col_mul
g := ('0x' + in_col[group_list[1].start..group_list[1].end]).int() << col_mul
b := ('0x' + in_col[group_list[2].start..group_list[2].end]).int() << col_mul
println('r: ${r} g: ${g} b: ${b}')
res = u32(r) << 16 | u32(g) << 8 | u32(b)
}
return res
}
```
Other utility functions are get_group_by_id and get_group_bounds_by_id that get directly the string of a group using its id:
```v
txt := 'my used string....'
for g_index := 0; g_index < re.group_count; g_index++ {
println('#${g_index} [${re.get_group_by_id(txt, g_index)}] \
}] bounds: ${re.get_group_bounds_by_id(g_index)}')
}
```
More helper functions are listed in the Groups query functions section.
Groups Continuous saving
In particular situations, it is useful to have a continuous group saving. This is possible by initializing the group_csave field in the RE struct.
This feature allows you to collect data in a continuous/streaming way.
In the example, we can pass a text, followed by an integer list, that we wish to collect. To achieve this task, we can use the continuous group saving, by enabling the right flag: re.group_csave_flag = true.
The .group_csave array will be filled then, following this logic:
re.group_csave[0] - number of total saved records re.group_csave[1+n*3] - id of the saved group re.group_csave[2+n*3] - start index in the source string of the saved group re.group_csave[3+n*3] - end index in the source string of the saved group
The regex will save groups, until it finishes, or finds that the array has no more space. If the space ends, no error is raised, and further records will not be saved.
```v
import regex
txt := 'http://www.ciao.mondo/hello/pippo12_/pera.html'
query := r'(?P<format>https?)|(?P<format>ftps?)://(?P<token>[\w_]+.)+'
mut re := regex.regex_opt(query) or { panic(err) }
// println(re.get_code()) // uncomment to see the print of the regex execution code
re.debug = 2 // enable maximum log
println('String: ${txt}')
println('Query : ${re.get_query()}')
re.debug = 0 // disable log
re.group_csave_flag = true
start, end := re.match_string(txt)
if start >= 0 {
println('Match (${start}, ${end}) => [${txt[start..end]}]')
} else {
println('No Match')
}
if re.group_csave_flag == true && start >= 0 && re.group_csave.len > 0 {
println('cg: ${re.group_csave}')
mut cs_i := 1
for cs_i < re.group_csave[0] * 3 {
g_id := re.group_csave[cs_i]
st := re.group_csave[cs_i + 1]
en := re.group_csave[cs_i + 2]
println('cg[${g_id}] ${st} ${en}:[${txt[st..en]}]')
cs_i += 3
}
}
```
The output will be:
```v
String: http://www.ciao.mondo/hello/pippo12_/pera.html
Query : #0(?P<format>https?)|{8,14}#0(?P<format>ftps?)://#1(?P<token>[\w_]+.)+
Match (0, 46) => [http://www.ciao.mondo/hello/pippo12_/pera.html]
cg: [8, 0, 0, 4, 1, 7, 11, 1, 11, 16, 1, 16, 22, 1, 22, 28, 1, 28, 37, 1, 37, 42, 1, 42, 46]
cg[0] 0 4:[http]
cg[1] 7 11:[www.]
cg[1] 11 16:[ciao.]
cg[1] 16 22:[mondo/]
cg[1] 22 28:[hello/]
cg[1] 28 37:[pippo12_/]
cg[1] 37 42:[pera.]
cg[1] 42 46:[html]
```v
#### Named capturing groups
This regex module supports partially the question mark ? PCRE syntax for groups.
- `(?:abcd)` non capturing group: the content of the group will not be saved.
- `(?P<mygroup>abcdef)` named group: the group content is saved and labeled as mygroup.
The label of the groups is saved in the `group_map` of the `RE` struct, that is a map from `string` to `int`, where the value is the index in `group_csave` list of indexes.
Here is an example for how to use them:
```v
import regex
txt := 'http://www.ciao.mondo/hello/pippo12_/pera.html'
query := r'(?P<format>https?)|(?P<format>ftps?)://(?P<token>[\w_]+.)+'
mut re := regex.regex_opt(query) or { panic(err) }
// println(re.get_code()) // uncomment to see the print of the regex execution code
re.debug = 2 // enable maximum log
println('String: ${txt}')
println('Query : ${re.get_query()}')
re.debug = 0 // disable log
start, end := re.match_string(txt)
if start >= 0 {
println('Match (${start}, ${end}) => [${txt[start..end]}]')
} else {
println('No Match')
}
for name in re.group_map.keys() {
println('group:${name} \t=> [${re.get_group_by_name(txt, name)}] \
}] bounds: ${re.get_group_bounds_by_name(name)}')
}
```
Output:
```
String: http://www.ciao.mondo/hello/pippo12_/pera.html
Query : #0(?P<format>https?)|{8,14}#0(?P<format>ftps?)://#1(?P<token>[\w_]+.)+
Match (0, 46) => [http://www.ciao.mondo/hello/pippo12_/pera.html]
group:format => [http] bounds: (0, 4)
group:token => [html] bounds: (42, 46)
```
In order to simplify the use of the named groups, it is possible to use a name map in the re struct, using the function `re.get_group_by_name`.
Here is a more complex example of using them:
```v
import regex
// This function demonstrate the use of the named groups
fn convert_html_rgb_n(in_col string) u32 {
mut n_digit := if in_col.len == 4 { 1 } else { 2 }
mut col_mul := if in_col.len == 4 { 4 } else { 0 }
query := r'#(?P<red>[a-fA-F0-9]{${n_digit}})(?P<green>[a-fA-F0-9]{${n_digit}})(?P<blue>[a-fA-F0-9]{${n_digit}})'
mut re := regex.regex_opt(query) or { panic(err) }
start, end := re.match_string(in_col)
println('start: ${start}, end: ${end}')
mut res := u32(0)
if start >= 0 {
red_s, red_e := re.get_group_bounds_by_name('red')
r := ('0x' + in_col[red_s..red_e]).int() << col_mul
green_s, green_e := re.get_group_bounds_by_name('green')
g := ('0x' + in_col[green_s..green_e]).int() << col_mul
blue_s, blue_e := re.get_group_bounds_by_name('blue')
b := ('0x' + in_col[blue_s..blue_e]).int() << col_mul
println('r: ${r} g: ${g} b: ${b}')
res = u32(r) << 16 | u32(g) << 8 | u32(b)
}
return res
}
```
Other utilities are `get_group_by_name` and `get_group_bounds_by_name`, that return the string of a group using its name:
```v
txt := 'my used string....'
for name in re.group_map.keys() {
println('group:${name} \t=> [${re.get_group_by_name(txt, name)}] \
}] bounds: ${re.get_group_bounds_by_name(name)}')
}
```
#### Groups query functions
These functions are helpers to query the captured groups
```v
// get_group_bounds_by_name get a group boundaries by its name
pub fn (re RE) get_group_bounds_by_name(group_name string) (int, int)
// get_group_by_name get a group string by its name
pub fn (re RE) get_group_by_name(group_name string) string
// get_group_by_id get a group boundaries by its id
pub fn (re RE) get_group_bounds_by_id(group_id int) (int, int)
// get_group_by_id get a group string by its id
pub fn (re RE) get_group_by_id(in_txt string, group_id int) string
struct Re_group {
pub:
start int = -1
end int = -1
}
// get_group_list return a list of Re_group for the found groups
pub fn (re RE) get_group_list() []Re_group
// get_group_list return a list of Re_group for the found groups
pub fn (re RE) get_group_list() []Re_group
Flags
It is possible to set some flags in the regex parser, that change the behavior of the parser itself.
```
#### init the regex struct with flags (optional)
```v
mut re := regex.new()
re.flag = regex.f_bin
// f_bin: parse a string as bytes, utf-8 management disabled.
// f_efm: exit on the first char matches in the query, used by thefind function.
//f_ms: matches only if the index of the start match is 0,same as ^ at the start of the query string.
// f_me: matches only if the end index of the match is the last charof the input string, same as $ end of query string.
// f_nl: stop the matching if found a new line char \n or \r
```
## Simplified initializer
```v
// regex create a regex object from the query string and compile it
pub fn regex_opt(in_query string) ?RE
// new create a RE of small size, usually sufficient for ordinary use
pub fn new() RE
//After an initializer is used, the regex expression must be compiled with:
// compile_opt compile RE pattern string, returning an error if the compilation fails
pub fn (mut re RE) compile_opt(pattern string) !
```
## Matching Functions
```v
// match_string try to match the input string, return start and end index if found else start is -1
pub fn (mut re RE) match_string(in_txt string) (int, int)
```
## Find functions
```v
// find try to find the first match in the input string
// return start and end index if found else start is -1
pub fn (mut re RE) find(in_txt string) (int, int)
// find_all find all the "non overlapping" occurrences of the matching pattern
// return a list of start end indexes like: [3,4,6,8]
// the matches are [3,4] and [6,8]
pub fn (mut re RE) find_all(in_txt string) []int
// find_all_str find all the "non overlapping" occurrences of the match pattern
// return a list of strings
// the result is like ['first match','secon match']
pub fn (mut re RE) find_all_str(in_txt string) []string
```
## Replace functions
```v
// replace return a string where the matches are replaced with the repl_str string,
// this function supports groups in the replace string
pub fn (mut re RE) replace(in_txt string, repl string) string
//replace string can include groups references:
txt := 'Today it is a good day.'
query := r'(a\w)[ ,.]'
mut re := regex.regex_opt(query)?
res := re.replace(txt, r'__[\0]__')
```
in above example we used the group 0 in the replace string: \0, the result will be:
Today it is a good day. => Tod__[ay]__it is a good d__[ay]__
Note > In the replace strings can be used only groups from 0 to 9.
If the usage of groups in the replace process, is not needed, it is possible to use a quick function:
```v
// replace_simple return a string where the matches are replaced with the replace string
pub fn (mut re RE) replace_simple(in_txt string, repl string) string
//If it is needed to replace N instances of the found strings it is possible to use:
// replace_n return a string where the first `count` matches are replaced with the repl_str string
// `count` indicate the number of max replacements that will be done.
// if count is > 0 the replace began from the start of the string toward the end
// if count is < 0 the replace began from the end of the string toward the start
// if count is 0 do nothing
pub fn (mut re RE) replace_n(in_txt string, repl_str string, count int) string
//For complex find and replace operations, you can use replace_by_fn . The replace_by_fn, uses a custom replace callback function, thus allowing customizations. The custom callback function is called for every non overlapped find.
// The custom callback function must be of the type:
// type of function used for custom replace
// in_txt source text
// start index of the start of the match in in_txt
// end index of the end of the match in in_txt
// --- the match is in in_txt[start..end] ---
fn (re RE, in_txt string, start int, end int) string
The following example will clarify its usage:
```
customized replace function example
```v
import regex
//
// it will be called on each non overlapped find
fn my_repl(re regex.RE, in_txt string, start int, end int) string {
g0 := re.get_group_by_id(in_txt, 0)
g1 := re.get_group_by_id(in_txt, 1)
g2 := re.get_group_by_id(in_txt, 2)
return'*${g0}*${g1}*${g2}*'
}
fn main() {
txt := 'today [John] is gone to his house with (Jack) and [Marie].'
query := r'(.)(\A\w+)(.)'
mut re := regex.regex_opt(query) or { panic(err) }
result := re.replace_by_fn(txt, my_repl)
println(result)
}
```
Output:
```txt
today *[*John*]* is gone to his house with *(*Jack*)* and *[*Marie*]*.
```

49
aiprompts/smtp.md Normal file
View File

@@ -0,0 +1,49 @@
module net.smtp
fn new_client(config Client) !&Client
new_client returns a new SMTP client and connects to it
enum BodyType {
text
html
}
struct Attachment {
filename string
bytes []u8
cid string
}
struct Client {
mut:
conn net.TcpConn
ssl_conn &ssl.SSLConn = unsafe { nil }
reader ?&io.BufferedReader
pub:
server string
port int = 25
username string
password string
from string
ssl bool
starttls bool
pub mut:
is_open bool
encrypted bool
}
fn (mut c Client) reconnect() !
reconnect reconnects to the SMTP server if the connection was closed
fn (mut c Client) send(config Mail) !
send sends an email
fn (mut c Client) quit() !
quit closes the connection to the server
struct Mail {
pub:
from string
to string
cc string
bcc string
date time.Time = time.now()
subject string
body_type BodyType
body string
attachments []Attachment
boundary string
}

View File

@@ -21,7 +21,7 @@ when I generate vlang scripts I will always use .vsh extension and use following
## to do argument parsing use following examples
```v
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
#!/usr/bin/env -S v -n -w -cg -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
import os
import flag

View File

@@ -0,0 +1,311 @@
## Description
V's `time` module, provides utilities for working with time and dates:
- parsing of time values expressed in one of the commonly used standard time/date formats
- formatting of time values
- arithmetic over times/durations
- converting between local time and UTC (timezone support)
- stop watches for accurately measuring time durations
- sleeping for a period of time
## Examples
You can see the current time. [See](https://play.vlang.io/?query=c121a6dda7):
```v
import time
println(time.now())
```
`time.Time` values can be compared, [see](https://play.vlang.io/?query=133d1a0ce5):
```v
import time
const time_to_test = time.Time{
year: 1980
month: 7
day: 11
hour: 21
minute: 23
second: 42
nanosecond: 123456789
}
println(time_to_test.format())
assert '1980-07-11 21:23' == time_to_test.format()
assert '1980-07-11 21:23:42' == time_to_test.format_ss()
assert '1980-07-11 21:23:42.123' == time_to_test.format_ss_milli()
assert '1980-07-11 21:23:42.123456' == time_to_test.format_ss_micro()
assert '1980-07-11 21:23:42.123456789' == time_to_test.format_ss_nano()
```
You can also parse strings to produce time.Time values,
[see](https://play.vlang.io/p/b02ca6027f):
```v
import time
s := '2018-01-27 12:48:34'
t := time.parse(s) or { panic('failing format: ${s} | err: ${err}') }
println(t)
println(t.unix())
```
V's time module also has these parse methods:
```v ignore
fn parse(s string) !Time
fn parse_iso8601(s string) !Time
fn parse_rfc2822(s string) !Time
fn parse_rfc3339(s string) !Time
```
Another very useful feature of the `time` module is the stop watch,
for when you want to measure short time periods, elapsed while you
executed other tasks. [See](https://play.vlang.io/?query=f6c008bc34):
```v
import time
fn do_something() {
time.sleep(510 * time.millisecond)
}
fn main() {
sw := time.new_stopwatch()
do_something()
println('Note: do_something() took: ${sw.elapsed().milliseconds()} ms')
}
```
```vlang
module time
const second = Duration(1000 * millisecond)
const long_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
'September', 'October', 'November', 'December']
const nanosecond = Duration(1)
const absolute_zero_year = i64(-292277022399)
const days_string = 'MonTueWedThuFriSatSun'
const long_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']!
const month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]!
const seconds_per_hour = 60 * seconds_per_minute
const millisecond = Duration(1000 * microsecond)
const days_per_4_years = days_in_year * 4 + 1
const microsecond = Duration(1000 * nanosecond)
const days_per_400_years = days_in_year * 400 + 97
const minute = Duration(60 * second)
const days_before = [
0,
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31,
]!
const months_string = 'JanFebMarAprMayJunJulAugSepOctNovDec'
const seconds_per_week = 7 * seconds_per_day
const hour = Duration(60 * minute)
const days_per_100_years = days_in_year * 100 + 24
const seconds_per_minute = 60
const days_in_year = 365
const infinite = Duration(i64(9223372036854775807))
const seconds_per_day = 24 * seconds_per_hour
fn date_from_days_after_unix_epoch(days int) Time
fn day_of_week(y int, m int, d int) int
fn days_from_unix_epoch(year int, month int, day int) int
fn days_in_month(month int, year int) !int
fn is_leap_year(year int) bool
fn new(t Time) Time
fn new_stopwatch(opts StopWatchOptions) StopWatch
fn new_time(t Time) Time
fn now() Time
fn offset() int
fn parse(s string) !Time
fn parse_format(s string, format string) !Time
fn parse_iso8601(s string) !Time
fn parse_rfc2822(s string) !Time
fn parse_rfc3339(s string) !Time
fn portable_timegm(t &C.tm) i64
fn since(t Time) Duration
fn sleep(duration Duration)
fn sys_mono_now() u64
fn ticks() i64
fn unix(epoch i64) Time
fn unix2(epoch i64, microsecond int) Time
fn unix_microsecond(epoch i64, microsecond int) Time
fn unix_nanosecond(abs_unix_timestamp i64, nanosecond int) Time
fn utc() Time
fn Time.new(t Time) Time
type Duration = i64
fn (d Duration) days() f64
fn (d Duration) debug() string
fn (d Duration) hours() f64
fn (d Duration) microseconds() i64
fn (d Duration) milliseconds() i64
fn (d Duration) minutes() f64
fn (d Duration) nanoseconds() i64
fn (d Duration) seconds() f64
fn (d Duration) str() string
fn (d Duration) sys_milliseconds() int
fn (d Duration) timespec() C.timespec
enum FormatDate {
ddmmyy
ddmmyyyy
mmddyy
mmddyyyy
mmmd
mmmdd
mmmddyy
mmmddyyyy
no_date
yyyymmdd
yymmdd
}
enum FormatDelimiter {
dot
hyphen
slash
space
no_delimiter
}
enum FormatTime {
hhmm12
hhmm24
hhmmss12
hhmmss24
hhmmss24_milli
hhmmss24_micro
hhmmss24_nano
no_time
}
struct C.mach_timebase_info_data_t {
numer u32
denom u32
}
struct C.timespec {
pub mut:
tv_sec i64
tv_nsec i64
}
struct C.timeval {
pub:
tv_sec u64
tv_usec u64
}
struct C.tm {
pub mut:
tm_sec int
tm_min int
tm_hour int
tm_mday int
tm_mon int
tm_year int
tm_wday int
tm_yday int
tm_isdst int
tm_gmtoff int
}
struct StopWatch {
mut:
elapsed u64
pub mut:
start u64
end u64
}
fn (mut t StopWatch) start()
fn (mut t StopWatch) restart()
fn (mut t StopWatch) stop()
fn (mut t StopWatch) pause()
fn (t StopWatch) elapsed() Duration
struct StopWatchOptions {
pub:
auto_start bool = true
}
struct Time {
unix i64
pub:
year int
month int
day int
hour int
minute int
second int
nanosecond int
is_local bool // used to make time.now().local().local() == time.now().local()
microsecond int @[deprecated: 'use t.nanosecond / 1000 instead'; deprecated_after: '2023-08-05']
}
fn (lhs Time) - (rhs Time) Duration
fn (t1 Time) < (t2 Time) bool
fn (t1 Time) == (t2 Time) bool
fn (t Time) add(duration_in_nanosecond Duration) Time
fn (t Time) add_days(days int) Time
fn (t Time) add_seconds(seconds int) Time
fn (t Time) as_local() Time
fn (t Time) as_utc() Time
fn (t Time) clean() string
fn (t Time) clean12() string
fn (t Time) custom_format(s string) string
fn (t Time) day_of_week() int
fn (t Time) days_from_unix_epoch() int
fn (t Time) ddmmy() string
fn (t Time) debug() string
fn (t Time) format() string
fn (t Time) format_rfc3339() string
fn (t Time) format_rfc3339_nano() string
fn (t Time) format_ss() string
fn (t Time) format_ss_micro() string
fn (t Time) format_ss_milli() string
fn (t Time) format_ss_nano() string
fn (t Time) get_fmt_date_str(fmt_dlmtr FormatDelimiter, fmt_date FormatDate) string
fn (t Time) get_fmt_str(fmt_dlmtr FormatDelimiter, fmt_time FormatTime, fmt_date FormatDate) string
fn (t Time) get_fmt_time_str(fmt_time FormatTime) string
fn (t Time) hhmm() string
fn (t Time) hhmm12() string
fn (t Time) hhmmss() string
fn (t Time) http_header_string() string
fn (t Time) is_utc() bool
fn (t Time) local() Time
fn (t Time) local_to_utc() Time
fn (t Time) long_weekday_str() string
fn (t Time) md() string
fn (t Time) relative() string
fn (t Time) relative_short() string
fn (t Time) smonth() string
fn (t Time) str() string
fn (t Time) strftime(fmt string) string
fn (t Time) unix() i64
fn (t Time) unix_micro() i64
fn (t Time) unix_milli() i64
fn (t Time) unix_nano() i64
fn (t Time) unix_time() i64
fn (t Time) unix_time_micro() i64
fn (t Time) unix_time_milli() i64
fn (t Time) unix_time_nano() i64
fn (t Time) utc_string() string
fn (u Time) utc_to_local() Time
fn (t Time) weekday_str() string
fn (t Time) year_day() int
fn (t Time) ymmdd() string
struct TimeParseError {
Error
code int
message string
}
fn (err TimeParseError) msg() string
```

View File

@@ -0,0 +1,43 @@
# module ui.console.chalk
Chalk offers functions:- `console.color_fg(text string, color string)` - To change the foreground color.
- `console.color_bg(text string, color string)` - To change the background color.
- `console.style(text string, style string)` - To change the text style.
Example:
```vlang
import freeflowuniverse.crystallib.ui.console
# basic usage
println('I am really ' + console.color_fg('happy', 'green'))
# you can also nest them
println('I am really ' + console.color_fg(console.style('ANGRY', 'bold'), 'red'))
```
Available colors:- black
- red
- green
- yellow
- blue
- magenta
- cyan
- default
- light_gray
- dark_gray
- light_red
- light_green
- light_yellow
- light_blue
- light_magenta
- light_cyan
- white
Available styles:- bold
- dim
- underline
- blink
- reverse
- hidden

205
aiprompts/ui console.md Normal file
View File

@@ -0,0 +1,205 @@
# module ui.console
has mechanisms to print better to console, see the methods below
import as
```vlang
import freeflowuniverse.crystallib.ui.console
```
## Methods
```v
fn clear()
//reset the console screen
fn color_bg(c BackgroundColor) string
// will give ansi codes to change background color . dont forget to call reset to change back to normal
fn color_fg(c ForegroundColor) string
// will give ansi codes to change foreground color . don't forget to call reset to change back to normal
struct PrintArgs {
pub mut:
foreground ForegroundColor
background BackgroundColor
text string
style Style
reset_before bool = true
reset_after bool = true
}
fn cprint(args PrintArgs)
// print with colors, reset...
// ```
// foreground ForegroundColor
// background BackgroundColor
// text string
// style Style
// reset_before bool = true
// reset_after bool = true
// ```
fn cprintln(args_ PrintArgs)
fn expand(txt_ string, l int, with string) string
// expand text till length l, with string which is normally ' '
fn lf()
line feed
fn new() UIConsole
fn print_array(arr [][]string, delimiter string, sort bool)
// print 2 dimensional array, delimeter is between columns
fn print_debug(i IPrintable)
fn print_debug_title(title string, txt string)
fn print_green(txt string)
fn print_header(txt string)
fn print_item(txt string)
fn print_lf(nr int)
fn print_stderr(txt string)
fn print_stdout(txt string)
fn reset() string
fn silent_get() bool
fn silent_set()
fn silent_unset()
fn style(c Style) string
// will give ansi codes to change style . don't forget to call reset to change back to normal
fn trim(c_ string) string
```
## Console Object
Is used to ask feedback to users
```v
struct UIConsole {
pub mut:
x_max int = 80
y_max int = 60
prev_lf bool
prev_title bool
prev_item bool
}
//DropDownArgs:
// - description string
// - items []string
// - warning string
// - clear bool = true
fn (mut c UIConsole) ask_dropdown_int(args_ DropDownArgs) !int
// return the dropdown as an int
fn (mut c UIConsole) ask_dropdown_multiple(args_ DropDownArgs) ![]string
// result can be multiple, aloso can select all description string items []string warning string clear bool = true
fn (mut c UIConsole) ask_dropdown(args DropDownArgs) !string
// will return the string as given as response description
// QuestionArgs:
// - description string
// - question string
// - warning: string (if it goes wrong, which message to use)
// - reset bool = true
// - regex: to check what result need to be part of
// - minlen: min nr of chars
fn (mut c UIConsole) ask_question(args QuestionArgs) !string
fn (mut c UIConsole) ask_time(args QuestionArgs) !string
fn (mut c UIConsole) ask_date(args QuestionArgs) !string
fn (mut c UIConsole) ask_yesno(args YesNoArgs) !bool
// yes is true, no is false
// args:
// - description string
// - question string
// - warning string
// - clear bool = true
fn (mut c UIConsole) reset()
fn (mut c UIConsole) status() string
```
## enums
```v
enum BackgroundColor {
default_color = 49 // 'default' is a reserved keyword in V
black = 40
red = 41
green = 42
yellow = 43
blue = 44
magenta = 45
cyan = 46
light_gray = 47
dark_gray = 100
light_red = 101
light_green = 102
light_yellow = 103
light_blue = 104
light_magenta = 105
light_cyan = 106
white = 107
}
enum ForegroundColor {
default_color = 39 // 'default' is a reserved keyword in V
white = 97
black = 30
red = 31
green = 32
yellow = 33
blue = 34
magenta = 35
cyan = 36
light_gray = 37
dark_gray = 90
light_red = 91
light_green = 92
light_yellow = 93
light_blue = 94
light_magenta = 95
light_cyan = 96
}
enum Style {
normal = 99
bold = 1
dim = 2
underline = 4
blink = 5
reverse = 7
hidden = 8
}
```

File diff suppressed because it is too large Load Diff

1221
aiprompts/veb.md Normal file

File diff suppressed because it is too large Load Diff

215
aiprompts/veb_assets.md Normal file
View File

@@ -0,0 +1,215 @@
module veb.assets
# Assets
The asset manager for veb. You can use this asset manager to minify CSS and JavaScript files, combine them into a single file and to make sure the asset you're using exists.
## Usage
Add `AssetManager` to your App struct to use the asset manager.
**Example:**
```v
module main
import veb
import veb.assets
pub struct Context {
veb.Context
}
pub struct App {
pub mut:
am assets.AssetManager
}
fn main() {
mut app := &App{}
veb.run[App, Context](mut app, 8080)
}
```
### Including assets
If you want to include an asset in your templates you can use the `include` method. First pass the type of asset (css or js), then specify the "include name" of an asset.
**Example:**
```html
@{app.am.include(.css, 'main.css')}
```
Will generate
```html
<link rel="stylesheet" href="/main.css" />
```
### Adding assets
To add an asset use the `add` method. You must specify the path of the asset and what its include name will be: the name that you will use in templates.
**Example:**
```v ignore
// add a css file at the path "css/main.css" and set its include name to "main.css"
app.am.add(.css, 'css/main.css', 'main.css')
```
### Minify and Combine assets
If you want to minify each asset you must set the `minify` field and specify the cache folder. Each assest you add is minifed and outputted in `cache_dir`.
**Example:**
```v ignore
pub struct App {
pub mut:
am assets.AssetManager = assets.AssetManager{
cache_dir: 'dist'
minify: true
}
}
```
To combine the all currently added assets into a single file you must call the `combine` method and specify which asset type you want to combine.
**Example:**
```v ignore
// `combine` returns the path of the minified file
minified_file := app.am.combine(.css)!
```
### Handle folders
You can use the asset manger in combination with veb's `StaticHandler` to serve assets in a folder as static assets.
**Example:**
```v ignore
pub struct App {
veb.StaticHandler
pub mut:
am assets.AssetManager
}
```
Let's say we have the following folder structure:
```
assets/
├── css/
│ └── main.css
└── js/
└── main.js
```
We can tell the asset manager to add all assets in the `static` folder
**Example:**
```v ignore
fn main() {
mut app := &App{}
// add all assets in the "assets" folder
app.am.handle_assets('assets')!
// serve all files in the "assets" folder as static files
app.handle_static('assets', false)!
// start the app
veb.run[App, Context](mut app, 8080)
}
```
The include name of each minified asset will be set to its relative path, so if you want to include `main.css` in your template you would write `@{app.am.include('css/main.css')}`
#### Minify
If you add an asset folder and want to minify those assets you can call the `cleanup_cache` method to remove old files from the cache folder that are no longer needed.
**Example:**
```v ignore
pub struct App {
veb.StaticHandler
pub mut:
am assets.AssetManager = assets.AssetManager{
cache_dir: 'dist'
minify: true
}
}
fn main() {
mut app := &App{}
// add all assets in the "assets" folder
app.am.handle_assets('assets')!
// remove all old cached files from the cache folder
app.am.cleanup_cache()!
// serve all files in the "assets" folder as static files
app.handle_static('assets', false)!
// start the app
veb.run[App, Context](mut app, 8080)
}
```
#### Prefix the include name
You can add a custom prefix to the include name of assets when adding a folder.
**Example:**
```v ignore
// add all assets in the "assets" folder
app.am.handle_assets_at('assets', 'static')!
```
Now if you want to include `main.css` you would write ``@{app.am.include('static/css/main.css')}`
fn minify_css(css string) string
Todo: implement proper minification
fn minify_js(js string) string
Todo: implement proper minification
enum AssetType {
css
js
all
}
struct Asset {
pub:
kind AssetType
file_path string
last_modified time.Time
include_name string
}
struct AssetManager {
mut:
css []Asset
js []Asset
cached_file_names []string
pub mut:
// when true assets will be minified
minify bool
// the directory to store the cached/combined files
cache_dir string
// how a combined file should be named. For example for css the extension '.css'
// will be added to the end of `combined_file_name`
combined_file_name string = 'combined'
}
fn (mut am AssetManager) handle_assets(directory_path string) !
handle_assets recursively walks `directory_path` and adds any assets to the asset manager
fn (mut am AssetManager) handle_assets_at(directory_path string, prepend string) !
handle_assets_at recursively walks `directory_path` and adds any assets to the asset manager. The include name of assets are prefixed with `prepend`
fn (am AssetManager) get_assets(asset_type AssetType) []Asset
get all assets of type `asset_type`
fn (mut am AssetManager) add(asset_type AssetType, file_path string, include_name string) !
add an asset to the asset manager
fn (mut am AssetManager) cleanup_cache() !
cleanup_cache removes all files in the cache directory that aren't cached at the time this function is called
fn (am AssetManager) exists(asset_type AssetType, include_name string) bool
check if an asset is already added to the asset manager
fn (am AssetManager) include(asset_type AssetType, include_name string) veb.RawHtml
include css/js files in your veb app from templates
Example: @{app.am.include(.css, 'main.css')}
fn (mut am AssetManager) combine(asset_type AssetType) !string
combine assets of type `asset_type` into a single file and return the outputted file path. If you call `combine` with asset type `all` the function will return an empty string, the minified files will be available at `combined_file_name`.`asset_type`

128
aiprompts/veb_auth.md Normal file
View File

@@ -0,0 +1,128 @@
module veb.auth
## Description
`veb.auth` is a module that helps with common logic required for authentication.
It allows to easily generate hashed and salted passwords and to compare password hashes.
It also handles authentication tokens, including DB table creation and insertion. All DBs are supported.
## Usage
```v
import veb
import db.pg
import veb.auth
pub struct App {
veb.StaticHandler
pub mut:
db pg.DB
auth auth.Auth[pg.DB] // or auth.Auth[sqlite.DB] etc
}
const port = 8081
pub struct Context {
veb.Context
current_user User
}
struct User {
id int @[primary; sql: serial]
name string
password_hash string
salt string
}
fn main() {
mut app := &App{
db: pg.connect(host: 'localhost', user: 'postgres', password: '', dbname: 'postgres')!
}
app.auth = auth.new(app.db)
veb.run[App, Context](mut app, port)
}
@[post]
pub fn (mut app App) register_user(mut ctx Context, name string, password string) veb.Result {
salt := auth.generate_salt()
new_user := User{
name: name
password_hash: auth.hash_password_with_salt(password, salt)
salt: salt
}
sql app.db {
insert new_user into User
} or {}
// Get new user ID (until RETURNING is supported by ORM)
if x := app.find_user_by_name(name) {
// Generate and insert the token using user ID
token := app.auth.add_token(x.id) or { '' }
// Authenticate the user by adding the token to the cookies
ctx.set_cookie(name: 'token', value: token)
}
return ctx.redirect('/')
}
@[post]
pub fn (mut app App) login_post(mut ctx Context, name string, password string) veb.Result {
user := app.find_user_by_name(name) or {
ctx.error('Bad credentials')
return ctx.redirect('/login')
}
// Verify user password using veb.auth
if !auth.compare_password_with_hash(password, user.salt, user.password_hash) {
ctx.error('Bad credentials')
return ctx.redirect('/login')
}
// Find the user token in the Token table
token := app.auth.add_token(user.id) or { '' }
// Authenticate the user by adding the token to the cookies
ctx.set_cookie(name: 'token', value: token)
return ctx.redirect('/')
}
pub fn (mut app App) find_user_by_name(name string) ?User {
// ... db query
return User{}
}
```
## Security considerations
`hash_password_with_salt` and its related functions use `sha256` for hashing with a single iteration. This is not secure for production use, and you should use a more secure hashing algorithm and multiple iterations.
See also:- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
fn compare_password_with_hash(plain_text_password string, salt string, hashed string) bool
fn generate_salt() string
fn hash_password_with_salt(plain_text_password string, salt string) string
fn new[T](db T) Auth[T]
fn set_rand_crypto_safe_seed()
fn (mut app Auth[T]) add_token(user_id int) !string
fn (mut app App) add_token(user_id int, ip string) !string {
fn (app &Auth[T]) find_token(value string) ?Token
fn (mut app Auth[T]) delete_tokens(user_id int) !
struct Auth[T] {
db T
// pub:
// salt string
}
struct Request {
pub:
client_id string
client_secret string
code string
state string
}
struct Token {
pub:
id int @[primary; sql: serial]
user_id int
value string
// ip string
}

257
aiprompts/veb_csrf.md Normal file
View File

@@ -0,0 +1,257 @@
> module veb.csrf
# Cross-Site Request Forgery (CSRF) protection
This module implements the [double submit cookie][owasp] technique to protect routes from CSRF attacks.
CSRF is a type of attack that occurs when a malicious program/website (and others) causes a user's web browser to perform an action without them knowing. A web browser automatically sends cookies to a website when it performs a request, including session cookies. So if a user is authenticated on your website the website can not distinguish a forged request by a legitimate request.
## When to not add CSRF-protection
If you are creating a service that is intended to be used by other servers e.g. an API, you probably don't want CSRF-protection. An alternative would be to send an Authorization token in, and only in, an HTTP-header (like JSON Web Tokens). If you do that your website isn't vulnerable to CSRF-attacks.
## Usage
To enable CSRF-protection for your veb app you must embed the `CsrfContext` struct on your `Context` struct. You must also provide configuration options (see [configuration & security](#configuration--security-considerations)).
**Example:**
```v
import veb
import veb.csrf
pub struct Context {
veb.Context
csrf.CsrfContext
}
```
Change `secret` and `allowed_hosts` in a production environment!
**Example:**
```v ignore
const csrf_config := csrf.CsrfConfig{
secret: 'my-secret'
allowed_hosts: ['*']
}
```
### Middleware
Enable CSRF protection for all routes, or a certain route(s) by using veb's middleware.
**Example:**
```v ignore
pub struct App {
veb.Middleware[Context]
}
fn main() {
mut app := &App{}
// register the CSRF middleware and pass our configuration
// protect a specific route
app.route_use('/login', csrf.middleware[Context](csrf_config))
veb.run[App, Context](mut app, 8080)
}
```
### Setting the token
For the CSRF-protection to work we have to generate an anti-CSRF token and set it as an hidden input field on any form that will be submitted to the route we want to protect.
**Example:** _main.v_
```v ignore
fn (app &App) index(mut ctx) veb.Result {
// this function will set a cookie header and generate a CSRF token
ctx.set_csrf_token(mut ctx)
return $veb.html()
}
@[post]
fn (app &App) login(mut ctx, password string) veb.Result {
// implement your own password validation here
if password == 'password' {
return ctx.text('You are logged in!')
} else {
return ctx.text('Invalid password!')
}
}
```
_templates/index.html_
```html
<h1>Log in</h1>
<form method="POST" action="/login">
@{ctx.csrf_token_input()}
<label for="password">Password:</label>
<input type="text" name="password" id="password" />
<button type="submit">Log in</button>
</form>
```
If we run the app with `v run main.v` and navigate to `http://localhost:8080/` we will see the login form and we can login using the password "password".
If we remove the hidden input, by removing the line `@{ctx.csrf_token_input()}` from our html code we will see an error message indicating that the CSRF token is not set or invalid! By default the CSRF module sends an HTTP-403 response when a token is invalid, if you want to send a custom response see the [advanced usage](#advanced-usage) section.
> **Note:** > Please read the security and configuration section! If you configure > the CSRF module in an unsafe way, the protection will be useless.
## Advanced Usage
If you want more control over what routes are protected or what action you want to do when a CSRF-token is invalid, you can call `csrf.protect` yourself whenever you want to protect a route against CSRF attacks. This function returns `false` if the current CSRF token and cookie combination is not valid.
**Example:**
```v ignore
@[post]
fn (app &App) login(mut ctx, password string) veb.Result {
if csrf.protect(mut ctx, csrf_config) == false {
// CSRF verification failed!
}
// ...
}
```
### Obtaining the anti-CSRF token
When `set_csrf_token` is called the token is stored in the `csrf_token` field. You access this field directly to use it in an input field, or call `csrf_token_input`.
**Example:**
```v ignore
fn (app &App) index(mut ctx) veb.Result {
token := ctx.set_csrf_token(mut ctx)
}
```
### Clearing the anti-CSRF token
If you want to remove the anti-CSRF token and the cookie header you can call `clear_csrf_token`
**Example:**
```v ignore
ctx.clear_csrf_token()
```
## How it works
This module implements the [double submit cookie][owasp] technique: a random token is generated, the CSRF-token. The hmac of this token and the secret key is stored in a cookie.
When a request is made, the CSRF-token should be placed inside a HTML form element. The CSRF-token the hmac of the CSRF-token in the formdata is compared to the cookie. If the values match, the request is accepted.
This approach has the advantage of being stateless: there is no need to store tokens on the server side and validate them. The token and cookie are bound cryptographically to each other so an attacker would need to know both values in order to make a CSRF-attack succeed. That is why is it important to **not leak the CSRF-token** via an url, or some other way. This is way by default the `HTTPOnly` flag on the cookie is set to true. See [client side CSRF][client-side-csrf] for more information.
This is a high level overview of the implementation.
## Configuration & Security Considerations
### The secret key
The secret key should be a random string that is not easily guessable.
### Sessions
If your app supports some kind of user sessions, it is recommended to cryptographically bind the CSRF-token to the users' session. You can do that by providing the name of the session ID cookie. If an attacker changes the session ID in the cookie, in the token or both the hmac will be different and the request will be rejected.
**Example**:
```v ignore
csrf_config = csrf.CsrfConfig{
// ...
session_cookie: 'my_session_id_cookie_name'
}
```
### Safe Methods
The HTTP methods `GET`, `OPTIONS`, `HEAD` are considered [safe methods][mozilla-safe-methods] meaning they should not alter the state of an application. If a request with a "safe method" is made, the csrf protection will be skipped.
You can change which methods are considered safe by changing `CsrfConfig.safe_methods`.
### Allowed Hosts
By default, both the http Origin and Referer headers are checked and matched strictly to the values in `allowed_hosts`. That means that you need to include each subdomain.
If the value of `allowed_hosts` contains the wildcard: `'*'` the headers will not be checked.
#### Domain name matching
The following configuration will not allow requests made from `test.example.com`, only from `example.com`.
**Example**
```v ignore
config := csrf.CsrfConfig{
secret: '...'
allowed_hosts: ['example.com']
}
```
#### Referer, Origin header check
In some cases (like if your server is behind a proxy), the Origin or Referer header will not be present. If that is your case you can set `check_origin_and_referer` to `false`. Request will now be accepted when the Origin _or_ Referer header is valid.
### Share csrf cookie with subdomains
If you need to share the CSRF-token cookie with subdomains, you can set `same_site` to `.same_site_lax_mode`.
## Configuration
All configuration options are defined in `CsrfConfig`.
[//]: # 'Sources' [owasp]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#double-submit-cookie [client-side-csrf]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#client-side-csrf [mozilla-safe-methods]: https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP
fn middleware[T](config CsrfConfig) veb.MiddlewareOptions[T]
middleware returns a handler that you can use with veb's middleware
fn protect(mut ctx veb.Context, config &CsrfConfig) bool
protect returns false and sends an http 401 response when the csrf verification fails. protect will always return true if the current request method is in `config.safe_methods`.
fn set_token(mut ctx veb.Context, config &CsrfConfig) string
set_token returns the csrftoken and sets an encrypted cookie with the hmac of `config.get_secret` and the csrftoken
struct CsrfConfig {
pub:
secret string
// how long the random part of the csrf-token should be
nonce_length int = 64
// HTTP "safe" methods meaning they shouldn't alter state.
// If a request with any of these methods is made, `protect` will always return true
// https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
safe_methods []http.Method = [.get, .head, .options]
// which hosts are allowed, enforced by checking the Origin and Referer header
// if allowed_hosts contains '*' the check will be skipped.
// Subdomains need to be included separately: a request from `"sub.example.com"`
// will be rejected when `allowed_host = ['example.com']`.
allowed_hosts []string
// if set to true both the Referer and Origin headers must match `allowed_hosts`
// else if either one is valid the request is accepted
check_origin_and_referer bool = true
// the name of the csrf-token in the hidden html input
token_name string = 'csrftoken'
// the name of the cookie that contains the session id
session_cookie string
// cookie options
cookie_name string = 'csrftoken'
same_site http.SameSite = .same_site_strict_mode
cookie_path string = '/'
// how long the cookie stays valid in seconds. Default is 30 days
max_age int = 60 * 60 * 24 * 30
cookie_domain string
// whether the cookie can be send only over HTTPS
secure bool
}
struct CsrfContext {
pub mut:
config CsrfConfig
exempt bool
// the csrftoken that should be placed in an html form
csrf_token string
}
fn (mut ctx CsrfContext) set_csrf_token[T](mut user_context T) string
set_token generates a new csrf_token and adds a Cookie to the response
fn (ctx &CsrfContext) clear_csrf_token[T](mut user_context T)
clear the csrf token and cookie header from the context
fn (ctx &CsrfContext) csrf_token_input() veb.RawHtml
csrf_token_input returns an HTML hidden input containing the csrf token

83
aiprompts/veb_sse.md Normal file
View File

@@ -0,0 +1,83 @@
module veb.sse
# Server Sent Events
This module implements the server side of `Server Sent Events`, SSE. See [mozilla SSE][mozilla_sse] as well as [whatwg][whatwg html spec] for detailed description of the protocol, and a simple web browser client example.
## Usage
With SSE we want to keep the connection open, so we are able to keep sending events to the client. But if we hold the connection open indefinitely veb isn't able to process any other requests.
We can let veb know that it can continue processing other requests and that we will handle the connection ourself by calling `ctx.takeover_conn()` and returning an empty result with `veb.no_result()`. veb will not close the connection and we can handle the connection in a separate thread.
**Example:**
```v ignore
import veb.sse
// endpoint handler for SSE connections
fn (app &App) sse(mut ctx Context) veb.Result {
// let veb know that the connection should not be closed
ctx.takeover_conn()
// handle the connection in a new thread
spawn handle_sse_conn(mut ctx)
// we will send a custom response ourself, so we can safely return an empty result
return veb.no_result()
}
fn handle_sse_conn(mut ctx Context) {
// pass veb.Context
mut sse_conn := sse.start_connection(mut ctx.Context)
// send a message every second 3 times
for _ in 0.. 3 {
time.sleep(time.second)
sse_conn.send_message(data: 'ping') or { break }
}
// close the SSE connection
sse_conn.close()
}
```
Javascript code:
```js
const eventSource = new EventSource('/sse');
eventSource.addEventListener('message', (event) => {
console.log('received message:', event.data);
});
eventSource.addEventListener('close', () => {
console.log('closing the connection');
// prevent browser from reconnecting
eventSource.close();
});
```
[mozilla_sse]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events [whatwg html spec]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
fn start_connection(mut ctx veb.Context) &SSEConnection
start an SSE connection
struct SSEConnection {
pub mut:
conn &net.TcpConn @[required]
}
fn (mut sse SSEConnection) send_message(message SSEMessage) !
send_message sends a single message to the http client that listens for SSE. It does not close the connection, so you can use it many times in a loop.
fn (mut sse SSEConnection) close()
send a 'close' event and close the tcp connection.
struct SSEMessage {
pub mut:
id string
event string
data string
retry int
}
This module implements the server side of `Server Sent Events`. See https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format as well as https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events for detailed description of the protocol, and a simple web browser client example.
> Event stream format > The event stream is a simple stream of text data which must be encoded using UTF-8. > Messages in the event stream are separated by a pair of newline characters. > A colon as the first character of a line is in essence a comment, and is ignored. > Note: The comment line can be used to prevent connections from timing out; > a server can send a comment periodically to keep the connection alive. > > Each message consists of one or more lines of text listing the fields for that message. > Each field is represented by the field name, followed by a colon, followed by the text > data for that field's value.

View File

@@ -0,0 +1,907 @@
# 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)
}
```

View File

@@ -0,0 +1,21 @@
# how to run the vshell example scripts
this is how we want example scripts to be, see the first line
```vlang
#!/usr/bin/env -S v -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.crystallib.installers.sysadmintools.daguserver
mut ds := daguserver.get()!
println(ds)
```
the files are in ~/code/github/freeflowuniverse/crystallib/examples for crystallib
## important instructions
- never use fn main() in a .vsh script

165
aiprompts/vtemplates.md Normal file
View File

@@ -0,0 +1,165 @@
V allows for easily using text templates, expanded at compile time to
V functions, that efficiently produce text output. This is especially
useful for templated HTML views, but the mechanism is general enough
to be used for other kinds of text output also.
# Template directives
Each template directive begins with an `@` sign.
Some directives contain a `{}` block, others only have `''` (string) parameters.
Newlines on the beginning and end are ignored in `{}` blocks,
otherwise this (see [if](#if) for this syntax):
```html
@if bool_val {
<span>This is shown if bool_val is true</span>
}
```
... would output:
```html
<span>This is shown if bool_val is true</span>
```
... which is less readable.
## if
The if directive, consists of three parts, the `@if` tag, the condition (same syntax like in V)
and the `{}` block, where you can write html, which will be rendered if the condition is true:
```
@if <condition> {}
```
### Example
```html
@if bool_val {
<span>This is shown if bool_val is true</span>
}
```
One-liner:
```html
@if bool_val { <span>This is shown if bool_val is true</span> }
```
The first example would result in:
```html
<span>This is shown if bool_val is true</span>
```
... while the one-liner results in:
```html
<span>This is shown if bool_val is true</span>
```
## for
The for directive consists of three parts, the `@for` tag,
the condition (same syntax like in V) and the `{}` block,
where you can write text, rendered for each iteration of the loop:
```
@for <condition> {}
```
### Example for @for
```html
@for i, val in my_vals {
<span>$i - $val</span>
}
```
One-liner:
```html
@for i, val in my_vals { <span>$i - $val</span> }
```
The first example would result in:
```html
<span>0 - "First"</span>
<span>1 - "Second"</span>
<span>2 - "Third"</span>
...
```
... while the one-liner results in:
```html
<span>0 - "First"</span>
<span>1 - "Second"</span>
<span>2 - "Third"</span>
...
```
You can also write (and all other for condition syntaxes that are allowed in V):
```html
@for i = 0; i < 5; i++ {
<span>$i</span>
}
```
## include
The include directive is for including other html files (which will be processed as well)
and consists of two parts, the `@include` tag and a following `'<path>'` string.
The path parameter is relative to the template file being called.
### Example for the folder structure of a project using templates:
```
Project root
/templates
- index.html
/headers
- base.html
```
`index.html`
```html
<div>@include 'header/base'</div>
```
> Note that there shouldn't be a file suffix,
> it is automatically appended and only allows `html` files.
## js
The js directive consists of two parts, the `@js` tag and `'<path>'` string,
where you can insert your src
```
@js '<url>'
```
### Example for the @js directive:
```html
@js 'myscripts.js'
```
# Variables
All variables, which are declared before the $tmpl can be used through the `@{my_var}` syntax.
It's also possible to use properties of structs here like `@{my_struct.prop}`.
# Escaping
The `@` symbol starts a template directive. If you need to use `@` as a regular
character within a template, escape it by using a double `@` like this: `@@`.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
#!/usr/bin/env -S v -n -w -cg -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
import os
import flag
@@ -12,7 +12,7 @@ fp.skip_executable()
mut path := fp.string('path', `p`, "", 'Path where to generate a module, if not mentioned will scan over all installers & clients.\nif . then will be path we are on.')
reset := fp.bool('reset', `r`, false, 'If we want to reset')
interactive := fp.bool('interactive', `i`, true, 'If we want to work interactive')
interactive := fp.bool('interactive', `i`, false, 'If we want to work interactive')
scan := fp.bool('scan', `s`, false, 'If we want to scan')
help_requested := fp.bool('help', `h`, false, 'Show help message')

View File

@@ -64,7 +64,7 @@ os.symlink('${abs_dir_of_script}/lib', '${os.home_dir()}/.vmodules/freeflowunive
println('Herolib installation completed successfully!')
// Add vtest alias
addtoscript('alias vtest=', 'alias vtest=\'v -stats -enable-globals -n -w -gc none -no-retry-compilation -cc tcc test\' %') or {
addtoscript('alias vtest=', 'alias vtest=\'v -stats -enable-globals -n -w -cg -gc none -no-retry-compilation -cc tcc test\' %') or {
eprintln('Failed to add vtest alias: ${err}')
}

View File

@@ -7,7 +7,6 @@ pub const version = '1.0.0'
const singleton = false
const default = true
// TODO: THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE TO STRUCT BELOW, IS STRUCTURED AS HEROSCRIPT
pub fn heroscript_default() string {
mail_from := os.getenv_opt('MAIL_FROM') or { 'info@example.com' }
mail_password := os.getenv_opt('MAIL_PASSWORD') or { 'secretpassword' }

View File

@@ -0,0 +1,7 @@
!!hero_code.generate_client
name: "mycelium"
classname: "Mycelium"
hasconfig: true
singleton: true
default: true
title: ""

View File

@@ -1,9 +1,8 @@
module mycelium
import net.http
import json
import freeflowuniverse.herolib.core.httpconnection
const server_url = 'http://localhost:8989/api/v1/messages'
pub struct MessageDestination {
pub:
@@ -36,74 +35,87 @@ pub:
msg_len string @[json: 'msgLen']
}
pub fn send_msg(pk string, payload string, wait bool) !InboundMessage {
mut url := server_url
if wait {
url = '${url}?reply_timeout=120'
pub fn (mut self Mycelium) connection() !&httpconnection.HTTPConnection {
mut c := self.conn or {
mut c2 := httpconnection.new(
name: 'mycelium'
url: self.server_url
retry: 3
)!
c2
}
msg_req := PushMessageBody{
dst: MessageDestination{
pk: pk
}
payload: payload
}
mut req := http.new_request(http.Method.post, url, json.encode(msg_req))
req.add_custom_header('content-type', 'application/json')!
if wait {
req.read_timeout = 1200000000000
}
res := req.do()!
msg := json.decode(InboundMessage, res.body)!
return msg
return c
}
pub fn receive_msg(wait bool) !InboundMessage {
mut url := server_url
if wait {
url = '${url}?timeout=60'
pub fn (mut self Mycelium) send_msg(pk string, payload string, wait bool) !InboundMessage {
mut conn := self.connection()!
mut params := {
'dst': json.encode(MessageDestination{pk: pk})
'payload': payload
}
mut req := http.new_request(http.Method.get, url, '')
mut prefix := ''
if wait {
req.read_timeout = 600000000000
prefix = '?reply_timeout=120'
}
res := req.do()!
msg := json.decode(InboundMessage, res.body)!
return msg
return conn.post_json_generic[InboundMessage](
method: .post
prefix: prefix
params: params
dataformat: .json
)!
}
pub fn receive_msg_opt(wait bool) ?InboundMessage {
mut url := server_url
pub fn (mut self Mycelium) receive_msg(wait bool) !InboundMessage {
mut conn := self.connection()!
mut prefix := ''
if wait {
url = '${url}?timeout=60'
prefix = '?timeout=60'
}
mut req := http.new_request(http.Method.get, url, '')
return conn.get_json_generic[InboundMessage](
method: .get
prefix: prefix
dataformat: .json
)!
}
pub fn (mut self Mycelium) receive_msg_opt(wait bool) ?InboundMessage {
mut conn := self.connection()!
mut prefix := ''
if wait {
req.read_timeout = 600000000000
prefix = '?timeout=60'
}
res := req.do() or { panic(error) }
if res.status_code == 204 {
res := conn.get_json_generic[InboundMessage](
method: .get
prefix: prefix
dataformat: .json
) or {
if err.msg().contains('204') {
return none
}
msg := json.decode(InboundMessage, res.body) or { panic(err) }
return msg
panic(err)
}
return res
}
pub fn get_msg_status(id string) !MessageStatusResponse {
mut url := '${server_url}/status/${id}'
res := http.get(url)!
msg_res := json.decode(MessageStatusResponse, res.body)!
return msg_res
pub fn (mut self Mycelium) get_msg_status(id string) !MessageStatusResponse {
mut conn := self.connection()!
return conn.get_json_generic[MessageStatusResponse](
method: .get
prefix: 'status/${id}'
dataformat: .json
)!
}
pub fn reply_msg(id string, pk string, payload string) !http.Status {
mut url := '${server_url}/reply/${id}'
msg_req := PushMessageBody{
dst: MessageDestination{
pk: pk
pub fn (mut self Mycelium) reply_msg(id string, pk string, payload string) ! {
mut conn := self.connection()!
mut params := {
'dst': json.encode(MessageDestination{pk: pk})
'payload': payload
}
payload: payload
}
res := http.post_json(url, json.encode(msg_req))!
return res.status()
conn.post_json_generic[json.Any](
method: .post
prefix: 'reply/${id}'
params: params
dataformat: .json
)!
}

View File

@@ -19,8 +19,94 @@ pub mut:
name string
}
fn args_get (args_ ArgsGet) ArgsGet {
mut model:=args_
if model.name == ""{
model.name = mycelium_default
}
if model.name == ""{
model.name = "default"
}
return model
}
pub fn get(args_ ArgsGet) !&Mycelium {
return &Mycelium{}
mut model := args_get(args_)
if !(model.name in mycelium_global) {
if model.name=="default"{
if ! config_exists(model){
if default{
config_save(model)!
}
}
config_load(model)!
}
}
return mycelium_global[model.name] or {
println(mycelium_global)
panic("could not get config for mycelium with name:${model.name}")
}
}
fn config_exists(args_ ArgsGet) bool {
mut model := args_get(args_)
mut context:=base.context() or { panic("bug") }
return context.hero_config_exists("mycelium",model.name)
}
fn config_load(args_ ArgsGet) ! {
mut model := args_get(args_)
mut context:=base.context()!
mut heroscript := context.hero_config_get("mycelium",model.name)!
play(heroscript:heroscript)!
}
fn config_save(args_ ArgsGet) ! {
mut model := args_get(args_)
mut context:=base.context()!
context.hero_config_set("mycelium",model.name,heroscript_default()!)!
}
fn set(o Mycelium)! {
mut o2:=obj_init(o)!
mycelium_global[o.name] = &o2
mycelium_default = o.name
}
@[params]
pub struct PlayArgs {
pub mut:
heroscript string //if filled in then plbook will be made out of it
plbook ?playbook.PlayBook
reset bool
}
pub fn play(args_ PlayArgs) ! {
mut model:=args_
if model.heroscript == "" {
model.heroscript = heroscript_default()!
}
mut plbook := model.plbook or {
playbook.new(text: model.heroscript)!
}
mut install_actions := plbook.find(filter: 'mycelium.configure')!
if install_actions.len > 0 {
for install_action in install_actions {
mut p := install_action.params
mycfg:=cfg_play(p)!
console.print_debug("install action mycelium.configure\n${mycfg}")
set(mycfg)!
}
}
}

View File

@@ -1,25 +1,37 @@
module mycelium
import freeflowuniverse.herolib.data.paramsparser
import freeflowuniverse.herolib.core.httpconnection
import os
pub const version = '1.14.3'
pub const version = '0.0.0'
const singleton = true
const default = true
pub fn heroscript_default() !string {
heroscript:="
!!mycelium.configure
name:'mycelium'
"
return heroscript
}
//THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
$[heap]
pub struct Mycelium {
pub mut:
name string = 'default'
mail_from string
mail_password string @[secret]
mail_port int
mail_server string
mail_username string
server_url string
conn ?&httpconnection.HTTPConnection
}
fn cfg_play(p paramsparser.Params) ! {
mut mycfg := Mycelium{
name: p.get_default('name', 'default')!
server_url: p.get_default('server_url', 'http://localhost:8989/api/v1/messages')!
}
set(mycfg)!
}
fn obj_init(obj_ Mycelium)!Mycelium{
@@ -27,6 +39,3 @@ fn obj_init(obj_ Mycelium)!Mycelium{
mut obj:=obj_
return obj
}

View File

@@ -1,30 +1,141 @@
# mycelium
# Mycelium Client
|
A V client library for interacting with the Mycelium messaging system. This client provides functionality for sending, receiving, and managing messages through a Mycelium server.
To get started
## Configuration
```vlang
The client can be configured either through V code or using heroscript.
### V Code Configuration
```v
import freeflowuniverse.herolib.clients.mycelium
mut client := mycelium.get()!
client...
// By default connects to http://localhost:8989/api/v1/messages
// To use a different server:
mut client := mycelium.get(name: "custom", server_url: "http://myserver:8989/api/v1/messages")!
```
## example heroscript
### Heroscript Configuration
```hero
!!mycelium.configure
secret: '...'
host: 'localhost'
port: 8888
name:'custom' # optional, defaults to 'default'
server_url:'http://myserver:8989/api/v1/messages' # optional, defaults to localhost:8989
```
Note: Configuration is not needed if using a locally running Mycelium server with default settings.
## Example Script
Save as `mycelium_example.vsh`:
```v
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.clients.mycelium
// Initialize client
mut client := mycelium.get()!
// Send a message and wait for reply
msg := client.send_msg(
pk: "recipient_public_key"
payload: "Hello!"
wait: true // wait for reply (timeout 120s)
)!
println('Message sent with ID: ${msg.id}')
// Check message status
status := client.get_msg_status(msg.id)!
println('Message status: ${status.state}')
// Receive messages with timeout
if incoming := client.receive_msg_opt(wait: true) {
println('Received message: ${incoming.payload}')
println('From: ${incoming.src_pk}')
// Reply to the message
client.reply_msg(
id: incoming.id
pk: incoming.src_pk
payload: "Got your message!"
)!
}
```
## API Reference
### Sending Messages
```v
// Send a message to a specific public key
// wait=true means wait for reply (timeout 120s)
msg := client.send_msg(pk: "recipient_public_key", payload: "Hello!", wait: true)!
// Get status of a sent message
status := client.get_msg_status(id: "message_id")!
```
### Receiving Messages
```v
// Receive a message (non-blocking)
msg := client.receive_msg(wait: false)!
// Receive a message with timeout (blocking for 60s)
msg := client.receive_msg(wait: true)!
// Receive a message (returns none if no message available)
if msg := client.receive_msg_opt(wait: false) {
println('Received: ${msg.payload}')
}
```
### Replying to Messages
```v
// Reply to a specific message
client.reply_msg(
id: "original_message_id",
pk: "sender_public_key",
payload: "Reply message"
)!
```
## Message Types
### InboundMessage
```v
struct InboundMessage {
id string
src_ip string
src_pk string
dst_ip string
dst_pk string
payload string
}
```
### MessageStatusResponse
```v
struct MessageStatusResponse {
id string
dst string
state string
created string
deadline string
msg_len string
}
```
## Heroscript Complete Example
```hero
!!mycelium.configure
name:'mycelium'
server_url:'http://localhost:8989/api/v1/messages'
# More heroscript commands can be added here as the API expands

View File

@@ -39,7 +39,7 @@ pub fn ask(path string) ! {
if model.hasconfig {
model.singleton = !myconsole.ask_yesno(
description: 'Can there be multiple instances (normally yes)?'
default: model.singleton
default: !model.singleton
)!
if model.cat == .installer {
model.templates = myconsole.ask_yesno(
@@ -64,6 +64,12 @@ pub fn ask(path string) ! {
)!
}
// if true{
// println(model)
// panic("Sdsd")
// }
gen_model_set(GenerateArgs{model: model, path: path})!
}

View File

@@ -37,6 +37,11 @@ pub fn do(args_ GenerateArgs) ! {
m
}
if model.classname == "" {
args.interactive = true
}
if create{
if args.path == '' {
return error("need to specify path fo ${args_} because we asked to create .heroscript ")
@@ -57,6 +62,8 @@ pub fn do(args_ GenerateArgs) ! {
if args.interactive{
ask(args.path)!
args.model = gen_model_get(args.path, false)!
}else{
args.model = model
}
console.print_debug(args)

View File

@@ -9,7 +9,7 @@ fn generate(args GenerateArgs) ! {
console.print_debug('generate code for path: ${args.path}')
//as used in the templates
model := args.model or { panic('bug') }
model := args.model or { panic('bug no model specified in generate') }
mut path_actions := pathlib.get(args.path + '/${model.name}_actions.v')
if args.reset {

View File

@@ -18,7 +18,7 @@ pub mut:
templates bool // means we will use templates in the installer, client doesn't do this'
reset bool // regenerate all, dangerous !!!
interactive bool //if we want to ask
startupmanager bool = true
startupmanager bool
build bool
hasconfig bool
cat Cat = .client
@@ -33,6 +33,7 @@ pub enum Cat {
pub fn gen_model_set(args GenerateArgs) ! {
console.print_debug("Code generator set: ${args}")
model := args.model or { return error('model is none') }
heroscript_templ := match model.cat {
.client { $tmpl('templates/heroscript_client') }
@@ -111,6 +112,8 @@ pub fn gen_model_get(path string, create bool) !GenModel {
model.name = os.base(path).to_lower()
}
console.print_debug("Code generator get: ${model}")
return model
// return GenModel{}
}

View File

@@ -15,6 +15,8 @@ pub mut:
// scan over a set of directories call the play where
pub fn scan(args ScannerArgs) ! {
console.print_debug("Code generator scan: ${args.path}")
if args.path == "" {
scan(path:"${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/installers")!
scan(path:"${os.home_dir()}/code/github/freeflowuniverse/herolib/lib/clients")!

View File

@@ -1,7 +1,7 @@
!!hero_code.generate_client
name: "${model.name}"
classname: "${model.classname}"
config: ${model.hasconfig}
hasconfig: ${model.hasconfig}
singleton: ${model.singleton}
default: ${model.default}
title: "${model.title}|
title: "${model.title}"

View File

@@ -1,7 +1,7 @@
!!hero_code.generate_installer
name: "${model.name}"
classname: "${model.classname}"
config: ${model.hasconfig}
hasconfig: ${model.hasconfig}
singleton: ${model.singleton}
default: ${model.default}
title: "${model.title}"

View File

@@ -2,7 +2,7 @@ module ${model.name}
import freeflowuniverse.herolib.data.paramsparser
import os
pub const version = '1.14.3'
pub const version = '0.0.0'
const singleton = ${model.singleton}
const default = ${model.default}

View File

@@ -22,6 +22,27 @@ mut conn := HTTPConnection{
}
```
## important: How to use it on a management class e.g. an installe or a client
```v
// e.g. HetznerManager is the object on which we want to have an http client
pub fn (mut h HetznerManager) connection() !&httpconnection.HTTPConnection {
mut c := h.conn or {
mut c2 := httpconnection.new(
name: 'hetzner_${h.name}'
url: h.baseurl
cache: true
retry: 3
)!
c2.basic_auth(h.user, h.password)
c2
}
return c
}
```
## Examples
### GET Request with JSON Response

View File

@@ -47,6 +47,7 @@ pub fn (mut c UIConsole) ask_question(args QuestionArgs) !string {
description: args.description
warning: 'Min lenght of answer is: ${args.minlen}'
question: args.question
default: args.default
)
}
return choice

View File

@@ -31,7 +31,7 @@ pub fn (mut c UIConsole) ask_yesno(args YesNoArgs) !bool {
print_debug('${question} (y/n) : ')
choice := os.get_raw_line().trim(' \n').to_lower()
if choice.trim_space() == "" {
return true
return args.default
}
if choice.starts_with('y') {
return true
@@ -50,5 +50,6 @@ pub fn (mut c UIConsole) ask_yesno(args YesNoArgs) !bool {
question: args.question
warning: "Please choose 'y' or 'n', then enter."
reset: true
default: args.default
)
}

View File

@@ -0,0 +1,117 @@
module cloudhypervisor
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.installers.virt.cloudhypervisor as cloudhypervisorinstaller
import os
@[heap]
pub struct CloudHypervisorFactory {
pub mut:
debug bool
}
pub fn new() !CloudHypervisorFactory {
mut inst := cloudhypervisorinstaller.get()!
inst.install()!
return CloudHypervisorFactory{}
}
// pub fn (mut lf CloudHypervisorFactory) vm_get_all() ![]VM {
// mut vms := []VM{}
// for vm in raw.list()! {
// // console.print_debug(vm)
// mut vm2 := VM{
// name: vm.name
// dir: vm.dir
// arch: vm.arch
// cpus: vm.cpus
// memory: vm.memory / 1000000
// disk: vm.disk / 1000000
// ssh_local_port: vm.ssh_local_port
// ssh_address: vm.ssh_address
// identity_file: vm.identity_file
// factory: &lf
// }
// match vm.status {
// 'Running' {
// vm2.status = .running
// }
// 'Stopped' {
// vm2.status = .stopped
// }
// else {
// console.print_debug(vm.status)
// panic('unknown status')
// }
// }
// vms << vm2
// }
// return vms
// }
// pub fn (mut lf CloudHypervisorFactory) vm_get(name string) !VM {
// for vm in lf.vm_get_all()! {
// if vm.name.to_lower() == name.to_lower() {
// return vm
// }
// }
// return error("Couldn't find vm with name: ${name}")
// }
// pub fn (mut lf CloudHypervisorFactory) vm_exists(name string) !bool {
// for vm in lf.vm_list()! {
// if vm.to_lower() == name.to_lower() {
// return true
// }
// }
// return false
// }
// pub fn (mut lf CloudHypervisorFactory) vm_stop_all() ! {
// for mut vm in lf.vm_get_all()! {
// vm.stop()!
// }
// }
// pub fn (mut lf CloudHypervisorFactory) vm_delete_all() ! {
// for mut vm in lf.vm_get_all()! {
// vm.delete()!
// }
// }
// pub fn (mut lf CloudHypervisorFactory) vm_list() ![]string {
// cmd := "qemuctl list -f '{{.Name}}'"
// res := os.execute(cmd)
// mut vms := []string{}
// if res.exit_code > 0 {
// return error('could not stop qemu vm.\n${res}')
// }
// for line in res.output.split_into_lines() {
// if line.trim_space() == '' {
// continue
// }
// vms << line.trim_space()
// }
// return vms
// }
// pub fn (mut lf CloudHypervisorFactory) vm_stop(name string) ! {
// console.print_header('vm: ${name} stop')
// cmd := 'qemuctl stop ${name}'
// os.execute(cmd)
// // if res.exit_code > 0 {
// // return error('could not delete qemu vm.\n${res}')
// // }
// cmd2 := 'qemuctl stop ${name} -f'
// os.execute(cmd2)
// }
// pub fn (mut lf CloudHypervisorFactory) vm_delete(name string) ! {
// console.print_header('vm: ${name} delete')
// lf.vm_stop(name)!
// cmd := 'qemuctl delete ${name} -f'
// res := os.execute(cmd)
// if res.exit_code > 0 {
// return error('could not delete qemu vm.\n${res}')
// }
// }

View File

@@ -0,0 +1,12 @@
module docker
import freeflowuniverse.herolib.data.paramsparser { Params }
@[params]
pub struct BuildArgs {
pub mut:
reset bool // resets the docker state, will build again
strict bool // question: ?
engine &DockerEngine
args Params
}

View File

@@ -0,0 +1,84 @@
module docker
import freeflowuniverse.herolib.data.paramsparser { Params }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.osal { exec, file_write }
import os
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct DockerComposeRecipe {
pub mut:
name string
content string
params Params
engine &DockerEngine @[str: skip]
items []&ComposeService
path string
}
@[params]
pub struct ComposeArgs {
pub mut:
name string @[required]
params string
composepath string // can be empty, if empty will be buildpath/compose
}
pub fn (mut e DockerEngine) compose_new(args_ ComposeArgs) DockerComposeRecipe {
mut args := args_
if args.name == '' {
panic('name cannot be empty.')
}
if args.composepath == '' {
args.composepath = '${e.buildpath}/compose'
}
return DockerComposeRecipe{
engine: &e
name: args.name
path: args.composepath + '/${args.name}'
}
}
pub fn (mut b DockerComposeRecipe) stop() ! {
// TODO:
}
pub fn (mut b DockerComposeRecipe) delete() ! {
b.stop()!
exec(cmd: 'rm -rf ${b.path} && mkdir -p ${b.path}', stdout: false)!
}
fn (mut b DockerComposeRecipe) render() ! {
if b.items.len == 0 {
return error('Cannot find items for the docker compose in service: ')
}
b.content = 'version: "3.9"\n'
b.content += 'services:\n'
for mut item in b.items {
c := item.render()!
b.content += texttools.indent(c, ' ')
}
}
pub fn (mut b DockerComposeRecipe) start() ! {
b.render()!
console.print_debug(b)
console.print_debug(' start compose file in: ${b.path}')
os.mkdir_all(b.path)!
file_write('${b.path}/docker-compose.yml', b.content)!
for composeitem in b.items {
for item in composeitem.files {
filename := item.path.all_after_first('/')
file_write('${b.path}/${filename}', item.to_string())!
}
}
cmd := '
set -ex
cd ${b.path}
docker compose up -d
'
exec(cmd: cmd)!
}

View File

@@ -0,0 +1,141 @@
module docker
import v.embed_file
import freeflowuniverse.herolib.data.paramsparser { Params }
@[heap]
pub struct ComposeService {
pub mut:
name string
content string // optional
params Params
files []embed_file.EmbedFileData
recipe &DockerComposeRecipe @[str: skip]
render bool = true
env map[string]string
ports []PortMap
volumes []VolumeMap
restart bool
image string
}
pub struct PortMap {
pub:
indocker int
host int
}
pub struct VolumeMap {
pub:
hostpath string
containerpath string
}
@[params]
pub struct ComposeServiceArgs {
pub:
name string @[required]
image string @[required]
params string
content string // optional, if this is set then will not render
}
pub fn (mut recipe DockerComposeRecipe) service_new(args ComposeServiceArgs) !&ComposeService {
if args.name == '' {
return error('name cannot be empty.')
}
mut cs := ComposeService{
recipe: &recipe
name: args.name
content: args.content
image: args.image
}
if args.content.len > 0 {
cs.render = false
}
recipe.items << &cs
return recipe.items.last()
}
// example:
// registry:
// restart: always
// image: registry:2
// ports:
// - 5000:5000
// environment:
// REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
// REGISTRY_HTTP_TLS_KEY: /certs/domain.key
// REGISTRY_AUTH: htpasswd
// REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
// REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
// volumes:
// - ${registry.datapath}/data:/var/lib/registry
// - ${registry.datapath}/certs:/certs
// - ${registry.datapath}/auth:/auth
// render the recipe to a compose file
fn (mut b ComposeService) render() !string {
mut out := '${b.name}:\n'
if b.render == false {
return b.content
}
out += ' image: ${b.image}\n'
if b.restart {
out += ' restart: always\n'
}
if b.ports.len > 0 {
out += ' ports:\n'
for port in b.ports {
out += ' - ${port.indocker}:${port.host}\n'
}
}
if b.env.len > 0 {
out += ' environment:\n'
for ekey, eval in b.env {
out += ' ${ekey.to_upper()}: ${eval}\n'
}
}
if b.volumes.len > 0 {
out += ' volumes:\n'
for volitem in b.volumes {
out += ' - ${volitem.hostpath}:${volitem.containerpath}\n'
}
}
b.content = out
return out
}
// add an environment variable to the service
pub fn (mut cs ComposeService) env_add(key string, val string) {
cs.env[key] = val
}
// make sure the service always restarts
pub fn (mut cs ComposeService) restart_set() {
cs.restart = true
}
// make sure the service always restarts
pub fn (mut cs ComposeService) port_expose(indocker int, host int) ! {
// TODO: should check if hostport is free
mp := PortMap{
indocker: indocker
host: host
}
cs.ports << mp
}
// make sure the service always restarts
pub fn (mut cs ComposeService) volume_add(hostpath string, containerpath string) ! {
mut vm := VolumeMap{
hostpath: hostpath
containerpath: containerpath
}
vm.check()!
cs.volumes << vm
}
pub fn (mut vm VolumeMap) check() ! {
// TODO: see if it exists on remote host
}

View File

@@ -0,0 +1,134 @@
module docker
import time
import freeflowuniverse.herolib.osal { exec }
import freeflowuniverse.herolib.data.ipaddress { IPAddress }
import freeflowuniverse.herolib.virt.utils
import freeflowuniverse.herolib.ui.console
// pub enum DockerContainerStatus {
// up
// down
// restarting
// paused
// dead
// created
// }
// need to fill in what is relevant
@[heap]
pub struct DockerContainer {
pub mut:
id string
name string
created time.Time
ssh_enabled bool // if yes make sure ssh is enabled to the container
ipaddr IPAddress
forwarded_ports []string
mounts []utils.ContainerVolume
ssh_port int // ssh port on node that is used to get ssh
ports []string
networks []string
labels map[string]string @[str: skip]
image &DockerImage @[str: skip]
engine &DockerEngine @[str: skip]
status utils.ContainerStatus
memsize int // in MB
command string
}
@[params]
pub struct DockerContainerCreateArgs {
name string
hostname string
forwarded_ports []string // ["80:9000/tcp", "1000, 10000/udp"]
mounted_volumes []string // ["/root:/root", ]
env map[string]string // map of environment variables that will be passed to the container
privileged bool
remove_when_done bool = true // remove the container when it shuts down
pub mut:
image_repo string
image_tag string
command string = '/bin/bash'
}
// create/start container (first need to get a dockercontainer before we can start)
pub fn (mut container DockerContainer) start() ! {
exec(cmd: 'docker start ${container.id}')!
container.status = utils.ContainerStatus.up
}
// delete docker container
pub fn (mut container DockerContainer) halt() ! {
osal.execute_stdout('docker stop ${container.id}') or { '' }
container.status = utils.ContainerStatus.down
}
// delete docker container
pub fn (mut container DockerContainer) delete() ! {
console.print_debug(' CONTAINER DELETE: ${container.name}')
exec(cmd: 'docker rm ${container.id} -f', stdout: false)!
mut x := 0
for container2 in container.engine.containers {
if container2.name == container.name {
container.engine.containers.delete(x)
}
x += 1
}
}
// save the docker container to image
pub fn (mut container DockerContainer) save2image(image_repo string, image_tag string) !string {
id := osal.execute_stdout('docker commit ${container.id} ${image_repo}:${image_tag}')!
container.image.id = id.trim(' ')
return id
}
// export docker to tgz
pub fn (mut container DockerContainer) export(path string) ! {
exec(cmd: 'docker export ${container.id} > ${path}')!
}
// // open ssh shell to the cobtainer
// pub fn (mut container DockerContainer) ssh_shell(cmd string) ! {
// container.engine.node.shell(cmd)!
// }
@[params]
pub struct DockerShellArgs {
pub mut:
cmd string
}
// open shell to the container using docker, is interactive, cannot use in script
pub fn (mut container DockerContainer) shell(args DockerShellArgs) ! {
mut cmd := ''
if args.cmd.len == 0 {
cmd = 'docker exec -ti ${container.id} /bin/bash'
} else {
cmd = "docker exec -ti ${container.id} /bin/bash -c '${args.cmd}'"
}
exec(cmd: cmd, shell: true, debug: true)!
}
pub fn (mut container DockerContainer) execute(cmd_ string, silent bool) ! {
cmd := 'docker exec ${container.id} ${cmd_}'
exec(cmd: cmd, stdout: !silent)!
}
// pub fn (mut container DockerContainer) ssh_enable() ! {
// // mut docker_pubkey := pubkey
// // cmd = "docker exec $container.id sh -c 'echo \"$docker_pubkey\" >> ~/.ssh/authorized_keys'"
// // if container.engine.node.executor is builder.ExecutorSSH {
// // mut sshkey := container.engine.node.executor.info()['sshkey'] + '.pub'
// // sshkey = os.read_file(sshkey) or { panic(err) }
// // // add pub sshkey on authorized keys of node and container
// // cmd = "echo \"$sshkey\" >> ~/.ssh/authorized_keys && docker exec $container.id sh -c 'echo \"$docker_pubkey\" >> ~/.ssh/authorized_keys && echo \"$sshkey\" >> ~/.ssh/authorized_keys'"
// // }
// // wait making sure container started correctly
// // time.sleep_ms(100 * time.millisecond)
// // container.engine.node.executor.exec(cmd) !
// }

View File

@@ -0,0 +1,49 @@
module docker
import freeflowuniverse.herolib.osal { exec }
pub fn (mut e DockerEngine) container_create(args DockerContainerCreateArgs) !&DockerContainer {
mut ports := ''
mut mounts := ''
mut env := ''
mut command := args.command
for var, value in args.env {
env += '-e ${var}="${value}"'
}
for port in args.forwarded_ports {
ports = ports + '-p ${port} '
}
for mount in args.mounted_volumes {
mounts += '-v ${mount} '
}
mut image := '${args.image_repo}'
if args.image_tag != '' {
image = image + ':${args.image_tag}'
}
if image == 'threefold' || image == 'threefold:latest' || image == '' {
image = 'threefoldtech/grid3_ubuntu_dev'
command = '/usr/local/bin/boot.sh'
}
privileged := if args.privileged { '--privileged' } else { '' }
// if forwarded ports passed in the args not containing mapping tp ssh (22) create one
if !contains_ssh_port(args.forwarded_ports) {
// find random free port in the node
mut port := e.get_free_port() or { panic('No free port.') }
ports += '-p ${port}:22/tcp'
}
exec(
cmd: 'docker run --hostname ${args.hostname} ${privileged} --sysctl net.ipv6.conf.all.disable_ipv6=0 --name ${args.name} ${ports} ${env} ${mounts} -d -t ${image} ${command}'
)!
// Have to reload the containers as container_get works from memory
e.containers_load()!
mut container := e.container_get(name: args.name)!
return container
}

View File

@@ -0,0 +1,259 @@
module docker
import freeflowuniverse.herolib.osal { cputype, exec, platform }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.virt.utils
import freeflowuniverse.herolib.ui.console
// import freeflowuniverse.herolib.installers.swarm
// https://docs.docker.com/reference/
@[heap]
pub struct DockerEngine {
name string
pub mut:
sshkeys_allowed []string // all keys here have access over ssh into the machine, when ssh enabled
images []DockerImage
containers []DockerContainer
buildpath string
localonly bool
cache bool = true
push bool
platform []BuildPlatformType // used to build
registries []DockerRegistry // one or more supported DockerRegistries
prefix string
}
pub enum BuildPlatformType {
linux_arm64
linux_amd64
}
// check docker has been installed & enabled on node
pub fn (mut e DockerEngine) init() ! {
if e.buildpath == '' {
e.buildpath = '/tmp/builder'
exec(cmd: 'mkdir -p ${e.buildpath}', stdout: false)!
}
if e.platform == [] {
if platform() == .ubuntu && cputype() == .intel {
e.platform = [.linux_amd64]
} else if platform() == .osx && cputype() == .arm {
e.platform = [.linux_arm64]
} else {
return error('only implemented ubuntu on amd and osx on arm for now for docker engine.')
}
}
e.load()!
}
// reload the state from system
pub fn (mut e DockerEngine) load() ! {
e.images_load()!
e.containers_load()!
}
// load all containers, they can be consulted in e.containers
// see obj: DockerContainer as result in e.containers
pub fn (mut e DockerEngine) containers_load() ! {
e.containers = []DockerContainer{}
mut ljob := exec(
// we used || because sometimes the command has | in it and this will ruin all subsequent columns
cmd: "docker ps -a --no-trunc --format '{{.ID}}||{{.Names}}||{{.Image}}||{{.Command}}||{{.CreatedAt}}||{{.Ports}}||{{.State}}||{{.Size}}||{{.Mounts}}||{{.Networks}}||{{.Labels}}'"
ignore_error_codes: [6]
stdout: false
)!
lines := ljob.output
for line in lines {
if line.trim_space() == '' {
continue
}
fields := line.split('||').map(utils.clear_str)
if fields.len < 11 {
panic('docker ps needs to output 11 parts.\n${fields}')
}
id := fields[0]
mut container := DockerContainer{
engine: &e
image: e.image_get(id: fields[2])!
}
container.id = id
container.name = texttools.name_fix(fields[1])
container.command = fields[3]
container.created = utils.parse_time(fields[4])!
container.ports = utils.parse_ports(fields[5])!
container.status = utils.parse_container_state(fields[6])!
container.memsize = utils.parse_size_mb(fields[7])!
container.mounts = utils.parse_mounts(fields[8])!
container.networks = utils.parse_networks(fields[9])!
container.labels = utils.parse_labels(fields[10])!
container.ssh_enabled = utils.contains_ssh_port(container.ports)
// console.print_debug(container)
e.containers << container
}
}
// EXISTS, GET
@[params]
pub struct ContainerGetArgs {
pub mut:
name string
id string
image_id string
// tag string
// digest string
}
pub struct ContainerGetError {
Error
pub:
args ContainerGetArgs
notfound bool
toomany bool
}
pub fn (err ContainerGetError) msg() string {
if err.notfound {
return 'Could not find image with args:\n${err.args}'
}
if err.toomany {
return 'Found more than 1 container with args:\n${err.args}'
}
panic('unknown error for ContainerGetError')
}
pub fn (err ContainerGetError) code() int {
if err.notfound {
return 1
}
if err.toomany {
return 2
}
panic('unknown error for ContainerGetError')
}
// get containers from memory
// params:
// name string (can also be a glob e.g. use *,? and [])
// id string
// image_id string
pub fn (mut e DockerEngine) containers_get(args_ ContainerGetArgs) ![]&DockerContainer {
mut args := args_
args.name = texttools.name_fix(args.name)
mut res := []&DockerContainer{}
for _, c in e.containers {
if args.name.contains('*') || args.name.contains('?') || args.name.contains('[') {
if c.name.match_glob(args.name) {
res << &c
continue
}
} else {
if c.name == args.name || c.id == args.id {
res << &c
continue
}
}
if args.image_id.len > 0 && c.image.id == args.image_id {
res << &c
}
}
if res.len == 0 {
return ContainerGetError{
args: args
notfound: true
}
}
return res
}
// get container from memory, can use match_glob see https://modules.vlang.io/index.html#string.match_glob
pub fn (mut e DockerEngine) container_get(args_ ContainerGetArgs) !&DockerContainer {
mut args := args_
args.name = texttools.name_fix(args.name)
mut res := e.containers_get(args)!
if res.len > 1 {
return ContainerGetError{
args: args
notfound: true
}
}
return res[0]
}
pub fn (mut e DockerEngine) container_exists(args ContainerGetArgs) !bool {
e.container_get(args) or {
if err.code() == 1 {
return false
}
return err
}
return true
}
pub fn (mut e DockerEngine) container_delete(args ContainerGetArgs) ! {
mut c := e.container_get(args)!
c.delete()!
e.load()!
}
// remove one or more container
pub fn (mut e DockerEngine) containers_delete(args ContainerGetArgs) ! {
mut cs := e.containers_get(args)!
for mut c in cs {
c.delete()!
}
e.load()!
}
// import a container into an image, run docker container with it
// image_repo examples ['myimage', 'myimage:latest']
// if DockerContainerCreateArgs contains a name, container will be created and restarted
pub fn (mut e DockerEngine) container_import(path string, mut args DockerContainerCreateArgs) !&DockerContainer {
mut image := args.image_repo
if args.image_tag != '' {
image = image + ':${args.image_tag}'
}
exec(cmd: 'docker import ${path} ${image}', stdout: false)!
// make sure we start from loaded image
return e.container_create(args)
}
// reset all images & containers, CAREFUL!
pub fn (mut e DockerEngine) reset_all() ! {
for mut container in e.containers.clone() {
container.delete()!
}
for mut image in e.images.clone() {
image.delete(true)!
}
exec(cmd: 'docker image prune -a -f', stdout: false) or { panic(err) }
exec(cmd: 'docker builder prune -a -f', stdout: false) or { panic(err) }
osal.done_reset()!
e.load()!
}
// Get free port
pub fn (mut e DockerEngine) get_free_port() ?int {
mut used_ports := []int{}
mut range := []int{}
for c in e.containers {
for p in c.forwarded_ports {
used_ports << p.split(':')[0].int()
}
}
for i in 20000 .. 40000 {
if i !in used_ports {
range << i
}
}
// arrays.shuffle<int>(mut range, 0)
if range.len == 0 {
return none
}
return range[0]
}

View File

@@ -0,0 +1,26 @@
module docker
@[params]
pub struct DockerEngineArgs {
pub mut:
sshkeys_allowed []string // ssh keys which will be inserted when a docker gets deployed (is not implemented yet)
name string = 'default'
localonly bool // do you build for local utilization only
prefix string // e.g. despiegk/ or myimage registry-host:5000/despiegk/) is added to the name when pushing
}
// if sshkeys_allowed empty array will check the local machine for loaded sshkeys
pub fn new(args DockerEngineArgs) !DockerEngine {
mut args2 := args
if args2.name == '' {
args2.name = 'local'
}
mut de := DockerEngine{
name: args2.name
sshkeys_allowed: args2.sshkeys_allowed
prefix: args.prefix
localonly: args.localonly
}
de.init()!
return de
}

View File

@@ -0,0 +1,134 @@
module docker
import time
import freeflowuniverse.herolib.osal { exec }
import freeflowuniverse.herolib.virt.utils
@[heap]
pub struct DockerImage {
pub mut:
repo string
id string
tag string
digest string
size int // size in MB
created time.Time
engine &DockerEngine @[str: skip]
}
// delete docker image
pub fn (mut image DockerImage) delete(force bool) ! {
mut forcestr := ''
if force {
forcestr = '-f'
}
exec(cmd: 'docker rmi ${image.id} ${forcestr}', stdout: false)!
mut x := 0
for image2 in image.engine.images {
if image2.id == image.id {
image.engine.images.delete(x)
}
x += 1
}
}
// export docker image to tar.gz
pub fn (mut image DockerImage) export(path string) !string {
exec(cmd: 'docker save ${image.id} > ${path}', stdout: false)!
return ''
}
// import docker image back into the local env
pub fn (mut image DockerImage) load(path string) ! {
exec(cmd: 'docker load < ${path}', stdout: false)!
}
@[params]
pub struct ImageGetArgs {
pub:
repo string
tag string
digest string
id string
}
pub struct ImageGetError {
Error
pub:
args ImageGetArgs
notfound bool
toomany bool
}
pub fn (err ImageGetError) msg() string {
if err.notfound {
return 'Could not find image with args:\n${err.args}'
}
if err.toomany {
return 'Found more than 1 image with args:\n${err.args}'
}
panic('unknown error for ImageGetError')
}
pub fn (err ImageGetError) code() int {
if err.notfound {
return 1
}
if err.toomany {
return 2
}
panic('unknown error for ImageGetError')
}
// find image based on repo and optional tag
// args:
// repo string
// tag string
// digest string
// id string
pub fn (mut e DockerEngine) image_get(args ImageGetArgs) !&DockerImage {
mut counter := 0
mut result_digest := ''
for i in e.images {
if args.digest == args.digest {
return &i
}
if args.digest != '' {
continue
}
if args.repo != '' && i.repo != args.repo {
continue
}
if args.tag != '' && i.tag != args.tag {
continue
}
if args.id != '' && i.id != args.id {
continue
}
result_digest = i.digest
counter += 1
}
if counter > 0 {
return ImageGetError{
args: args
toomany: true
}
}
if counter == 0 {
return ImageGetError{
args: args
notfound: true
}
}
return e.image_get(digest: result_digest)!
}
pub fn (mut e DockerEngine) image_exists(args ImageGetArgs) !bool {
e.image_get(args) or {
if err.code() == 1 {
return false
}
return err
}
return true
}

View File

@@ -0,0 +1,286 @@
module docker
import freeflowuniverse.herolib.data.paramsparser { Params }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.osal { exec, file_write }
import crypto.md5
import v.embed_file
import os
import freeflowuniverse.herolib.ui.console
// only 2 supported for now
pub enum PlatformType {
alpine
ubuntu
}
type RecipeItem = AddFileEmbeddedItem
| CmdItem
| CopyItem
| EntryPointItem
| EnvItem
| ExposeItem
| FromItem
| PackageItem
| RunItem
| VolumeItem
| WorkDirItem
| ZinitItem
pub struct RecipeArgs {
pub:
name string
prefix string // string (e.g. despiegk/ or myimage registry-host:5000/despiegk/) is added to the name when pushing
// also exists on docker engine level, if not used here will come from the docker engine level
tag string //(default is 'latest' but can be a version nr)
platform PlatformType
zinit bool = true
}
// params
// ```
// name string
// prefix string (e.g. despiegk/ or myimage registry-host:5000/despiegk/) is added to the name when pushing
// also exists on docker engine level, if not used here will come from the docker engine level
// tag string (default is 'latest' but can be a version nr)
// platform PlatformType
// zinit bool = true
// ```
pub fn (mut e DockerEngine) recipe_new(args RecipeArgs) DockerBuilderRecipe {
if args.name == '' {
panic('name cannot be empty.')
}
return DockerBuilderRecipe{
engine: &e
platform: args.platform
name: args.name
zinit: args.zinit
tag: args.tag
prefix: args.prefix
}
}
@[heap]
pub struct DockerBuilderRecipe {
pub mut:
name string
prefix string
tag string
params Params
files []embed_file.EmbedFileData
engine &DockerEngine @[str: skip]
items []RecipeItem
platform PlatformType
zinit bool
}
// delete the working directory
pub fn (mut b DockerBuilderRecipe) delete() ! {
// exec(cmd:)
panic('implement')
}
pub fn (mut b DockerBuilderRecipe) render() !string {
b.check_from_statement()!
b.check_conf_add()!
mut items := []string{}
mut zinitexists := false
for mut item in b.items {
item_str := match mut item {
FromItem {
item.check()!
item.render()!
}
RunItem {
item.check()!
item.render()!
}
PackageItem {
item.check()!
item.render()!
}
CmdItem {
item.check()!
item.render()!
}
AddFileEmbeddedItem {
item.check()!
item.render()!
}
EntryPointItem {
item.check()!
item.render()!
}
EnvItem {
item.check()!
item.render()!
}
WorkDirItem {
item.check()!
item.render()!
}
CopyItem {
item.check()!
item.render()!
}
ExposeItem {
item.check()!
item.render()!
}
VolumeItem {
item.check()!
item.render()!
}
ZinitItem {
zinitexists = true
item.check()!
item.render()!
}
}
items << item_str
}
if zinitexists {
// means there is a zinit, we need to add the directory for zinit
items << 'COPY zinit /etc/zinit\n'
}
return items.join('\n')
}
fn (mut b DockerBuilderRecipe) check_from_statement() ! {
mut fromfound := false
for item3 in b.items {
if item3 is FromItem {
fromfound = true
}
}
if fromfound == false {
// put automatically alpine or ubuntu in
// console.print_debug(" *** put automatically alpine or ubuntu in")
if b.platform == .alpine {
b.items.prepend(FromItem{ recipe: &b, image: 'alpine', tag: 'latest' })
} else {
b.items.prepend(FromItem{ recipe: &b, image: 'ubuntu', tag: 'latest' })
}
}
}
fn (mut b DockerBuilderRecipe) check_conf_add() ! {
// we need to make sure we insert it after the last FromItem
mut lastfromcounter := 0
mut counter := 0
for item3 in b.items {
if item3 is FromItem {
lastfromcounter = counter
}
counter += 1
}
for item in b.items {
if item is AddFileEmbeddedItem {
if item.source == 'conf.sh' {
return
}
}
}
b.items.insert(lastfromcounter + 1, AddFileEmbeddedItem{
recipe: &b
source: 'conf.sh'
dest: '/conf.sh'
check_embed: false
})
}
pub fn (mut b DockerBuilderRecipe) path() string {
destpath := '${b.engine.buildpath}/${b.name}'
return destpath
}
pub fn (mut b DockerBuilderRecipe) build(reset bool) ! {
dockerfilecontent := b.render()!
destpath := b.path()
os.mkdir_all(destpath)!
file_write('${destpath}/Dockerfile', dockerfilecontent)!
for item in b.files {
filename := item.path.all_after_first('/')
file_write('${destpath}/${filename}', item.to_string())!
}
confsh := '
export NAME="${b.name}"
'
file_write('${destpath}/conf.sh', texttools.dedent(confsh))!
if b.engine.localonly {
b.tag = 'local'
} else {
b.tag = 'latest'
}
mut bplatform := '--platform='
for plat in b.engine.platform {
if plat == .linux_arm64 {
bplatform += ',linux/arm64'
}
if plat == .linux_amd64 {
bplatform += ',linux/amd64'
}
bplatform = bplatform.replace('=,', '=').trim(',')
}
mut nocache := ''
if b.engine.cache == false || reset {
nocache = '--no-cache'
}
mut bpush := ''
if b.engine.push == true {
bpush = '--push'
}
cmd := '
set -ex
cd ${destpath}
docker buildx build . -t ${b.name}:${b.tag} --ssh default=\${SSH_AUTH_SOCK} ${nocache} ${bplatform} ${bpush}
'
mut cmdshell := 'set -ex\ncd ${destpath}\ndocker rm ${b.name} -f > /dev/null 2>&1\ndocker run --name ${b.name} '
if b.zinit { // means zinit is in docker
cmdshell += '-d'
// cmdshell += ' -v ${destpath}/zinit:/etc/zinit \\\n' //we don\t want to add zinit any more
} else {
cmdshell += '-it \\\n'
}
cmdshell += ' -v ${destpath}:/src \\\n'
cmdshell += ' -v /run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock \\\n'
cmdshell += ' -e SSH_AUTH_SOCK="/run/host-services/ssh-auth.sock" \\\n'
cmdshell += ' --privileged \\\n'
cmdshell += ' --hostname ${b.name} ${b.name}:${b.tag}'
if b.zinit {
cmdshell += '\n\ndocker exec -ti ${b.name} /bin/shell.sh'
} else {
cmdshell += " '/bin/shell.sh'\n"
}
cmdshell += '\ndocker rm ${b.name} -f > /dev/null 2>&1\n'
// console.print_debug(cmdshell)
mut tohash := dockerfilecontent + b.name + cmdshell + cmd
for mut item in b.items {
if mut item is AddFileEmbeddedItem {
c := item.getcontent()!
tohash += c
}
}
image_exists := b.engine.image_exists(repo: b.name)!
hashnew := md5.hexhash(tohash)
if image_exists && reset == false && osal.done_exists('build_${b.name}') {
hashlast := osal.done_get('build_${b.name}') or { '' }
if hashnew == hashlast {
console.print_debug('\n ** BUILD ALREADY DONE FOR ${b.name.to_upper()}\n')
return
}
}
file_write('${destpath}/shell.sh', cmdshell)!
os.chmod('${destpath}/shell.sh', 0o777)!
exec(scriptpath: '${destpath}/build.sh', cmd: cmd, scriptkeep: true)!
osal.done_set('build_${b.name}', hashnew)!
}

View File

@@ -0,0 +1,65 @@
module docker
import freeflowuniverse.herolib.osal { file_read }
@[params]
pub struct AddFileEmbeddedArgs {
pub mut:
source string // is the filename, needs to be embedded
dest string // in the container we're building
make_executable bool
}
pub struct AddFileEmbeddedItem {
pub mut:
source string
dest string // in the container we're building
recipe &DockerBuilderRecipe @[str: skip]
make_executable bool
check_embed bool = true
}
// to do something like: 'Add alpine:latest'
pub fn (mut b DockerBuilderRecipe) add_file_embedded(args AddFileEmbeddedArgs) ! {
mut item := AddFileEmbeddedItem{
source: args.source
dest: args.dest
make_executable: args.make_executable
recipe: &b
}
if item.source == '' {
return error('source cant be empty, \n${b}')
}
if item.dest == '' {
return error('dest cant be empty, \n${b}')
}
b.items << item
}
pub fn (mut i AddFileEmbeddedItem) check() ! {
if i.check_embed == false {
return
}
for fileitem in i.recipe.files {
filename := fileitem.path.all_after_first('/')
if filename == i.source {
return
}
}
return error('Could not find filename: ${i.source} in embedded files. \n ${i}')
}
pub fn (mut i AddFileEmbeddedItem) render() !string {
mut out := 'ADD ${i.source} ${i.dest}'
if i.make_executable {
out += '\nRUN chmod +x ${i.dest}\n'
}
return out
}
// get code from the file added
fn (mut i AddFileEmbeddedItem) getcontent() !string {
srcpath := '${i.recipe.engine.buildpath}/${i.recipe.name}/${i.source}'
content := file_read(srcpath)!
return content
}

View File

@@ -0,0 +1,38 @@
module docker
@[params]
pub struct CmdArgs {
pub mut:
cmd string
}
pub struct CmdItem {
pub mut:
cmd string
recipe &DockerBuilderRecipe @[str: skip]
}
// add run command to docker, is the cmd which is run when docker get's built
pub fn (mut b DockerBuilderRecipe) add_cmd(args CmdArgs) ! {
mut item := CmdItem{
cmd: args.cmd
recipe: &b
}
if item.cmd == '' {
return error('cmd cannot be empty, \n${b}')
}
b.items << item
}
pub fn (mut i CmdItem) check() ! {
// TODO checks to see if is valid
}
pub fn (mut i CmdItem) render() !string {
// todo: need to be able to deal with argement e.g. bash /bin/shell.sh this needs to be 2 elements
mut cmds := i.cmd.fields()
for mut cmd_ in cmds {
cmd_ = "\"${cmd_}\""
}
return 'CMD [${cmds.join(', ')}]'
}

View File

@@ -0,0 +1,52 @@
module docker
import freeflowuniverse.herolib.develop.gittools
import freeflowuniverse.herolib.ui.console
// import freeflowuniverse.herolib.core.pathlib
@[params]
pub struct CodeGetArgs {
pub mut:
url string // e.g. https://github.com/vlang/v
// other example url := 'https://github.com/threefoldfoundation/www_examplesite/tree/development/manual'
pull bool
reset bool
name string
dest string // where does the directory need to be checked out to
}
// checkout a code repository on right location
pub fn (mut r DockerBuilderRecipe) add_codeget(args_ CodeGetArgs) ! {
mut args := args_
mut gs := gittools.get(coderoot: '${r.path()}/code')!
locator := gs.locator_new(args.url)!
mut gr := gs.repo_get(locator: locator, pull: args.pull, reset: args.reset)!
if args.name == '' {
args.name = gr.addr.name
}
if args.dest == '' {
args.dest = '/code/${args.name}'
}
// gs.repos_print(filter: '')
// console.print_debug(gr)
// this will show the exact path of the manual
// console.print_debug(gr.path_content_get())
// mut gitaddr := gs.addr_get_from_url(url: url)!
if args.dest.len < 2 {
return error("dest is to short (min 3): now '${args.dest}'")
}
commonpath := gr.path_relative()
if commonpath.contains('..') {
panic('bug should not be')
}
r.add_copy(source: commonpath, dest: args.dest)!
}

View File

@@ -0,0 +1,73 @@
module docker
pub fn (mut r DockerBuilderRecipe) add_gobuilder() ! {
r.add_package(name: 'musl-dev,gcc, g++, go, make')!
r.add_env('GOPATH', '/app')!
r.add_workdir(workdir: '/app')!
}
@[params]
pub struct GoBuildArgs {
pub mut:
url string // e.g. https://github.com/valeriansaliou/sonic
pull bool
reset bool
buildcmd string = 'go run build.go '
copycmd string
name string
}
// do a build of a go package .
// will get the code, pull and/or reset .
// will the pull all dependencies .
// will do the build
// DEBUG TRICK: put debug flag on, which will not execute the build cmd .
// you can go to /tmp/build/buildname and do shell.sh to debug
pub fn (mut r DockerBuilderRecipe) add_gobuild_from_code(args GoBuildArgs) ! {
r.add_codeget(url: args.url, name: args.name, reset: args.reset, pull: args.pull)!
if args.name == '' {
return error('name needs to be specified.')
}
r.add_run(
cmd: '
cd /code/${args.name}
${args.buildcmd}
${args.copycmd}
'
)!
}
@[params]
pub struct GoPackageArgs {
pub mut:
name string // can be comma separated, can also be url e.g. github.com/caddyserver/xcaddy/cmd/xcaddy@latest
postcmd string // normally empty
}
// install go components
pub fn (mut r DockerBuilderRecipe) add_go_package(args GoPackageArgs) ! {
if args.name == '' {
return error('name cannot be empty, name can be comma separated')
}
mut names := []string{}
if args.name.contains(',') {
for item2 in args.name.split(',') {
names << item2.trim_space()
}
} else {
if args.name != '' {
names << args.name.trim_space()
}
}
for name in names {
r.add_run(
cmd: '
go install ${name}
${args.postcmd}
'
)!
}
}

View File

@@ -0,0 +1,79 @@
module docker
// import freeflowuniverse.herolib.develop.gittools
// import freeflowuniverse.herolib.core.pathlib
@[params]
pub struct RustBuildArgs {
pub mut:
url string // e.g. https://github.com/valeriansaliou/sonic
pull bool
reset bool
buildcmd string = 'cargo build --release'
copycmd string
debug bool // to be able to easily debug the intermediate step
name string
}
// do a build of a rust package .
// will get the code, pull and/or reset .
// will the pull all dependencies .
// will do the build
// DEBUG TRICK: put debug flag on, which will not execute the build cmd .
// you can go to /tmp/build/buildname and do shell.sh to debug
pub fn (mut r DockerBuilderRecipe) add_rustbuild_from_code(args RustBuildArgs) ! {
r.add_codeget(url: args.url, name: args.name, reset: args.reset, pull: args.pull)!
r.add_run(
cmd: '
source ~/.cargo/env
cd /code/${args.name}
cargo update --dry-run
'
)!
if !args.debug {
r.add_run(
cmd: '
source ~/.cargo/env
cd /code/${args.name}
${args.buildcmd}
${args.copycmd}
'
)!
}
}
@[params]
pub struct RustPackageArgs {
pub mut:
name string // can be comma separated
copycmd string // normally empty
}
// use cargo install to install rust components
pub fn (mut r DockerBuilderRecipe) add_rust_package(args RustPackageArgs) ! {
if args.name == '' {
return error('name cannot be empty, name can be comma separated')
}
mut names := []string{}
if args.name.contains(',') {
for item2 in args.name.split(',') {
names << item2.trim_space()
}
} else {
if args.name != '' {
names << args.name.trim_space()
}
}
for name in names {
r.add_run(
cmd: '
source ~/.cargo/env
cargo install ${name}
${args.copycmd}
'
)!
}
}

View File

@@ -0,0 +1,52 @@
module docker
@[params]
pub struct CopyArgs {
pub mut:
from string
source string
dest string
make_executable bool // if set will make the file copied executable
}
pub struct CopyItem {
pub mut:
from string
source string
dest string
recipe &DockerBuilderRecipe @[str: skip]
make_executable bool // if set will make the file copied executable
// check_embed bool = true
}
// to do something like: 'Add alpine:latest'
pub fn (mut b DockerBuilderRecipe) add_copy(args CopyArgs) ! {
mut item := CopyItem{
from: args.from
source: args.source
dest: args.dest
make_executable: args.make_executable
recipe: &b
}
if item.source == '' {
return error('source cant be empty, \n${b}')
}
if item.dest == '' {
return error('dest cant be empty, \n${b}')
}
b.items << item
}
pub fn (mut i CopyItem) check() ! {
}
pub fn (mut i CopyItem) render() !string {
mut out := 'COPY ${i.source} ${i.dest}'
if i.from != '' {
out = 'COPY --from=${i.from} ${i.source} ${i.dest}\n'
}
if i.make_executable {
out += '\nRUN chmod +x ${i.dest}\n'
}
return out
}

View File

@@ -0,0 +1,55 @@
module docker
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.osal { download }
@[params]
pub struct DownloadArgs {
pub mut:
name string // unique name which will be used in the cache
url string
reset bool // will remove
minsize_kb u32 = 10 // is always in kb
maxsize_kb u32
hash string // if hash is known, will verify what hash is
dest string // if specified will copy to that destination
timeout int = 180
retry int = 3
}
// checkout a code repository on right location
pub fn (mut r DockerBuilderRecipe) add_download(args_ DownloadArgs) ! {
mut args := args_
if args.dest.len > 0 && args.dest.len < 2 {
return error("dest is to short (min 3): now '${args.dest}'")
}
if args.dest.contains('@name') {
args.dest = args.dest.replace('@name', args.name)
}
if args.url.contains('@name') {
args.url = args.url.replace('@name', args.name)
}
download_dir := '${r.path()}/downloads'
mut p := download(
url: args.url
name: args.name
reset: args.reset
dest: '${download_dir}/${args.name}'
minsize_kb: args.minsize_kb
maxsize_kb: args.maxsize_kb
timeout: args.timeout
retry: args.retry
hash: args.hash
)!
commonpath := pathlib.path_relative(r.path(), p.path)!
if commonpath.contains('..') {
panic('bug should not be')
}
r.add_copy(source: commonpath, dest: args.dest)!
}

View File

@@ -0,0 +1,39 @@
module docker
// import freeflowuniverse.herolib.builder
@[params]
pub struct EntryPointArgs {
pub mut:
cmd string
}
pub struct EntryPointItem {
pub mut:
cmd string
recipe &DockerBuilderRecipe @[str: skip]
}
pub fn (mut b DockerBuilderRecipe) add_entrypoint(args EntryPointArgs) ! {
mut item := EntryPointItem{
cmd: args.cmd
recipe: &b
}
if item.cmd == '' {
return error('cmd cannot be empty, \n${b}')
}
b.items << item
}
pub fn (mut i EntryPointItem) check() ! {
// TODO checks to see if is valid
}
pub fn (mut i EntryPointItem) render() !string {
// todo: need to be able to deal with argement e.g. bash /bin/shell.sh this needs to be 2 elements
mut cmds := i.cmd.fields()
for mut cmd_ in cmds {
cmd_ = "\"${cmd_}\""
}
return 'ENTRYPOINT [${cmds.join(', ')}]'
}

View File

@@ -0,0 +1,29 @@
module docker
pub struct EnvItem {
pub mut:
name string
value string
}
pub fn (mut b DockerBuilderRecipe) add_env(name string, val string) ! {
if name.len < 3 {
return error('min length of name is 3')
}
if val.len < 3 {
return error('min length of val is 3')
}
mut item := EnvItem{
name: name.to_upper()
value: val
}
b.items << item
}
pub fn (mut i EnvItem) check() ! {
// nothing much we can do here I guess
}
pub fn (mut i EnvItem) render() !string {
return "ENV ${i.name}='${i.value}'"
}

View File

@@ -0,0 +1,37 @@
module docker
@[params]
pub struct ExposeARgs {
pub mut:
ports []string
}
pub struct ExposeItem {
pub mut:
ports []string
recipe &DockerBuilderRecipe @[str: skip]
}
// to do something like: 'Expose 8080/udp'
pub fn (mut b DockerBuilderRecipe) add_expose(args ExposeARgs) ! {
mut item := ExposeItem{
ports: args.ports
recipe: &b
}
b.items << item
}
pub fn (mut i ExposeItem) check() ! {
if i.ports.len == 0 {
return error('ports list cannot be empty')
}
}
pub fn (mut i ExposeItem) render() !string {
mut out := 'EXPOSE'
for s in i.ports {
out += ' ${s}'
}
return out
}

View File

@@ -0,0 +1,59 @@
module docker
@[params]
pub struct FromArgs {
pub mut:
image string
tag string
alias string
}
pub struct FromItem {
pub mut:
image string
tag string
recipe &DockerBuilderRecipe @[str: skip]
alias string
}
// to do something like: 'FROM alpine:latest'
pub fn (mut b DockerBuilderRecipe) add_from(args FromArgs) ! {
mut item := FromItem{
image: args.image
tag: args.tag
alias: args.alias
recipe: &b
}
if item.tag == '' {
if b.engine.localonly {
item.tag = 'local'
} else {
item.tag = 'latest'
}
}
mut prefix := b.prefix
if b.engine.prefix.len > 0 {
prefix = b.engine.prefix
}
if prefix.len > 0 && !(prefix.ends_with('/')) {
return error("cannot use prefix if it doesn't end with /, was '${prefix}'")
}
item.image = '${prefix}${item.image}'
if item.image == '' {
return error('image name cannot be empty')
}
b.items << item
}
pub fn (mut i FromItem) check() ! {
// TODO checks to see if is valid
}
pub fn (mut i FromItem) render() !string {
if i.alias == '' {
return 'FROM ${i.image}:${i.tag}'
} else {
return 'FROM ${i.image}:${i.tag} AS ${i.alias}'
}
}

View File

@@ -0,0 +1,109 @@
module docker
import freeflowuniverse.herolib.ui.console
@[params]
pub struct PackageArgs {
pub mut:
name string
names []string
}
pub struct PackageItem {
pub mut:
names []string
recipe &DockerBuilderRecipe @[str: skip]
platform PlatformType
}
// add one of more name (alpine packages), no need to do update, upgrade first,
pub fn (mut b DockerBuilderRecipe) add_package(args PackageArgs) ! {
mut package := PackageItem{
recipe: &b
names: args.names
platform: b.platform
}
if args.name == '' && args.names == [] {
return error('name or names cannot be empty, name can be comma separated')
}
if args.name.contains(',') {
for item2 in args.name.split(',') {
package.names << item2.trim_space()
}
} else {
if args.name != '' {
package.names << args.name.trim_space()
}
}
b.check_from_statement()!
// now check if we after each from find an apk upgrade
mut updatedone := false
for item3 in b.items {
if item3 is FromItem {
updatedone = false
}
if item3 is RunItem {
if item3.cmd.contains('apk upgrade') {
updatedone = true
}
}
}
if updatedone == false {
if b.platform == .alpine {
// means we first need to do an update
b.add_run(
cmd: '
#rm -rf /tmp/*
#rm -rf /var/cache/name/*
apk update
apk upgrade
'
) or { return error('Failed to add run') }
} else if b.platform == .ubuntu {
b.add_run(
cmd: '
apt-get update
apt-get install -y apt-transport-https'
)!
} else {
panic('implement for ubuntu')
}
}
// lets now check of the package has already not been set before
for package0 in b.items {
if package0 is PackageItem {
for packagename in package0.names {
for packagenamecompare in package.names {
if packagenamecompare == packagename {
// we found a double
return error('Cannot add the package again, there is a double. ${packagename} \n${b}')
}
}
}
}
}
// console.print_debug(package)
if package.names.len == 0 {
return error('could not find package names.\n ${b}\nARGS:\n${args}')
}
b.items << package
}
pub fn (mut i PackageItem) check() ! {
// maybe over time we can in redis hold a list of name possible names, so we can check at compile time if its going to work
}
pub fn (mut i PackageItem) render() !string {
mut names := ''
for name in i.names {
names += ' ${name} '
}
mut pkg_manager := 'apk add --no-cache'
if i.platform == .ubuntu {
pkg_manager = 'apt-get -y install'
}
return 'RUN ${pkg_manager} ${names}'
}

View File

@@ -0,0 +1,58 @@
module docker
import freeflowuniverse.herolib.core.texttools
@[params]
pub struct RunArgs {
pub mut:
cmd string
}
pub struct RunItem {
pub mut:
cmd string
recipe &DockerBuilderRecipe @[str: skip]
}
// to do something like: 'FROM alpine:latest'
pub fn (mut b DockerBuilderRecipe) add_run(args RunArgs) ! {
mut item := RunItem{
cmd: args.cmd
recipe: &b
}
if item.cmd == '' {
return error('cmd name cannot be empty')
}
b.items << item
}
pub fn (mut i RunItem) check() ! {
// nothing much we can do here I guess
}
pub fn (mut i RunItem) render() !string {
mut out := []string{}
mut first := true
i.cmd = texttools.dedent(i.cmd)
for mut line in i.cmd.split_into_lines() {
line = line.replace('\t', ' ')
if line == '' {
continue
}
if line.contains('&&') {
return error("don't use && in scripts")
}
if line.trim_space().starts_with('#') {
continue
}
if first {
out << 'RUN ${line} \\'
first = false
} else {
out << ' && ${line} \\'
}
}
mut out2 := out.join_lines()
out2 = out2.trim_string_right('\n').trim_string_right('\\')
return out2
}

View File

@@ -0,0 +1,91 @@
module docker
// import freeflowuniverse.herolib.develop.gittools
// import freeflowuniverse.herolib.core.pathlib
pub fn (mut r DockerBuilderRecipe) add_zinit() ! {
mut pkg_manager := 'apk add'
if r.platform == .ubuntu {
pkg_manager = 'apt install'
}
r.add_run(
cmd: '
${pkg_manager} wget
wget https://github.com/threefoldtech/zinit/releases/download/v0.2.5/zinit -O /sbin/zinit
chmod +x /sbin/zinit
touch /etc/environment
mkdir -p /etc/zinit/
'
)!
r.add_entrypoint(cmd: '/sbin/zinit init --container')!
}
@[params]
pub struct ExecuteArgs {
pub mut:
source string // is the filename, needs to be embedded
debug bool
}
// execute the file as embedded
pub fn (mut r DockerBuilderRecipe) execute(args ExecuteArgs) ! {
if args.source == '' {
return error('source cant be empty, \n ${r}')
}
path := args.source
r.add_file_embedded(source: path, dest: '/tmp/${path}', make_executable: true)!
if !args.debug {
r.add_run(cmd: '/tmp/${path}')!
}
}
pub fn (mut r DockerBuilderRecipe) add_nodejsbuilder() ! {
r.add_package(name: 'nodejs, npm')!
}
pub fn (mut r DockerBuilderRecipe) add_vbuilder() ! {
r.add_package(name: 'git, musl-dev, clang, gcc, openssh-client, make')!
r.add_run(
cmd: "
git clone --depth 1 https://github.com/vlang/v /opt/vlang
cd /opt/vlang
make VFLAGS='-cc gcc'
./v -version
./v symlink
"
)!
r.add_workdir(workdir: '/opt/vlang')!
}
// add ssh server and init scripts (note: zinit needs to be installed)
pub fn (mut r DockerBuilderRecipe) add_sshserver() ! {
r.add_package(name: 'openssh-server')!
r.add_zinit_cmd(
name: 'sshd-setup'
oneshot: true
exec: "
mkdir -p /run/sshd
ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa
ssh-keygen -f /etc/ssh/ssh_host_dsa_key -N '' -t dsa
ssh-keygen -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa -b 521
ssh-keygen -f /etc/ssh/ssh_host_ed25519_key -N '' -t ed25519
"
)!
r.add_zinit_cmd(
name: 'ssh-keys'
after: 'sshd-setup'
exec: '
if [ ! -d /root/.ssh ]; then
mkdir -m 700 /root/.ssh
fi
echo \$SSH_KEY >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
'
)!
r.add_zinit_cmd(name: 'sshd', exec: '/usr/sbin/sshd -D -e', after: 'sshd-setup')!
}

View File

@@ -0,0 +1,37 @@
module docker
@[params]
pub struct VolumeArgs {
pub mut:
mount_points []string
}
pub struct VolumeItem {
pub mut:
mount_points []string
recipe &DockerBuilderRecipe @[str: skip]
}
// to do something like: 'Volume /data'
pub fn (mut b DockerBuilderRecipe) add_volume(args VolumeArgs) ! {
mut item := VolumeItem{
mount_points: args.mount_points
recipe: &b
}
b.items << item
}
pub fn (mut i VolumeItem) check() ! {
if i.mount_points.len == 0 {
return error('mount points list cannot be empty')
}
}
pub fn (mut i VolumeItem) render() !string {
mut out := 'VOLUME'
for s in i.mount_points {
out += ' ${s}'
}
return out
}

View File

@@ -0,0 +1,30 @@
module docker
@[params]
pub struct WorkDirArgs {
pub mut:
workdir string
}
pub struct WorkDirItem {
pub mut:
workdir string
recipe &DockerBuilderRecipe @[str: skip]
}
// to do something like: 'FROM alpine:latest'
pub fn (mut b DockerBuilderRecipe) add_workdir(args WorkDirArgs) ! {
mut item := WorkDirItem{
recipe: &b
workdir: args.workdir
}
b.items << item
}
pub fn (mut i WorkDirItem) check() ! {
// nothing much we can do here I guess
}
pub fn (mut i WorkDirItem) render() !string {
return 'WORKDIR ${i.workdir}'
}

View File

@@ -0,0 +1,35 @@
module docker
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.core.pathlib
@[params]
pub struct WriteFileArgs {
pub mut:
name string // such a file needs to have a name
content string
dest string // where the content will be saved
make_executable bool // if true then will be made executable
}
// the content will be written to dest
// all trailing spaces will be removed (dedent)
pub fn (mut r DockerBuilderRecipe) write_file(args WriteFileArgs) ! {
if args.name == '' {
return error('name cant be empty, \n ${r}')
}
if args.content == '' {
return error('content cant be empty, \n ${r}')
}
if args.dest == '' {
return error('dest cant be empty, \n ${r}')
}
mut ff := pathlib.get_file(path: r.path() + '/snippets/${args.name}', create: true)!
content := texttools.dedent(args.content)
ff.write(content)!
r.add_copy(
source: 'snippets/${args.name}'
dest: args.dest
make_executable: args.make_executable
)!
}

View File

@@ -0,0 +1,130 @@
module docker
import freeflowuniverse.herolib.core.pathlib
import freeflowuniverse.herolib.core.texttools
pub struct ZinitItem {
pub mut:
name string
exec string // e.g. redis-server --port 7777
test string // e.g. redis-cli -p 7777 PING
after []string // e.g. copy,tmuxp these are the names of the ones we are depending on
log ZinitLogType
signal string // could be 'SIGKILL' as well, if not specified then 'SIGTERM'
oneshot bool // is the command supposed to stop and only be used one, so not as daemon
}
enum ZinitLogType {
ring
stdout
null
}
@[params]
pub struct ZinitAddArgs {
pub mut:
// more info see https://github.com/threefoldtech/zinit/tree/master/docs
name string
exec string // e.g. redis-server --port 7777
test string // e.g. redis-cli -p 7777 PING
after string // e.g. 'copy,tmuxp' these are the names of the ones we are depending on, is comma separated
log ZinitLogType
signal string // could be 'SIGKILL' as well, if not specified then 'SIGTERM'
oneshot bool // is the command supposed to stop and only be used one, so not as daemon
}
// add a zinit to the docker container
// each init will launch a process in the container
// the init files are rendered as yaml files in the build directory and added to the docker at the end
// how to use zinit see: https://github.com/threefoldtech/zinit/tree/master/docs
pub fn (mut b DockerBuilderRecipe) add_zinit_cmd(args ZinitAddArgs) ! {
if args.name.len < 3 {
return error('min length of name is 3')
}
if args.exec.len < 3 {
return error('min length of val is 3')
}
// TODO: make sure after is lowercase and trimmed
mut item := ZinitItem{
name: args.name
exec: args.exec
test: args.test
log: args.log
signal: args.signal
oneshot: args.oneshot
}
mut afterargs := []string{}
if args.after.len > 0 {
for afteritem in args.after.split(',') {
afterargs << afteritem.to_lower().trim_space()
}
item.after = afterargs
}
b.items << item
mut zinitfilecontent := ''
if args.exec.contains('\n') {
// we now suppose this file is a bash file
mut content := texttools.dedent(args.exec)
if !content.trim_space().starts_with('set ') {
content = 'set -ex\n\n${content}'
}
b.write_file(
dest: '/cmds/${args.name}.sh'
content: content
make_executable: true
name: args.name + '.sh'
)!
zinitfilecontent += 'exec: /bin/bash /cmds/${args.name}.sh\n'
} else {
zinitfilecontent += 'exec: ${args.exec}\n'
}
if args.test.len > 0 {
zinitfilecontent += 'test: ${args.test}\n'
}
if args.after.len > 0 {
zinitfilecontent += 'after:\n'
for a in afterargs {
zinitfilecontent += ' - ${a}\n'
}
}
if args.signal.len > 0 {
zinitfilecontent += 'signal:\n'
zinitfilecontent += ' stop: ${args.signal.to_upper()} \n'
}
if args.log == .stdout {
zinitfilecontent += 'log: stdout\n'
} else if args.log == .ring {
zinitfilecontent += 'log: ring\n'
} else if args.log == .null {
zinitfilecontent += 'log: null\n'
} else {
panic('other log')
}
if args.oneshot {
zinitfilecontent += 'oneshot: true\n'
}
mut ff := pathlib.get_file(path: b.path() + '/zinit/${args.name}.yaml', create: true)!
ff.write(zinitfilecontent)!
}
pub fn (mut i ZinitItem) check() ! {
// nothing much we can do here I guess
}
pub fn (mut i ZinitItem) render() !string {
// nothing to render only in final step
return ''
}
// example file format
// exec: redis-server --port 7777
// test: redis-cli -p 7777 PING
// after:
// - copy
// - tmuxp

View File

@@ -0,0 +1,113 @@
module docker
import freeflowuniverse.herolib.crypt.openssl
import freeflowuniverse.herolib.clients.httpconnection
import freeflowuniverse.herolib.osal { exec }
import os
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct DockerRegistry {
pub mut:
name string = 'default'
datapath string
ssl bool
}
@[params]
pub struct DockerRegistryArgs {
pub mut:
name string = 'default' @[required]
datapath string
ssl bool
reset bool // if reset will reset existing running one
reset_ssl bool // if reset will reset the sslkey
secret string = '1234' @[required]
}
// registry:
// restart: always
// image: registry:2
// ports:
// - 5000:5000
// environment:
// REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt
// REGISTRY_HTTP_TLS_KEY: /certs/domain.key
// REGISTRY_AUTH: htpasswd
// REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd
// REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
// volumes:
// - ${registry.datapath}/data:/var/lib/registry
// - ${registry.datapath}/certs:/certs
// - ${registry.datapath}/auth:/auth
// check docker has been installed & enabled on node
pub fn (mut e DockerEngine) registry_add(args DockerRegistryArgs) ! {
mut registry := DockerRegistry{
name: args.name
datapath: args.datapath
ssl: args.ssl
}
if registry.datapath.len < 4 {
return error('datapath needs to be len +3')
}
mut composer := e.compose_new(name: 'docker_registry')
mut service := composer.service_new(name: 'registry', image: 'registry:2')!
service.restart_set()
service.port_expose(5000, 5000)!
if registry.ssl {
service.env_add('REGISTRY_HTTP_TLS_CERTIFICATE', '/certs/domain.crt')
service.env_add('REGISTRY_HTTP_TLS_KEY', ' /certs/domain.key')
service.env_add('REGISTRY_AUTH', 'htpasswd')
service.env_add('REGISTRY_AUTH_HTPASSWD_PATH', '/auth/htpasswd')
service.env_add('REGISTRY_AUTH_HTPASSWD_REALM', 'Registry Realm')
service.env_add('REGISTRY_LOGLEVEL', 'debug')
service.env_add('REGISTRY_HTTP_SECRET', args.secret)
service.volume_add('${registry.datapath}/data', '/var/lib/registry')!
service.volume_add('${registry.datapath}/certs', '/certs')!
service.volume_add('${registry.datapath}/auth', '/auth')!
p1 := '${registry.datapath}/certs/domain.crt'
p2 := '${registry.datapath}/certs/domain.key'
if !os.exists(p1) || !os.exists(p2) || args.reset_ssl {
// means we are missing a key
mut ossl := openssl.new()!
k := ossl.get(name: 'docker_registry')!
os.mkdir_all('${registry.datapath}/certs')!
os.cp(k.path_cert.path, p1)!
os.cp(k.path_key.path, p2)!
}
}
e.registries << registry
// delete all previous containers, uses wildcards see https://modules.vlang.io/index.html#string.match_glob
e.container_delete(name: 'docker_registry*')!
composer.start()!
exec(cmd: 'curl https://localhost:5000/v2/ -k', retry: 4) or {
return error('could not start docker registry, did not answer')
}
mut conn := httpconnection.new(
name: 'localdockerhub'
url: 'https://localhost:5000/v2/'
retry: 10
)!
// r := conn.get_json_dict(mut prefix: 'errors')!
// r := conn.get_json_dict(mut prefix: 'errors')!
r := conn.get(method: .get)!
console.print_debug('Sdsd')
console.print_debug(r)
if true {
panic('sdsd')
}
// now we need to check if we can connect
}

View File

@@ -0,0 +1,3 @@
module docker
// https://docs.docker.com/engine/api/v1.41/#

View File

@@ -0,0 +1,69 @@
module docker
import freeflowuniverse.herolib.ui.console
fn test_docker1() {
// mut engine := engine_local([]) or { panic(err) }
// engine.reset_all()!
// create an ssh enabled alpine container
// push to threefold docker hub
// have a default sshkey in, which is known to our docker classes here
// the following NEW method gets this container with the default SSH key understood in node
// mut container := engine.container_get('test_container') or {
// panic('Cannot get test container')
// }
// container.start()!
// mut res := node.exec('ls /')!
// do some assert test
// check assert that there is 1 container in engine.containers_list()
// console.print_debug(container)
// NOW DO SOME MORE TESTS,
// engine.node = builder.node_new(name: 'test')
// console.print_debug(engine.images_list() or { []&DockerImage{} })
// panic('A')
// mut containers := engine.containers_list()
// mut container := containers[0]
// console.print_debug(container)
// container.start()
// mut engine2 := DockerEngine<ExecutorLocal>{}
// engine2.executor.name = "aaa"
// console.print_debug(engine2.images_list())
// mut engine := get(Executor(ExecutorSSH{}))
// console.print_debug(engine)
// console.print_debug(engine.images_list())
// mut engine2 := get(ExecutorLocal{})
// console.print_debug(engine2.images_list())
}
// fn test_remote_docker() {
// node := builder.Node{
// name: "remote digitalocean",
// platform: builder.PlatformType.ubuntu,
// executor: builder.ExecutorSSH{
// sshkey: "~/.ssh/id_rsa_test",
// user: "root",
// ipaddr: builder.IPAddress{
// addr: "104.236.53.191",
// port: builder.Port{
// number: 22,
// cat: builder.PortType.tcp
// },
// cat: builder.IpAddressType.ipv4
// }
// }
// }
// engine.node = node
// // console.print_debug(engine.images_list())
// mut containers := engine.containers_list()
// console.print_debug(containers)
// mut container := containers[0]
// console.print_debug(container)
// container.start() or {panic(err)}
// }

224
lib/virt/docker/readme.md Normal file
View File

@@ -0,0 +1,224 @@
# Docker Module Documentation
The Docker module provides a comprehensive set of tools for working with Docker containers, images, and compose files in V. It offers high-level abstractions for common Docker operations while maintaining flexibility for advanced use cases.
## Core Components
### DockerEngine
The main entry point for Docker operations. It manages:
- Docker images and containers
- Build operations
- Registry interactions
- Platform-specific settings
```v
// Create a new Docker engine
mut engine := docker.new(
name: 'myengine'
localonly: true
)!
```
### DockerContainer
Represents a Docker container with operations for:
- Starting/stopping containers
- Shell access
- Port forwarding
- Volume mounting
- Container state management
```v
// Create and start a container
mut container := engine.container_create(
name: 'mycontainer'
image_repo: 'ubuntu'
image_tag: 'latest'
)!
container.start()!
```
### DockerImage
Handles Docker image operations:
- Image loading/saving
- Deletion
- Export/import
- Image metadata
```v
// Get an image by name
mut image := engine.image_get(
repo: 'ubuntu'
tag: 'latest'
)!
```
### DockerCompose
Manages Docker Compose operations:
- Service definitions
- Multi-container deployments
- Network configuration
- Volume management
```v
// Create a new compose configuration
mut composer := engine.compose_new(
name: 'myapp'
)!
// Add a service
mut service := composer.service_new(C
name: 'web'
image: 'nginx:latest'
)!
// Configure service
service.port_expose(80, 8080)!
service.env_add('NGINX_HOST', 'localhost')
service.restart_set()
// Start the compose stack
composer.start()!
```
### DockerRegistry
Provides functionality for:
- Registry setup and management
- Image pushing/pulling
- Authentication
- SSL configuration
## Recipe System
The module includes a powerful recipe system for building Docker images:
### DockerBuilderRecipe
Allows declarative definition of Docker images with:
- Multi-stage builds
- Package installation
- File operations
- Environment configuration
- Service initialization
```v
// Create a new recipe
mut recipe := engine.recipe_new(RecipeArgs{
name: 'myapp'
platform: .ubuntu
})!
// Add build steps
recipe.add_package(name: 'nginx,curl')!
recipe.add_copy(source: 'app', dest: '/app')!
recipe.add_env('APP_ENV', 'production')!
recipe.add_expose(ports: ['80/tcp'])!
// Build the image
recipe.build(false)!
```
### Recipe Components
The recipe system supports various Dockerfile instructions through specialized components:
- **FromItem**: Base image specification
- **RunItem**: Command execution
- **CmdItem**: Container entry point
- **EnvItem**: Environment variables
- **WorkDirItem**: Working directory
- **CopyItem**: File copying
- **ExposeItem**: Port exposure
- **VolumeItem**: Volume mounting
- **ZinitItem**: Process management
## Examples
### Basic Container Management
```v
// Create and manage a container
mut engine := docker.new(DockerEngineArgs{name: 'default'})!
mut container := engine.container_create(
name: 'webserver'
image_repo: 'nginx'
forwarded_ports: ['80:8080/tcp']
)!
container.start()!
```
### Custom Image Build
```v
mut recipe := engine.recipe_new(
name: 'custom-nginx'
platform: .ubuntu
)!
recipe.add_package(name: 'nginx')!
recipe.add_file_embedded(
source: 'nginx.conf'
dest: '/etc/nginx/nginx.conf'
)!
recipe.add_expose(ports: ['80/tcp'])!
recipe.build(false)!
```
### Multi-Container Setup
```v
mut composer := engine.compose_new(name: 'webapp')!
// Web service
mut web := composer.service_new
name: 'web'
image: 'nginx'
)!
web.port_expose(80, 8080)!
// Database service
mut db := composer.service_new(
name: 'db'
image: 'postgres'
)!
db.env_add('POSTGRES_PASSWORD', 'secret')
composer.start()!
```
## Error Handling
The module provides specific error types for common scenarios:
- **ImageGetError**: Image retrieval issues
- **ContainerGetError**: Container access problems
Always handle these errors appropriately in your code:
```v
engine.image_get(
repo: 'nonexistent'
) or {
if err is ImageGetError {
if err.notfound {
// Handle missing image
}
}
return err
}
## Builders
see https://github.com/threefoldtech/vbuilders/tree/development/builders for examples
How to get started see
https://github.com/threefoldtech/vbuilders/blob/development/docsrc/src/gettingstarted/ubuntu/ubuntu.md

View File

@@ -0,0 +1,242 @@
module herocontainers
import freeflowuniverse.herolib.osal
// import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.installers.lang.herolib
import freeflowuniverse.herolib.core.pathlib
import os
import json
// is builderah containers
// pub enum ContainerStatus {
// up
// down
// restarting
// paused
// dead
// created
// }
// need to fill in what is relevant
@[heap]
pub struct Builder {
pub mut:
id string
builder bool
imageid string
imagename string
containername string
engine &CEngine @[skip; str: skip]
// hero_in_container bool //once the hero has been installed this is on, does it once per session
// created time.Time
// ssh_enabled bool // if yes make sure ssh is enabled to the container
// ipaddr IPAddress
// forwarded_ports []string
// mounts []ContainerVolume
// ssh_port int // ssh port on node that is used to get ssh
// ports []string
// networks []string
// labels map[string]string @[str: skip]
// image &Image @[str: skip]
// engine &CEngine @[str: skip]
// status ContainerStatus
// memsize int // in MB
// command string
}
@[params]
pub struct RunArgs {
pub mut:
cmd string
// TODO:/..
}
@[params]
pub struct PackageInstallArgs {
pub mut:
names string
// TODO:/..
}
// TODO: mimic osal.package_install('mc,tmux,git,rsync,curl,screen,redis,wget,git-lfs')!
// pub fn (mut self Builder) package_install(args PackageInstallArgs) !{
// //TODO
// names := texttools.to_array(args.names)
// //now check which OS, need to make platform function on container level so we know which platform it is
// panic("implement")
// }
@[params]
pub struct Command {
pub mut:
name string // to give a name to your command, good to see logs...
cmd string
description string
timeout int = 3600 // timeout in sec
stdout bool = true
stdout_log bool = true
raise_error bool = true // if false, will not raise an error but still error report
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
work_folder string // location where cmd will be executed
environment map[string]string // env variables
ignore_error_codes []int
scriptpath string // is the path where the script will be put which is executed
scriptkeep bool // means we don't remove the script
debug bool // if debug will put +ex in the script which is being executed and will make sure script stays
shell bool // means we will execute it in a shell interactive
retry int
interactive bool = true
async bool
runtime osal.RunTime
}
pub enum RunTime {
bash
python
heroscript
herocmd
v
}
pub fn (mut self Builder) run(cmd Command) ! {
mut rt := RunTime.bash
scriptpath := osal.cmd_to_script_path(cmd: cmd.cmd, runtime: cmd.runtime)!
if cmd.runtime == .heroscript || cmd.runtime == .herocmd {
self.hero_copy()!
}
script_basename := os.base(scriptpath)
script_path_in_container := '/tmp/${script_basename}'
self.copy(scriptpath, script_path_in_container)!
// console.print_debug("copy ${scriptpath} into container '${self.containername}'")
cmd_str := 'buildah run ${self.id} ${script_path_in_container}'
// console.print_debug(cmd_str)
if cmd.runtime == .heroscript || cmd.runtime == .herocmd {
self.hero_copy()!
}
osal.exec(
name: cmd.name
cmd: cmd_str
description: cmd.description
timeout: cmd.timeout
stdout: cmd.stdout
stdout_log: cmd.stdout_log
raise_error: cmd.raise_error
ignore_error: cmd.ignore_error
ignore_error_codes: cmd.ignore_error_codes
scriptpath: cmd.scriptpath
scriptkeep: cmd.scriptkeep
debug: cmd.debug
shell: cmd.shell
retry: cmd.retry
interactive: cmd.interactive
async: cmd.async
) or {
mut epath := pathlib.get_file(path: scriptpath, create: false)!
c := epath.read()!
return error('cannot execute:\n${c}\nerror:\n${err}')
}
}
pub fn (mut self Builder) copy(src string, dest string) ! {
cmd := 'buildah copy ${self.id} ${src} ${dest}'
osal.exec(cmd: cmd, stdout: false)!
}
// copies the hero from host into guest
pub fn (mut self Builder) hero_copy() ! {
if !osal.cmd_exists('hero') {
herolib.hero_compile()!
}
heropath := osal.cmd_path('hero')!
self.copy(heropath, '/usr/local/bin/hero')!
}
// copies the hero from host into guest and then execute the heroscript or commandline
pub fn (mut self Builder) hero_execute_cmd(cmd string) ! {
self.hero_copy()!
self.run(cmd: cmd, runtime: .herocmd)!
}
pub fn (mut self Builder) hero_execute_script(cmd string) ! {
self.hero_copy()!
self.run(cmd: cmd, runtime: .heroscript)!
}
pub fn (mut self Builder) shell() ! {
cmd := 'buildah run --terminal --env TERM=xterm ${self.id} /bin/bash'
osal.execute_interactive(cmd)!
}
pub fn (mut self Builder) clean() ! {
cmd := '
#set -x
set +e
rm -rf /root/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/share/doc
pacman -Rns $(pacman -Qtdq) --noconfirm
pacman -Scc --noconfirm
rm -rf /var/lib/pacman/sync/*
rm -rf /tmp/*
rm -rf /var/tmp/*
find /var/log -type f -name "*.log" -exec truncate -s 0 {} \\;
rm -rf /home/*/.cache/*
rm -rf /usr/share/doc/*
rm -rf /usr/share/man/*
rm -rf /usr/share/info/*
rm -rf /usr/share/licenses/*
find /usr/share/locale -mindepth 1 -maxdepth 1 ! -name "en*" -exec rm -rf {} \\;
rm -rf /usr/share/i18n
rm -rf /usr/share/icons/*
rm -rf /usr/lib/modules/*
rm -rf /var/cache/pacman
journalctl --vacuum-time=1s
'
self.run(cmd: cmd, stdout: false)!
}
pub fn (mut self Builder) delete() ! {
self.engine.builder_delete(self.containername)!
}
pub fn (mut self Builder) inspect() !BuilderInfo {
cmd := 'buildah inspect ${self.containername}'
out := osal.execute_silent(cmd)!
mut r := json.decode(BuilderInfo, out) or {
return error('Failed to decode JSON for inspect: ${err}')
}
return r
}
// mount the build container to a path and return
pub fn (mut self Builder) mount_to_path() !string {
cmd := 'buildah mount ${self.containername}'
out := osal.execute_silent(cmd)!
return out.trim_space()
}
pub fn (mut self Builder) commit(image_name string) ! {
cmd := 'buildah commit ${self.containername} ${image_name}'
osal.exec(cmd: cmd)!
}
pub fn (self Builder) set_entrypoint(entrypoint string) ! {
cmd := 'buildah config --entrypoint ${entrypoint} ${self.containername}'
osal.exec(cmd: cmd)!
}
pub fn (self Builder) set_workingdir(workdir string) ! {
cmd := 'buildah config --workingdir ${workdir} ${self.containername}'
osal.exec(cmd: cmd)!
}
pub fn (self Builder) set_cmd(command string) ! {
cmd := 'buildah config --cmd ${command} ${self.containername}'
osal.exec(cmd: cmd)!
}

View File

@@ -0,0 +1,113 @@
module herocontainers
struct BuilderInfo {
type_ string @[json: 'Type']
from_image string @[json: 'FromImage']
from_image_id string @[json: 'FromImageID']
from_image_digest string @[json: 'FromImageDigest']
group_add []string @[json: 'GroupAdd']
config string @[json: 'Config']
manifest string @[json: 'Manifest']
container string @[json: 'Container']
container_id string @[json: 'ContainerID']
mount_point string @[json: 'MountPoint']
process_label string @[json: 'ProcessLabel']
mount_label string @[json: 'MountLabel']
image_annotations ImageAnnotations @[json: 'ImageAnnotations']
image_created_by string @[json: 'ImageCreatedBy']
oci_v1 OCIv1 @[json: 'OCIv1']
docker Docker @[json: 'Docker']
default_mounts_file_path string @[json: 'DefaultMountsFilePath']
isolation string @[json: 'Isolation']
namespace_options []NamespaceOption @[json: 'NamespaceOptions']
capabilities []string @[json: 'Capabilities']
configure_network string @[json: 'ConfigureNetwork']
// cni_plugin_path string @[json: 'CNIPluginPath']
// cni_config_dir string @[json: 'CNIConfigDir']
// id_mapping_options IDMappingOptions @[json: 'IDMappingOptions']
history []string @[json: 'History']
devices []string @[json: 'Devices']
}
struct ImageAnnotations {
org_opencontainers_image_base_digest string @[json: 'org.opencontainers.image.base.digest']
org_opencontainers_image_base_name string @[json: 'org.opencontainers.image.base.name']
}
struct OCIv1 {
created string @[json: 'created']
architecture string @[json: 'architecture']
os string @[json: 'os']
config map[string]string @[json: 'config']
rootfs Rootfs @[json: 'rootfs']
}
struct Rootfs {
type_ string @[json: 'type']
diff_ids []string @[json: 'diff_ids']
}
struct Docker {
created string @[json: 'created']
container_config ContainerConfig @[json: 'container_config']
config DockerConfig @[json: 'config']
architecture string @[json: 'architecture']
os string @[json: 'os']
}
struct ContainerConfig {
hostname string @[json: 'Hostname']
domainname string @[json: 'Domainname']
user string @[json: 'User']
attach_stdin bool @[json: 'AttachStdin']
attach_stdout bool @[json: 'AttachStdout']
attach_stderr bool @[json: 'AttachStderr']
tty bool @[json: 'Tty']
open_stdin bool @[json: 'OpenStdin']
stdin_once bool @[json: 'StdinOnce']
env []string @[json: 'Env']
cmd []string @[json: 'Cmd']
image string @[json: 'Image']
volumes map[string]string @[json: 'Volumes']
working_dir string @[json: 'WorkingDir']
entrypoint []string @[json: 'Entrypoint']
on_build []string @[json: 'OnBuild']
labels map[string]string @[json: 'Labels']
}
struct DockerConfig {
// Assuming identical structure to ContainerConfig
// Define fields with @json: mapping if different
}
struct NamespaceOption {
name string @[json: 'Name']
host bool @[json: 'Host']
path string @[json: 'Path']
}
// struct IDMappingOptions {
// host_uid_mapping bool @[json: 'HostUIDMapping']
// host_gid_mapping bool @[json: 'HostGIDMapping']
// // uid_map []UIDMap @[json: 'UIDMap']
// // gid_map []GIDMap @[json: 'GIDMap']
// auto_user_ns bool @[json: 'AutoUserNs']
// auto_user_ns_opts AutoUserNsOpts @[json: 'AutoUserNsOpts']
// }
// struct UIDMap {
// // Define the structure with @json: mappings
// }
// struct GIDMap {
// // Define the structure with @json: mappings
// }
// struct AutoUserNsOpts {
// size int @[json: 'Size']
// initial_size int @[json: 'InitialSize']
// passwd_file string @[json: 'PasswdFile']
// group_file string @[json: 'GroupFile']
// additional_uid_mappings []UIDMap @[json: 'AdditionalUIDMappings']
// additional_gid_mappings []GIDMap @[json: 'AdditionalGIDMappings']
// }

View File

@@ -0,0 +1,81 @@
module herocontainers
import freeflowuniverse.herolib.osal { exec }
@[heap]
pub struct CEngine {
pub mut:
sshkeys_allowed []string // all keys here have access over ssh into the machine, when ssh enabled
images []Image
containers []Container
builders []Builder
buildpath string
localonly bool
cache bool = true
push bool
// platform []BuildPlatformType // used to build
// registries []BAHRegistry // one or more supported BAHRegistries
prefix string
}
pub enum BuildPlatformType {
linux_arm64
linux_amd64
}
fn (mut e CEngine) init() ! {
if e.buildpath == '' {
e.buildpath = '/tmp/builder'
exec(cmd: 'mkdir -p ${e.buildpath}', stdout: false)!
}
e.load()!
}
// reload the state from system
pub fn (mut e CEngine) load() ! {
e.builders_load()!
e.images_load()!
e.containers_load()!
}
// reset all images & containers, CAREFUL!
pub fn (mut e CEngine) reset_all() ! {
e.load()!
for mut container in e.containers.clone() {
container.delete()!
}
for mut image in e.images.clone() {
image.delete(true)!
}
exec(cmd: 'podman rm -a -f', stdout: false)!
exec(cmd: 'podman rmi -a -f', stdout: false)!
e.builders_delete_all()!
osal.done_reset()!
if osal.platform() == .arch {
exec(cmd: 'systemctl status podman.socket', stdout: false)!
}
e.load()!
}
// Get free port
pub fn (mut e CEngine) get_free_port() ?int {
mut used_ports := []int{}
mut range := []int{}
for c in e.containers {
for p in c.forwarded_ports {
used_ports << p.split(':')[0].int()
}
}
for i in 20000 .. 40000 {
if i !in used_ports {
range << i
}
}
// arrays.shuffle<int>(mut range, 0)
if range.len == 0 {
return none
}
return range[0]
}

View File

@@ -0,0 +1,141 @@
module herocontainers
import freeflowuniverse.herolib.osal
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.installers.virt.pacman
import os
@[params]
pub struct GetArgs {
reset bool
}
// builder machine based on arch and install vlang
pub fn (mut e CEngine) builder_base(args GetArgs) !Builder {
name := 'base'
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
console.print_header('buildah base build')
// mut installer:= pacman.get()!
// installer.install()!
mut builder := e.builder_new(name: name, from: 'scratch', delete: true)!
mount_path := builder.mount_to_path()!
osal.exec(
cmd: 'pacstrap -D -c ${mount_path} base screen bash coreutils curl mc unzip sudo which openssh'
)!
// builder.set_entrypoint('redis-server')!
builder.commit('localhost/${name}')!
return builder
}
// builder machine based on arch and install vlang
pub fn (mut e CEngine) builder_go_rust(args GetArgs) !Builder {
console.print_header('buildah builder go rust')
name := 'builder_go_rust'
e.builder_base(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder := e.builder_new(name: name, from: 'localhost/base', delete: true)!
builder.hero_execute_cmd('installers -n golang,rust')!
// builder.clean()!
builder.commit('localhost/${name}')!
e.load()!
return builder
}
pub fn (mut e CEngine) builder_js(args GetArgs) !Builder {
console.print_header('buildah builder js')
name := 'builder_js'
e.builder_base(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder := e.builder_new(name: name, from: 'localhost/base', delete: true)!
builder.hero_execute_cmd('installers -n nodejs')!
// builder.clean()!
builder.commit('localhost/${name}')!
e.load()!
return builder
}
pub fn (mut e CEngine) builder_js_python(args GetArgs) !Builder {
console.print_header('buildah builder js python')
name := 'builder_js_python'
e.builder_js(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder := e.builder_new(name: name, from: 'localhost/builder_js', delete: true)!
builder.hero_execute_cmd('installers -n python')!
// builder.clean()!
builder.commit('localhost/${name}')!
e.load()!
return builder
}
pub fn (mut e CEngine) builder_hero(args GetArgs) !Builder {
console.print_header('buildah builder hero dev')
name := 'builder_hero'
e.builder_js_python(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder := e.builder_new(name: name, from: 'localhost/builder_js_python', delete: true)!
builder.hero_execute_cmd('installers -n hero')!
// builder.clean()!
builder.commit('localhost/${name}')!
e.load()!
return builder
}
pub fn (mut e CEngine) builder_herodev(args GetArgs) !Builder {
console.print_header('buildah builder hero dev')
name := 'builder_herodev'
e.builder_js_python(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder := e.builder_new(name: name, from: 'localhost/builder_hero', delete: true)!
builder.hero_execute_cmd('installers -n herodev')!
// builder.clean()!
builder.commit('localhost/${name}')!
e.load()!
return builder
}
pub fn (mut e CEngine) builder_heroweb(args GetArgs) !Builder {
console.print_header('buildah builder hero web')
name := 'builder_heroweb'
e.builder_go_rust(reset: false)!
e.builder_hero(reset: false)!
if !args.reset && e.builder_exists(name)! {
return e.builder_get(name)!
}
mut builder0 := e.builder_new(
name: 'builder_heroweb_temp'
from: 'localhost/builder_go_rust'
delete: true
)!
builder0.hero_execute_cmd('installers -n heroweb')!
// builder0.hero_execute_cmd("installers -n heroweb")!
mpath := builder0.mount_to_path()!
// copy the built binary to host
osal.exec(
cmd: '
mkdir -p ${os.home_dir()}/hero/var/bin
cp ${mpath}/usr/local/bin/* ${os.home_dir()}/hero/var/bin/
'
)!
builder0.delete()!
mut builder2 := e.builder_new(name: name, from: 'localhost/builder_hero', delete: true)!
builder2.copy('${os.home_dir()}/hero/var/bin/', '/usr/local/bin/')!
builder2.commit('localhost/${name}')!
e.load()!
return builder2
}

View File

@@ -0,0 +1,83 @@
module herocontainers
import freeflowuniverse.herolib.osal
// import freeflowuniverse.herolib.ui.console
import json
fn (mut e CEngine) builders_load() ! {
cmd := 'buildah containers --json'
out := osal.execute_silent(cmd)!
mut r := json.decode([]Builder, out) or { return error('Failed to decode JSON: ${err}') }
for mut item in r {
item.engine = &e
}
e.builders = r
}
@[params]
pub struct BuilderNewArgs {
pub mut:
name string = 'default'
from string = 'docker.io/archlinux:latest'
// arch_scratch bool // means start from scratch with arch linux
delete bool = true
}
pub fn (mut e CEngine) builder_new(args_ BuilderNewArgs) !Builder {
mut args := args_
if args.delete {
e.builder_delete(args.name)!
}
osal.exec(cmd: 'buildah --name ${args.name} from ${args.from}')!
e.builders_load()!
return e.builder_get(args.name)
}
// get buildah containers
pub fn (mut e CEngine) builders_get() ![]Builder {
if e.builders.len == 0 {
e.builders_load()!
}
return e.builders
}
pub fn (mut e CEngine) builder_exists(name string) !bool {
r := e.builders_get()!
res := r.filter(it.containername == name)
if res.len == 1 {
return true
}
if res.len > 1 {
panic('bug')
}
return false
}
pub fn (mut e CEngine) builder_get(name string) !Builder {
r := e.builders_get()!
res := r.filter(it.containername == name)
if res.len == 1 {
return res[0]
}
if res.len > 1 {
panic('bug')
}
return error('couldnt find builder with name ${name}')
}
pub fn (mut e CEngine) builders_delete_all() ! {
osal.execute_stdout('buildah rm -a')!
e.builders_load()!
}
pub fn (mut e CEngine) builder_delete(name string) ! {
if e.builder_exists(name)! {
osal.execute_stdout('buildah rm ${name}')!
e.builders_load()!
}
}
pub fn (mut e CEngine) builder_names() ![]string {
r := e.builders_get()!
return r.map(it.containername)
}

View File

@@ -0,0 +1,163 @@
module herocontainers
import freeflowuniverse.herolib.osal { exec }
// import freeflowuniverse.herolib.data.ipaddress { IPAddress }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.virt.utils
// import freeflowuniverse.herolib.ui.console
// load all containers, they can be consulted in e.containers
// see obj: Container as result in e.containers
fn (mut e CEngine) containers_load() ! {
e.containers = []Container{}
mut ljob := exec(
// we used || because sometimes the command has | in it and this will ruin all subsequent columns
cmd: "podman container list -a --no-trunc --size --format '{{.ID}}||{{.Names}}||{{.ImageID}}||{{.Command}}||{{.CreatedAt}}||{{.Ports}}||{{.State}}||{{.Size}}||{{.Mounts}}||{{.Networks}}||{{.Labels}}'"
ignore_error_codes: [6]
stdout: false
)!
lines := ljob.output.split_into_lines()
for line in lines {
if line.trim_space() == '' {
continue
}
fields := line.split('||').map(utils.clear_str)
if fields.len < 11 {
panic('podman ps needs to output 11 parts.\n${fields}')
}
id := fields[0]
// if image doesn't have id skip this container, maybe ran from filesystme
if fields[2] == '' {
continue
}
mut image := e.image_get(id_full: fields[2])!
mut container := Container{
engine: &e
image: &image
}
container.id = id
container.name = texttools.name_fix(fields[1])
container.command = fields[3]
container.created = utils.parse_time(fields[4])!
container.ports = utils.parse_ports(fields[5])!
container.status = utils.parse_container_state(fields[6])!
container.memsize = utils.parse_size_mb(fields[7])!
container.mounts = utils.parse_mounts(fields[8])!
container.mounts = []
container.networks = utils.parse_networks(fields[9])!
container.labels = utils.parse_labels(fields[10])!
container.ssh_enabled = utils.contains_ssh_port(container.ports)
e.containers << container
}
}
@[params]
pub struct ContainerGetArgs {
pub mut:
name string
id string
image_id string
// tag string
// digest string
}
// get containers from memory
// params:
// name string (can also be a glob e.g. use *,? and [])
// id string
// image_id string
pub fn (mut e CEngine) containers_get(args_ ContainerGetArgs) ![]&Container {
mut args := args_
args.name = texttools.name_fix(args.name)
mut res := []&Container{}
for _, c in e.containers {
if args.name.contains('*') || args.name.contains('?') || args.name.contains('[') {
if c.name.match_glob(args.name) {
res << &c
continue
}
} else {
if c.name == args.name || c.id == args.id {
res << &c
continue
}
}
if args.image_id.len > 0 && c.image.id == args.image_id {
res << &c
}
}
if res.len == 0 {
return ContainerGetError{
args: args
notfound: true
}
}
return res
}
// get container from memory, can use match_glob see https://modules.vlang.io/index.html#string.match_glob
pub fn (mut e CEngine) container_get(args_ ContainerGetArgs) !&Container {
mut args := args_
args.name = texttools.name_fix(args.name)
mut res := e.containers_get(args)!
if res.len > 1 {
return ContainerGetError{
args: args
notfound: true
}
}
return res[0]
}
pub fn (mut e CEngine) container_exists(args ContainerGetArgs) !bool {
e.container_get(args) or {
if err.code() == 1 {
return false
}
return err
}
return true
}
pub fn (mut e CEngine) container_delete(args ContainerGetArgs) ! {
mut c := e.container_get(args)!
c.delete()!
e.load()!
}
// remove one or more container
pub fn (mut e CEngine) containers_delete(args ContainerGetArgs) ! {
mut cs := e.containers_get(args)!
for mut c in cs {
c.delete()!
}
e.load()!
}
pub struct ContainerGetError {
Error
pub:
args ContainerGetArgs
notfound bool
toomany bool
}
pub fn (err ContainerGetError) msg() string {
if err.notfound {
return 'Could not find image with args:\n${err.args}'
}
if err.toomany {
return 'can not get container, Found more than 1 container with args:\n${err.args}'
}
panic('unknown error for ContainerGetError')
}
pub fn (err ContainerGetError) code() int {
if err.notfound {
return 1
}
if err.toomany {
return 2
}
panic('unknown error for ContainerGetError')
}

View File

@@ -0,0 +1,138 @@
module herocontainers
import freeflowuniverse.herolib.virt.utils
import freeflowuniverse.herolib.osal { exec }
import time
import freeflowuniverse.herolib.ui.console
fn (mut e CEngine) images_load() ! {
e.images = []Image{}
mut lines := osal.execute_silent("podman images --format '{{.ID}}||{{.Id}}||{{.Repository}}||{{.Tag}}||{{.Digest}}||{{.Size}}||{{.CreatedAt}}'")!
for line in lines.split_into_lines() {
fields := line.split('||').map(utils.clear_str)
if fields.len != 7 {
panic('podman image needs to output 7 parts.\n${fields}')
}
mut image := Image{
engine: &e
}
image.id = fields[0]
image.id_full = fields[1]
image.repo = fields[2]
image.tag = fields[3]
image.digest = utils.parse_digest(fields[4]) or { '' }
image.size = utils.parse_size_mb(fields[5]) or { 0 }
image.created = utils.parse_time(fields[6]) or { time.now() }
e.images << image
}
}
// import herocontainers image back into the local env
pub fn (mut engine CEngine) image_load(path string) ! {
exec(cmd: 'podman load < ${path}', stdout: false)!
engine.images_load()!
}
@[params]
pub struct ImageGetArgs {
pub:
repo string
tag string
digest string
id string
id_full string
}
// find image based on repo and optional tag
// args:
// repo string
// tag string
// digest string
// id string
// id_full
pub fn (mut e CEngine) image_get(args ImageGetArgs) !Image {
for i in e.images {
if args.digest != '' && i.digest == args.digest {
return i
}
if args.id != '' && i.id == args.id {
return i
}
if args.id_full != '' && i.id_full == args.id_full {
return i
}
}
if args.repo != '' || args.tag != '' {
mut counter := 0
mut result_digest := ''
for i in e.images {
if args.repo != '' && i.repo != args.repo {
continue
}
if args.tag != '' && i.tag != args.tag {
continue
}
console.print_debug('found image for get: ${i} -- ${args}')
result_digest = i.digest
counter += 1
}
if counter > 1 {
return ImageGetError{
args: args
toomany: true
}
}
return e.image_get(digest: result_digest)!
}
return ImageGetError{
args: args
notfound: true
}
}
pub fn (mut e CEngine) image_exists(args ImageGetArgs) !bool {
e.image_get(args) or {
if err.code() == 1 {
return false
}
return err
}
return true
}
// get buildah containers
pub fn (mut e CEngine) images_get() ![]Image {
if e.builders.len == 0 {
e.images_load()!
}
return e.images
}
pub fn (err ImageGetError) msg() string {
if err.notfound {
return 'Could not find image with args:\n${err.args}'
}
if err.toomany {
return 'Found more than 1 image with args:\n${err.args}'
}
panic('unknown error for ImageGetError')
}
pub fn (err ImageGetError) code() int {
if err.notfound {
return 1
}
if err.toomany {
return 2
}
panic('unknown error for ImageGetError')
}
pub struct ImageGetError {
Error
pub:
args ImageGetArgs
notfound bool
toomany bool
}

View File

@@ -0,0 +1,135 @@
module herocontainers
import time
import freeflowuniverse.herolib.osal { exec }
import freeflowuniverse.herolib.data.ipaddress { IPAddress }
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.virt.utils
import freeflowuniverse.herolib.ui.console
@[heap]
pub struct Container {
pub mut:
id string
name string
created time.Time
ssh_enabled bool // if yes make sure ssh is enabled to the container
ipaddr IPAddress
forwarded_ports []string
mounts []utils.ContainerVolume
ssh_port int // ssh port on node that is used to get ssh
ports []string
networks []string
labels map[string]string @[str: skip]
image &Image @[str: skip]
engine &CEngine @[skip; str: skip]
status utils.ContainerStatus
memsize int // in MB
command string
}
@[params]
pub struct ContainerCreateArgs {
name string
hostname string
forwarded_ports []string // ["80:9000/tcp", "1000, 10000/udp"]
mounted_volumes []string // ["/root:/root", ]
env map[string]string // map of environment variables that will be passed to the container
privileged bool
remove_when_done bool = true // remove the container when it shuts down
pub mut:
image_repo string
image_tag string
command string = '/bin/bash'
}
// TODO: implement
// import a container into an image, run podman container with it
// image_repo examples ['myimage', 'myimage:latest']
// if ContainerCreateArgs contains a name, container will be created and restarted
// pub fn (mut e CEngine) container_import(path string, mut args ContainerCreateArgs) !&Container {
// mut image := args.image_repo
// if args.image_tag != '' {
// image = image + ':${args.image_tag}'
// }
// exec(cmd: 'herocontainers import ${path} ${image}', stdout: false)!
// // make sure we start from loaded image
// return e.container_create(args)
// }
// create/start container (first need to get a herocontainerscontainer before we can start)
pub fn (mut container Container) start() ! {
exec(cmd: 'podman start ${container.id}')!
container.status = utils.ContainerStatus.up
}
// delete container
pub fn (mut container Container) halt() ! {
osal.execute_stdout('podman stop ${container.id}') or { '' }
container.status = utils.ContainerStatus.down
}
// delete container
pub fn (mut container Container) delete() ! {
console.print_debug('container delete: ${container.name}')
cmd := 'podman rm ${container.id} -f'
// console.print_debug(cmd)
exec(cmd: cmd, stdout: false)!
}
// save the container to image
pub fn (mut container Container) save2image(image_repo string, image_tag string) !string {
id := osal.execute_stdout('podman commit ${container.id} ${image_repo}:${image_tag}')!
return id
}
// export herocontainers to tgz
pub fn (mut container Container) export(path string) ! {
exec(cmd: 'podman export ${container.id} > ${path}')!
}
// // open ssh shell to the cobtainer
// pub fn (mut container Container) ssh_shell(cmd string) ! {
// container.engine.node.shell(cmd)!
// }
@[params]
pub struct BAHShellArgs {
pub mut:
cmd string
}
// open shell to the container using podman, is interactive, cannot use in script
pub fn (mut container Container) shell(args BAHShellArgs) ! {
mut cmd := ''
if args.cmd.len == 0 {
cmd = 'podman exec -ti ${container.id} /bin/bash'
} else {
cmd = "podman exec -ti ${container.id} /bin/bash -c '${args.cmd}'"
}
exec(cmd: cmd, shell: true, debug: true)!
}
pub fn (mut container Container) execute(cmd_ string, silent bool) ! {
cmd := 'podman exec ${container.id} ${cmd_}'
exec(cmd: cmd, stdout: !silent)!
}
// pub fn (mut container Container) ssh_enable() ! {
// // mut herocontainers_pubkey := pubkey
// // cmd = "podman exec $container.id sh -c 'echo \"$herocontainers_pubkey\" >> ~/.ssh/authorized_keys'"
// // if container.engine.node.executor is builder.ExecutorSSH {
// // mut sshkey := container.engine.node.executor.info()['sshkey'] + '.pub'
// // sshkey = os.read_file(sshkey) or { panic(err) }
// // // add pub sshkey on authorized keys of node and container
// // cmd = "echo \"$sshkey\" >> ~/.ssh/authorized_keys && podman exec $container.id sh -c 'echo \"$herocontainers_pubkey\" >> ~/.ssh/authorized_keys && echo \"$sshkey\" >> ~/.ssh/authorized_keys'"
// // }
// // wait making sure container started correctly
// // time.sleep_ms(100 * time.millisecond)
// // container.engine.node.executor.exec(cmd) !
// }

View File

@@ -0,0 +1,42 @@
module herocontainers
import freeflowuniverse.herolib.installers.virt.podman as podman_installer
import freeflowuniverse.herolib.installers.virt.buildah as buildah_installer
import freeflowuniverse.herolib.installers.lang.herolib
import freeflowuniverse.herolib.osal
@[params]
pub struct NewArgs {
pub mut:
install bool = true
reset bool
herocompile bool
}
pub fn new(args_ NewArgs) !CEngine {
mut args := args_
if !osal.is_linux() {
return error('only linux supported as host for now')
}
if args.install {
mut podman_installer0 := podman_installer.get()!
mut buildah_installer0 := buildah_installer.get()!
podman_installer0.install()!
buildah_installer0.install()!
}
if args.herocompile {
herolib.check()! // will check if install, if not will do
herolib.hero_compile(reset: true)!
}
mut engine := CEngine{}
engine.init()!
if args.reset {
engine.reset_all()!
}
return engine
}

View File

@@ -0,0 +1,35 @@
module herocontainers
import freeflowuniverse.herolib.osal { exec }
import time
import freeflowuniverse.herolib.virt.utils
import freeflowuniverse.herolib.ui.console
// TODO: needs to be implemented for buildah, is still code from docker
@[heap]
pub struct Image {
pub mut:
repo string
id string
id_full string
tag string
digest string
size int // size in MB
created time.Time
engine &CEngine @[skip; str: skip]
}
// delete podman image
pub fn (mut image Image) delete(force bool) ! {
mut forcestr := ''
if force {
forcestr = '-f'
}
exec(cmd: 'podman rmi ${image.id} ${forcestr}', stdout: false)!
}
// export podman image to tar.gz
pub fn (mut image Image) export(path string) !string {
exec(cmd: 'podman save ${image.id} > ${path}', stdout: false)!
return ''
}

View File

@@ -0,0 +1,72 @@
# Herocontainers
Tools to work with containers
```go
#!/usr/bin/env -S v -n -cg -w -enable-globals run
import freeflowuniverse.herolib.virt.herocontainers
import freeflowuniverse.herolib.ui.console
import freeflowuniverse.herolib.builder
//interative means will ask for login/passwd
console.print_header("BUILDAH Demo.")
//if herocompile on, then will forced compile hero, which might be needed in debug mode for hero
// to execute hero scripts inside build container
mut pm:=herocontainers.new(herocompile=true)!
//mut b:=pm.builder_new(name:"test")!
//create
pm.builderv_create()!
//get the container
//mut b2:=pm.builder_get("builderv")!
//b2.shell()!
```
## buildah tricks
```bash
#find the containers as have been build, these are the active ones you can work with
buildah ls
#see the images
buildah images
```
result is something like
```bash
CONTAINER ID BUILDER IMAGE ID IMAGE NAME CONTAINER NAME
a9946633d4e7 * scratch base
86ff0deb00bf * 4feda76296d6 localhost/builder:latest base_go_rust
```
some tricks
```bash
#run interactive in one (here we chose the builderv one)
buildah run --terminal --env TERM=xterm base /bin/bash
#or
buildah run --terminal --env TERM=xterm default /bin/bash
#or
buildah run --terminal --env TERM=xterm base_go_rust /bin/bash
```
to check inside the container about diskusage
```bash
pacman -Su ncdu
ncdu
```
## future
should make this module compatible with https://github.com/containerd/nerdctl

View File

@@ -0,0 +1,8 @@
!!hero_code.generate_client
name:'hetzner'
classname:'HetznerManager'
singleton:0
default:1
hasconfig:1
reset:0

Some files were not shown because too many files have changed in this diff Show More