add refactored schemas modules from 6-openrpc-code-generator branch

This commit is contained in:
Timur Gordon
2025-03-13 12:41:35 +01:00
parent 28359984ff
commit 269d0474c5
29 changed files with 1901 additions and 372 deletions

View File

@@ -1,186 +0,0 @@
module jsonschema
import freeflowuniverse.herolib.code.codemodel { Alias, Attribute, CodeItem, Struct, StructField, Type }
const vtypes = {
'integer': 'int'
'string': 'string'
}
pub fn (schema Schema) v_encode() !string {
module_name := 'schema.title.'
structs := schema.vstructs_encode()!
// todo: report bug: return $tmpl(...)
encoded := $tmpl('templates/schema.vtemplate')
return encoded
}
// vstructs_encode encodes a schema into V structs.
// if a schema has nested object type schemas or defines object type schemas,
// recrusively encodes object type schemas and pushes to the array of structs.
// returns an array of schemas that have been encoded into V structs.
pub fn (schema Schema) vstructs_encode() ![]string {
mut schemas := []string{}
mut properties := ''
// loop over properties
for name, property_ in schema.properties {
mut property := Schema{}
mut typesymbol := ''
if property_ is Reference {
// if reference, set typesymbol as reference name
ref := property_ as Reference
typesymbol = ref.ref.all_after_last('/')
} else {
property = property_ as Schema
typesymbol = property.vtype_encode()!
// recursively encode property if object
// todo: handle duplicates
if property.typ == 'object' {
structs := property.vstructs_encode()!
schemas << structs
}
}
properties += '\n\t${name} ${typesymbol}'
if name in schema.required {
properties += ' @[required]'
}
}
schemas << $tmpl('templates/struct.vtemplate')
return schemas
}
// code_type generates a typesymbol for the schema
pub fn (schema Schema) vtype_encode() !string {
mut property_str := ''
if schema.typ == 'null' {
return ''
}
if schema.typ == 'object' {
if schema.title == '' {
return error('Object schemas must define a title.')
}
// todo: enfore uppercase
property_str = schema.title
} else if schema.typ == 'array' {
// todo: handle multiple item schemas
if schema.items is SchemaRef {
// items := schema.items as SchemaRef
if schema.items is Schema {
items_schema := schema.items as Schema
property_str = '[]${items_schema.typ}'
}
}
} else if schema.typ in vtypes.keys() {
property_str = vtypes[schema.typ]
} else if schema.title != '' {
property_str = schema.title
} else {
return error('unknown type `${schema.typ}` ')
}
return property_str
}
pub fn (schema Schema) to_code() !CodeItem {
if schema.typ == 'object' {
return CodeItem(schema.to_struct()!)
}
if schema.typ in vtypes {
return Alias{
name: schema.title
typ: Type{
symbol: vtypes[schema.typ]
}
}
}
if schema.typ == 'array' {
if schema.items is SchemaRef {
if schema.items is Schema {
items_schema := schema.items as Schema
return Alias{
name: schema.title
typ: Type{
symbol: '[]${items_schema.typ}'
}
}
} else if schema.items is Reference {
items_ref := schema.items as Reference
return Alias{
name: schema.title
typ: Type{
symbol: '[]${items_ref.to_type_symbol()}'
}
}
}
}
}
return error('Schema typ ${schema.typ} not supported for code generation')
}
pub fn (schema Schema) to_struct() !Struct {
mut fields := []StructField{}
for key, val in schema.properties {
mut field := val.to_struct_field(key)!
if field.name in schema.required {
field.attrs << Attribute{
name: 'required'
}
}
fields << field
}
return Struct{
name: schema.title
description: schema.description
fields: fields
}
}
pub fn (schema SchemaRef) to_struct_field(name string) !StructField {
if schema is Reference {
return StructField{
name: name
typ: Type{
symbol: schema.to_type_symbol()
}
}
} else if schema is Schema {
mut field := StructField{
name: name
description: schema.description
}
if schema.typ == 'object' {
// then is anonymous struct
field.anon_struct = schema.to_struct()!
return field
} else if schema.typ in vtypes {
field.typ.symbol = vtypes[schema.typ]
return field
}
return error('Schema typ ${schema.typ} not supported for code generation')
}
return error('Schema typ not supported for code generation')
}
pub fn (sr SchemaRef) to_code() !Type {
return if sr is Reference {
sr.to_type()
} else {
Type{
symbol: (sr as Schema).vtype_encode()!
}
}
}
pub fn (ref Reference) to_type_symbol() string {
return ref.ref.all_after_last('/')
}
pub fn (ref Reference) to_type() Type {
return Type{
symbol: ref.to_type_symbol()
}
}

View File

@@ -1,53 +0,0 @@
module jsonschema
import json
import x.json2 { Any }
import os
import freeflowuniverse.herolib.core.pathlib
pub fn decode(data string) !Schema {
schema_map := json2.raw_decode(data)!.as_map()
mut schema := json.decode(Schema, data)!
for key, value in schema_map {
if key == 'properties' {
schema.properties = decode_schemaref_map(value.as_map())!
} else if key == 'additionalProperties' {
schema.additional_properties = decode_schemaref(value.as_map())!
} else if key == 'items' {
schema.items = decode_items(value)!
}
}
return schema
}
pub fn decode_items(data Any) !Items {
if data.str().starts_with('{') {
return decode_schemaref(data.as_map())!
}
if !data.str().starts_with('[') {
return error('items field must either be list of schemarefs or a schemaref')
}
mut items := []SchemaRef{}
for val in data.arr() {
items << decode_schemaref(val.as_map())!
}
return items
}
pub fn decode_schemaref_map(data_map map[string]Any) !map[string]SchemaRef {
mut schemaref_map := map[string]SchemaRef{}
for key, val in data_map {
schemaref_map[key] = decode_schemaref(val.as_map())!
}
return schemaref_map
}
pub fn decode_schemaref(data_map map[string]Any) !SchemaRef {
if '\$ref' in data_map {
return Reference{
ref: data_map['\$ref'].str()
}
}
return decode(data_map.str())!
}

View File

@@ -1,23 +0,0 @@
module jsonschema
import freeflowuniverse.herolib.code.codemodel
import freeflowuniverse.herolib.ui.console
fn test_struct_to_schema() {
struct_ := codemodel.Struct{
name: 'test_name'
description: 'a codemodel struct to test struct to schema serialization'
fields: [
codemodel.StructField{
name: 'test_field'
description: 'a field of the test struct to test fields serialization into schema'
typ: codemodel.Type{
symbol: 'string'
}
},
]
}
schema := struct_to_schema(struct_)
console.print_debug(schema)
}

