This commit is contained in:
2024-12-25 09:23:31 +01:00
parent 01ca5897db
commit 4e030b794d
306 changed files with 35071 additions and 22 deletions

View File

@@ -0,0 +1,8 @@
module httpconnection
import encoding.base64
pub fn (mut conn HTTPConnection) basic_auth(username string, password string) {
credentials := base64.encode_str('${username}:${password}')
conn.default_header.add(.authorization, 'Basic ${credentials}')
}

View File

@@ -0,0 +1,100 @@
module httpconnection
import crypto.md5
import json
import net.http { Method }
// https://cassiomolin.com/2016/09/09/which-http-status-codes-are-cacheable/
const default_cacheable_codes = [200, 203, 204, 206, 300, 404, 405, 410, 414, 501]
const unsafe_http_methods = [Method.put, .patch, .post, .delete]
pub struct CacheConfig {
pub mut:
key string // as used to identity in redis
allowable_methods []Method = [.get, .head]
allowable_codes []int = default_cacheable_codes
disable bool = true // default cache is not working
expire_after int = 3600 // default expire_after is 1h
match_headers bool // cache the request header to be matched later
}
pub struct Result {
pub mut:
code int
data string
}
// calculate the key for the cache starting from data and url
fn (mut h HTTPConnection) cache_key(req Request) string {
url := h.url(req).split('!')
encoded_url := md5.hexhash(url[0]) // without params
mut key := 'http:${h.cache.key}:${req.method}:${encoded_url}'
mut req_data := req.data
if h.cache.match_headers {
req_data += json.encode(h.header())
}
req_data += if url.len > 1 { url[1] } else { '' } // add url param if exist
key += if req_data.len > 0 { ':${md5.hexhash(req_data)}' } else { '' }
return key
}
// Get request result from cache, return -1 if missed.
fn (mut h HTTPConnection) cache_get(req Request) !Result {
key := h.cache_key(req)
mut data := h.redis.get(key) or {
assert '${err}' == 'none'
// console.print_debug("cache get: ${key} not in redis")
return Result{
code: -1
}
}
if data == '' {
// console.print_debug("cache get: ${key} empty data")
return Result{
code: -1
}
}
result := json.decode(Result, data) or {
// console.print_debug("cache get: ${key} coud not decode")
return error('failed to decode result with error: ${err}.\ndata:\n${data}')
}
// console.print_debug("cache get: ${key} ok")
return result
}
// Set response result in cache
fn (mut h HTTPConnection) cache_set(req Request, res Result) ! {
key := h.cache_key(req)
value := json.encode(res)
h.redis.set(key, value)!
h.redis.expire(key, h.cache.expire_after)!
}
// Invalidate cache for specific url
fn (mut h HTTPConnection) cache_invalidate(req Request) ! {
url := h.url(req).split('!')
encoded_url := md5.hexhash(url[0])
mut to_drop := []string{}
to_drop << 'http:${h.cache.key}:*:${encoded_url}*'
if req.id.len > 0 {
url_no_id := url[0].trim_string_right('/${req.id}')
encoded_url_no_id := md5.hexhash(url_no_id)
to_drop << 'http:${h.cache.key}:*:${encoded_url_no_id}*'
}
for pattern in to_drop {
all_keys := h.redis.keys(pattern)!
for key in all_keys {
h.redis.del(key)!
}
}
}
// drop full cache for specific cache_key
pub fn (mut h HTTPConnection) cache_drop() ! {
todrop := 'http:${h.cache.key}*'
all_keys := h.redis.keys(todrop)!
for key in all_keys {
h.redis.del(key)!
}
}

View File

@@ -0,0 +1,22 @@
module httpconnection
import net.http { Header }
import freeflowuniverse.herolib.clients.redisclient { Redis }
@[heap]
pub struct HTTPConnection {
pub mut:
redis Redis @[str: skip]
base_url string // the base url
default_header Header
cache CacheConfig
retry int = 5
}
// Join headers from httpconnection and Request
fn (mut h HTTPConnection) header(req Request) Header {
mut header := req.header or { return h.default_header }
return h.default_header.join(header)
}

View File

@@ -0,0 +1,212 @@
// /*
// METHODS NOTES
// * Our target to wrap the default http methods used in V to be cached using redis
// * By default cache enabled in all Request, if you need to disable cache, set req.cache_disable true
// *
// * Flow will be:
// * 1 - Check cache if enabled try to get result from cache
// * 2 - Check result
// * 3 - Do request, if needed
// * 4 - Set in cache if enabled or invalidate cache
// * 5 - Return result
// Suggestion: Send function now enough to do what we want, no need to any post*, get* additional functions
// */
module httpconnection
import x.json2
import net.http
import freeflowuniverse.herolib.core.herojson
import freeflowuniverse.herolib.ui.console
// Build url from Request and httpconnection
fn (mut h HTTPConnection) url(req Request) string {
mut u := '${h.base_url}/${req.prefix.trim('/')}'
if req.id.len > 0 {
u += '/${req.id}'
}
if req.params.len > 0 && req.method != .post {
u += '?${http.url_encode_form_data(req.params)}'
}
return u
}
// Return if request cacheable, depeds on connection cache and request arguments.
fn (h HTTPConnection) is_cacheable(req Request) bool {
return !(h.cache.disable || req.cache_disable) && req.method in h.cache.allowable_methods
}
// Return true if we need to invalidate cache after unsafe method
fn (h HTTPConnection) needs_invalidate(req Request, result_code int) bool {
return !(h.cache.disable || req.cache_disable) && req.method in unsafe_http_methods
&& req.method !in h.cache.allowable_methods && result_code >= 200 && result_code <= 399
}
// Core fucntion to be used in all other function
pub fn (mut h HTTPConnection) send(req_ Request) !Result {
mut result := Result{}
mut response := http.Response{}
mut err_message := ''
mut from_cache := false // used to know if result came from cache
mut req := req_
is_cacheable := h.is_cacheable(req)
// console.print_debug("is cacheable: ${is_cacheable}")
// 1 - Check cache if enabled try to get result from cache
if is_cacheable {
result = h.cache_get(req)!
if result.code != -1 {
from_cache = true
}
}
// 2 - Check result
if result.code in [0, -1] {
// 3 - Do request, if needed
if req.method == .post {
if req.dataformat == .urlencoded && req.data == '' && req.params.len > 0 {
req.data = http.url_encode_form_data(req.params)
}
}
url := h.url(req)
// println("----")
// println(url)
// println(req.data)
// println("----")
mut new_req := http.new_request(req.method, url, req.data)
// joining the header from the HTTPConnection with the one from Request
new_req.header = h.header()
if new_req.header.contains(http.CommonHeader.content_type) {
panic('bug: content_type should not be set as part of default header')
}
match req.dataformat {
.json {
new_req.header.set(http.CommonHeader.content_type, 'application/json')
}
.urlencoded {
new_req.header.set(http.CommonHeader.content_type, 'application/x-www-form-urlencoded')
}
.multipart_form {
new_req.header.set(http.CommonHeader.content_type, 'multipart/form-data')
}
}
println(new_req)
if req.debug {
console.print_debug('http request:\n${new_req.str()}')
}
for _ in 0 .. h.retry {
response = new_req.do() or {
err_message = 'Cannot send request:${req}\nerror:${err}'
// console.print_debug(err_message)
continue
}
break
}
if req.debug {
console.print_debug(response.str())
}
if response.status_code == 0 {
return error(err_message)
}
result.code = response.status_code
result.data = response.body
}
// 4 - Set in cache if enabled
if !from_cache && is_cacheable && result.code in h.cache.allowable_codes {
h.cache_set(req, result)!
}
if h.needs_invalidate(req, result.code) {
h.cache_invalidate(req)!
}
// 5 - Return result
return result
}
pub fn (r Result) is_ok() bool {
return r.code >= 200 && r.code <= 399
}
// dict_key string //if the return is a dict, then will take the element out of the dict with the key and process further
pub fn (mut h HTTPConnection) post_json_str(req_ Request) !string {
mut req := req_
req.method = .post
result := h.send(req)!
if result.is_ok() {
mut data_ := result.data
if req.dict_key.len > 0 {
data_ = herojson.json_dict_get_string(data_, false, req.dict_key)!
}
return data_
}
return error('Could not post ${req}\result:\n${result}')
}
// do a request with certain prefix on the already specified url
// parse as json
pub fn (mut h HTTPConnection) get_json_dict(req Request) !map[string]json2.Any {
data_ := h.get(req)!
mut data := map[string]json2.Any{}
data = herojson.json_dict_filter_any(data_, false, [], [])!
return data
}
// dict_key string //if the return is a dict, then will take the element out of the dict with the key and process further
// list_dict_key string //if the output is a list of dicts, then will process each element of the list to take the val with key out of that dict
// e.g. the input is a list of dicts e.g. [{"key":{"name":"kristof@incubaid.com",...},{"key":...}]
pub fn (mut h HTTPConnection) get_json_list(req Request) ![]string {
mut data_ := h.get(req)!
if req.dict_key.len > 0 {
data_ = herojson.json_dict_get_string(data_, false, req.dict_key)!
}
if req.list_dict_key.len > 0 {
return herojson.json_list_dict_get_string(data_, false, req.list_dict_key)!
}
data := herojson.json_list(data_, false)
return data
}
// dict_key string //if the return is a dict, then will take the element out of the dict with the key and process further
pub fn (mut h HTTPConnection) get_json(req Request) !string {
h.default_header.add(.content_language, 'Content-Type: application/json')
mut data_ := h.get(req)!
if req.dict_key.len > 0 {
data_ = herojson.json_dict_get_string(data_, false, req.dict_key)!
}
return data_
}
// Get Request with json data and return response as string
pub fn (mut h HTTPConnection) get(req_ Request) !string {
mut req := req_
req.debug = true
req.method = .get
result := h.send(req)!
return result.data
}
// Delete Request with json data and return response as string
pub fn (mut h HTTPConnection) delete(req_ Request) !string {
mut req := req_
req.method = .delete
result := h.send(req)!
return result.data
}
// performs a multi part form data request
pub fn (mut h HTTPConnection) post_multi_part(req Request, form http.PostMultipartFormConfig) !http.Response {
mut req_form := form
mut header := h.header()
header.set(http.CommonHeader.content_type, 'multipart/form-data')
req_form.header = header
url := h.url(req)
return http.post_multipart_form(url, req_form)!
}

View File

@@ -0,0 +1,22 @@
module httpconnection
import json
pub fn (mut h HTTPConnection) get_json_generic[T](req Request) !T {
data := h.get_json(req)!
return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
}
pub fn (mut h HTTPConnection) post_json_generic[T](req Request) !T {
data := h.post_json_str(req)!
return json.decode(T, data) or { return error("couldn't decode json for ${req} for ${data}") }
}
pub fn (mut h HTTPConnection) get_json_list_generic[T](req Request) ![]T {
mut r := []T{}
for item in h.get_json_list(req)! {
// println(item)
r << json.decode(T, item) or { return error("couldn't decode json for ${req} for ${item}") }
}
return r
}

View File

@@ -0,0 +1,39 @@
module httpconnection
import net.http
import freeflowuniverse.herolib.clients.redisclient { RedisURL }
@[params]
pub struct HTTPConnectionArgs {
pub:
name string @[required]
url string @[required]
cache bool
retry int = 1
}
pub fn new(args HTTPConnectionArgs) !&HTTPConnection {
// mut f := factory
mut header := http.new_header()
if args.url.replace(' ', '') == '' {
panic("URL is empty, can't create http connection with empty url")
}
// Init connection
mut conn := HTTPConnection{
redis: redisclient.core_get(RedisURL{})!
default_header: header
cache: CacheConfig{
disable: !args.cache
key: args.name
}
retry: args.retry
base_url: args.url.trim('/')
}
return &conn
}

View File

@@ -0,0 +1,171 @@
# HTTPConnection Module
The HTTPConnection module provides a robust HTTP client implementation with support for JSON handling, custom headers, retries, and caching.
## Features
- Generic JSON methods for type-safe requests
- Custom header support
- Built-in retry mechanism
- Cache configuration
- URL encoding support
## Basic Usage
```v
import freeflowuniverse.herolib.clients.httpconnection
// Create a new HTTP connection
mut conn := HTTPConnection{
base_url: 'https://api.example.com'
retry: 5 // number of retries for failed requests
}
```
## Examples
### GET Request with JSON Response
```v
// Define your data structure
struct User {
id int
name string
email string
}
// Make a GET request and decode JSON response
user := conn.get_json_generic[User](
method: .get
prefix: 'users/1'
dataformat: .urlencoded
)!
```
### GET Request for List of Items
```v
// Get a list of items and decode each one
users := conn.get_json_list_generic[User](
method: .get
prefix: 'users'
list_dict_key: 'users' // if response is wrapped in a key
dataformat: .urlencoded
)!
```
### POST Request with JSON Data
```v
// Create new resource with POST
new_user := conn.post_json_generic[User](
method: .post
prefix: 'users'
dataformat: .urlencoded
params: {
'name': 'John Doe'
'email': 'john@example.com'
}
)!
```
### Real-World Example: SSH Key Management
Here's a practical example inspired by SSH key management in a cloud API:
```v
// Define the SSH key structure
struct SSHKey {
pub mut:
name string
fingerprint string
type_ string @[json: 'type']
size int
created_at string
data string
}
// Get all SSH keys
fn get_ssh_keys(mut conn HTTPConnection) ![]SSHKey {
return conn.get_json_list_generic[SSHKey](
method: .get
prefix: 'key'
list_dict_key: 'key'
dataformat: .urlencoded
)!
}
// Create a new SSH key
fn create_ssh_key(mut conn HTTPConnection, name string, key_data string) !SSHKey {
return conn.post_json_generic[SSHKey](
method: .post
prefix: 'key'
dataformat: .urlencoded
params: {
'name': name
'data': key_data
}
)!
}
// Delete an SSH key
fn delete_ssh_key(mut conn HTTPConnection, fingerprint string) ! {
conn.delete(
method: .delete
prefix: 'key/${fingerprint}'
dataformat: .urlencoded
)!
}
```
## Custom Headers
You can set default headers for all requests or specify headers for individual requests:
```v
import net.http { Header }
// Set default headers for all requests
conn.default_header = http.new_header(
key: .authorization
value: 'Bearer your-token-here'
)
// Add custom headers for specific request
response := conn.get_json(
method: .get
prefix: 'protected/resource'
header: http.new_header(
key: .content_type
value: 'application/json'
)
)!
```
## Error Handling
The module uses V's built-in error handling. All methods that can fail return a Result type:
```v
// Handle potential errors
user := conn.get_json_generic[User](
method: .get
prefix: 'users/1'
) or {
println('Error: ${err}')
return
}
```
## Cache Configuration
The module supports caching of responses. Configure caching behavior through the `CacheConfig` struct:
```v
mut conn := HTTPConnection{
base_url: 'https://api.example.com'
cache: CacheConfig{
enabled: true
// Add other cache configuration as needed
}
}

View File

@@ -0,0 +1,25 @@
module httpconnection
import net.http { Header, Method }
pub enum DataFormat {
json // application/json
urlencoded //
multipart_form //
}
@[params]
pub struct Request {
pub mut:
method Method
prefix string
id string
params map[string]string
data string
cache_disable bool // do not put this default on true, this is set on the connection, this is here to be overruled in specific cases
header ?Header
dict_key string // if the return is a dict, then will take the element out of the dict with the key and process further
list_dict_key string // if the output is a list of dicts, then will process each element of the list to take the val with key out of that dict
debug bool
dataformat DataFormat
}

View File

@@ -0,0 +1,7 @@
!!hero_code.generate_client
name:'mailclient'
classname:'MailClient'
singleton:0
default:1
reset:0

View File

@@ -0,0 +1,71 @@
module mailclient
import freeflowuniverse.herolib.core.texttools
import net.smtp
import time
@[params]
pub struct SendArgs {
pub mut:
markdown bool
from string
to string
cc string
bcc string
date time.Time = time.now()
subject string
body_type BodyType
body string
}
pub enum BodyType {
text
html
markdown
}
// ```
// cl.send(markdown:true,subject:'this is a test',to:'kds@something.com,kds2@else.com',body:'
// this is my email content
// ')!
// args:
// markdown bool
// from string
// to string
// cc string
// bcc string
// date time.Time = time.now()
// subject string
// body_type BodyType (.html, .text, .markdown)
// body string
// ```
pub fn (mut cl MailClient) send(args_ SendArgs) ! {
mut args := args_
args.body = texttools.dedent(args.body)
mut body_type := smtp.BodyType.text
if args.body_type == .html || args.body_type == .markdown {
body_type = smtp.BodyType.html
}
mut m := smtp.Mail{
from: args.from
to: args.to
cc: args.cc
bcc: args.bcc
date: args.date
subject: args.subject
body: args.body
body_type: body_type
}
mut smtp_client := smtp.new_client(
server: cl.mail_server
port: cl.mail_port
username: cl.mail_username
password: cl.mail_password
from: cl.mail_from
ssl: cl.ssl
starttls: cl.tls
)!
return smtp_client.send(m)
}

View File

