From 55ebaa4d68ada5ec05965376a1ab6ebfd0b62048 Mon Sep 17 00:00:00 2001 From: Maxime Van Hees Date: Wed, 26 Nov 2025 15:20:46 +0100 Subject: [PATCH] first impl of client lib for herodb --- examples/clients/herodb.vsh | 34 ++++++++++ lib/clients/herodb/README.md | 52 ++++++++++++++ lib/clients/herodb/herodb.v | 127 +++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 examples/clients/herodb.vsh create mode 100644 lib/clients/herodb/README.md create mode 100644 lib/clients/herodb/herodb.v diff --git a/examples/clients/herodb.vsh b/examples/clients/herodb.vsh new file mode 100644 index 00000000..266a9243 --- /dev/null +++ b/examples/clients/herodb.vsh @@ -0,0 +1,34 @@ +#!/usr/bin/env -S v -n -w -enable-globals run + +import incubaid.herolib.clients.herodb + +// Initialize the client +mut client := herodb.new(herodb.Config{ + url: 'http://localhost:3000' +})! + +println('Connecting to HeroDB at ${client.server_url}...') + +// List instances +instances := client.list_instances()! + +println('Found ${instances.len} instances:') + +for instance in instances { + println('----------------------------------------') + println('Index: ${instance.index}') + println('Name: ${instance.name}') + println('Created At: ${instance.created_at}') + + // Parse backend info + backend := instance.get_backend_info() or { + println('Backend: Unknown/Error (${err})') + continue + } + + println('Backend Type: ${backend.type_name}') + if backend.path != '' { + println('Backend Path: ${backend.path}') + } +} +println('----------------------------------------') \ No newline at end of file diff --git a/lib/clients/herodb/README.md b/lib/clients/herodb/README.md new file mode 100644 index 00000000..8dccb071 --- /dev/null +++ b/lib/clients/herodb/README.md @@ -0,0 +1,52 @@ +# HeroDB Client + +A V client library for interacting with the HeroDB JSON-RPC API. + +## Features + +- Connects to HeroDB's JSON-RPC server (default port 3000). +- Lists running database instances. +- Parses polymorphic backend types (InMemory, Redb, LanceDb). + +## Usage + +```v +import incubaid.herolib.clients.herodb + +fn main() { + // Initialize the client + mut client := herodb.new(herodb.Config{ + url: 'http://localhost:3000' + })! + + // List instances + instances := client.list_instances()! + + for instance in instances { + println('Index: ${instance.index}') + println('Name: ${instance.name}') + + // Parse backend info + backend := instance.get_backend_info()! + println('Backend: ${backend.type_name}') + if backend.path != '' { + println('Path: ${backend.path}') + } + println('---') + } +} +``` + +## API Reference + +### `fn new(cfg Config) !HeroDB` + +Creates a new HeroDB client instance. + +### `fn (mut self HeroDB) list_instances() ![]InstanceMetadata` + +Retrieves a list of all currently loaded database instances. + +### `fn (m InstanceMetadata) get_backend_info() !BackendInfo` + +Helper method to parse the `backend_type` field from `InstanceMetadata` into a structured `BackendInfo` object. \ No newline at end of file diff --git a/lib/clients/herodb/herodb.v b/lib/clients/herodb/herodb.v new file mode 100644 index 00000000..6f313547 --- /dev/null +++ b/lib/clients/herodb/herodb.v @@ -0,0 +1,127 @@ +module herodb + +import json +import incubaid.herolib.core.httpconnection + +// JSON-RPC 2.0 Structures + +struct JsonRpcRequest { + jsonrpc string = '2.0' + method string + params []string + id int +} + +struct JsonRpcResponse[T] { + jsonrpc string + result T + error ?JsonRpcError + id int +} + +struct JsonRpcError { + code int + message string + data string +} + +// HeroDB Specific Structures + +pub struct InstanceMetadata { +pub: + index int + name string + // backend_type can be a string ("InMemory") or an object ({"Redb": "path"}). + // We use the `raw` attribute to capture the raw JSON and parse it manually. + backend_type string @[raw] + created_at string +} + +// Helper struct to represent the parsed backend info in a usable way +pub struct BackendInfo { +pub: + type_name string // "InMemory", "Redb", "LanceDb" + path string // Empty for InMemory +} + +pub struct HeroDB { +pub: + server_url string +pub mut: + conn ?&httpconnection.HTTPConnection +} + +pub struct Config { +pub: + url string = 'http://localhost:3000' +} + +pub fn new(cfg Config) !HeroDB { + return HeroDB{ + server_url: cfg.url + } +} + +pub fn (mut self HeroDB) connection() !&httpconnection.HTTPConnection { + if mut conn := self.conn { + return conn + } + + mut new_conn := httpconnection.new( + name: 'herodb' + url: self.server_url + retry: 3 + )! + self.conn = new_conn + return new_conn +} + +fn (mut self HeroDB) rpc_call[T](method string) !T { + mut conn := self.connection()! + + req := JsonRpcRequest{ + method: method + id: 1 + params: [] + } + + response := conn.post_json_generic[JsonRpcResponse[T]]( + method: .post + prefix: '' + data: json.encode(req) + dataformat: .json + )! + + if err := response.error { + return error('RPC Error ${err.code}: ${err.message}') + } + + return response.result +} + +pub fn (mut self HeroDB) list_instances() ![]InstanceMetadata { + return self.rpc_call[[]InstanceMetadata]('db_listInstances')! +} + +pub fn (m InstanceMetadata) get_backend_info() !BackendInfo { + if m.backend_type.len == 0 { + return error('empty backend_type') + } + if m.backend_type[0] == `"` { + // It's a string + val := json.decode(string, m.backend_type)! + return BackendInfo{ + type_name: val + } + } else if m.backend_type[0] == `{` { + // It's an object + val := json.decode(map[string]string, m.backend_type)! + for k, v in val { + return BackendInfo{ + type_name: k + path: v + } + } + } + return error('unknown backend_type format: ${m.backend_type}') +} \ No newline at end of file