View File

@@ -1,38 +0,0 @@
module jsonschema
type Items = SchemaRef | []SchemaRef
pub type SchemaRef = Reference | Schema
pub struct Reference {
pub:
ref string @[json: 'ref']
}
type Number = int
// https://json-schema.org/draft-07/json-schema-release-notes.html
pub struct Schema {
pub mut:
schema string @[json: 'schema']
id string @[json: 'id']
title string
description string
typ string @[json: 'type']
properties map[string]SchemaRef
additional_properties SchemaRef @[json: 'additionalProperties']
required []string
items Items
defs map[string]SchemaRef
one_of []SchemaRef @[json: 'oneOf']
format string
// todo: make fields optional upon the fixing of https://github.com/vlang/v/issues/18775
// from https://git.sr.ht/~emersion/go-jsonschema/tree/master/item/schema.go
// Validation for numbers
multiple_of int @[json: 'multipleOf'; omitempty]
maximum int @[omitempty]
exclusive_maximum int @[json: 'exclusiveMaximum'; omitempty]
minimum int @[omitempty]
exclusive_minimum int @[json: 'exclusiveMinimum'; omitempty]
enum_ []string @[json: 'enum'; omitempty]
}

View File

@@ -1 +0,0 @@
module jsonschema

View 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).

View File

@@ -0,0 +1,81 @@
module jsonrpc
// IRPCTransportClient defines the interface for transport mechanisms used by the JSON-RPC client.
// This allows for different transport implementations (HTTP, WebSocket, etc.) to be used
// with the same client code.
pub interface IRPCTransportClient {
mut:
// send transmits a JSON-RPC request string and returns the response as a string.
// Parameters:
// - request: The JSON-RPC request string to send
// - params: Configuration parameters for the send operation
// Returns:
// - The response string or an error if the send operation fails
send(request string, params SendParams) !string
}
// Client implements a JSON-RPC 2.0 client that can send requests and process responses.
// It uses a pluggable transport layer that implements the IRPCTransportClient interface.
pub struct Client {
mut:
// The transport implementation used to send requests and receive responses
transport IRPCTransportClient
}
// new_client creates a new JSON-RPC client with the specified transport.
//
// Parameters:
// - client: A Client struct with the transport field initialized
//
// Returns:
// - A pointer to a new Client instance
pub fn new_client(client Client) &Client {
return &Client{...client}
}
// SendParams defines configuration options for sending JSON-RPC requests.
// These parameters control timeout and retry behavior.
@[params]
pub struct SendParams {
// Maximum time in seconds to wait for a response (default: 60)
timeout int = 60
// Number of times to retry the request if it fails
retry int
}
// send sends a JSON-RPC request with parameters of type T and expects a response with result of type D.
// This method handles the full request-response cycle including validation and error handling.
//
// Type Parameters:
// - T: The type of the request parameters
// - D: The expected type of the response result
//
// Parameters:
// - request: The JSON-RPC request object with parameters of type T
// - params: Configuration parameters for the send operation
//
// Returns:
// - The response result of type D or an error if any step in the process fails
pub fn (mut c Client) send[T, D](request RequestGeneric[T], params SendParams) !D {
// Send the encoded request through the transport layer
response_json := c.transport.send(request.encode(), params)!
// Decode the response JSON into a strongly-typed response object
response := decode_response_generic[D](response_json) or {
return error('Unable to decode response.\n- Response: ${response_json}\n- Error: ${err}')
}
// Validate the response according to the JSON-RPC specification
response.validate() or {
return error('Received invalid response: ${err}')
}
// Ensure the response ID matches the request ID to prevent response/request mismatch
if response.id != request.id {
return error('Received response with different id ${response}')
}
// Return the result or propagate any error from the response
return response.result()!
}

View File

@@ -0,0 +1,100 @@
module jsonrpc
import time
// This file contains tests for the JSON-RPC client implementation.
// It uses a mock transport client to simulate JSON-RPC server responses without requiring an actual server.
// TestRPCTransportClient is a mock implementation of the RPCTransport interface.
// It simulates a JSON-RPC server by returning predefined responses based on the method name.
struct TestRPCTransportClient {}
// send implements the RPCTransport interface's send method.
// Instead of sending the request to a real server, it decodes the request and returns
// a predefined response based on the method name.
//
// Parameters:
// - request_json: The JSON-RPC request as a JSON string
// - params: Additional parameters for sending the request
//
// Returns:
// - A JSON-encoded response string or an error if decoding fails
fn (t TestRPCTransportClient) send(request_json string, params SendParams) !string {
// Decode the incoming request to determine which response to return
request := decode_request(request_json)!
// Return different responses based on the method name:
// - 'echo': Returns the params as the result
// - 'test_error': Returns an error response
// - anything else: Returns a method_not_found error
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()
}
// TestClient extends the Client struct for testing purposes.
struct TestClient {
Client
}
// test_new tests the creation of a new JSON-RPC client with a mock transport.
fn test_new() {
// Create a new client with the mock transport
client := new_client(
transport: TestRPCTransportClient{}
)
}
// test_send_json_rpc tests the client's ability to send requests and handle responses.
// It tests three scenarios:
// 1. Successful response from an 'echo' method
// 2. Error response from a 'test_error' method
// 3. Method not found error from a non-existent method
fn test_send_json_rpc() {
// Create a new client with the mock transport
mut client := new_client(
transport: TestRPCTransportClient{}
)
// Test case 1: Successful echo response
request0 := new_request_generic[string]('echo', 'ECHO!')
response0 := client.send[string, string](request0)!
assert response0 == 'ECHO!'
// Test case 2: Error response
request1 := new_request_generic[string]('test_error', '')
if response1 := client.send[string, string](request1) {
assert false, 'Should return internal error'
} else {
// Verify the error details
assert err is RPCError
assert err.code() == 1
assert err.msg() == 'intentional jsonrpc error response'
}
// Test case 3: Method not found error
request2 := new_request_generic[string]('nonexistent_method', '')
if response2 := client.send[string, string](request2) {
assert false, 'Should return not found error'
} else {
// Verify the error details
assert err is RPCError
assert err.code() == -32601
assert err.msg() == 'Method not found'
}
// Duplicate of test case 1 (can be removed or kept for additional verification)
request := new_request_generic[string]('echo', 'ECHO!')
response := client.send[string, string](request)!
assert response == 'ECHO!'
}

