the base
This commit is contained in:
8
lib/clients/httpconnection/authentication.v
Normal file
8
lib/clients/httpconnection/authentication.v
Normal 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}')
|
||||
}
|
||||
100
lib/clients/httpconnection/caching.v
Normal file
100
lib/clients/httpconnection/caching.v
Normal 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)!
|
||||
}
|
||||
}
|
||||
22
lib/clients/httpconnection/connection.v
Normal file
22
lib/clients/httpconnection/connection.v
Normal 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)
|
||||
}
|
||||
|
||||
212
lib/clients/httpconnection/connection_methods.v
Normal file
212
lib/clients/httpconnection/connection_methods.v
Normal 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)!
|
||||
}
|
||||
22
lib/clients/httpconnection/connection_methods_generic.v
Normal file
22
lib/clients/httpconnection/connection_methods_generic.v
Normal 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
|
||||
}
|
||||
39
lib/clients/httpconnection/factory.v
Normal file
39
lib/clients/httpconnection/factory.v
Normal 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
|
||||
|
||||
}
|
||||
|
||||
171
lib/clients/httpconnection/readme.md
Normal file
171
lib/clients/httpconnection/readme.md
Normal 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
|
||||
}
|
||||
}
|
||||
25
lib/clients/httpconnection/request.v
Normal file
25
lib/clients/httpconnection/request.v
Normal 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
|
||||
}
|
||||
7
lib/clients/mailclient/.heroscript
Normal file
7
lib/clients/mailclient/.heroscript
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
!!hero_code.generate_client
|
||||
name:'mailclient'
|
||||
classname:'MailClient'
|
||||
singleton:0
|
||||
default:1
|
||||
reset:0
|
||||
71
lib/clients/mailclient/client.v
Normal file
71
lib/clients/mailclient/client.v
Normal 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)
|
||||
}
|
||||
107
lib/clients/mailclient/mailclient_factory.v
Normal file
107
lib/clients/mailclient/mailclient_factory.v
Normal 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)!
|
||||
}
|
||||
}
|
||||
}
|
||||
70
lib/clients/mailclient/mailclient_model.v
Normal file
70
lib/clients/mailclient/mailclient_model.v
Normal 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
|
||||
}
|
||||
50
lib/clients/mailclient/readme.md
Normal file
50
lib/clients/mailclient/readme.md
Normal 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
|
||||
7
lib/clients/meilisearch/.heroscript
Normal file
7
lib/clients/meilisearch/.heroscript
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
!!hero_code.generate_client
|
||||
name:'meilisearch'
|
||||
classname:'MeilisearchClient'
|
||||
singleton:0
|
||||
default:1
|
||||
reset:0
|
||||
457
lib/clients/meilisearch/client.v
Normal file
457
lib/clients/meilisearch/client.v
Normal 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)
|
||||
}
|
||||
287
lib/clients/meilisearch/document_test.v
Normal file
287
lib/clients/meilisearch/document_test.v
Normal 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
|
||||
}
|
||||
236
lib/clients/meilisearch/documents.v
Normal file
236
lib/clients/meilisearch/documents.v
Normal 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)
|
||||
}
|
||||
86
lib/clients/meilisearch/index_test.v
Executable file
86
lib/clients/meilisearch/index_test.v
Executable 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
|
||||
}
|
||||
11
lib/clients/meilisearch/meilisearch.code-workspace
Normal file
11
lib/clients/meilisearch/meilisearch.code-workspace
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../../../herolib/clients/httpconnection"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
104
lib/clients/meilisearch/meilisearch_factory_.v
Normal file
104
lib/clients/meilisearch/meilisearch_factory_.v
Normal 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
|
||||
}
|
||||
59
lib/clients/meilisearch/meilisearch_model.v
Normal file
59
lib/clients/meilisearch/meilisearch_model.v
Normal 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
|
||||
}
|
||||
166
lib/clients/meilisearch/models.v
Normal file
166
lib/clients/meilisearch/models.v
Normal 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']
|
||||
}
|
||||
59
lib/clients/meilisearch/readme.md
Normal file
59
lib/clients/meilisearch/readme.md
Normal 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.
|
||||
109
lib/clients/mycelium/mycelium.v
Normal file
109
lib/clients/mycelium/mycelium.v
Normal 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()
|
||||
}
|
||||
23
lib/clients/openai/actions.v
Normal file
23
lib/clients/openai/actions.v
Normal 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
110
lib/clients/openai/audio.v
Normal 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)!
|
||||
}
|
||||
70
lib/clients/openai/completions.v
Normal file
70
lib/clients/openai/completions.v
Normal 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
|
||||
}
|
||||
54
lib/clients/openai/embeddings.v
Normal file
54
lib/clients/openai/embeddings.v
Normal 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)!
|
||||
}
|
||||
64
lib/clients/openai/factory.v
Normal file
64
lib/clients/openai/factory.v
Normal 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)!
|
||||
}
|
||||
90
lib/clients/openai/files.v
Normal file
90
lib/clients/openai/files.v
Normal 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
|
||||
}
|
||||
93
lib/clients/openai/fine_tunes.v
Normal file
93
lib/clients/openai/fine_tunes.v
Normal 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
189
lib/clients/openai/images.v
Normal 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)!
|
||||
}
|
||||
75
lib/clients/openai/model_enums.v
Normal file
75
lib/clients/openai/model_enums.v
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
46
lib/clients/openai/models.v
Normal file
46
lib/clients/openai/models.v
Normal 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)!
|
||||
}
|
||||
80
lib/clients/openai/moderation.v
Normal file
80
lib/clients/openai/moderation.v
Normal 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)!
|
||||
}
|
||||
50
lib/clients/openai/readme.md
Normal file
50
lib/clients/openai/readme.md
Normal 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)!
|
||||
```
|
||||
56
lib/clients/postgres/client.v
Normal file
56
lib/clients/postgres/client.v
Normal 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
107
lib/clients/postgres/cmds.v
Normal 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)!
|
||||
}
|
||||
}
|
||||
91
lib/clients/postgres/configure.v
Normal file
91
lib/clients/postgres/configure.v
Normal 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)!
|
||||
// }
|
||||
// }
|
||||
29
lib/clients/postgres/factory.v
Normal file
29
lib/clients/postgres/factory.v
Normal 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
|
||||
}
|
||||
76
lib/clients/postgres/readme.md
Normal file
76
lib/clients/postgres/readme.md
Normal 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)
|
||||
58
lib/clients/redisclient/factory.v
Normal file
58
lib/clients/redisclient/factory.v
Normal 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
|
||||
//}
|
||||
}
|
||||
19
lib/clients/redisclient/readme.md
Normal file
19
lib/clients/redisclient/readme.md
Normal 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/
|
||||
|
||||
57
lib/clients/redisclient/rediscache.v
Normal file
57
lib/clients/redisclient/rediscache.v
Normal 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)!
|
||||
}
|
||||
}
|
||||
305
lib/clients/redisclient/redisclient_commands.v
Normal file
305
lib/clients/redisclient/redisclient_commands.v
Normal 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
|
||||
}
|
||||
24
lib/clients/redisclient/redisclient_core.v
Normal file
24
lib/clients/redisclient/redisclient_core.v
Normal 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
|
||||
}
|
||||
173
lib/clients/redisclient/redisclient_encode.v
Normal file
173
lib/clients/redisclient/redisclient_encode.v
Normal 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
|
||||
}
|
||||
121
lib/clients/redisclient/redisclient_internal.v
Normal file
121
lib/clients/redisclient/redisclient_internal.v
Normal 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)!
|
||||
// }
|
||||
}
|
||||
41
lib/clients/redisclient/redisclient_queue.v
Normal file
41
lib/clients/redisclient/redisclient_queue.v
Normal 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)!
|
||||
}
|
||||
136
lib/clients/redisclient/redisclient_rpc.v
Normal file
136
lib/clients/redisclient/redisclient_rpc.v
Normal 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)!
|
||||
}
|
||||
25
lib/clients/redisclient/redisclient_sadd_test.v
Normal file
25
lib/clients/redisclient/redisclient_sadd_test.v
Normal 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]
|
||||
}
|
||||
6
lib/clients/redisclient/redisclient_script.v
Normal file
6
lib/clients/redisclient/redisclient_script.v
Normal 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])!
|
||||
}
|
||||
55
lib/clients/redisclient/redisclient_send.v
Normal file
55
lib/clients/redisclient/redisclient_send.v
Normal 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)
|
||||
}
|
||||
864
lib/clients/redisclient/redisclient_test.v
Normal file
864
lib/clients/redisclient/redisclient_test.v
Normal 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
|
||||
}
|
||||
} == ''
|
||||
}
|
||||
33
lib/clients/redisclient/rpc_test.v
Normal file
33
lib/clients/redisclient/rpc_test.v
Normal 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'
|
||||
}
|
||||
53
lib/clients/sendgrid/README.md
Normal file
53
lib/clients/sendgrid/README.md
Normal 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/)
|
||||
41
lib/clients/sendgrid/client.v
Normal file
41
lib/clients/sendgrid/client.v
Normal 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)
|
||||
}
|
||||
}
|
||||
152
lib/clients/sendgrid/email.v
Normal file
152
lib/clients/sendgrid/email.v
Normal 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
|
||||
}]
|
||||
}
|
||||
}
|
||||
102
lib/clients/sendgrid/personalizations.v
Normal file
102
lib/clients/sendgrid/personalizations.v
Normal 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
26
lib/clients/zdb/readme.md
Normal 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
229
lib/clients/zdb/zdb.v
Normal 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
|
||||
}
|
||||
19
lib/clients/zdb/zdb_test.v
Normal file
19
lib/clients/zdb/zdb_test.v
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user