@@ -0,0 +1,107 @@
module mailclient
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.core.playbook
__global (
mailclient_global map[string]&MailClient
mailclient_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'default'
}
fn args_get(args_ ArgsGet) ArgsGet {
mut args := args_
if args.name == '' {
args.name = mailclient_default
}
if args.name == '' {
args.name = 'default'
}
return args
}
pub fn get(args_ ArgsGet) !&MailClient {
mut args := args_get(args_)
if args.name !in mailclient_global {
if !config_exists() {
if default {
config_save()!
}
}
config_load()!
}
return mailclient_global[args.name] or { panic('bug') }
}
// switch instance to be used for mailclient
pub fn switch(name string) {
mailclient_default = name
}
fn config_exists(args_ ArgsGet) bool {
mut args := args_get(args_)
mut context := base.context() or { panic('bug') }
return context.hero_config_exists('mailclient', args.name)
}
fn config_load(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
mut heroscript := context.hero_config_get('mailclient', args.name)!
play(heroscript: heroscript)!
}
fn config_save(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
context.hero_config_set('mailclient', args.name, heroscript_default())!
}
fn set(o MailClient) ! {
mut o2 := obj_init(o)!
mailclient_global['default'] = &o2
}
@[params]
pub struct InstallPlayArgs {
pub mut:
name string = 'default'
heroscript string // if filled in then plbook will be made out of it
plbook ?playbook.PlayBook
reset bool
start bool
stop bool
restart bool
delete bool
configure bool // make sure there is at least one installed
}
pub fn play(args_ InstallPlayArgs) ! {
mut args := args_
println('debguzo1')
mut plbook := args.plbook or {
println('debguzo2')
heroscript := if args.heroscript == '' {
heroscript_default()
} else {
args.heroscript
}
playbook.new(text: heroscript)!
}
mut install_actions := plbook.find(filter: 'mailclient.configure')!
println('debguzo3 ${install_actions}')
if install_actions.len > 0 {
for install_action in install_actions {
mut p := install_action.params
cfg_play(p)!
}
}
}

View File

@@ -0,0 +1,70 @@
module mailclient
import freeflowuniverse.herolib.data.paramsparser
import os
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' }
mail_port := (os.getenv_opt('MAIL_PORT') or { '465' }).int()
mail_server := os.getenv_opt('MAIL_SERVER') or { 'smtp-relay.brevo.com' }
mail_username := os.getenv_opt('MAIL_USERNAME') or { 'kristof@incubaid.com' }
heroscript := "
!!mailclient.configure name:'default'
mail_from: '${mail_from}'
mail_password: '${mail_password}'
mail_port: ${mail_port}
mail_server: '${mail_server}'
mail_username: '${mail_username}'
"
return heroscript
}
pub struct MailClient {
pub mut:
name string = 'default'
mail_from string
mail_password string @[secret]
mail_port int = 465
mail_server string
mail_username string
ssl bool = true
tls bool
}
fn cfg_play(p paramsparser.Params) ! {
mut mycfg := MailClient{
name: p.get_default('name', 'default')!
mail_from: p.get('mail_from')!
mail_password: p.get('mail_password')!
mail_port: p.get_int_default('mail_port', 465)!
mail_server: p.get('mail_server')!
mail_username: p.get('mail_username')!
}
set(mycfg)!
}
fn obj_init(obj_ MailClient) !MailClient {
// never call get here, only thing we can do here is work on object itself
mut obj := obj_
return obj
}
// user needs to us switch to make sure we get the right object
pub fn configure(config MailClient) !MailClient {
client := MailClient{
...config
}
set(client)!
return client
// THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED
// implement if steps need to be done for configuration
}

View File

@@ -0,0 +1,50 @@
# mailclient
To get started
```vlang
import freeflowuniverse.herolib.clients. mailclient
mut client:= mailclient.get()!
client.send(subject:'this is a test',to:'kds@something.com,kds2@else.com',body:'
this is my email content
')!
```
## example heroscript
```hero
!!mailclient.configure
secret: '...'
host: 'localhost'
port: 8888
```
## use of env variables
if you have a secrets file you could import as
```bash
//e.g. source ~/code/git.ourworld.tf/despiegk/hero_secrets/mysecrets.sh
```
following env variables are supported
- MAIL_FROM=
- MAIL_PASSWORD=
- MAIL_PORT=465
- MAIL_SERVER=smtp-relay.brevo.com
- MAIL_USERNAME=kristof@incubaid.com
these variables will only be set at configure time
## brevo remark
- use ssl
- use port: 465

View File

@@ -0,0 +1,7 @@
!!hero_code.generate_client
name:'meilisearch'
classname:'MeilisearchClient'
singleton:0
default:1
reset:0

View File