View File

@@ -0,0 +1,72 @@
module jsonrpc
import log
import net.websocket
// This file implements a JSON-RPC 2.0 handler for WebSocket servers.
// It provides functionality to register procedure handlers and process incoming JSON-RPC requests.
// Handler is a JSON-RPC request handler that maps method names to their corresponding procedure handlers.
// It can be used with a WebSocket server to handle incoming JSON-RPC requests.
pub struct Handler {
pub:
// A map where keys are method names and values are the corresponding procedure handler functions
procedures map[string]ProcedureHandler
}
// ProcedureHandler is a function type that processes a JSON-RPC request payload and returns a response.
// The function should:
// 1. Decode the payload to extract parameters
// 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.
type ProcedureHandler = fn (payload string) !string
// new_handler creates a new JSON-RPC handler with the specified procedure handlers.
//
// Parameters:
// - handler: A Handler struct with the procedures field initialized
//
// Returns:
// - A pointer to a new Handler instance or an error if creation fails
pub fn new_handler(handler Handler) !&Handler {
return &Handler{...handler}
}
// 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.
//
// Parameters:
// - client: The WebSocket client that sent the message
// - message: The JSON-RPC request message as a string
//
// 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) }
}
// 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.
//
// Parameters:
// - message: The JSON-RPC request message as a string
//
// 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
}
// Execute the procedure handler with the request payload
response := procedure_func(message) or { panic(err) }
return response
}

View File

@@ -0,0 +1,171 @@
module jsonrpc
// This file contains tests for the JSON-RPC handler implementation.
// It tests the handler's ability to process requests, invoke the appropriate procedure,
// and return properly formatted responses.
// method_echo is a simple test method that returns the input text.
// Used to test successful request handling.
//
// Parameters:
// - text: A string to echo back
//
// Returns:
// - The same string that was passed in
fn method_echo(text string) !string {
return text
}
// TestStruct is a simple struct used for testing struct parameter handling.
pub struct TestStruct {
data string
}
// method_echo_struct is a test method that returns the input struct.
// Used to test handling of complex types in JSON-RPC.
//
// Parameters:
// - structure: A TestStruct instance to echo back
//
// Returns:
// - The same TestStruct that was passed in
fn method_echo_struct(structure TestStruct) !TestStruct {
return structure
}
// method_error is a test method that always returns an error.
// Used to test error handling in the JSON-RPC flow.
//
// Parameters:
// - text: A string (not used)
//
// Returns:
// - Always returns an error
fn method_error(text string) !string {
return error('some error')
}
// method_echo_handler is a procedure handler for the method_echo function.
// It decodes the request, calls method_echo, and encodes the response.
//
// Parameters:
// - data: The JSON-RPC request as a string
//
// Returns:
// - A JSON-encoded response string
fn method_echo_handler(data string) !string {
// Decode the request with string parameters
request := decode_request_generic[string](data)!
// Call the echo method and handle any errors
result := method_echo(request.params) or {
// If an error occurs, create an error response
response := new_error_response(request.id,
code: err.code()
message: err.msg()
)
return response.encode()
}
// Create a success response with the result
response := new_response_generic(request.id, result)
return response.encode()
}
// method_echo_struct_handler is a procedure handler for the method_echo_struct function.
// It demonstrates handling of complex struct types in JSON-RPC.
//
// Parameters:
// - data: The JSON-RPC request as a string
//
// Returns:
// - A JSON-encoded response string
fn method_echo_struct_handler(data string) !string {
// Decode the request with TestStruct parameters
request := decode_request_generic[TestStruct](data)!
// Call the echo struct method and handle any errors
result := method_echo_struct(request.params) or {
// If an error occurs, create an error response
response := new_error_response(request.id,
code: err.code()
message: err.msg()
)
return response.encode()
}
// Create a success response with the struct result
response := new_response_generic[TestStruct](request.id, result)
return response.encode()
}
// method_error_handler is a procedure handler for the method_error function.
// It demonstrates error handling in JSON-RPC procedure handlers.
//
// Parameters:
// - data: The JSON-RPC request as a string
//
// Returns:
// - A JSON-encoded error response string
fn method_error_handler(data string) !string {
// Decode the request with string parameters
request := decode_request_generic[string](data)!
// Call the error method, which always returns an error
result := method_error(request.params) or {
// Create an error response with the error details
response := new_error_response(request.id,
code: err.code()
message: err.msg()
)
return response.encode()
}
// This code should never be reached since method_error always returns an error
response := new_response_generic(request.id, result)
return response.encode()
}
// test_new tests the creation of a new JSON-RPC handler.
fn test_new() {
// Create a new handler with no procedures
handler := new_handler(Handler{})!
}
// test_handle tests the handler's ability to process different types of requests.
// It tests three scenarios:
// 1. A successful string echo request
// 2. A successful struct echo request
// 3. A request that results in an error
fn test_handle() {
// Create a new handler with three test procedures
handler := new_handler(Handler{
procedures: {
'method_echo': method_echo_handler
'method_echo_struct': method_echo_struct_handler
'method_error': method_error_handler
}
})!
// Test case 1: String echo request
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
// Test case 2: Struct echo request
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
// Test case 3: Error request
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'
}

BIN
lib/schemas/jsonrpc/jsonrpc.dylib Executable file

Binary file not shown.

View File

