port schema modules from crystallib
This commit is contained in:
124
lib/schemas/jsonrpc/README.md
Normal file
124
lib/schemas/jsonrpc/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# JSON-RPC Module
|
||||
|
||||
This module provides a robust implementation of the JSON-RPC 2.0 protocol in VLang. It includes utilities for creating, sending, and handling JSON-RPC requests and responses, with support for custom transports, strong typing, and error management.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Request and Response Handling**:
|
||||
- Create and encode JSON-RPC requests (generic or non-generic).
|
||||
- Decode and validate JSON-RPC responses.
|
||||
- Manage custom parameters and IDs for requests.
|
||||
|
||||
- **Error Management**:
|
||||
- Predefined JSON-RPC errors based on the official specification.
|
||||
- Support for custom error creation and validation.
|
||||
|
||||
- **Generic Support**:
|
||||
- Strongly typed request and response handling using generics.
|
||||
|
||||
- **Customizable Transport**:
|
||||
- Pluggable transport client interface for flexibility (e.g., WebSocket, HTTP).
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. **Client Setup**
|
||||
|
||||
Create a new JSON-RPC client using a custom transport layer.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Implement the IRPCTransportClient interface for your transport (e.g., WebSocket)
|
||||
struct WebSocketTransport {
|
||||
// Add your transport-specific implementation here
|
||||
}
|
||||
|
||||
// Create a new JSON-RPC client
|
||||
mut client := jsonrpc.new_client(jsonrpc.Client{
|
||||
transport: WebSocketTransport{}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. **Sending a Request**
|
||||
|
||||
Send a strongly-typed JSON-RPC request and handle the response.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Define a request method and parameters
|
||||
params := YourParams{...}
|
||||
request := jsonrpc.new_request_generic('methodName', params)
|
||||
|
||||
// Configure send parameters
|
||||
send_params := jsonrpc.SendParams{
|
||||
timeout: 30
|
||||
retry: 3
|
||||
}
|
||||
|
||||
// Send the request and process the response
|
||||
response := client.send[YourParams, YourResult](request, send_params) or {
|
||||
eprintln('Error sending request: $err')
|
||||
return
|
||||
}
|
||||
|
||||
println('Response result: $response')
|
||||
```
|
||||
|
||||
### 3. **Handling Errors**
|
||||
|
||||
Use the predefined JSON-RPC errors or create custom ones.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Predefined error
|
||||
err := jsonrpc.method_not_found
|
||||
|
||||
// Custom error
|
||||
custom_err := jsonrpc.RPCError{
|
||||
code: 12345
|
||||
message: 'Custom error message'
|
||||
data: 'Additional details'
|
||||
}
|
||||
|
||||
// Attach the error to a response
|
||||
response := jsonrpc.new_error('request_id', custom_err)
|
||||
println(response)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules and Key Components
|
||||
|
||||
### 1. **`model_request.v`**
|
||||
Handles JSON-RPC requests:
|
||||
- Structs: `Request`, `RequestGeneric`
|
||||
- Methods: `new_request`, `new_request_generic`, `decode_request`, etc.
|
||||
|
||||
### 2. **`model_response.v`**
|
||||
Handles JSON-RPC responses:
|
||||
- Structs: `Response`, `ResponseGeneric`
|
||||
- Methods: `new_response`, `new_response_generic`, `decode_response`, `validate`, etc.
|
||||
|
||||
### 3. **`model_error.v`**
|
||||
Manages JSON-RPC errors:
|
||||
- Struct: `RPCError`
|
||||
- Predefined errors: `parse_error`, `invalid_request`, etc.
|
||||
- Methods: `msg`, `is_empty`, etc.
|
||||
|
||||
### 4. **`client.v`**
|
||||
Implements the JSON-RPC client:
|
||||
- Structs: `Client`, `SendParams`, `ClientConfig`
|
||||
- Interface: `IRPCTransportClient`
|
||||
- Method: `send`
|
||||
|
||||
---
|
||||
|
||||
## JSON-RPC Specification Reference
|
||||
|
||||
This module adheres to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification).
|
||||
47
lib/schemas/jsonrpc/client.v
Normal file
47
lib/schemas/jsonrpc/client.v
Normal file
@@ -0,0 +1,47 @@
|
||||
module jsonrpc
|
||||
|
||||
// Interface for a transport client used by the JSON-RPC client.
|
||||
pub interface IRPCTransportClient {
|
||||
mut:
|
||||
send(string, SendParams) !string // Sends a request and returns the response as a string
|
||||
}
|
||||
|
||||
// JSON-RPC WebSocket client implementation.
|
||||
pub struct Client {
|
||||
mut:
|
||||
transport IRPCTransportClient // Transport layer to handle communication
|
||||
}
|
||||
|
||||
// Creates a new JSON-RPC client instance.
|
||||
pub fn new_client(client Client) &Client {
|
||||
return &Client{...client}
|
||||
}
|
||||
|
||||
// Parameters for configuring the `send` function.
|
||||
@[params]
|
||||
pub struct SendParams {
|
||||
timeout int = 60 // Timeout in seconds (default: 60)
|
||||
retry int // Number of retry attempts
|
||||
}
|
||||
|
||||
// Sends a JSON-RPC request and returns the response result of type `D`.
|
||||
// Validates the response and ensures the request/response IDs match.
|
||||
pub fn (mut c Client) send[T, D](request RequestGeneric[T], params SendParams) !D {
|
||||
response_json := c.transport.send(request.encode(), params)! // Send the encoded request
|
||||
response := decode_response_generic[D](response_json) or {
|
||||
return error('Unable to decode response.\n- Response: ${response_json}\n- Error: ${err}')
|
||||
}
|
||||
|
||||
response.validate() or {
|
||||
return error('Received invalid response: ${err}')
|
||||
}
|
||||
|
||||
if response.id != request.id {
|
||||
return error('Received response with different id ${response}')
|
||||
}
|
||||
|
||||
println('response ${response}')
|
||||
|
||||
// Return the result or propagate the error.
|
||||
return response.result()!
|
||||
}
|
||||
67
lib/schemas/jsonrpc/client_test.v
Normal file
67
lib/schemas/jsonrpc/client_test.v
Normal file
@@ -0,0 +1,67 @@
|
||||
module jsonrpc
|
||||
|
||||
import time
|
||||
|
||||
struct TestRPCTransportClient {}
|
||||
|
||||
fn (t TestRPCTransportClient) send(request_json string, params SendParams) !string {
|
||||
request := decode_request(request_json)!
|
||||
|
||||
// instead of sending request and returning response from rpc server
|
||||
// our test rpc transport client return a response
|
||||
response := if request.method == 'echo' {
|
||||
new_response(request.id, request.params)
|
||||
} else if request.method == 'test_error' {
|
||||
error := RPCError{
|
||||
code: 1
|
||||
message: 'intentional jsonrpc error response'
|
||||
}
|
||||
new_error_response(request.id, error)
|
||||
} else {
|
||||
new_error_response(request.id, method_not_found)
|
||||
}
|
||||
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
struct TestClient {
|
||||
Client
|
||||
}
|
||||
|
||||
fn test_new() {
|
||||
client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
}
|
||||
|
||||
fn test_send_json_rpc() {
|
||||
mut client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
|
||||
request0 := new_request_generic[string]('echo', 'ECHO!')
|
||||
response0 := client.send[string, string](request0)!
|
||||
assert response0 == 'ECHO!'
|
||||
|
||||
request1 := new_request_generic[string]('test_error', '')
|
||||
if response1 := client.send[string, string](request1) {
|
||||
assert false, 'Should return internal error'
|
||||
} else {
|
||||
assert err is RPCError
|
||||
assert err.code() == 1
|
||||
assert err.msg() == 'intentional jsonrpc error response'
|
||||
}
|
||||
|
||||
request2 := new_request_generic[string]('nonexistent_method', '')
|
||||
if response2 := client.send[string, string](request2) {
|
||||
assert false, 'Should return not found error'
|
||||
} else {
|
||||
assert err is RPCError
|
||||
assert err.code() == 32601
|
||||
assert err.msg() == 'Method not found'
|
||||
}
|
||||
|
||||
request := new_request_generic[string]('echo', 'ECHO!')
|
||||
response := client.send[string, string](request)!
|
||||
assert response == 'ECHO!'
|
||||
}
|
||||
35
lib/schemas/jsonrpc/handler.v
Normal file
35
lib/schemas/jsonrpc/handler.v
Normal file
@@ -0,0 +1,35 @@
|
||||
module jsonrpc
|
||||
|
||||
import log
|
||||
import net.websocket
|
||||
|
||||
// JSON-RPC WebSoocket Server
|
||||
|
||||
pub struct Handler {
|
||||
pub:
|
||||
// map of method names to procedure handlers
|
||||
procedures map[string]ProcedureHandler
|
||||
}
|
||||
|
||||
// ProcedureHandler handles executing procedure calls
|
||||
// decodes payload, execute procedure function, return encoded result
|
||||
type ProcedureHandler = fn (payload string) !string
|
||||
|
||||
pub fn new_handler(handler Handler) !&Handler {
|
||||
return &Handler{...handler}
|
||||
}
|
||||
|
||||
pub fn (handler Handler) handler(client &websocket.Client, message string) string {
|
||||
return handler.handle(message) or { panic(err) }
|
||||
}
|
||||
|
||||
pub fn (handler Handler) handle(message string) !string {
|
||||
method := decode_request_method(message)!
|
||||
log.info('Handling remote procedure call to method: ${method}')
|
||||
procedure_func := handler.procedures[method] or {
|
||||
log.error('No procedure handler for method ${method} found')
|
||||
return method_not_found
|
||||
}
|
||||
response := procedure_func(message) or { panic(err) }
|
||||
return response
|
||||
}
|
||||
92
lib/schemas/jsonrpc/handler_test.v
Normal file
92
lib/schemas/jsonrpc/handler_test.v
Normal file
@@ -0,0 +1,92 @@
|
||||
module jsonrpc
|
||||
|
||||
// echo method for testing purposes
|
||||
fn method_echo(text string) !string {
|
||||
return text
|
||||
}
|
||||
|
||||
pub struct TestStruct {
|
||||
data string
|
||||
}
|
||||
|
||||
// structure echo method for testing purposes
|
||||
fn method_echo_struct(structure TestStruct) !TestStruct {
|
||||
return structure
|
||||
}
|
||||
|
||||
// method that returns error for testing purposes
|
||||
fn method_error(text string) !string {
|
||||
return error('some error')
|
||||
}
|
||||
|
||||
fn method_echo_handler(data string) !string {
|
||||
request := decode_request_generic[string](data)!
|
||||
result := method_echo(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn method_echo_struct_handler(data string) !string {
|
||||
request := decode_request_generic[TestStruct](data)!
|
||||
result := method_echo_struct(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic[TestStruct](request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn method_error_handler(data string) !string {
|
||||
request := decode_request_generic[string](data)!
|
||||
result := method_error(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn test_new() {
|
||||
handler := new_handler(Handler{})!
|
||||
}
|
||||
|
||||
fn test_handle() {
|
||||
handler := new_handler(Handler{
|
||||
procedures: {
|
||||
'method_echo': method_echo_handler
|
||||
'method_echo_struct': method_echo_struct_handler
|
||||
'method_error': method_error_handler
|
||||
}
|
||||
})!
|
||||
|
||||
params0 := 'ECHO!'
|
||||
request0 := new_request_generic[string]('method_echo', params0)
|
||||
decoded0 := handler.handle(request0.encode())!
|
||||
response0 := decode_response_generic[string](decoded0)!
|
||||
assert response0.result()! == params0
|
||||
|
||||
params1 := TestStruct{'ECHO!'}
|
||||
request1 := new_request_generic[TestStruct]('method_echo_struct', params1)
|
||||
decoded1 := handler.handle(request1.encode())!
|
||||
response1 := decode_response_generic[TestStruct](decoded1)!
|
||||
assert response1.result()! == params1
|
||||
|
||||
params2 := 'ECHO!'
|
||||
request2 := new_request_generic[string]('method_error', params2)
|
||||
decoded2 := handler.handle(request2.encode())!
|
||||
response2 := decode_response_generic[string](decoded2)!
|
||||
assert response2.is_error()
|
||||
assert response2.error()?.message == 'some error'
|
||||
}
|
||||
60
lib/schemas/jsonrpc/model_error.v
Normal file
60
lib/schemas/jsonrpc/model_error.v
Normal file
@@ -0,0 +1,60 @@
|
||||
module jsonrpc
|
||||
|
||||
// Predefined JSON-RPC errors as per the specification: https://www.jsonrpc.org/specification
|
||||
pub const parse_error = RPCError{
|
||||
code: 32700
|
||||
message: 'Parse error'
|
||||
data: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
|
||||
}
|
||||
pub const invalid_request = RPCError{
|
||||
code: 32600
|
||||
message: 'Invalid Request'
|
||||
data: 'The JSON sent is not a valid Request object.'
|
||||
}
|
||||
pub const method_not_found = RPCError{
|
||||
code: 32601
|
||||
message: 'Method not found'
|
||||
data: 'The method does not exist / is not available.'
|
||||
}
|
||||
pub const invalid_params = RPCError{
|
||||
code: 32602
|
||||
message: 'Invalid params'
|
||||
data: 'Invalid method parameter(s).'
|
||||
}
|
||||
pub const internal_error = RPCError{
|
||||
code: 32603
|
||||
message: 'Internal RPCError'
|
||||
data: 'Internal JSON-RPC error.'
|
||||
}
|
||||
|
||||
// Represents a JSON-RPC error object with a code, message, and optional data.
|
||||
pub struct RPCError {
|
||||
pub mut:
|
||||
code int // Error code indicating the type of error
|
||||
message string // Brief error description
|
||||
data string // Additional details about the error
|
||||
}
|
||||
|
||||
// Creates a new error response for a given request ID.
|
||||
pub fn new_error(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the error message.
|
||||
pub fn (err RPCError) msg() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// Returns the error code.
|
||||
pub fn (err RPCError) code() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
// Checks if the error object is empty (i.e., uninitialized).
|
||||
pub fn (err RPCError) is_empty() bool {
|
||||
return err.code == 0
|
||||
}
|
||||
80
lib/schemas/jsonrpc/model_request.v
Normal file
80
lib/schemas/jsonrpc/model_request.v
Normal file
@@ -0,0 +1,80 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import rand
|
||||
|
||||
// Represents a JSON-RPC request with essential fields.
|
||||
pub struct Request {
|
||||
pub mut:
|
||||
jsonrpc string @[required] // JSON-RPC version, e.g., "2.0"
|
||||
method string @[required] // Method to invoke
|
||||
params string @[required] // JSON-encoded parameters
|
||||
id string @[required] // Unique request ID
|
||||
}
|
||||
|
||||
// Creates a new JSON-RPC request with the specified method and parameters.
|
||||
pub fn new_request(method string, params string) Request {
|
||||
return Request{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4() // Automatically generate a unique ID
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a `Request` object.
|
||||
pub fn decode_request(data string) !Request {
|
||||
return json2.decode[Request](data)!
|
||||
}
|
||||
|
||||
// Encodes the `Request` object into a JSON string.
|
||||
pub fn (req Request) encode() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
|
||||
// A generic JSON-RPC request struct allowing strongly-typed parameters.
|
||||
pub struct RequestGeneric[T] {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
method string @[required]
|
||||
params T @[required]
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// Creates a new generic JSON-RPC request.
|
||||
pub fn new_request_generic[T](method string, params T) RequestGeneric[T] {
|
||||
return RequestGeneric[T]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4()
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the `id` field from a JSON string.
|
||||
// Returns an error if the field is missing.
|
||||
pub fn decode_request_id(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
id_any := data_map['id'] or { return error('ID field not found') }
|
||||
return id_any.str()
|
||||
}
|
||||
|
||||
// Extracts the `method` field from a JSON string.
|
||||
// Returns an error if the field is missing.
|
||||
pub fn decode_request_method(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
method_any := data_map['method'] or { return error('Method field not found') }
|
||||
return method_any.str()
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a generic `Request` object.
|
||||
pub fn decode_request_generic[T](data string) !RequestGeneric[T] {
|
||||
return json2.decode[RequestGeneric[T]](data)!
|
||||
}
|
||||
|
||||
// Encodes a generic `Request` object into a JSON string.
|
||||
pub fn (req RequestGeneric[T]) encode[T]() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
@@ -0,0 +1,73 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_request() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
request := decode_request(data) or {
|
||||
assert false, 'Failed to decode request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_encode() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_new_request_generic() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == params
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request_id() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
id := decode_request_id(data) or {
|
||||
assert false, 'Failed to decode request ID: $err'
|
||||
return
|
||||
}
|
||||
assert id == '123'
|
||||
}
|
||||
|
||||
fn test_decode_request_method() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
method := decode_request_method(data) or {
|
||||
assert false, 'Failed to decode request method: $err'
|
||||
return
|
||||
}
|
||||
assert method == 'test_method'
|
||||
}
|
||||
|
||||
fn test_decode_request_generic() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":{"key":"value"},"id":"123"}'
|
||||
request := decode_request_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == {'key': 'value'}
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_generic_encode() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
181
lib/schemas/jsonrpc/model_response.v
Normal file
181
lib/schemas/jsonrpc/model_response.v
Normal file
@@ -0,0 +1,181 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import json
|
||||
|
||||
// The JSON-RPC version used for responses.
|
||||
const jsonrpc_version = '2.0'
|
||||
|
||||
// Represents a JSON-RPC response, which includes a result, an error, or both.
|
||||
pub struct Response {
|
||||
pub:
|
||||
jsonrpc string @[required] // JSON-RPC version, e.g., "2.0"
|
||||
result ?string // JSON-encoded result (optional)
|
||||
error_ ?RPCError @[json: 'error'] // Error object if the request failed (optional)
|
||||
id string @[required] // Matches the request ID
|
||||
}
|
||||
|
||||
// Creates a successful response with the given result.
|
||||
pub fn new_response(id string, result string) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an error response with the given error object.
|
||||
pub fn new_error_response(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a `Response` object.
|
||||
pub fn decode_response(data string) !Response {
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
if err := raw_map['error'] {
|
||||
id_any := raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}
|
||||
return Response {
|
||||
id: id_any.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
return Response {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: raw_map['result']!.str()
|
||||
}
|
||||
}
|
||||
|
||||
// Encodes the `Response` object into a JSON string.
|
||||
pub fn (resp Response) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// Validates that the response does not contain both `result` and `error`.
|
||||
pub fn (resp Response) validate() ! {
|
||||
// if err := resp.error_ && resp.result != '' {
|
||||
// return error('Response contains both error and result.\n- Error: ${resp.error_.str()}\n- Result: ${resp.result}')
|
||||
// }
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) error() ?RPCError {
|
||||
if err := resp.error_ {
|
||||
return err
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// Returns the result if no error is present.
|
||||
pub fn (resp Response) result() !string {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {''}
|
||||
}
|
||||
|
||||
// A generic JSON-RPC response, allowing strongly-typed results.
|
||||
pub struct ResponseGeneric[D] {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
result ?D
|
||||
error_ ?RPCError @[json: 'error'] // Error object if the request failed (optional)
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// Creates a successful generic response with the given result.
|
||||
pub fn new_response_generic[D](id string, result D) ResponseGeneric[D] {
|
||||
return ResponseGeneric[D]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a generic `ResponseGeneric` object.
|
||||
pub fn decode_response_generic[D](data string) !ResponseGeneric[D] {
|
||||
println('respodata ${data}')
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
if err := raw_map['error'] {
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
resp := json.decode(ResponseGeneric[D], data)!
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: resp.result
|
||||
}
|
||||
}
|
||||
|
||||
// Encodes the generic `ResponseGeneric` object into a JSON string.
|
||||
pub fn (resp ResponseGeneric[D]) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// Validates that the generic response does not contain both `result` and `error`.
|
||||
pub fn (resp ResponseGeneric[D]) validate() ! {
|
||||
if resp.is_error() && resp.is_result() {
|
||||
return error('Response contains both error and result.\n- Error: ${resp.error.str()}\n- Result: ${resp.result}')
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp ResponseGeneric[D]) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp ResponseGeneric[D]) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
|
||||
// Returns the error if present in the generic response.
|
||||
pub fn (resp ResponseGeneric[D]) error() ?RPCError {
|
||||
return resp.error_?
|
||||
}
|
||||
|
||||
// Returns the result if no error is present in the generic response.
|
||||
pub fn (resp ResponseGeneric[D]) result() !D {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {D{}}
|
||||
}
|
||||
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
@@ -0,0 +1,189 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_response() {
|
||||
response := new_response('123', 'test_result')
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_error_response() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
|
||||
response.validate()!
|
||||
assert response.is_error()
|
||||
assert !response.is_result() // Ensure no result is set
|
||||
assert response.id == '123'
|
||||
|
||||
response_error := response.error()?
|
||||
assert response_error == error
|
||||
}
|
||||
|
||||
fn test_decode_response() {
|
||||
data := '{"jsonrpc":"2.0","result":"test_result","id":"123"}'
|
||||
response := decode_response(data) or {
|
||||
assert false, 'Failed to decode response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_response_encode() {
|
||||
response := new_response('123', 'test_result')
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_validate() {
|
||||
response := new_response('123', 'test_result')
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := Response{
|
||||
jsonrpc: '2.0'
|
||||
result: 'test_result'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_result() {
|
||||
response := new_response('123', 'test_result')
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_response_generic() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_decode_response_generic() {
|
||||
data := '{"jsonrpc":"2.0","result":{"key":"value"},"id":"123"}'
|
||||
response := decode_response_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_response_generic_encode() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_generic_validate() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := ResponseGeneric{
|
||||
jsonrpc: '2.0'
|
||||
result: {'key': 'value'}
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_generic_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := ResponseGeneric[map[string]string]{
|
||||
jsonrpc: '2.0'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_generic_result() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
27
lib/schemas/jsonrpc/testdata/handler_code.v
vendored
Normal file
27
lib/schemas/jsonrpc/testdata/handler_code.v
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
@[heap]
|
||||
struct TesterHandler {
|
||||
state Tester
|
||||
}
|
||||
|
||||
// handle handles an incoming JSON-RPC encoded message and returns an encoded response
|
||||
pub fn (handler TesterHandler) handle(msg string) string {
|
||||
method := jsonrpc.jsonrpcrequest_decode_method(msg)!
|
||||
match method {
|
||||
'test_notification_method' {
|
||||
jsonrpc.notify[string](msg, handler.state.test_notification_method)!
|
||||
}
|
||||
'test_invocation_method' {
|
||||
return jsonrpc.invoke[string](msg, handler.state.test_invocation_method)!
|
||||
}
|
||||
'test_method' {
|
||||
return jsonrpc.call[string, string](msg, handler.state.test_method)!
|
||||
}
|
||||
'test_method_structs' {
|
||||
return jsonrpc.call[Key, Value](msg, handler.state.test_method_structs)!
|
||||
}
|
||||
else {
|
||||
return error('method ${method} not handled')
|
||||
}
|
||||
}
|
||||
return error('this should never happen')
|
||||
}
|
||||
5
lib/schemas/jsonrpc/testdata/testfunction.v
vendored
Normal file
5
lib/schemas/jsonrpc/testdata/testfunction.v
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module main
|
||||
|
||||
pub fn testfunction(param string) string {
|
||||
return param
|
||||
}
|
||||
14
lib/schemas/jsonrpc/testdata/testmodule/testfunctions.v
vendored
Normal file
14
lib/schemas/jsonrpc/testdata/testmodule/testfunctions.v
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
module testmodule
|
||||
|
||||
pub fn testfunction0(param string) string {
|
||||
return param
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
name string
|
||||
number int
|
||||
}
|
||||
|
||||
pub fn testfunction1(config Config) []string {
|
||||
return []string{len: config.number, init: config.name}
|
||||
}
|
||||
54
lib/schemas/jsonrpc/testdata/testserver/openrpc_server.v
vendored
Normal file
54
lib/schemas/jsonrpc/testdata/testserver/openrpc_server.v
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
module main
|
||||
|
||||
import log
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc { JsonRpcHandler, jsonrpcrequest_decode }
|
||||
import freeflowuniverse.herolib.data.rpcwebsocket
|
||||
import data.jsonrpc.testdata.testmodule { Config, testfunction0, testfunction1 }
|
||||
|
||||
struct CustomJsonRpcHandler {
|
||||
JsonRpcHandler
|
||||
}
|
||||
|
||||
fn testfunction0_handler(data string) !string {
|
||||
request := jsonrpcrequest_decode[string](data)!
|
||||
result := testfunction0(request.params)
|
||||
response := jsonrpc.JsonRpcResponse[string]{
|
||||
jsonrpc: '2.0.0'
|
||||
id: request.id
|
||||
result: result
|
||||
}
|
||||
return response.to_json()
|
||||
}
|
||||
|
||||
fn testfunction1_handler(data string) !string {
|
||||
request := jsonrpcrequest_decode[Config](data)!
|
||||
result := testfunction1(request.params)
|
||||
response := jsonrpc.JsonRpcResponse[[]string]{
|
||||
jsonrpc: '2.0.0'
|
||||
id: request.id
|
||||
result: result
|
||||
}
|
||||
return response.to_json()
|
||||
}
|
||||
|
||||
// run_server creates and runs a jsonrpc ws server
|
||||
// handles rpc requests to reverse_echo function
|
||||
pub fn run_server() ! {
|
||||
mut logger := log.Logger(&log.Log{
|
||||
level: .debug
|
||||
})
|
||||
|
||||
mut handler := CustomJsonRpcHandler{
|
||||
JsonRpcHandler: jsonrpc.new_handler(&logger)!
|
||||
}
|
||||
|
||||
handler.state = &state
|
||||
// register rpc methods
|
||||
handler.register(testfunction0, testfunction0_handle)!
|
||||
handler.register(testfunction1, testfunction1_handle)!
|
||||
|
||||
// create & run rpc ws server
|
||||
mut jsonrpc_ws_server := rpcwebsocket.new_rpcwsserver(8080, handler, &logger)!
|
||||
jsonrpc_ws_server.run()!
|
||||
}
|
||||
Reference in New Issue
Block a user