@@ -0,0 +1,457 @@
module meilisearch
import freeflowuniverse.herolib.clients.httpconnection
import x.json2
import json
// health checks if the server is healthy
pub fn (mut client MeilisearchClient) health() !Health {
req := httpconnection.Request{
prefix: 'health'
}
mut http := client.httpclient()!
response := http.get_json(req)!
return json2.decode[Health](response)
}
// version gets the version of the Meilisearch server
pub fn (mut client MeilisearchClient) version() !Version {
req := httpconnection.Request{
prefix: 'version'
}
mut http := client.httpclient()!
response := http.get_json(req)!
return json2.decode[Version](response)
}
// create_index creates a new index with the given UID
pub fn (mut client MeilisearchClient) create_index(args CreateIndexArgs) !CreateIndexResponse {
req := httpconnection.Request{
prefix: 'indexes'
method: .post
data: json2.encode(args)
}
mut http := client.httpclient()!
response := http.post_json_str(req)!
return json2.decode[CreateIndexResponse](response)
}
// get_index retrieves information about an index
pub fn (mut client MeilisearchClient) get_index(uid string) !GetIndexResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}'
}
mut http := client.httpclient()!
response := http.get_json(req)!
return json2.decode[GetIndexResponse](response)
}
// list_indexes retrieves all indexes
pub fn (mut client MeilisearchClient) list_indexes(args ListIndexArgs) ![]GetIndexResponse {
req := httpconnection.Request{
prefix: 'indexes?limit=${args.limit}&offset=${args.offset}'
}
mut http := client.httpclient()!
response := http.get_json(req)!
list_response := json.decode(ListResponse[GetIndexResponse], response)!
return list_response.results
}
// delete_index deletes an index
pub fn (mut client MeilisearchClient) delete_index(uid string) !DeleteIndexResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}'
}
mut http := client.httpclient()!
response := http.delete(req)!
return json2.decode[DeleteIndexResponse](response)
}
// get_settings retrieves all settings of an index
pub fn (mut client MeilisearchClient) get_settings(uid string) !IndexSettings {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
mut settings := IndexSettings{}
if ranking_rules := response['rankingRules'] {
settings.ranking_rules = ranking_rules.arr().map(it.str())
}
if distinct_attribute := response['distinctAttribute'] {
settings.distinct_attribute = distinct_attribute.str()
}
if searchable_attributes := response['searchableAttributes'] {
settings.searchable_attributes = searchable_attributes.arr().map(it.str())
}
if displayed_attributes := response['displayedAttributes'] {
settings.displayed_attributes = displayed_attributes.arr().map(it.str())
}
if stop_words := response['stopWords'] {
settings.stop_words = stop_words.arr().map(it.str())
}
if filterable_attributes := response['filterableAttributes'] {
settings.filterable_attributes = filterable_attributes.arr().map(it.str())
}
if sortable_attributes := response['sortableAttributes'] {
settings.sortable_attributes = sortable_attributes.arr().map(it.str())
}
return settings
}
// update_settings updates all settings of an index
pub fn (mut client MeilisearchClient) update_settings(uid string, settings IndexSettings) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings'
method: .patch
data: json2.encode(settings)
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_settings resets all settings of an index to default values
pub fn (mut client MeilisearchClient) reset_settings(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_ranking_rules retrieves ranking rules of an index
pub fn (mut client MeilisearchClient) get_ranking_rules(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/ranking-rules'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['rankingRules']!.arr().map(it.str())
}
// update_ranking_rules updates ranking rules of an index
pub fn (mut client MeilisearchClient) update_ranking_rules(uid string, rules []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/ranking-rules'
method: .put
data: json2.encode({
'rankingRules': rules
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_ranking_rules resets ranking rules of an index to default values
pub fn (mut client MeilisearchClient) reset_ranking_rules(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/ranking-rules'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_distinct_attribute retrieves distinct attribute of an index
pub fn (mut client MeilisearchClient) get_distinct_attribute(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/distinct-attribute'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['distinctAttribute']!.str()
}
// update_distinct_attribute updates distinct attribute of an index
pub fn (mut client MeilisearchClient) update_distinct_attribute(uid string, attribute string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/distinct-attribute'
method: .put
data: json2.encode({
'distinctAttribute': attribute
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_distinct_attribute resets distinct attribute of an index
pub fn (mut client MeilisearchClient) reset_distinct_attribute(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/distinct-attribute'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_searchable_attributes retrieves searchable attributes of an index
pub fn (mut client MeilisearchClient) get_searchable_attributes(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/searchable-attributes'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['searchableAttributes']!.arr().map(it.str())
}
// update_searchable_attributes updates searchable attributes of an index
pub fn (mut client MeilisearchClient) update_searchable_attributes(uid string, attributes []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/searchable-attributes'
method: .put
data: json2.encode({
'searchableAttributes': attributes
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_searchable_attributes resets searchable attributes of an index
pub fn (mut client MeilisearchClient) reset_searchable_attributes(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/searchable-attributes'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_displayed_attributes retrieves displayed attributes of an index
pub fn (mut client MeilisearchClient) get_displayed_attributes(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/displayed-attributes'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['displayedAttributes']!.arr().map(it.str())
}
// update_displayed_attributes updates displayed attributes of an index
pub fn (mut client MeilisearchClient) update_displayed_attributes(uid string, attributes []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/displayed-attributes'
method: .put
data: json2.encode({
'displayedAttributes': attributes
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_displayed_attributes resets displayed attributes of an index
pub fn (mut client MeilisearchClient) reset_displayed_attributes(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/displayed-attributes'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_stop_words retrieves stop words of an index
pub fn (mut client MeilisearchClient) get_stop_words(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/stop-words'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['stopWords']!.arr().map(it.str())
}
// update_stop_words updates stop words of an index
pub fn (mut client MeilisearchClient) update_stop_words(uid string, words []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/stop-words'
method: .put
data: json2.encode({
'stopWords': words
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_stop_words resets stop words of an index
pub fn (mut client MeilisearchClient) reset_stop_words(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/stop-words'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_synonyms retrieves synonyms of an index
pub fn (mut client MeilisearchClient) get_synonyms(uid string) !map[string][]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/synonyms'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
mut synonyms := map[string][]string{}
for key, value in response['synonyms']!.as_map() {
synonyms[key] = value.arr().map(it.str())
}
return synonyms
}
// update_synonyms updates synonyms of an index
pub fn (mut client MeilisearchClient) update_synonyms(uid string, synonyms map[string][]string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/synonyms'
method: .put
data: json2.encode({
'synonyms': synonyms
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_synonyms resets synonyms of an index
pub fn (mut client MeilisearchClient) reset_synonyms(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/synonyms'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_filterable_attributes retrieves filterable attributes of an index
pub fn (mut client MeilisearchClient) get_filterable_attributes(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/filterable-attributes'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['filterableAttributes']!.arr().map(it.str())
}
// update_filterable_attributes updates filterable attributes of an index
pub fn (mut client MeilisearchClient) update_filterable_attributes(uid string, attributes []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/filterable-attributes'
method: .put
data: json.encode(attributes)
}
mut http := client.httpclient()!
response := http.send(req)!
return response.data
}
// reset_filterable_attributes resets filterable attributes of an index
pub fn (mut client MeilisearchClient) reset_filterable_attributes(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/filterable-attributes'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_sortable_attributes retrieves sortable attributes of an index
pub fn (mut client MeilisearchClient) get_sortable_attributes(uid string) ![]string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/sortable-attributes'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
return response['sortableAttributes']!.arr().map(it.str())
}
// update_sortable_attributes updates sortable attributes of an index
pub fn (mut client MeilisearchClient) update_sortable_attributes(uid string, attributes []string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/sortable-attributes'
method: .put
data: json2.encode({
'sortableAttributes': attributes
})
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_sortable_attributes resets sortable attributes of an index
pub fn (mut client MeilisearchClient) reset_sortable_attributes(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/sortable-attributes'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
// get_typo_tolerance retrieves typo tolerance settings of an index
pub fn (mut client MeilisearchClient) get_typo_tolerance(uid string) !TypoTolerance {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/typo-tolerance'
}
mut http := client.httpclient()!
response := http.get_json_dict(req)!
min_word_size_for_typos := json2.decode[MinWordSizeForTypos](response['minWordSizeForTypos']!.json_str())!
mut typo_tolerance := TypoTolerance{
enabled: response['enabled']!.bool()
min_word_size_for_typos: min_word_size_for_typos
}
if disable_on_words := response['disableOnWords'] {
typo_tolerance.disable_on_words = disable_on_words.arr().map(it.str())
}
if disable_on_attributes := response['disableOnAttributes'] {
typo_tolerance.disable_on_attributes = disable_on_attributes.arr().map(it.str())
}
return typo_tolerance
}
// update_typo_tolerance updates typo tolerance settings of an index
pub fn (mut client MeilisearchClient) update_typo_tolerance(uid string, typo_tolerance TypoTolerance) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/typo-tolerance'
method: .patch
data: json2.encode(typo_tolerance)
}
mut http := client.httpclient()!
return http.post_json_str(req)
}
// reset_typo_tolerance resets typo tolerance settings of an index
pub fn (mut client MeilisearchClient) reset_typo_tolerance(uid string) !string {
req := httpconnection.Request{
prefix: 'indexes/${uid}/settings/typo-tolerance'
method: .delete
}
mut http := client.httpclient()!
return http.delete(req)
}
@[params]
pub struct EperimentalFeaturesArgs {
pub mut:
vector_store bool @[json: 'vectorStore']
metrics bool @[json: 'metrics']
logs_route bool @[json: 'logsRoute']
contains_filter bool @[json: 'containsFilter']
edit_documents_by_function bool @[json: 'editDocumentsByFunction']
}
pub fn (mut client MeilisearchClient) enable_eperimental_feature(args EperimentalFeaturesArgs) !EperimentalFeaturesArgs {
req := httpconnection.Request{
prefix: 'experimental-features'
method: .patch
data: json.encode(args)
}
mut http := client.httpclient()!
response := http.send(req)!
return json.decode(EperimentalFeaturesArgs, response.data)
}

View File

@@ -0,0 +1,287 @@
module meilisearch
import rand
import time
struct MeiliDocument {
pub mut:
id int
title string
content string
}
// Set up a test client instance
fn setup_client() !&MeilisearchClient {
mut client := get()!
return client
}
fn test_add_document() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
content: 'Shazam is a 2019 American superhero film based on the DC Comics character of the same name.'
title: 'Shazam'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
}
fn test_get_document() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Shazam'
content: 'Shazam is a 2019 American superhero film based on the DC Comics character of the same name.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
doc_ := client.get_document[MeiliDocument](
uid: index_name
document_id: 1
fields: ['id', 'title']
)!
assert doc_.title == 'Shazam'
assert doc_.id == 1
}
fn test_get_documents() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'The Kit kat'
content: 'The kit kat is an Egypton film that was released in 2019.'
},
MeiliDocument{
id: 2
title: 'Elli Bali Balak'
content: 'Elli Bali Balak is an Egyptian film that was released in 2019.'
},
]
q := DocumentsQuery{
fields: ['title', 'id']
}
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
mut docs := client.get_documents[MeiliDocument](index_name, q)!
assert docs.len > 0
assert docs[0].title == 'The Kit kat'
assert docs[0].id == 1
assert docs[1].title == 'Elli Bali Balak'
assert docs[1].id == 2
}
fn test_delete_document() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Shazam'
content: 'Shazam is a 2019 American superhero film based on the DC Comics character of the same name.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
mut doc_ := client.delete_document(
uid: index_name
document_id: 1
)!
assert doc_.index_uid == index_name
assert doc_.type_ == 'documentDeletion'
}
fn test_delete_documents() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Shazam'
content: 'Shazam is a 2019 American superhero film based on the DC Comics character of the same name.'
},
MeiliDocument{
id: 2
title: 'Shazam2'
content: 'Shazam2 is a 2019 American superhero film based on the DC Comics character of the same name.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
mut doc_ := client.delete_all_documents(index_name)!
assert doc_.index_uid == index_name
assert doc_.type_ == 'documentDeletion'
time.sleep(500 * time.millisecond)
q := DocumentsQuery{
fields: ['title', 'id']
}
mut docs := client.get_documents[MeiliDocument](index_name, q)!
assert docs.len == 0
}
fn test_search() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Power of rich people'
content: 'Power of rich people is an American film.'
},
MeiliDocument{
id: 2
title: 'Capten America'
content: 'Capten America is an American film.'
},
MeiliDocument{
id: 3
title: 'Coldplay'
content: 'Coldplay is a british rock band.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
mut doc_ := client.search[MeiliDocument](index_name, q: 'Coldplay')!
assert doc_.hits[0].id == 3
}
fn test_facet_search() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Life'
content: 'Two men in 1930s Mississippi become friends after being sentenced to life in prison together for a crime they did not commit.'
},
MeiliDocument{
id: 2
title: 'Life'
content: 'In 1955, young photographer Dennis Stock develops a close bond with actor James Dean while shooting pictures of the rising Hollywood star.'
},
MeiliDocument{
id: 3
title: 'Coldplay'
content: 'Coldplay is a british rock band.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
res := client.update_filterable_attributes(index_name, ['title'])!
time.sleep(500 * time.millisecond)
settings := client.get_settings(index_name)!
assert ['title'] == settings.filterable_attributes
mut doc_ := client.facet_search(index_name,
facet_name: 'title'
filter: 'title = life'
)!
assert doc_.facet_hits[0].count == 2
}
fn test_similar_documents() {
mut client := setup_client()!
index_name := rand.string(5)
documents := [
MeiliDocument{
id: 1
title: 'Life'
content: 'Two men in 1930s Mississippi become friends after being sentenced to life in prison together for a crime they did not commit.'
},
MeiliDocument{
id: 2
title: 'Life'
content: 'In 1955, young photographer Dennis Stock develops a close bond with actor James Dean while shooting pictures of the rising Hollywood star.'
},
MeiliDocument{
id: 3
title: 'Coldplay'
content: 'Coldplay is a british rock band.'
},
]
mut doc := client.add_documents(index_name, documents)!
assert doc.index_uid == index_name
assert doc.type_ == 'documentAdditionOrUpdate'
time.sleep(500 * time.millisecond)
mut doc_ := client.similar_documents(index_name,
id: 1
)!
// TODO: Check the meilisearch.SimilarDocumentsResponse error
println('doc_: ${doc_}')
// assert doc_.facet_hits[0].count == 2
}
// Delete all created indexes
fn test_delete_index() {
mut client := setup_client()!
mut index_list := client.list_indexes(limit: 100)!
for index in index_list {
client.delete_index(index.uid)!
time.sleep(500 * time.millisecond)
}
index_list = client.list_indexes(limit: 100)!
assert index_list.len == 0
}

View File

@@ -0,0 +1,236 @@
module meilisearch
import freeflowuniverse.herolib.clients.httpconnection
import x.json2
import json
// add_documents adds documents to an index
pub fn (mut client MeilisearchClient) add_documents[T](uid string, documents []T) !AddDocumentResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}/documents'
method: .post
data: json2.encode(documents)
}
mut http := client.httpclient()!
response := http.post_json_str(req)!
return json2.decode[AddDocumentResponse](response)!
}
@[params]
struct GetDocumentArgs {
pub mut:
uid string @[required]
document_id int @[required]
fields []string
retrieve_vectors bool @[json: 'retrieveVectors']
}
// get_document retrieves one document by its id
pub fn (mut client MeilisearchClient) get_document[T](args GetDocumentArgs) !T {
mut params := map[string]string{}
if args.fields.len > 0 {
params['fields'] = args.fields.join(',')
}
params['retrieveVectors'] = args.retrieve_vectors.str()
req := httpconnection.Request{
prefix: 'indexes/${args.uid}/documents/${args.document_id}'
params: params
}
mut http := client.httpclient()!
response := http.get_json(req)!
return json.decode(T, response)
}
// get_documents retrieves documents with optional parameters
pub fn (mut client MeilisearchClient) get_documents[T](uid string, query DocumentsQuery) ![]T {
mut params := map[string]string{}
params['limit'] = query.limit.str()
params['offset'] = query.offset.str()
if query.fields.len > 0 {
params['fields'] = query.fields.join(',')
}
if query.filter.len > 0 {
params['filter'] = query.filter
}
if query.sort.len > 0 {
params['sort'] = query.sort.join(',')
}
req := httpconnection.Request{
prefix: 'indexes/${uid}/documents'
params: params
}
mut http := client.httpclient()!
response := http.get_json(req)!
decoded := json.decode(ListResponse[T], response)!
return decoded.results
}
@[params]
struct DeleteDocumentArgs {
pub mut:
uid string @[required]
document_id int @[required]
}
// delete_document deletes one document by its id
pub fn (mut client MeilisearchClient) delete_document(args DeleteDocumentArgs) !DeleteDocumentResponse {
req := httpconnection.Request{
prefix: 'indexes/${args.uid}/documents/${args.document_id}'
method: .delete
}
mut http := client.httpclient()!
response := http.delete(req)!
return json2.decode[DeleteDocumentResponse](response)!
}
// delete_all_documents deletes all documents in an index
pub fn (mut client MeilisearchClient) delete_all_documents(uid string) !DeleteDocumentResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}/documents'
method: .delete
}
mut http := client.httpclient()!
response := http.delete(req)!
return json2.decode[DeleteDocumentResponse](response)!
}
// update_documents updates documents in an index
pub fn (mut client MeilisearchClient) update_documents(uid string, documents string) !TaskInfo {
req := httpconnection.Request{
prefix: 'indexes/${uid}/documents'
method: .put
data: documents
}
mut http := client.httpclient()!
response := http.post_json_str(req)!
return json2.decode[TaskInfo](response)!
}
@[params]
struct SearchArgs {
pub mut:
q string @[json: 'q'; required]
offset int @[json: 'offset']
limit int = 20 @[json: 'limit']
hits_per_page int = 1 @[json: 'hitsPerPage']
page int = 1 @[json: 'page']
filter ?string
facets ?[]string
attributes_to_retrieve []string = ['*'] @[json: 'attributesToRetrieve']
attributes_to_crop ?[]string @[json: 'attributesToCrop']
crop_length int = 10 @[json: 'cropLength']
crop_marker string = '...' @[json: 'cropMarker']
attributes_to_highlight ?[]string @[json: 'attributesToHighlight']
highlight_pre_tag string = '<em>' @[json: 'highlightPreTag']
highlight_post_tag string = '</em>' @[json: 'highlightPostTag']
show_matches_position bool @[json: 'showMatchesPosition']
sort ?[]string
matching_strategy string = 'last' @[json: 'matchingStrategy']
show_ranking_score bool @[json: 'showRankingScore']
show_ranking_score_details bool @[json: 'showRankingScoreDetails']
ranking_score_threshold ?f64 @[json: 'rankingScoreThreshold']
attributes_to_search_on []string = ['*'] @[json: 'attributesToSearchOn']
hybrid ?map[string]string
vector ?[]f64
retrieve_vectors bool @[json: 'retrieveVectors']
locales ?[]string
}
// search performs a search query on an index
pub fn (mut client MeilisearchClient) search[T](uid string, args SearchArgs) !SearchResponse[T] {
req := httpconnection.Request{
prefix: 'indexes/${uid}/search'
method: .post
data: json.encode(args)
}
mut http := client.httpclient()!
rsponse := http.post_json_str(req)!
return json.decode(SearchResponse[T], rsponse)
}
@[params]
struct FacetSearchArgs {
facet_name ?string @[json: 'facetName'] // Facet name to search values on
facet_query ?string @[json: 'facetQuery'] // Search query for a given facet value. Defaults to placeholder search if not specified.
q string // Query string
filter ?string // Filter queries by an attribute's value
matching_strategy string = 'last' @[json: 'matchingStrategy'] // Strategy used to match query terms within documents
attributes_to_search_on ?[]string @[json: 'attributesToSearchOn'] // Restrict search to the specified attributes
}
@[params]
struct FacetSearchHitsResponse {
value string @[json: 'value'] // Facet value matching the facetQuery
count int @[json: 'count'] // Number of documents with a facet value matching value
}
@[params]
struct FacetSearchResponse {
facet_hits []FacetSearchHitsResponse @[json: 'facetHits'] // Facet value matching the facetQuery
facet_query string @[json: 'facetQuery'] // The original facetQuery
processing_time_ms int @[json: 'processingTimeMs'] // Processing time of the query
}
pub fn (mut client MeilisearchClient) facet_search(uid string, args FacetSearchArgs) !FacetSearchResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}/facet-search'
method: .post
data: json.encode(args)
}
mut http := client.httpclient()!
rsponse := http.post_json_str(req)!
return json.decode(FacetSearchResponse, rsponse)
}
@[params]
struct SimilarDocumentsArgs {
id SimilarDocumentsID @[json: 'id'] // Identifier of the target document (mandatory)
embedder string = 'default' @[json: 'embedder'] // Embedder to use when computing recommendations
attributes_to_retrieve []string = ['*'] @[json: 'attributesToRetrieve'] // Attributes to display in the returned documents
offset int @[json: 'offset'] // Number of documents to skip
limit int = 20 @[json: 'limit'] // Maximum number of documents returned
filter ?string @[json: 'filter'] // Filter queries by an attribute's value
show_ranking_score bool @[json: 'showRankingScore'] // Display the global ranking score of a document
show_ranking_score_details bool @[json: 'showRankingScoreDetails'] // Display detailed ranking score information
ranking_score_threshold ?f64 @[json: 'rankingScoreThreshold'] // Exclude results with low ranking scores
retrieve_vectors bool @[json: 'retrieveVectors'] // Return document vector data
}
type SimilarDocumentsID = string | int
@[params]
struct SimilarDocumentsResponse {
hits []SimilarDocumentsHit @[json: 'hits'] // List of hit items
id string @[json: 'id'] // Identifier of the response
processing_time_ms int @[json: 'processingTimeMs'] // Processing time in milliseconds
limit int = 20 @[json: 'limit'] // Maximum number of documents returned
offset int @[json: 'offset'] // Number of documents to skip
estimated_total_hits int @[json: 'estimatedTotalHits'] // Estimated total number of hits
}
struct SimilarDocumentsHit {
id SimilarDocumentsID @[json: 'id'] // Identifier of the hit item
title string @[json: 'title'] // Title of the hit item
}
pub fn (mut client MeilisearchClient) similar_documents(uid string, args SimilarDocumentsArgs) !SimilarDocumentsResponse {
req := httpconnection.Request{
prefix: 'indexes/${uid}/similar'
method: .post
data: json.encode(args)
}
res := client.enable_eperimental_feature(vector_store: true)! // Enable the feature first.
mut http := client.httpclient()!
rsponse := http.post_json_str(req)!
println('rsponse: ${rsponse}')
return json.decode(SimilarDocumentsResponse, rsponse)
}

View File

@@ -0,0 +1,86 @@
module meilisearch
import rand
import time
__global (
created_indices []string
)
// Set up a test client instance
fn setup_client() !&MeilisearchClient {
mut client := get()!
return client
}
// Tests the health endpoint for server status
fn test_health() {
mut client := setup_client()!
health := client.health()!
assert health.status == 'available'
}
// Tests the version endpoint to ensure version information is present
fn test_version() {
mut client := setup_client()!
version := client.version()!
assert version.pkg_version.len > 0
assert version.commit_sha.len > 0
assert version.commit_date.len > 0
}
// Tests index creation and verifies if the index UID matches
fn test_create_index() {
index_name := 'test_' + rand.string(4)
mut client := setup_client()!
index := client.create_index(uid: index_name)!
created_indices << index_name
assert index.index_uid == index_name
assert index.type_ == 'indexCreation'
}
// Tests index retrieval and verifies if the retrieved index UID matches
fn test_get_index() {
index_name := 'test_' + rand.string(4)
indes_primary_key := 'id'
mut client := setup_client()!
created_index := client.create_index(uid: index_name, primary_key: indes_primary_key)!
created_indices << index_name
assert created_index.index_uid == index_name
assert created_index.type_ == 'indexCreation'
time.sleep(1 * time.second) // Wait for the index to be created.
retrieved_index := client.get_index(index_name)!
assert retrieved_index.uid == index_name
assert retrieved_index.primary_key == indes_primary_key
}
// Tests listing all indexes to ensure the created index is in the list
fn test_list_indexes() {
mut client := setup_client()!
index_name := 'test_' + rand.string(4)
mut index_list := client.list_indexes()!
assert index_list.len > 0
}
// Tests deletion of an index and confirms it no longer exists
fn test_delete_index() {
mut client := setup_client()!
mut index_list := client.list_indexes(limit: 100)!
for index in index_list {
client.delete_index(index.uid)!
time.sleep(500 * time.millisecond)
}
index_list = client.list_indexes(limit: 100)!
assert index_list.len == 0
created_indices.clear()
assert created_indices.len == 0
}

View File

@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../../../herolib/clients/httpconnection"
}
],
"settings": {}
}

View File

@@ -0,0 +1,104 @@
module meilisearch
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.core.playbook
__global (
meilisearch_global map[string]&MeilisearchClient
meilisearch_default string
)
/////////FACTORY
@[params]
pub struct ArgsGet {
pub mut:
name string = 'default'
}
fn args_get(args_ ArgsGet) ArgsGet {
mut args := args_
if args.name == '' {
args.name = meilisearch_default
}
if args.name == '' {
args.name = 'default'
}
return args
}
pub fn get(args_ ArgsGet) !&MeilisearchClient {
mut args := args_get(args_)
if args.name !in meilisearch_global {
if !config_exists() {
if default {
config_save()!
}
}
config_load()!
}
return meilisearch_global[args.name] or {
println(meilisearch_global)
panic('bug in get from factory: ')
}
}
fn config_exists(args_ ArgsGet) bool {
mut args := args_get(args_)
mut context := base.context() or { panic('bug') }
return context.hero_config_exists('meilisearch', args.name)
}
fn config_load(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
mut heroscript := context.hero_config_get('meilisearch', args.name)!
play(heroscript: heroscript)!
}
fn config_save(args_ ArgsGet) ! {
mut args := args_get(args_)
mut context := base.context()!
context.hero_config_set('meilisearch', args.name, heroscript_default()!)!
}
fn set(o MeilisearchClient) ! {
mut o2 := obj_init(o)!
meilisearch_global['default'] = &o2
}
@[params]
pub struct PlayArgs {
pub mut:
name string = 'default'
heroscript string // if filled in then plbook will be made out of it
plbook ?playbook.PlayBook
reset bool
start bool
stop bool
restart bool
delete bool
configure bool // make sure there is at least one installed
}
pub fn play(args_ PlayArgs) ! {
mut args := args_
if args.heroscript == '' {
args.heroscript = heroscript_default()!
}
mut plbook := args.plbook or { playbook.new(text: args.heroscript)! }
mut install_actions := plbook.find(filter: 'meilisearch.configure')!
if install_actions.len > 0 {
for install_action in install_actions {
mut p := install_action.params
cfg_play(p)!
}
}
}
// switch instance to be used for meilisearch
pub fn switch(name string) {
meilisearch_default = name
}

View File

@@ -0,0 +1,59 @@
module meilisearch
import freeflowuniverse.herolib.data.paramsparser
import freeflowuniverse.herolib.clients.httpconnection
import os
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 {
heroscript := "
!!meilisearch.configure
name:'default'
host:'http://localhost:7700'
api_key:'be61fdce-c5d4-44bc-886b-3a484ff6c531'
"
return heroscript
}
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
pub struct MeilisearchClient {
pub mut:
name string = 'default'
api_key string @[secret]
host string
}
fn cfg_play(p paramsparser.Params) ! {
// THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE WITH struct above
mut mycfg := MeilisearchClient{
name: p.get_default('name', 'default')!
host: p.get('host')!
api_key: p.get('api_key')!
}
set(mycfg)!
}
fn obj_init(obj_ MeilisearchClient) !MeilisearchClient {
// never call get here, only thing we can do here is work on object itself
mut obj := obj_
// set the http client
return obj
}
fn (mut self MeilisearchClient) httpclient() !&httpconnection.HTTPConnection {
mut http_conn := httpconnection.new(
name: 'meilisearch'
url: self.host
)!
// Add authentication header if API key is provided
if self.api_key.len > 0 {
http_conn.default_header.add(.authorization, 'Bearer ${self.api_key}')
}
return http_conn
}

View File

@@ -0,0 +1,166 @@
module meilisearch
// ClientConfig holds configuration for MeilisearchClient
pub struct ClientConfig {
pub:
host string // Base URL of Meilisearch server (e.g., "http://localhost:7700")
api_key string // Master key or API key for authentication
timeout int = 30 // Request timeout in seconds
max_retry int = 3 // Maximum number of retries for failed requests
}
// Health represents the health status of the Meilisearch server
pub struct Health {
pub:
status string @[json: 'status']
}
// Version represents version information of the Meilisearch server
pub struct Version {
pub:
pkg_version string @[json: 'pkgVersion']
commit_sha string @[json: 'commitSha']
commit_date string @[json: 'commitDate']
}
// IndexSettings represents all configurable settings for an index
pub struct IndexSettings {
pub mut:
ranking_rules []string @[json: 'rankingRules']
distinct_attribute string @[json: 'distinctAttribute']
searchable_attributes []string @[json: 'searchableAttributes']
displayed_attributes []string @[json: 'displayedAttributes']
stop_words []string @[json: 'stopWords']
synonyms map[string][]string @[json: 'synonyms']
filterable_attributes []string @[json: 'filterableAttributes']
sortable_attributes []string @[json: 'sortableAttributes']
typo_tolerance TypoTolerance @[json: 'typoTolerance']
}
// TypoTolerance settings for controlling typo behavior
pub struct TypoTolerance {
pub mut:
enabled bool = true @[json: 'enabled']
min_word_size_for_typos MinWordSizeForTypos @[json: 'minWordSizeForTypos']
disable_on_words []string @[json: 'disableOnWords']
disable_on_attributes []string @[json: 'disableOnAttributes']
}
// MinWordSizeForTypos controls minimum word sizes for one/two typos
pub struct MinWordSizeForTypos {
pub mut:
one_typo int = 5 @[json: 'oneTypo']
two_typos int = 9 @[json: 'twoTypos']
}
// DocumentsQuery represents query parameters for document operations
pub struct DocumentsQuery {
pub mut:
limit int = 20
offset int
fields []string
filter string
sort []string
}
// TaskInfo represents information about an asynchronous task
pub struct TaskInfo {
pub:
uid int @[json: 'taskUid']
index_uid string @[json: 'indexUid']
status string @[json: 'status']
task_type string @[json: 'type']
details map[string]string @[json: 'details']
error string @[json: 'error']
duration string @[json: 'duration']
enqueued_at string @[json: 'enqueuedAt']
started_at string @[json: 'startedAt']
finished_at string @[json: 'finishedAt']
}
// CreateIndexArgs represents the arguments for creating an index
@[params]
pub struct CreateIndexArgs {
pub mut:
uid string
primary_key string @[json: 'primaryKey']
}
// IndexCreation represents information about the index creation
pub struct CreateIndexResponse {
pub mut:
uid int @[json: 'taskUid']
index_uid string @[json: 'indexUid']
status string @[json: 'status']
type_ string @[json: 'type']
enqueued_at string @[json: 'enqueuedAt']
}
// IndexCreation represents information about the index creation
pub struct GetIndexResponse {
pub mut:
uid string @[json: 'uid']
created_at string @[json: 'createdAt']
updated_at string @[json: 'updatedAt']
primary_key string @[json: 'primaryKey']
}
// ListIndexResponse represents information about the index list
pub struct ListResponse[T] {
pub mut:
results []T
total int
offset int
limit int
}
// ListIndexArgs represents the arguments for listing indexes
@[params]
pub struct ListIndexArgs {
pub mut:
limit int = 20
offset int
}
// DeleteIndexResponse represents information about the index deletion
pub struct DeleteIndexResponse {
pub mut:
uid int @[json: 'taskUid']
index_uid string @[json: 'indexUid']
status string @[json: 'status']
type_ string @[json: 'type']
enqueued_at string @[json: 'enqueuedAt']
}
struct AddDocumentResponse {
pub mut:
task_uid int @[json: 'taskUid']
index_uid string @[json: 'indexUid']
status string
type_ string @[json: 'type']
enqueued_at string @[json: 'enqueuedAt']
}
struct DeleteDocumentResponse {
pub mut:
task_uid int @[json: 'taskUid']
index_uid string @[json: 'indexUid']
status string
type_ string @[json: 'type']
enqueued_at string @[json: 'enqueuedAt']
}
struct SearchResponse[T] {
pub mut:
hits []T @[json: 'hits']
offset int @[json: 'offset']
limit int @[json: 'limit']
estimated_total_hits int @[json: 'estimatedTotalHits']
total_hits int @[json: 'totalHits']
total_pages int @[json: 'totalPages']
hits_per_page int @[json: 'hitsPerPage']
page int @[json: 'page']
facet_stats map[string]map[string]f64 @[json: 'facetStats']
processing_time_ms int @[json: 'processingTimeMs']
query string @[json: 'query']
}

View File

@@ -0,0 +1,59 @@
## Meilisearch V Client
This is a simple V client for interacting with a [self-hosted Meilisearch instance](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_medium=home-page&utm_source=docs#setup-and-installation), enabling you to perform operations such as adding, retrieving, deleting, and searching documents within indexes.
### Getting Started with Self-Hosted Meilisearch
To use this V client, ensure you have a **self-hosted Meilisearch instance installed and running**.
This quick start will walk you through installing Meilisearch, adding documents, and performing your first search.
#### Requirements
To follow this setup, you will need `curl` installed
### Setup and Installation
To install Meilisearch locally, run the following command:
```bash
# Install Meilisearch
curl -L https://install.meilisearch.com | sh
```
### Running Meilisearch
Start Meilisearch with the following command, replacing `"aSampleMasterKey"` with your preferred master key:
```bash
# Launch Meilisearch
meilisearch --master-key="aSampleMasterKey"
```
---
### Running the V Client Tests
This client includes various test cases that demonstrate common operations in Meilisearch, such as creating indexes, adding documents, retrieving documents, deleting documents, and performing searches. To run the tests, you can use the following commands:
```bash
# Run document-related tests
v -enable-globals -stats herolib/clients/meilisearch/document_test.v
# Run index-related tests
v -enable-globals -stats herolib/clients/meilisearch/index_test.v
```
### Example: Getting Meilisearch Server Version
Here is a quick example of how to retrieve the Meilisearch server version using this V client:
```v
import freeflowuniverse.herolib.clients.meilisearch
mut client := meilisearch.get() or { panic(err) }
version := client.version() or { panic(err) }
println('Meilisearch version: $version')
```
This example connects to your local Meilisearch instance and prints the server version to verify your setup is correct.

View File

@@ -0,0 +1,109 @@
module mycelium
import net.http
import json
const server_url = 'http://localhost:8989/api/v1/messages'
pub struct MessageDestination {
pub:
pk string
}
pub struct PushMessageBody {
pub:
dst MessageDestination
payload string
}
pub struct InboundMessage {
pub:
id string
src_ip string @[json: 'srcIP']
src_pk string @[json: 'srcPk']
dst_ip string @[json: 'dstIp']
dst_pk string @[json: 'dstPk']
payload string
}
pub struct MessageStatusResponse {
pub:
id string
dst string
state string
created string
deadline string
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'
}
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
}
pub fn receive_msg(wait bool) !InboundMessage {
mut url := server_url
if wait {
url = '${url}?timeout=60'
}
mut req := http.new_request(http.Method.get, url, '')
if wait {
req.read_timeout = 600000000000
}
res := req.do()!
msg := json.decode(InboundMessage, res.body)!
return msg
}
pub fn receive_msg_opt(wait bool) ?InboundMessage {
mut url := server_url
if wait {
url = '${url}?timeout=60'
}
mut req := http.new_request(http.Method.get, url, '')
if wait {
req.read_timeout = 600000000000
}
res := req.do() or { panic(error) }
if res.status_code == 204 {
return none
}
msg := json.decode(InboundMessage, res.body) or { panic(err) }
return msg
}
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 reply_msg(id string, pk string, payload string) !http.Status {
mut url := '${server_url}/reply/${id}'
msg_req := PushMessageBody{
dst: MessageDestination{
pk: pk
}
payload: payload
}
res := http.post_json(url, json.encode(msg_req))!
return res.status()
}

View File

@@ -0,0 +1,23 @@
module openai
// run heroscript starting from path, text or giturl
//```
// !!OpenAIclient.define
// name:'default'
// openaikey: ''
// description:'...'
//```
pub fn heroplay(mut plbook playbook.PlayBook) ! {
for mut action in plbook.find(filter: 'openaiclient.define')! {
mut p := action.params
instance := p.get_default('instance', 'default')!
// cfg.keyname = p.get('keyname')!
mut cl := get(instance,
openaikey: p.get('openaikey')!
description: p.get_default('description', '')!
)!
cl.config_save()!
}
}
//>TODO: this needs to be extended to chats, ...

110
lib/clients/openai/audio.v Normal file
View File

@@ -0,0 +1,110 @@
module openai
import json
import freeflowuniverse.herolib.clients.httpconnection
import os
import net.http
pub enum AudioRespType {
json
text
srt
verbose_json
vtt
}
const audio_model = 'whisper-1'
const audio_mime_types = {
'.mp3': 'audio/mpeg'
'.mp4': 'audio/mp4'
'.mpeg': 'audio/mpeg'
'.mpga': 'audio/mp4'
'.m4a': 'audio/mp4'
'.wav': 'audio/vnd.wav'
'.webm': 'application/octet-stream'
}
fn audio_resp_type_str(i AudioRespType) string {
return match i {
.json {
'json'
}
.text {
'text'
}
.srt {
'srt'
}
.verbose_json {
'verbose_json'
}
.vtt {
'vtt'
}
}
}
pub struct AudioArgs {
pub mut:
filepath string
prompt string
response_format AudioRespType
temperature int
language string
}
pub struct AudioResponse {
pub mut:
text string
}
// create transcription from an audio file
// supported audio formats are mp3, mp4, mpeg, mpga, m4a, wav, or webm
pub fn (mut f OpenAIClient[Config]) create_transcription(args AudioArgs) !AudioResponse {
return f.create_audio_request(args, 'audio/transcriptions')
}
// create translation to english from an audio file
// supported audio formats are mp3, mp4, mpeg, mpga, m4a, wav, or webm
pub fn (mut f OpenAIClient[Config]) create_tranlation(args AudioArgs) !AudioResponse {
return f.create_audio_request(args, 'audio/translations')
}
fn (mut f OpenAIClient[Config]) create_audio_request(args AudioArgs, endpoint string) !AudioResponse {
file_content := os.read_file(args.filepath)!
ext := os.file_ext(args.filepath)
mut file_mime_type := ''
if ext in audio_mime_types {
file_mime_type = audio_mime_types[ext]
} else {
return error('file extenion not supported')
}
file_data := http.FileData{
filename: os.base(args.filepath)
content_type: file_mime_type
data: file_content
}
form := http.PostMultipartFormConfig{
files: {
'file': [file_data]
}
form: {
'model': audio_model
'prompt': args.prompt
'response_format': audio_resp_type_str(args.response_format)
'temperature': args.temperature.str()
'language': args.language
}
}
req := httpconnection.Request{
prefix: endpoint
}
r := f.connection.post_multi_part(req, form)!
if r.status_code != 200 {
return error('got error from server: ${r.body}')
}
return json.decode(AudioResponse, r.body)!
}

View File

@@ -0,0 +1,70 @@
module openai
import json
pub struct ChatCompletion {
pub mut:
id string
object string
created u32
choices []Choice
usage Usage
}
pub struct Choice {
pub mut:
index int
message MessageRaw
finish_reason string
}
pub struct Message {
pub mut:
role RoleType
content string
}
pub struct Usage {
pub mut:
prompt_tokens int
completion_tokens int
total_tokens int
}
pub struct Messages {
pub mut:
messages []Message
}
pub struct MessageRaw {
pub mut:
role string
content string
}
struct ChatMessagesRaw {
mut:
model string
messages []MessageRaw
}
// creates a new chat completion given a list of messages
// each message consists of message content and the role of the author
pub fn (mut f OpenAIClient[Config]) chat_completion(model_type ModelType, msgs Messages) !ChatCompletion {
model_type0 := modelname_str(model_type)
mut m := ChatMessagesRaw{
model: model_type0
}
for msg in msgs.messages {
mr := MessageRaw{
role: roletype_str(msg.role)
content: msg.content
}
m.messages << mr
}
data := json.encode(m)
r := f.connection.post_json_str(prefix: 'chat/completions', data: data)!
res := json.decode(ChatCompletion, r)!
return res
}

View File

@@ -0,0 +1,54 @@
module openai
import json
pub enum EmbeddingModel {
text_embedding_ada
}
fn embedding_model_str(e EmbeddingModel) string {
return match e {
.text_embedding_ada {
'text-embedding-ada-002'
}
}
}
@[params]
pub struct EmbeddingCreateArgs {
input []string @[required]
model EmbeddingModel @[required]
user string
}
pub struct EmbeddingCreateRequest {
input []string
model string
user string
}
pub struct Embedding {
pub mut:
object string
embedding []f32
index int
}
pub struct EmbeddingResponse {
pub mut:
object string
data []Embedding
model string
usage Usage
}
pub fn (mut f OpenAIClient[Config]) create_embeddings(args EmbeddingCreateArgs) !EmbeddingResponse {
req := EmbeddingCreateRequest{
input: args.input
model: embedding_model_str(args.model)
user: args.user
}
data := json.encode(req)
r := f.connection.post_json_str(prefix: 'embeddings', data: data)!
return json.decode(EmbeddingResponse, r)!
}

View File

@@ -0,0 +1,64 @@
module openai
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.core.playbook
import freeflowuniverse.herolib.ui as gui
import freeflowuniverse.herolib.clients.httpconnection
// import freeflowuniverse.herolib.ui.console
pub struct OpenAIClient[T] {
base.BaseConfig[T]
pub mut:
connection &httpconnection.HTTPConnection
}
@[params]
pub struct Config {
pub mut:
openaikey string @[secret]
description string
}
pub fn get(instance string, cfg Config) !OpenAIClient[Config] {
mut self := OpenAIClient[Config]{
connection: &httpconnection.HTTPConnection{}
}
if cfg.openaikey.len > 0 {
// first the type of the instance, then name of instance, then action
self.init('openaiclient', instance, .set, cfg)!
} else {
self.init('openaiclient', instance, .get)!
}
mut conn := httpconnection.new(
name: 'openai'
url: 'https://api.openai.com/v1/'
)!
conn.default_header.add(.authorization, 'Bearer ${self.config()!.openaikey}')
// req.add_custom_header('x-disable-pagination', 'True') !
self.connection = conn
return self
}
// get a new OpenAI client, will create if it doesn't exist or ask for new configuration
pub fn configure(instance_ string) ! {
mut cfg := Config{}
mut ui := gui.new()!
mut instance := instance_
if instance == '' {
instance = ui.ask_question(
question: 'name for Dagu client'
default: instance
)!
}
cfg.openaikey = ui.ask_question(
question: '\nPlease specify your openai secret (instance:${instance}).'
)!
get(instance, cfg)!
}

View File

@@ -0,0 +1,90 @@
module openai
import json
import freeflowuniverse.herolib.clients.httpconnection
import os
import net.http
const jsonl_mime_type = 'text/jsonl'
@[params]
pub struct FileUploadArgs {
pub:
filepath string
purpose string
}
pub struct File {
pub mut:
id string
object string
bytes int
created_at int
filename string
purpose string
}
pub struct Files {
pub mut:
data []File
}
pub struct DeleteResp {
pub mut:
id string
object string
deleted bool
}
// upload file to client org, usually used for fine tuning
pub fn (mut f OpenAIClient[Config]) upload_file(args FileUploadArgs) !File {
file_content := os.read_file(args.filepath)!
file_data := http.FileData{
filename: os.base(args.filepath)
data: file_content
content_type: jsonl_mime_type
}
form := http.PostMultipartFormConfig{
files: {
'file': [file_data]
}
form: {
'purpose': args.purpose
}
}
req := httpconnection.Request{
prefix: 'files'
}
r := f.connection.post_multi_part(req, form)!
if r.status_code != 200 {
return error('got error from server: ${r.body}')
}
return json.decode(File, r.body)!
}
// list all files in client org
pub fn (mut f OpenAIClient[Config]) list_files() !Files {
r := f.connection.get(prefix: 'files')!
return json.decode(Files, r)!
}
// deletes a file
pub fn (mut f OpenAIClient[Config]) delete_file(file_id string) !DeleteResp {
r := f.connection.delete(prefix: 'files/' + file_id)!
return json.decode(DeleteResp, r)!
}
// returns a single file metadata
pub fn (mut f OpenAIClient[Config]) get_file(file_id string) !File {
r := f.connection.get(prefix: 'files/' + file_id)!
return json.decode(File, r)!
}
// returns the content of a specific file
pub fn (mut f OpenAIClient[Config]) get_file_content(file_id string) !string {
r := f.connection.get(prefix: 'files/' + file_id + '/content')!
return r
}

View File

@@ -0,0 +1,93 @@
module openai
import json
pub struct FineTune {
pub:
id string
object string
model string
created_at int
events []FineTuneEvent
fine_tuned_model string
hyperparams FineTuneHyperParams
organization_id string
result_files []File
status string
validation_files []File
training_files []File
updated_at int
}
pub struct FineTuneEvent {
pub:
object string
created_at int
level string
message string
}
pub struct FineTuneHyperParams {
pub:
batch_size int
learning_rate_multiplier f64
n_epochs int
prompt_loss_weight f64
}
pub struct FineTuneList {
pub:
object string
data []FineTune
}
pub struct FineTuneEventList {
pub:
object string
data []FineTuneEvent
}
@[params]
pub struct FineTuneCreateArgs {
pub mut:
training_file string @[required]
model string
n_epochs int = 4
batch_size int
learning_rate_multiplier f32
prompt_loss_weight f64
compute_classification_metrics bool
suffix string
}
// creates a new fine-tune based on an already uploaded file
pub fn (mut f OpenAIClient[Config]) create_fine_tune(args FineTuneCreateArgs) !FineTune {
data := json.encode(args)
r := f.connection.post_json_str(prefix: 'fine-tunes', data: data)!
return json.decode(FineTune, r)!
}
// returns all fine-tunes in this account
pub fn (mut f OpenAIClient[Config]) list_fine_tunes() !FineTuneList {
r := f.connection.get(prefix: 'fine-tunes')!
return json.decode(FineTuneList, r)!
}
// get a single fine-tune information
pub fn (mut f OpenAIClient[Config]) get_fine_tune(fine_tune string) !FineTune {
r := f.connection.get(prefix: 'fine-tunes/' + fine_tune)!
return json.decode(FineTune, r)!
}
// cancel a fine-tune that didn't finish yet
pub fn (mut f OpenAIClient[Config]) cancel_fine_tune(fine_tune string) !FineTune {
r := f.connection.post_json_str(prefix: 'fine-tunes/' + fine_tune + '/cancel')!
return json.decode(FineTune, r)!
}
// returns all events for a fine tune in this account
pub fn (mut f OpenAIClient[Config]) list_fine_tune_events(fine_tune string) !FineTuneEventList {
r := f.connection.get(prefix: 'fine-tunes/' + fine_tune + '/events')!
return json.decode(FineTuneEventList, r)!
}

189
lib/clients/openai/images.v Normal file
View File

@@ -0,0 +1,189 @@
module openai
import json
import net.http
import os
import freeflowuniverse.herolib.clients.httpconnection
const image_mine_type = 'image/png'
pub enum ImageSize {
size_256_256
size_512_512
size_1024_1024
}
fn image_size_str(i ImageSize) string {
return match i {
.size_256_256 {
'256x256'
}
.size_512_512 {
'512x512'
}
.size_1024_1024 {
'1024x1024'
}
}
}
pub enum ImageRespType {
url
b64_json
}
fn image_resp_type_str(i ImageRespType) string {
return match i {
.url {
'url'
}
.b64_json {
'b64_json'
}
}
}
pub struct ImageCreateArgs {
pub mut:
prompt string
num_images int
size ImageSize
format ImageRespType
user string
}
pub struct ImageEditArgs {
pub mut:
image_path string
mask_path string
prompt string
num_images int
size ImageSize
format ImageRespType
user string
}
pub struct ImageVariationArgs {
pub mut:
image_path string
num_images int
size ImageSize
format ImageRespType
user string
}
pub struct ImageRequest {
pub mut:
prompt string
n int
size string
response_format string
user string
}
pub struct ImageResponse {
pub mut:
url string
b64_json string
}
pub struct Images {
pub mut:
created int
data []ImageResponse
}
// Create new images generation given a prompt
// the amount of images returned is specified by `num_images`
pub fn (mut f OpenAIClient[Config]) create_image(args ImageCreateArgs) !Images {
image_size := image_size_str(args.size)
response_format := image_resp_type_str(args.format)
request := ImageRequest{
prompt: args.prompt
n: args.num_images
size: image_size
response_format: response_format
user: args.user
}
data := json.encode(request)
r := f.connection.post_json_str(prefix: 'images/generations', data: data)!
return json.decode(Images, r)!
}
// edit images generation given a prompt and an existing image
// image needs to be in PNG format and transparent or else a mask of the same size needs
// to be specified to indicate where the image should be in the generated image
// the amount of images returned is specified by `num_images`
pub fn (mut f OpenAIClient[Config]) create_edit_image(args ImageEditArgs) !Images {
image_content := os.read_file(args.image_path)!
image_file := http.FileData{
filename: os.base(args.image_path)
content_type: image_mine_type
data: image_content
}
mut mask_file := []http.FileData{}
if args.mask_path != '' {
mask_content := os.read_file(args.mask_path)!
mask_file << http.FileData{
filename: os.base(args.mask_path)
content_type: image_mine_type
data: mask_content
}
}
form := http.PostMultipartFormConfig{
files: {
'image': [image_file]
'mask': mask_file
}
form: {
'prompt': args.prompt
'n': args.num_images.str()
'response_format': image_resp_type_str(args.format)
'size': image_size_str(args.size)
'user': args.user
}
}
req := httpconnection.Request{
prefix: 'images/edits'
}
r := f.connection.post_multi_part(req, form)!
if r.status_code != 200 {
return error('got error from server: ${r.body}')
}
return json.decode(Images, r.body)!
}
// create variations of the given image
// image needs to be in PNG format
// the amount of images returned is specified by `num_images`
pub fn (mut f OpenAIClient[Config]) create_variation_image(args ImageVariationArgs) !Images {
image_content := os.read_file(args.image_path)!
image_file := http.FileData{
filename: os.base(args.image_path)
content_type: image_mine_type
data: image_content
}
form := http.PostMultipartFormConfig{
files: {
'image': [image_file]
}
form: {
'n': args.num_images.str()
'response_format': image_resp_type_str(args.format)
'size': image_size_str(args.size)
'user': args.user
}
}
req := httpconnection.Request{
prefix: 'images/variations'
}
r := f.connection.post_multi_part(req, form)!
if r.status_code != 200 {
return error('got error from server: ${r.body}')
}
return json.decode(Images, r.body)!
}

View File

@@ -0,0 +1,75 @@
module openai
pub enum ModelType {
gpt_3_5_turbo
gpt_4
gpt_4_0613
gpt_4_32k
gpt_4_32k_0613
gpt_3_5_turbo_0613
gpt_3_5_turbo_16k
gpt_3_5_turbo_16k_0613
whisper_1
}
fn modelname_str(e ModelType) string {
if e == .gpt_4 {
return 'gpt-4'
}
if e == .gpt_3_5_turbo {
return 'gpt-3.5-turbo'
}
return match e {
.gpt_4 {
'gpt-4'
}
.gpt_3_5_turbo {
'gpt-3.5-turbo'
}
.gpt_4_0613 {
'gpt-4-0613'
}
.gpt_4_32k {
'gpt-4-32k'
}
.gpt_4_32k_0613 {
'gpt-4-32k-0613'
}
.gpt_3_5_turbo_0613 {
'gpt-3.5-turbo-0613'
}
.gpt_3_5_turbo_16k {
'gpt-3.5-turbo-16k'
}
.gpt_3_5_turbo_16k_0613 {
'gpt-3.5-turbo-16k-0613'
}
.whisper_1 {
'whisper-1'
}
}
}
pub enum RoleType {
system
user
assistant
function
}
fn roletype_str(x RoleType) string {
return match x {
.system {
'system'
}
.user {
'user'
}
.assistant {
'assistant'
}
.function {
'function'
}
}
}

View File

@@ -0,0 +1,46 @@
module openai
import json
pub struct Model {
pub mut:
id string
created int
object string
owned_by string
root string
parent string
permission []ModelPermission
}
pub struct ModelPermission {
pub mut:
id string
created int
object string
allow_create_engine bool
allow_sampling bool
allow_logprobs bool
allow_search_indices bool
allow_view bool
allow_fine_tuning bool
organization string
is_blocking bool
}
pub struct Models {
pub mut:
data []Model
}
// list current models available in Open AI
pub fn (mut f OpenAIClient[Config]) list_models() !Models {
r := f.connection.get(prefix: 'models')!
return json.decode(Models, r)!
}
// returns details of a model using the model id
pub fn (mut f OpenAIClient[Config]) get_model(model string) !Model {
r := f.connection.get(prefix: 'models/' + model)!
return json.decode(Model, r)!
}

View File

@@ -0,0 +1,80 @@
module openai
import json
pub enum ModerationModel {
text_moderation_latest
text_moderation_stable
}
fn moderation_model_str(m ModerationModel) string {
return match m {
.text_moderation_latest {
'text-moderation-latest'
}
.text_moderation_stable {
'text-moderation-stable'
}
}
}
@[params]
pub struct ModerationRequest {
mut:
input string
model string
}
pub struct ModerationResult {
pub mut:
categories ModerationResultCategories
category_scores ModerationResultCategoryScores
flagged bool
}
pub struct ModerationResultCategories {
pub mut:
sexual bool
hate bool
harassment bool
selfharm bool @[json: 'self-harm']
sexual_minors bool @[json: 'sexual/minors']
hate_threatening bool @[json: 'hate/threatening']
violence_graphic bool @[json: 'violence/graphic']
selfharm_intent bool @[json: 'self-harm/intent']
selfharm_instructions bool @[json: 'self-harm/instructions']
harassment_threatening bool @[json: 'harassment/threatening']
violence bool
}
pub struct ModerationResultCategoryScores {
pub mut:
sexual f32
hate f32
harassment f32
selfharm f32 @[json: 'self-harm']
sexual_minors f32 @[json: 'sexual/minors']
hate_threatening f32 @[json: 'hate/threatening']
violence_graphic f32 @[json: 'violence/graphic']
selfharm_intent f32 @[json: 'self-harm/intent']
selfharm_instructions f32 @[json: 'self-harm/instructions']
harassment_threatening f32 @[json: 'harassment/threatening']
violence f32
}
pub struct ModerationResponse {
pub mut:
id string
model string
results []ModerationResult
}
pub fn (mut f OpenAIClient[Config]) create_moderation(input string, model ModerationModel) !ModerationResponse {
req := ModerationRequest{
input: input
model: moderation_model_str(model)
}
data := json.encode(req)
r := f.connection.post_json_str(prefix: 'moderations', data: data)!
return json.decode(ModerationResponse, r)!
}

View File

@@ -0,0 +1,50 @@
# OpenAI
An implementation of an OpenAI client using Vlang.
## Supported methods
- List available models
- Chat Completion
- Translate Audio
- Transcribe Audio
- Create image based on prompt
- Edit an existing image
- Create variation of an image
## Usage
To use the client you need a OpenAi key which can be generated from [here](https://platform.openai.com/account/api-keys).
The key should be exposed in an environment variable as following:
```bash
export OPENAI_API_KEY=<your-api-key>
```
To get a new instance of the client:
```v
import freeflowuniverse.herolib.clients.openai
ai_cli := openai.new()!
```
Then it is possible to perform all the listed operations:
```v
// listing models
models := ai_cli.list_models()!
// creating a new chat completion
mut msg := []op.Message{}
msg << op.Message{
role: op.RoleType.user
content: 'Say this is a test!'
}
mut msgs := op.Messages{
messages: msg
}
res := ai_cli.chat_completion(op.ModelType.gpt_3_5_turbo, msgs)!
```

View File

@@ -0,0 +1,56 @@
module postgres
import freeflowuniverse.herolib.core.base
import db.pg
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.ui.console
// pub struct PostgresClient {
// base.BaseConfig
// pub mut:
// config Config
// db pg.DB
// }
// @[params]
// pub struct ClientArgs {
// pub mut:
// instance string @[required]
// // playargs ?play.PlayArgs
// }
// pub fn get(clientargs ClientArgs) !PostgresClient {
// // mut plargs := clientargs.playargs or {
// // // play.PlayArgs
// // // {
// // // }
// // }
// // mut cfg := configurator(clientargs.instance, plargs)!
// // mut args := cfg.get()!
// args.instance = texttools.name_fix(args.instance)
// if args.instance == '' {
// args.instance = 'default'
// }
// // console.print_debug(args)
// mut db := pg.connect(
// host: args.host
// user: args.user
// port: args.port
// password: args.password
// dbname: args.dbname
// )!
// // console.print_debug(postgres_client)
// return PostgresClient{
// instance: args.instance
// db: db
// config: args
// }
// }
struct LocalConfig {
name string
path string
passwd string
}

107
lib/clients/postgres/cmds.v Normal file
View File

@@ -0,0 +1,107 @@
module postgres
import db.pg
import freeflowuniverse.herolib.core.texttools
import freeflowuniverse.herolib.osal
import os
import freeflowuniverse.herolib.ui.console
pub fn (mut self PostgresClient[Config]) check() ! {
mut db := self.db
db.exec('SELECT version();') or { return error('can\t select version from database.\n${self}') }
}
pub fn (mut self PostgresClient[Config]) exec(c_ string) ![]pg.Row {
mut db := self.db
mut c := c_
if !(c.trim_space().ends_with(';')) {
c += ';'
}
config := self.config()!
return db.exec(c) or {
return error('can\t execute query on ${config.host}:${config.dbname}.\n${c}\n${err}')
}
}
pub fn (mut self PostgresClient[Config]) db_exists(name_ string) !bool {
mut db := self.db
r := db.exec("SELECT datname FROM pg_database WHERE datname='${name_}';")!
if r.len == 1 {
// console.print_header(' db exists: ${name_}')
return true
}
if r.len > 1 {
return error('should not have more than 1 db with name ${name_}')
}
return false
}
pub fn (mut self PostgresClient[Config]) db_create(name_ string) ! {
name := texttools.name_fix(name_)
mut db := self.db
db_exists := self.db_exists(name_)!
if !db_exists {
console.print_header(' db create: ${name}')
db.exec('CREATE DATABASE ${name};')!
}
db_exists2 := self.db_exists(name_)!
if !db_exists2 {
return error('Could not create db: ${name_}, could not find in DB.')
}
}
pub fn (mut self PostgresClient[Config]) db_delete(name_ string) ! {
mut db := self.db
name := texttools.name_fix(name_)
self.check()!
db_exists := self.db_exists(name_)!
if db_exists {
console.print_header(' db delete: ${name_}')
db.exec('DROP DATABASE ${name};')!
}
db_exists2 := self.db_exists(name_)!
if db_exists2 {
return error('Could not delete db: ${name_}, could not find in DB.')
}
}
pub fn (mut self PostgresClient[Config]) db_names() ![]string {
mut res := []string{}
sqlstr := "SELECT datname FROM pg_database WHERE datistemplate = false and datname != 'postgres' and datname != 'root';"
for row in self.exec(sqlstr)! {
v := row.vals[0] or { '' }
res << v or { '' }
}
return res
}
@[params]
pub struct BackupParams {
pub mut:
dbname string
dest string
}
pub fn (mut self PostgresClient[Config]) backup(args BackupParams) ! {
if args.dest == '' {
return error('specify the destination please')
}
if !os.exists(args.dest) {
os.mkdir_all(args.dest)!
}
if args.dbname == '' {
for dbname in self.db_names()! {
self.backup(dbname: dbname, dest: args.dest)!
}
} else {
config := self.config()!
cmd := '
export PGPASSWORD=\'${config.password}\'
pg_dump -h ${config.host} -p ${config.port} -U ${config.user} --dbname=${args.dbname} --format=c > "${args.dest}/${args.dbname}.bak"
' // console.print_debug(cmd)
osal.exec(cmd: cmd, stdout: true)!
}
}

View File

@@ -0,0 +1,91 @@
module postgres
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.ui
import freeflowuniverse.herolib.ui.console
@[params]
pub struct Config {
pub mut:
instance string = 'default'
user string = 'root'
port int = 5432
host string = 'localhost'
password string
dbname string = 'postgres'
heroscript string
reset bool
}
pub fn configure(instance string, cfg_ Config) !PostgresClient[Config] {
mut config := cfg_
mut server := PostgresClient[Config]{}
server.init('postgres', instance, .set, config)!
return get(instance)!
}
pub fn configure_interactive(args_ Config, mut session base.Session) ! {
mut args := args_
mut myui := ui.new()!
console.clear()
console.print_debug('\n## Configure Postgres Client')
console.print_debug('============================\n\n')
instance := myui.ask_question(
question: 'name for postgres client'
default: args.instance
)!
args.user = myui.ask_question(
question: 'user'
minlen: 3
default: args.user
)!
args.password = myui.ask_question(
question: 'password'
minlen: 3
default: args.password
)!
args.dbname = myui.ask_question(
question: 'dbname'
minlen: 3
default: args.dbname
)!
args.host = myui.ask_question(
question: 'host'
minlen: 3
default: args.host
)!
mut port := myui.ask_question(
question: 'port'
default: '${args.port}'
)!
args.port = port.int()
mut client := PostgresClient[Config]{}
client.init('postgres', instance, .set, args)!
}
// pub fn play_session(mut session base.Session) ! {
// for mut action in session.plbook.find(filter: 'postgresclient.define')! {
// mut p := action.params
// mut args := config()
// panic('implement')
// // args.instance = p.get_default('name','')!
// // if args.instance == ""{
// // args.instance = p.get_default('instance', 'default')!
// // }
// // args.mail_from = p.get('mail_from')!
// // args.smtp_addr = p.get('smtp_addr')!
// // args.smtp_login = p.get('smtp_login')!
// // args.smtp_passwd = p.get('smtp_passwd')!
// // args.smpt_port = p.get_int('smpt_port')!
// // mut c:=configurator(args.instance,session:session)!
// // c.set(args)!
// }
// }

View File

@@ -0,0 +1,29 @@
module postgres
import freeflowuniverse.herolib.core.base
import freeflowuniverse.herolib.ui as gui
import freeflowuniverse.herolib.ui.console
import db.pg
pub struct PostgresClient[T] {
base.BaseConfig[T]
pub mut:
db pg.DB
}
pub fn get(instance string) !PostgresClient[Config] {
mut self := PostgresClient[Config]{}
self.init('postgres', instance, .get)!
config := self.config()!
mut db := pg.connect(
host: config.host
user: config.user
port: config.port
password: config.password
dbname: config.dbname
)!
self.db = db
return self
}

View File

@@ -0,0 +1,76 @@
# postgres client
## use hero to work with postgres
```bash
Usage: hero postgres [flags] [commands]
manage postgresql
Flags:
-help Prints help information.
-man Prints the auto-generated manpage.
Commands:
exec execute a query
check check the postgresql connection
configure configure a postgresl connection.
backup backup
print print configure info.
list list databases
```
## configure
the postgres configuration is stored on the filesystem for further use, can be configured as follows
```v
import freeflowuniverse.herolib.clients.postgres
postgres.configure(name:'default',
user :'root'
port : 5432
host : 'localhost'
password : 'ssss'
dbname :'postgres')!
mut db:=postgres.get(name:'default')!
```
## configure through heroscript
```v
import freeflowuniverse.herolib.clients.postgres
heroscript:='
!!postgresclient.define name:'default'
//TO IMPLEMENT
'
postgres.configure(heroscript:heroscript)!
//can also be done through get directly
mut cl:=postgres.get(reset:true,name:'default',heroscript:heroscript)
```
## some postgresql cmds
```v
import freeflowuniverse.herolib.clients.postgres
mut cl:=postgres.get()! //will default get postgres client with name 'default'
cl.db_exists("mydb")!
```
## use the good module of v
- [https://modules.vlang.io/db.pg.html#DB.exec](https://modules.vlang.io/db.pg.html#DB.exec)

View File

@@ -0,0 +1,58 @@
module redisclient
// original code see https://github.com/patrickpissurno/vredis/blob/master/vredis_test.v
// credits see there as well (-:
import net
// import sync
// import strconv
__global (
redis_connections []Redis
)
const default_read_timeout = net.infinite_timeout
@[heap]
pub struct Redis {
pub:
addr string
mut:
socket net.TcpConn
}
// https://redis.io/topics/protocol
// examples:
// localhost:6379
// /tmp/redis-default.sock
pub fn new(addr string) !Redis {
// lock redis_connections {
for mut conn in redis_connections {
if conn.addr == addr {
return conn
}
}
// means there is no connection yet
mut r := Redis{
addr: addr
}
r.socket_connect()!
redis_connections << r
return r
//}
// panic("bug")
}
pub fn reset() ! {
// lock redis_connections {
for mut conn in redis_connections {
conn.disconnect()
}
redis_connections = []Redis{}
//}
}
pub fn checkempty() {
// lock redis_connections {
assert redis_connections.len == 0
//}
}

View File

@@ -0,0 +1,19 @@
# Redisclient
## basic example to connect to local redis on 127.0.0.1:6379
```v
import freeflowuniverse.herolib.clients.redisclient
mut redis := redisclient.core_get()!
redis.set('test', 'some data') or { panic('set' + err.str() + '\n' + c.str()) }
r := redis.get('test')?
if r != 'some data' {
panic('get error different result.' + '\n' + c.str())
}
```
> redis commands can be found on https://redis.io/commands/

View File

@@ -0,0 +1,57 @@
module redisclient
import freeflowuniverse.herolib.ui.console
pub struct RedisCache {
mut:
redis &Redis @[str: skip]
namespace string
enabled bool = true
}
// return a cache object starting from a redis connection
pub fn (mut r Redis) cache(namespace string) RedisCache {
return RedisCache{
redis: &r
namespace: namespace
}
}
pub fn (mut h RedisCache) get(key string) ?string {
if !h.enabled {
return none
}
key2 := h.namespace + ':' + key
hit := h.redis.get('cache:${key2}') or {
console.print_debug('[-] cache: cache miss, ${key2}')
return none
}
console.print_debug('[+] cache: cache hit: ${key2}')
return hit
}
pub fn (mut h RedisCache) set(key string, val string, expire int) ! {
if !h.enabled {
return
}
key2 := h.namespace + ':' + key
h.redis.set_ex('cache:${key2}', val, expire.str())!
}
pub fn (mut h RedisCache) exists(key string) bool {
h.get(key) or { return false }
return true
}
pub fn (mut h RedisCache) reset() ! {
key_check := 'cache:' + h.namespace
// console.print_debug(key_check)
keys := h.redis.keys(key_check)!
// console.print_debug(keys)
for key in keys {
// console.print_debug(key)
h.redis.del(key)!
}
}

View File

@@ -0,0 +1,305 @@
module redisclient
import freeflowuniverse.herolib.data.resp
import time
pub fn (mut r Redis) ping() !string {
return r.send_expect_strnil(['PING'])
}
pub fn (mut r Redis) set(key string, value string) ! {
return r.send_expect_ok(['SET', key, value])
}
pub fn (mut r Redis) set_ex(key string, value string, ex string) ! {
return r.send_expect_ok(['SET', key, value, 'EX', ex])
}
pub fn (mut r Redis) set_opts(key string, value string, opts SetOpts) !bool {
ex := if opts.ex == -4 && opts.px == -4 {
''
} else if opts.ex != -4 {
' EX ${opts.ex}'
} else {
' PX ${opts.px}'
}
nx := if opts.nx == false && opts.xx == false {
''
} else if opts.nx == true {
' NX'
} else {
' XX'
}
keep_ttl := if opts.keep_ttl == false { '' } else { ' KEEPTTL' }
message := 'SET "${key}" "${value}"${ex}${nx}${keep_ttl}\r\n'
r.write(message.bytes()) or { return false }
time.sleep(1 * time.millisecond)
res := r.read_line()!
match res {
'+OK\r\n' {
return true
}
else {
return false
}
}
}
pub fn (mut r Redis) get(key string) !string {
// mut key2 := key.trim("\"'")
return r.send_expect_strnil(['GET', key])
}
pub fn (mut r Redis) exists(key string) !bool {
r2 := r.send_expect_int(['EXISTS', key])!
return r2 == 1
}
pub fn (mut r Redis) del(key string) !int {
return r.send_expect_int(['DEL', key])
}
pub fn (mut r Redis) hset(key string, skey string, value string) ! {
r.send_expect_int(['HSET', key, skey, value])!
}
pub fn (mut r Redis) hget(key string, skey string) !string {
// mut key2 := key.trim("\"'")
return r.send_expect_strnil(['HGET', key, skey])
}
pub fn (mut r Redis) hgetall(key string) !map[string]string {
// mut key2 := key.trim("\"'")
res := r.send_expect_list_str(['HGETALL', key])!
mut mapped := map[string]string{}
mut i := 0
for i < res.len && i + 1 < res.len {
mapped[res[i]] = res[i + 1]
i += 2
}
return mapped
}
pub fn (mut r Redis) hexists(key string, skey string) !bool {
return r.send_expect_bool(['HEXISTS', key, skey])
}
pub fn (mut r Redis) hdel(key string, skey string) !int {
return r.send_expect_int(['HDEL', key, skey])
}
pub fn (mut r Redis) incrby(key string, increment int) !int {
return r.send_expect_int(['INCRBY', key, increment.str()])
}
pub fn (mut r Redis) incr(key string) !int {
return r.incrby(key, 1)
}
pub fn (mut r Redis) decr(key string) !int {
return r.incrby(key, -1)
}
pub fn (mut r Redis) decrby(key string, decrement int) !int {
return r.incrby(key, -decrement)
}
pub fn (mut r Redis) incrbyfloat(key string, increment f64) !f64 {
res := r.send_expect_str(['INCRBYFLOAT', key, increment.str()])!
count := res.f64()
return count
}
pub fn (mut r Redis) append(key string, value string) !int {
return r.send_expect_int(['APPEND', key, value])
}
pub fn (mut r Redis) setrange(key string, offset int, value string) !int {
return r.send_expect_int(['SETRANGE', key, offset.str(), value.str()])
}
pub fn (mut r Redis) lpush(key string, element string) !int {
return r.send_expect_int(['LPUSH', key, element])
}
pub fn (mut r Redis) rpush(key string, element string) !int {
return r.send_expect_int(['RPUSH', key, element])
}
pub fn (mut r Redis) lrange(key string, start int, end int) ![]resp.RValue {
return r.send_expect_list(['LRANGE', key, start.str(), end.str()])
}
pub fn (mut r Redis) expire(key string, seconds int) !int {
return r.send_expect_int(['EXPIRE', key, seconds.str()])
}
pub fn (mut r Redis) pexpire(key string, millis int) !int {
return r.send_expect_int(['PEXPIRE', key, millis.str()])
}
pub fn (mut r Redis) expireat(key string, timestamp int) !int {
return r.send_expect_int(['EXPIREAT', key, timestamp.str()])
}
pub fn (mut r Redis) pexpireat(key string, millistimestamp i64) !int {
return r.send_expect_int(['PEXPIREAT', key, millistimestamp.str()])
}
pub fn (mut r Redis) persist(key string) !int {
return r.send_expect_int(['PERSIST', key])
}
pub fn (mut r Redis) getset(key string, value string) !string {
return r.send_expect_strnil(['GETSET', key, value])
}
pub fn (mut r Redis) getrange(key string, start int, end int) !string {
return r.send_expect_str(['GETRANGE', key, start.str(), end.str()])
}
pub fn (mut r Redis) keys(pattern string) ![]string {
response := r.send_expect_list(['KEYS', pattern])!
mut result := []string{}
for item in response {
result << resp.get_redis_value(item)
}
return result
}
pub fn (mut r Redis) hkeys(key string) ![]string {
response := r.send_expect_list(['HKEYS', key])!
mut result := []string{}
for item in response {
result << resp.get_redis_value(item)
}
return result
}
pub fn (mut r Redis) randomkey() !string {
return r.send_expect_strnil(['RANDOMKEY'])
}
pub fn (mut r Redis) strlen(key string) !int {
return r.send_expect_int(['STRLEN', key])
}
pub fn (mut r Redis) lpop(key string) !string {
return r.send_expect_strnil(['LPOP', key])
}
pub fn (mut r Redis) blpop(keys []string, timeout f64) ![]string {
mut request := ['BLPOP']
request << keys
request << '${timeout}'
res := r.send_expect_list_str(request)!
if res.len != 2 || res[1] == '' {
return error('timeout on blpop')
}
return res
}
pub fn (mut r Redis) brpop(keys []string, timeout f64) ![]string {
mut request := ['BRPOP']
request << keys
request << '${timeout}'
res := r.send_expect_list_str(request)!
if res.len != 2 {
return error('timeout on brpop')
}
return res
}
pub fn (mut r Redis) rpop(key string) !string {
return r.send_expect_strnil(['RPOP', key])
}
pub fn (mut r Redis) llen(key string) !int {
return r.send_expect_int(['LLEN', key])
}
pub fn (mut r Redis) ttl(key string) !int {
return r.send_expect_int(['TTL', key])
}
pub fn (mut r Redis) pttl(key string) !int {
return r.send_expect_int(['PTTL', key])
}
pub fn (mut r Redis) rename(key string, newkey string) ! {
return r.send_expect_ok(['RENAME', key, newkey])
}
pub fn (mut r Redis) renamenx(key string, newkey string) !int {
return r.send_expect_int(['RENAMENX', key, newkey])
}
pub fn (mut r Redis) setex(key string, second i64, value string) ! {
return r.send_expect_ok(['SETEX', key, second.str(), value])
}
pub fn (mut r Redis) psetex(key string, millisecond i64, value string) ! {
return r.send_expect_ok(['PSETEX', key, millisecond.str(), value])
}
pub fn (mut r Redis) setnx(key string, value string) !int {
return r.send_expect_int(['SETNX', key, value])
}
pub fn (mut r Redis) type_of(key string) !string {
return r.send_expect_strnil(['TYPE', key])
}
pub fn (mut r Redis) flushall() ! {
return r.send_expect_ok(['FLUSHALL'])
}
pub fn (mut r Redis) flushdb() ! {
return r.send_expect_ok(['FLUSHDB'])
}
// select is reserved
pub fn (mut r Redis) selectdb(database int) ! {
return r.send_expect_ok(['SELECT', database.str()])
}
pub fn (mut r Redis) scan(cursor int) !(string, []string) {
res := r.send_expect_list(['SCAN', cursor.str()])!
if res[0] !is resp.RBString {
return error('Redis SCAN wrong response type (cursor)')
}
if res[1] !is resp.RArray {
return error('Redis SCAN wrong response type (list content)')
}
mut values := []string{}
for i in 0 .. resp.get_redis_array_len(res[1]) {
values << resp.get_redis_value_by_index(res[1], i)
}
return resp.get_redis_value(res[0]), values
}
// Add the specified members to the set stored at key. Specified members that are already a member
// of this set are ignored. If key does not exist, a new set is created before adding the specified members.
// An error is returned when the value stored at key is not a set.
pub fn (mut r Redis) sadd(key string, members []string) !int {
mut tosend := ['SADD', key]
for k in members {
tosend << k
}
return r.send_expect_int(tosend)
}
// Returns if member is a member of the set stored at key.
pub fn (mut r Redis) smismember(key string, members []string) ![]int {
// mut key2 := key.trim("\"'")
mut tosend := ['SMISMEMBER', key]
for k in members {
tosend << k
}
res := r.send_expect_list_int(tosend)!
return res
}

View File

@@ -0,0 +1,24 @@
module redisclient
@[params]
pub struct RedisURL {
address string = '127.0.0.1'
port int = 6379
// db int
}
pub fn get_redis_url(url string) !RedisURL {
if !url.contains(':') {
return error('url doesnt contain port')
} else {
return RedisURL{
address: url.all_before_last(':')
port: url.all_after_last(':').u16()
}
}
}
pub fn core_get(url RedisURL) !Redis {
mut r := new('${url.address}:${url.port}')!
return r
}

View File

@@ -0,0 +1,173 @@
module redisclient
import freeflowuniverse.herolib.data.resp
pub fn (mut r Redis) get_response() !resp.RValue {
line := r.read_line()!
if line.starts_with('-') {
return resp.RError{
value: line[1..]
}
}
if line.starts_with(':') {
return resp.RInt{
value: line[1..].int()
}
}
if line.starts_with('+') {
return resp.RString{
value: line[1..]
}
}
if line.starts_with('$') {
mut bulkstring_size := line[1..].int()
if bulkstring_size == -1 {
return resp.RNil{}
}
if bulkstring_size == 0 {
// extract final \r\n and not reading
// any payload
r.read_line()!
return resp.RString{
value: ''
}
}
// read payload
buffer := r.read(bulkstring_size) or { panic(err) }
// extract final \r\n
r.read_line()!
// console.print_debug("readline result:'$buffer.bytestr()'")
return resp.RBString{
value: buffer
} // TODO: won't support binary (afaik), need to fix? WHY not (despiegk)?
}
if line.starts_with('*') {
mut arr := resp.RArray{
values: []resp.RValue{}
}
items := line[1..].int()
// proceed each entries, they can be of any types
for _ in 0 .. items {
value := r.get_response()!
arr.values << value
}
return arr
}
return error('unsupported response type')
}
// TODO: needs to use the resp library
pub fn (mut r Redis) get_int() !int {
line := r.read_line()!
if line.starts_with(':') {
return line[1..].int()
} else {
return error("Did not find int, did find:'${line}'")
}
}
pub fn (mut r Redis) get_list_int() ![]int {
line := r.read_line()!
mut res := []int{}
if line.starts_with('*') {
items := line[1..].int()
// proceed each entries, they can be of any types
for _ in 0 .. items {
value := r.get_int()!
res << value
}
return res
} else {
return error("Did not find int, did find:'${line}'")
}
}
pub fn (mut r Redis) get_list_str() ![]string {
line := r.read_line()!
mut res := []string{}
if line.starts_with('*') {
items := line[1..].int()
// proceed each entries, they can be of any types
for _ in 0 .. items {
value := r.get_string()!
res << value
}
return res
} else {
return error("Did not find int, did find:'${line}'")
}
}
pub fn (mut r Redis) get_string() !string {
line := r.read_line()!
if line.starts_with('+') {
// console.print_debug("getstring:'${line[1..]}'")
return line[1..]
}
if line.starts_with('$') {
r2 := r.get_bytes_from_line(line)!
return r2.bytestr()
} else {
return error("Did not find string, did find:'${line}'")
}
}
pub fn (mut r Redis) get_string_nil() !string {
r2 := r.get_bytes_nil()!
return r2.bytestr()
}
pub fn (mut r Redis) get_bytes_nil() ![]u8 {
line := r.read_line()!
if line.starts_with('+') {
return line[1..].bytes()
}
if line.starts_with('$-1') {
return []u8{}
}
if line.starts_with('$') {
return r.get_bytes_from_line(line)
} else {
return error("Did not find string or nil, did find:'${line}'")
}
}
pub fn (mut r Redis) get_bool() !bool {
i := r.get_int()!
return i == 1
}
pub fn (mut r Redis) get_bytes() ![]u8 {
line := r.read_line()!
if line.starts_with('$') {
return r.get_bytes_from_line(line)
} else {
return error("Did not find bulkstring, did find:'${line}'")
}
}
fn (mut r Redis) get_bytes_from_line(line string) ![]u8 {
mut bulkstring_size := line[1..].int()
if bulkstring_size == -1 {
// return none
return error('bulkstring_size is -1')
}
if bulkstring_size == 0 {
// extract final \r\n, there is no payload
r.read_line()!
return []
}
// read payload
buffer := r.read(bulkstring_size) or { panic('Could not read payload: ${err}') }
// extract final \r\n
r.read_line()!
return buffer
}

View File

@@ -0,0 +1,121 @@
module redisclient
import os
import net
import freeflowuniverse.herolib.data.resp
import time
import net.unix
pub struct SetOpts {
ex int = -4
px int = -4
nx bool
xx bool
keep_ttl bool
}
pub enum KeyType {
t_none
t_string
t_list
t_set
t_zset
t_hash
t_stream
t_unknown
}
fn (mut r Redis) socket_connect() ! {
// print_backtrace()
addr := os.expand_tilde_to_home(r.addr)
// console.print_debug(' - REDIS CONNECT: ${addr}')
if !addr.contains(':') {
unix_socket := unix.connect_stream(addr)!
tcp_socket := net.tcp_socket_from_handle_raw(unix_socket.sock.Socket.handle)
tcp_conn := net.TcpConn{
sock: tcp_socket
handle: unix_socket.sock.Socket.handle
}
r.socket = tcp_conn
} else {
r.socket = net.dial_tcp(addr)!
}
r.socket.set_blocking(true)!
r.socket.set_read_timeout(1 * time.second)
// console.print_debug("---OK")
}
fn (mut r Redis) socket_check() ! {
r.socket.peer_addr() or {
// console.print_debug(' - re-connect socket for redis')
r.socket_connect()!
}
}
pub fn (mut r Redis) read_line() !string {
return r.socket.read_line().trim_right('\r\n')
}
// write *all the data* into the socket
// This function loops, till *everything is written*
// (some of the socket write ops could be partial)
fn (mut r Redis) write(data []u8) ! {
r.socket_check()!
mut remaining := data.len
for remaining > 0 {
// zdbdata[data.len - remaining..].bytestr())
written_bytes := r.socket.write(data[data.len - remaining..])!
remaining -= written_bytes
}
}
fn (mut r Redis) read(size int) ![]u8 {
r.socket_check() or {}
mut buf := []u8{len: size}
mut remaining := size
for remaining > 0 {
read_bytes := r.socket.read(mut buf[buf.len - remaining..])!
remaining -= read_bytes
}
return buf
}
pub fn (mut r Redis) disconnect() {
r.socket.close() or {}
}
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////
// TODO: need to implement a way how to use multiple connections at once
const cr_lf_bytes = [u8(`\r`), `\n`]
fn (mut r Redis) write_line(data []u8) ! {
r.write(data)!
r.write(cr_lf_bytes)!
}
// write resp value to the redis channel
pub fn (mut r Redis) write_rval(val resp.RValue) ! {
r.write(val.encode())!
}
// write list of strings to redis challen
fn (mut r Redis) write_cmd(item string) ! {
a := resp.r_bytestring(item.bytes())
r.write_rval(a)!
}
// write list of strings to redis challen
fn (mut r Redis) write_cmds(items []string) ! {
// if items.len==1{
// a := resp.r_bytestring(items[0].bytes())
// r.write_rval(a)!
// }{
a := resp.r_list_bstring(items)
r.write_rval(a)!
// }
}

View File

@@ -0,0 +1,41 @@
module redisclient
import time
pub struct RedisQueue {
pub mut:
key string
redis &Redis
}
pub fn (mut r Redis) queue_get(key string) RedisQueue {
return RedisQueue{
key: key
redis: r
}
}
pub fn (mut q RedisQueue) add(val string) ! {
q.redis.lpush(q.key, val)!
}
// timeout in msec
pub fn (mut q RedisQueue) get(timeout u64) !string {
start := u64(time.now().unix_milli())
for {
r := q.redis.rpop(q.key) or { '' }
if r != '' {
return r
}
if u64(time.now().unix_milli()) > (start + timeout) {
break
}
time.sleep(time.microsecond)
}
return error('timeout on ${q.key}')
}
// get without timeout, returns none if nil
pub fn (mut q RedisQueue) pop() !string {
return q.redis.rpop(q.key)!
}

View File

@@ -0,0 +1,136 @@
module redisclient
import rand
import time
import json
pub struct RedisRpc {
pub mut:
key string // queue name as used by this rpc
redis &Redis
}
// return a rpc mechanism
pub fn (mut r Redis) rpc_get(key string) RedisRpc {
return RedisRpc{
key: key
redis: r
}
}
pub struct RPCArgs {
pub:
cmd string @[required]
data string @[required]
timeout u64 = 60000 // 60 sec
wait bool = true
}
pub struct Message {
pub:
ret_queue string
now i64
cmd string
data string
}
pub struct Response {
pub:
result string
error string
}
// send data to a queue and wait till return comes back
// timeout in milliseconds
// params
// cmd string @[required]
// data string @[required]
// timeout u64=60000 //60 sec
// wait bool=true
pub fn (mut q RedisRpc) call(args RPCArgs) !string {
retqueue := rand.uuid_v4()
now := time.now().unix()
message := Message{
ret_queue: retqueue
now: now
cmd: args.cmd
data: args.data
}
encoded := json.encode(message)
q.redis.lpush(q.key, encoded)!
if args.wait {
return q.result(args.timeout, retqueue)!
}
return ''
}
// get return once result processed
pub fn (mut q RedisRpc) result(timeout u64, retqueue string) !string {
start := u64(time.now().unix_milli())
for {
r := q.redis.rpop(retqueue) or { '' }
if r != '' {
res := json.decode(Response, r)!
if res.error != '' {
return res.error
}
return res.result
}
if u64(time.now().unix_milli()) > (start + timeout) {
break
}
time.sleep(time.millisecond)
}
return error('timeout on returnqueue: ${retqueue}')
}
@[params]
pub struct ProcessParams {
pub:
timeout u64
}
// to be used by processor, to get request and execute, this is the server side of a RPC mechanism
// 2nd argument is a function which needs to execute the job: fn (string,string) !string
pub fn (mut q RedisRpc) process(op fn (string, string) !string, params ProcessParams) !string {
start := u64(time.now().unix_milli())
for {
r := q.redis.rpop(q.key) or { '' }
if r != '' {
msg := json.decode(Message, r)!
returnqueue := msg.ret_queue
// epochtime:=parts[1].u64() //we don't do anything with it now
cmd := msg.cmd
data := msg.data
// if true{panic("sd")}
datareturn := op(cmd, data) or {
response := Response{
result: ''
error: err.str()
}
encoded := json.encode(response)
q.redis.lpush(returnqueue, encoded)!
return ''
}
response := Response{
result: datareturn
error: ''
}
encoded := json.encode(response)
q.redis.lpush(returnqueue, encoded)!
return returnqueue
}
if params.timeout != 0 && u64(time.now().unix_milli()) > (start + params.timeout) {
break
}
time.sleep(time.millisecond)
}
return error('timeout for waiting for cmd on ${q.key}')
}
// get without timeout, returns none if nil
pub fn (mut q RedisRpc) delete() ! {
q.redis.del(q.key)!
}

View File

@@ -0,0 +1,25 @@
import freeflowuniverse.herolib.clients.redisclient
fn setup() !&redisclient.Redis {
mut redis := redisclient.core_get()!
redis.selectdb(10) or { panic(err) }
return &redis
}
fn cleanup(mut redis redisclient.Redis) ! {
redis.flushall()!
// redis.disconnect()
}
fn test_sadd() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.sadd('mysadd', ['a', 'b', 'c']) or { panic(err) }
r := redis.smismember('mysadd', ['a', 'b', 'c']) or { panic(err) }
assert r == [1, 1, 1]
r2 := redis.smismember('mysadd', ['a', 'd', 'c']) or { panic(err) }
assert r2 == [1, 0, 1]
}

View File

@@ -0,0 +1,6 @@
module redisclient
// load a script and return the hash
pub fn (mut r Redis) script_load(script string) !string {
return r.send_expect_str(['SCRIPT LOAD', script])!
}

View File

@@ -0,0 +1,55 @@
module redisclient
import freeflowuniverse.herolib.data.resp
import freeflowuniverse.herolib.ui.console
// send list of strings, expect OK back
pub fn (mut r Redis) send_expect_ok(items []string) ! {
r.write_cmds(items)!
res := r.get_string()!
if res != 'OK' {
console.print_debug("'${res}'")
return error('did not get ok back')
}
}
// send list of strings, expect int back
pub fn (mut r Redis) send_expect_int(items []string) !int {
r.write_cmds(items)!
return r.get_int()
}
pub fn (mut r Redis) send_expect_bool(items []string) !bool {
r.write_cmds(items)!
return r.get_bool()
}
// send list of strings, expect string back
pub fn (mut r Redis) send_expect_str(items []string) !string {
r.write_cmds(items)!
return r.get_string()
}
// send list of strings, expect string or nil back
pub fn (mut r Redis) send_expect_strnil(items []string) !string {
r.write_cmds(items)!
d := r.get_string_nil()!
return d
}
// send list of strings, expect list of strings back
pub fn (mut r Redis) send_expect_list_str(items []string) ![]string {
r.write_cmds(items)!
return r.get_list_str()
}
pub fn (mut r Redis) send_expect_list_int(items []string) ![]int {
r.write_cmds(items)!
return r.get_list_int()
}
pub fn (mut r Redis) send_expect_list(items []string) ![]resp.RValue {
r.write_cmds(items)!
res := r.get_response()!
return resp.get_redis_array(res)
}

View File

@@ -0,0 +1,864 @@
import freeflowuniverse.herolib.clients.redisclient
import time
import freeflowuniverse.herolib.ui.console
// original code see https://github.com/patrickpissurno/vredis/blob/master/vredis_test.v
// credits see there as well (-:
fn setup() !&redisclient.Redis {
mut redis := redisclient.core_get()!
// Select db 10 to be away from default one '0'
redis.selectdb(10) or { panic(err) }
return &redis
}
fn cleanup(mut redis redisclient.Redis) ! {
redis.flushall()!
// redis.disconnect()
}
fn test_set() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
// console.print_debug('start')
// for _ in 0 .. 10000 {
// redis.set('test0', '123')!
// }
console.print_debug('stop')
redis.set('test0', '456')!
res := redis.get('test0')!
assert res == '456'
redis.hset('x', 'a', '222')!
redis.hset('x', 'b', '333')!
mut res3 := redis.hget('x', 'b')!
assert res3 == '333'
redis.hdel('x', 'b')!
res3 = redis.hget('x', 'b')!
assert res3 == ''
e := redis.hexists('x', 'a')!
assert e
}
fn test_large_value() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
rr := 'SSS' + 'a'.repeat(40000) + 'EEE'
mut rr2 := ''
for i in 0 .. 50 {
redis.set('test_large_value0', rr)!
rr2 = redis.get('test_large_value0')!
assert rr.len == rr2.len
assert rr == rr2
}
for i3 in 0 .. 100 {
redis.set('test_large_value${i3}', rr)!
}
for i4 in 0 .. 100 {
rr4 := redis.get('test_large_value${i4}')!
assert rr.len == rr4.len
redis.del('test_large_value${i4}')!
}
}
fn test_queue() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
mut q := redis.queue_get('kds:q')
q.add('test1')!
q.add('test2')!
mut res := q.get(1)!
assert res == 'test1'
res = q.get(1)!
assert res == 'test2'
console.print_debug('start')
res = q.get(100) or { '' }
console.print_debug('stop')
assert res == ''
console.print_debug(res)
}
fn test_scan() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
console.print_debug('stop')
redis.set('test3', '12')!
redis.set('test4', '34')!
redis.set('test5', '56')!
redis.set('test6', '78')!
redis.set('test7', '9')!
cursor, data := redis.scan(0)!
console.print_debug(data)
assert cursor == '0'
}
// fn test_set_opts() {
// mut redis := setup()!
// defer {
// cleanup(mut redis) or { panic(err) }
// }
// assert redis.set_opts('test8', '123', redisclient.SetOpts{
// ex: 2
// }) or {false}== true
// assert redis.set_opts('test8', '456', redisclient.SetOpts{
// px: 2000
// xx: true
// }) or {false} == true
// assert redis.set_opts('test8', '789', redisclient.SetOpts{
// px: 1000
// nx: true
// }) or {false}== false
// // Works with redis version > 6
// assert redis.set_opts('test8', '012', redisclient.SetOpts{ keep_ttl: true }) or {false}== true
// }
fn test_setex() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.setex('test9', 2, '123')!
mut r := redis.get('test9')!
assert r == '123'
time.sleep(2100 * time.millisecond)
r = redis.get('test9')!
assert r == ''
}
fn test_psetex() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.psetex('test10', 200, '123')!
mut r := redis.get('test10') or {
assert false
return
}
assert r == '123'
time.sleep(220 * time.millisecond)
r = redis.get('test10')!
assert r == ''
}
fn test_setnx() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
mut r1 := redis.setnx('test11', '123')!
assert r1 == 1
r1 = redis.setnx('test11', '456')!
assert r1 == 0
val := redis.get('test11') or {
assert false
return
}
assert val == '123'
}
fn test_incrby() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test12', '100')!
r1 := redis.incrby('test12', 4) or {
assert false
return
}
assert r1 == 104
r2 := redis.incrby('test13', 2) or {
assert false
return
}
assert r2 == 2
redis.set('test14', 'nan')!
redis.incrby('test14', 1) or {
assert true
return
}
assert false
}
fn test_incr() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test15', '100')!
r1 := redis.incr('test15') or {
assert false
return
}
assert r1 == 101
r2 := redis.incr('test16') or {
assert false
return
}
assert r2 == 1
redis.set('test17', 'nan')!
redis.incr('test17') or {
assert true
return
}
assert false
}
fn test_decr() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test18', '100')!
r1 := redis.decr('test18') or {
assert false
return
}
assert r1 == 99
r2 := redis.decr('test19') or {
assert false
return
}
assert r2 == -1
redis.set('test20', 'nan')!
redis.decr('test20') or {
assert true
return
}
assert false
}
fn test_decrby() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test21', '100')!
r1 := redis.decrby('test21', 4) or {
assert false
return
}
assert r1 == 96
r2 := redis.decrby('test22', 2) or {
assert false
return
}
assert r2 == -2
redis.set('test23', 'nan')!
redis.decrby('test23', 1) or {
assert true
return
}
assert false
}
fn test_incrbyfloat() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test24', '3.1415')!
r1 := redis.incrbyfloat('test24', 3.1415) or {
assert false
return
}
assert r1 == 6.283
r2 := redis.incrbyfloat('test25', 3.14) or {
assert false
return
}
assert r2 == 3.14
r3 := redis.incrbyfloat('test25', -3.14) or {
assert false
return
}
assert r3 == 0
redis.set('test26', 'nan')!
redis.incrbyfloat('test26', 1.5) or {
assert true
return
}
assert false
}
fn test_append() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test27', 'bac')!
r1 := redis.append('test27', 'on') or {
assert false
return
}
assert r1 == 5
r2 := redis.get('test27') or {
assert false
return
}
assert r2 == 'bacon'
}
fn test_lpush() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r := redis.lpush('test28', 'item 1') or {
assert false
return
}
assert r == 1
}
fn test_rpush() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r := redis.rpush('test29', 'item 1') or {
assert false
return
}
assert r == 1
}
fn test_setrange() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.setrange('test30', 0, 'bac') or {
assert false
return
}
assert r1 == 3
r2 := redis.setrange('test30', 3, 'on') or {
assert false
return
}
assert r2 == 5
}
fn test_expire() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.expire('test31', 2) or {
assert false
return
}
assert r1 == 0
redis.set('test31', '123')!
r2 := redis.expire('test31', 2) or {
assert false
return
}
assert r2 == 1
}
fn test_pexpire() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.pexpire('test32', 200) or {
assert false
return
}
assert r1 == 0
redis.set('test32', '123')!
r2 := redis.pexpire('test32', 200) or {
assert false
return
}
assert r2 == 1
}
fn test_expireat() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.expireat('test33', 1293840000) or {
assert false
return
}
assert r1 == 0
redis.set('test33', '123')!
r2 := redis.expireat('test33', 1293840000) or {
assert false
return
}
assert r2 == 1
}
fn test_pexpireat() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.pexpireat('test34', 1555555555005) or {
assert false
return
}
assert r1 == 0
redis.set('test34', '123')!
r2 := redis.pexpireat('test34', 1555555555005) or {
assert false
return
}
assert r2 == 1
}
fn test_persist() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.persist('test35') or {
assert false
return
}
assert r1 == 0
redis.setex('test35', 2, '123')!
r2 := redis.persist('test35') or {
assert false
return
}
assert r2 == 1
}
fn test_get() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test36', '123')!
mut r := redis.get('test36')!
assert r == '123'
assert helper_get_key_not_found(mut redis, 'test37') == true
}
fn test_getset() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
mut r1 := redis.getset('test38', '10') or { '' }
assert r1 == ''
r2 := redis.getset('test38', '15') or {
assert false
return
}
assert r2 == '10'
r3 := redis.get('test38') or {
assert false
return
}
assert r3 == '15'
}
fn test_getrange() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test39', 'community')!
r1 := redis.getrange('test39', 4, -1) or {
assert false
return
}
assert r1 == 'unity'
r2 := redis.getrange('test40', 0, -1) or {
assert false
return
}
assert r2 == ''
}
fn test_randomkey() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
assert helper_randomkey_database_empty(mut redis) == true
redis.set('test41', '123')!
r2 := redis.randomkey() or {
assert false
return
}
assert r2 == 'test41'
assert helper_get_key_not_found(mut redis, 'test42') == true
}
fn test_strlen() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test43', 'bacon')!
r1 := redis.strlen('test43') or {
assert false
return
}
assert r1 == 5
r2 := redis.strlen('test44') or {
assert false
return
}
assert r2 == 0
}
fn test_lpop() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.lpush('test45', '123') or {
assert false
return
}
r1 := redis.lpop('test45') or {
assert false
return
}
assert r1 == '123'
assert helper_lpop_key_not_found(mut redis, 'test46') == true
}
fn test_rpop() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.lpush('test47', '123') or {
assert false
return
}
r1 := redis.rpop('test47') or {
assert false
return
}
assert r1 == '123'
assert helper_rpop_key_not_found(mut redis, 'test48') == true
}
fn test_brpop() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.lpush('test47', '123')!
redis.lpush('test48', 'balbal')!
r1 := redis.brpop(['test47', 'test48'], 1)!
assert r1[0] == 'test47'
assert r1[1] == '123'
r2 := redis.brpop(['test47', 'test48'], 1)!
assert r2[0] == 'test48'
assert r2[1] == 'balbal'
r3 := redis.brpop(['test47'], 1) or { return }
assert false, 'brpop should timeout'
}
fn test_lrpop() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.lpush('test47', '123')!
redis.lpush('test48', 'balbal')!
r1 := redis.blpop(['test47', 'test48'], 1)!
assert r1[0] == 'test47'
assert r1[1] == '123'
r2 := redis.blpop(['test47', 'test48'], 1)!
assert r2[0] == 'test48'
assert r2[1] == 'balbal'
r3 := redis.blpop(['test47'], 1) or { return }
assert false, 'blpop should timeout'
}
fn test_llen() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.lpush('test49', '123') or {
assert false
return
}
r2 := redis.llen('test49') or {
assert false
return
}
assert r2 == r1
r3 := redis.llen('test50') or {
assert false
return
}
assert r3 == 0
redis.set('test51', 'not a list')!
redis.llen('test51') or {
assert true
return
}
assert false
}
fn test_ttl() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.setex('test52', 15, '123')!
r1 := redis.ttl('test52') or {
assert false
return
}
assert r1 == 15
redis.set('test53', '123')!
r2 := redis.ttl('test53') or {
assert false
return
}
assert r2 == -1
r3 := redis.ttl('test54') or {
assert false
return
}
assert r3 == -2
}
fn test_pttl() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.psetex('test55', 1500, '123')!
r1 := redis.pttl('test55') or {
assert false
return
}
assert r1 >= 1490 && r1 <= 1500
redis.set('test56', '123')!
r2 := redis.pttl('test56') or {
assert false
return
}
assert r2 == -1
r3 := redis.pttl('test57') or {
assert false
return
}
assert r3 == -2
}
fn test_exists() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
r1 := redis.exists('test58') or {
assert false
return
}
assert r1 == false
redis.set('test59', '123')!
r2 := redis.exists('test59') or {
assert false
return
}
assert r2 == true
}
fn test_type_of() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
_ := redis.type_of('test60') or {
assert true
return
}
redis.set('test61', '123')!
mut r := redis.type_of('test61') or {
assert false
return
}
assert r == 'string'
_ := redis.lpush('test62', '123')!
r = redis.type_of('test62') or {
assert false
return
}
assert r == 'list'
}
fn test_del() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test63', '123')!
c := redis.del('test63') or {
assert false
return
}
assert c == 1
assert helper_get_key_not_found(mut redis, 'test63') == true
}
fn test_rename() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.rename('test64', 'test65') or { console.print_debug('key not found') }
redis.set('test64', 'will be 65')!
redis.rename('test64', 'test65')!
r := redis.get('test65') or {
assert false
return
}
assert r == 'will be 65'
}
fn test_renamenx() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
assert helper_renamenx_err_helper(mut redis, 'test66', 'test67') == 'no such key'
redis.set('test68', '123')!
redis.set('test66', 'will be 67')!
r1 := redis.renamenx('test66', 'test67') or {
assert false
return
}
assert r1 == 1
r2 := redis.get('test67') or {
assert false
return
}
assert r2 == 'will be 67'
r3 := redis.renamenx('test67', 'test68') or {
assert false
return
}
assert r3 == 0
}
fn test_flushall() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test69', '123')!
redis.flushall()!
assert helper_get_key_not_found(mut redis, 'test69') == true
}
fn test_keys() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
redis.set('test70:1', '1')!
redis.set('test70:2', '2')!
r1 := redis.keys('test70:*') or {
assert false
return
}
assert r1.len == 2
}
fn helper_get_key_not_found(mut redis redisclient.Redis, key string) bool {
return redis.get(key) or {
if err.msg() == 'key not found' || err.msg() == '' {
return true
} else {
return false
}
} == ''
}
fn helper_randomkey_database_empty(mut redis redisclient.Redis) bool {
return redis.randomkey() or {
if err.msg() == 'database is empty' || err.msg() == '' {
return true
} else {
return false
}
} == ''
}
fn helper_renamenx_err_helper(mut redis redisclient.Redis, key string, newkey string) string {
redis.renamenx(key, newkey) or { return 'no such key' }
return ''
}
fn helper_lpop_key_not_found(mut redis redisclient.Redis, key string) bool {
return redis.lpop(key) or {
if err.msg() == 'key not found' || err.msg() == '' {
return true
} else {
return false
}
} == ''
}
fn helper_rpop_key_not_found(mut redis redisclient.Redis, key string) bool {
return redis.rpop(key) or {
if err.msg() == 'key not found' || err.msg() == '' {
return true
} else {
return false
}
} == ''
}