@@ -0,0 +1,109 @@
module jsonrpc
// Standard JSON-RPC 2.0 error codes and messages as defined in the specification
// See: https://www.jsonrpc.org/specification#error_object
// parse_error indicates that the server received invalid JSON.
// This error is returned when the server is unable to parse the request.
// Error code: -32700
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.'
}
// invalid_request indicates that the sent JSON is not a valid Request object.
// This error is returned when the request object doesn't conform to the JSON-RPC 2.0 specification.
// Error code: -32600
pub const invalid_request = RPCError{
code: -32600
message: 'Invalid Request'
data: 'The JSON sent is not a valid Request object.'
}
// method_not_found indicates that the requested method doesn't exist or is not available.
// This error is returned when the method specified in the request is not supported.
// Error code: -32601
pub const method_not_found = RPCError{
code: -32601
message: 'Method not found'
data: 'The method does not exist / is not available.'
}
// invalid_params indicates that the method parameters are invalid.
// This error is returned when the parameters provided to the method are incorrect or incompatible.
// Error code: -32602
pub const invalid_params = RPCError{
code: -32602
message: 'Invalid params'
data: 'Invalid method parameter(s).'
}
// internal_error indicates an internal JSON-RPC error.
// This is a generic server-side error when no more specific error is applicable.
// Error code: -32603
pub const internal_error = RPCError{
code: -32603
message: 'Internal Error'
data: 'Internal JSON-RPC error.'
}
// RPCError represents a JSON-RPC 2.0 error object as defined in the specification.
// Error objects contain a code, message, and optional data field to provide
// more information about the error that occurred.
pub struct RPCError {
pub mut:
// Numeric error code. Predefined codes are in the range -32768 to -32000.
// Custom error codes should be outside this range.
code int
// Short description of the error
message string
// Additional information about the error (optional)
data string
}
// new_error creates a new error response for a given request ID.
// This is a convenience function to create a Response object with an error.
//
// Parameters:
// - id: The request ID that this error is responding to
// - error: The RPCError object to include in the response
//
// Returns:
// - A Response object containing the error
pub fn new_error(id string, error RPCError) Response {
return Response{
jsonrpc: jsonrpc_version
error_: error
id: id
}
}
// msg returns the error message.
// This is a convenience method to access the message field.
//
// Returns:
// - The error message string
pub fn (err RPCError) msg() string {
return err.message
}
// code returns the error code.
// This is a convenience method to access the code field.
//
// Returns:
// - The numeric error code
pub fn (err RPCError) code() int {
return err.code
}
// is_empty checks if the error object is empty (uninitialized).
// An error is considered empty if its code is 0, which is not a valid JSON-RPC error code.
//
// Returns:
// - true if the error is empty, false otherwise
pub fn (err RPCError) is_empty() bool {
return err.code == 0
}

View File

@@ -0,0 +1,161 @@
module jsonrpc
import x.json2
import rand
// Request represents a JSON-RPC 2.0 request object.
// It contains all the required fields according to the JSON-RPC 2.0 specification.
// See: https://www.jsonrpc.org/specification#request_object
pub struct Request {
pub mut:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string @[required]
// The name of the method to be invoked on the server
method string @[required]
// The parameters to the method, encoded as a JSON string
// This can be omitted if the method doesn't require parameters
params string
// An identifier established by the client that must be included in the response
// This is used to correlate requests with their corresponding responses
id string @[required]
}
// new_request creates a new JSON-RPC request with the specified method and parameters.
// It automatically sets the JSON-RPC version to the current version and generates a unique ID.
//
// Parameters:
// - method: The name of the method to invoke on the server
// - params: The parameters to the method, encoded as a JSON string
//
// Returns:
// - A fully initialized Request object
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 using UUID v4
}
}
// decode_request parses a JSON string into a Request object.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC request
//
// Returns:
// - A Request object or an error if parsing fails
pub fn decode_request(data string) !Request {
return json2.decode[Request](data)!
}
// encode serializes the Request object into a JSON string.
//
// Returns:
// - A JSON string representation of the Request
pub fn (req Request) encode() string {
return json2.encode(req)
}
// validate checks if the Request object contains all required fields
// according to the JSON-RPC 2.0 specification.
//
// Returns:
// - An error if validation fails, otherwise nothing
pub fn (req Request) validate() ! {
if req.jsonrpc == '' {
return error('request jsonrpc version not specified')
} else if req.id == '' {
return error('request id is empty')
} else if req.method == '' {
return error('request method is empty')
}
}
// RequestGeneric is a type-safe version of the Request struct that allows
// for strongly-typed parameters using generics.
// This provides compile-time type safety for request parameters.
pub struct RequestGeneric[T] {
pub mut:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string @[required]
// The name of the method to be invoked on the server
method string @[required]
// The parameters to the method, with a specific type T
params T
// An identifier established by the client
id string @[required]
}
// new_request_generic creates a new generic JSON-RPC request with strongly-typed parameters.
// It automatically sets the JSON-RPC version and generates a unique ID.
//
// Parameters:
// - method: The name of the method to invoke on the server
// - params: The parameters to the method, of type T
//
// Returns:
// - A fully initialized RequestGeneric object with parameters of type T
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()
}
}
// decode_request_id extracts just the ID field from a JSON-RPC request string.
// This is useful when you only need the ID without parsing the entire request.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC request
//
// Returns:
// - The ID as a string, or an error if the ID 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()
}
// decode_request_method extracts just the method field from a JSON-RPC request string.
// This is useful when you need to determine the method without parsing the entire request.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC request
//
// Returns:
// - The method name as a string, or an error if the method 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()
}
// decode_request_generic parses a JSON string into a RequestGeneric object with parameters of type T.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC request
//
// Returns:
// - A RequestGeneric object with parameters of type T, or an error if parsing fails
pub fn decode_request_generic[T](data string) !RequestGeneric[T] {
return json2.decode[RequestGeneric[T]](data)!
}
// encode serializes the RequestGeneric object into a JSON string.
//
// Returns:
// - A JSON string representation of the RequestGeneric object
pub fn (req RequestGeneric[T]) encode[T]() string {
return json2.encode(req)
}

View 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"')
}

View File

