diff --git a/aiprompts/herolib_advanced/openrpcexample.json b/aiprompts/herolib_advanced/openrpcexample.json new file mode 100644 index 00000000..55b087e6 --- /dev/null +++ b/aiprompts/herolib_advanced/openrpcexample.json @@ -0,0 +1,78 @@ +{ + "openrpc": "1.0.0-rc1", + "info": { + "title": "Simple RPC overview", + "version": "2.0.0" + }, + "methods": [ + { + "name": "get_versions", + "summary": "List API versions", + "params": [], + "result": { + "name": "get_version_result", + "schema": { + "type": "object" + } + }, + "examples": [ + { + "name": "v2", + "summary": "its a v2 example pairing!", + "description": "aight so this is how it works. You foo the bar then you baz the razmataz", + "params": [], + "result": { + "name": "versionsExample", + "value": { + "versions": [ + { + "status": "CURRENT", + "updated": "2011-01-21T11:33:21Z", + "id": "v2.0", + "urls": [ + { + "href": "http://127.0.0.1:8774/v2/", + "rel": "self" + } + ] + }, + { + "status": "EXPERIMENTAL", + "updated": "2013-07-23T11:33:21Z", + "id": "v3.0", + "urls": [ + { + "href": "http://127.0.0.1:8774/v3/", + "rel": "self" + } + ] + } + ] + } + } + } + ] + }, + { + "name": "get_version_details", + "summary": "Show API version details", + "params": [], + "result": { + "name": "foo", + "schema": { + "type": "string" + } + }, + "examples": [ + { + "name": "stringifiedVersionsExample", + "params": [], + "result": { + "name": "bliggityblaow", + "value": "{\n \"versions\": [\n {\n \"status\": \"CURRENT\",\n \"updated\": \"2011-01-21T11:33:21Z\",\n \"id\": \"v2.0\",\n \"urls\": [\n {\n \"href\": \"http://127.0.0.1:8774/v2/\",\n \"rel\": \"self\"\n }\n ]\n },\n {\n \"status\": \"EXPERIMENTAL\",\n \"updated\": \"2013-07-23T11:33:21Z\",\n \"id\": \"v3.0\",\n \"urls\": [\n {\n \"href\": \"http://127.0.0.1:8774/v3/\",\n \"rel\": \"self\"\n }\n ]\n }\n ]\n}\n" + } + } + ] + } + ] +} diff --git a/aiprompts/v_core/json/cjson.md b/aiprompts/v_core/json/cjson.md new file mode 100644 index 00000000..385ed19a --- /dev/null +++ b/aiprompts/v_core/json/cjson.md @@ -0,0 +1,212 @@ +# module cjson + + +## Contents +- [create_array](#create_array) +- [create_bool](#create_bool) +- [create_false](#create_false) +- [create_null](#create_null) +- [create_number](#create_number) +- [create_object](#create_object) +- [create_raw](#create_raw) +- [create_string](#create_string) +- [create_true](#create_true) +- [delete](#delete) +- [version](#version) +- [Node](#Node) + - [add_item_to_object](#add_item_to_object) + - [add_item_to_array](#add_item_to_array) + - [print](#print) + - [print_unformatted](#print_unformatted) + - [str](#str) +- [CJsonType](#CJsonType) +- [C.cJSON](#C.cJSON) + +## create_array +```v +fn create_array() &Node +``` + +create_array creates a new JSON array item. Use .add_item_to_array(value) calls, to add items to it later. + +[[Return to contents]](#Contents) + +## create_bool +```v +fn create_bool(val bool) &Node +``` + +create_bool creates a new JSON boolean item. + +[[Return to contents]](#Contents) + +## create_false +```v +fn create_false() &Node +``` + +create_false creates a new JSON boolean item, with value `false`. + +[[Return to contents]](#Contents) + +## create_null +```v +fn create_null() &Node +``` + +create_null creates a new JSON NULL item, with the value `null`. It symbolises a missing value for a given key in an object. + +[[Return to contents]](#Contents) + +## create_number +```v +fn create_number(val f64) &Node +``` + +create_number creates a new JSON number item. + +[[Return to contents]](#Contents) + +## create_object +```v +fn create_object() &Node +``` + +create_object creates a new JSON object/map item. Use .add_item_to_object(key, value) calls, to add other items to it later. + +[[Return to contents]](#Contents) + +## create_raw +```v +fn create_raw(const_val string) &Node +``` + +create_raw creates a new JSON RAW string item. + +[[Return to contents]](#Contents) + +## create_string +```v +fn create_string(val string) &Node +``` + +create_string creates a new JSON string item. + +[[Return to contents]](#Contents) + +## create_true +```v +fn create_true() &Node +``` + +create_true creates a new JSON boolean item, with value `true`. + +[[Return to contents]](#Contents) + +## delete +```v +fn delete(node &Node) +``` + +delete removes the given node from memory. NB: DO NOT USE that node, after you have called `unsafe { delete(node) }` ! + +[[Return to contents]](#Contents) + +## version +```v +fn version() string +``` + +version returns the version of cJSON as a string. + +[[Return to contents]](#Contents) + +## Node +```v +type Node = C.cJSON +``` + +[[Return to contents]](#Contents) + +## add_item_to_object +```v +fn (mut obj Node) add_item_to_object(key string, item &Node) +``` + +add_item_to_array adds the given item to the object, under the given `key`. + +[[Return to contents]](#Contents) + +## add_item_to_array +```v +fn (mut obj Node) add_item_to_array(item &Node) +``` + +add_item_to_array append the given item to the object. + +[[Return to contents]](#Contents) + +## print +```v +fn (mut obj Node) print() string +``` + +print serialises the node to a string, formatting its structure, so the resulting string is more prettier/human readable. + +[[Return to contents]](#Contents) + +## print_unformatted +```v +fn (mut obj Node) print_unformatted() string +``` + +print serialises the node to a string, without formatting its structure, so the resulting string is shorter/cheaper to transmit. + +[[Return to contents]](#Contents) + +## str +```v +fn (mut obj Node) str() string +``` + +str returns the unformatted serialisation to string of the given Node. + +[[Return to contents]](#Contents) + +## CJsonType +```v +enum CJsonType { + t_false + t_true + t_null + t_number + t_string + t_array + t_object + t_raw +} +``` + +[[Return to contents]](#Contents) + +## C.cJSON +```v +struct C.cJSON { +pub: + next &C.cJSON // next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem + prev &C.cJSON + child &C.cJSON // An array or object item will have a child pointer pointing to a chain of the items in the array/object + + type CJsonType // The type of the item, as above + + valueint int // writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead + valuedouble f64 // The item's number, if type==cJSON_Number + valuestring &char // The item's string, if type==cJSON_String and type == cJSON_Raw + // @string &char // The item's name string, if this item is the child of, or is in the list of subitems of an object + // TODO: `@string &char` from above does not work. It should be fixed, at least inside `struct C.`. +} +``` + +[[Return to contents]](#Contents) + +#### Powered by vdoc. Generated on: 2 Sep 2025 07:37:38 diff --git a/aiprompts/v_core/json/json.md b/aiprompts/v_core/json/json.md new file mode 100644 index 00000000..a52bee2a --- /dev/null +++ b/aiprompts/v_core/json/json.md @@ -0,0 +1,48 @@ +# module json + + +## Contents +- [decode](#decode) +- [encode](#encode) +- [encode_pretty](#encode_pretty) +- [C.cJSON](#C.cJSON) + +## decode +```v +fn decode(typ voidptr, s string) !voidptr +``` + +decode tries to decode the provided JSON string, into a V structure. If it can not do that, it returns an error describing the reason for the parsing failure. + +[[Return to contents]](#Contents) + +## encode +```v +fn encode(x voidptr) string +``` + +encode serialises the provided V value as a JSON string, optimised for shortness. + +[[Return to contents]](#Contents) + +## encode_pretty +```v +fn encode_pretty(x voidptr) string +``` + +encode_pretty serialises the provided V value as a JSON string, in a formatted way, optimised for viewing by humans. + +[[Return to contents]](#Contents) + +## C.cJSON +```v +struct C.cJSON { + valueint int + valuedouble f64 + valuestring &char +} +``` + +[[Return to contents]](#Contents) + +#### Powered by vdoc. Generated on: 2 Sep 2025 07:37:38 diff --git a/aiprompts/v_core/json2/decoder2.md b/aiprompts/v_core/json2/decoder2.md new file mode 100644 index 00000000..b86483b7 --- /dev/null +++ b/aiprompts/v_core/json2/decoder2.md @@ -0,0 +1,100 @@ +# module decoder2 + + +## Contents +- [decode](#decode) +- [decode_array](#decode_array) +- [BooleanDecoder](#BooleanDecoder) +- [NullDecoder](#NullDecoder) +- [NumberDecoder](#NumberDecoder) +- [StringDecoder](#StringDecoder) +- [JsonDecodeError](#JsonDecodeError) + +## decode +```v +fn decode[T](val string) !T +``` + +decode decodes a JSON string into a specified type. + +[[Return to contents]](#Contents) + +## decode_array +```v +fn decode_array[T](src string) !T +``` + +decode_array is a generic function that decodes a JSON string into the array target type. + +[[Return to contents]](#Contents) + +## BooleanDecoder +```v +interface BooleanDecoder { +mut: + // called with converted bool + // already checked so no error needed + from_json_boolean(boolean_value bool) +} +``` + +implements decoding json true/false + +[[Return to contents]](#Contents) + +## NullDecoder +```v +interface NullDecoder { +mut: + // only has one value + // already checked so no error needed + from_json_null() +} +``` + +implements decoding json null + +[[Return to contents]](#Contents) + +## NumberDecoder +```v +interface NumberDecoder { +mut: + // called with raw string of number e.g. '-1.234e23' + from_json_number(raw_number string) ! +} +``` + +implements decoding json numbers, e.g. -1.234e23 + +[[Return to contents]](#Contents) + +## StringDecoder +```v +interface StringDecoder { +mut: + // called with raw string (minus apostrophes) e.g. 'hello, \u2164!' + from_json_string(raw_string string) ! +} +``` + +implements decoding json strings, e.g. "hello, \u2164!" + +[[Return to contents]](#Contents) + +## JsonDecodeError +```v +struct JsonDecodeError { + Error + context string +pub: + message string + + line int + character int +} +``` + +[[Return to contents]](#Contents) + +#### Powered by vdoc. Generated on: 2 Sep 2025 07:37:54 diff --git a/aiprompts/v_core/json2/strict.md b/aiprompts/v_core/json2/strict.md new file mode 100644 index 00000000..c1310632 --- /dev/null +++ b/aiprompts/v_core/json2/strict.md @@ -0,0 +1,63 @@ +# module strict + + +## Contents +- [get_keys_from_json](#get_keys_from_json) +- [strict_check](#strict_check) +- [KeyType](#KeyType) +- [KeyStruct](#KeyStruct) +- [StructCheckResult](#StructCheckResult) + +## get_keys_from_json +```v +fn get_keys_from_json(tokens []string) []KeyStruct +``` + +get_keys_from_json . + +[[Return to contents]](#Contents) + +## strict_check +```v +fn strict_check[T](json_data string) StructCheckResult +``` + +strict_check . + +[[Return to contents]](#Contents) + +## KeyType +```v +enum KeyType { + literal + map + array +} +``` + +[[Return to contents]](#Contents) + +## KeyStruct +```v +struct KeyStruct { +pub: + key string + value_type KeyType + token_pos int // the position of the token +} +``` + +[[Return to contents]](#Contents) + +## StructCheckResult +```v +struct StructCheckResult { +pub: + duplicates []string + superfluous []string +} +``` + +[[Return to contents]](#Contents) + +#### Powered by vdoc. Generated on: 2 Sep 2025 07:37:54 diff --git a/aiprompts/v_core/json2/x.json2.md b/aiprompts/v_core/json2/x.json2.md new file mode 100644 index 00000000..9311c316 --- /dev/null +++ b/aiprompts/v_core/json2/x.json2.md @@ -0,0 +1,490 @@ +# module x.json2 + + +## Contents +- [Constants](#Constants) +- [decode](#decode) +- [decode_array](#decode_array) +- [encode](#encode) +- [encode_pretty](#encode_pretty) +- [fast_raw_decode](#fast_raw_decode) +- [map_from](#map_from) +- [raw_decode](#raw_decode) +- [Encodable](#Encodable) +- [Any](#Any) + - [arr](#arr) + - [as_map](#as_map) + - [as_map_of_strings](#as_map_of_strings) + - [bool](#bool) + - [f32](#f32) + - [f64](#f64) + - [i16](#i16) + - [i32](#i32) + - [i64](#i64) + - [i8](#i8) + - [int](#int) + - [json_str](#json_str) + - [prettify_json_str](#prettify_json_str) + - [str](#str) + - [to_time](#to_time) + - [u16](#u16) + - [u32](#u32) + - [u64](#u64) + - [u8](#u8) +- [Parser](#Parser) + - [decode](#decode) +- [[]Any](#[]Any) + - [str](#str) +- [map[string]Any](#map[string]Any) + - [str](#str) +- [DecodeError](#DecodeError) + - [code](#code) + - [msg](#msg) +- [Encoder](#Encoder) + - [encode_value](#encode_value) +- [InvalidTokenError](#InvalidTokenError) + - [code](#code) + - [msg](#msg) +- [Null](#Null) + - [from_json_null](#from_json_null) +- [Token](#Token) + - [full_col](#full_col) +- [UnknownTokenError](#UnknownTokenError) + - [code](#code) + - [msg](#msg) + +## Constants +```v +const null = Null{} +``` + +null is an instance of the Null type, to ease comparisons with it. + +[[Return to contents]](#Contents) + +## decode +```v +fn decode[T](src string) !T +``` + +decode is a generic function that decodes a JSON string into the target type. + +[[Return to contents]](#Contents) + +## decode_array +```v +fn decode_array[T](src string) ![]T +``` + +decode_array is a generic function that decodes a JSON string into the array target type. + +[[Return to contents]](#Contents) + +## encode +```v +fn encode[T](val T) string +``` + +encode is a generic function that encodes a type into a JSON string. + +[[Return to contents]](#Contents) + +## encode_pretty +```v +fn encode_pretty[T](typed_data T) string +``` + +encode_pretty ... + +[[Return to contents]](#Contents) + +## fast_raw_decode +```v +fn fast_raw_decode(src string) !Any +``` + +Same with `raw_decode`, but skips the type conversion for certain types when decoding a certain value. + +[[Return to contents]](#Contents) + +## map_from +```v +fn map_from[T](t T) map[string]Any +``` + +map_from converts a struct to a map of Any. + +[[Return to contents]](#Contents) + +## raw_decode +```v +fn raw_decode(src string) !Any +``` + +Decodes a JSON string into an `Any` type. Returns an option. + +[[Return to contents]](#Contents) + +## Encodable +```v +interface Encodable { + json_str() string +} +``` + +Encodable is an interface, that allows custom implementations for encoding structs to their string based JSON representations. + +[[Return to contents]](#Contents) + +## Any +## arr +```v +fn (f Any) arr() []Any +``` + +arr uses `Any` as an array. + +[[Return to contents]](#Contents) + +## as_map +```v +fn (f Any) as_map() map[string]Any +``` + +as_map uses `Any` as a map. + +[[Return to contents]](#Contents) + +## as_map_of_strings +```v +fn (f Any) as_map_of_strings() map[string]string +``` + +[[Return to contents]](#Contents) + +## bool +```v +fn (f Any) bool() bool +``` + +bool uses `Any` as a bool. + +[[Return to contents]](#Contents) + +## f32 +```v +fn (f Any) f32() f32 +``` + +f32 uses `Any` as a 32-bit float. + +[[Return to contents]](#Contents) + +## f64 +```v +fn (f Any) f64() f64 +``` + +f64 uses `Any` as a 64-bit float. + +[[Return to contents]](#Contents) + +## i16 +```v +fn (f Any) i16() i16 +``` + +i16 uses `Any` as a 16-bit integer. + +[[Return to contents]](#Contents) + +## i32 +```v +fn (f Any) i32() i32 +``` + +i32 uses `Any` as a 32-bit integer. + +[[Return to contents]](#Contents) + +## i64 +```v +fn (f Any) i64() i64 +``` + +i64 uses `Any` as a 64-bit integer. + +[[Return to contents]](#Contents) + +## i8 +```v +fn (f Any) i8() i8 +``` + +i8 uses `Any` as a 16-bit integer. + +[[Return to contents]](#Contents) + +## int +```v +fn (f Any) int() int +``` + +int uses `Any` as an integer. + +[[Return to contents]](#Contents) + +## json_str +```v +fn (f Any) json_str() string +``` + +json_str returns the JSON string representation of the `Any` type. + +[[Return to contents]](#Contents) + +## prettify_json_str +```v +fn (f Any) prettify_json_str() string +``` + +prettify_json_str returns the pretty-formatted JSON string representation of the `Any` type. + +[[Return to contents]](#Contents) + +## str +```v +fn (f Any) str() string +``` + +str returns the string representation of the `Any` type. Use the `json_str` method. If you want to use the escaped str() version of the `Any` type. + +[[Return to contents]](#Contents) + +## to_time +```v +fn (f Any) to_time() !time.Time +``` + +to_time uses `Any` as a time.Time. + +[[Return to contents]](#Contents) + +## u16 +```v +fn (f Any) u16() u16 +``` + +u16 uses `Any` as a 16-bit unsigned integer. + +[[Return to contents]](#Contents) + +## u32 +```v +fn (f Any) u32() u32 +``` + +u32 uses `Any` as a 32-bit unsigned integer. + +[[Return to contents]](#Contents) + +## u64 +```v +fn (f Any) u64() u64 +``` + +u64 uses `Any` as a 64-bit unsigned integer. + +[[Return to contents]](#Contents) + +## u8 +```v +fn (f Any) u8() u8 +``` + +u8 uses `Any` as a 8-bit unsigned integer. + +[[Return to contents]](#Contents) + +## Parser +## decode +```v +fn (mut p Parser) decode() !Any +``` + +decode - decodes provided JSON + +[[Return to contents]](#Contents) + +## []Any +## str +```v +fn (f []Any) str() string +``` + +str returns the JSON string representation of the `[]Any` type. + +[[Return to contents]](#Contents) + +## map[string]Any +## str +```v +fn (f map[string]Any) str() string +``` + +str returns the JSON string representation of the `map[string]Any` type. + +[[Return to contents]](#Contents) + +## DecodeError +```v +struct DecodeError { + line int + column int + message string +} +``` + +[[Return to contents]](#Contents) + +## code +```v +fn (err DecodeError) code() int +``` + +code returns the error code of DecodeError + +[[Return to contents]](#Contents) + +## msg +```v +fn (err DecodeError) msg() string +``` + +msg returns the message of the DecodeError + +[[Return to contents]](#Contents) + +## Encoder +```v +struct Encoder { +pub: + newline u8 + newline_spaces_count int + escape_unicode bool = true +} +``` + +Encoder encodes the an `Any` type into JSON representation. It provides parameters in order to change the end result. + +[[Return to contents]](#Contents) + +## encode_value +```v +fn (e &Encoder) encode_value[T](val T, mut buf []u8) ! +``` + +encode_value encodes a value to the specific buffer. + +[[Return to contents]](#Contents) + +## InvalidTokenError +```v +struct InvalidTokenError { + DecodeError + token Token + expected TokenKind +} +``` + +[[Return to contents]](#Contents) + +## code +```v +fn (err InvalidTokenError) code() int +``` + +code returns the error code of the InvalidTokenError + +[[Return to contents]](#Contents) + +## msg +```v +fn (err InvalidTokenError) msg() string +``` + +msg returns the message of the InvalidTokenError + +[[Return to contents]](#Contents) + +## Null +```v +struct Null { + is_null bool = true +} +``` + +Null is a simple representation of the `null` value in JSON. + +[[Return to contents]](#Contents) + +## from_json_null +```v +fn (mut n Null) from_json_null() +``` + +from_json_null implements a custom decoder for json2 + +[[Return to contents]](#Contents) + +## Token +```v +struct Token { + lit []u8 // literal representation of the token + kind TokenKind // the token number/enum; for quick comparisons + line int // the line in the source where the token occurred + col int // the column in the source where the token occurred +} +``` + +[[Return to contents]](#Contents) + +## full_col +```v +fn (t Token) full_col() int +``` + +full_col returns the full column information which includes the length. + +[[Return to contents]](#Contents) + +## UnknownTokenError +```v +struct UnknownTokenError { + DecodeError + token Token + kind ValueKind = .unknown +} +``` + +[[Return to contents]](#Contents) + +## code +```v +fn (err UnknownTokenError) code() int +``` + +code returns the error code of the UnknownTokenError + +[[Return to contents]](#Contents) + +## msg +```v +fn (err UnknownTokenError) msg() string +``` + +msg returns the error message of the UnknownTokenError + +[[Return to contents]](#Contents) + +#### Powered by vdoc. Generated on: 2 Sep 2025 07:37:54 diff --git a/lib/hero/heromodels/ai_openrpcprompt.md b/lib/hero/heromodels/ai_openrpcprompt.md new file mode 100644 index 00000000..49d2bcc2 --- /dev/null +++ b/lib/hero/heromodels/ai_openrpcprompt.md @@ -0,0 +1,27 @@ +lets make an openrpc server +over unixsocker + +on /tmp/heromodels + +put code in lib/hero/heromodels/openrpc + +do example for comment.v + +make struct called RPCServer + +put as methods + +- comment_get(args CommentGetArgs)[]Comment! //chose the params well is @[params] struct CommentGetArgs always with id… in this case maybe author. … + - walk over the hset with data, find the one we are looking for based on the args +- comment_set(obj Comment)! +- comment_delete(id…) +- comment_list() ![]u32 +- discover()!string //returns a full openrpc spec + +make one .v file per type of object now comment_… + +we will then do for the other objects too + +also generate the openrpc spec based on the methods we have and the objects we return + + diff --git a/lib/hero/heromodels/openrpc/README.md b/lib/hero/heromodels/openrpc/README.md new file mode 100644 index 00000000..57153d7c --- /dev/null +++ b/lib/hero/heromodels/openrpc/README.md @@ -0,0 +1,130 @@ +# HeroModels OpenRPC Server + +This module provides an OpenRPC server for HeroModels that runs over Unix domain sockets. It exposes comment management functionality through a JSON-RPC 2.0 interface. + +## Features + +- **Unix Socket Communication**: Efficient local communication via Unix domain sockets +- **JSON-RPC 2.0 Protocol**: Standard JSON-RPC 2.0 implementation +- **Comment Management**: Full CRUD operations for comments +- **OpenRPC Specification**: Auto-generated OpenRPC spec via `discover` method +- **Concurrent Handling**: Multiple client connections supported + +## API Methods + +### comment_get +Retrieve comments by ID, author, or parent. + +**Parameters:** +- `id` (optional): Comment ID to retrieve +- `author` (optional): Author ID to filter by +- `parent` (optional): Parent comment ID to filter by + +**Returns:** Comment object or array of comments + +### comment_set +Create a new comment. + +**Parameters:** +- `comment`: Comment text content +- `parent`: Parent comment ID (0 for top-level) +- `author`: Author user ID + +**Returns:** Object with created comment ID + +### comment_delete +Delete a comment by ID. + +**Parameters:** +- `id`: Comment ID to delete + +**Returns:** Success status and deleted comment ID + +### comment_list +List all comment IDs. + +**Parameters:** None + +**Returns:** Array of all comment IDs + +### discover +Get the OpenRPC specification for this service. + +**Parameters:** None + +**Returns:** Complete OpenRPC specification object + +## Usage + +### Starting the Server + +```v +import freeflowuniverse.herolib.hero.heromodels.openrpc + +mut server := openrpc.new_rpc_server(socket_path: '/tmp/heromodels')! +server.start()! // Blocks and serves requests +``` + +### Example Client + +```v +import net.unix +import json +import freeflowuniverse.herolib.hero.heromodels.openrpc + +// Connect to server +mut conn := unix.connect_stream('/tmp/heromodels')! + +// Create a comment +request := openrpc.JsonRpcRequest{ + jsonrpc: '2.0' + method: 'comment_set' + params: json.encode({ + 'comment': 'Hello World' + 'parent': 0 + 'author': 1 + }) + id: 1 +} + +// Send request +conn.write_string(json.encode(request))! + +// Read response +mut buffer := []u8{len: 4096} +bytes_read := conn.read(mut buffer)! +response := buffer[..bytes_read].bytestr() +``` + +## Files + +- `server.v` - Main RPC server implementation +- `types.v` - JSON-RPC and parameter type definitions +- `comment.v` - Comment-specific RPC method implementations +- `discover.v` - OpenRPC specification generation +- `example.vsh` - Server example script +- `client_example.vsh` - Client example script + +## Running Examples + +Start the server: +```bash +vrun lib/hero/heromodels/openrpc/example.vsh +``` + +Test with client (in another terminal): +```bash +vrun lib/hero/heromodels/openrpc/client_example.vsh +``` + +## Dependencies + +- Redis (for data storage via heromodels) +- Unix domain socket support +- JSON encoding/decoding + +## Socket Path + +Default socket path: `/tmp/heromodels` + +The socket file is automatically cleaned up when the server starts and stops. \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/client_example.vsh b/lib/hero/heromodels/openrpc/client_example.vsh new file mode 100644 index 00000000..1e9d796f --- /dev/null +++ b/lib/hero/heromodels/openrpc/client_example.vsh @@ -0,0 +1,88 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import net.unix +import x.json2 +import freeflowuniverse.herolib.ui.console +import freeflowuniverse.herolib.hero.heromodels.openrpc + +// Example client to test the HeroModels OpenRPC server +fn main() { + console.print_header('HeroModels OpenRPC Client Example') + + // Connect to the server + mut conn := unix.connect_stream('/tmp/heromodels')! + defer { + conn.close() or {} + } + + console.print_item('Connected to server') + + // Test 1: Get OpenRPC specification + console.print_header('Test 1: Discover OpenRPC Specification') + discover_request := openrpc.JsonRpcRequest{ + jsonrpc: '2.0' + method: 'discover' + params: json2.null + id: json2.Any(1) + } + + send_request(mut conn, discover_request)! + response := read_response(mut conn)! + console.print_item('OpenRPC Spec received: ${response.len} characters') + + // Test 2: Create a comment + console.print_header('Test 2: Create Comment') + comment_json := '{"comment": "This is a test comment from OpenRPC client", "parent": 0, "author": 1}' + + create_request := openrpc.JsonRpcRequest{ + jsonrpc: '2.0' + method: 'comment_set' + params: json2.raw_decode(comment_json)! + id: json2.Any(2) + } + + send_request(mut conn, create_request)! + create_response := read_response(mut conn)! + console.print_item('Comment created: ${create_response}') + + // Test 3: List all comments + console.print_header('Test 3: List All Comments') + list_request := openrpc.JsonRpcRequest{ + jsonrpc: '2.0' + method: 'comment_list' + params: json2.null + id: json2.Any(3) + } + + send_request(mut conn, list_request)! + list_response := read_response(mut conn)! + console.print_item('Comment list: ${list_response}') + + // Test 4: Get comment by author + console.print_header('Test 4: Get Comments by Author') + get_args_json := '{"author": 1}' + + get_request := openrpc.JsonRpcRequest{ + jsonrpc: '2.0' + method: 'comment_get' + params: json2.raw_decode(get_args_json)! + id: json2.Any(4) + } + + send_request(mut conn, get_request)! + get_response := read_response(mut conn)! + console.print_item('Comments by author: ${get_response}') + + console.print_header('All tests completed successfully!') +} + +fn send_request(mut conn unix.StreamConn, request openrpc.JsonRpcRequest) ! { + request_json := json2.encode(request) + conn.write_string(request_json)! +} + +fn read_response(mut conn unix.StreamConn) !string { + mut buffer := []u8{len: 8192} + bytes_read := conn.read(mut buffer)! + return buffer[..bytes_read].bytestr() +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/comment.v b/lib/hero/heromodels/openrpc/comment.v new file mode 100644 index 00000000..6e9205dc --- /dev/null +++ b/lib/hero/heromodels/openrpc/comment.v @@ -0,0 +1,97 @@ +module openrpc + +import x.json2 +import freeflowuniverse.herolib.hero.heromodels +import freeflowuniverse.herolib.core.redisclient + +// comment_get retrieves comments based on the provided arguments +fn (mut server RPCServer) comment_get(params json2.Any) !json2.Any { + args := json2.decode[CommentGetArgs](params.json_str())! + + // If ID is provided, get specific comment + if id := args.id { + comment := heromodels.comment_get(id)! + return json2.encode(comment) + } + + // If author is provided, find comments by author + if author := args.author { + return server.get_comments_by_author(author)! + } + + // If parent is provided, find child comments + if parent := args.parent { + return server.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 +fn (mut server RPCServer) comment_set(params json2.Any) !json2.Any { + comment_arg := json2.decode[heromodels.CommentArg](params.json_str())! + id := heromodels.comment_set(comment_arg)! + return json2.encode({'id': id}) +} + +// comment_delete removes a comment by ID +fn (mut server RPCServer) comment_delete(params json2.Any) !json2.Any { + args := json2.decode[CommentDeleteArgs](params.json_str())! + + // Check if comment exists + if !heromodels.comment_exist(args.id)! { + return error('Comment with id ${args.id} does not exist') + } + + // Delete from Redis + mut redis := redisclient.core_get()! + redis.hdel('db:comments:data', args.id.str())! + + result_json := '{"success": true, "id": ${args.id}}' + return json2.raw_decode(result_json)! +} + +// comment_list returns all comment IDs +fn (mut server RPCServer) comment_list() !json2.Any { + mut redis := redisclient.core_get()! + keys := redis.hkeys('db:comments:data')! + mut ids := []u32{} + + for key in keys { + ids << key.u32() + } + + return json2.encode(ids) +} + +// Helper function to get comments by author +fn (mut server RPCServer) get_comments_by_author(author u32) !json2.Any { + mut redis := redisclient.core_get()! + all_data := redis.hgetall('db:comments:data')! + mut matching_comments := []heromodels.Comment{} + + for _, data in all_data { + comment := heromodels.comment_load(data.bytes())! + if comment.author == author { + matching_comments << comment + } + } + + return json2.encode(matching_comments) +} + +// Helper function to get comments by parent +fn (mut server RPCServer) get_comments_by_parent(parent u32) !json2.Any { + mut redis := redisclient.core_get()! + all_data := redis.hgetall('db:comments:data')! + mut matching_comments := []heromodels.Comment{} + + for _, data in all_data { + comment := heromodels.comment_load(data.bytes())! + if comment.parent == parent { + matching_comments << comment + } + } + + return json2.encode(matching_comments) +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/discover.v b/lib/hero/heromodels/openrpc/discover.v new file mode 100644 index 00000000..36ee8ed7 --- /dev/null +++ b/lib/hero/heromodels/openrpc/discover.v @@ -0,0 +1,9 @@ +module openrpc + +import x.json2 + +// discover returns the OpenRPC specification for the HeroModels service +fn (mut server RPCServer) discover() !string { + spec_json := $tmpl("openrpc.json") + return spec_json +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/example.vsh b/lib/hero/heromodels/openrpc/example.vsh new file mode 100644 index 00000000..12bbf04f --- /dev/null +++ b/lib/hero/heromodels/openrpc/example.vsh @@ -0,0 +1,26 @@ +#!/usr/bin/env -S v -n -w -cg -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.hero.heromodels.openrpc +import freeflowuniverse.herolib.hero.heromodels +import freeflowuniverse.herolib.ui.console +import time +import os + +// Example usage of the HeroModels OpenRPC server +fn main() { + console.print_header('HeroModels OpenRPC Server Example') + + // Create and start the server + mut server := openrpc.new_rpc_server(socket_path: '/tmp/heromodels')! + + // Start server in a separate thread + spawn server.start() + + console.print_item('Server started on /tmp/heromodels') + console.print_item('Press Ctrl+C to stop the server') + + // Keep the main thread alive + for { + time.sleep(1 * time.second) + } +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/openrpc.json b/lib/hero/heromodels/openrpc/openrpc.json new file mode 100644 index 00000000..d03b493e --- /dev/null +++ b/lib/hero/heromodels/openrpc/openrpc.json @@ -0,0 +1,213 @@ +{ + "openrpc": "1.0.0-rc1", + "info": { + "version": "1.0.0", + "title": "HeroModels OpenRPC API", + "description": "OpenRPC API for HeroModels comment management over Unix socket", + "contact": { + "name": "HeroLib Team", + "url": "https://github.com/freeflowuniverse/herolib" + } + }, + "servers": [ + { + "name": "Unix Socket Server", + "url": "${server.socket_path}", + "description": "Unix domain socket server for HeroModels" + } + ], + "methods": [ + { + "name": "comment_get", + "description": "Retrieve comments by ID, author, or parent", + "params": [ + { + "name": "args", + "description": "Comment search arguments", + "required": true, + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Comment ID to retrieve" + }, + "author": { + "type": "integer", + "description": "Author ID to filter by" + }, + "parent": { + "type": "integer", + "description": "Parent comment ID to filter by" + } + } + } + } + ], + "result": { + "name": "comments", + "description": "Comment(s) matching the criteria", + "schema": { + "oneOf": [ + { + "\\$ref": "#/components/schemas/Comment" + }, + { + "type": "array", + "items": { + "\\$ref": "#/components/schemas/Comment" + } + } + ] + } + } + }, + { + "name": "comment_set", + "description": "Create a new comment", + "params": [ + { + "name": "comment", + "description": "Comment data to create", + "required": true, + "schema": { + "\\$ref": "#/components/schemas/CommentArg" + } + } + ], + "result": { + "name": "result", + "description": "Created comment ID", + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of the created comment" + } + } + } + } + }, + { + "name": "comment_delete", + "description": "Delete a comment by ID", + "params": [ + { + "name": "args", + "description": "Comment deletion arguments", + "required": true, + "schema": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "ID of comment to delete", + "required": true + } + } + } + } + ], + "result": { + "name": "result", + "description": "Deletion result", + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "id": { + "type": "integer" + } + } + } + } + }, + { + "name": "comment_list", + "description": "List all comment IDs", + "params": [], + "result": { + "name": "ids", + "description": "Array of all comment IDs", + "schema": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + { + "name": "discover", + "description": "Get the OpenRPC specification for this service", + "params": [], + "result": { + "name": "spec", + "description": "OpenRPC specification", + "schema": { + "type": "object" + } + } + } + ], + "components": { + "schemas": { + "Comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "Unique comment identifier" + }, + "comment": { + "type": "string", + "description": "Comment text content" + }, + "parent": { + "type": "integer", + "description": "Parent comment ID (0 if top-level)" + }, + "updated_at": { + "type": "integer", + "description": "Unix timestamp of last update" + }, + "author": { + "type": "integer", + "description": "Author user ID" + } + }, + "required": [ + "id", + "comment", + "parent", + "updated_at", + "author" + ] + }, + "CommentArg": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "description": "Comment text content" + }, + "parent": { + "type": "integer", + "description": "Parent comment ID (0 if top-level)" + }, + "author": { + "type": "integer", + "description": "Author user ID" + } + }, + "required": [ + "comment", + "author" + ] + } + } + } +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/server.v b/lib/hero/heromodels/openrpc/server.v new file mode 100644 index 00000000..26dddf49 --- /dev/null +++ b/lib/hero/heromodels/openrpc/server.v @@ -0,0 +1,133 @@ +module openrpc + +import x.json2 +import net.unix +import os +import freeflowuniverse.herolib.ui.console + +pub struct RPCServer { +mut: + listener &unix.StreamListener + socket_path string +} + +@[params] +pub struct RPCServerArgs { +pub mut: + socket_path string = '/tmp/heromodels' +} + +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}', json2.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 + request := json2.decode[JsonRpcRequest](request_data)! + + // Route to appropriate method + result := match request.method { + 'comment_get' { + server.comment_get(request.params)! + } + 'comment_set' { + server.comment_set(request.params)! + } + 'comment_delete' { + server.comment_delete(request.params)! + } + 'comment_list' { + server.comment_list()! + } + 'discover' { + server.discover()! + } + else { + return server.create_error_response(-32601, 'Method not found', request.id) + } + } + + return server.create_success_response(result, request.id) +} + +fn (mut server RPCServer) create_success_response(result json2.Any, id json2.Any) string { + response := JsonRpcResponse{ + jsonrpc: '2.0' + result: result + id: id + } + return json2.encode(response) +} + +fn (mut server RPCServer) create_error_response(code int, message string, id json2.Any) string { + error := JsonRpcError{ + code: code + message: message + data: json2.null + } + response := JsonRpcResponse{ + jsonrpc: '2.0' + error: error + id: id + } + return json2.encode(response) +} \ No newline at end of file diff --git a/lib/hero/heromodels/openrpc/types.v b/lib/hero/heromodels/openrpc/types.v new file mode 100644 index 00000000..ba571add --- /dev/null +++ b/lib/hero/heromodels/openrpc/types.v @@ -0,0 +1,44 @@ +module openrpc + +import x.json2 + +// JSON-RPC 2.0 request structure +pub struct JsonRpcRequest { +pub: + jsonrpc string = '2.0' + method string + params json2.Any + id json2.Any +} + +// JSON-RPC 2.0 response structure +pub struct JsonRpcResponse { +pub: + jsonrpc string = '2.0' + result json2.Any + error ?JsonRpcError + id json2.Any +} + +// JSON-RPC 2.0 error structure +pub struct JsonRpcError { +pub: + code int + message string + data json2.Any +} + +// Comment-specific argument structures +@[params] +pub struct CommentGetArgs { +pub mut: + id ?u32 + author ?u32 + parent ?u32 +} + +@[params] +pub struct CommentDeleteArgs { +pub mut: + id u32 +} \ No newline at end of file