View File

@@ -0,0 +1,33 @@
import freeflowuniverse.herolib.clients.redisclient
import freeflowuniverse.herolib.ui.console
fn setup() !&redisclient.Redis {
mut redis := redisclient.core_get()!
// Select db 10 to be away from default one '0'
redis.selectdb(10) or { panic(err) }
return &redis
}
fn cleanup(mut redis redisclient.Redis) ! {
redis.flushall()!
// redis.disconnect()
}
fn process_test(cmd string, data string) !string {
return '${cmd}+++++${data}\n\n\n\n'
}
fn test_rpc() {
mut redis := setup()!
defer {
cleanup(mut redis) or { panic(err) }
}
mut r := redis.rpc_get('testrpc')
r.call(cmd: 'test.cmd', data: 'this is my data, normally json', wait: false)!
returnqueue := r.process(10000, process_test)!
mut res := r.result(10000, returnqueue)!
console.print_debug(res)
assert res.str().trim_space() == 'test.cmd+++++this is my data, normally json'
}

View File

@@ -0,0 +1,53 @@
# SendGrid Client
The SendGrid module allows you to use SendGrid services.
## About SendGrid
SendGrid is a cloud-based email delivery and communication platform that empowers businesses and developers to send transactional and marketing emails to their customers or users. It offers tools and APIs to manage email campaigns, monitor delivery, and gather analytics on recipient engagement.
## Requirements
To utilize this module, you will need:
- A SendGrid API key: Create a SendGrid account and acquire your API key [here](https://sendgrid.com/).
## Usage
To send an email using the SendGrid module, follow these steps:
### 1. Set Up a new email
In your V code, set up the email as shown below:
```v
email := sendgrid.new_email(
['target_email@example.com', 'target_email2@example.com'],
'source_email@example.com',
'Email Title', 'Email content; can include HTML')
```
### 2. Execute the program
You can execute the program using the following command:
```shell
v run sendgrid/example/main.v -t "YOUR_API_TOKEN"
```
You can provide the API key using the -t command-line argument, or you can export the API key using the following command:
```shell
export SENDGRID_AUTH_TOKEN="YOUR_API_TOKEN"
```
Additionally, you can enable debug mode by passing the -d flag:
```shell
v run sendgrid/example/main.v -d -t "YOUR_API_TOKEN"
```
## Advanced
We provide some useful structs and methods in [email](./email) and [personalization](./personalizations.v) that you can leverage to tailor the emails according to your specific requirements.
You can check the SendGrid API reference [here](https://docs.sendgrid.com/api-reference/how-to-use-the-sendgrid-v3-api/)

View File

@@ -0,0 +1,41 @@
module sendgrid
import net.http
import json
pub struct Client {
pub:
token string
}
const send_api_endpoint = 'https://api.sendgrid.com/v3/mail/send'
pub fn new_client(token string) !Client {
if token.len == 0 {
return error('empty token')
}
return Client{
token: token
}
}
fn (c Client) get_headers() !http.Header {
headers_map := {
'Authorization': 'Bearer ${c.token}'
'Content-Type': 'application/json'
}
headers := http.new_custom_header_from_map(headers_map)!
return headers
}
pub fn (c Client) send(email Email) ! {
mut request := http.new_request(http.Method.post, send_api_endpoint, json.encode(email))
request.header = c.get_headers()!
res := request.do()!
if res.status_code != int(http.Status.accepted) {
return error(res.body)
}
}

View File

@@ -0,0 +1,152 @@
module sendgrid
pub struct Content {
type_ string = 'text/html' @[json: 'type']
value string
}
struct Recipient {
email string @[required]
name ?string
}
struct Attachment {
content string @[required]
type_ ?string @[json: 'type']
filename string @[required]
disposition ?string
content_id ?string
}
struct UnsubscribeGroups {
group_id i64 @[required]
group_to_display []i64
}
struct BypassListManagement {
enable ?bool
}
struct BypassBounceManagement {
enable ?bool
}
struct BypassUnsubscribeManagement {
enable ?bool
}
struct Footer {
enable ?bool
text ?string
html ?string
}
struct SandboxMode {
enable ?bool
}
struct MailSettings {
bypass_list_management ?BypassListManagement
bypass_bounce_management ?BypassBounceManagement
bypass_unsubscribe_management ?BypassUnsubscribeManagement
footer ?Footer
sandbox_mode ?SandboxMode
}
struct ClickTrackingSettings {
enable ?bool
enable_text ?bool
}
struct OpenTrackingSettings {
enable ?bool
substitution_tag ?string
}
struct SubscriptionTrackingSettings {
enable ?bool
text ?string
html ?string
substitution_tag ?string
}
struct GoogleAnalyticsSettings {
enable ?bool
utm_source ?string
utm_medium ?string
utm_term ?string
utm_content ?string
utm_campaign ?string
}
struct TrackingSettings {
click_tracking ?ClickTrackingSettings
open_tracking ?OpenTrackingSettings
subscription_tracking ?SubscriptionTrackingSettings
ganalytics ?GoogleAnalyticsSettings
}
pub struct Email {
pub mut:
personalizations []Personalizations @[required]
from Recipient @[required]
subject string @[required]
content []Content @[required]
reply_to ?Recipient
reply_to_list ?[]Recipient
attachments ?[]Attachment
template_id ?string
headers ?map[string]string
categories ?[]string
custom_args ?string
send_at ?i64
batch_id ?string
asm_ ?UnsubscribeGroups @[json: 'asm']
ip_pool_name ?string
mail_settings ?MailSettings
tracking_settings ?TrackingSettings
}
pub fn (mut e Email) add_personalization(personalizations []Personalizations) {
e.personalizations << personalizations
}
pub fn (mut e Email) add_content(content []Content) {
e.content << content
}
pub fn (mut e Email) add_headers(headers map[string]string) {
e.headers or {
e.headers = headers.clone()
return
}
for k, v in headers {
e.headers[k] = v
}
}
pub fn new_email(to []string, from string, subject string, content string) Email {
mut recipients := []Recipient{}
for email in to {
recipients << Recipient{
email: email
}
}
personalization := Personalizations{
to: recipients
}
return Email{
personalizations: [personalization]
from: Recipient{
email: from
}
subject: subject
content: [Content{
value: content
}]
}
}

View File

@@ -0,0 +1,102 @@
module sendgrid
@[params]
pub struct Personalizations {
pub mut:
to []Recipient @[required]
from ?Recipient
cc ?[]Recipient
bcc ?[]Recipient
subject ?string
headers ?map[string]string
substitutions ?map[string]string
dynamic_template_data ?map[string]string
custom_args ?map[string]string
send_at ?i64
}
// add_to adds a list of recipients to which this email should be sent.
fn (mut p Personalizations) add_to(r []Recipient) {
p.to << r
}
// set_from assigns the from field in the email.
fn (mut p Personalizations) set_from(r Recipient) {
p.from = r
}
// add_cc adds an array of recipients who will receive a copy of your email.
fn (mut p Personalizations) add_cc(r []Recipient) {
p.cc or {
p.cc = r
return
}
for item in r {
p.cc << item
}
}
// set_subject assigns the subject of the email.
fn (mut p Personalizations) set_subject(s string) {
p.subject = s
}
// add_headers adds a playbook of key/value pairs to specify handling instructions for your email.
// if some of the new headers already existed, their values are overwritten.
fn (mut p Personalizations) add_headers(new_headers map[string]string) {
p.headers or {
p.headers = new_headers.clone()
return
}
for k, v in new_headers {
p.headers[k] = v
}
}
// add_substitution adds a playbook of key/value pairs to allow you to insert data without using Dynamic Transactional Templates.
// if some of the keys already existed, their values are overwritten.
fn (mut p Personalizations) add_substitution(new_subs map[string]string) {
p.substitutions or {
p.substitutions = new_subs.clone()
return
}
for k, v in new_subs {
p.substitutions[k] = v
}
}
// add_dynamic_template_data adds a playbook of key/value pairs to dynamic template data.
// Dynamic template data is available using Handlebars syntax in Dynamic Transactional Templates.
// if some of the keys already existed, their values are overwritten.
fn (mut p Personalizations) add_dynamic_template_data(new_dynamic_template_data map[string]string) {
p.dynamic_template_data or {
p.dynamic_template_data = new_dynamic_template_data.clone()
return
}
for k, v in new_dynamic_template_data {
p.dynamic_template_data[k] = v
}
}
// add_custom_args adds a playbook of key/value pairs to custom_args.
// custom args are values that are specific to this personalization that will be carried along with the email and its activity data.
// if some of the keys already existed, their values are overwritten.
fn (mut p Personalizations) add_custom_args(new_custom_args map[string]string) {
p.custom_args or {
p.custom_args = new_custom_args.clone()
return
}
for k, v in new_custom_args {
p.custom_args[k] = v
}
}
// set_send_at specifies when your email should be delivered. scheduling delivery more than 72 hours in advance is forbidden.
fn (mut p Personalizations) set_send_at(send_at i64) {
p.send_at = send_at
}

26
lib/clients/zdb/readme.md Normal file
View File

@@ -0,0 +1,26 @@
## Vlang ZDB Client
to use:
- build zero db from source: https://github.com/threefoldtech/0-db
- run zero db from root of 0db folder:
`./zdbd/zdb --help || true` for more info
## to use test
```bash
#must set unix domain with --socket argument when running zdb
#run zdb as following:
mkdir -p ~/.zdb
zdb --socket ~/.zdb/socket --admin 1234
redis-cli -s ~/.zdb/socket
#or easier:
redis-cli -s ~/.zdb/socket --raw nsinfo default
```
then in the redis-cli can do e.g.
```
nsinfo default
```

229
lib/clients/zdb/zdb.v Normal file
View File

@@ -0,0 +1,229 @@
module zdb
import freeflowuniverse.herolib.clients.redisclient
import freeflowuniverse.herolib.ui.console
pub struct ZDB {
pub mut:
redis redisclient.Redis
}
// https://redis.io/topics/protocol
// examples:
// localhost:6379
// /tmp/redis-default.sock
pub fn get(addr string, auth string, namespace string) !ZDB {
console.print_header(' ZDB get: addr:${addr} namespace:${namespace}')
mut redis := redisclient.get(addr)!
mut zdb := ZDB{
redis: redis
}
if auth != '' {
zdb.redis.send_expect_ok(['AUTH', auth])!
}
if namespace != '' {
mut namespaces := zdb.redis.send_expect_list_str(['NSLIST'])!
namespaces.map(it.to_lower())
if namespace.to_lower() !in namespaces {
zdb.redis.send_expect_ok(['NSNEW', namespace])!
}
}
return zdb
}
pub fn (mut zdb ZDB) ping() !string {
return zdb.redis.send_expect_str(['PING'])!
}
// if key not specified will get incremental key
pub fn (mut zdb ZDB) set(key string, val string) !string {
return zdb.redis.send_expect_str(['SET', key, val])!
}
pub fn (mut zdb ZDB) get(key string) !string {
return zdb.redis.send_expect_str(['GET', key])!
}
pub fn (mut zdb ZDB) mget(key string) !string {
return zdb.redis.send_expect_str(['GET', key])!
}
pub fn (mut zdb ZDB) del(key string) !string {
return zdb.redis.send_expect_str(['DEL', key])!
}
// used only for debugging, to check memory leaks
pub fn (mut zdb ZDB) stop() !string {
return zdb.redis.send_expect_str(['STOP'])!
}
pub fn (mut zdb ZDB) exists(key string) !string {
return zdb.redis.send_expect_str(['EXISTS', key])!
}
pub fn (mut zdb ZDB) check(key string) !string {
return zdb.redis.send_expect_str(['CHECK', key])!
}
pub fn (mut zdb ZDB) keycur(key string) !string {
return zdb.redis.send_expect_str(['KEYCUR', key])!
}
pub fn (mut zdb ZDB) info() !string {
i := zdb.redis.send_expect_str(['INFO'])!
return i
}
pub fn (mut zdb ZDB) nsnew(namespace string) !string {
i := zdb.redis.send_expect_str(['NSNEW', namespace])!
return i
}
pub fn (mut zdb ZDB) nsdel(namespace string) !string {
i := zdb.redis.send_expect_str(['NSDEL', namespace])!
return i
}
pub fn (mut zdb ZDB) nsinfo(namespace string) !map[string]string {
i := zdb.redis.send_expect_str(['NSINFO', namespace])!
mut res := map[string]string{}
for line in i.split_into_lines() {
if line.starts_with('#') {
continue
}
if !(line.contains(':')) {
continue
}
splitted := line.split(':')
key := splitted[0]
val := splitted[1]
res[key.trim_space()] = val.trim_space()
}
return res
}
pub fn (mut zdb ZDB) nslist() ![]string {
i := zdb.redis.send_expect_list_str(['NSLIST'])!
return i
}
pub fn (mut zdb ZDB) nssset(ns string, prop string, val string) !string {
i := zdb.redis.send_expect_str(['NSSET', ns, prop, val])!
return i
}
struct SelectArgs {
namespace string
password string
}
pub fn (mut zdb ZDB) select_ns(args SelectArgs) !string {
mut redis_args := ['SELECT', args.namespace]
if args.password != '' {
redis_args << 'SECURE'
redis_args << args.password
}
i := zdb.redis.send_expect_str(redis_args)!
return i
}
pub fn (mut zdb ZDB) dbsize() !string {
i := zdb.redis.send_expect_str(['DBSIZE'])!
return i
}
pub fn (mut zdb ZDB) time() !string {
i := zdb.redis.send_expect_str(['TIME'])!
return i
}
pub fn (mut zdb ZDB) auth(password string) !string {
i := zdb.redis.send_expect_str(['AUTH', password])!
return i
}
pub fn (mut zdb ZDB) auth_secure() !string {
i := zdb.redis.send_expect_str(['AUTH', 'SECURE'])!
return i
}
pub struct ScanArgs {
cursor string
}
pub fn (mut zdb ZDB) scan(args ScanArgs) !string {
mut redis_args := ['SCAN']
if args.cursor != '' {
redis_args << args.cursor
}
i := zdb.redis.send_expect_str(redis_args)!
return i
}
// this is just an alias for SCAN
pub fn (mut zdb ZDB) scanx(args ScanArgs) !string {
mut redis_args := ['SCANX']
if args.cursor != '' {
redis_args << args.cursor
}
i := zdb.redis.send_expect_str(redis_args)!
return i
}
pub fn (mut zdb ZDB) rscan(args ScanArgs) !string {
mut redis_args := ['RSCAN']
if args.cursor != '' {
redis_args << args.cursor
}
i := zdb.redis.send_expect_str(redis_args)!
return i
}
struct WaitArgs {
cmd string
timeout string = '5'
}
pub fn (mut zdb ZDB) wait(args WaitArgs) !string {
i := zdb.redis.send_expect_str(['WAIT', args.cmd, args.timeout])!
return i
}
struct HistoryArgs {
key string
bin_data string
}
pub fn (mut zdb ZDB) history(args HistoryArgs) ![]string {
mut redis_args := ['HISTORY', args.key]
if args.bin_data != '' {
redis_args << args.bin_data
}
i := zdb.redis.send_expect_list_str(redis_args)!
return i
}
pub fn (mut zdb ZDB) flush() !string {
i := zdb.redis.send_expect_str(['FLUSH'])!
return i
}
pub fn (mut zdb ZDB) hooks() ![]string {
i := zdb.redis.send_expect_list_str(['HOOKS'])!
return i
}
pub fn (mut zdb ZDB) index_dirty() ![]string {
i := zdb.redis.send_expect_list_str(['INDEX DIRTY'])!
return i
}
pub fn (mut zdb ZDB) index_dirty_reset() !string {
i := zdb.redis.send_expect_str(['INDEX DIRTY RESET'])!
return i
}

View File

@@ -0,0 +1,19 @@
module zdb
// TODO: enable this test when we have running zdb in ci also implement missing tests
fn test_get() {
// // must set unix domain with --socket argument when running zdb
// // run zdb as following:
// // mkdir -p ~/.zdb/ && zdb --socket ~/.zdb/socket --admin 1234
// mut zdb := get('~/.zdb/socket', '1234', 'test')!
// // check info returns info about zdb
// info := zdb.info()!
// assert info.contains('server_name: 0-db')
// nslist := zdb.nslist()!
// assert nslist == ['default', 'test']
// nsinfo := zdb.nsinfo('default')!
// assert 'name: default' in nsinfo
}