@@ -0,0 +1,282 @@
module jsonrpc
import x.json2
import json
// The JSON-RPC version used for all requests and responses according to the specification.
const jsonrpc_version = '2.0'
// Response represents a JSON-RPC 2.0 response object.
// According to the specification, a response must contain either a result or an error, but not both.
// See: https://www.jsonrpc.org/specification#response_object
pub struct Response {
pub:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string @[required]
// The result of the method invocation (only present if the call was successful)
result ?string
// Error object if the request failed (only present if the call failed)
error_ ?RPCError @[json: 'error']
// Must match the id of the request that generated this response
id string @[required]
}
// new_response creates a successful JSON-RPC response with the given result.
//
// Parameters:
// - id: The ID from the request that this response is answering
// - result: The result of the method call, encoded as a JSON string
//
// Returns:
// - A Response object containing the result
pub fn new_response(id string, result string) Response {
return Response{
jsonrpc: jsonrpc.jsonrpc_version
result: result
id: id
}
}
// new_error_response creates an error JSON-RPC response with the given error object.
//
// Parameters:
// - id: The ID from the request that this response is answering
// - error: The error that occurred during the method call
//
// Returns:
// - A Response object containing the error
pub fn new_error_response(id string, error RPCError) Response {
return Response{
jsonrpc: jsonrpc.jsonrpc_version
error_: error
id: id
}
}
// decode_response parses a JSON string into a Response object.
// This function handles the complex validation rules for JSON-RPC responses.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC 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_map := raw.as_map()
// Validate that the response contains either result or error, but not both or neither
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.')
}
// Handle error responses
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())!
}
}
// Handle successful responses
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()
}
}
// encode serializes the Response object into a JSON string.
//
// Returns:
// - A JSON string representation of the Response
pub fn (resp Response) encode() string {
return json2.encode(resp)
}
// validate checks that the Response object follows the JSON-RPC 2.0 specification.
// A valid response must not contain both result and error.
//
// Returns:
// - An error if validation fails, otherwise nothing
pub fn (resp Response) validate() ! {
// Note: This validation is currently commented out but should be implemented
// if err := resp.error_ && resp.result != '' {
// return error('Response contains both error and result.\n- Error: ${resp.error_.str()}\n- Result: ${resp.result}')
// }
}
// is_error checks if the response contains an error.
//
// Returns:
// - true if the response contains an error, false otherwise
pub fn (resp Response) is_error() bool {
return resp.error_ != none
}
// is_result checks if the response contains a result.
//
// Returns:
// - true if the response contains a result, false otherwise
pub fn (resp Response) is_result() bool {
return resp.result != none
}
// error returns the error object if present in the response.
//
// Returns:
// - The error object if present, or none if no error is present
pub fn (resp Response) error() ?RPCError {
if err := resp.error_ {
return err
}
return none
}
// result returns the result string if no error is present.
// If an error is present, it returns the error instead.
//
// Returns:
// - The result string or an error if the response contains an error
pub fn (resp Response) result() !string {
if err := resp.error() {
return err
} // Ensure no error is present
return resp.result or {''}
}
// ResponseGeneric is a type-safe version of the Response struct that allows
// for strongly-typed results using generics.
// This provides compile-time type safety for response results.
pub struct ResponseGeneric[D] {
pub mut:
// The JSON-RPC protocol version, must be exactly "2.0"
jsonrpc string @[required]
// The result of the method invocation with a specific type D
result ?D
// Error object if the request failed
error_ ?RPCError @[json: 'error']
// Must match the id of the request that generated this response
id string @[required]
}
// new_response_generic creates a successful generic JSON-RPC response with a strongly-typed result.
//
// Parameters:
// - id: The ID from the request that this response is answering
// - result: The result of the method call, of type D
//
// Returns:
// - A ResponseGeneric object with result of type D
pub fn new_response_generic[D](id string, result D) ResponseGeneric[D] {
return ResponseGeneric[D]{
jsonrpc: jsonrpc.jsonrpc_version
result: result
id: id
}
}
// decode_response_generic parses a JSON string into a ResponseGeneric object with result of type D.
// This function handles the complex validation rules for JSON-RPC responses.
//
// Parameters:
// - data: A JSON string representing a JSON-RPC response
//
// Returns:
// - A ResponseGeneric object with result of type D, or an error if parsing fails
pub fn decode_response_generic[D](data string) !ResponseGeneric[D] {
// Debug output - consider removing in production
println('respodata ${data}')
raw := json2.raw_decode(data)!
raw_map := raw.as_map()
// Validate that the response contains either result or error, but not both or neither
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.')
}
// Handle error responses
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())!
}
}
// Handle successful responses
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
}
}
// encode serializes the ResponseGeneric object into a JSON string.
//
// Returns:
// - A JSON string representation of the ResponseGeneric object
pub fn (resp ResponseGeneric[D]) encode() string {
return json2.encode(resp)
}
// validate checks that the ResponseGeneric object follows the JSON-RPC 2.0 specification.
// A valid response must not contain both result and error.
//
// Returns:
// - An error if validation fails, otherwise nothing
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}')
}
}
// is_error checks if the response contains an error.
//
// Returns:
// - true if the response contains an error, false otherwise
pub fn (resp ResponseGeneric[D]) is_error() bool {
return resp.error_ != none
}
// is_result checks if the response contains a result.
//
// Returns:
// - true if the response contains a result, false otherwise
pub fn (resp ResponseGeneric[D]) is_result() bool {
return resp.result != none
}
// error returns the error object if present in the generic response.
//
// Returns:
// - The error object if present, or none if no error is present
pub fn (resp ResponseGeneric[D]) error() ?RPCError {
return resp.error_?
}
// result returns the result of type D if no error is present.
// If an error is present, it returns the error instead.
//
// Returns:
// - The result of type D or an error if the response contains an error
pub fn (resp ResponseGeneric[D]) result() !D {
if err := resp.error() {
return err
} // Ensure no error is present
return resp.result or {D{}}
}

View 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'}
}

View File

