diff --git a/lib/data/jsonschema/codegen.v b/lib/data/jsonschema/codegen.v deleted file mode 100644 index 4f06d4b3..00000000 --- a/lib/data/jsonschema/codegen.v +++ /dev/null @@ -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() - } -} diff --git a/lib/data/jsonschema/decode.v b/lib/data/jsonschema/decode.v deleted file mode 100644 index 91cdf5ec..00000000 --- a/lib/data/jsonschema/decode.v +++ /dev/null @@ -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())! -} diff --git a/lib/data/jsonschema/generate_test.v b/lib/data/jsonschema/generate_test.v deleted file mode 100644 index eb01d3aa..00000000 --- a/lib/data/jsonschema/generate_test.v +++ /dev/null @@ -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) -} diff --git a/lib/data/jsonschema/model.v b/lib/data/jsonschema/model.v deleted file mode 100644 index 70651984..00000000 --- a/lib/data/jsonschema/model.v +++ /dev/null @@ -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] -} diff --git a/lib/data/jsonschema/reflection.v b/lib/data/jsonschema/reflection.v deleted file mode 100644 index 87a06678..00000000 --- a/lib/data/jsonschema/reflection.v +++ /dev/null @@ -1 +0,0 @@ -module jsonschema diff --git a/lib/schemas/jsonrpc/README.md b/lib/schemas/jsonrpc/README.md new file mode 100644 index 00000000..5e627057 --- /dev/null +++ b/lib/schemas/jsonrpc/README.md @@ -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). diff --git a/lib/schemas/jsonrpc/client.v b/lib/schemas/jsonrpc/client.v new file mode 100644 index 00000000..5d6bbb02 --- /dev/null +++ b/lib/schemas/jsonrpc/client.v @@ -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()! +} \ No newline at end of file diff --git a/lib/schemas/jsonrpc/client_test.v b/lib/schemas/jsonrpc/client_test.v new file mode 100644 index 00000000..572de20a --- /dev/null +++ b/lib/schemas/jsonrpc/client_test.v @@ -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!' +} diff --git a/lib/schemas/jsonrpc/handler.v b/lib/schemas/jsonrpc/handler.v new file mode 100644 index 00000000..854e2b3e --- /dev/null +++ b/lib/schemas/jsonrpc/handler.v @@ -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 +} diff --git a/lib/schemas/jsonrpc/handler_test.v b/lib/schemas/jsonrpc/handler_test.v new file mode 100644 index 00000000..d98143da --- /dev/null +++ b/lib/schemas/jsonrpc/handler_test.v @@ -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' +} diff --git a/lib/schemas/jsonrpc/jsonrpc.dylib b/lib/schemas/jsonrpc/jsonrpc.dylib new file mode 100755 index 00000000..66ce237b Binary files /dev/null and b/lib/schemas/jsonrpc/jsonrpc.dylib differ diff --git a/lib/schemas/jsonrpc/model_error.v b/lib/schemas/jsonrpc/model_error.v new file mode 100644 index 00000000..e47ab1f8 --- /dev/null +++ b/lib/schemas/jsonrpc/model_error.v @@ -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 +} \ No newline at end of file diff --git a/lib/schemas/jsonrpc/model_request.v b/lib/schemas/jsonrpc/model_request.v new file mode 100644 index 00000000..a87a9404 --- /dev/null +++ b/lib/schemas/jsonrpc/model_request.v @@ -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) +} \ No newline at end of file diff --git a/lib/schemas/jsonrpc/model_request_test.v b/lib/schemas/jsonrpc/model_request_test.v new file mode 100644 index 00000000..1fdb3c25 --- /dev/null +++ b/lib/schemas/jsonrpc/model_request_test.v @@ -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"') +} \ No newline at end of file diff --git a/lib/schemas/jsonrpc/model_response.v b/lib/schemas/jsonrpc/model_response.v new file mode 100644 index 00000000..0a9d85c6 --- /dev/null +++ b/lib/schemas/jsonrpc/model_response.v @@ -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{}} +} \ No newline at end of file diff --git a/lib/schemas/jsonrpc/model_response_test.v b/lib/schemas/jsonrpc/model_response_test.v new file mode 100644 index 00000000..db8f82e9 --- /dev/null +++ b/lib/schemas/jsonrpc/model_response_test.v @@ -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'} +} \ No newline at end of file diff --git a/lib/data/jsonschema/README.md b/lib/schemas/jsonschema/README.md similarity index 94% rename from lib/data/jsonschema/README.md rename to lib/schemas/jsonschema/README.md index 5d32e165..e725b7ba 100644 --- a/lib/data/jsonschema/README.md +++ b/lib/schemas/jsonschema/README.md @@ -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" } diff --git a/lib/schemas/jsonschema/codegen/codegen.v b/lib/schemas/jsonschema/codegen/codegen.v new file mode 100644 index 00000000..411dd354 --- /dev/null +++ b/lib/schemas/jsonschema/codegen/codegen.v @@ -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)) +} \ No newline at end of file diff --git a/lib/data/jsonschema/codegen_test.v b/lib/schemas/jsonschema/codegen/codegen_test.v similarity index 61% rename from lib/data/jsonschema/codegen_test.v rename to lib/schemas/jsonschema/codegen/codegen_test.v index dff91e24..c921ea73 100644 --- a/lib/data/jsonschema/codegen_test.v +++ b/lib/schemas/jsonschema/codegen/codegen_test.v @@ -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()) +} \ No newline at end of file diff --git a/lib/data/jsonschema/generate.v b/lib/schemas/jsonschema/codegen/generate.v similarity index 84% rename from lib/data/jsonschema/generate.v rename to lib/schemas/jsonschema/codegen/generate.v index c9cbc291..6503a627 100644 --- a/lib/data/jsonschema/generate.v +++ b/lib/schemas/jsonschema/codegen/generate.v @@ -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') diff --git a/lib/schemas/jsonschema/codegen/generate_test.v b/lib/schemas/jsonschema/codegen/generate_test.v new file mode 100644 index 00000000..f3d33c3d --- /dev/null +++ b/lib/schemas/jsonschema/codegen/generate_test.v @@ -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()) +} diff --git a/lib/data/jsonschema/templates/schema.vtemplate b/lib/schemas/jsonschema/codegen/templates/schema.vtemplate similarity index 100% rename from lib/data/jsonschema/templates/schema.vtemplate rename to lib/schemas/jsonschema/codegen/templates/schema.vtemplate diff --git a/lib/data/jsonschema/templates/struct.vtemplate b/lib/schemas/jsonschema/codegen/templates/struct.vtemplate similarity index 100% rename from lib/data/jsonschema/templates/struct.vtemplate rename to lib/schemas/jsonschema/codegen/templates/struct.vtemplate diff --git a/lib/schemas/jsonschema/consts_numeric.v b/lib/schemas/jsonschema/consts_numeric.v new file mode 100644 index 00000000..79738529 --- /dev/null +++ b/lib/schemas/jsonschema/consts_numeric.v @@ -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." +} \ No newline at end of file diff --git a/lib/schemas/jsonschema/decode.v b/lib/schemas/jsonschema/decode.v new file mode 100644 index 00000000..41c108bd --- /dev/null +++ b/lib/schemas/jsonschema/decode.v @@ -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())! +} \ No newline at end of file diff --git a/lib/data/jsonschema/decode_test.v b/lib/schemas/jsonschema/decode_test.v similarity index 78% rename from lib/data/jsonschema/decode_test.v rename to lib/schemas/jsonschema/decode_test.v index 59f249fe..312d7715 100644 --- a/lib/data/jsonschema/decode_test.v +++ b/lib/schemas/jsonschema/decode_test.v @@ -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'] } } diff --git a/lib/schemas/jsonschema/jsonschema.dylib b/lib/schemas/jsonschema/jsonschema.dylib new file mode 100755 index 00000000..977bdaa5 Binary files /dev/null and b/lib/schemas/jsonschema/jsonschema.dylib differ diff --git a/lib/schemas/jsonschema/model.v b/lib/schemas/jsonschema/model.v new file mode 100644 index 00000000..d5325b37 --- /dev/null +++ b/lib/schemas/jsonschema/model.v @@ -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: '-'] +} \ No newline at end of file diff --git a/lib/data/jsonschema/testdata/pet.json b/lib/schemas/jsonschema/testdata/pet.json similarity index 100% rename from lib/data/jsonschema/testdata/pet.json rename to lib/schemas/jsonschema/testdata/pet.json