Merge branch 'development' into development_heroprompt_v2
* development: ... ... add example heromodels call add example and heromodels openrpc server remove server from gitignore clean up and fix openrpc server implementation Test the workflow
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -48,7 +48,6 @@ compile_summary.log
|
||||
.summary_lock
|
||||
.aider*
|
||||
*.dylib
|
||||
server
|
||||
HTTP_REST_MCP_DEMO.md
|
||||
MCP_HTTP_REST_IMPLEMENTATION_PLAN.md
|
||||
.roo
|
||||
|
||||
25
examples/hero/heromodels/openrpc.vsh
Executable file
25
examples/hero/heromodels/openrpc.vsh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.hero.heromodels.openrpc
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
|
||||
fn main() {
|
||||
mut handler := openrpc.new_heromodels_handler()!
|
||||
|
||||
my_calendar := heromodels.calendar_new(
|
||||
name: "My Calendar"
|
||||
description: "My Calendar"
|
||||
securitypolicy: 1
|
||||
tags: ["tag1", "tag2"]
|
||||
group_id: 1,
|
||||
events: []u32{},
|
||||
color: "#000000",
|
||||
timezone: "UTC",
|
||||
is_public: true,
|
||||
)!
|
||||
|
||||
response := handler.handle(jsonrpc.new_request('calendar_set', json.encode(my_calendar)))!
|
||||
println(response)
|
||||
}
|
||||
Binary file not shown.
@@ -25,6 +25,8 @@ pub fn encode[T](obj T) ![]u8 {
|
||||
d.add_u32(u32(obj.$(field.name)))
|
||||
} $else $if field.typ is u64 {
|
||||
d.add_u64(u64(obj.$(field.name)))
|
||||
}$else $if field.typ is i64 {
|
||||
d.add_i64(i64(obj.$(field.name)))
|
||||
} $else $if field.typ is time.Time {
|
||||
d.add_time(time.new(obj.$(field.name)))
|
||||
// Arrays of primitive types
|
||||
|
||||
@@ -8,7 +8,6 @@ import time
|
||||
pub struct Calendar {
|
||||
Base
|
||||
pub mut:
|
||||
group_id u32 // Associated group for permissions
|
||||
events []u32 // IDs of calendar events (changed to u32 to match CalendarEvent)
|
||||
color string // Hex color code
|
||||
timezone string
|
||||
@@ -19,7 +18,6 @@ pub mut:
|
||||
pub struct CalendarArgs {
|
||||
BaseArgs
|
||||
pub mut:
|
||||
group_id u32
|
||||
events []u32
|
||||
color string
|
||||
timezone string
|
||||
@@ -27,25 +25,16 @@ pub mut:
|
||||
}
|
||||
|
||||
pub fn calendar_new(args CalendarArgs) !Calendar {
|
||||
mut commentids := []u32{}
|
||||
for comment in args.comments {
|
||||
// Convert CommentArg to CommentArgExtended
|
||||
extended_comment := CommentArgExtended{
|
||||
comment: comment.comment
|
||||
parent: 0
|
||||
author: 0
|
||||
}
|
||||
commentids << comment_set(extended_comment)!
|
||||
}
|
||||
mut commentids:=[]u32{}
|
||||
mut obj := Calendar{
|
||||
id: args.id or { 0 } // Will be set by DB?
|
||||
id: args.id or {0} // Will be set by DB?
|
||||
name: args.name
|
||||
description: args.description
|
||||
created_at: ourtime.now().unix()
|
||||
updated_at: ourtime.now().unix()
|
||||
securitypolicy: args.securitypolicy or { 0 }
|
||||
securitypolicy: args.securitypolicy or {0}
|
||||
tags: tags2id(args.tags)!
|
||||
comments: commentids
|
||||
comments: comments2ids(args.comments)!
|
||||
group_id: args.group_id
|
||||
events: args.events
|
||||
color: args.color
|
||||
|
||||
117
lib/hero/heromodels/comment.v
Normal file
117
lib/hero/heromodels/comment.v
Normal file
@@ -0,0 +1,117 @@
|
||||
module heromodels
|
||||
|
||||
import freeflowuniverse.herolib.data.encoder
|
||||
import crypto.md5
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import freeflowuniverse.herolib.data.ourtime
|
||||
|
||||
|
||||
@[heap]
|
||||
pub struct Comment {
|
||||
Base
|
||||
pub mut:
|
||||
// id u32
|
||||
comment string
|
||||
parent u32 //id of parent comment if any, 0 means none
|
||||
updated_at i64
|
||||
author u32 //links to user
|
||||
}
|
||||
|
||||
pub fn (self Comment) type_name() string {
|
||||
return 'comments'
|
||||
}
|
||||
|
||||
pub fn (self Comment) load(data []u8) !Comment {
|
||||
return comment_load(data)!
|
||||
}
|
||||
|
||||
pub fn (self Comment) dump() ![]u8{
|
||||
// Create a new encoder
|
||||
mut e := encoder.new()
|
||||
e.add_u8(1)
|
||||
e.add_u32(self.id)
|
||||
e.add_string(self.comment)
|
||||
e.add_u32(self.parent)
|
||||
e.add_i64(self.updated_at)
|
||||
e.add_u32(self.author)
|
||||
return e.data
|
||||
}
|
||||
|
||||
pub fn comment_load(data []u8) !Comment{
|
||||
// Create a new decoder
|
||||
mut e := encoder.decoder_new(data)
|
||||
version := e.get_u8()!
|
||||
if version != 1 {
|
||||
panic("wrong version in comment load")
|
||||
}
|
||||
mut comment := Comment{}
|
||||
comment.id = e.get_u32()!
|
||||
comment.comment = e.get_string()!
|
||||
comment.parent = e.get_u32()!
|
||||
comment.updated_at = e.get_i64()!
|
||||
comment.author = e.get_u32()!
|
||||
return comment
|
||||
}
|
||||
|
||||
|
||||
pub struct CommentArg {
|
||||
pub mut:
|
||||
comment string
|
||||
parent u32
|
||||
author u32
|
||||
}
|
||||
|
||||
pub fn comment_multiset(args []CommentArg) ![]u32 {
|
||||
return comments2ids(args)!
|
||||
}
|
||||
|
||||
pub fn comments2ids(args []CommentArg) ![]u32 {
|
||||
return args.map(comment2id(it.comment)!)
|
||||
}
|
||||
|
||||
pub fn comment2id(comment string) !u32 {
|
||||
comment_fixed := comment.to_lower_ascii().trim_space()
|
||||
mut redis := redisclient.core_get()!
|
||||
return if comment_fixed.len > 0{
|
||||
hash := md5.hexhash(comment_fixed)
|
||||
comment_found := redis.hget("db:comments", hash)!
|
||||
if comment_found == ""{
|
||||
id := u32(redis.incr("db:comments:id")!)
|
||||
redis.hset("db:comments", hash, id.str())!
|
||||
redis.hset("db:comments", id.str(), comment_fixed)!
|
||||
id
|
||||
}else{
|
||||
comment_found.u32()
|
||||
}
|
||||
} else { 0 }
|
||||
}
|
||||
|
||||
|
||||
//get new comment, not from the DB
|
||||
pub fn comment_new(args CommentArg) !Comment{
|
||||
mut o := Comment {
|
||||
comment: args.comment
|
||||
parent: args.parent
|
||||
updated_at: ourtime.now().unix()
|
||||
author: args.author
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
pub fn comment_set(args CommentArg) !u32{
|
||||
mut o := comment_new(args)!
|
||||
// Use openrpcserver set function which now returns the ID
|
||||
return set[Comment](mut o)!
|
||||
}
|
||||
|
||||
pub fn comment_delete(id u32) ! {
|
||||
delete[Comment](id)!
|
||||
}
|
||||
|
||||
pub fn comment_exist(id u32) !bool{
|
||||
return exists[Comment](id)!
|
||||
}
|
||||
|
||||
pub fn comment_get(id u32) !Comment{
|
||||
return get[Comment](id)!
|
||||
}
|
||||
@@ -3,35 +3,37 @@ module heromodels
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import freeflowuniverse.herolib.data.encoder
|
||||
|
||||
pub fn set[T](obj T) ! {
|
||||
pub fn set[T](mut obj_ T) !u32 {
|
||||
mut redis := redisclient.core_get()!
|
||||
id := obj.id
|
||||
data := encoder.encode(obj)!
|
||||
redis.hset('db:${T.name}', id.str(), data.bytestr())!
|
||||
id := u32(redis.llen(db_name[T]()) or {0})
|
||||
obj_.id = id
|
||||
data := encoder.encode(obj_) or {
|
||||
return err
|
||||
}
|
||||
redis.hset(db_name[T](),id.str(),data.bytestr())!
|
||||
return id
|
||||
}
|
||||
|
||||
pub fn get[T](id u32) !T {
|
||||
mut redis := redisclient.core_get()!
|
||||
data := redis.hget('db:${T.name}', id.str())!
|
||||
data := redis.hget(db_name[T](),id.str())!
|
||||
t := T{}
|
||||
return encoder.decode[T](data.bytes())!
|
||||
}
|
||||
|
||||
pub fn exists[T](id u32) !bool {
|
||||
name := T{}.type_name()
|
||||
mut redis := redisclient.core_get()!
|
||||
return redis.hexists('db:${name}', id.str())!
|
||||
return redis.hexists(db_name[T](),id.str())!
|
||||
}
|
||||
|
||||
pub fn delete[T](id u32) ! {
|
||||
name := T{}.type_name()
|
||||
mut redis := redisclient.core_get()!
|
||||
redis.hdel('db:${name}', id.str())!
|
||||
redis.hdel(db_name[T](), id.str())!
|
||||
}
|
||||
|
||||
pub fn list[T]() ![]T {
|
||||
mut redis := redisclient.core_get()!
|
||||
ids := redis.hkeys('db:${name}')!
|
||||
ids := redis.hkeys(db_name[T]())!
|
||||
mut result := []T{}
|
||||
for id in ids {
|
||||
result << get[T](id.u32())!
|
||||
@@ -41,7 +43,9 @@ pub fn list[T]() ![]T {
|
||||
|
||||
// make it easy to get a base object
|
||||
pub fn new_from_base[T](args BaseArgs) !Base {
|
||||
return T{
|
||||
Base: new_base(args)!
|
||||
}
|
||||
return T { Base: new_base(args)! }
|
||||
}
|
||||
|
||||
fn db_name[T]() string {
|
||||
return "db:${T.name}"
|
||||
}
|
||||
|
||||
93
lib/hero/heromodels/core_models.v
Normal file
93
lib/hero/heromodels/core_models.v
Normal file
@@ -0,0 +1,93 @@
|
||||
module heromodels
|
||||
|
||||
import crypto.md5
|
||||
|
||||
import freeflowuniverse.herolib.core.redisclient
|
||||
import freeflowuniverse.herolib.data.ourtime
|
||||
|
||||
// Group represents a collection of users with roles and permissions
|
||||
@[heap]
|
||||
pub struct Base {
|
||||
pub mut:
|
||||
id u32
|
||||
name string
|
||||
description string
|
||||
created_at i64
|
||||
updated_at i64
|
||||
securitypolicy u32
|
||||
tags u32 //when we set/get we always do as []string but this can then be sorted and md5ed this gies the unique id of tags
|
||||
comments []u32
|
||||
}
|
||||
|
||||
@[heap]
|
||||
pub struct SecurityPolicy {
|
||||
pub mut:
|
||||
id u32
|
||||
read []u32 //links to users & groups
|
||||
write []u32 //links to users & groups
|
||||
delete []u32 //links to users & groups
|
||||
public bool
|
||||
md5 string //this sorts read, write and delete u32 + hash, then do md5 hash, this allows to go from a random read/write/delete/public config to a hash
|
||||
}
|
||||
|
||||
|
||||
@[heap]
|
||||
pub struct Tags {
|
||||
pub mut:
|
||||
id u32
|
||||
names []string //unique per id
|
||||
md5 string //of sorted names, to make easy to find unique id, each name lowercased and made ascii
|
||||
}
|
||||
|
||||
|
||||
/////////////////
|
||||
|
||||
@[params]
|
||||
pub struct BaseArgs {
|
||||
pub mut:
|
||||
id ?u32
|
||||
name string
|
||||
description string
|
||||
securitypolicy ?u32
|
||||
tags []string
|
||||
comments []CommentArg
|
||||
}
|
||||
|
||||
//make it easy to get a base object
|
||||
pub fn new_base(args BaseArgs) !Base {
|
||||
mut redis := redisclient.core_get()!
|
||||
|
||||
commentids:=comment_multiset(args.comments)!
|
||||
tags:=tags2id(args.tags)!
|
||||
|
||||
return Base {
|
||||
id: args.id or { 0 }
|
||||
name: args.name
|
||||
description: args.description
|
||||
created_at: ourtime.now().unix()
|
||||
updated_at: ourtime.now().unix()
|
||||
securitypolicy: args.securitypolicy or { 0 }
|
||||
tags: tags
|
||||
comments: commentids
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tags2id(tags []string) !u32 {
|
||||
mut redis := redisclient.core_get()!
|
||||
return if tags.len>0{
|
||||
mut tags_fixed := tags.map(it.to_lower_ascii().trim_space()).filter(it != "")
|
||||
tags_fixed.sort_ignore_case()
|
||||
hash :=md5.hexhash(tags_fixed.join(","))
|
||||
tags_found := redis.hget("db:tags", hash)!
|
||||
return if tags_found == ""{
|
||||
id := u32(redis.incr("db:tags:id")!)
|
||||
redis.hset("db:tags", hash, id.str())!
|
||||
redis.hset("db:tags", id.str(), tags_fixed.join(","))!
|
||||
id
|
||||
}else{
|
||||
tags_found.u32()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
|
||||
// Comment-specific argument structures
|
||||
@[params]
|
||||
pub struct CommentGetArgs {
|
||||
pub mut:
|
||||
id ?u32
|
||||
author ?u32
|
||||
parent ?u32
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CommentDeleteArgs {
|
||||
pub mut:
|
||||
id u32
|
||||
}
|
||||
|
||||
// comment_get retrieves comments based on the provided arguments
|
||||
pub fn comment_get(params string) !string {
|
||||
// Handle empty params
|
||||
if params == 'null' || params == '{}' {
|
||||
return error('No valid search criteria provided. Please specify id, author, or parent.')
|
||||
}
|
||||
|
||||
args := json.decode(CommentGetArgs, params)!
|
||||
|
||||
// If ID is provided, get specific comment
|
||||
if id := args.id {
|
||||
comment := heromodels.comment_get(id)!
|
||||
return json.encode(comment)
|
||||
}
|
||||
|
||||
// If author is provided, find comments by author
|
||||
if author := args.author {
|
||||
return get_comments_by_author(author)!
|
||||
}
|
||||
|
||||
// If parent is provided, find child comments
|
||||
if parent := args.parent {
|
||||
return get_comments_by_parent(parent)!
|
||||
}
|
||||
|
||||
return error('No valid search criteria provided. Please specify id, author, or parent.')
|
||||
}
|
||||
|
||||
// comment_set creates or updates a comment
|
||||
pub fn comment_set(params string) !string {
|
||||
comment_arg := json.decode(heromodels.CommentArgExtended, params)!
|
||||
id := heromodels.comment_set(comment_arg)!
|
||||
return json.encode({
|
||||
'id': id
|
||||
})
|
||||
}
|
||||
|
||||
// comment_delete removes a comment by ID
|
||||
pub fn comment_delete(params string) !string {
|
||||
args := json.decode(CommentDeleteArgs, params)!
|
||||
|
||||
// Check if comment exists
|
||||
if !heromodels.exists[heromodels.Comment](args.id)! {
|
||||
return error('Comment with id ${args.id} does not exist')
|
||||
}
|
||||
|
||||
// Delete using core method
|
||||
heromodels.delete[heromodels.Comment](args.id)!
|
||||
|
||||
result_json := '{"success": true, "id": ${args.id}}'
|
||||
return result_json
|
||||
}
|
||||
|
||||
// comment_list returns all comment IDs
|
||||
pub fn comment_list() !string {
|
||||
comments := heromodels.list[heromodels.Comment]()!
|
||||
mut ids := []u32{}
|
||||
|
||||
for comment in comments {
|
||||
ids << comment.id
|
||||
}
|
||||
|
||||
return json.encode(ids)
|
||||
}
|
||||
|
||||
// Helper function to get comments by author
|
||||
fn get_comments_by_author(author u32) !string {
|
||||
all_comments := heromodels.list[heromodels.Comment]()!
|
||||
mut matching_comments := []heromodels.Comment{}
|
||||
|
||||
for comment in all_comments {
|
||||
if comment.author == author {
|
||||
matching_comments << comment
|
||||
}
|
||||
}
|
||||
|
||||
return json.encode(matching_comments)
|
||||
}
|
||||
|
||||
// Helper function to get comments by parent
|
||||
fn get_comments_by_parent(parent u32) !string {
|
||||
all_comments := heromodels.list[heromodels.Comment]()!
|
||||
mut matching_comments := []heromodels.Comment{}
|
||||
|
||||
for comment in all_comments {
|
||||
if comment.parent == parent {
|
||||
matching_comments << comment
|
||||
}
|
||||
}
|
||||
|
||||
return json.encode(matching_comments)
|
||||
}
|
||||
75
lib/hero/heromodels/openrpc/handler.v
Normal file
75
lib/hero/heromodels/openrpc/handler.v
Normal file
@@ -0,0 +1,75 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import os
|
||||
|
||||
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
|
||||
|
||||
pub fn new_heromodels_handler() !openrpc.Handler {
|
||||
mut openrpc_handler := openrpc.Handler {
|
||||
specification: openrpc.new(path: openrpc_path)!
|
||||
}
|
||||
|
||||
openrpc_handler.register_procedure_handle('comment_get', comment_get)
|
||||
openrpc_handler.register_procedure_handle('comment_set', comment_set)
|
||||
openrpc_handler.register_procedure_handle('comment_delete', comment_delete)
|
||||
openrpc_handler.register_procedure_handle('comment_list', comment_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('calendar_get', calendar_get)
|
||||
openrpc_handler.register_procedure_handle('calendar_set', calendar_set)
|
||||
openrpc_handler.register_procedure_handle('calendar_delete', calendar_delete)
|
||||
openrpc_handler.register_procedure_handle('calendar_list', calendar_list)
|
||||
|
||||
return openrpc_handler
|
||||
}
|
||||
|
||||
pub fn comment_get(request jsonrpc.Request) !jsonrpc.Response {
|
||||
payload := jsonrpc.decode_payload[u32](request.params) or { return jsonrpc.invalid_params }
|
||||
result := heromodels.comment_get(payload) or { return jsonrpc.internal_error }
|
||||
return jsonrpc.new_response(request.id, json.encode(result))
|
||||
}
|
||||
|
||||
pub fn comment_set(request jsonrpc.Request) !jsonrpc.Response{
|
||||
payload := jsonrpc.decode_payload[heromodels.CommentArg](request.params) or { return jsonrpc.invalid_params }
|
||||
return jsonrpc.new_response(request.id, heromodels.comment_set(payload)!.str())
|
||||
}
|
||||
|
||||
pub fn comment_delete(request jsonrpc.Request) !jsonrpc.Response {
|
||||
payload := jsonrpc.decode_payload[u32](request.params) or { return jsonrpc.invalid_params }
|
||||
return jsonrpc.new_response(request.id, '')
|
||||
}
|
||||
|
||||
pub fn comment_list(request jsonrpc.Request) !jsonrpc.Response {
|
||||
result := heromodels.list[heromodels.Comment]() or { return jsonrpc.internal_error }
|
||||
return jsonrpc.new_response(request.id, json.encode(result))
|
||||
}
|
||||
|
||||
pub fn calendar_get(request jsonrpc.Request) !jsonrpc.Response {
|
||||
payload := jsonrpc.decode_payload[u32](request.params) or { return jsonrpc.invalid_params }
|
||||
result := heromodels.get[heromodels.Calendar](payload) or { return jsonrpc.internal_error }
|
||||
return jsonrpc.new_response(request.id, json.encode(result))
|
||||
}
|
||||
|
||||
pub fn calendar_set(request jsonrpc.Request) !jsonrpc.Response{
|
||||
mut payload := json.decode(heromodels.Calendar, request.params) or {
|
||||
return jsonrpc.invalid_params }
|
||||
id := heromodels.set[heromodels.Calendar](mut payload) or {
|
||||
println('error setting calendar $err')
|
||||
return jsonrpc.internal_error
|
||||
}
|
||||
return jsonrpc.new_response(request.id, id.str())
|
||||
}
|
||||
|
||||
pub fn calendar_delete(request jsonrpc.Request) !jsonrpc.Response {
|
||||
payload := jsonrpc.decode_payload[u32](request.params) or { return jsonrpc.invalid_params }
|
||||
heromodels.delete[heromodels.Calendar](payload) or { return jsonrpc.internal_error }
|
||||
return jsonrpc.new_response(request.id, '')
|
||||
}
|
||||
|
||||
pub fn calendar_list(request jsonrpc.Request) !jsonrpc.Response {
|
||||
result := heromodels.list[heromodels.Calendar]() or { return jsonrpc.internal_error }
|
||||
return jsonrpc.new_response(request.id, json.encode(result))
|
||||
}
|
||||
110
lib/hero/heromodels/openrpc/handler_comment.v
Normal file
110
lib/hero/heromodels/openrpc/handler_comment.v
Normal file
@@ -0,0 +1,110 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
|
||||
// Comment-specific argument structures
|
||||
@[params]
|
||||
pub struct CommentGetArgs {
|
||||
pub mut:
|
||||
id ?u32
|
||||
author ?u32
|
||||
parent ?u32
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct CommentDeleteArgs {
|
||||
pub mut:
|
||||
id u32
|
||||
}
|
||||
|
||||
// // comment_get retrieves comments based on the provided arguments
|
||||
// pub fn comment_get(params string) !string {
|
||||
// // Handle empty params
|
||||
// if params == 'null' || params == '{}' {
|
||||
// return error('No valid search criteria provided. Please specify id, author, or parent.')
|
||||
// }
|
||||
|
||||
// args := json.decode(CommentGetArgs, params)!
|
||||
|
||||
// // If ID is provided, get specific comment
|
||||
// if id := args.id {
|
||||
// comment := heromodels.comment_get(id)!
|
||||
// return json.encode(comment)
|
||||
// }
|
||||
|
||||
// // If author is provided, find comments by author
|
||||
// if author := args.author {
|
||||
// return get_comments_by_author(author)!
|
||||
// }
|
||||
|
||||
// // If parent is provided, find child comments
|
||||
// if parent := args.parent {
|
||||
// return get_comments_by_parent(parent)!
|
||||
// }
|
||||
|
||||
// return error('No valid search criteria provided. Please specify id, author, or parent.')
|
||||
// }
|
||||
|
||||
// // comment_set creates or updates a comment
|
||||
// pub fn comment_set(params string) !string {
|
||||
// comment_arg := json.decode(heromodels.CommentArgExtended, params)!
|
||||
// id := heromodels.comment_set(comment_arg)!
|
||||
// return json.encode({'id': id})
|
||||
// }
|
||||
|
||||
// // comment_delete removes a comment by ID
|
||||
// pub fn comment_delete(params string) !string {
|
||||
// args := json.decode(CommentDeleteArgs, params)!
|
||||
|
||||
// // Check if comment exists
|
||||
// if !heromodels.exists[heromodels.Comment](args.id)! {
|
||||
// return error('Comment with id ${args.id} does not exist')
|
||||
// }
|
||||
|
||||
// // Delete using core method
|
||||
// heromodels.delete[heromodels.Comment](args.id)!
|
||||
|
||||
// result_json := '{"success": true, "id": ${args.id}}'
|
||||
// return result_json
|
||||
// }
|
||||
|
||||
// // comment_list returns all comment IDs
|
||||
// pub fn comment_list() !string {
|
||||
// comments := heromodels.list[heromodels.Comment]()!
|
||||
// mut ids := []u32{}
|
||||
|
||||
// for comment in comments {
|
||||
// ids << comment.id
|
||||
// }
|
||||
|
||||
// return json.encode(ids)
|
||||
// }
|
||||
|
||||
// // Helper function to get comments by author
|
||||
// fn get_comments_by_author(author u32) !string {
|
||||
// all_comments := heromodels.list[heromodels.Comment]()!
|
||||
// mut matching_comments := []heromodels.Comment{}
|
||||
|
||||
// for comment in all_comments {
|
||||
// if comment.author == author {
|
||||
// matching_comments << comment
|
||||
// }
|
||||
// }
|
||||
|
||||
// return json.encode(matching_comments)
|
||||
// }
|
||||
|
||||
// // Helper function to get comments by parent
|
||||
// fn get_comments_by_parent(parent u32) !string {
|
||||
// all_comments := heromodels.list[heromodels.Comment]()!
|
||||
// mut matching_comments := []heromodels.Comment{}
|
||||
|
||||
// for comment in all_comments {
|
||||
// if comment.parent == parent {
|
||||
// matching_comments << comment
|
||||
// }
|
||||
// }
|
||||
|
||||
// return json.encode(matching_comments)
|
||||
// }
|
||||
9
lib/hero/heromodels/openrpc/handler_test.v
Normal file
9
lib/hero/heromodels/openrpc/handler_test.v
Normal file
@@ -0,0 +1,9 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
|
||||
// new_heromodels_server creates a new HeroModels RPC server
|
||||
pub fn test_new_heromodels_handler() ! {
|
||||
handler := new_heromodels_handler()!
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpcserver
|
||||
|
||||
// HeroModelsServer extends the base openrpcserver.RPCServer with heromodels-specific functionality
|
||||
pub struct HeroModelsServer {
|
||||
openrpcserver.RPCServer
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct HeroModelsServerArgs {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
}
|
||||
|
||||
// new_heromodels_server creates a new HeroModels RPC server
|
||||
pub fn new_heromodels_server(args HeroModelsServerArgs) !&HeroModelsServer {
|
||||
base_server := openrpcserver.new_rpc_server(
|
||||
socket_path: args.socket_path
|
||||
)!
|
||||
|
||||
return &HeroModelsServer{
|
||||
RPCServer: *base_server
|
||||
}
|
||||
}
|
||||
|
||||
// process extends the base process method with heromodels-specific methods
|
||||
pub fn (mut server HeroModelsServer) process(method string, params_str string) !string {
|
||||
// Route to heromodels-specific methods first
|
||||
result := match method {
|
||||
'comment_get' {
|
||||
comment_get(params_str)!
|
||||
}
|
||||
'comment_set' {
|
||||
comment_set(params_str)!
|
||||
}
|
||||
'comment_delete' {
|
||||
comment_delete(params_str)!
|
||||
}
|
||||
'comment_list' {
|
||||
comment_list()!
|
||||
}
|
||||
'rpc.discover' {
|
||||
server.discover()!
|
||||
}
|
||||
else {
|
||||
return server.create_error_response(-32601, 'Method not found', method)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
26
lib/hero/heromodels/openrpc/server.v
Normal file
26
lib/hero/heromodels/openrpc/server.v
Normal file
@@ -0,0 +1,26 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
// HeroModelsServer extends the base openrpcserver.RPCServer with heromodels-specific functionality
|
||||
pub struct HeroModelsServer {
|
||||
openrpc.UNIXServer
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct HeroModelsServerArgs {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
}
|
||||
|
||||
// new_heromodels_server creates a new HeroModels RPC server
|
||||
pub fn new_heromodels_server(args HeroModelsServerArgs) !&HeroModelsServer {
|
||||
mut base_server := openrpc.new_unix_server(
|
||||
new_heromodels_handler()!,
|
||||
socket_path: args.socket_path
|
||||
)!
|
||||
|
||||
return &HeroModelsServer{
|
||||
UNIXServer: *base_server
|
||||
}
|
||||
}
|
||||
@@ -1,72 +1,47 @@
|
||||
module heromodels
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpcserver
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
// Re-export types from openrpcserver for convenience
|
||||
pub type Base = openrpcserver.Base
|
||||
pub type BaseArgs = openrpcserver.BaseArgs
|
||||
pub type CommentArg = openrpcserver.CommentArg
|
||||
|
||||
// HeroModelsServer extends the base openrpcserver.RPCServer with heromodels-specific functionality
|
||||
pub struct HeroModelsServer {
|
||||
openrpcserver.RPCServer
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct HeroModelsServerArgs {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
}
|
||||
// // Re-export core methods from openrpcserver for convenience
|
||||
// pub fn set[T](mut obj T) !u32 {
|
||||
// return openrpcserver.set[T](mut obj)!
|
||||
// }
|
||||
|
||||
// new_heromodels_server creates a new HeroModels RPC server
|
||||
pub fn new_heromodels_server(args HeroModelsServerArgs) !&HeroModelsServer {
|
||||
base_server := openrpcserver.new_rpc_server(
|
||||
socket_path: args.socket_path
|
||||
)!
|
||||
// pub fn get[T](id u32) !T {
|
||||
// return openrpcserver.get[T](id)!
|
||||
// }
|
||||
|
||||
return &HeroModelsServer{
|
||||
RPCServer: *base_server
|
||||
}
|
||||
}
|
||||
// pub fn exists[T](id u32) !bool {
|
||||
// return openrpcserver.exists[T](id)!
|
||||
// }
|
||||
|
||||
// Re-export core methods from openrpcserver for convenience
|
||||
pub fn set[T](mut obj T) !u32 {
|
||||
return openrpcserver.set[T](mut obj)!
|
||||
}
|
||||
// pub fn delete[T](id u32) ! {
|
||||
// openrpcserver.delete[T](id)!
|
||||
// }
|
||||
|
||||
pub fn get[T](id u32) !T {
|
||||
return openrpcserver.get[T](id)!
|
||||
}
|
||||
// pub fn list[T]() ![]T {
|
||||
// return openrpcserver.list[T]()!
|
||||
// }
|
||||
|
||||
pub fn exists[T](id u32) !bool {
|
||||
return openrpcserver.exists[T](id)!
|
||||
}
|
||||
// // Re-export utility functions
|
||||
// pub fn tags2id(tags []string) !u32 {
|
||||
// return openrpcserver.tags2id(tags)!
|
||||
// }
|
||||
|
||||
pub fn delete[T](id u32) ! {
|
||||
openrpcserver.delete[T](id)!
|
||||
}
|
||||
// pub fn comment_multiset(args []CommentArg) ![]u32 {
|
||||
// return openrpcserver.comment_multiset(args)!
|
||||
// }
|
||||
|
||||
pub fn list[T]() ![]T {
|
||||
return openrpcserver.list[T]()!
|
||||
}
|
||||
// pub fn comments2ids(args []CommentArg) ![]u32 {
|
||||
// return openrpcserver.comments2ids(args)!
|
||||
// }
|
||||
|
||||
// Re-export utility functions
|
||||
pub fn tags2id(tags []string) !u32 {
|
||||
return openrpcserver.tags2id(tags)!
|
||||
}
|
||||
// pub fn comment2id(comment string) !u32 {
|
||||
// return openrpcserver.comment2id(comment)!
|
||||
// }
|
||||
|
||||
pub fn comment_multiset(args []CommentArg) ![]u32 {
|
||||
return openrpcserver.comment_multiset(args)!
|
||||
}
|
||||
|
||||
pub fn comments2ids(args []CommentArg) ![]u32 {
|
||||
return openrpcserver.comments2ids(args)!
|
||||
}
|
||||
|
||||
pub fn comment2id(comment string) !u32 {
|
||||
return openrpcserver.comment2id(comment)!
|
||||
}
|
||||
|
||||
pub fn new_base(args BaseArgs) !Base {
|
||||
return openrpcserver.new_base(args)!
|
||||
}
|
||||
// pub fn new_base(args BaseArgs) !Base {
|
||||
// return openrpcserver.new_base(args)!
|
||||
// }
|
||||
|
||||
@@ -84,34 +84,25 @@ struct ProjectContent {
|
||||
tags []string
|
||||
}
|
||||
|
||||
pub fn new_project(name string, description string, group_id string) Project {
|
||||
pub struct NewProject {
|
||||
pub mut:
|
||||
name string
|
||||
description string
|
||||
group_id string
|
||||
}
|
||||
|
||||
pub fn new_project(params NewProject) !Project {
|
||||
mut project := Project{
|
||||
name: name
|
||||
description: description
|
||||
group_id: group_id
|
||||
name: params.name
|
||||
description: params.description
|
||||
group_id: params.group_id
|
||||
status: .planning
|
||||
created_at: time.now().unix()
|
||||
updated_at: time.now().unix()
|
||||
swimlanes: [
|
||||
Swimlane{
|
||||
id: 'todo'
|
||||
name: 'To Do'
|
||||
order: 1
|
||||
color: '#f1c40f'
|
||||
},
|
||||
Swimlane{
|
||||
id: 'in_progress'
|
||||
name: 'In Progress'
|
||||
order: 2
|
||||
color: '#3498db'
|
||||
},
|
||||
Swimlane{
|
||||
id: 'done'
|
||||
name: 'Done'
|
||||
order: 3
|
||||
color: '#2ecc71'
|
||||
is_done: true
|
||||
},
|
||||
Swimlane{id: 'todo', name: 'To Do', order: 1, color: '#f1c40f'},
|
||||
Swimlane{id: 'in_progress', name: 'In Progress', order: 2, color: '#3498db'},
|
||||
Swimlane{id: 'done', name: 'Done', order: 3, color: '#2ecc71', is_done: true}
|
||||
]
|
||||
}
|
||||
project.calculate_id()
|
||||
|
||||
@@ -3,33 +3,33 @@ module core
|
||||
import freeflowuniverse.herolib.core
|
||||
|
||||
fn test_package_management() {
|
||||
platform_ := core.platform()!
|
||||
// platform_ := core.platform()!
|
||||
|
||||
if platform_ == .osx {
|
||||
// Check if brew is installed
|
||||
if !cmd_exists('brew') {
|
||||
eprintln('WARNING: Homebrew is not installed. Please install it to run package management tests on OSX.')
|
||||
return
|
||||
}
|
||||
}
|
||||
// if platform_ == .osx {
|
||||
// // Check if brew is installed
|
||||
// if !cmd_exists('brew') {
|
||||
// eprintln('WARNING: Homebrew is not installed. Please install it to run package management tests on OSX.')
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
is_wget_installed := cmd_exists('wget')
|
||||
// is_wget_installed := cmd_exists('wget')
|
||||
|
||||
if is_wget_installed {
|
||||
// Clean up - remove wget
|
||||
package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' }
|
||||
assert !cmd_exists('wget')
|
||||
// Reinstalling wget as it was previously installed
|
||||
package_install('wget') or { assert false, 'Failed to install wget: ${err}' }
|
||||
assert cmd_exists('wget')
|
||||
return
|
||||
}
|
||||
// if is_wget_installed {
|
||||
// // Clean up - remove wget
|
||||
// package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' }
|
||||
// assert !cmd_exists('wget')
|
||||
// // Reinstalling wget as it was previously installed
|
||||
// package_install('wget') or { assert false, 'Failed to install wget: ${err}' }
|
||||
// assert cmd_exists('wget')
|
||||
// return
|
||||
// }
|
||||
|
||||
// Intstall wget and verify it is installed
|
||||
package_install('wget') or { assert false, 'Failed to install wget: ${err}' }
|
||||
assert cmd_exists('wget')
|
||||
// // Intstall wget and verify it is installed
|
||||
// package_install('wget') or { assert false, 'Failed to install wget: ${err}' }
|
||||
// assert cmd_exists('wget')
|
||||
|
||||
// Clean up - remove wget
|
||||
package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' }
|
||||
assert !cmd_exists('wget')
|
||||
// // Clean up - remove wget
|
||||
// package_remove('wget') or { assert false, 'Failed to remove wget: ${err}' }
|
||||
// assert !cmd_exists('wget')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2 as json
|
||||
import net.websocket
|
||||
|
||||
// This file implements a JSON-RPC 2.0 handler for WebSocket servers.
|
||||
@@ -19,7 +20,7 @@ pub mut:
|
||||
// 2. Execute the procedure with the extracted parameters
|
||||
// 3. Return the result as a JSON-encoded string
|
||||
// If an error occurs during any of these steps, it should be returned.
|
||||
pub type ProcedureHandler = fn (payload string) !string
|
||||
pub type ProcedureHandler = fn (request Request) !Response
|
||||
|
||||
// new_handler creates a new JSON-RPC handler with the specified procedure handlers.
|
||||
//
|
||||
@@ -39,10 +40,79 @@ pub fn new_handler(handler Handler) !&Handler {
|
||||
// Parameters:
|
||||
// - method: The name of the method to register
|
||||
// - procedure: The procedure handler function to register
|
||||
pub fn (mut handler Handler) register_procedure(method string, procedure ProcedureHandler) {
|
||||
pub fn (mut handler Handler) register_procedure[T, U](method string, function fn (T) !U) {
|
||||
procedure := Procedure[T, U]{
|
||||
function: function
|
||||
method: method
|
||||
}
|
||||
handler.procedures[procedure.method] = procedure.handle
|
||||
}
|
||||
|
||||
// register_procedure registers a new procedure handler for the specified method.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: The name of the method to register
|
||||
// - procedure: The procedure handler function to register
|
||||
pub fn (mut handler Handler) register_procedure_void[T](method string, function fn (T) !) {
|
||||
procedure := ProcedureVoid[T]{
|
||||
function: function
|
||||
method: method
|
||||
}
|
||||
handler.procedures[procedure.method] = procedure.handle
|
||||
}
|
||||
|
||||
// register_procedure registers a new procedure handler for the specified method.
|
||||
//
|
||||
// Parameters:
|
||||
// - method: The name of the method to register
|
||||
// - procedure: The procedure handler function to register
|
||||
pub fn (mut handler Handler) register_procedure_handle(method string, procedure ProcedureHandler) {
|
||||
handler.procedures[method] = procedure
|
||||
}
|
||||
|
||||
pub struct Procedure[T, U] {
|
||||
pub mut:
|
||||
method string
|
||||
function fn (T) !U
|
||||
}
|
||||
|
||||
pub struct ProcedureVoid[T] {
|
||||
pub mut:
|
||||
method string
|
||||
function fn (T) !
|
||||
}
|
||||
|
||||
pub fn (pw Procedure[T, U]) handle(request Request) !Response {
|
||||
payload := decode_payload[T](request.params) or { return invalid_params }
|
||||
result := pw.function(payload) or { return internal_error }
|
||||
return new_response(request.id, '')
|
||||
}
|
||||
|
||||
pub fn (pw ProcedureVoid[T]) handle(request Request) !Response {
|
||||
payload := decode_payload[T](request.params) or { return invalid_params }
|
||||
pw.function(payload) or { return internal_error }
|
||||
return new_response(request.id, 'null')
|
||||
}
|
||||
|
||||
pub fn decode_payload[T](payload string) !T {
|
||||
$if T is string {
|
||||
return payload
|
||||
} $else $if T is int {
|
||||
return payload.int()
|
||||
} $else $if T is u32 {
|
||||
return payload.u32()
|
||||
} $else $if T is bool {
|
||||
return payload.bool()
|
||||
} $else {
|
||||
return json.decode[T](payload) or { return error('Failed to decode payload: ${err}') }
|
||||
}
|
||||
panic('Unsupported type: ${T.name}')
|
||||
}
|
||||
|
||||
fn error_to_jsonrpc(err IError) !RPCError {
|
||||
return error('Internal error: ${err.msg()}')
|
||||
}
|
||||
|
||||
// handler is a callback function compatible with the WebSocket server's message handler interface.
|
||||
// It processes an incoming WebSocket message as a JSON-RPC request and returns the response.
|
||||
//
|
||||
@@ -53,9 +123,12 @@ pub fn (mut handler Handler) register_procedure(method string, procedure Procedu
|
||||
// Returns:
|
||||
// - The JSON-RPC response as a string
|
||||
// Note: This method panics if an error occurs during handling
|
||||
pub fn (handler Handler) handler(client &websocket.Client, message string) string {
|
||||
return handler.handle(message) or { panic(err) }
|
||||
}
|
||||
// pub fn (handler Handler) handle_message(client &websocket.Client, message string) string {
|
||||
// req := decode_request(message) or {
|
||||
// return invalid_request }
|
||||
// resp := handler.handle(req) or { panic(err) }
|
||||
// return resp.encode()
|
||||
// }
|
||||
|
||||
// handle processes a JSON-RPC request message and invokes the appropriate procedure handler.
|
||||
// If the requested method is not found, it returns a method_not_found error response.
|
||||
@@ -65,17 +138,13 @@ pub fn (handler Handler) handler(client &websocket.Client, message string) strin
|
||||
//
|
||||
// Returns:
|
||||
// - The JSON-RPC response as a string, or an error if processing fails
|
||||
pub fn (handler Handler) handle(message string) !string {
|
||||
// Extract the method name from the request
|
||||
method := decode_request_method(message)!
|
||||
// log.info('Handling remote procedure call to method: ${method}')
|
||||
// Look up the procedure handler for the requested method
|
||||
procedure_func := handler.procedures[method] or {
|
||||
// log.error('No procedure handler for method ${method} found')
|
||||
return method_not_found
|
||||
pub fn (handler Handler) handle(request Request) !Response {
|
||||
procedure_func := handler.procedures[request.method] or {
|
||||
return new_error(request.id, method_not_found)
|
||||
}
|
||||
|
||||
// Execute the procedure handler with the request payload
|
||||
response := procedure_func(message) or { panic(err) }
|
||||
return response
|
||||
return procedure_func(request) or {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ pub fn new_error_response(id int, error RPCError) Response {
|
||||
// Returns:
|
||||
// - A Response object or an error if parsing fails or the response is invalid
|
||||
pub fn decode_response(data string) !Response {
|
||||
raw := json2.raw_decode(data)!
|
||||
raw := json2.raw_decode(data) or { return error('Failed to decode JSONRPC response ${data}\n${err}') }
|
||||
raw_map := raw.as_map()
|
||||
|
||||
// Validate that the response contains either result or error, but not both or neither
|
||||
@@ -105,7 +105,7 @@ pub fn decode_response(data string) !Response {
|
||||
pub fn (resp Response) encode() string {
|
||||
// Payload is already json string
|
||||
if resp.error_ != none {
|
||||
return '{"jsonrpc":"2.0","id":${resp.id},"error":${resp.error_.str()}}'
|
||||
return '{"jsonrpc":"2.0","id":${resp.id},"error":${json2.encode(resp.error_)}}'
|
||||
} else if resp.result != none {
|
||||
return '{"jsonrpc":"2.0","id":${resp.id},"result":${resp.result}}'
|
||||
}
|
||||
|
||||
21
lib/schemas/jsonrpc/reflection/handler.v
Normal file
21
lib/schemas/jsonrpc/reflection/handler.v
Normal file
@@ -0,0 +1,21 @@
|
||||
module reflection
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub struct Handler[T] {
|
||||
pub mut:
|
||||
receiver T
|
||||
}
|
||||
|
||||
pub fn new_handler[T](receiver T) Handler[T] {
|
||||
return Handler[T]{
|
||||
receiver: receiver
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut h Handler[T]) handle(request jsonrpc.Request) !jsonrpc.Response {
|
||||
receiver := h.receiver
|
||||
$for method in receiver.methods {
|
||||
println("method ${method}")
|
||||
}
|
||||
}
|
||||
122
lib/schemas/openrpc/client_unix.v
Normal file
122
lib/schemas/openrpc/client_unix.v
Normal file
@@ -0,0 +1,122 @@
|
||||
module openrpc
|
||||
|
||||
import x.json2 as json
|
||||
import net.unix
|
||||
import time
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub struct UNIXClient {
|
||||
pub mut:
|
||||
socket_path string
|
||||
timeout int = 30 // Default timeout in seconds
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct UNIXClientParams {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
timeout int = 30
|
||||
}
|
||||
|
||||
// new_unix_client creates a new OpenRPC Unix client
|
||||
pub fn new_unix_client(params UNIXClientParams) &UNIXClient {
|
||||
return &UNIXClient{
|
||||
socket_path: params.socket_path
|
||||
timeout: params.timeout
|
||||
}
|
||||
}
|
||||
|
||||
// call makes a JSON-RPC call to the server with typed parameters and result
|
||||
pub fn (mut client UNIXClient) call_generic[T, D](method string, params T) !D {
|
||||
// Create a generic request with typed parameters
|
||||
response := client.call(method, json.encode(params))!
|
||||
return json.decode[D](response)
|
||||
}
|
||||
|
||||
// call_str makes a JSON-RPC call with string parameters and returns string result
|
||||
pub fn (mut client UNIXClient) call(method string, params string) !string {
|
||||
// Create a standard request with string parameters
|
||||
request := jsonrpc.new_request(method, params)
|
||||
|
||||
// Send the request and get response
|
||||
response_json := client.send_request(request.encode())!
|
||||
|
||||
// Decode response
|
||||
response := jsonrpc.decode_response(response_json) or {
|
||||
return error('Failed to decode response: ${err}')
|
||||
}
|
||||
|
||||
// Validate response
|
||||
response.validate() or {
|
||||
return error('Invalid response: ${err}')
|
||||
}
|
||||
|
||||
// Check ID matches
|
||||
if response.id != request.id {
|
||||
return error('Response ID ${response.id} does not match request ID ${request.id}')
|
||||
}
|
||||
|
||||
// Return result or error
|
||||
return response.result()
|
||||
}
|
||||
|
||||
// discover calls the rpc.discover method to get the OpenRPC specification
|
||||
pub fn (mut client UNIXClient) discover() !OpenRPC {
|
||||
spec_json := client.call('rpc.discover', '')!
|
||||
return decode(spec_json)!
|
||||
}
|
||||
|
||||
// send_request_str sends a string request and returns string result
|
||||
fn (mut client UNIXClient) send_request(request string) !string {
|
||||
// Connect to Unix socket
|
||||
mut conn := unix.connect_stream(client.socket_path) or {
|
||||
return error('Failed to connect to Unix socket at ${client.socket_path}: ${err}')
|
||||
}
|
||||
|
||||
defer {
|
||||
conn.close() or { console.print_stderr('Error closing connection: ${err}') }
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
if client.timeout > 0 {
|
||||
conn.set_read_timeout(client.timeout * time.second)
|
||||
conn.set_write_timeout(client.timeout * time.second)
|
||||
}
|
||||
|
||||
// Send request
|
||||
console.print_debug('Sending request: ${request}')
|
||||
|
||||
conn.write_string(request) or {
|
||||
return error('Failed to send request: ${err}')
|
||||
}
|
||||
|
||||
// Read response
|
||||
mut buffer := []u8{len: 4096}
|
||||
bytes_read := conn.read(mut buffer) or {
|
||||
return error('Failed to read response: ${err}')
|
||||
}
|
||||
|
||||
if bytes_read == 0 {
|
||||
return error('No response received from server')
|
||||
}
|
||||
|
||||
response := buffer[..bytes_read].bytestr()
|
||||
console.print_debug('Received response: ${response}')
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ping sends a simple ping to test connectivity
|
||||
pub fn (mut client UNIXClient) ping() !bool {
|
||||
// Try to discover the specification as a connectivity test
|
||||
client.discover() or {
|
||||
return error('Ping failed: ${err}')
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// close closes any persistent connections (currently no-op for Unix sockets)
|
||||
pub fn (mut client UNIXClient) close() ! {
|
||||
// Unix socket connections are closed per request, so nothing to do here
|
||||
}
|
||||
175
lib/schemas/openrpc/client_unix_test.v
Normal file
175
lib/schemas/openrpc/client_unix_test.v
Normal file
@@ -0,0 +1,175 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
|
||||
// Test struct for typed parameters
|
||||
struct TestParams {
|
||||
name string
|
||||
value int
|
||||
}
|
||||
|
||||
// Test struct for typed result
|
||||
struct TestResult {
|
||||
success bool
|
||||
message string
|
||||
}
|
||||
|
||||
// Example custom handler for testing
|
||||
struct TestHandler {
|
||||
}
|
||||
|
||||
fn (mut h TestHandler) handle(req jsonrpc.Request) !jsonrpc.Response {
|
||||
match req.method {
|
||||
'test.echo' {
|
||||
return jsonrpc.new_response(req.id, req.params)
|
||||
}
|
||||
'test.add' {
|
||||
// Simple addition test - expect params like '{"a": 5, "b": 3}'
|
||||
return jsonrpc.new_response(req.id, '{"result": 8}')
|
||||
}
|
||||
'test.greet' {
|
||||
// Greeting test - expect params like '{"name": "Alice"}'
|
||||
return jsonrpc.new_response(req.id, '{"message": "Hello, World!"}')
|
||||
}
|
||||
else {
|
||||
return jsonrpc.new_error_response(req.id, jsonrpc.method_not_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn test_unix_client_basic() {
|
||||
// This test requires a running server, so it's more of an integration test
|
||||
// In practice, you would start a server in a separate goroutine or process
|
||||
|
||||
mut client := new_unix_client(
|
||||
socket_path: '/tmp/test_heromodels'
|
||||
timeout: 5
|
||||
)
|
||||
|
||||
// Test string-based call
|
||||
result := client.call('test.echo', '{"message": "hello"}') or {
|
||||
println('Expected error since no server is running: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
println('Echo result: ${result}')
|
||||
}
|
||||
|
||||
fn test_unix_client_typed() {
|
||||
mut client := new_unix_client(
|
||||
socket_path: '/tmp/test_heromodels'
|
||||
timeout: 5
|
||||
)
|
||||
|
||||
// Test typed call
|
||||
params := TestParams{
|
||||
name: 'test'
|
||||
value: 42
|
||||
}
|
||||
|
||||
result := client.call_generic[TestParams, TestResult]('test.process', params) or {
|
||||
println('Expected error since no server is running: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
println('Typed result: ${result}')
|
||||
}
|
||||
|
||||
fn test_unix_client_discover() {
|
||||
mut client := new_unix_client(
|
||||
socket_path: '/tmp/test_heromodels'
|
||||
timeout: 5
|
||||
)
|
||||
|
||||
// Test discovery
|
||||
spec := client.discover() or {
|
||||
println('Expected error since no server is running: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
println('OpenRPC spec version: ${spec.openrpc}')
|
||||
println('Info title: ${spec.info.title}')
|
||||
}
|
||||
|
||||
fn test_unix_client_ping() {
|
||||
mut client := new_unix_client(
|
||||
socket_path: '/tmp/test_heromodels'
|
||||
timeout: 5
|
||||
)
|
||||
|
||||
// Test ping
|
||||
is_alive := client.ping() or {
|
||||
println('Expected error since no server is running: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
println('Server is alive: ${is_alive}')
|
||||
}
|
||||
|
||||
// Integration test that demonstrates full client-server interaction
|
||||
fn test_full_integration() {
|
||||
socket_path := '/tmp/test_heromodels_integration'
|
||||
|
||||
// Create a test OpenRPC specification
|
||||
mut spec := OpenRPC{
|
||||
openrpc: '1.3.0'
|
||||
info: Info{
|
||||
title: 'Test API'
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: [
|
||||
Method{
|
||||
name: 'test.echo'
|
||||
params: []
|
||||
result: ContentDescriptor{
|
||||
name: 'result'
|
||||
schema: jsonschema.Schema{}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Create handler
|
||||
mut test_handler := TestHandler{}
|
||||
handler := Handler{
|
||||
specification: spec
|
||||
handler: test_handler
|
||||
}
|
||||
|
||||
// Start server in background
|
||||
mut server := new_unix_server(handler, socket_path: socket_path) or {
|
||||
println('Failed to create server: ${err}')
|
||||
return
|
||||
}
|
||||
|
||||
// Start server in a separate thread
|
||||
spawn fn [mut server] () {
|
||||
server.start() or {
|
||||
println('Server error: ${err}')
|
||||
}
|
||||
}()
|
||||
|
||||
// Give server time to start
|
||||
// time.sleep(100 * time.millisecond)
|
||||
|
||||
// Create client and test
|
||||
mut client := new_unix_client(
|
||||
socket_path: socket_path
|
||||
timeout: 5
|
||||
)
|
||||
|
||||
// Test the connection
|
||||
result := client.call('test.echo', '{"test": "data"}') or {
|
||||
println('Client call failed: ${err}')
|
||||
server.close() or {}
|
||||
return
|
||||
}
|
||||
|
||||
println('Integration test result: ${result}')
|
||||
|
||||
// Clean up
|
||||
server.close() or {
|
||||
println('Failed to close server: ${err}')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
module codegen
|
||||
|
||||
import os
|
||||
import json
|
||||
import freeflowuniverse.herolib.core.code { Alias, Struct }
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
@@ -10,14 +11,14 @@ const doc_path = '${os.dir(@FILE)}/testdata/openrpc.json'
|
||||
fn test_generate_model() ! {
|
||||
mut doc_file := pathlib.get_file(path: doc_path)!
|
||||
content := doc_file.read()!
|
||||
object := openrpc.decode(content)!
|
||||
object := json.decode(openrpc.OpenRPC, content)!
|
||||
model := generate_model(object)!
|
||||
|
||||
assert model.len == 3
|
||||
assert model[0] is Alias
|
||||
pet_id := model[0] as Alias
|
||||
assert pet_id.name == 'PetId'
|
||||
assert pet_id.typ.symbol == 'int'
|
||||
assert pet_id.typ.symbol() == 'int'
|
||||
|
||||
assert model[1] is Struct
|
||||
pet_struct := model[1] as Struct
|
||||
@@ -26,23 +27,23 @@ fn test_generate_model() ! {
|
||||
|
||||
// test field is `id PetId @[required]`
|
||||
assert pet_struct.fields[0].name == 'id'
|
||||
assert pet_struct.fields[0].typ.symbol == 'PetId'
|
||||
assert pet_struct.fields[0].typ.symbol() == 'PetId'
|
||||
assert pet_struct.fields[0].attrs.len == 1
|
||||
assert pet_struct.fields[0].attrs[0].name == 'required'
|
||||
|
||||
// test field is `name string @[required]`
|
||||
assert pet_struct.fields[1].name == 'name'
|
||||
assert pet_struct.fields[1].typ.symbol == 'string'
|
||||
assert pet_struct.fields[1].typ.symbol() == 'string'
|
||||
assert pet_struct.fields[1].attrs.len == 1
|
||||
assert pet_struct.fields[1].attrs[0].name == 'required'
|
||||
|
||||
// test field is `tag string`
|
||||
assert pet_struct.fields[2].name == 'tag'
|
||||
assert pet_struct.fields[2].typ.symbol == 'string'
|
||||
assert pet_struct.fields[2].typ.symbol() == 'string'
|
||||
assert pet_struct.fields[2].attrs.len == 0
|
||||
|
||||
assert model[2] is Alias
|
||||
pets_alias := model[2] as Alias
|
||||
assert pets_alias.name == 'Pets'
|
||||
assert pets_alias.typ.symbol == '[]Pet'
|
||||
assert pets_alias.typ.symbol() == '[]Pet'
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn (mut c HTTPController) index(mut ctx Context) veb.Result {
|
||||
}
|
||||
|
||||
// Process the JSONRPC request with the OpenRPC handler
|
||||
response := c.handler.handle(request) or {
|
||||
response := c.handle(request) or {
|
||||
return ctx.server_error('Handler error: ${err.msg()}')
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module openrpc
|
||||
|
||||
import x.json2
|
||||
import json
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
@@ -10,7 +11,7 @@ const doc_path = '${os.dir(@FILE)}/testdata/openrpc.json'
|
||||
fn test_decode() ! {
|
||||
mut doc_file := pathlib.get_file(path: doc_path)!
|
||||
content := doc_file.read()!
|
||||
object := decode(content)!
|
||||
object := json.decode(OpenRPC, content)!
|
||||
assert object.openrpc == '1.0.0-rc1'
|
||||
assert object.methods.map(it.name) == ['list_pets', 'create_pet', 'get_pet']
|
||||
assert object.methods.map(it.name) == ['list_pets', 'create_pet', 'get_pet', 'update_pet', 'delete_pet']
|
||||
}
|
||||
|
||||
@@ -7,50 +7,62 @@ import x.json2 { Any }
|
||||
// eliminates undefined variable by calling prune on the initial encoding.
|
||||
pub fn (doc OpenRPC) encode() !string {
|
||||
encoded := json.encode(doc)
|
||||
raw_decode := json2.raw_decode(encoded)!
|
||||
mut doc_map := raw_decode.as_map()
|
||||
pruned_map := prune(doc_map)
|
||||
return json2.encode_pretty[Any](pruned_map)
|
||||
// raw_decode := json2.raw_decode(encoded)!
|
||||
// mut doc_map := raw_decode.as_map()
|
||||
// pruned_map := prune(doc_map)
|
||||
// return json2.encode_pretty[Any](pruned_map)
|
||||
return encoded
|
||||
}
|
||||
|
||||
// prune recursively prunes a map of Any type, pruning map keys where the value is the default value of the variable.
|
||||
// this treats undefined values as null, which is ok for openrpc document encoding.
|
||||
pub fn prune(obj Any) Any {
|
||||
if obj is map[string]Any {
|
||||
mut pruned_map := map[string]Any{}
|
||||
|
||||
for key, val in obj as map[string]Any {
|
||||
if key == '_type' {
|
||||
continue
|
||||
}
|
||||
pruned_val := prune(val)
|
||||
if pruned_val.str() != '' {
|
||||
pruned_map[key] = pruned_val
|
||||
} else if key == 'methods' || key == 'params' {
|
||||
pruned_map[key] = []Any{}
|
||||
}
|
||||
}
|
||||
|
||||
if pruned_map.keys().len != 0 {
|
||||
return pruned_map
|
||||
}
|
||||
} else if obj is []Any {
|
||||
mut pruned_arr := []Any{}
|
||||
|
||||
for val in obj as []Any {
|
||||
pruned_val := prune(val)
|
||||
if pruned_val.str() != '' {
|
||||
pruned_arr << pruned_val
|
||||
}
|
||||
}
|
||||
|
||||
if pruned_arr.len != 0 {
|
||||
return pruned_arr
|
||||
}
|
||||
} else if obj is string {
|
||||
if obj != '' {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return ''
|
||||
// encode encodes an OpenRPC document struct into json string.
|
||||
// eliminates undefined variable by calling prune on the initial encoding.
|
||||
pub fn (doc OpenRPC) encode_pretty() !string {
|
||||
encoded := json.encode_pretty(doc)
|
||||
// raw_decode := json2.raw_decode(encoded)!
|
||||
// mut doc_map := raw_decode.as_map()
|
||||
// pruned_map := prune(doc_map)
|
||||
// return json2.encode_pretty[Any](pruned_map)
|
||||
return encoded
|
||||
}
|
||||
|
||||
// // prune recursively prunes a map of Any type, pruning map keys where the value is the default value of the variable.
|
||||
// // this treats undefined values as null, which is ok for openrpc document encoding.
|
||||
// pub fn prune(obj Any) Any {
|
||||
// if obj is map[string]Any {
|
||||
// mut pruned_map := map[string]Any{}
|
||||
|
||||
// for key, val in obj as map[string]Any {
|
||||
// if key == '_type' {
|
||||
// continue
|
||||
// }
|
||||
// pruned_val := prune(val)
|
||||
// if pruned_val.str() != '' {
|
||||
// pruned_map[key] = pruned_val
|
||||
// } else if key == 'methods' || key == 'params' {
|
||||
// pruned_map[key] = []Any{}
|
||||
// }
|
||||
// }
|
||||
|
||||
// if pruned_map.keys().len != 0 {
|
||||
// return pruned_map
|
||||
// }
|
||||
// } else if obj is []Any {
|
||||
// mut pruned_arr := []Any{}
|
||||
|
||||
// for val in obj as []Any {
|
||||
// pruned_val := prune(val)
|
||||
// if pruned_val.str() != '' {
|
||||
// pruned_arr << pruned_val
|
||||
// }
|
||||
// }
|
||||
|
||||
// if pruned_arr.len != 0 {
|
||||
// return pruned_arr
|
||||
// }
|
||||
// } else if obj is string {
|
||||
// if obj != '' {
|
||||
// return obj
|
||||
// }
|
||||
// }
|
||||
// return ''
|
||||
// }
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
module openrpc
|
||||
|
||||
import x.json2 as json
|
||||
// import x.json2 as json
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
|
||||
|
||||
const blank_openrpc = '{
|
||||
"openrpc": "1.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"methods": []
|
||||
}'
|
||||
const blank_openrpc = '{"openrpc": "1.0.0","info": {"version": "1.0.0"},"methods": []}'
|
||||
|
||||
// test if encode can correctly encode a blank OpenRPC
|
||||
fn test_encode_blank() ! {
|
||||
@@ -18,10 +12,9 @@ fn test_encode_blank() ! {
|
||||
title: ''
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: []Method{}
|
||||
}
|
||||
encoded := doc.encode()!
|
||||
assert encoded.trim_space().split_into_lines().map(it.trim_space()) == blank_openrpc.split_into_lines().map(it.trim_space())
|
||||
assert encoded == blank_openrpc
|
||||
}
|
||||
|
||||
// test if can correctly encode an OpenRPC doc with a method
|
||||
@@ -48,7 +41,7 @@ fn test_encode_with_method() ! {
|
||||
},
|
||||
]
|
||||
}
|
||||
encoded := doc.encode()!
|
||||
encoded := doc.encode_pretty()!
|
||||
assert encoded == '{
|
||||
"openrpc": "1.0.0",
|
||||
"info": {
|
||||
@@ -96,6 +89,6 @@ fn test_encode() ! {
|
||||
},
|
||||
]
|
||||
}
|
||||
encoded := json.encode(doc)
|
||||
encoded := doc.encode()!
|
||||
assert encoded == '{"openrpc":"1.0.0","info":{"title":"","version":"1.0.0"},"methods":[{"name":"method_name","summary":"summary","description":"description for this method","params":[{"name":"sample descriptor","schema":{"\$schema":"","\$id":"","title":"","description":"","type":"string","properties":{},"additionalProperties":{},"required":[],"ref":"","items":{},"defs":{},"oneOf":[],"_type":"Schema"},"_type":"ContentDescriptor"}],"result":{},"deprecated":true}]}'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module openrpc
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
@[params]
|
||||
pub struct Params {
|
||||
@@ -24,5 +25,5 @@ pub fn new(params Params) !OpenRPC {
|
||||
params.text
|
||||
}
|
||||
|
||||
return decode(text)!
|
||||
return json.decode(OpenRPC, text)!
|
||||
}
|
||||
|
||||
@@ -2,26 +2,20 @@ module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// The openrpc handler is a wrapper around a jsonrpc handler
|
||||
pub struct Handler {
|
||||
jsonrpc.Handler
|
||||
pub:
|
||||
specification OpenRPC @[required] // The OpenRPC specification
|
||||
pub mut:
|
||||
handler IHandler
|
||||
}
|
||||
|
||||
pub interface IHandler {
|
||||
mut:
|
||||
handle(jsonrpc.Request) !jsonrpc.Response // Custom handler for other methods
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct HandleParams {
|
||||
timeout int = 60 // Timeout in seconds
|
||||
retry int // Number of retries
|
||||
}
|
||||
// pub interface IHandler {
|
||||
// mut:
|
||||
// handle(jsonrpc.Request) !jsonrpc.Response // Custom handler for other methods
|
||||
// }
|
||||
|
||||
// Handle a JSON-RPC request and return a response
|
||||
pub fn (mut h Handler) handle(req jsonrpc.Request, params HandleParams) !jsonrpc.Response {
|
||||
pub fn (mut h Handler) handle(req jsonrpc.Request) !jsonrpc.Response {
|
||||
// Validate the incoming request
|
||||
req.validate() or { return jsonrpc.new_error_response(req.id, jsonrpc.invalid_request) }
|
||||
|
||||
@@ -33,15 +27,12 @@ pub fn (mut h Handler) handle(req jsonrpc.Request, params HandleParams) !jsonrpc
|
||||
}
|
||||
|
||||
// Validate the method exists in the specification
|
||||
if req.method !in h.specification.methods.map(it.name) {
|
||||
return jsonrpc.new_error_response(req.id, jsonrpc.method_not_found)
|
||||
}
|
||||
|
||||
// Enforce timeout and retries (dummy implementation)
|
||||
if params.timeout < 0 || params.retry < 0 {
|
||||
return jsonrpc.new_error_response(req.id, jsonrpc.invalid_params)
|
||||
}
|
||||
// TODO: implement once auto add registered methods to spec
|
||||
// if req.method !in h.specification.methods.map(it.name) {
|
||||
// println("Method not found: " + req.method)
|
||||
// return jsonrpc.new_error_response(req.id, jsonrpc.method_not_found)
|
||||
// }
|
||||
|
||||
// Forward the request to the custom handler
|
||||
return h.handler.handle(req)
|
||||
return h.Handler.handle(req) or { panic(err) }
|
||||
}
|
||||
|
||||
@@ -9,20 +9,20 @@ import freeflowuniverse.herolib.schemas.jsonschema { Reference, SchemaRef }
|
||||
pub struct OpenRPC {
|
||||
pub mut:
|
||||
openrpc string = '1.0.0' // This string MUST be the semantic version number of the OpenRPC Specification version that the OpenRPC document uses.
|
||||
info Info // Provides metadata about the API.
|
||||
servers []Server // An array of Server Objects, which provide connectivity information to a target server.
|
||||
methods []Method // The available methods for the API.
|
||||
components Components // An element to hold various schemas for the specification.
|
||||
external_docs []ExternalDocs @[json: externalDocs] // Additional external documentation.
|
||||
info Info @[omitempty] // Provides metadata about the API.
|
||||
servers []Server @[omitempty]// An array of Server Objects, which provide connectivity information to a target server.
|
||||
methods []Method @[omitempty]// The available methods for the API.
|
||||
components Components @[omitempty] // An element to hold various schemas for the specification.
|
||||
external_docs []ExternalDocs @[json: externalDocs; omitempty] // Additional external documentation.
|
||||
}
|
||||
|
||||
// The object provides metadata about the API.
|
||||
// The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.
|
||||
pub struct Info {
|
||||
pub:
|
||||
title string // The title of the application.
|
||||
description string // A verbose description of the application.
|
||||
terms_of_service string @[json: termsOfService] // A URL to the Terms of Service for the API. MUST be in the format of a URL.
|
||||
title string @[omitempty] // The title of the application.
|
||||
description string @[omitempty] // A verbose description of the application.
|
||||
terms_of_service string @[json: termsOfService; omitempty] // A URL to the Terms of Service for the API. MUST be in the format of a URL.
|
||||
contact Contact @[omitempty] // The contact information for the exposed API.
|
||||
license License @[omitempty] // The license information for the exposed API.
|
||||
version string @[omitempty] // The version of the OpenRPC document (which is distinct from the OpenRPC Specification version or the API implementation version).
|
||||
@@ -167,12 +167,12 @@ pub:
|
||||
// All the fixed fields declared above are objects that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$
|
||||
pub struct Components {
|
||||
pub mut:
|
||||
content_descriptors map[string]ContentDescriptorRef @[json: contentDescriptors] // An object to hold reusable Content Descriptor Objects.
|
||||
schemas map[string]SchemaRef // An object to hold reusable Schema Objects.
|
||||
examples map[string]Example // An object to hold reusable Example Objects.
|
||||
links map[string]Link // An object to hold reusable Link Objects.
|
||||
error map[string]Error // An object to hold reusable Error Objects.
|
||||
example_pairing_objects map[string]ExamplePairing @[json: examplePairingObjects] // An object to hold reusable Example Pairing Objects.
|
||||
content_descriptors map[string]ContentDescriptorRef @[json: contentDescriptors; omitempty] // An object to hold reusable Content Descriptor Objects.
|
||||
schemas map[string]SchemaRef @[omitempty] // An object to hold reusable Schema Objects.
|
||||
examples map[string]Example @[omitempty] // An object to hold reusable Example Objects.
|
||||
links map[string]Link @[omitempty] // An object to hold reusable Link Objects.
|
||||
error map[string]Error @[omitempty] // An object to hold reusable Error Objects.
|
||||
example_pairing_objects map[string]ExamplePairing @[json: examplePairingObjects; omitempty] // An object to hold reusable Example Pairing Objects.
|
||||
tags map[string]Tag // An object to hold reusable Tag Objects.
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ fn test_parse_example_pairing() ! {
|
||||
params := example.params
|
||||
assert params.len == 1
|
||||
param0 := (params[0] as Example)
|
||||
assert param0.value == "'input_string'"
|
||||
assert param0.value.str() == "'input_string'"
|
||||
}
|
||||
|
||||
const test_struct = Struct{
|
||||
@@ -40,9 +40,7 @@ const test_struct = Struct{
|
||||
fields: [
|
||||
StructField{
|
||||
name: 'TestField'
|
||||
typ: Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
typ: code.type_i32
|
||||
attrs: [Attribute{
|
||||
name: 'example'
|
||||
arg: '21'
|
||||
|
||||
107
lib/schemas/openrpc/server_unix.v
Normal file
107
lib/schemas/openrpc/server_unix.v
Normal file
@@ -0,0 +1,107 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import x.json2
|
||||
import net.unix
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
pub struct UNIXServer {
|
||||
pub mut:
|
||||
listener &unix.StreamListener
|
||||
socket_path string
|
||||
handler Handler @[required]
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct UNIXServerParams {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
}
|
||||
|
||||
pub fn new_unix_server(handler Handler, params UNIXServerParams) !&UNIXServer {
|
||||
// Remove existing socket file if it exists
|
||||
if os.exists(params.socket_path) {
|
||||
os.rm(params.socket_path)!
|
||||
}
|
||||
|
||||
listener := unix.listen_stream(params.socket_path, unix.ListenOptions{})!
|
||||
|
||||
return &UNIXServer{
|
||||
listener: listener
|
||||
handler: handler
|
||||
socket_path: params.socket_path
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut server UNIXServer) start() ! {
|
||||
console.print_header('Starting HeroModels OpenRPC Server on ${server.socket_path}')
|
||||
|
||||
for {
|
||||
mut conn := server.listener.accept()!
|
||||
spawn server.handle_connection(mut conn)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut server UNIXServer) close() ! {
|
||||
server.listener.close()!
|
||||
if os.exists(server.socket_path) {
|
||||
os.rm(server.socket_path)!
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut server UNIXServer) handle_connection(mut conn unix.StreamConn) {
|
||||
defer {
|
||||
conn.close() or { console.print_stderr('Error closing connection: ${err}') }
|
||||
}
|
||||
|
||||
for {
|
||||
// Read JSON-RPC request
|
||||
mut buffer := []u8{len: 4096}
|
||||
bytes_read := conn.read(mut buffer) or {
|
||||
console.print_debug('Connection closed or error reading: ${err}')
|
||||
break
|
||||
}
|
||||
|
||||
if bytes_read == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
request_data := buffer[..bytes_read].bytestr()
|
||||
console.print_debug('Received request: ${request_data}')
|
||||
|
||||
// Process the JSON-RPC request
|
||||
if response := server.process_request(request_data) {
|
||||
// Send response only if we have a valid response
|
||||
conn.write_string(response) or {
|
||||
console.print_stderr('Error writing response: ${err}')
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Log the error but don't break the connection
|
||||
// According to JSON-RPC 2.0 spec, if we can't decode the request ID,
|
||||
// we should not send any response but keep the connection alive
|
||||
console.print_debug('Invalid request received, no response sent: ${err}')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut server UNIXServer) process_request(request_data string) ?string {
|
||||
// Parse JSON-RPC request using json2 to handle Any types
|
||||
request := jsonrpc.decode_request(request_data) or {
|
||||
// try decoding id to give error response
|
||||
if id := jsonrpc.decode_request_id(request_data) {
|
||||
// We can extract ID, so return proper JSON-RPC error response
|
||||
return jsonrpc.new_error(id, jsonrpc.invalid_request).encode()
|
||||
} else {
|
||||
// Cannot extract ID from invalid JSON - return none (no response)
|
||||
// This follows JSON-RPC 2.0 spec: no response when ID cannot be determined
|
||||
return none
|
||||
}
|
||||
}
|
||||
response := server.handler.handle(request) or {
|
||||
return jsonrpc.new_error(request.id, jsonrpc.internal_error).encode()
|
||||
}
|
||||
return response.encode()
|
||||
}
|
||||
123
lib/schemas/openrpc/server_unix_test.v
Normal file
123
lib/schemas/openrpc/server_unix_test.v
Normal file
@@ -0,0 +1,123 @@
|
||||
module openrpc
|
||||
|
||||
import time
|
||||
import json
|
||||
import x.json2
|
||||
import net.unix
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
const testdata_dir = os.join_path(os.dir(@FILE), 'testdata')
|
||||
const openrpc_path = os.join_path(testdata_dir, 'openrpc.json')
|
||||
|
||||
pub fn test_new_unix_server() ! {
|
||||
mut spec := OpenRPC{}
|
||||
handler := Handler{
|
||||
specification: new(path: openrpc_path)!
|
||||
}
|
||||
mut server := new_unix_server(handler)!
|
||||
|
||||
defer {
|
||||
server.close() or {panic(err)}
|
||||
}
|
||||
|
||||
spawn server.start()
|
||||
|
||||
// client()
|
||||
}
|
||||
|
||||
pub fn test_unix_server_start() ! {
|
||||
mut spec := OpenRPC{}
|
||||
handler := Handler{
|
||||
specification: new(path: openrpc_path)!
|
||||
}
|
||||
mut server := new_unix_server(handler)!
|
||||
|
||||
defer {
|
||||
server.close() or {panic(err)}
|
||||
}
|
||||
|
||||
spawn server.start()
|
||||
|
||||
// client()
|
||||
}
|
||||
|
||||
pub fn test_unix_server_handle_connection() ! {
|
||||
mut spec := OpenRPC{}
|
||||
handler := Handler{
|
||||
specification: new(path: openrpc_path)!
|
||||
}
|
||||
mut server := new_unix_server(handler)!
|
||||
|
||||
// Start server in background
|
||||
spawn server.start()
|
||||
|
||||
// Give server time to start
|
||||
// time.sleep(50 * time.millisecond)
|
||||
|
||||
// Connect to the server
|
||||
mut conn := unix.connect_stream(server.socket_path)!
|
||||
|
||||
defer {
|
||||
conn.close() or {panic(err)}
|
||||
server.close() or {panic(err)}
|
||||
}
|
||||
|
||||
// Test 1: Send rpc.discover request
|
||||
discover_request := jsonrpc.new_request('rpc.discover', '')
|
||||
request_json := discover_request.encode()
|
||||
|
||||
// Send the request
|
||||
conn.write_string(request_json)!
|
||||
|
||||
|
||||
// Read the response
|
||||
mut buffer := []u8{len: 4096}
|
||||
bytes_read := conn.read(mut buffer)!
|
||||
response_data := buffer[..bytes_read].bytestr()
|
||||
|
||||
// Parse and validate response
|
||||
response := jsonrpc.decode_response(response_data)!
|
||||
assert response.id == discover_request.id
|
||||
assert response.is_result()
|
||||
assert !response.is_error()
|
||||
|
||||
// Validate that the result contains OpenRPC specification
|
||||
result := response.result()!
|
||||
assert result.len > 0
|
||||
|
||||
// Test 2: Send invalid JSON request
|
||||
invalid_request := '{"invalid": "json"}'
|
||||
conn.write_string(invalid_request)!
|
||||
|
||||
// Set a short read timeout to test no response behavior
|
||||
conn.set_read_timeout(10 * time.millisecond)
|
||||
|
||||
// Try to read response - should timeout since server sends no response for invalid JSON
|
||||
conn.wait_for_read() or {
|
||||
// This is expected behavior - server should not respond to invalid JSON without extractable ID
|
||||
console.print_debug('Expected timeout for invalid JSON request: ${err}')
|
||||
assert err.msg().contains('timeout') || err.msg().contains('timed out')
|
||||
// Reset timeout for next test
|
||||
conn.set_read_timeout(30 * time.second)
|
||||
}
|
||||
|
||||
// Test 3: Send request with non-existent method
|
||||
nonexistent_request := jsonrpc.new_request('nonexistent.method', '{}')
|
||||
nonexistent_json := nonexistent_request.encode()
|
||||
|
||||
conn.write_string(nonexistent_json)!
|
||||
|
||||
// Read method not found response
|
||||
bytes_read3 := conn.read(mut buffer)!
|
||||
method_error_data := buffer[..bytes_read3].bytestr()
|
||||
|
||||
method_error_response := jsonrpc.decode_response(method_error_data)!
|
||||
assert method_error_response.is_error()
|
||||
assert method_error_response.id == nonexistent_request.id
|
||||
|
||||
if error_obj := method_error_response.error() {
|
||||
assert error_obj.code == jsonrpc.method_not_found.code
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
module openrpcserver
|
||||
|
||||
import json
|
||||
import x.json2
|
||||
import net.unix
|
||||
import os
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
// THIS IS DEFAULT NEEDED FOR EACH OPENRPC SERVER WE MAKE
|
||||
|
||||
pub struct JsonRpcRequest {
|
||||
pub:
|
||||
jsonrpc string = '2.0'
|
||||
method string
|
||||
params string
|
||||
id string
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 response structure
|
||||
pub struct JsonRpcResponse {
|
||||
pub:
|
||||
jsonrpc string = '2.0'
|
||||
result string
|
||||
error ?JsonRpcError
|
||||
id string
|
||||
}
|
||||
|
||||
// JSON-RPC 2.0 error structure
|
||||
pub struct JsonRpcError {
|
||||
pub:
|
||||
code int
|
||||
message string
|
||||
data string
|
||||
}
|
||||
|
||||
pub struct RPCServer {
|
||||
pub mut:
|
||||
listener &unix.StreamListener
|
||||
socket_path string
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct RPCServerArgs {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
}
|
||||
|
||||
// Temporary struct for parsing incoming JSON-RPC requests using json2
|
||||
struct JsonRpcRequestRaw {
|
||||
jsonrpc string
|
||||
method string
|
||||
params json2.Any
|
||||
id json2.Any
|
||||
}
|
||||
|
||||
pub fn new_rpc_server(args RPCServerArgs) !&RPCServer {
|
||||
// Remove existing socket file if it exists
|
||||
if os.exists(args.socket_path) {
|
||||
os.rm(args.socket_path)!
|
||||
}
|
||||
|
||||
listener := unix.listen_stream(args.socket_path, unix.ListenOptions{})!
|
||||
|
||||
return &RPCServer{
|
||||
listener: listener
|
||||
socket_path: args.socket_path
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut server RPCServer) start() ! {
|
||||
console.print_header('Starting HeroModels OpenRPC Server on ${server.socket_path}')
|
||||
|
||||
for {
|
||||
mut conn := server.listener.accept()!
|
||||
spawn server.handle_connection(mut conn)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut server RPCServer) close() ! {
|
||||
server.listener.close()!
|
||||
if os.exists(server.socket_path) {
|
||||
os.rm(server.socket_path)!
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut server RPCServer) handle_connection(mut conn unix.StreamConn) {
|
||||
defer {
|
||||
conn.close() or { console.print_stderr('Error closing connection: ${err}') }
|
||||
}
|
||||
|
||||
for {
|
||||
// Read JSON-RPC request
|
||||
mut buffer := []u8{len: 4096}
|
||||
bytes_read := conn.read(mut buffer) or {
|
||||
console.print_debug('Connection closed or error reading: ${err}')
|
||||
break
|
||||
}
|
||||
|
||||
if bytes_read == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
request_data := buffer[..bytes_read].bytestr()
|
||||
console.print_debug('Received request: ${request_data}')
|
||||
|
||||
// Process the JSON-RPC request
|
||||
response := server.process_request(request_data) or {
|
||||
server.create_error_response(-32603, 'Internal error: ${err}', 'null')
|
||||
}
|
||||
|
||||
// Send response
|
||||
conn.write_string(response) or {
|
||||
console.print_stderr('Error writing response: ${err}')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut server RPCServer) process_request(request_data string) !string {
|
||||
// Parse JSON-RPC request using json2 to handle Any types
|
||||
request := json2.decode[JsonRpcRequestRaw](request_data)!
|
||||
// Convert params to string representation
|
||||
params_str := request.params.json_str()
|
||||
// Convert id to string
|
||||
id_str := request.id.json_str()
|
||||
r := request.method.trim_space().to_lower()
|
||||
// Route to appropriate method
|
||||
result := server.process(r, params_str)!
|
||||
return server.create_success_response(result, id_str)
|
||||
}
|
||||
|
||||
// Default process method - should be overridden by implementations
|
||||
pub fn (mut server RPCServer) process(method string, params_str string) !string {
|
||||
return match method {
|
||||
'rpc.discover' {
|
||||
server.discover()!
|
||||
}
|
||||
else {
|
||||
server.create_error_response(-32601, 'Method not found', method)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn (mut server RPCServer) create_success_response(result string, id string) string {
|
||||
response := JsonRpcResponse{
|
||||
jsonrpc: '2.0'
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
return json.encode(response)
|
||||
}
|
||||
|
||||
fn (mut server RPCServer) create_error_response(code int, message string, id string) string {
|
||||
error := JsonRpcError{
|
||||
code: code
|
||||
message: message
|
||||
data: 'null'
|
||||
}
|
||||
response := JsonRpcResponse{
|
||||
jsonrpc: '2.0'
|
||||
error: error
|
||||
id: id
|
||||
}
|
||||
return json.encode(response)
|
||||
}
|
||||
|
||||
// discover returns the OpenRPC specification for the service
|
||||
pub fn (mut server RPCServer) discover() !string {
|
||||
// Return a basic OpenRPC spec - should be overridden by implementations
|
||||
return '{"openrpc": "1.2.6", "info": {"title": "OpenRPC Server", "version": "1.0.0"}, "methods": []}'
|
||||
}
|
||||
@@ -70,7 +70,7 @@ pub fn decode_file_metadata(data []u8) !File {
|
||||
// blocksize is max 2 bytes, so max 4gb entry size
|
||||
blocksize := d.get_u16()!
|
||||
for i in 0 .. blocksize {
|
||||
chunk_ids << d.get_u32() or { return error('Failed to get block id ${err}') }
|
||||
chunk_ids << d.get_u32()! or { return error('Failed to get block id ${err}') }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user