@@ -12,10 +12,10 @@ The generate.v file provides functions that can generate JSONSchema from [codemo
Example:
```go
struct_ := codemodel.Struct {
struct_ := code.Struct {
name: "Mystruct"
fields: [
codemodel.StructField {
code.StructField {
name: "myfield"
typ: "string"
}

View File

@@ -0,0 +1,206 @@
module codegen
import freeflowuniverse.herolib.core.code { Alias, Attribute, CodeItem, Struct, StructField, Type, type_from_symbol, Object, Array}
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
const vtypes = {
'integer': 'int'
'number': 'int'
'string': 'string'
'u32': 'u32'
'boolean': 'bool'
}
pub fn schema_to_v(schema Schema) string {
module_name := 'schema.title.'
structs := schema_to_structs(schema)
// todo: report bug: return $tmpl(...)
encoded := $tmpl('templates/schema.vtemplate')
return encoded
}
// schema_to_structs encodes a schema into V structs.
// if a schema has nested object type schemas or defines object type schemas,
// recursively encodes object type schemas and pushes to the array of structs.
// returns an array of schemas that have been encoded into V structs.
pub fn schema_to_structs(schema Schema) []string {
mut schemas := []string{}
mut properties := ''
// loop over properties
for name, property_ in schema.properties {
mut property := Schema{}
mut typesymbol := ''
if property_ is Reference {
// if reference, set typesymbol as reference name
ref := property_ as Reference
typesymbol = ref_to_symbol(ref)
} else {
property = property_ as Schema
typesymbol = schema_to_type(property).symbol()
// recursively encode property if object
// todo: handle duplicates
if property.typ == 'object' {
structs := schema_to_structs(property)
schemas << structs
}
}
properties += '\n\t${name} ${typesymbol}'
if name in schema.required {
properties += ' @[required]'
}
}
schemas << $tmpl('templates/struct.vtemplate')
return schemas
}
// schema_to_type generates a typesymbol for the schema
pub fn schema_to_type(schema Schema) Type {
if schema.typ == 'null' {
Type{}
}
mut property_str := ''
return match schema.typ {
'object' {
if schema.title == '' {
panic('Object schemas must define a title. ${schema}')
}
if schema.properties.len == 0 {
if additional_props := schema.additional_properties {
code.Map{code.String{}}
} else {Object{schema.title}}
}
else {Object{schema.title}}
}
'array' {
// todo: handle multiple item schemas
if items := schema.items {
if items is []SchemaRef {
panic('items of type []SchemaRef not implemented')
}
Array {
typ: schemaref_to_type(items as SchemaRef)
}
} else {
panic('items should not be none for arrays')
}
} else {
if schema.typ == 'integer' && schema.format != '' {
match schema.format {
'int8' { code.type_i8 }
'uint8' { code.type_u8 }
'int16' { code.type_i16 }
'uint16' { code.type_u16 }
'int32' { code.type_i32 }
'uint32' { code.type_u32 }
'int64' { code.type_i64 }
'uint64' { code.type_u64 }
else { code.Integer{} } // Default to 'int' if the format doesn't match any known type
}
}
else if schema.typ in vtypes.keys() {
type_from_symbol(vtypes[schema.typ])
} else if schema.title != '' {
type_from_symbol(schema.title)
} else {
panic('unknown type `${schema.typ}` ')
}
}
}
}
pub fn schema_to_code(schema Schema) CodeItem {
if schema.typ == 'object' {
return CodeItem(schema_to_struct(schema))
}
if schema.typ in vtypes {
return Alias{
name: schema.title
typ: type_from_symbol(vtypes[schema.typ])
}
}
if schema.typ == 'array' {
if items := schema.items {
if items is SchemaRef {
if items is Schema {
items_schema := items as Schema
return Alias{
name: schema.title
typ: type_from_symbol('[]${items_schema.typ}')
}
} else if items is Reference {
items_ref := items as Reference
return Alias{
name: schema.title
typ: type_from_symbol('[]${ref_to_symbol(items_ref)}')
}
}
}
} else {
panic('items of type []SchemaRef not implemented')
}
}
panic('Schema type ${schema.typ} not supported for code generation')
}
pub fn schema_to_struct(schema Schema) Struct {
mut fields := []StructField{}
for key, val in schema.properties {
mut field := ref_to_field(val, key)
if field.name in schema.required {
field.attrs << Attribute{
name: 'required'
}
}
fields << field
}
return Struct{
name: schema.title
description: schema.description
fields: fields
is_pub: true
}
}
pub fn ref_to_field(schema_ref SchemaRef, name string) StructField {
if schema_ref is Reference {
return StructField{
name: name
typ: type_from_symbol(ref_to_symbol(schema_ref))
}
} else if schema_ref is Schema {
mut field := StructField{
name: name
description: schema_ref.description
}
if schema_ref.typ == 'object' || schema_ref.typ == 'array' {
field.typ = schemaref_to_type(schema_ref)
return field
} else if schema_ref.typ in vtypes {
field.typ = type_from_symbol(vtypes[schema_ref.typ])
return field
}
panic('Schema type ${schema_ref.typ} not supported for code generation')
}
panic('Schema type not supported for code generation')
}
pub fn schemaref_to_type(schema_ref SchemaRef) Type {
return if schema_ref is Reference {
ref_to_type_from_reference(schema_ref as Reference)
} else {
schema_to_type(schema_ref as Schema)
}
}
pub fn ref_to_symbol(reference Reference) string {
return reference.ref.all_after_last('/')
}
pub fn ref_to_type_from_reference(reference Reference) Type {
return type_from_symbol(ref_to_symbol(reference))
}

View File

@@ -1,8 +1,9 @@
module jsonschema
module codegen
import freeflowuniverse.herolib.ui.console
import log
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
fn test_encode_simple() ! {
fn test_schema_to_structs_simple() ! {
struct_str := '
// person struct used for test schema encoding
struct TestPerson {
@@ -11,27 +12,27 @@ struct TestPerson {
}'
schema := Schema{
schema: 'test'
title: 'TestPerson'
schema: 'test'
title: 'TestPerson'
description: 'person struct used for test schema encoding'
typ: 'object'
properties: {
typ: 'object'
properties: {
'name': Schema{
typ: 'string'
typ: 'string'
description: 'name of the test person'
}
'age': Schema{
typ: 'integer'
typ: 'integer'
description: 'age of the test person'
}
}
}
encoded := schema.vstructs_encode()!
encoded := schema_to_structs(schema)
assert encoded.len == 1
assert encoded[0].trim_space() == struct_str.trim_space()
}
fn test_encode_schema_with_reference() ! {
fn test_schema_to_structs_with_reference() ! {
struct_str := '
// person struct used for test schema encoding
struct TestPerson {
@@ -41,17 +42,17 @@ struct TestPerson {
}'
schema := Schema{
schema: 'test'
title: 'TestPerson'
schema: 'test'
title: 'TestPerson'
description: 'person struct used for test schema encoding'
typ: 'object'
properties: {
typ: 'object'
properties: {
'name': Schema{
typ: 'string'
typ: 'string'
description: 'name of the test person'
}
'age': Schema{
typ: 'integer'
typ: 'integer'
description: 'age of the test person'
}
'friend': Reference{
@@ -59,43 +60,43 @@ struct TestPerson {
}
}
}
encoded := schema.vstructs_encode()!
encoded := schema_to_structs(schema)
assert encoded.len == 1
assert encoded[0].trim_space() == struct_str.trim_space()
}
fn test_encode_recursive() ! {
fn test_schema_to_structs_recursive() ! {
schema := Schema{
schema: 'test'
title: 'TestPerson'
schema: 'test'
title: 'TestPerson'
description: 'person struct used for test schema encoding'
typ: 'object'
properties: {
typ: 'object'
properties: {
'name': Schema{
typ: 'string'
typ: 'string'
description: 'name of the test person'
}
'age': Schema{
typ: 'integer'
typ: 'integer'
description: 'age of the test person'
}
'friend': Schema{
title: 'TestFriend'
typ: 'object'
title: 'TestFriend'
typ: 'object'
description: 'friend of the test person'
properties: {
properties: {
'name': Schema{
typ: 'string'
typ: 'string'
description: 'name of the test friend person'
}
'age': Schema{
typ: 'integer'
typ: 'integer'
description: 'age of the test friend person'
}
}
}
}
}
encoded := schema.vstructs_encode()!
console.print_debug(encoded)
}
encoded := schema_to_structs(schema)
log.debug(encoded.str())
}

View File

@@ -1,21 +1,22 @@
module jsonschema
module codegen
import freeflowuniverse.herolib.code.codemodel { Param, Result, Struct, Type }
import freeflowuniverse.herolib.core.code { Param, Result, Struct, Type }
import freeflowuniverse.herolib.schemas.jsonschema { SchemaRef, Schema, Reference, Number}
// struct_to_schema generates a json schema or reference from a struct model
pub fn sumtype_to_schema(sumtype codemodel.Sumtype) SchemaRef {
pub fn sumtype_to_schema(sumtype code.Sumtype) SchemaRef {
mut one_of := []SchemaRef{}
for type_ in sumtype.types {
property_schema := typesymbol_to_schema(type_.symbol)
property_schema := typesymbol_to_schema(type_.symbol())
one_of << property_schema
}
title := sumtype.name
return SchemaRef(Schema{
title: title
title: title
description: sumtype.description
one_of: one_of
one_of: one_of
})
}
@@ -24,7 +25,7 @@ pub fn struct_to_schema(struct_ Struct) SchemaRef {
mut properties := map[string]SchemaRef{}
for field in struct_.fields {
mut property_schema := SchemaRef(Schema{})
if field.typ.symbol.starts_with('_VAnonStruct') {
if field.typ.symbol().starts_with('_VAnonStruct') {
property_schema = struct_to_schema(field.anon_struct)
} else {
property_schema = type_to_schema(field.typ)
@@ -46,24 +47,14 @@ pub fn struct_to_schema(struct_ Struct) SchemaRef {
}
return SchemaRef(Schema{
title: title
title: title
description: struct_.description
properties: properties
properties: properties
})
}
pub fn param_to_schema(param Param) SchemaRef {
if param.struct_ != Struct{} {
return struct_to_schema(param.struct_)
}
return typesymbol_to_schema(param.typ.symbol)
}
pub fn result_to_schema(result Result) SchemaRef {
if result.structure != Struct{} {
return struct_to_schema(result.structure)
}
return typesymbol_to_schema(result.typ.symbol)
return typesymbol_to_schema(param.typ.symbol())
}
// typesymbol_to_schema receives a typesymbol, if the typesymbol belongs to a user defined struct
@@ -77,20 +68,20 @@ pub fn typesymbol_to_schema(symbol_ string) SchemaRef {
} else if symbol.starts_with('[]') {
mut array_type := symbol.trim_string_left('[]')
return SchemaRef(Schema{
typ: 'array'
typ: 'array'
items: typesymbol_to_schema(array_type)
})
} else if symbol.starts_with('map[string]') {
mut map_type := symbol.trim_string_left('map[string]')
return SchemaRef(Schema{
typ: 'object'
typ: 'object'
additional_properties: typesymbol_to_schema(map_type)
})
} else if symbol[0].is_capital() {
// todo: better imported type handling
if symbol == 'Uint128' {
return SchemaRef(Schema{
typ: 'integer'
typ: 'integer'
minimum: Number(0)
// todo: implement uint128 number
// maximum: Number('340282366920938463463374607431768211455')
@@ -171,28 +162,28 @@ pub fn typesymbol_to_schema(symbol_ string) SchemaRef {
}
pub fn type_to_schema(typ Type) SchemaRef {
mut symbol := typ.symbol.trim_string_left('!').trim_string_left('?')
mut symbol := typ.symbol().trim_string_left('!').trim_string_left('?')
if symbol == '' {
return SchemaRef(Schema{
typ: 'null'
})
} else if symbol.starts_with('[]') || typ.is_array {
} else if symbol.starts_with('[]') {
mut array_type := symbol.trim_string_left('[]')
return SchemaRef(Schema{
typ: 'array'
typ: 'array'
items: typesymbol_to_schema(array_type)
})
} else if symbol.starts_with('map[string]') {
mut map_type := symbol.trim_string_left('map[string]')
return SchemaRef(Schema{
typ: 'object'
typ: 'object'
additional_properties: typesymbol_to_schema(map_type)
})
} else if symbol[0].is_capital() {
// todo: better imported type handling
if symbol == 'Uint128' {
return SchemaRef(Schema{
typ: 'integer'
typ: 'integer'
minimum: Number(0)
// todo: implement uint128 number
// maximum: Number('340282366920938463463374607431768211455')

View File

@@ -0,0 +1,21 @@
module codegen
import log
import freeflowuniverse.herolib.core.code
fn test_struct_to_schema() {
struct_ := code.Struct{
name: 'test_name'
description: 'a codemodel struct to test struct to schema serialization'
fields: [
code.StructField{
name: 'test_field'
description: 'a field of the test struct to test fields serialization into schema'
typ: code.String{}
},
]
}
schema := struct_to_schema(struct_)
log.debug(schema.str())
}

View File

@@ -0,0 +1,77 @@
module jsonschema
// Define numeric schemas
const schema_u8 = Schema{
typ: "integer"
format: 'uint8'
minimum: 0
maximum: 255
description: "An unsigned 8-bit integer."
}
const schema_i8 = Schema{
typ: "integer"
format: 'int8'
minimum: -128
maximum: 127
description: "A signed 8-bit integer."
}
const schema_u16 = Schema{
typ: "integer"
format: 'uint16'
minimum: 0
maximum: 65535
description: "An unsigned 16-bit integer."
}
const schema_i16 = Schema{
typ: "integer"
format: 'int16'
minimum: -32768
maximum: 32767
description: "A signed 16-bit integer."
}
const schema_u32 = Schema{
typ: "integer"
format: 'uint32'
minimum: 0
maximum: 4294967295
description: "An unsigned 32-bit integer."
}
const schema_i32 = Schema{
typ: "integer"
format: 'int32'
minimum: -2147483648
maximum: 2147483647
description: "A signed 32-bit integer."
}
const schema_u64 = Schema{
typ: "integer"
format: 'uint64'
minimum: 0
maximum: 18446744073709551615
description: "An unsigned 64-bit integer."
}
const schema_i64 = Schema{
typ: "integer"
format: 'int64'
minimum: -9223372036854775808
maximum: 9223372036854775807
description: "A signed 64-bit integer."
}
const schema_f32 = Schema{
typ: "number"
description: "A 32-bit floating-point number."
}
const schema_f64 = Schema{
typ: "number"
description: "A 64-bit floating-point number."
}

View File

@@ -0,0 +1,86 @@
module jsonschema
import x.json2 { Any }
import json
// decode parses a JSON string into a Schema object.
// This function is necessary because of limitations in V's JSON decoding for complex types.
// It handles special fields like 'properties', 'additionalProperties', and 'items' that
// require custom decoding logic due to their complex structure.
//
// Parameters:
// - data: A JSON string representing a JSON Schema
//
// Returns:
// - A fully populated Schema object or an error if parsing fails
pub fn decode(data string) !Schema {
schema_map := json2.raw_decode(data)!.as_map()
mut schema := json.decode(Schema, data)!
for key, value in schema_map {
if key == 'properties' {
schema.properties = decode_schemaref_map(value.as_map())!
} else if key == 'additionalProperties' {
schema.additional_properties = decode_schemaref(value.as_map())!
} else if key == 'items' {
schema.items = decode_items(value)!
}
}
return schema
}
// decode_items parses the 'items' field from a JSON Schema, which can be either
// a single schema or an array of schemas.
//
// Parameters:
// - data: The raw JSON data for the 'items' field
//
// Returns:
// - Either a single SchemaRef or an array of SchemaRef objects
pub fn decode_items(data Any) !Items {
if data.str().starts_with('{') {
// If the items field is an object, it's a single schema
return decode_schemaref(data.as_map())!
}
if !data.str().starts_with('[') {
return error('items field must either be list of schemarefs or a schemaref')
}
// If the items field is an array, it's a list of schemas
mut items := []SchemaRef{}
for val in data.arr() {
items << decode_schemaref(val.as_map())!
}
return items
}
// decode_schemaref_map parses a map of schema references, typically used for the 'properties' field.
//
// Parameters:
// - data_map: A map where keys are property names and values are schema references
//
// Returns:
// - A map of property names to their corresponding schema references
pub fn decode_schemaref_map(data_map map[string]Any) !map[string]SchemaRef {
mut schemaref_map := map[string]SchemaRef{}
for key, val in data_map {
schemaref_map[key] = decode_schemaref(val.as_map())!
}
return schemaref_map
}
// decode_schemaref parses a single schema reference, which can be either a direct schema
// or a reference to another schema via the $ref keyword.
//
// Parameters:
// - data_map: The raw JSON data for a schema or reference
//
// Returns:
// - Either a Reference object or a Schema object
pub fn decode_schemaref(data_map map[string]Any) !SchemaRef {
if ref := data_map['\$ref'] {
return Reference{
ref: ref.str()
}
}
return decode(data_map.str())!
}

View File

@@ -1,7 +1,5 @@
module jsonschema
import json
import x.json2
import os
import freeflowuniverse.herolib.core.pathlib
@@ -13,34 +11,34 @@ struct Pet {
fn test_decode() ! {
mut pet_schema_file := pathlib.get_file(
path: '${testdata}/pet.json'
path: '${jsonschema.testdata}/pet.json'
)!
pet_schema_str := pet_schema_file.read()!
pet_schema := decode(pet_schema_str)!
assert pet_schema == Schema{
typ: 'object'
typ: 'object'
properties: {
'name': Schema{
typ: 'string'
}
}
required: ['name']
required: ['name']
}
}
fn test_decode_schemaref() ! {
mut pet_schema_file := pathlib.get_file(
path: '${testdata}/pet.json'
path: '${jsonschema.testdata}/pet.json'
)!
pet_schema_str := pet_schema_file.read()!
pet_schemaref := decode(pet_schema_str)!
assert pet_schemaref == Schema{
typ: 'object'
typ: 'object'
properties: {
'name': Schema{
typ: 'string'
}
}
required: ['name']
required: ['name']
}
}

Binary file not shown.

View File

@@ -0,0 +1,88 @@
module jsonschema
import x.json2 as json
// Items represents either a single schema reference or an array of schema references.
// This type is used for the 'items' field in JSON Schema which can be either a schema
// or an array of schemas.
pub type Items = SchemaRef | []SchemaRef
// SchemaRef represents either a direct Schema object or a Reference to a schema.
// This allows for both inline schema definitions and references to external schemas.
pub type SchemaRef = Reference | Schema
// Reference represents a JSON Schema reference using the $ref keyword.
// References point to definitions in the same document or external documents.
pub struct Reference {
pub:
// The reference path, e.g., "#/definitions/Person" or "http://example.com/schema.json#"
ref string @[json: '\$ref'; omitempty]
}
// Number is a type alias for numeric values in JSON Schema.
pub type Number = int
// Schema represents a JSON Schema document according to the JSON Schema specification.
// This implementation is based on JSON Schema draft-07.
// See: https://json-schema.org/draft-07/json-schema-release-notes.html
pub struct Schema {
pub mut:
// The $schema keyword identifies which version of JSON Schema the schema was written for
schema string @[json: 'schema'; omitempty]
// The $id keyword defines a URI for the schema
id string @[json: 'id'; omitempty]
// Human-readable title for the schema
title string @[omitempty]
// Human-readable description of the schema
description string @[omitempty]
// The data type for the schema (string, number, object, array, boolean, null)
typ string @[json: 'type'; omitempty]
// Object properties when type is "object"
properties map[string]SchemaRef @[omitempty]
// Controls additional properties not defined in the properties map
additional_properties ?SchemaRef @[json: 'additionalProperties'; omitempty]
// List of required property names
required []string @[omitempty]
// Schema for array items when type is "array"
items ?Items @[omitempty]
// Definitions of reusable schemas
defs map[string]SchemaRef @[omitempty]
// List of schemas, where data must validate against exactly one schema
one_of []SchemaRef @[json: 'oneOf'; omitempty]
// Semantic format of the data (e.g., "date-time", "email", "uri")
format string @[omitempty]
// === Validation for numbers ===
// The value must be a multiple of this number
multiple_of int @[json: 'multipleOf'; omitempty]
// The maximum allowed value
maximum int @[omitempty]
// The exclusive maximum allowed value (value must be less than, not equal to)
exclusive_maximum int @[json: 'exclusiveMaximum'; omitempty]
// The minimum allowed value
minimum int @[omitempty]
// The exclusive minimum allowed value (value must be greater than, not equal to)
exclusive_minimum int @[json: 'exclusiveMinimum'; omitempty]
// Enumerated list of allowed values
enum_ []string @[json: 'enum'; omitempty]
// Example value that would validate against this schema (not used for validation)
example json.Any @[json: '-']
}