port schema modules from crystallib
This commit is contained in:
124
lib/schemas/jsonrpc/README.md
Normal file
124
lib/schemas/jsonrpc/README.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# JSON-RPC Module
|
||||
|
||||
This module provides a robust implementation of the JSON-RPC 2.0 protocol in VLang. It includes utilities for creating, sending, and handling JSON-RPC requests and responses, with support for custom transports, strong typing, and error management.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Request and Response Handling**:
|
||||
- Create and encode JSON-RPC requests (generic or non-generic).
|
||||
- Decode and validate JSON-RPC responses.
|
||||
- Manage custom parameters and IDs for requests.
|
||||
|
||||
- **Error Management**:
|
||||
- Predefined JSON-RPC errors based on the official specification.
|
||||
- Support for custom error creation and validation.
|
||||
|
||||
- **Generic Support**:
|
||||
- Strongly typed request and response handling using generics.
|
||||
|
||||
- **Customizable Transport**:
|
||||
- Pluggable transport client interface for flexibility (e.g., WebSocket, HTTP).
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. **Client Setup**
|
||||
|
||||
Create a new JSON-RPC client using a custom transport layer.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Implement the IRPCTransportClient interface for your transport (e.g., WebSocket)
|
||||
struct WebSocketTransport {
|
||||
// Add your transport-specific implementation here
|
||||
}
|
||||
|
||||
// Create a new JSON-RPC client
|
||||
mut client := jsonrpc.new_client(jsonrpc.Client{
|
||||
transport: WebSocketTransport{}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. **Sending a Request**
|
||||
|
||||
Send a strongly-typed JSON-RPC request and handle the response.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Define a request method and parameters
|
||||
params := YourParams{...}
|
||||
request := jsonrpc.new_request_generic('methodName', params)
|
||||
|
||||
// Configure send parameters
|
||||
send_params := jsonrpc.SendParams{
|
||||
timeout: 30
|
||||
retry: 3
|
||||
}
|
||||
|
||||
// Send the request and process the response
|
||||
response := client.send[YourParams, YourResult](request, send_params) or {
|
||||
eprintln('Error sending request: $err')
|
||||
return
|
||||
}
|
||||
|
||||
println('Response result: $response')
|
||||
```
|
||||
|
||||
### 3. **Handling Errors**
|
||||
|
||||
Use the predefined JSON-RPC errors or create custom ones.
|
||||
|
||||
```v
|
||||
import jsonrpc
|
||||
|
||||
// Predefined error
|
||||
err := jsonrpc.method_not_found
|
||||
|
||||
// Custom error
|
||||
custom_err := jsonrpc.RPCError{
|
||||
code: 12345
|
||||
message: 'Custom error message'
|
||||
data: 'Additional details'
|
||||
}
|
||||
|
||||
// Attach the error to a response
|
||||
response := jsonrpc.new_error('request_id', custom_err)
|
||||
println(response)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Modules and Key Components
|
||||
|
||||
### 1. **`model_request.v`**
|
||||
Handles JSON-RPC requests:
|
||||
- Structs: `Request`, `RequestGeneric`
|
||||
- Methods: `new_request`, `new_request_generic`, `decode_request`, etc.
|
||||
|
||||
### 2. **`model_response.v`**
|
||||
Handles JSON-RPC responses:
|
||||
- Structs: `Response`, `ResponseGeneric`
|
||||
- Methods: `new_response`, `new_response_generic`, `decode_response`, `validate`, etc.
|
||||
|
||||
### 3. **`model_error.v`**
|
||||
Manages JSON-RPC errors:
|
||||
- Struct: `RPCError`
|
||||
- Predefined errors: `parse_error`, `invalid_request`, etc.
|
||||
- Methods: `msg`, `is_empty`, etc.
|
||||
|
||||
### 4. **`client.v`**
|
||||
Implements the JSON-RPC client:
|
||||
- Structs: `Client`, `SendParams`, `ClientConfig`
|
||||
- Interface: `IRPCTransportClient`
|
||||
- Method: `send`
|
||||
|
||||
---
|
||||
|
||||
## JSON-RPC Specification Reference
|
||||
|
||||
This module adheres to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification).
|
||||
47
lib/schemas/jsonrpc/client.v
Normal file
47
lib/schemas/jsonrpc/client.v
Normal file
@@ -0,0 +1,47 @@
|
||||
module jsonrpc
|
||||
|
||||
// Interface for a transport client used by the JSON-RPC client.
|
||||
pub interface IRPCTransportClient {
|
||||
mut:
|
||||
send(string, SendParams) !string // Sends a request and returns the response as a string
|
||||
}
|
||||
|
||||
// JSON-RPC WebSocket client implementation.
|
||||
pub struct Client {
|
||||
mut:
|
||||
transport IRPCTransportClient // Transport layer to handle communication
|
||||
}
|
||||
|
||||
// Creates a new JSON-RPC client instance.
|
||||
pub fn new_client(client Client) &Client {
|
||||
return &Client{...client}
|
||||
}
|
||||
|
||||
// Parameters for configuring the `send` function.
|
||||
@[params]
|
||||
pub struct SendParams {
|
||||
timeout int = 60 // Timeout in seconds (default: 60)
|
||||
retry int // Number of retry attempts
|
||||
}
|
||||
|
||||
// Sends a JSON-RPC request and returns the response result of type `D`.
|
||||
// Validates the response and ensures the request/response IDs match.
|
||||
pub fn (mut c Client) send[T, D](request RequestGeneric[T], params SendParams) !D {
|
||||
response_json := c.transport.send(request.encode(), params)! // Send the encoded request
|
||||
response := decode_response_generic[D](response_json) or {
|
||||
return error('Unable to decode response.\n- Response: ${response_json}\n- Error: ${err}')
|
||||
}
|
||||
|
||||
response.validate() or {
|
||||
return error('Received invalid response: ${err}')
|
||||
}
|
||||
|
||||
if response.id != request.id {
|
||||
return error('Received response with different id ${response}')
|
||||
}
|
||||
|
||||
println('response ${response}')
|
||||
|
||||
// Return the result or propagate the error.
|
||||
return response.result()!
|
||||
}
|
||||
67
lib/schemas/jsonrpc/client_test.v
Normal file
67
lib/schemas/jsonrpc/client_test.v
Normal file
@@ -0,0 +1,67 @@
|
||||
module jsonrpc
|
||||
|
||||
import time
|
||||
|
||||
struct TestRPCTransportClient {}
|
||||
|
||||
fn (t TestRPCTransportClient) send(request_json string, params SendParams) !string {
|
||||
request := decode_request(request_json)!
|
||||
|
||||
// instead of sending request and returning response from rpc server
|
||||
// our test rpc transport client return a response
|
||||
response := if request.method == 'echo' {
|
||||
new_response(request.id, request.params)
|
||||
} else if request.method == 'test_error' {
|
||||
error := RPCError{
|
||||
code: 1
|
||||
message: 'intentional jsonrpc error response'
|
||||
}
|
||||
new_error_response(request.id, error)
|
||||
} else {
|
||||
new_error_response(request.id, method_not_found)
|
||||
}
|
||||
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
struct TestClient {
|
||||
Client
|
||||
}
|
||||
|
||||
fn test_new() {
|
||||
client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
}
|
||||
|
||||
fn test_send_json_rpc() {
|
||||
mut client := new_client(
|
||||
transport: TestRPCTransportClient{}
|
||||
)
|
||||
|
||||
request0 := new_request_generic[string]('echo', 'ECHO!')
|
||||
response0 := client.send[string, string](request0)!
|
||||
assert response0 == 'ECHO!'
|
||||
|
||||
request1 := new_request_generic[string]('test_error', '')
|
||||
if response1 := client.send[string, string](request1) {
|
||||
assert false, 'Should return internal error'
|
||||
} else {
|
||||
assert err is RPCError
|
||||
assert err.code() == 1
|
||||
assert err.msg() == 'intentional jsonrpc error response'
|
||||
}
|
||||
|
||||
request2 := new_request_generic[string]('nonexistent_method', '')
|
||||
if response2 := client.send[string, string](request2) {
|
||||
assert false, 'Should return not found error'
|
||||
} else {
|
||||
assert err is RPCError
|
||||
assert err.code() == 32601
|
||||
assert err.msg() == 'Method not found'
|
||||
}
|
||||
|
||||
request := new_request_generic[string]('echo', 'ECHO!')
|
||||
response := client.send[string, string](request)!
|
||||
assert response == 'ECHO!'
|
||||
}
|
||||
35
lib/schemas/jsonrpc/handler.v
Normal file
35
lib/schemas/jsonrpc/handler.v
Normal file
@@ -0,0 +1,35 @@
|
||||
module jsonrpc
|
||||
|
||||
import log
|
||||
import net.websocket
|
||||
|
||||
// JSON-RPC WebSoocket Server
|
||||
|
||||
pub struct Handler {
|
||||
pub:
|
||||
// map of method names to procedure handlers
|
||||
procedures map[string]ProcedureHandler
|
||||
}
|
||||
|
||||
// ProcedureHandler handles executing procedure calls
|
||||
// decodes payload, execute procedure function, return encoded result
|
||||
type ProcedureHandler = fn (payload string) !string
|
||||
|
||||
pub fn new_handler(handler Handler) !&Handler {
|
||||
return &Handler{...handler}
|
||||
}
|
||||
|
||||
pub fn (handler Handler) handler(client &websocket.Client, message string) string {
|
||||
return handler.handle(message) or { panic(err) }
|
||||
}
|
||||
|
||||
pub fn (handler Handler) handle(message string) !string {
|
||||
method := decode_request_method(message)!
|
||||
log.info('Handling remote procedure call to method: ${method}')
|
||||
procedure_func := handler.procedures[method] or {
|
||||
log.error('No procedure handler for method ${method} found')
|
||||
return method_not_found
|
||||
}
|
||||
response := procedure_func(message) or { panic(err) }
|
||||
return response
|
||||
}
|
||||
92
lib/schemas/jsonrpc/handler_test.v
Normal file
92
lib/schemas/jsonrpc/handler_test.v
Normal file
@@ -0,0 +1,92 @@
|
||||
module jsonrpc
|
||||
|
||||
// echo method for testing purposes
|
||||
fn method_echo(text string) !string {
|
||||
return text
|
||||
}
|
||||
|
||||
pub struct TestStruct {
|
||||
data string
|
||||
}
|
||||
|
||||
// structure echo method for testing purposes
|
||||
fn method_echo_struct(structure TestStruct) !TestStruct {
|
||||
return structure
|
||||
}
|
||||
|
||||
// method that returns error for testing purposes
|
||||
fn method_error(text string) !string {
|
||||
return error('some error')
|
||||
}
|
||||
|
||||
fn method_echo_handler(data string) !string {
|
||||
request := decode_request_generic[string](data)!
|
||||
result := method_echo(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn method_echo_struct_handler(data string) !string {
|
||||
request := decode_request_generic[TestStruct](data)!
|
||||
result := method_echo_struct(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic[TestStruct](request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn method_error_handler(data string) !string {
|
||||
request := decode_request_generic[string](data)!
|
||||
result := method_error(request.params) or {
|
||||
response := new_error_response(request.id,
|
||||
code: err.code()
|
||||
message: err.msg()
|
||||
)
|
||||
return response.encode()
|
||||
}
|
||||
response := new_response_generic(request.id, result)
|
||||
return response.encode()
|
||||
}
|
||||
|
||||
fn test_new() {
|
||||
handler := new_handler(Handler{})!
|
||||
}
|
||||
|
||||
fn test_handle() {
|
||||
handler := new_handler(Handler{
|
||||
procedures: {
|
||||
'method_echo': method_echo_handler
|
||||
'method_echo_struct': method_echo_struct_handler
|
||||
'method_error': method_error_handler
|
||||
}
|
||||
})!
|
||||
|
||||
params0 := 'ECHO!'
|
||||
request0 := new_request_generic[string]('method_echo', params0)
|
||||
decoded0 := handler.handle(request0.encode())!
|
||||
response0 := decode_response_generic[string](decoded0)!
|
||||
assert response0.result()! == params0
|
||||
|
||||
params1 := TestStruct{'ECHO!'}
|
||||
request1 := new_request_generic[TestStruct]('method_echo_struct', params1)
|
||||
decoded1 := handler.handle(request1.encode())!
|
||||
response1 := decode_response_generic[TestStruct](decoded1)!
|
||||
assert response1.result()! == params1
|
||||
|
||||
params2 := 'ECHO!'
|
||||
request2 := new_request_generic[string]('method_error', params2)
|
||||
decoded2 := handler.handle(request2.encode())!
|
||||
response2 := decode_response_generic[string](decoded2)!
|
||||
assert response2.is_error()
|
||||
assert response2.error()?.message == 'some error'
|
||||
}
|
||||
60
lib/schemas/jsonrpc/model_error.v
Normal file
60
lib/schemas/jsonrpc/model_error.v
Normal file
@@ -0,0 +1,60 @@
|
||||
module jsonrpc
|
||||
|
||||
// Predefined JSON-RPC errors as per the specification: https://www.jsonrpc.org/specification
|
||||
pub const parse_error = RPCError{
|
||||
code: 32700
|
||||
message: 'Parse error'
|
||||
data: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.'
|
||||
}
|
||||
pub const invalid_request = RPCError{
|
||||
code: 32600
|
||||
message: 'Invalid Request'
|
||||
data: 'The JSON sent is not a valid Request object.'
|
||||
}
|
||||
pub const method_not_found = RPCError{
|
||||
code: 32601
|
||||
message: 'Method not found'
|
||||
data: 'The method does not exist / is not available.'
|
||||
}
|
||||
pub const invalid_params = RPCError{
|
||||
code: 32602
|
||||
message: 'Invalid params'
|
||||
data: 'Invalid method parameter(s).'
|
||||
}
|
||||
pub const internal_error = RPCError{
|
||||
code: 32603
|
||||
message: 'Internal RPCError'
|
||||
data: 'Internal JSON-RPC error.'
|
||||
}
|
||||
|
||||
// Represents a JSON-RPC error object with a code, message, and optional data.
|
||||
pub struct RPCError {
|
||||
pub mut:
|
||||
code int // Error code indicating the type of error
|
||||
message string // Brief error description
|
||||
data string // Additional details about the error
|
||||
}
|
||||
|
||||
// Creates a new error response for a given request ID.
|
||||
pub fn new_error(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the error message.
|
||||
pub fn (err RPCError) msg() string {
|
||||
return err.message
|
||||
}
|
||||
|
||||
// Returns the error code.
|
||||
pub fn (err RPCError) code() int {
|
||||
return err.code
|
||||
}
|
||||
|
||||
// Checks if the error object is empty (i.e., uninitialized).
|
||||
pub fn (err RPCError) is_empty() bool {
|
||||
return err.code == 0
|
||||
}
|
||||
80
lib/schemas/jsonrpc/model_request.v
Normal file
80
lib/schemas/jsonrpc/model_request.v
Normal file
@@ -0,0 +1,80 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import rand
|
||||
|
||||
// Represents a JSON-RPC request with essential fields.
|
||||
pub struct Request {
|
||||
pub mut:
|
||||
jsonrpc string @[required] // JSON-RPC version, e.g., "2.0"
|
||||
method string @[required] // Method to invoke
|
||||
params string @[required] // JSON-encoded parameters
|
||||
id string @[required] // Unique request ID
|
||||
}
|
||||
|
||||
// Creates a new JSON-RPC request with the specified method and parameters.
|
||||
pub fn new_request(method string, params string) Request {
|
||||
return Request{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4() // Automatically generate a unique ID
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a `Request` object.
|
||||
pub fn decode_request(data string) !Request {
|
||||
return json2.decode[Request](data)!
|
||||
}
|
||||
|
||||
// Encodes the `Request` object into a JSON string.
|
||||
pub fn (req Request) encode() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
|
||||
// A generic JSON-RPC request struct allowing strongly-typed parameters.
|
||||
pub struct RequestGeneric[T] {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
method string @[required]
|
||||
params T @[required]
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// Creates a new generic JSON-RPC request.
|
||||
pub fn new_request_generic[T](method string, params T) RequestGeneric[T] {
|
||||
return RequestGeneric[T]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
method: method
|
||||
params: params
|
||||
id: rand.uuid_v4()
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the `id` field from a JSON string.
|
||||
// Returns an error if the field is missing.
|
||||
pub fn decode_request_id(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
id_any := data_map['id'] or { return error('ID field not found') }
|
||||
return id_any.str()
|
||||
}
|
||||
|
||||
// Extracts the `method` field from a JSON string.
|
||||
// Returns an error if the field is missing.
|
||||
pub fn decode_request_method(data string) !string {
|
||||
data_any := json2.raw_decode(data)!
|
||||
data_map := data_any.as_map()
|
||||
method_any := data_map['method'] or { return error('Method field not found') }
|
||||
return method_any.str()
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a generic `Request` object.
|
||||
pub fn decode_request_generic[T](data string) !RequestGeneric[T] {
|
||||
return json2.decode[RequestGeneric[T]](data)!
|
||||
}
|
||||
|
||||
// Encodes a generic `Request` object into a JSON string.
|
||||
pub fn (req RequestGeneric[T]) encode[T]() string {
|
||||
return json2.encode(req)
|
||||
}
|
||||
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
73
lib/schemas/jsonrpc/model_request_test.v
Normal file
@@ -0,0 +1,73 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_request() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
request := decode_request(data) or {
|
||||
assert false, 'Failed to decode request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == 'test_params'
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_encode() {
|
||||
request := new_request('test_method', 'test_params')
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_new_request_generic() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
assert request.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == params
|
||||
assert request.id != '' // Ensure the ID is generated
|
||||
}
|
||||
|
||||
fn test_decode_request_id() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
id := decode_request_id(data) or {
|
||||
assert false, 'Failed to decode request ID: $err'
|
||||
return
|
||||
}
|
||||
assert id == '123'
|
||||
}
|
||||
|
||||
fn test_decode_request_method() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":"test_params","id":"123"}'
|
||||
method := decode_request_method(data) or {
|
||||
assert false, 'Failed to decode request method: $err'
|
||||
return
|
||||
}
|
||||
assert method == 'test_method'
|
||||
}
|
||||
|
||||
fn test_decode_request_generic() {
|
||||
data := '{"jsonrpc":"2.0","method":"test_method","params":{"key":"value"},"id":"123"}'
|
||||
request := decode_request_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic request: $err'
|
||||
return
|
||||
}
|
||||
assert request.jsonrpc == '2.0'
|
||||
assert request.method == 'test_method'
|
||||
assert request.params == {'key': 'value'}
|
||||
assert request.id == '123'
|
||||
}
|
||||
|
||||
fn test_request_generic_encode() {
|
||||
params := {'key': 'value'}
|
||||
request := new_request_generic('test_method', params)
|
||||
json := request.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"method"') && json.contains('"params"') && json.contains('"id"')
|
||||
}
|
||||
181
lib/schemas/jsonrpc/model_response.v
Normal file
181
lib/schemas/jsonrpc/model_response.v
Normal file
@@ -0,0 +1,181 @@
|
||||
module jsonrpc
|
||||
|
||||
import x.json2
|
||||
import json
|
||||
|
||||
// The JSON-RPC version used for responses.
|
||||
const jsonrpc_version = '2.0'
|
||||
|
||||
// Represents a JSON-RPC response, which includes a result, an error, or both.
|
||||
pub struct Response {
|
||||
pub:
|
||||
jsonrpc string @[required] // JSON-RPC version, e.g., "2.0"
|
||||
result ?string // JSON-encoded result (optional)
|
||||
error_ ?RPCError @[json: 'error'] // Error object if the request failed (optional)
|
||||
id string @[required] // Matches the request ID
|
||||
}
|
||||
|
||||
// Creates a successful response with the given result.
|
||||
pub fn new_response(id string, result string) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an error response with the given error object.
|
||||
pub fn new_error_response(id string, error RPCError) Response {
|
||||
return Response{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
error_: error
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a `Response` object.
|
||||
pub fn decode_response(data string) !Response {
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
if err := raw_map['error'] {
|
||||
id_any := raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}
|
||||
return Response {
|
||||
id: id_any.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
return Response {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: raw_map['result']!.str()
|
||||
}
|
||||
}
|
||||
|
||||
// Encodes the `Response` object into a JSON string.
|
||||
pub fn (resp Response) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// Validates that the response does not contain both `result` and `error`.
|
||||
pub fn (resp Response) validate() ! {
|
||||
// if err := resp.error_ && resp.result != '' {
|
||||
// return error('Response contains both error and result.\n- Error: ${resp.error_.str()}\n- Result: ${resp.result}')
|
||||
// }
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp Response) error() ?RPCError {
|
||||
if err := resp.error_ {
|
||||
return err
|
||||
}
|
||||
return none
|
||||
}
|
||||
|
||||
// Returns the result if no error is present.
|
||||
pub fn (resp Response) result() !string {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {''}
|
||||
}
|
||||
|
||||
// A generic JSON-RPC response, allowing strongly-typed results.
|
||||
pub struct ResponseGeneric[D] {
|
||||
pub mut:
|
||||
jsonrpc string @[required]
|
||||
result ?D
|
||||
error_ ?RPCError @[json: 'error'] // Error object if the request failed (optional)
|
||||
id string @[required]
|
||||
}
|
||||
|
||||
// Creates a successful generic response with the given result.
|
||||
pub fn new_response_generic[D](id string, result D) ResponseGeneric[D] {
|
||||
return ResponseGeneric[D]{
|
||||
jsonrpc: jsonrpc.jsonrpc_version
|
||||
result: result
|
||||
id: id
|
||||
}
|
||||
}
|
||||
|
||||
// Decodes a JSON string into a generic `ResponseGeneric` object.
|
||||
pub fn decode_response_generic[D](data string) !ResponseGeneric[D] {
|
||||
println('respodata ${data}')
|
||||
raw := json2.raw_decode(data)!
|
||||
raw_map := raw.as_map()
|
||||
|
||||
if 'error' !in raw_map.keys() && 'result' !in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, no error and result found.')
|
||||
} else if 'error' in raw_map.keys() && 'result' in raw_map.keys() {
|
||||
return error('Invalid JSONRPC response, both error and result found.')
|
||||
}
|
||||
|
||||
if err := raw_map['error'] {
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
error_: json2.decode[RPCError](err.str())!
|
||||
}
|
||||
}
|
||||
|
||||
resp := json.decode(ResponseGeneric[D], data)!
|
||||
return ResponseGeneric[D] {
|
||||
id: raw_map['id'] or {return error('Invalid JSONRPC response, no ID Field found')}.str()
|
||||
jsonrpc: jsonrpc_version
|
||||
result: resp.result
|
||||
}
|
||||
}
|
||||
|
||||
// Encodes the generic `ResponseGeneric` object into a JSON string.
|
||||
pub fn (resp ResponseGeneric[D]) encode() string {
|
||||
return json2.encode(resp)
|
||||
}
|
||||
|
||||
// Validates that the generic response does not contain both `result` and `error`.
|
||||
pub fn (resp ResponseGeneric[D]) validate() ! {
|
||||
if resp.is_error() && resp.is_result() {
|
||||
return error('Response contains both error and result.\n- Error: ${resp.error.str()}\n- Result: ${resp.result}')
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp ResponseGeneric[D]) is_error() bool {
|
||||
return resp.error_ != none
|
||||
}
|
||||
|
||||
// Returns the error if present in the response.
|
||||
pub fn (resp ResponseGeneric[D]) is_result() bool {
|
||||
return resp.result != none
|
||||
}
|
||||
|
||||
|
||||
// Returns the error if present in the generic response.
|
||||
pub fn (resp ResponseGeneric[D]) error() ?RPCError {
|
||||
return resp.error_?
|
||||
}
|
||||
|
||||
// Returns the result if no error is present in the generic response.
|
||||
pub fn (resp ResponseGeneric[D]) result() !D {
|
||||
if err := resp.error() {
|
||||
return err
|
||||
} // Ensure no error is present
|
||||
return resp.result or {D{}}
|
||||
}
|
||||
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
189
lib/schemas/jsonrpc/model_response_test.v
Normal file
@@ -0,0 +1,189 @@
|
||||
module jsonrpc
|
||||
|
||||
fn test_new_response() {
|
||||
response := new_response('123', 'test_result')
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_error_response() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
|
||||
response.validate()!
|
||||
assert response.is_error()
|
||||
assert !response.is_result() // Ensure no result is set
|
||||
assert response.id == '123'
|
||||
|
||||
response_error := response.error()?
|
||||
assert response_error == error
|
||||
}
|
||||
|
||||
fn test_decode_response() {
|
||||
data := '{"jsonrpc":"2.0","result":"test_result","id":"123"}'
|
||||
response := decode_response(data) or {
|
||||
assert false, 'Failed to decode response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_response_encode() {
|
||||
response := new_response('123', 'test_result')
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_validate() {
|
||||
response := new_response('123', 'test_result')
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := Response{
|
||||
jsonrpc: '2.0'
|
||||
result: 'test_result'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := new_error_response('123', error)
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_result() {
|
||||
response := new_response('123', 'test_result')
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == 'test_result'
|
||||
}
|
||||
|
||||
fn test_new_response_generic() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
assert response.jsonrpc == jsonrpc.jsonrpc_version
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_decode_response_generic() {
|
||||
data := '{"jsonrpc":"2.0","result":{"key":"value"},"id":"123"}'
|
||||
response := decode_response_generic[map[string]string](data) or {
|
||||
assert false, 'Failed to decode generic response: $err'
|
||||
return
|
||||
}
|
||||
assert response.jsonrpc == '2.0'
|
||||
assert response.id == '123'
|
||||
|
||||
assert response.is_result()
|
||||
assert !response.is_error() // Ensure no error is set
|
||||
result := response.result() or {
|
||||
assert false, 'Response should have result'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
|
||||
fn test_response_generic_encode() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
json := response.encode()
|
||||
assert json.contains('"jsonrpc"') && json.contains('"result"') && json.contains('"id"')
|
||||
}
|
||||
|
||||
fn test_response_generic_validate() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
response.validate() or { assert false, 'Validation failed for valid response: $err' }
|
||||
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
invalid_response := ResponseGeneric{
|
||||
jsonrpc: '2.0'
|
||||
result: {'key': 'value'}
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
invalid_response.validate() or {
|
||||
assert err.msg().contains('Response contains both error and result.')
|
||||
}
|
||||
}
|
||||
|
||||
fn test_response_generic_error() {
|
||||
error := RPCError{
|
||||
code: 123
|
||||
message: 'Test error'
|
||||
data: 'Error details'
|
||||
}
|
||||
response := ResponseGeneric[map[string]string]{
|
||||
jsonrpc: '2.0'
|
||||
error_: error
|
||||
id: '123'
|
||||
}
|
||||
err := response.error() or {
|
||||
assert false, 'Failed to get error: $err'
|
||||
return
|
||||
}
|
||||
assert err.code == 123
|
||||
assert err.message == 'Test error'
|
||||
assert err.data == 'Error details'
|
||||
}
|
||||
|
||||
fn test_response_generic_result() {
|
||||
response := new_response_generic('123', {'key': 'value'})
|
||||
result := response.result() or {
|
||||
assert false, 'Failed to get result: $err'
|
||||
return
|
||||
}
|
||||
assert result == {'key': 'value'}
|
||||
}
|
||||
27
lib/schemas/jsonrpc/testdata/handler_code.v
vendored
Normal file
27
lib/schemas/jsonrpc/testdata/handler_code.v
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
@[heap]
|
||||
struct TesterHandler {
|
||||
state Tester
|
||||
}
|
||||
|
||||
// handle handles an incoming JSON-RPC encoded message and returns an encoded response
|
||||
pub fn (handler TesterHandler) handle(msg string) string {
|
||||
method := jsonrpc.jsonrpcrequest_decode_method(msg)!
|
||||
match method {
|
||||
'test_notification_method' {
|
||||
jsonrpc.notify[string](msg, handler.state.test_notification_method)!
|
||||
}
|
||||
'test_invocation_method' {
|
||||
return jsonrpc.invoke[string](msg, handler.state.test_invocation_method)!
|
||||
}
|
||||
'test_method' {
|
||||
return jsonrpc.call[string, string](msg, handler.state.test_method)!
|
||||
}
|
||||
'test_method_structs' {
|
||||
return jsonrpc.call[Key, Value](msg, handler.state.test_method_structs)!
|
||||
}
|
||||
else {
|
||||
return error('method ${method} not handled')
|
||||
}
|
||||
}
|
||||
return error('this should never happen')
|
||||
}
|
||||
5
lib/schemas/jsonrpc/testdata/testfunction.v
vendored
Normal file
5
lib/schemas/jsonrpc/testdata/testfunction.v
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module main
|
||||
|
||||
pub fn testfunction(param string) string {
|
||||
return param
|
||||
}
|
||||
14
lib/schemas/jsonrpc/testdata/testmodule/testfunctions.v
vendored
Normal file
14
lib/schemas/jsonrpc/testdata/testmodule/testfunctions.v
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
module testmodule
|
||||
|
||||
pub fn testfunction0(param string) string {
|
||||
return param
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
name string
|
||||
number int
|
||||
}
|
||||
|
||||
pub fn testfunction1(config Config) []string {
|
||||
return []string{len: config.number, init: config.name}
|
||||
}
|
||||
54
lib/schemas/jsonrpc/testdata/testserver/openrpc_server.v
vendored
Normal file
54
lib/schemas/jsonrpc/testdata/testserver/openrpc_server.v
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
module main
|
||||
|
||||
import log
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc { JsonRpcHandler, jsonrpcrequest_decode }
|
||||
import freeflowuniverse.herolib.data.rpcwebsocket
|
||||
import data.jsonrpc.testdata.testmodule { Config, testfunction0, testfunction1 }
|
||||
|
||||
struct CustomJsonRpcHandler {
|
||||
JsonRpcHandler
|
||||
}
|
||||
|
||||
fn testfunction0_handler(data string) !string {
|
||||
request := jsonrpcrequest_decode[string](data)!
|
||||
result := testfunction0(request.params)
|
||||
response := jsonrpc.JsonRpcResponse[string]{
|
||||
jsonrpc: '2.0.0'
|
||||
id: request.id
|
||||
result: result
|
||||
}
|
||||
return response.to_json()
|
||||
}
|
||||
|
||||
fn testfunction1_handler(data string) !string {
|
||||
request := jsonrpcrequest_decode[Config](data)!
|
||||
result := testfunction1(request.params)
|
||||
response := jsonrpc.JsonRpcResponse[[]string]{
|
||||
jsonrpc: '2.0.0'
|
||||
id: request.id
|
||||
result: result
|
||||
}
|
||||
return response.to_json()
|
||||
}
|
||||
|
||||
// run_server creates and runs a jsonrpc ws server
|
||||
// handles rpc requests to reverse_echo function
|
||||
pub fn run_server() ! {
|
||||
mut logger := log.Logger(&log.Log{
|
||||
level: .debug
|
||||
})
|
||||
|
||||
mut handler := CustomJsonRpcHandler{
|
||||
JsonRpcHandler: jsonrpc.new_handler(&logger)!
|
||||
}
|
||||
|
||||
handler.state = &state
|
||||
// register rpc methods
|
||||
handler.register(testfunction0, testfunction0_handle)!
|
||||
handler.register(testfunction1, testfunction1_handle)!
|
||||
|
||||
// create & run rpc ws server
|
||||
mut jsonrpc_ws_server := rpcwebsocket.new_rpcwsserver(8080, handler, &logger)!
|
||||
jsonrpc_ws_server.run()!
|
||||
}
|
||||
37
lib/schemas/jsonschema/README.md
Normal file
37
lib/schemas/jsonschema/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# JSON Schema
|
||||
|
||||
A V library for the JSON Schema model, and a few handy functions.
|
||||
|
||||
## JSON Schema Model
|
||||
|
||||
Defined [here](https://json-schema.org/), "JSON Schema is a declarative language that allows you to annotate and validate JSON documents." The model in this module provides a struct that can easily be encoded into a JSON Schema.
|
||||
|
||||
## Generating a Schema
|
||||
|
||||
The generate.v file provides functions that can generate JSONSchema from [codemodels](../codemodel/). This allows for easy generation of JSON Schema from structs, and is useful for generating schemas from parsed code in v.
|
||||
|
||||
Example:
|
||||
```go
|
||||
struct_ := code.Struct {
|
||||
name: "Mystruct"
|
||||
fields: [
|
||||
code.StructField {
|
||||
name: "myfield"
|
||||
typ: "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
schema := struct_to_schema(struct_)
|
||||
```
|
||||
|
||||
### Generating Schemas for Anonymous Structs
|
||||
|
||||
The properties of a JSON Schema is a list of key value pairs, where keys represent the subschema's name and the value is the schema (or the reference to the schema which is defined elsewhere) of the property. This is analogous to the fields of a struct, which is represented by a field name and a type.
|
||||
|
||||
It's good practice to define object type schemas separately and reference them in properties, especially if the same schema is used in multiple places. However, object type schemas can also be defined in property definitions. This may make sense if the schema is exclusively used as a property of a schema, similar to using an anonymous struct for the type definition of a field of a struct.
|
||||
|
||||
As such, schema's generated from structs that declare anonymous structs as field types, include a schema definition in the property field.
|
||||
|
||||
## Notes
|
||||
|
||||
As [this issue](https://github.com/vlang/v/issues/15081) is still not resolved, a json schema cannot be decoded into the json schema structure defined in this module. As such, to decode json schema string into a structure the `pub fn decode(data str) !Schema` function defined in `decode.v` must be used.
|
||||
187
lib/schemas/jsonschema/codegen/codegen.v
Normal file
187
lib/schemas/jsonschema/codegen/codegen.v
Normal file
@@ -0,0 +1,187 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { Alias, Attribute, CodeItem, Struct, StructField, Type }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
|
||||
|
||||
const vtypes = {
|
||||
'integer': 'int'
|
||||
'string': 'string'
|
||||
}
|
||||
|
||||
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)!
|
||||
// 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) !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: enforce 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_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{
|
||||
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: '[]${ref_to_symbol(items_ref)}'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return error('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
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ref_to_field(schema_ref SchemaRef, name string) !StructField {
|
||||
if schema_ref is Reference {
|
||||
return StructField{
|
||||
name: name
|
||||
typ: Type{
|
||||
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' {
|
||||
// then it is an anonymous struct
|
||||
field.anon_struct = schema_to_struct(schema_ref as Schema)!
|
||||
return field
|
||||
} else if schema_ref.typ in vtypes {
|
||||
field.typ.symbol = vtypes[schema_ref.typ]
|
||||
return field
|
||||
}
|
||||
return error('Schema type ${schema_ref.typ} not supported for code generation')
|
||||
}
|
||||
return error('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 {
|
||||
Type{
|
||||
symbol: 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{
|
||||
symbol: ref_to_symbol(reference)
|
||||
}
|
||||
}
|
||||
102
lib/schemas/jsonschema/codegen/codegen_test.v
Normal file
102
lib/schemas/jsonschema/codegen/codegen_test.v
Normal file
@@ -0,0 +1,102 @@
|
||||
module codegen
|
||||
|
||||
import log
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef, Reference }
|
||||
|
||||
fn test_schema_to_structs_simple() ! {
|
||||
struct_str := '
|
||||
// person struct used for test schema encoding
|
||||
struct TestPerson {
|
||||
name string
|
||||
age int
|
||||
}'
|
||||
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema_to_structs(schema)!
|
||||
assert encoded.len == 1
|
||||
assert encoded[0].trim_space() == struct_str.trim_space()
|
||||
}
|
||||
|
||||
fn test_schema_to_structs_with_reference() ! {
|
||||
struct_str := '
|
||||
// person struct used for test schema encoding
|
||||
struct TestPerson {
|
||||
name string
|
||||
age int
|
||||
friend Friend
|
||||
}'
|
||||
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
'friend': Reference{
|
||||
ref: '#components/schemas/Friend'
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema_to_structs(schema)!
|
||||
assert encoded.len == 1
|
||||
assert encoded[0].trim_space() == struct_str.trim_space()
|
||||
}
|
||||
|
||||
fn test_schema_to_structs_recursive() ! {
|
||||
schema := Schema{
|
||||
schema: 'test'
|
||||
title: 'TestPerson'
|
||||
description: 'person struct used for test schema encoding'
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
description: 'name of the test person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
description: 'age of the test person'
|
||||
}
|
||||
'friend': Schema{
|
||||
title: 'TestFriend'
|
||||
typ: 'object'
|
||||
description: 'friend of the test person'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
description: 'name of the test friend person'
|
||||
}
|
||||
'age': Schema{
|
||||
typ: 'integer'
|
||||
description: 'age of the test friend person'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
encoded := schema_to_structs(schema)!
|
||||
log.debug(encoded.str())
|
||||
}
|
||||
269
lib/schemas/jsonschema/codegen/generate.v
Normal file
269
lib/schemas/jsonschema/codegen/generate.v
Normal file
@@ -0,0 +1,269 @@
|
||||
module codegen
|
||||
|
||||
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 code.Sumtype) SchemaRef {
|
||||
mut one_of := []SchemaRef{}
|
||||
for type_ in sumtype.types {
|
||||
property_schema := typesymbol_to_schema(type_.symbol)
|
||||
one_of << property_schema
|
||||
}
|
||||
|
||||
title := sumtype.name
|
||||
|
||||
return SchemaRef(Schema{
|
||||
title: title
|
||||
description: sumtype.description
|
||||
one_of: one_of
|
||||
})
|
||||
}
|
||||
|
||||
// struct_to_schema generates a json schema or reference from a struct model
|
||||
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') {
|
||||
property_schema = struct_to_schema(field.anon_struct)
|
||||
} else {
|
||||
property_schema = type_to_schema(field.typ)
|
||||
}
|
||||
if mut property_schema is Schema {
|
||||
properties[field.name] = SchemaRef(Schema{
|
||||
...property_schema
|
||||
description: field.description
|
||||
})
|
||||
} else {
|
||||
properties[field.name] = property_schema
|
||||
}
|
||||
}
|
||||
|
||||
title := if struct_.name.starts_with('_VAnonStruct') {
|
||||
''
|
||||
} else {
|
||||
struct_.name
|
||||
}
|
||||
|
||||
return SchemaRef(Schema{
|
||||
title: title
|
||||
description: struct_.description
|
||||
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)
|
||||
}
|
||||
|
||||
// typesymbol_to_schema receives a typesymbol, if the typesymbol belongs to a user defined struct
|
||||
// it returns a reference to the schema, else it returns a schema for the typesymbol
|
||||
pub fn typesymbol_to_schema(symbol_ string) SchemaRef {
|
||||
mut symbol := symbol_.trim_string_left('!').trim_string_left('?')
|
||||
if symbol == '' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
} else if symbol.starts_with('[]') {
|
||||
mut array_type := symbol.trim_string_left('[]')
|
||||
return SchemaRef(Schema{
|
||||
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'
|
||||
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'
|
||||
minimum: Number(0)
|
||||
// todo: implement uint128 number
|
||||
// maximum: Number('340282366920938463463374607431768211455')
|
||||
})
|
||||
}
|
||||
return SchemaRef(Reference{
|
||||
ref: '#/components/schemas/${symbol}'
|
||||
})
|
||||
} else if symbol.starts_with('_VAnonStruct') {
|
||||
return SchemaRef(Reference{
|
||||
ref: '#/components/schemas/${symbol}'
|
||||
})
|
||||
} else {
|
||||
if symbol == 'void' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
}
|
||||
if symbol == 'bool' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'boolean'
|
||||
})
|
||||
}
|
||||
if symbol == 'int' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u8' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u16' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u32' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == 'f32' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == 'f64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == '!' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
}
|
||||
if symbol == 'i64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == 'byte' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
return SchemaRef(Schema{
|
||||
typ: symbol
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_to_schema(typ Type) SchemaRef {
|
||||
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 {
|
||||
mut array_type := symbol.trim_string_left('[]')
|
||||
return SchemaRef(Schema{
|
||||
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'
|
||||
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'
|
||||
minimum: Number(0)
|
||||
// todo: implement uint128 number
|
||||
// maximum: Number('340282366920938463463374607431768211455')
|
||||
})
|
||||
}
|
||||
return SchemaRef(Reference{
|
||||
ref: '#/components/schemas/${symbol}'
|
||||
})
|
||||
} else if symbol.starts_with('_VAnonStruct') {
|
||||
return SchemaRef(Reference{
|
||||
ref: '#/components/schemas/${symbol}'
|
||||
})
|
||||
} else {
|
||||
if symbol == 'void' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
}
|
||||
if symbol == 'bool' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'boolean'
|
||||
})
|
||||
}
|
||||
if symbol == 'int' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u8' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u16' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u32' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
})
|
||||
}
|
||||
if symbol == 'u64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == 'f64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == '!' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'null'
|
||||
})
|
||||
}
|
||||
if symbol == 'i64' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
if symbol == 'byte' {
|
||||
return SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
return SchemaRef(Schema{
|
||||
typ: symbol
|
||||
})
|
||||
}
|
||||
}
|
||||
23
lib/schemas/jsonschema/codegen/generate_test.v
Normal file
23
lib/schemas/jsonschema/codegen/generate_test.v
Normal file
@@ -0,0 +1,23 @@
|
||||
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.Type{
|
||||
symbol: 'string'
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
schema := struct_to_schema(struct_)
|
||||
log.debug(schema.str())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module @module_name
|
||||
// @schema.title
|
||||
// @schema.description
|
||||
|
||||
@for name in structs
|
||||
@name
|
||||
@end
|
||||
@@ -0,0 +1,3 @@
|
||||
// @schema.description
|
||||
struct @schema.title {@properties
|
||||
}
|
||||
51
lib/schemas/jsonschema/decode.v
Normal file
51
lib/schemas/jsonschema/decode.v
Normal file
@@ -0,0 +1,51 @@
|
||||
module jsonschema
|
||||
|
||||
import json
|
||||
import x.json2 { Any }
|
||||
|
||||
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 := data_map['\$ref'] {
|
||||
return Reference{
|
||||
ref: ref.str()
|
||||
}
|
||||
}
|
||||
return decode(data_map.str())!
|
||||
}
|
||||
44
lib/schemas/jsonschema/decode_test.v
Normal file
44
lib/schemas/jsonschema/decode_test.v
Normal file
@@ -0,0 +1,44 @@
|
||||
module jsonschema
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
|
||||
const testdata = '${os.dir(@FILE)}/testdata'
|
||||
|
||||
struct Pet {
|
||||
name string
|
||||
}
|
||||
|
||||
fn test_decode() ! {
|
||||
mut pet_schema_file := pathlib.get_file(
|
||||
path: '${jsonschema.testdata}/pet.json'
|
||||
)!
|
||||
pet_schema_str := pet_schema_file.read()!
|
||||
pet_schema := decode(pet_schema_str)!
|
||||
assert pet_schema == Schema{
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
}
|
||||
}
|
||||
required: ['name']
|
||||
}
|
||||
}
|
||||
|
||||
fn test_decode_schemaref() ! {
|
||||
mut pet_schema_file := pathlib.get_file(
|
||||
path: '${jsonschema.testdata}/pet.json'
|
||||
)!
|
||||
pet_schema_str := pet_schema_file.read()!
|
||||
pet_schemaref := decode(pet_schema_str)!
|
||||
assert pet_schemaref == Schema{
|
||||
typ: 'object'
|
||||
properties: {
|
||||
'name': Schema{
|
||||
typ: 'string'
|
||||
}
|
||||
}
|
||||
required: ['name']
|
||||
}
|
||||
}
|
||||
36
lib/schemas/jsonschema/model.v
Normal file
36
lib/schemas/jsonschema/model.v
Normal file
@@ -0,0 +1,36 @@
|
||||
module jsonschema
|
||||
|
||||
type Items = SchemaRef | []SchemaRef
|
||||
|
||||
pub type SchemaRef = Reference | Schema
|
||||
|
||||
pub struct Reference {
|
||||
pub:
|
||||
ref string @[json: 'ref']
|
||||
}
|
||||
|
||||
pub type Number = int
|
||||
|
||||
// https://json-schema.org/draft-07/json-schema-release-notes.html
|
||||
pub struct Schema {
|
||||
pub mut:
|
||||
schema string @[json: 'schema'; omitempty]
|
||||
id string @[json: 'id'; omitempty]
|
||||
title string @[omitempty]
|
||||
description string @[omitempty]
|
||||
typ string @[json: 'type'; omitempty]
|
||||
properties map[string]SchemaRef @[omitempty]
|
||||
additional_properties SchemaRef @[json: 'additionalProperties'; omitempty]
|
||||
required []string @[omitempty]
|
||||
items Items @[omitempty]
|
||||
defs map[string]SchemaRef @[omitempty]
|
||||
one_of []SchemaRef @[json: 'oneOf'; omitempty]
|
||||
format string @[omitempty]
|
||||
// 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]
|
||||
}
|
||||
1
lib/schemas/jsonschema/reflection.v
Normal file
1
lib/schemas/jsonschema/reflection.v
Normal file
@@ -0,0 +1 @@
|
||||
module jsonschema
|
||||
11
lib/schemas/jsonschema/testdata/pet.json
vendored
Normal file
11
lib/schemas/jsonschema/testdata/pet.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
0
lib/schemas/openapi/README.md
Normal file
0
lib/schemas/openapi/README.md
Normal file
208
lib/schemas/openapi/decode.v
Normal file
208
lib/schemas/openapi/decode.v
Normal file
@@ -0,0 +1,208 @@
|
||||
module openapi
|
||||
|
||||
import json
|
||||
import x.json2 {Any}
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
|
||||
|
||||
|
||||
pub fn json_decode(data string) !OpenAPI {
|
||||
// Decode the raw JSON into a map to allow field-specific processing
|
||||
raw_map := json2.raw_decode(data)!.as_map()
|
||||
|
||||
// Decode the entire OpenAPI structure using standard JSON decoding
|
||||
mut spec := json.decode(OpenAPI, data)!
|
||||
|
||||
// Decode all schema and schemaref fields using `jsonschema.decode_schemaref`
|
||||
// 1. Process components.schemas
|
||||
if 'paths' in raw_map {
|
||||
mut paths := raw_map['paths'].as_map()
|
||||
for key, path in paths {
|
||||
spec.paths[key] = json_decode_path(spec.paths[key], path.as_map())!
|
||||
}
|
||||
}
|
||||
|
||||
if 'components' in raw_map {
|
||||
components_map := raw_map['components'].as_map()
|
||||
spec.components = json_decode_components(spec.components, components_map)!
|
||||
}
|
||||
|
||||
// Return the fully decoded OpenAPI structure
|
||||
return spec
|
||||
}
|
||||
|
||||
pub fn json_decode_components(components_ Components, components_map map[string]Any) !Components {
|
||||
mut components := components_
|
||||
|
||||
if 'schemas' in components_map {
|
||||
components.schemas = jsonschema.decode_schemaref_map(components_map['schemas'].as_map())!
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
pub fn json_decode_path(path_ PathItem, path_map map[string]Any) !PathItem {
|
||||
mut path := path_
|
||||
|
||||
for key in path_map.keys() {
|
||||
match key {
|
||||
'get' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.get = json_decode_operation(path.get, operation_map)!
|
||||
}
|
||||
'post' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.post = json_decode_operation(path.post, operation_map)!
|
||||
}
|
||||
'put' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.put = json_decode_operation(path.put, operation_map)!
|
||||
}
|
||||
'delete' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.delete = json_decode_operation(path.delete, operation_map)!
|
||||
}
|
||||
'options' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.options = json_decode_operation(path.options, operation_map)!
|
||||
}
|
||||
'head' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.head = json_decode_operation(path.head, operation_map)!
|
||||
}
|
||||
'patch' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.patch = json_decode_operation(path.patch, operation_map)!
|
||||
}
|
||||
'trace' {
|
||||
operation_map := path_map[key].as_map()
|
||||
path.trace = json_decode_operation(path.trace, operation_map)!
|
||||
}
|
||||
else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
pub fn json_decode_operation(operation_ Operation, operation_map map[string]Any) !Operation {
|
||||
mut operation := operation_
|
||||
|
||||
if 'requestBody' in operation_map {
|
||||
request_body_any := operation_map['requestBody']
|
||||
request_body_map := request_body_any.as_map()
|
||||
|
||||
if 'content' in request_body_map {
|
||||
mut request_body := json.decode(RequestBody, request_body_any.str())!
|
||||
// mut request_body := operation.request_body as RequestBody
|
||||
mut content := request_body.content.clone()
|
||||
content_map := request_body_map['content'].as_map()
|
||||
request_body.content = json_decode_content(content, content_map)!
|
||||
operation.request_body = request_body
|
||||
}
|
||||
}
|
||||
|
||||
if 'responses' in operation_map {
|
||||
responses_map := operation_map['responses'].as_map()
|
||||
for key, response_any in responses_map {
|
||||
response_map := response_any.as_map()
|
||||
if 'content' in response_map {
|
||||
mut response := operation.responses[key]
|
||||
mut content := response.content.clone()
|
||||
content_map := response_map['content'].as_map()
|
||||
response.content = json_decode_content(content, content_map)!
|
||||
operation.responses[key] = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 'parameters' in operation_map {
|
||||
parameters_arr := operation_map['parameters'].arr()
|
||||
mut parameters := []Parameter{}
|
||||
for i, parameter_any in parameters_arr {
|
||||
parameter_map := parameter_any.as_map()
|
||||
if 'schema' in parameter_map {
|
||||
mut parameter := operation.parameters[i]
|
||||
parameter.schema = jsonschema.decode_schemaref(parameter_map['schema'].as_map())!
|
||||
parameters << parameter
|
||||
} else {
|
||||
parameters << operation.parameters[i]
|
||||
}
|
||||
}
|
||||
operation.parameters = parameters
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
fn json_decode_content(content_ map[string]MediaType, content_map map[string]Any) !map[string]MediaType {
|
||||
mut content := content_.clone()
|
||||
for key, item in content_map {
|
||||
media_type_map := item.as_map()
|
||||
schema_any := media_type_map['schema']
|
||||
mut media_type := content[key]
|
||||
media_type.schema = jsonschema.decode_schemaref(schema_any.as_map())!
|
||||
content[key] = media_type
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
// pub fn json_decode(data string) !OpenAPI {
|
||||
// // Decode the raw JSON into the OpenAPI structure
|
||||
// mut spec := json.decode(OpenAPI, data)!
|
||||
|
||||
// data_map := json2.raw_decode(data)!.as_map()
|
||||
|
||||
// // Recursively process the structure to decode SchemaRef and Schema fields
|
||||
// spec = decode_recursive(spec, data_map)!
|
||||
|
||||
// return spec
|
||||
// }
|
||||
|
||||
// fn decode_recursive[T](obj T, data_map map[string]Any) !T {
|
||||
// // data_map := json2.raw_decode(data)!.as_map()
|
||||
|
||||
// $for field in T.fields {
|
||||
// $if field.is_array {
|
||||
// val := obj.$(field.name)
|
||||
// field_array := data_map[field.name].arr()
|
||||
// // mut data_fmt := data.replace(action_str, '')
|
||||
// // data_fmt = data.replace('define.${obj_name}', 'define')
|
||||
// arr := decode_array(val, field_array)!
|
||||
// obj.$(field.name) = arr
|
||||
// }
|
||||
|
||||
|
||||
// println('field ${field.name} ${typeof(field.typ)}')
|
||||
// field_map := data_map[field.name].as_map()
|
||||
// // Check if the field is of type Schema or SchemaRef
|
||||
// $if field.typ is SchemaRef {
|
||||
// obj.$(field.name) = jsonschema.decode_schemaref(field_map)!
|
||||
// } $else $if field.typ is map[string]SchemaRef {
|
||||
// // Check if the field is a map with SchemaRef or Schema as values
|
||||
// obj.$(field.name) = jsonschema.decode_schemaref_map(field_map)!
|
||||
// } $else {
|
||||
// val := obj.$(field.name)
|
||||
// obj.$(field.name) = decode_recursive(val, field_map)!
|
||||
// }
|
||||
// }
|
||||
|
||||
// return obj
|
||||
// }
|
||||
|
||||
// pub fn decode_array[T](_ []T, data_arr []Any) ![]T {
|
||||
// mut arr := []T{}
|
||||
// for data in data_arr {
|
||||
// value := T{}
|
||||
// $if T is $struct {
|
||||
// arr << decode_recursive(value, data.as_map())!
|
||||
// } $else {
|
||||
// arr << value
|
||||
// }
|
||||
// }
|
||||
// return arr
|
||||
// }
|
||||
|
||||
fn (o OpenAPI) json_encode() string {
|
||||
return json.encode(o).replace('ref', '\$ref')
|
||||
}
|
||||
395
lib/schemas/openapi/decode_test.v
Normal file
395
lib/schemas/openapi/decode_test.v
Normal file
@@ -0,0 +1,395 @@
|
||||
module openapi
|
||||
|
||||
import os
|
||||
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef}
|
||||
|
||||
const spec_path = '${os.dir(@FILE)}/testdata/openapi.json'
|
||||
const spec_json = os.read_file(spec_path) or {panic(err)}
|
||||
|
||||
const spec = openapi.OpenAPI{
|
||||
openapi: '3.0.3'
|
||||
info: openapi.Info{
|
||||
title: 'Pet Store API'
|
||||
description: 'A sample API for a pet store'
|
||||
version: '1.0.0'
|
||||
}
|
||||
servers: [
|
||||
ServerSpec{
|
||||
url: 'https://api.petstore.example.com/v1'
|
||||
description: 'Production server'
|
||||
},
|
||||
ServerSpec{
|
||||
url: 'https://staging.petstore.example.com/v1'
|
||||
description: 'Staging server'
|
||||
}
|
||||
]
|
||||
paths: {
|
||||
'/pets': openapi.PathItem{
|
||||
get: openapi.Operation{
|
||||
summary: 'List all pets'
|
||||
operation_id: 'listPets'
|
||||
parameters: [
|
||||
openapi.Parameter{
|
||||
name: 'limit'
|
||||
in_: 'query'
|
||||
description: 'Maximum number of pets to return'
|
||||
required: false
|
||||
schema: Schema{
|
||||
typ: 'integer'
|
||||
format: 'int32'
|
||||
}
|
||||
}
|
||||
]
|
||||
responses: {
|
||||
'200': ResponseSpec{
|
||||
description: 'A paginated list of pets'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/Pets'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'400': ResponseSpec{
|
||||
description: 'Invalid request'
|
||||
}
|
||||
}
|
||||
}
|
||||
post: openapi.Operation{
|
||||
summary: 'Create a new pet'
|
||||
operation_id: 'createPet'
|
||||
request_body: openapi.RequestBody{
|
||||
required: true
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/NewPet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
'201': ResponseSpec{
|
||||
description: 'Pet created'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/Pet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'400': ResponseSpec{
|
||||
description: 'Invalid input'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/pets/{petId}': openapi.PathItem{
|
||||
get: openapi.Operation{
|
||||
summary: 'Get a pet by ID'
|
||||
operation_id: 'getPet'
|
||||
parameters: [
|
||||
openapi.Parameter{
|
||||
name: 'petId'
|
||||
in_: 'path'
|
||||
description: 'ID of the pet to retrieve'
|
||||
required: true
|
||||
schema: Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
}
|
||||
]
|
||||
responses: {
|
||||
'200': ResponseSpec{
|
||||
description: 'A pet'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/Pet'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'404': ResponseSpec{
|
||||
description: 'Pet not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
delete: openapi.Operation{
|
||||
summary: 'Delete a pet by ID'
|
||||
operation_id: 'deletePet'
|
||||
parameters: [
|
||||
openapi.Parameter{
|
||||
name: 'petId'
|
||||
in_: 'path'
|
||||
description: 'ID of the pet to delete'
|
||||
required: true
|
||||
schema: Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
}
|
||||
]
|
||||
responses: {
|
||||
'204': ResponseSpec{
|
||||
description: 'Pet deleted'
|
||||
}
|
||||
'404': ResponseSpec{
|
||||
description: 'Pet not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/orders': openapi.PathItem{
|
||||
get: openapi.Operation{
|
||||
summary: 'List all orders'
|
||||
operation_id: 'listOrders'
|
||||
responses: {
|
||||
'200': ResponseSpec{
|
||||
description: 'A list of orders'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Schema{
|
||||
typ: 'array'
|
||||
items: SchemaRef(Reference{
|
||||
ref: '#/components/schemas/Order'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/orders/{orderId}': openapi.PathItem{
|
||||
get: openapi.Operation{
|
||||
summary: 'Get an order by ID'
|
||||
operation_id: 'getOrder'
|
||||
parameters: [
|
||||
openapi.Parameter{
|
||||
name: 'orderId'
|
||||
in_: 'path'
|
||||
description: 'ID of the order to retrieve'
|
||||
required: true
|
||||
schema: Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
}
|
||||
]
|
||||
responses: {
|
||||
'200': ResponseSpec{
|
||||
description: 'An order'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/Order'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'404': ResponseSpec{
|
||||
description: 'Order not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
delete: openapi.Operation{
|
||||
summary: 'Delete an order by ID'
|
||||
operation_id: 'deleteOrder'
|
||||
parameters: [
|
||||
openapi.Parameter{
|
||||
name: 'orderId'
|
||||
in_: 'path'
|
||||
description: 'ID of the order to delete'
|
||||
required: true
|
||||
schema: Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
}
|
||||
}
|
||||
]
|
||||
responses: {
|
||||
'204': ResponseSpec{
|
||||
description: 'Order deleted'
|
||||
}
|
||||
'404': ResponseSpec{
|
||||
description: 'Order not found'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/users': openapi.PathItem{
|
||||
post: openapi.Operation{
|
||||
summary: 'Create a user'
|
||||
operation_id: 'createUser'
|
||||
request_body: openapi.RequestBody{
|
||||
required: true
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/NewUser'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
'201': ResponseSpec{
|
||||
description: 'User created'
|
||||
content: {
|
||||
'application/json': openapi.MediaType{
|
||||
schema: Reference{
|
||||
ref: '#/components/schemas/User'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
components: openapi.Components{
|
||||
schemas: {
|
||||
'Pet': SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
required: ['id', 'name']
|
||||
properties: {
|
||||
'id': SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
})
|
||||
'name': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'tag': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
})
|
||||
'NewPet': SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
required: ['name']
|
||||
properties: {
|
||||
'name': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'tag': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
})
|
||||
'Pets': SchemaRef(Schema{
|
||||
typ: 'array'
|
||||
items: SchemaRef(Reference{
|
||||
ref: '#/components/schemas/Pet'
|
||||
})
|
||||
})
|
||||
'Order': SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
required: ['id', 'petId', 'quantity', 'shipDate']
|
||||
properties: {
|
||||
'id': SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
})
|
||||
'petId': SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
})
|
||||
'quantity': SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
format: 'int32'
|
||||
})
|
||||
'shipDate': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
format: 'date-time'
|
||||
})
|
||||
'status': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
enum_: ['placed', 'approved', 'delivered']
|
||||
})
|
||||
'complete': SchemaRef(Schema{
|
||||
typ: 'boolean'
|
||||
})
|
||||
}
|
||||
})
|
||||
'User': SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
required: ['id', 'username']
|
||||
properties: {
|
||||
'id': SchemaRef(Schema{
|
||||
typ: 'integer'
|
||||
format: 'int64'
|
||||
})
|
||||
'username': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'email': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'phone': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
})
|
||||
'NewUser': SchemaRef(Schema{
|
||||
typ: 'object'
|
||||
required: ['username']
|
||||
properties: {
|
||||
'username': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'email': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
'phone': SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn testsuite_begin() {}
|
||||
|
||||
fn test_decode() {
|
||||
decoded := json_decode(spec_json)!
|
||||
|
||||
assert decoded.openapi == spec.openapi
|
||||
assert decoded.info == spec.info
|
||||
assert decoded.servers == spec.servers
|
||||
for key, path in decoded.paths {
|
||||
assert path.ref == spec.paths[key].ref, 'Paths ${key} dont match.'
|
||||
assert path.summary == spec.paths[key].summary, 'Paths ${key} dont match.'
|
||||
assert path.description == spec.paths[key].description, 'Paths ${key} dont match.'
|
||||
match_operations(path.get, spec.paths[key].get)
|
||||
match_operations(path.put, spec.paths[key].put)
|
||||
match_operations(path.post, spec.paths[key].post)
|
||||
match_operations(path.delete, spec.paths[key].delete)
|
||||
}
|
||||
assert decoded.webhooks == spec.webhooks
|
||||
for key, schema in decoded.components.schemas {
|
||||
assert schema == spec.components.schemas[key], 'Schemas ${key} dont match.'
|
||||
}
|
||||
assert decoded.components == spec.components
|
||||
assert decoded.security == spec.security
|
||||
}
|
||||
|
||||
fn match_operations(a Operation, b Operation) {
|
||||
println(a.responses['200'].content['application/json'].schema)
|
||||
assert a.tags == b.tags, 'Tags do not match.'
|
||||
assert a.summary == b.summary, 'Summary does not match.'
|
||||
assert a.description == b.description, 'Description does not match.'
|
||||
assert a.external_docs == b.external_docs, 'External documentation does not match.'
|
||||
assert a.operation_id == b.operation_id, 'Operation ID does not match.'
|
||||
assert a.parameters == b.parameters, 'Parameters do not match.'
|
||||
assert a.request_body == b.request_body, 'Request body does not match.'
|
||||
assert a.responses == b.responses, 'Responses do not match.'
|
||||
assert a.callbacks == b.callbacks, 'Callbacks do not match.'
|
||||
assert a.deprecated == b.deprecated, 'Deprecated flag does not match.'
|
||||
assert a.security == b.security, 'Security requirements do not match.'
|
||||
assert a.servers == b.servers, 'Servers do not match.'
|
||||
}
|
||||
26
lib/schemas/openapi/factory.v
Normal file
26
lib/schemas/openapi/factory.v
Normal file
@@ -0,0 +1,26 @@
|
||||
module openapi
|
||||
|
||||
import os
|
||||
|
||||
@[params]
|
||||
pub struct Params {
|
||||
pub:
|
||||
path string // path to openrpc.json file
|
||||
text string // content of openrpc specification text
|
||||
}
|
||||
|
||||
pub fn new(params Params) !OpenAPI {
|
||||
if params.path == '' && params.text == '' {
|
||||
return error('Either provide path or text')
|
||||
}
|
||||
|
||||
if params.text != '' && params.path != '' {
|
||||
return error('Either provide path or text')
|
||||
}
|
||||
|
||||
text := if params.path != '' {
|
||||
os.read_file(params.path)!
|
||||
} else { params.text }
|
||||
|
||||
return json_decode(text)!
|
||||
}
|
||||
53
lib/schemas/openapi/handler.v
Normal file
53
lib/schemas/openapi/handler.v
Normal file
@@ -0,0 +1,53 @@
|
||||
module openapi
|
||||
|
||||
import net.http {CommonHeader}
|
||||
import x.json2 {Any}
|
||||
|
||||
pub struct Request {
|
||||
pub:
|
||||
path string // The requested path
|
||||
method string // HTTP method (e.g., GET, POST)
|
||||
key string
|
||||
body string // Request body
|
||||
operation Operation
|
||||
arguments map[string]Any
|
||||
parameters map[string]string
|
||||
header http.Header @[omitempty; str: skip; json: '-']// Request headers
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub mut:
|
||||
status http.Status // HTTP status
|
||||
body string // Response body
|
||||
header http.Header @[omitempty; str: skip; json:'-']// Response headers
|
||||
}
|
||||
|
||||
pub interface IHandler {
|
||||
mut:
|
||||
handle(Request) !Response
|
||||
}
|
||||
|
||||
pub struct Handler {
|
||||
pub:
|
||||
routes map[string]fn (Request) !Response // Map of route handlers
|
||||
}
|
||||
|
||||
// Handle a request and return a response
|
||||
pub fn (handler Handler) handle(request Request) !Response {
|
||||
// Match the route based on the request path
|
||||
if route_handler := handler.routes[request.path] {
|
||||
// Call the corresponding route handler
|
||||
return route_handler(request)
|
||||
}
|
||||
|
||||
// Return 404 if no route matches
|
||||
return Response{
|
||||
status: .not_found
|
||||
body: 'Not Found'
|
||||
header: http.new_header(
|
||||
key: CommonHeader.content_type,
|
||||
value: 'text/plain'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
255
lib/schemas/openapi/model.v
Normal file
255
lib/schemas/openapi/model.v
Normal file
@@ -0,0 +1,255 @@
|
||||
module openapi
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonschema {Schema, Reference, SchemaRef}
|
||||
|
||||
// todo: report bug: when comps is optional, doesnt work
|
||||
pub struct OpenAPI {
|
||||
pub mut:
|
||||
openapi string @[required] // This string MUST be the version number of the OpenAPI Specification that the OpenAPI document uses. The openapi field SHOULD be used by tooling to interpret the OpenAPI document. This is not related to the API info.version string.
|
||||
info Info @[required] // Provides metadata about the API. The metadata MAY be used by tooling as required.
|
||||
json_schema_dialect string // The default value for the $schema keyword within Schema Objects contained within this OAS document. This MUST be in the form of a URI.
|
||||
servers []ServerSpec // An array of ServerSpec Objects, which provide connectivity information to a target server. If the servers property is not provided, or is an empty array, the default value would be a ServerSpec Object with a url value of /.
|
||||
paths map[string]PathItem // The available paths and operations for the API.
|
||||
webhooks map[string]PathRef // The incoming webhooks that MAY be received as part of this API and that the API consumer MAY choose to implement. Closely related to the callbacks feature, this section describes requests initiated other than by an API call, for example by an out of band registration. The key name is a unique string to refer to each webhook, while the (optionally referenced) Path Item Object describes a request that may be initiated by the API provider and the expected responses. An example is available.
|
||||
components Components // An element to hold various schemas for the document.
|
||||
security []SecurityRequirement // A declaration of which security mechanisms can be used across the API. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. Individual operations can override this definition. To make security optional, an empty security requirement ({}) can be included in the array.
|
||||
tags []Tag // A list of tags used by the document with additional metadata. The order of the tags can be used to reflect on their order by the parsing tools. Not all tags that are used by the Operation Object must be declared. The tags that are not declared MAY be organized randomly or based on the tools’ logic. Each tag name in the list MUST be unique.
|
||||
external_docs ExternalDocumentation // Additional external documentation.
|
||||
}
|
||||
|
||||
pub fn (spec OpenAPI) plain() string {
|
||||
return '${spec}'.split('\n').filter(!it.contains('Option(none)')).join('\n')
|
||||
}
|
||||
|
||||
// ```
|
||||
// {
|
||||
// "title": "Sample Pet Store App",
|
||||
// "summary": "A pet store manager.",
|
||||
// "description": "This is a sample server for a pet store.",
|
||||
// "termsOfService": "https://example.com/terms/",
|
||||
// "contact": {
|
||||
// "name": "API Support",
|
||||
// "url": "https://www.example.com/support",
|
||||
// "email": "support@example.com"
|
||||
// },
|
||||
// "license": {
|
||||
// "name": "Apache 2.0",
|
||||
// "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
// },
|
||||
// "version": "1.0.1"
|
||||
// }
|
||||
// ```
|
||||
// The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.
|
||||
pub struct Info {
|
||||
pub mut:
|
||||
title string @[required] // The title of the API
|
||||
summary string // A short summary of the API.
|
||||
description string // A description of the API. CommonMark syntax MAY be used for rich text representation.
|
||||
terms_of_service string // A URL to the Terms of Service for the API. This MUST be in the form of a URL.
|
||||
contact Contact // The contact information for the exposed API.
|
||||
license License // The license information for the exposed API.
|
||||
version string @[required] // The version of the OpenAPI document (which is distinct from the OpenAPI Specification version or the API implementation version).
|
||||
}
|
||||
|
||||
// ```{
|
||||
// "name": "API Support",
|
||||
// "url": "https://www.example.com/support",
|
||||
// "email": "support@example.com"
|
||||
// }```
|
||||
// Contact information for the exposed API.
|
||||
pub struct Contact {
|
||||
pub:
|
||||
name string // The identifying name of the contact person/organization.
|
||||
url string // The URL pointing to the contact information. This MUST be in the form of a URL.
|
||||
email string // The email address of the contact person/organization. This MUST be in the form of an email address.
|
||||
}
|
||||
|
||||
// ```{
|
||||
// "name": "Apache 2.0",
|
||||
// "identifier": "Apache-2.0"
|
||||
// }```
|
||||
// License information for the exposed API.
|
||||
pub struct License {
|
||||
pub:
|
||||
name string // The license name used for the API.
|
||||
identifier string // An SPDX license expression for the API. The identifier field is mutually exclusive of the url field.
|
||||
url string // A URL to the license used for the API. This MUST be in the form of a URL. The url field is mutually exclusive of the identifier field.
|
||||
}
|
||||
|
||||
|
||||
// ```{
|
||||
// "url": "https://development.gigantic-server.com/v1",
|
||||
// "description": "Development server"
|
||||
// }```
|
||||
pub struct ServerSpec {
|
||||
pub:
|
||||
url string @[required] // A URL to the target host. This URL supports ServerSpec Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenAPI document is being served. Variable substitutions will be made when a variable is named in {brackets}.
|
||||
description string // An optional string describing the host designated by the URL. CommonMark syntax MAY be used for rich text representation.
|
||||
variables map[string]ServerVariable // A map between a variable name and its value. The value is used for substitution in the server’s URL template.
|
||||
}
|
||||
|
||||
// An object representing a ServerSpec Variable for server URL template substitution.
|
||||
pub struct ServerVariable {
|
||||
pub:
|
||||
enum_ []string @[json: 'enum'] // An enumeration of string values to be used if the substitution options are from a limited set.
|
||||
default_ string @[json: 'default'; required] // The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Object’s treatment of default values, because in those cases parameter values are optional.
|
||||
description string @[omitempty] // An optional description for the server variable. GitHub Flavored Markdown syntax MAY be used for rich text representation.
|
||||
}
|
||||
|
||||
pub struct Path {}
|
||||
|
||||
// pub struct Reference {
|
||||
// ref string @[json: 'ref'] // The reference identifier. This MUST be in the form of a URI.
|
||||
// summary string // A short summary which by default SHOULD override that of the referenced component. If the referenced object-type does not allow a summary field, then this field has no effect.
|
||||
// description string // A description which by default SHOULD override that of the referenced component. CommonMark syntax MAY be used for rich text representation. If the referenced object-type does not allow a description field, then this field has no effect.
|
||||
// }
|
||||
|
||||
pub type PathRef = Path | Reference
|
||||
|
||||
pub struct Components {
|
||||
pub mut:
|
||||
schemas map[string]SchemaRef // An object to hold reusable Schema Objects.
|
||||
responses map[string]ResponseRef // An object to hold reusable ResponseSpec Objects.
|
||||
parameters map[string]ParameterRef // An object to hold reusable Parameter Objects.
|
||||
examples map[string]ExampleRef // An object to hold reusable Example Objects.
|
||||
request_bodies map[string]RequestBodyRef // An object to hold reusable Request Body Objects.
|
||||
headers map[string]HeaderRef // An object to hold reusable Header Objects.
|
||||
security_schemes map[string]SecuritySchemeRef // An object to hold reusable Security Scheme Objects.
|
||||
links map[string]LinkRef // An object to hold reusable Link Objects.
|
||||
callbacks map[string]CallbackRef // An object to hold reusable Callback Objects.
|
||||
path_items map[string]PathItemRef // An object to hold reusable Path Item Object.
|
||||
}
|
||||
|
||||
|
||||
type Items = SchemaRef | []SchemaRef
|
||||
|
||||
// type Number = int
|
||||
|
||||
// pub struct Request {
|
||||
// description string // A brief description of the request body. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
|
||||
// content map[string]MediaType // The content of the request body. The key is a media type (e.g., `application/json`) and the value describes it.
|
||||
// required bool = false // Determines if the request body is required in the request. Defaults to false.
|
||||
// }
|
||||
|
||||
pub type ResponseRef = Reference | ResponseSpec
|
||||
pub type ParameterRef = Parameter | Reference
|
||||
pub type SecuritySchemeRef = Reference | SecurityScheme
|
||||
pub type ExampleRef = Example | Reference
|
||||
pub type RequestBodyRef = Reference | RequestBody
|
||||
pub type HeaderRef = Header | Reference
|
||||
pub type LinkRef = Link | Reference
|
||||
pub type CallbackRef = Callback | Reference
|
||||
pub type PathItemRef = PathItem | Reference
|
||||
// type RequestRef = Reference | Request
|
||||
|
||||
pub struct PathItem {
|
||||
pub mut:
|
||||
ref string @[omitempty] // Allows for a referenced definition of this path item. The referenced structure MUST be in the form of a Path Item Object. In case a Path Item Object field appears both in the defined object and the referenced object, the behavior is undefined. See the rules for resolving Relative References.
|
||||
summary string @[omitempty] // An optional, string summary, intended to apply to all operations in this path.
|
||||
description string @[omitempty] // An optional, string description, intended to apply to all operations in this path. CommonMark syntax MAY be used for rich text representation.
|
||||
get Operation @[omitempty] // A definition of a GET operation on this path.
|
||||
put Operation @[omitempty] // A definition of a PUT operation on this path.
|
||||
post Operation @[omitempty] // A definition of a POST operation on this path.
|
||||
delete Operation @[omitempty] // A definition of a DELETE operation on this path.
|
||||
options Operation @[omitempty] // A definition of a OPTIONS operation on this path.
|
||||
head Operation @[omitempty] // A definition of a HEAD operation on this path.
|
||||
patch Operation @[omitempty] // A definition of a PATCH operation on this path.
|
||||
trace Operation @[omitempty] // A definition of a TRACE operation on this path.
|
||||
servers []ServerSpec @[omitempty] // An alternative server array to service all operations in this path.
|
||||
parameters []Parameter @[omitempty]// A list of parameters that are applicable for all the operations described under this path. These parameters can be overridden at the operation level, but cannot be removed there. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object’s components/parameters.
|
||||
}
|
||||
|
||||
pub struct Operation {
|
||||
pub mut:
|
||||
tags []string @[omitempty]// A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier.
|
||||
summary string @[omitempty]// A short summary of what the operation does.
|
||||
description string @[omitempty] // A verbose explanation of the operation behavior. CommonMark syntax MAY be used for rich text representation.
|
||||
external_docs ExternalDocumentation @[json: 'externalDocs'; omitempty] // Additional external documentation for this operation.
|
||||
operation_id string @[json: 'operationId'; omitempty] // Unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.
|
||||
parameters []Parameter @[omitempty]// A list of parameters that are applicable for this operation. If a parameter is already defined at the Path Item, the new definition will override it but can never remove it. The list MUST NOT include duplicated parameters. A unique parameter is defined by a combination of a name and location. The list can use the Reference Object to link to parameters that are defined at the OpenAPI Object’s components/parameters.
|
||||
request_body RequestBodyRef @[json: 'requestBody'; omitempty] // The request body applicable for this operation. The requestBody is fully supported in HTTP methods where the HTTP 1.1 specification [RFC7231] has explicitly defined semantics for request bodies. In other cases where the HTTP spec is vague (such as GET, HEAD and DELETE), requestBody is permitted but does not have well-defined semantics and SHOULD be avoided if possible.
|
||||
responses map[string]ResponseSpec @[omitempty] // The list of possible responses as they are returned from executing this operation.
|
||||
callbacks map[string]CallbackRef @[omitempty] // A map of possible out-of band callbacks related to the parent operation. The key is a unique identifier for the Callback Object. Each value in the map is a Callback Object that describes a request that may be initiated by the API provider and the expected responses.
|
||||
deprecated bool @[omitempty]// Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is false.
|
||||
security []SecurityRequirement @[omitempty] // A declaration of which security mechanisms can be used for this operation. The list of values includes alternative security requirement objects that can be used. Only one of the security requirement objects need to be satisfied to authorize a request. To make security optional, an empty security requirement ({}) can be included in the array. This definition overrides any declared top-level security. To remove a top-level security declaration, an empty array can be used.
|
||||
servers []ServerSpec @[omitempty]// An alternative server array to service this operation. If an alternative server object is specified at the Path Item Object or Root level, it will be overridden by this value.
|
||||
}
|
||||
|
||||
// TODO: currently using map[string]ResponseSpec
|
||||
pub struct Responses {
|
||||
pub:
|
||||
default ResponseRef
|
||||
}
|
||||
|
||||
pub struct Callback {
|
||||
pub:
|
||||
callback string
|
||||
}
|
||||
|
||||
pub struct Link {
|
||||
pub:
|
||||
link string
|
||||
}
|
||||
|
||||
pub struct Header {
|
||||
pub:
|
||||
header string
|
||||
}
|
||||
|
||||
pub struct ResponseSpec {
|
||||
pub mut:
|
||||
description string @[required] // A description of the response. CommonMark syntax MAY be used for rich text representation.
|
||||
headers map[string]HeaderRef // Maps a header name to its definition. [RFC7230] states header names are case insensitive. If a response header is defined with the name "Content-Type", it SHALL be ignored.
|
||||
content map[string]MediaType // A map containing descriptions of potential response payloads. The key is a media type or media type range and the value describes it. For responses that match multiple keys, only the most specific key is applicable. e.g. text/plain overrides text/*
|
||||
links map[string]LinkRef // A map of operations links that can be followed from the response. The key of the map is a short name for the link, following the naming constraints of the names for Component Objects.
|
||||
}
|
||||
|
||||
// TODO: media type example any field
|
||||
pub struct MediaType {
|
||||
pub mut:
|
||||
schema SchemaRef // The schema defining the content of the request, response, or parameter.
|
||||
example string // Example of the media type. The example object SHOULD be in the correct format as specified by the media type. The example field is mutually exclusive of the examples field. Furthermore, if referencing a schema which contains an example, the example value SHALL override the example provided by the schema.
|
||||
examples map[string]ExampleRef // Examples of the media type. Each example object SHOULD match the media type and specified schema if present. The examples field is mutually exclusive of the example field. Furthermore, if referencing a schema which contains an example, the examples value SHALL override the example provided by the schema.
|
||||
encoding map[string]Encoding // A map between a property name and its encoding information. The key, being the property name, MUST exist in the schema as a property. The encoding object SHALL only apply to requestBody objects when the media type is multipart or application/x-www-form-urlencoded.
|
||||
}
|
||||
|
||||
pub struct Encoding {
|
||||
pub:
|
||||
content_type string @[json: 'contentType'] // The Content-Type for encoding a specific property. Default value depends on the property type: for object - application/json; for array – the default is defined based on the inner type; for all other cases the default is application/octet-stream. The value can be a specific media type (e.g. application/json), a wildcard media type (e.g. image/*), or a comma-separated list of the two types.
|
||||
headers map[string]HeaderRef // A map allowing additional information to be provided as headers, for example Content-Disposition. Content-Type is described separately and SHALL be ignored in this section. This property SHALL be ignored if the request body media type is not a multipart.
|
||||
style string // Describes how a specific property value will be serialized depending on its type. See Parameter Object for details on the style property. The behavior follows the same values as query parameters, including default values. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored.
|
||||
explode bool // When this is true, property values of type array or object generate separate parameters for each value of the array, or key-value-pair of the map. For other types of properties this property has no effect. When style is form, the default value is true. For all other styles, the default value is false. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored.
|
||||
allow_reserved bool // Determines whether the parameter value SHOULD allow reserved characters, as defined by [RFC3986] :/?#[]@!$&'()*+,;= to be included without percent-encoding. The default value is false. This property SHALL be ignored if the request body media type is not application/x-www-form-urlencoded or multipart/form-data. If a value is explicitly defined, then the value of contentType (implicit or explicit) SHALL be ignored.
|
||||
}
|
||||
|
||||
pub struct Parameter {
|
||||
pub mut:
|
||||
name string @[required] // The name of the parameter. Parameter names are case sensitive.
|
||||
in_ string @[json: 'in'; required] // The location of the parameter. Possible values are "query", "header", "path" or "cookie".
|
||||
description string @[omitempty]// A brief description of the parameter. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
|
||||
required bool @[omitempty]// Determines whether this parameter is mandatory. If the parameter location is "path", this property is REQUIRED and its value MUST be true. Otherwise, the property MAY be included and its default value is false.
|
||||
deprecated bool @[omitempty]// Specifies that a parameter is deprecated and SHOULD be transitioned out of usage. Default value is false.
|
||||
allow_empty_value bool @[json: 'allowEmptyValue'] // Sets the ability to pass empty-valued parameters. This is valid only for query parameters and allows sending a parameter with an empty value. Default value is false. If style is used, and if behavior is n/a (cannot be serialized), the value of allowEmptyValue SHALL be ignored. Use of this property is NOT RECOMMENDED, as it is likely to be removed in a later revision.
|
||||
schema SchemaRef // The schema defining the type used for the parameter.
|
||||
}
|
||||
|
||||
pub struct Example {
|
||||
example string
|
||||
}
|
||||
|
||||
pub struct SecurityScheme {}
|
||||
|
||||
pub struct RequestBody {
|
||||
pub mut:
|
||||
description string // A brief description of the request body. This could contain examples of use. CommonMark syntax MAY be used for rich text representation.
|
||||
content map[string]MediaType // The content of the request body. The key is a media type (e.g., `application/json`) and the value describes it.
|
||||
required bool // Determines if the request body is required in the request. Defaults to false.
|
||||
}
|
||||
|
||||
pub struct SecurityRequirement {}
|
||||
|
||||
pub struct Tag {}
|
||||
|
||||
pub struct ExternalDocumentation {
|
||||
external string
|
||||
}
|
||||
214
lib/schemas/openapi/server.v
Normal file
214
lib/schemas/openapi/server.v
Normal file
@@ -0,0 +1,214 @@
|
||||
module openapi
|
||||
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.jsonschema {Schema}
|
||||
import x.json2 {Any}
|
||||
import net.http
|
||||
|
||||
pub struct Controller {
|
||||
pub:
|
||||
specification OpenAPI
|
||||
pub mut:
|
||||
handler IHandler
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// Matches a request path against OpenAPI path templates in the parsed structs
|
||||
// Returns the matching path key and corresponding PathItem if found
|
||||
fn match_path(req_path string, spec OpenAPI) !PathItem {
|
||||
// Iterate through all paths in the OpenAPI specification
|
||||
for template, path_item in spec.paths {
|
||||
if is_path_match(req_path, template) {
|
||||
// Return the matching path template and its PathItem
|
||||
return path_item
|
||||
}
|
||||
}
|
||||
// If no match is found, return an error
|
||||
return error('Path not found')
|
||||
}
|
||||
|
||||
// Checks if a request path matches a given OpenAPI path template
|
||||
// Allows for dynamic path segments like `{petId}` in templates
|
||||
fn is_path_match(req_path string, template string) bool {
|
||||
// Split the request path and template into segments
|
||||
req_segments := req_path.split('/')
|
||||
template_segments := template.split('/')
|
||||
|
||||
// If the number of segments doesn't match, the paths can't match
|
||||
if req_segments.len != template_segments.len {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare each segment in the template and request path
|
||||
for i, segment in template_segments {
|
||||
// If the segment is not dynamic (doesn't start with `{`), ensure it matches exactly
|
||||
if !segment.starts_with('{') && segment != req_segments[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// If all segments match or dynamic segments are valid, return true
|
||||
return true
|
||||
}
|
||||
|
||||
@['/:path...'; get; post; put; delete; patch]
|
||||
pub fn (mut server Controller) index(mut ctx Context, path string) veb.Result {
|
||||
println('Requested path: $path')
|
||||
|
||||
// Extract the HTTP method
|
||||
method := ctx.req.method.str().to_lower()
|
||||
|
||||
// Matches the request path against the OpenAPI specification and retrieves the corresponding PathItem
|
||||
path_item := match_path(path, server.specification) or {
|
||||
// Return a 404 error if no matching path is found
|
||||
return ctx.not_found()
|
||||
}
|
||||
|
||||
|
||||
// // Check if the path exists in the OpenAPI specification
|
||||
// path_item := server.specification.paths[path] or {
|
||||
// // Return a 404 error if the path is not defined
|
||||
// return ctx.not_found()
|
||||
// }
|
||||
|
||||
// Match the HTTP method with the OpenAPI specification
|
||||
operation := match method {
|
||||
'get' { path_item.get }
|
||||
'post' { path_item.post }
|
||||
'put' { path_item.put }
|
||||
'delete' { path_item.delete }
|
||||
'patch' { path_item.patch }
|
||||
else {
|
||||
// Return 405 Method Not Allowed if the method is not supported
|
||||
return ctx.method_not_allowed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mut arg_map := map[string]Any
|
||||
path_arg := path.all_after_last('/')
|
||||
// the OpenAPI Parameter specification belonging to the path argument
|
||||
arg_params := operation.parameters.filter(it.in_ == 'path')
|
||||
if arg_params.len > 1 {
|
||||
// TODO: use path template to support multiple arguments (right now just last arg supported)
|
||||
panic('implement')
|
||||
} else if arg_params.len == 1 {
|
||||
arg_map[arg_params[0].name] = arg_params[0].typed(path_arg)
|
||||
}
|
||||
|
||||
mut parameters := ctx.query.clone()
|
||||
// Build the Request object
|
||||
request := Request{
|
||||
path: path
|
||||
operation: operation
|
||||
method: method
|
||||
arguments: arg_map
|
||||
parameters: parameters
|
||||
body: ctx.req.data
|
||||
header: ctx.req.header
|
||||
}
|
||||
|
||||
// Use the handler to process the request
|
||||
response := server.handler.handle(request) or {
|
||||
// Use OpenAPI spec to determine the response status for the error
|
||||
return ctx.handle_error(operation.responses, err)
|
||||
}
|
||||
|
||||
// Return the response to the client
|
||||
ctx.res.set_status(response.status)
|
||||
|
||||
// ctx.res.header = response.header
|
||||
// ctx.set_content_type('application/json')
|
||||
|
||||
// return ctx.ok('[]')
|
||||
return ctx.send_response_to_client('application/json', response.body)
|
||||
}
|
||||
|
||||
// Handles errors and maps them to OpenAPI-defined response statuses
|
||||
fn (mut ctx Context) handle_error(possible_responses map[string]ResponseSpec, err IError) veb.Result {
|
||||
// Match the error with the defined responses
|
||||
for code, _ in possible_responses {
|
||||
if matches_error_to_status(err, code.int()) {
|
||||
ctx.res.set_status(http.status_from_int(code.int()))
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.send_response_to_client(
|
||||
'application/json',
|
||||
'{"error": "$err.msg()", "status": $code}'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Default to 500 Internal Controller Error if no match is found
|
||||
ctx.res.set_status(.internal_server_error)
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.send_response_to_client(
|
||||
'application/json',
|
||||
'{"error": "Internal Controller Error", "status": 500}'
|
||||
)
|
||||
}
|
||||
|
||||
// Helper to match an error to a specific response status
|
||||
fn matches_error_to_status(err IError, status int) bool {
|
||||
// This can be customized to map specific errors to statuses
|
||||
// For simplicity, we'll use a direct comparison here.
|
||||
return err.code() == status
|
||||
}
|
||||
|
||||
// Helper for 405 Method Not Allowed response
|
||||
fn (mut ctx Context) method_not_allowed() veb.Result {
|
||||
ctx.res.set_status(.method_not_allowed)
|
||||
ctx.set_content_type('application/json')
|
||||
return ctx.send_response_to_client(
|
||||
'application/json',
|
||||
'{"error": "Method Not Allowed", "status": 405}'
|
||||
)
|
||||
}
|
||||
|
||||
pub fn (param Parameter) typed(value string) Any {
|
||||
param_schema := param.schema as Schema
|
||||
param_type := param_schema.typ
|
||||
param_format := param_schema.format
|
||||
|
||||
// Convert parameter value to corresponding type
|
||||
typ := match param_type {
|
||||
'integer' {
|
||||
param_format
|
||||
}
|
||||
'number' {
|
||||
param_format
|
||||
}
|
||||
else {
|
||||
param_type // Leave as param type for unknown types
|
||||
}
|
||||
}
|
||||
return typed(value, typ)
|
||||
}
|
||||
|
||||
// typed gets a value that is string and a desired type, and returns the typed string in Any Type.
|
||||
pub fn typed(value string, typ string) Any {
|
||||
match typ {
|
||||
'int32' {
|
||||
return value.int() // Convert to int
|
||||
}
|
||||
'int64' {
|
||||
return value.i64() // Convert to i64
|
||||
}
|
||||
'string' {
|
||||
return value // Already a string
|
||||
}
|
||||
'boolean' {
|
||||
return value.bool() // Convert to bool
|
||||
}
|
||||
'float' {
|
||||
return value.f32() // Convert to float
|
||||
}
|
||||
'double' {
|
||||
return value.f64() // Convert to double
|
||||
}
|
||||
else {
|
||||
return value.f64() // Leave as string for unknown types
|
||||
}
|
||||
}
|
||||
}
|
||||
346
lib/schemas/openapi/testdata/openapi.json
vendored
Normal file
346
lib/schemas/openapi/testdata/openapi.json
vendored
Normal file
@@ -0,0 +1,346 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Pet Store API",
|
||||
"description": "A sample API for a pet store",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://api.petstore.example.com/v1",
|
||||
"description": "Production server"
|
||||
},
|
||||
{
|
||||
"url": "https://staging.petstore.example.com/v1",
|
||||
"description": "Staging server"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/pets": {
|
||||
"get": {
|
||||
"summary": "List all pets",
|
||||
"operationId": "listPets",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"description": "Maximum number of pets to return",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A paginated list of pets",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pets"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid request"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"summary": "Create a new pet",
|
||||
"operationId": "createPet",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NewPet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Pet created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid input"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/pets/{petId}": {
|
||||
"get": {
|
||||
"summary": "Get a pet by ID",
|
||||
"operationId": "getPet",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "ID of the pet to retrieve",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A pet",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Pet not found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete a pet by ID",
|
||||
"operationId": "deletePet",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "petId",
|
||||
"in": "path",
|
||||
"description": "ID of the pet to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Pet deleted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Pet not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orders": {
|
||||
"get": {
|
||||
"summary": "List all orders",
|
||||
"operationId": "listOrders",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A list of orders",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Order"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/orders/{orderId}": {
|
||||
"get": {
|
||||
"summary": "Get an order by ID",
|
||||
"operationId": "getOrder",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orderId",
|
||||
"in": "path",
|
||||
"description": "ID of the order to retrieve",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "An order",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Order"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"summary": "Delete an order by ID",
|
||||
"operationId": "deleteOrder",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "orderId",
|
||||
"in": "path",
|
||||
"description": "ID of the order to delete",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Order deleted"
|
||||
},
|
||||
"404": {
|
||||
"description": "Order not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users": {
|
||||
"post": {
|
||||
"summary": "Create a user",
|
||||
"operationId": "createUser",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NewUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "User created",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/User"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Pet": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NewPet": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
},
|
||||
"Order": {
|
||||
"type": "object",
|
||||
"required": ["id", "petId", "quantity", "shipDate"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"petId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"quantity": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"shipDate": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["placed", "approved", "delivered"]
|
||||
},
|
||||
"complete": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"User": {
|
||||
"type": "object",
|
||||
"required": ["id", "username"],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NewUser": {
|
||||
"type": "object",
|
||||
"required": ["username"],
|
||||
"properties": {
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
112
lib/schemas/openrpc/README.md
Normal file
112
lib/schemas/openrpc/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# OpenRPC
|
||||
|
||||
OpenRPC V library. Model for OpenRPC, client code generation, and specification generation from code.
|
||||
|
||||
## Definitions
|
||||
|
||||
- OpenRPC Specifications: Specifications that define standards for describing JSON-RPC API's.
|
||||
|
||||
- [OpenRPC Document](https://spec.open-rpc.org/#openrpc-document): "A document that defines or describes an API conforming to the OpenRPC Specification."
|
||||
|
||||
- OpenRPC Client: An API Client (using either HTTP or Websocket) that governs functions (one per RPC Method defined in OpenRPC Document) to communicate with RPC servers and perform RPCs.
|
||||
|
||||
## OpenRPC Document Generation
|
||||
|
||||
The OpenRPC Document Generator generates a [JSON-RPC](https://www.jsonrpc.org/) API description conforming to [OpenRPC Specifications](https://spec.open-rpc.org/), from an OpenRPC Client module written in V.
|
||||
|
||||
To use the document generator, you need to have a V module with at least one V file.
|
||||
|
||||
The recommended way to generate an OpenRPC document for a V module is to use the OpenRPC CLI. Note below: if you are not using the openrpc cli as a binary executable, replace `openrpc` with `path_to_herolib.core.openrpc/cli/cli.v` in the commands below
|
||||
|
||||
`openrpc docgen .`
|
||||
|
||||
Running this command in a V client module's directory will generate an OpenRPC document from the code in the module and output it to a openrpc.json file in the same directory. See [annotation code for openrpc document generation](#annotating-code-for-openrpc-document-generation) for information on how to format V client module to be compatible for OpenRPC Document Generation.
|
||||
|
||||
The following output parameter and source path argument can be used to generate an OpenRPC Document for a module that is in a different directory, and output the document in a desired path.
|
||||
|
||||
`openrpc docgen -o <output_path> <source_path>`
|
||||
|
||||
Run `openrpc docgen help` for more information. The CLI also has flags for filtering files and directories on input source, or choosing to generate document for only public struct and functions.
|
||||
|
||||
|
||||
### Annotating code for OpenRPC Document Generation
|
||||
|
||||
The docgen module uses the [codeparser module](../../codeparser) to parse the source code for document generation. Therefore, the V code from which an OpenRPC Document will be generated must conform to the [V Annotation guidelines for code parsing](../../codeparser/README.md/#annotating-code-in-v), such that the document generator can harvest method and schema information such as descriptions from the comments in the code.
|
||||
|
||||
Below is an example OpenRPC compliant JSON Method and Schema descriptions generated from a properly annotated v file.
|
||||
|
||||
```go
|
||||
// this is a description of the struct
|
||||
struct Example {
|
||||
field0 string // this comment describes field0
|
||||
field1 int // this comment describes field1
|
||||
}
|
||||
|
||||
// some_function is described by the words following the functions name
|
||||
// - param0: this sentence after the colon describes param0
|
||||
// - param1: this sentence after the colon describes param1
|
||||
// returns the desired result, this sentence after the comma describes 'the desired result'
|
||||
fn some_function(param0 string, param1 int) result []string {}
|
||||
```
|
||||
|
||||
The following OpenRPC JSON Descriptions are generated from the above code:
|
||||
|
||||
```js
|
||||
// schema generated from Example struct
|
||||
{
|
||||
'name': 'Example'
|
||||
'description': 'this is a description of the struct'
|
||||
'properties': [
|
||||
'field0': {
|
||||
'type': 'string'
|
||||
},
|
||||
'field1': {
|
||||
'type': 'int'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// method generated from some_function function
|
||||
{
|
||||
'name': 'some_function'
|
||||
'description': 'is described by the words following the functions name'
|
||||
'params': [
|
||||
{
|
||||
'name': 'param0'
|
||||
'description': 'this sentence after the colon describes param0'
|
||||
'schema': {
|
||||
'type': 'string'
|
||||
}
|
||||
},
|
||||
{
|
||||
'name': 'param1'
|
||||
'description': 'this sentence after the colon describes param1'
|
||||
'schema': {
|
||||
'type': 'int'
|
||||
}
|
||||
}
|
||||
]
|
||||
'result': {
|
||||
'name': 'the desired result'
|
||||
'description': 'this sentence after the comma describes \'the desired result\''
|
||||
'schema': {
|
||||
'type': 'array'
|
||||
'items': {
|
||||
'type': 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## Examples
|
||||
|
||||
The best way to understand how the document generation works is through the examples in this module
|
||||
|
||||
### Pet Store Example
|
||||
|
||||
Run this command from the root of the openrpc module.
|
||||
|
||||
```bash
|
||||
v run cli/cli.v docgen -t 'PetStore API' -o examples/petstore_client -f 'client.v' examples/petstore_client
|
||||
```
|
||||
This generates an OpenRPC Document called PetStore API from the code in examples/petstore_client, excluding the client.v file, and writes it to examples/petstore_client/openrpc.json
|
||||
103
lib/schemas/openrpc/codegen/docgen.v
Normal file
103
lib/schemas/openrpc/codegen/docgen.v
Normal file
@@ -0,0 +1,103 @@
|
||||
module codegen
|
||||
|
||||
// import freeflowuniverse.herolib.core.code
|
||||
// import freeflowuniverse.herolib.ui.console
|
||||
// import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
// // configuration parameters for OpenRPC Document generation.
|
||||
// @[params]
|
||||
// pub struct DocGenConfig {
|
||||
// title string // Title of the JSON-RPC API
|
||||
// description string // Description of the JSON-RPC API
|
||||
// version string = '1.0.0' // OpenRPC Version used
|
||||
// source string // Source code directory to generate doc from
|
||||
// strict bool // Strict mode generates document for only methods and struct with the attribute `openrpc`
|
||||
// exclude_dirs []string // directories to be excluded when parsing source for document generation
|
||||
// exclude_files []string // files to be excluded when parsing source for document generation
|
||||
// only_pub bool // excludes all non-public declarations from document generation
|
||||
// }
|
||||
|
||||
// // docgen returns OpenRPC Document struct for JSON-RPC API defined in the config params.
|
||||
// // returns generated OpenRPC struct which can be encoded into json using `OpenRPC.encode()`
|
||||
// pub fn docgen(config DocGenConfig) !OpenRPC {
|
||||
// $if debug {
|
||||
// console.print_debug('Generating OpenRPC Document from path: ${config.source}')
|
||||
// }
|
||||
|
||||
// // parse source code into code items
|
||||
// code := codeparser.parse_v(config.source,
|
||||
// exclude_dirs: config.exclude_dirs
|
||||
// exclude_files: config.exclude_files
|
||||
// only_pub: config.only_pub
|
||||
// recursive: true
|
||||
// )!
|
||||
|
||||
// mut schemas := map[string]jsonschema.SchemaRef{}
|
||||
// mut methods := []Method{}
|
||||
|
||||
// // generate JSONSchema compliant schema definitions for structs in code
|
||||
// for struct_ in code.filter(it is Struct).map(it as Struct) {
|
||||
// schema := jsonschema.struct_to_schema(struct_)
|
||||
// schemas[struct_.name] = schema
|
||||
// }
|
||||
|
||||
// // generate JSONSchema compliant schema definitions for sumtypes in code
|
||||
// for sumtype in code.filter(it is Sumtype).map(it as Sumtype) {
|
||||
// schema := jsonschema.sumtype_to_schema(sumtype)
|
||||
// schemas[sumtype.name] = schema
|
||||
// }
|
||||
|
||||
// // generate OpenRPC compliant method definitions for functions in code
|
||||
// for function in code.filter(it is Function).map(it as Function) {
|
||||
// method := fn_to_method(function)
|
||||
// methods << method
|
||||
// }
|
||||
|
||||
// return OpenRPC{
|
||||
// info: Info{
|
||||
// title: config.title
|
||||
// version: config.version
|
||||
// }
|
||||
// methods: methods
|
||||
// components: Components{
|
||||
// schemas: schemas
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // fn_to_method turns a codemodel function into a openrpc method description
|
||||
// fn fn_to_method(function Function) Method {
|
||||
// $if debug {
|
||||
// println('Creating openrpc method description for function: ${function.name}')
|
||||
// }
|
||||
|
||||
// params := params_to_descriptors(function.params)
|
||||
// result_schema := jsonschema.typesymbol_to_schema(function.result.typ.symbol)
|
||||
|
||||
// // if result name isn't set, set it to
|
||||
// result_name := if function.result.name != '' {
|
||||
// function.result.name
|
||||
// } else {
|
||||
// function.result.typ.symbol
|
||||
// }
|
||||
|
||||
// result := ContentDescriptor{
|
||||
// name: result_name
|
||||
// schema: result_schema
|
||||
// description: function.result.description
|
||||
// }
|
||||
|
||||
// pascal_name := texttools.name_fix_snake_to_pascal(function.name)
|
||||
// function_name := if function.mod != '' {
|
||||
// '${function.mod}.${pascal_name}'
|
||||
// } else {
|
||||
// pascal_name
|
||||
// }
|
||||
|
||||
// return Method{
|
||||
// name: function_name
|
||||
// description: function.description
|
||||
// params: params
|
||||
// result: result
|
||||
// }
|
||||
// }
|
||||
45
lib/schemas/openrpc/codegen/generate.v
Normal file
45
lib/schemas/openrpc/codegen/generate.v
Normal file
@@ -0,0 +1,45 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { VFile, File, Function, Struct , Module}
|
||||
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC}
|
||||
|
||||
// pub struct OpenRPCCode {
|
||||
// pub mut:
|
||||
// openrpc_json File
|
||||
// handler VFile
|
||||
// handler_test VFile
|
||||
// client VFile
|
||||
// client_test VFile
|
||||
// server VFile
|
||||
// server_test VFile
|
||||
// }
|
||||
|
||||
|
||||
pub fn generate_module(o OpenRPC, receiver Struct, methods_map map[string]Function, objects_map map[string]Struct) !Module {
|
||||
openrpc_json := o.encode()!
|
||||
openrpc_file := File{
|
||||
name: 'openrpc'
|
||||
extension: 'json'
|
||||
content: openrpc_json
|
||||
}
|
||||
|
||||
client_file := generate_client_file(o, objects_map)!
|
||||
client_test_file := generate_client_test_file(o, methods_map, objects_map)!
|
||||
|
||||
handler_file := generate_handler_file(o, receiver, methods_map, objects_map)!
|
||||
handler_test_file := generate_handler_test_file(o, receiver, methods_map, objects_map)!
|
||||
|
||||
interface_file := generate_interface_file(o)!
|
||||
interface_test_file := generate_interface_test_file(o)!
|
||||
|
||||
return Module{
|
||||
files: [
|
||||
client_file
|
||||
client_test_file
|
||||
handler_file
|
||||
handler_test_file
|
||||
interface_file
|
||||
interface_test_file
|
||||
]
|
||||
}
|
||||
}
|
||||
71
lib/schemas/openrpc/codegen/generate_client.v
Normal file
71
lib/schemas/openrpc/codegen/generate_client.v
Normal file
@@ -0,0 +1,71 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { VFile, CodeItem, CustomCode, Function, Struct, parse_function }
|
||||
// import freeflowuniverse.herolib.schemas.jsonrpc.codegen {generate_client_struct}
|
||||
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC}
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
// generate_structs geenrates struct codes for schemas defined in an openrpc document
|
||||
pub fn generate_client_file(o OpenRPC, object_map map[string]Struct) !VFile {
|
||||
name := texttools.name_fix(o.info.title)
|
||||
client_struct_name := '${o.info.title}Client'
|
||||
// client_struct := generate_client_struct(client_struct_name)
|
||||
|
||||
mut items := []CodeItem{}
|
||||
// code << client_struct
|
||||
// code << jsonrpc.generate_ws_factory_code(client_struct_name)!
|
||||
// methods := jsonrpc.generate_client_methods(client_struct, o.methods.map(it.to_code()!))!
|
||||
// imports := [code.parse_import('freeflowuniverse.herolib.schemas.jsonrpc'),
|
||||
// code.parse_import('freeflowuniverse.herolib.schemas.rpcwebsocket'),
|
||||
// code.parse_import('log')]
|
||||
// code << methods.map(CodeItem(it))
|
||||
mut file := VFile{
|
||||
name: 'client'
|
||||
mod: name
|
||||
// imports: imports
|
||||
items: items
|
||||
}
|
||||
for key, object in object_map {
|
||||
file.add_import(mod: object.mod, types: [object.name])!
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// generate_structs generates struct codes for schemas defined in an openrpc document
|
||||
pub fn generate_client_test_file(o OpenRPC, methods_map map[string]Function, object_map map[string]Struct) !VFile {
|
||||
name := texttools.name_fix(o.info.title)
|
||||
// client_struct_name := '${o.info.title}Client'
|
||||
// client_struct := jsonrpc.generate_client_struct(client_struct_name)
|
||||
|
||||
// code << client_struct
|
||||
// code << jsonrpc.(client_struct_name)
|
||||
// methods := jsonrpc.generate_client_methods(client_struct, o.methods.map(Function{name: it.name}))!
|
||||
|
||||
mut fn_test_factory := parse_function('fn test_new_ws_client() !')!
|
||||
fn_test_factory.body = "mut client := new_ws_client(address:'ws://127.0.0.1:\${port}')!"
|
||||
|
||||
mut items := []CodeItem{}
|
||||
items << CustomCode{'const port = 3100'}
|
||||
items << fn_test_factory
|
||||
for key, method in methods_map {
|
||||
mut func := parse_function('fn test_${method.name}() !')!
|
||||
func_call := method.generate_call(receiver: 'client')!
|
||||
func.body = "mut client := new_ws_client(address:'ws://127.0.0.1:\${port}')!\n${func_call}"
|
||||
items << func
|
||||
}
|
||||
mut file := VFile{
|
||||
name: 'client_test'
|
||||
mod: name
|
||||
imports: [
|
||||
code.parse_import('freeflowuniverse.herolib.schemas.jsonrpc'),
|
||||
code.parse_import('freeflowuniverse.herolib.schemas.rpcwebsocket'),
|
||||
code.parse_import('log'),
|
||||
]
|
||||
items: items
|
||||
}
|
||||
|
||||
for key, object in object_map {
|
||||
file.add_import(mod: object.mod, types: [object.name])!
|
||||
}
|
||||
return file
|
||||
}
|
||||
108
lib/schemas/openrpc/codegen/generate_handler.v
Normal file
108
lib/schemas/openrpc/codegen/generate_handler.v
Normal file
@@ -0,0 +1,108 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { VFile, CodeItem, Param, CustomCode, Function, Result, Struct, parse_import }
|
||||
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC}
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import rand
|
||||
|
||||
pub fn generate_handler_file(o OpenRPC, receiver Struct, method_map map[string]Function, object_map map[string]Struct) !VFile {
|
||||
name := texttools.name_fix(o.info.title)
|
||||
|
||||
imports := [
|
||||
parse_import('freeflowuniverse.herolib.schemas.jsonrpc'),
|
||||
parse_import('json'),
|
||||
parse_import('x.json2'),
|
||||
parse_import('import freeflowuniverse.herolib.core.texttools'),
|
||||
]
|
||||
|
||||
mut file := VFile{
|
||||
name: 'handler'
|
||||
mod: name
|
||||
imports: imports
|
||||
// TODO
|
||||
// items: jsonrpc.generate_handler(
|
||||
// methods: method_map.values()
|
||||
// receiver: receiver
|
||||
// )!
|
||||
}
|
||||
|
||||
for key, object in object_map {
|
||||
file.add_import(mod: object.mod, types: [object.name])!
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
pub fn generate_handler_test_file(o OpenRPC, receiver Struct, method_map map[string]Function, object_map map[string]Struct) !VFile {
|
||||
name := texttools.name_fix(o.info.title)
|
||||
|
||||
handler_name := texttools.name_fix_pascal_to_snake(receiver.name)
|
||||
|
||||
consts := CustomCode{"const actor_name = '${handler_name}_test_actor'"}
|
||||
clean_code := 'mut actor := get(name: actor_name)!\nactor.backend.reset()!'
|
||||
|
||||
testsuite_begin := Function{
|
||||
name: 'testsuite_begin'
|
||||
body: clean_code
|
||||
}
|
||||
|
||||
testsuite_end := Function{
|
||||
name: 'testsuite_end'
|
||||
body: clean_code
|
||||
}
|
||||
|
||||
mut handle_tests := []Function{}
|
||||
for key, method in method_map {
|
||||
if method.params.len == 0 {
|
||||
continue
|
||||
}
|
||||
if method.params[0].typ.symbol[0].is_capital() {
|
||||
continue
|
||||
}
|
||||
method_handle_test := Function{
|
||||
name: 'test_handle_${method.name}'
|
||||
result: Param{
|
||||
is_result: true
|
||||
}
|
||||
body: "mut handler := ${receiver.name}Handler {${handler_name}.get(name: actor_name)!}
|
||||
request := new_jsonrpcrequest[${method.params[0].typ.symbol}]('${method.name}', ${get_mock_value(method.params[0].typ.symbol)!})
|
||||
response_json := handler.handle(request.to_json())!"
|
||||
}
|
||||
handle_tests << method_handle_test
|
||||
}
|
||||
|
||||
mut items := []CodeItem{}
|
||||
|
||||
items = [
|
||||
consts,
|
||||
testsuite_begin,
|
||||
testsuite_end,
|
||||
]
|
||||
|
||||
items << handle_tests.map(CodeItem(it))
|
||||
|
||||
imports := parse_import('freeflowuniverse.herolib.schemas.jsonrpc {new_jsonrpcrequest, jsonrpcresponse_decode, jsonrpcerror_decode}')
|
||||
|
||||
mut file := VFile{
|
||||
name: 'handler_test'
|
||||
mod: name
|
||||
imports: [imports]
|
||||
items: items
|
||||
}
|
||||
|
||||
for key, object in object_map {
|
||||
file.add_import(mod: object.mod, types: [object.name])!
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
fn get_mock_value(typ string) !string {
|
||||
if typ == 'string' {
|
||||
return "'mock_string_${rand.string(3)}'"
|
||||
} else if typ == 'int' || typ == 'u32' {
|
||||
return '42'
|
||||
} else {
|
||||
return error('mock values for types other than strings and ints are not yet supported')
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn ()
|
||||
70
lib/schemas/openrpc/codegen/generate_interface.v
Normal file
70
lib/schemas/openrpc/codegen/generate_interface.v
Normal file
@@ -0,0 +1,70 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { VFile, CustomCode, parse_function, parse_import }
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC}
|
||||
|
||||
// pub fn (mut handler AccountantHandler) handle_ws(client &websocket.Client, message string) string {
|
||||
// return handler.handle(message) or { panic(err) }
|
||||
// }
|
||||
|
||||
// pub fn run_wsserver(port int) ! {
|
||||
// mut logger := log.Logger(&log.Log{level: .debug})
|
||||
// mut handler := AccountantHandler{get(name: 'accountant')!}
|
||||
// mut server := rpcwebsocket.new_rpcwsserver(port, handler.handle_ws, logger)!
|
||||
// server.run()!
|
||||
// }
|
||||
|
||||
pub fn generate_interface_file(specification OpenRPC) !VFile {
|
||||
name := texttools.name_fix(specification.info.title)
|
||||
|
||||
mut handle_ws_fn := parse_function('pub fn (mut handler ${name.title()}Handler) handle_ws(client &websocket.Client, message string) string ')!
|
||||
handle_ws_fn.body = 'return handler.handle(message) or { panic(err) }'
|
||||
|
||||
mut run_wsserver_fn := parse_function('pub fn run_wsserver(port int) !')!
|
||||
run_wsserver_fn.body = "
|
||||
log_ := log.Log{level: .debug}
|
||||
mut logger := log.Logger(&log_)
|
||||
mut handler := ${name.title()}Handler{get(name: '${name}')!}
|
||||
mut server := rpcwebsocket.new_rpcwsserver(port, handler.handle_ws, logger)!
|
||||
server.run()!"
|
||||
items := handle_ws_fn
|
||||
|
||||
return VFile{
|
||||
mod: name
|
||||
name: 'server'
|
||||
imports: [
|
||||
parse_import('log'),
|
||||
parse_import('net.websocket'),
|
||||
parse_import('freeflowuniverse.herolib.schemas.rpcwebsocket {RpcWsServer}'),
|
||||
]
|
||||
items: [
|
||||
handle_ws_fn, run_wsserver_fn
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_interface_test_file(specification OpenRPC) !VFile {
|
||||
name := texttools.name_fix(specification.info.title)
|
||||
// mut handle_ws_fn := parse_function('pub fn (mut handler ${name.title()}Handler) handle_ws(client &websocket.Client, message string) string ')!
|
||||
// handle_ws_fn.body = "return handler.handle(message) or { panic(err) }"
|
||||
|
||||
// mut run_wsserver_fn := parse_function('pub fn run_wsserver(port int) !')!
|
||||
// run_wsserver_fn.body = "mut logger := log.Logger(&log.Log{level: .debug})
|
||||
// mut handler := ${name.title()}Handler{get(name: '${name}')!}
|
||||
// mut server := rpcwebsocket.new_rpcwsserver(port, handler.handle_ws, logger)!
|
||||
// server.run()!"
|
||||
// items := handle_ws_fn
|
||||
|
||||
mut test_fn := parse_function('pub fn test_wsserver() !')!
|
||||
test_fn.body = 'spawn run_wsserver(port)'
|
||||
|
||||
return VFile{
|
||||
mod: name
|
||||
name: 'server_test'
|
||||
items: [
|
||||
CustomCode{'const port = 3000'},
|
||||
test_fn,
|
||||
]
|
||||
}
|
||||
}
|
||||
43
lib/schemas/openrpc/codegen/generate_model.v
Normal file
43
lib/schemas/openrpc/codegen/generate_model.v
Normal file
@@ -0,0 +1,43 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { CodeItem }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen { schema_to_code }
|
||||
import freeflowuniverse.herolib.schemas.openrpc {OpenRPC}
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
// generate_structs geenrates struct codes for schemas defined in an openrpc document
|
||||
pub fn generate_model(o OpenRPC) ![]CodeItem {
|
||||
components := o.components
|
||||
mut structs := []CodeItem{}
|
||||
for key, schema_ in components.schemas {
|
||||
if schema_ is Schema {
|
||||
mut schema := schema_
|
||||
if schema.title == '' {
|
||||
schema.title = texttools.name_fix_snake_to_pascal(key)
|
||||
}
|
||||
structs << schema_to_code(schema)!
|
||||
}
|
||||
}
|
||||
return structs
|
||||
}
|
||||
|
||||
// pub fn (s Schema) to_struct() code.Struct {
|
||||
// mut attributes := []Attribute{}
|
||||
// if c.depracated {
|
||||
// attributes << Attribute {name: 'deprecated'}
|
||||
// }
|
||||
// if !c.required {
|
||||
// attributes << Attribute {name: 'params'}
|
||||
// }
|
||||
|
||||
// return code.Struct {
|
||||
// name: name
|
||||
// description: summary
|
||||
// required: required
|
||||
// schema: Schema {
|
||||
|
||||
// }
|
||||
// attrs: attributes
|
||||
// }
|
||||
// }
|
||||
49
lib/schemas/openrpc/codegen/generate_model_test.v
Normal file
49
lib/schemas/openrpc/codegen/generate_model_test.v
Normal file
@@ -0,0 +1,49 @@
|
||||
module codegen
|
||||
|
||||
import os
|
||||
import json
|
||||
import freeflowuniverse.herolib.core.code { Alias, Struct }
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
const doc_path = '${os.dir(@FILE)}/testdata/openrpc.json'
|
||||
|
||||
fn test_generate_model() ! {
|
||||
mut doc_file := pathlib.get_file(path: doc_path)!
|
||||
content := doc_file.read()!
|
||||
object := openrpc.decode(content)!
|
||||
model := generate_model(object)!
|
||||
|
||||
assert model.len == 3
|
||||
assert model[0] is Alias
|
||||
pet_id := model[0] as Alias
|
||||
assert pet_id.name == 'PetId'
|
||||
assert pet_id.typ.symbol == 'int'
|
||||
|
||||
assert model[1] is Struct
|
||||
pet_struct := model[1] as Struct
|
||||
assert pet_struct.name == 'Pet'
|
||||
assert pet_struct.fields.len == 3
|
||||
|
||||
// test field is `id PetId @[required]`
|
||||
assert pet_struct.fields[0].name == 'id'
|
||||
assert pet_struct.fields[0].typ.symbol == 'PetId'
|
||||
assert pet_struct.fields[0].attrs.len == 1
|
||||
assert pet_struct.fields[0].attrs[0].name == 'required'
|
||||
|
||||
// test field is `name string @[required]`
|
||||
assert pet_struct.fields[1].name == 'name'
|
||||
assert pet_struct.fields[1].typ.symbol == 'string'
|
||||
assert pet_struct.fields[1].attrs.len == 1
|
||||
assert pet_struct.fields[1].attrs[0].name == 'required'
|
||||
|
||||
// test field is `tag string`
|
||||
assert pet_struct.fields[2].name == 'tag'
|
||||
assert pet_struct.fields[2].typ.symbol == 'string'
|
||||
assert pet_struct.fields[2].attrs.len == 0
|
||||
|
||||
assert model[2] is Alias
|
||||
pets_alias := model[2] as Alias
|
||||
assert pets_alias.name == 'Pets'
|
||||
assert pets_alias.typ.symbol == '[]Pet'
|
||||
}
|
||||
146
lib/schemas/openrpc/codegen/generate_openrpc.v
Normal file
146
lib/schemas/openrpc/codegen/generate_openrpc.v
Normal file
@@ -0,0 +1,146 @@
|
||||
module codegen
|
||||
|
||||
// import freeflowuniverse.herolib.schemas.jsonschema
|
||||
// import freeflowuniverse.herolib.core.code { Function, Struct, Sumtype }
|
||||
// import freeflowuniverse.herolib.ui.console
|
||||
// import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
// // configuration parameters for OpenRPC Document generation.
|
||||
// @[params]
|
||||
// pub struct DocGenConfig {
|
||||
// title string // Title of the JSON-RPC API
|
||||
// description string // Description of the JSON-RPC API
|
||||
// version string = '1.0.0' // OpenRPC Version used
|
||||
// source string // Source code directory to generate doc from
|
||||
// strict bool // Strict mode generates document for only methods and struct with the attribute `openrpc`
|
||||
// exclude_dirs []string // directories to be excluded when parsing source for document generation
|
||||
// exclude_files []string // files to be excluded when parsing source for document generation
|
||||
// only_pub bool // excludes all non-public declarations from document generation
|
||||
// }
|
||||
|
||||
// // docgen returns OpenRPC Document struct for JSON-RPC API defined in the config params.
|
||||
// // returns generated OpenRPC struct which can be encoded into json using `OpenRPC.encode()`
|
||||
// pub fn docgen(config DocGenConfig) !OpenRPC {
|
||||
// $if debug {
|
||||
// console.print_debug('Generating OpenRPC Document from path: ${config.source}')
|
||||
// }
|
||||
|
||||
// // parse source code into code items
|
||||
// code := codeparser.parse_v(config.source,
|
||||
// exclude_dirs: config.exclude_dirs
|
||||
// exclude_files: config.exclude_files
|
||||
// only_pub: config.only_pub
|
||||
// recursive: true
|
||||
// )!
|
||||
|
||||
// mut schemas := map[string]jsonschema.SchemaRef{}
|
||||
// mut methods := []Method{}
|
||||
|
||||
// // generate JSONSchema compliant schema definitions for structs in code
|
||||
// for struct_ in code.filter(it is Struct).map(it as Struct) {
|
||||
// schema := jsonschema.struct_to_schema(struct_)
|
||||
// schemas[struct_.name] = schema
|
||||
// }
|
||||
|
||||
// // generate JSONSchema compliant schema definitions for sumtypes in code
|
||||
// for sumtype in code.filter(it is Sumtype).map(it as Sumtype) {
|
||||
// schema := jsonschema.sumtype_to_schema(sumtype)
|
||||
// schemas[sumtype.name] = schema
|
||||
// }
|
||||
|
||||
// // generate OpenRPC compliant method definitions for functions in code
|
||||
// for function in code.filter(it is Function).map(it as Function) {
|
||||
// method := fn_to_method(function)
|
||||
// methods << method
|
||||
// }
|
||||
|
||||
// return OpenRPC{
|
||||
// info: Info{
|
||||
// title: config.title
|
||||
// version: config.version
|
||||
// }
|
||||
// methods: methods
|
||||
// components: Components{
|
||||
// schemas: schemas
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // fn_to_method turns a codemodel function into a openrpc method description
|
||||
// pub fn fn_to_method(function Function) Method {
|
||||
// $if debug {
|
||||
// console.print_debug('Creating openrpc method description for function: ${function.name}')
|
||||
// }
|
||||
|
||||
// params := params_to_descriptors(function.params)
|
||||
// // if result name isn't set, set it to
|
||||
// result_name := if function.result.name != '' {
|
||||
// function.result.name
|
||||
// } else {
|
||||
// function.result.typ.symbol
|
||||
// }
|
||||
|
||||
// // result := ContentDescriptor{
|
||||
// // name: result_name
|
||||
// // schema: result_schema
|
||||
// // description: function.result.description
|
||||
// // }
|
||||
|
||||
// pascal_name := texttools.name_fix_snake_to_pascal(function.name)
|
||||
// function_name := if function.mod != '' {
|
||||
// '${pascal_name}'
|
||||
// } else {
|
||||
// pascal_name
|
||||
// }
|
||||
// m := Method{
|
||||
// name: function_name
|
||||
// description: function.description
|
||||
// params: params
|
||||
// result: param_to_descriptor(function.result)
|
||||
// }
|
||||
|
||||
// return Method{
|
||||
// name: function_name
|
||||
// description: function.description
|
||||
// params: params
|
||||
// result: param_to_descriptor(function.result)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // get_param_descriptors returns content descriptors generated for a list of params
|
||||
// fn params_to_descriptors(params []code.Param) []ContentDescriptorRef {
|
||||
// mut descriptors := []ContentDescriptorRef{}
|
||||
// for param in params {
|
||||
// descriptors << param_to_descriptor(param)
|
||||
// }
|
||||
// return descriptors
|
||||
// }
|
||||
|
||||
// // get_param_descriptors returns content descriptors generated for a list of params
|
||||
// fn param_to_descriptor(param code.Param) ContentDescriptorRef {
|
||||
// schemaref := jsonschema.param_to_schema(param)
|
||||
// return ContentDescriptorRef(ContentDescriptor{
|
||||
// name: param.name
|
||||
// schema: schemaref
|
||||
// description: param.description
|
||||
// required: param.required
|
||||
// })
|
||||
// }
|
||||
|
||||
// // get_param_descriptors returns content descriptors generated for a list of params
|
||||
// pub fn result_to_descriptor(result code.Result) ContentDescriptorRef {
|
||||
// schemaref := jsonschema.result_to_schema(result)
|
||||
|
||||
// name := if result.name != '' {
|
||||
// result.name
|
||||
// } else if result.typ.symbol != '' {
|
||||
// result.typ.symbol
|
||||
// } else {
|
||||
// 'null'
|
||||
// }
|
||||
// return ContentDescriptorRef(ContentDescriptor{
|
||||
// name: name
|
||||
// schema: schemaref
|
||||
// description: result.description
|
||||
// })
|
||||
// }
|
||||
25
lib/schemas/openrpc/codegen/templates/client.v.template
Normal file
25
lib/schemas/openrpc/codegen/templates/client.v.template
Normal file
@@ -0,0 +1,25 @@
|
||||
module @{module}
|
||||
|
||||
import freeflowuniverse.herolib.data.rpcwebsocket
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import log
|
||||
import json
|
||||
|
||||
pub struct @{name}OpenRpcClient {
|
||||
mut:
|
||||
transport &jsonrpc.IRpcTransportClient
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct ClientConfig {
|
||||
address string
|
||||
logger log.Logger
|
||||
}
|
||||
|
||||
pub fn new_ws_client(config ClientConfig) !PetstoreOpenRpcClient {
|
||||
mut transport := rpcwebsocket.new_rpcwsclient(config.address, config.logger)!
|
||||
spawn transport.run()
|
||||
return PetstoreJsonRpcClient {
|
||||
transport: transport
|
||||
}
|
||||
}
|
||||
205
lib/schemas/openrpc/codegen/testdata/openrpc.json
vendored
Normal file
205
lib/schemas/openrpc/codegen/testdata/openrpc.json
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"openrpc": "1.0.0-rc1",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Petstore",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "localhost",
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "list_pets",
|
||||
"summary": "List all pets",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "How many items to return at one time (max 100)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pets",
|
||||
"description": "A paged array of pets",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pets"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": 100,
|
||||
"message": "pets busy"
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"name": "listPetExample",
|
||||
"description": "List pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "limit",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "listPetResultExample",
|
||||
"value": [
|
||||
{
|
||||
"id": 7,
|
||||
"name": "fluffy",
|
||||
"tag": "poodle"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "create_pet",
|
||||
"summary": "Create a pet",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "newPetName",
|
||||
"description": "Name of pet to create",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "newPetTag",
|
||||
"description": "Pet tag to create",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"name": "createPetExample",
|
||||
"description": "Create pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "newPetName",
|
||||
"value": "fluffy"
|
||||
},
|
||||
{
|
||||
"name": "tag",
|
||||
"value": "poodle"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "listPetResultExample",
|
||||
"value": 7
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"$ref": "#/components/contentDescriptors/PetId"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_pet",
|
||||
"summary": "Info for a specific pet",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"$ref": "#/components/contentDescriptors/PetId"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet",
|
||||
"description": "Expected response to a valid request",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "getPetExample",
|
||||
"description": "get pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "petId",
|
||||
"value": 7
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "getPetExampleResult",
|
||||
"value": {
|
||||
"name": "fluffy",
|
||||
"tag": "poodle",
|
||||
"id": 7
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"contentDescriptors": {
|
||||
"PetId": {
|
||||
"name": "petId",
|
||||
"required": true,
|
||||
"description": "The id of the pet to retrieve",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PetId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"PetId": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"Pet": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/components/schemas/PetId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
lib/schemas/openrpc/codegen/to_code.v
Normal file
55
lib/schemas/openrpc/codegen/to_code.v
Normal file
@@ -0,0 +1,55 @@
|
||||
module codegen
|
||||
|
||||
import freeflowuniverse.herolib.core.code { VFile, CodeItem, CustomCode, Function, Struct, parse_function }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema.codegen as jsonschema_codegen {schemaref_to_type}
|
||||
import freeflowuniverse.herolib.schemas.openrpc {Method, ContentDescriptor}
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
|
||||
// converts OpenRPC Method to Code Function
|
||||
pub fn method_to_function(method Method) !Function {
|
||||
mut params := []code.Param{}
|
||||
for param in method.params {
|
||||
if param is ContentDescriptor {
|
||||
params << content_descriptor_to_parameter(param)!
|
||||
}
|
||||
}
|
||||
result := if method.result is ContentDescriptor {
|
||||
content_descriptor_to_parameter(method.result)!
|
||||
} else {
|
||||
panic('Method must be inflated')
|
||||
}
|
||||
|
||||
return Function{
|
||||
name: texttools.name_fix_pascal_to_snake(method.name)
|
||||
params: params
|
||||
result: result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_descriptor_to_parameter(cd ContentDescriptor) !code.Param {
|
||||
return code.Param{
|
||||
name: cd.name
|
||||
typ: schemaref_to_type(cd.schema)!
|
||||
}
|
||||
}
|
||||
|
||||
// //
|
||||
// pub fn (s Schema) to_struct() code.Struct {
|
||||
// mut attributes := []Attribute{}
|
||||
// if c.depracated {
|
||||
// attributes << Attribute {name: 'deprecated'}
|
||||
// }
|
||||
// if !c.required {
|
||||
// attributes << Attribute {name: 'params'}
|
||||
// }
|
||||
|
||||
// return code.Struct {
|
||||
// name: name
|
||||
// description: summary
|
||||
// required: required
|
||||
// schema: Schema {
|
||||
|
||||
// }
|
||||
// attrs: attributes
|
||||
// }
|
||||
// }
|
||||
85
lib/schemas/openrpc/decode.v
Normal file
85
lib/schemas/openrpc/decode.v
Normal file
@@ -0,0 +1,85 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import x.json2 { Any }
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Reference, decode_schemaref }
|
||||
|
||||
pub fn decode(data string) !OpenRPC {
|
||||
mut object := json.decode(OpenRPC, data) or { return error('Failed to decode json\n${err}') }
|
||||
data_map := json2.raw_decode(data)!.as_map()
|
||||
if 'components' in data_map {
|
||||
object.components = decode_components(data_map) or {
|
||||
return error('Failed to decode components\n${err}')
|
||||
}
|
||||
}
|
||||
|
||||
for i, method in data_map['methods'].arr() {
|
||||
method_map := method.as_map()
|
||||
params_arr := method_map['params'].arr()
|
||||
result := if 'result' in method_map {
|
||||
method_map['result']
|
||||
} else {
|
||||
''
|
||||
}
|
||||
object.methods[i].params = params_arr.map(decode_content_descriptor_ref(it.as_map()) or {
|
||||
return error('Failed to decode params\n${err}')
|
||||
})
|
||||
object.methods[i].result = decode_content_descriptor_ref(result.as_map()) or {
|
||||
return error('Failed to decode result\n${err}')
|
||||
}
|
||||
}
|
||||
// object.methods = decode_method(data_map['methods'].as_array)!
|
||||
return object
|
||||
}
|
||||
|
||||
// fn decode_method(data_map map[string]Any) !Method {
|
||||
// method := Method {
|
||||
// name: data_map['name']
|
||||
// description: data_map['description']
|
||||
// result: json.decode(data_map['result'])
|
||||
// }
|
||||
|
||||
// return method
|
||||
// }
|
||||
|
||||
// fn decode_method_param(data_map map[string]Any) !Method {
|
||||
// method := Method {}
|
||||
|
||||
// return method
|
||||
// }
|
||||
|
||||
fn decode_components(data_map map[string]Any) !Components {
|
||||
mut components := Components{}
|
||||
components_map := data_map['components'].as_map()
|
||||
|
||||
if 'contentDescriptors' in components_map {
|
||||
descriptors_map := components_map['contentDescriptors'].as_map()
|
||||
for key, value in descriptors_map {
|
||||
descriptor := decode_content_descriptor_ref(value.as_map())!
|
||||
components.content_descriptors[key] = descriptor
|
||||
}
|
||||
}
|
||||
|
||||
if 'schemas' in components_map {
|
||||
schemas_map := components_map['schemas'].as_map()
|
||||
for key, value in schemas_map {
|
||||
schema := jsonschema.decode(value.str())!
|
||||
components.schemas[key] = schema
|
||||
}
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
fn decode_content_descriptor_ref(data_map map[string]Any) !ContentDescriptorRef {
|
||||
if '\$ref' in data_map {
|
||||
return Reference{
|
||||
ref: data_map['\$ref'].str()
|
||||
}
|
||||
}
|
||||
mut descriptor := json.decode(ContentDescriptor, data_map.str())!
|
||||
if schema_any := data_map['schema'] {
|
||||
descriptor.schema = decode_schemaref(schema_any.as_map())!
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
18
lib/schemas/openrpc/decode_test.v
Normal file
18
lib/schemas/openrpc/decode_test.v
Normal file
@@ -0,0 +1,18 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import x.json2
|
||||
import os
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.schemas.jsonschema
|
||||
|
||||
const doc_path = '${os.dir(@FILE)}/testdata/openrpc.json'
|
||||
|
||||
fn test_decode() ! {
|
||||
mut doc_file := pathlib.get_file(path: openrpc.doc_path)!
|
||||
content := doc_file.read()!
|
||||
object := decode(content)!
|
||||
|
||||
assert object.openrpc == '1.0.0-rc1'
|
||||
assert object.methods.map(it.name) == ['list_pets', 'create_pet', 'get_pet']
|
||||
}
|
||||
56
lib/schemas/openrpc/encode.v
Normal file
56
lib/schemas/openrpc/encode.v
Normal file
@@ -0,0 +1,56 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import x.json2 { Any }
|
||||
|
||||
// encode encodes an OpenRPC document struct into json string.
|
||||
// eliminates undefined variable by calling prune on the initial encoding.
|
||||
pub fn (doc OpenRPC) encode() !string {
|
||||
encoded := json.encode(doc)
|
||||
raw_decode := json2.raw_decode(encoded)!
|
||||
mut doc_map := raw_decode.as_map()
|
||||
pruned_map := prune(doc_map)
|
||||
return json2.encode_pretty[Any](pruned_map)
|
||||
}
|
||||
|
||||
// prune recursively prunes a map of Any type, pruning map keys where the value is the default value of the variable.
|
||||
// this treats undefined values as null, which is ok for openrpc document encoding.
|
||||
pub fn prune(obj Any) Any {
|
||||
if obj is map[string]Any {
|
||||
mut pruned_map := map[string]Any{}
|
||||
|
||||
for key, val in obj as map[string]Any {
|
||||
if key == '_type' {
|
||||
continue
|
||||
}
|
||||
pruned_val := prune(val)
|
||||
if pruned_val.str() != '' {
|
||||
pruned_map[key] = pruned_val
|
||||
} else if key == 'methods' || key == 'params' {
|
||||
pruned_map[key] = []Any{}
|
||||
}
|
||||
}
|
||||
|
||||
if pruned_map.keys().len != 0 {
|
||||
return pruned_map
|
||||
}
|
||||
} else if obj is []Any {
|
||||
mut pruned_arr := []Any{}
|
||||
|
||||
for val in obj as []Any {
|
||||
pruned_val := prune(val)
|
||||
if pruned_val.str() != '' {
|
||||
pruned_arr << pruned_val
|
||||
}
|
||||
}
|
||||
|
||||
if pruned_arr.len != 0 {
|
||||
return pruned_arr
|
||||
}
|
||||
} else if obj is string {
|
||||
if obj != '' {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
101
lib/schemas/openrpc/encode_test.v
Normal file
101
lib/schemas/openrpc/encode_test.v
Normal file
@@ -0,0 +1,101 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Schema, SchemaRef }
|
||||
|
||||
const blank_openrpc = '{
|
||||
"openrpc": "1.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"methods": []
|
||||
}'
|
||||
|
||||
// test if encode can correctly encode a blank OpenRPC
|
||||
fn test_encode_blank() ! {
|
||||
doc := OpenRPC{
|
||||
info: Info{
|
||||
title: ''
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: []Method{}
|
||||
}
|
||||
encoded := doc.encode()!
|
||||
assert encoded.trim_space().split_into_lines().map(it.trim_space()) == openrpc.blank_openrpc.split_into_lines().map(it.trim_space())
|
||||
}
|
||||
|
||||
// test if can correctly encode an OpenRPC doc with a method
|
||||
fn test_encode_with_method() ! {
|
||||
doc := OpenRPC{
|
||||
info: Info{
|
||||
title: ''
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: [
|
||||
Method{
|
||||
name: 'method_name'
|
||||
summary: 'summary'
|
||||
description: 'description for this method'
|
||||
deprecated: true
|
||||
params: [
|
||||
ContentDescriptor{
|
||||
name: 'sample descriptor'
|
||||
schema: SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
encoded := doc.encode()!
|
||||
assert encoded == '{
|
||||
"openrpc": "1.0.0",
|
||||
"info": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"methods": [
|
||||
{
|
||||
"name": "method_name",
|
||||
"summary": "summary",
|
||||
"description": "description for this method",
|
||||
"params": [
|
||||
{
|
||||
"name": "sample descriptor",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}'
|
||||
}
|
||||
|
||||
// test if can correctly encode a complete OpenRPC doc
|
||||
fn test_encode() ! {
|
||||
doc := OpenRPC{
|
||||
info: Info{
|
||||
title: ''
|
||||
version: '1.0.0'
|
||||
}
|
||||
methods: [
|
||||
Method{
|
||||
name: 'method_name'
|
||||
summary: 'summary'
|
||||
description: 'description for this method'
|
||||
deprecated: true
|
||||
params: [
|
||||
ContentDescriptor{
|
||||
name: 'sample descriptor'
|
||||
schema: SchemaRef(Schema{
|
||||
typ: 'string'
|
||||
})
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
encoded := json.encode(doc)
|
||||
assert encoded == '{"openrpc":"1.0.0","info":{"title":"","version":"1.0.0"},"methods":[{"name":"method_name","summary":"summary","description":"description for this method","params":[{"name":"sample descriptor","schema":{"\$schema":"","\$id":"","title":"","description":"","type":"string","properties":{},"additionalProperties":{},"required":[],"ref":"","items":{},"defs":{},"oneOf":[],"_type":"Schema"},"_type":"ContentDescriptor"}],"result":{},"deprecated":true}]}'
|
||||
}
|
||||
26
lib/schemas/openrpc/factory.v
Normal file
26
lib/schemas/openrpc/factory.v
Normal file
@@ -0,0 +1,26 @@
|
||||
module openrpc
|
||||
|
||||
import os
|
||||
|
||||
@[params]
|
||||
pub struct Params {
|
||||
pub:
|
||||
path string // path to openrpc.json file
|
||||
text string // content of openrpc specification text
|
||||
}
|
||||
|
||||
pub fn new(params Params) !OpenRPC {
|
||||
if params.path == '' && params.text == '' {
|
||||
return OpenRPC{}
|
||||
}
|
||||
|
||||
if params.text != '' && params.path != '' {
|
||||
return error('Either provide path or text')
|
||||
}
|
||||
|
||||
text := if params.path != '' {
|
||||
os.read_file(params.path)!
|
||||
} else { params.text }
|
||||
|
||||
return decode(text)!
|
||||
}
|
||||
71
lib/schemas/openrpc/interface.v
Normal file
71
lib/schemas/openrpc/interface.v
Normal file
@@ -0,0 +1,71 @@
|
||||
module openrpc
|
||||
|
||||
import veb
|
||||
import x.json2
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// Main controller for handling RPC requests
|
||||
pub struct Controller {
|
||||
pub:
|
||||
specification OpenRPC @[required] // OpenRPC specification
|
||||
pub mut:
|
||||
handler Handler @[required] // Handles JSON-RPC requests
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// Creates a new Controller instance
|
||||
pub fn new_controller(c Controller) &Controller {
|
||||
return &Controller{...c}
|
||||
}
|
||||
|
||||
// Parameters for running the server
|
||||
@[params]
|
||||
pub struct RunParams {
|
||||
pub:
|
||||
port int = 8080 // Default to port 8080
|
||||
}
|
||||
|
||||
// Starts the server
|
||||
pub fn (mut c Controller) run(params RunParams) {
|
||||
veb.run[Controller, Context](mut c, 8080)
|
||||
}
|
||||
|
||||
// Handles POST requests at the index endpoint
|
||||
@[post]
|
||||
pub fn (mut c Controller) index(mut ctx Context) veb.Result {
|
||||
req_raw := json2.raw_decode(ctx.req.data) or {
|
||||
return ctx.server_error('Invalid JSON body') // Return error if JSON is malformed
|
||||
}
|
||||
|
||||
req_map := req_raw.as_map() // Converts JSON to a map
|
||||
|
||||
// Create a jsonrpc.Request using the decoded data
|
||||
request := jsonrpc.Request{
|
||||
jsonrpc: req_map['jsonrpc'].str()
|
||||
id: req_map['id'].str()
|
||||
method: req_map['method'].str()
|
||||
params: req_map['params'].str()
|
||||
}
|
||||
|
||||
// Process the request with the handler
|
||||
response := c.handler.handle(request) or {
|
||||
return ctx.server_error('Handler error: ${err.msg}')
|
||||
}
|
||||
|
||||
// Return the handler's response as JSON
|
||||
return ctx.json(response)
|
||||
}
|
||||
|
||||
pub struct Handler {
|
||||
specification OpenRPC
|
||||
pub mut:
|
||||
handler fn(jsonrpc.Request) !jsonrpc.Response
|
||||
}
|
||||
|
||||
// Handle a request and return a response
|
||||
pub fn (h Handler) handle(req jsonrpc.Request) !jsonrpc.Response {
|
||||
return h.handler(req)!
|
||||
}
|
||||
41
lib/schemas/openrpc/interface_test.v
Normal file
41
lib/schemas/openrpc/interface_test.v
Normal file
@@ -0,0 +1,41 @@
|
||||
module openrpc
|
||||
|
||||
import os
|
||||
import veb
|
||||
import x.json2 {Any}
|
||||
import net.http {CommonHeader}
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
const specification_path = os.join_path(os.dir(@FILE), '/testdata/openrpc.json')
|
||||
|
||||
// handler for test echoes JSONRPC Request as JSONRPC Response
|
||||
fn handler(request jsonrpc.Request) !jsonrpc.Response {
|
||||
return jsonrpc.Response {
|
||||
jsonrpc: request.jsonrpc
|
||||
id: request.id
|
||||
result: request.params
|
||||
}
|
||||
}
|
||||
|
||||
fn test_new_server() {
|
||||
specification := new(path: specification_path)!
|
||||
new_controller(
|
||||
specification: specification
|
||||
handler: Handler{
|
||||
specification: specification
|
||||
handler: handler
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn test_run_server() {
|
||||
specification := new(path: specification_path)!
|
||||
mut controller := new_controller(
|
||||
specification: specification
|
||||
handler: Handler{
|
||||
specification: specification
|
||||
handler: handler
|
||||
}
|
||||
)
|
||||
spawn controller.run()
|
||||
}
|
||||
225
lib/schemas/openrpc/model.v
Normal file
225
lib/schemas/openrpc/model.v
Normal file
@@ -0,0 +1,225 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Reference, SchemaRef }
|
||||
|
||||
// This is the root object of the OpenRPC document.
|
||||
// The contents of this object represent a whole OpenRPC document.
|
||||
// How this object is constructed or stored is outside the scope of the OpenRPC Specification.
|
||||
pub struct OpenRPC {
|
||||
pub mut:
|
||||
openrpc string = '1.0.0' // This string MUST be the semantic version number of the OpenRPC Specification version that the OpenRPC document uses.
|
||||
info Info // Provides metadata about the API.
|
||||
servers []Server // An array of Server Objects, which provide connectivity information to a target server.
|
||||
methods []Method // The available methods for the API.
|
||||
components Components // An element to hold various schemas for the specification.
|
||||
external_docs []ExternalDocs @[json: externalDocs] // Additional external documentation.
|
||||
}
|
||||
|
||||
// The object provides metadata about the API.
|
||||
// The metadata MAY be used by the clients if needed, and MAY be presented in editing or documentation generation tools for convenience.
|
||||
pub struct Info {
|
||||
pub:
|
||||
title string // The title of the application.
|
||||
description string // A verbose description of the application.
|
||||
terms_of_service string @[json: termsOfService] // A URL to the Terms of Service for the API. MUST be in the format of a URL.
|
||||
contact Contact @[omitempty] // The contact information for the exposed API.
|
||||
license License @[omitempty] // The license information for the exposed API.
|
||||
version string @[omitempty] // The version of the OpenRPC document (which is distinct from the OpenRPC Specification version or the API implementation version).
|
||||
}
|
||||
|
||||
// Contact information for the exposed API.
|
||||
pub struct Contact {
|
||||
name string @[omitempty] // The identifying name of the contact person/organization.
|
||||
email string @[omitempty] // The URL pointing to the contact information. MUST be in the format of a URL.
|
||||
url string @[omitempty] // The email address of the contact person/organization. MUST be in the format of an email address.
|
||||
}
|
||||
|
||||
// License information for the exposed API.
|
||||
pub struct License {
|
||||
name string @[omitempty] // The license name used for the API.
|
||||
url string @[omitempty] // A URL to the license used for the API. MUST be in the format of a URL.
|
||||
}
|
||||
|
||||
// An object representing a Server.
|
||||
// TODO: make variables field optional bug fixed: https://github.com/vlang/v/issues/18000
|
||||
// TODO: server name is required but not for version 1.0.0
|
||||
pub struct Server {
|
||||
pub:
|
||||
name string @[omitempty] // A name to be used as the cannonical name for the server.
|
||||
url RuntimeExpression @[omitempty] // A URL to the target host. This URL supports Server Variables and MAY be relative, to indicate that the host location is relative to the location where the OpenRPC document is being served. Server Variables are passed into the Runtime Expression to produce a server URL.
|
||||
summary string @[omitempty] // A short summary of what the server is.
|
||||
description string @[omitempty] // An optional string describing the host designated by the URL.
|
||||
variables map[string]ServerVariable @[omitempty] // A map between a variable name and its value. The value is passed into the Runtime Expression to produce a server URL.
|
||||
}
|
||||
|
||||
// An object representing a Server Variable for server URL template substitution.
|
||||
pub struct ServerVariable {
|
||||
enum_ []string @[json: 'enum'] // An enumeration of string values to be used if the substitution options are from a limited set.
|
||||
default_ string @[json: 'default'; required] // The default value to use for substitution, which SHALL be sent if an alternate value is not supplied. Note this behavior is different than the Schema Object’s treatment of default values, because in those cases parameter values are optional.
|
||||
description string @[omitempty] // An optional description for the server variable. GitHub Flavored Markdown syntax MAY be used for rich text representation.
|
||||
}
|
||||
|
||||
// Describes the interface for the given method name. The method name is used as the method field of the JSON-RPC body. It therefore MUST be unique.
|
||||
// TODO: make result optional once issue is solved: https://github.com/vlang/v/issues/18001
|
||||
pub struct Method {
|
||||
pub mut:
|
||||
name string @[omitempty] // The cannonical name for the method. The name MUST be unique within the methods array.
|
||||
tags []TagRef @[omitempty] // A list of tags for API documentation control. Tags can be used for logical grouping of methods by resources or any other qualifier.
|
||||
summary string @[omitempty] // A short summary of what the method does.
|
||||
description string @[omitempty] // A verbose explanation of the method behavior.
|
||||
external_docs ExternalDocs @[json: externalDocs; omitempty] // Additional external documentation for this method.
|
||||
params []ContentDescriptorRef @[omitempty] // A list of parameters that are applicable for this method. The list MUST NOT include duplicated parameters and therefore require name to be unique. The list can use the Reference Object to link to parameters that are defined by the Content Descriptor Object. All optional params (content descriptor objects with “required”: false) MUST be positioned after all required params in the list.
|
||||
result ContentDescriptorRef @[omitempty] // The description of the result returned by the method. If defined, it MUST be a Content Descriptor or Reference Object. If undefined, the method MUST only be used as a notification.
|
||||
deprecated bool @[omitempty] // Declares this method to be deprecated. Consumers SHOULD refrain from usage of the declared method. Default value is false.
|
||||
servers []Server @[omitempty] // An alternative servers array to service this method. If an alternative servers array is specified at the Root level, it will be overridden by this value.
|
||||
errors []ErrorRef @[omitempty] // A list of custom application defined errors that MAY be returned. The Errors MUST have unique error codes.
|
||||
links []LinkRef @[omitempty] // A list of possible links from this method call.
|
||||
param_structure ParamStructure @[json: paramStructure; omitempty] // The expected format of the parameters. As per the JSON-RPC 2.0 specification, the params of a JSON-RPC request object may be an array, object, or either (represented as by-position, by-name, and either respectively). When a method has a paramStructure value of by-name, callers of the method MUST send a JSON-RPC request object whose params field is an object. Further, the key names of the params object MUST be the same as the contentDescriptor.names for the given method. Defaults to "either".
|
||||
examples []ExamplePairing @[omitempty] // Array of Example Pairing Object where each example includes a valid params-to-result Content Descriptor pairing.
|
||||
}
|
||||
|
||||
enum ParamStructure {
|
||||
either
|
||||
by_name
|
||||
by_position
|
||||
}
|
||||
|
||||
pub type ContentDescriptorRef = ContentDescriptor | Reference
|
||||
|
||||
// Content Descriptors are objects that do just as they suggest - describe content.
|
||||
// They are reusable ways of describing either parameters or result.
|
||||
// They MUST have a schema.
|
||||
pub struct ContentDescriptor {
|
||||
pub mut:
|
||||
name string @[omitempty] // Name of the content that is being described. If the content described is a method parameter assignable by-name, this field SHALL define the parameter’s key (ie name).
|
||||
summary string @[omitempty] // A short summary of the content that is being described.
|
||||
description string @[omitempty] // A verbose explanation of the content descriptor behavior.
|
||||
required bool @[omitempty] // Determines if the content is a required field. Default value is false.
|
||||
schema SchemaRef @[omitempty] // Schema that describes the content.
|
||||
deprecated bool @[omitempty] // Specifies that the content is deprecated and SHOULD be transitioned out of usage. Default value is false.
|
||||
}
|
||||
|
||||
// The Example Pairing object consists of a set of example params and result.
|
||||
// The result is what you can expect from the JSON-RPC service given the exact params.
|
||||
pub struct ExamplePairing {
|
||||
pub mut:
|
||||
name string @[omitempty] // Name for the example pairing.
|
||||
description string @[omitempty] // A verbose explanation of the example pairing.
|
||||
summary string @[omitempty] // Short description for the example pairing.
|
||||
params []ExampleRef @[omitempty] // Example parameters.
|
||||
result ExampleRef @[omitempty] // Example result. When undefined, the example pairing represents usage of the method as a notification.
|
||||
}
|
||||
|
||||
type ExampleRef = Example | Reference
|
||||
|
||||
// The Example object is an object that defines an example that is intended to match the schema of a given Content Descriptor.
|
||||
// Question: how to handle any type for value?
|
||||
pub struct Example {
|
||||
name string @[omitempty] // Cannonical name of the example.
|
||||
summary string @[omitempty] // Short description for the example.
|
||||
description string @[omitempty] // A verbose explanation of the example.
|
||||
value string @[omitempty] // Embedded literal example. The value field and externalValue field are mutually exclusive. To represent examples of media types that cannot naturally represented in JSON, use a string value to contain the example, escaping where necessary.
|
||||
external_value string @[json: externalValue; omitempty] // A URL that points to the literal example. This provides the capability to reference examples that cannot easily be included in JSON documents. The value field and externalValue field are mutually exclusive.
|
||||
}
|
||||
|
||||
type LinkRef = Link | Reference
|
||||
|
||||
// The Link object represents a possible design-time link for a result. The presence of a link does not guarantee the caller’s ability to successfully invoke it, rather it provides a known relationship and traversal mechanism between results and other methods.
|
||||
// Unlike dynamic links (i.e. links provided in the result payload), the OpenRPC linking mechanism does not require link information in the runtime result.
|
||||
// For computing links, and providing instructions to execute them, a runtime expression is used for accessing values in an method and using them as parameters while invoking the linked method.
|
||||
// TODO: change params map's value type to LinkParam once issue is solved: https://github.com/vlang/v/issues/18002
|
||||
pub struct Link {
|
||||
name string // Cannonical name of the link.
|
||||
description string // A description of the link.
|
||||
summary string // Short description for the link.
|
||||
method string // The name of an existing, resolvable OpenRPC method, as defined with a unique method. This field MUST resolve to a unique Method Object. As opposed to Open Api, Relative method values ARE NOT permitted.
|
||||
params map[string]string // A map representing parameters to pass to a method as specified with method. The key is the parameter name to be used, whereas the value can be a constant or a runtime expression to be evaluated and passed to the linked method.
|
||||
server Server // A server object to be used by the target method.
|
||||
}
|
||||
|
||||
// // TODO: handle any, normally should be Any | RuntimeExpression not string
|
||||
// type LinkParam = string | RuntimeExpression
|
||||
|
||||
// Runtime expressions allow the user to define an expression which will evaluate to a string once the desired value(s) are known. They are used when the desired value of a link or server can only be constructed at run time. This mechanism is used by Link Objects and Server Variables.
|
||||
// The runtime expression makes use of JSON Template Language syntax.
|
||||
// The table below provides examples of runtime expressions and examples of their use in a value:
|
||||
// Runtime expressions preserve the type of the referenced value.
|
||||
type RuntimeExpression = string
|
||||
|
||||
pub type ErrorRef = ErrorSpec | Reference
|
||||
|
||||
// TODO: handle any type for data field
|
||||
// Defines an application level error.
|
||||
pub struct ErrorSpec {
|
||||
pub:
|
||||
code int // A Number that indicates the error type that occurred. This MUST be an integer. The error codes from and including -32768 to -32000 are reserved for pre-defined errors. These pre-defined errors SHOULD be assumed to be returned from any JSON-RPC api.
|
||||
message string // A String providing a short description of the error. The message SHOULD be limited to a concise single sentence.
|
||||
data SchemaRef // A Primitive or Structured value that contains additional information about the error. This may be omitted. The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.).
|
||||
}
|
||||
|
||||
// TODO: enforce regex requirements
|
||||
// Holds a set of reusable objects for different aspects of the OpenRPC.
|
||||
// All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object.
|
||||
// All the fixed fields declared above are objects that MUST use keys that match the regular expression: ^[a-zA-Z0-9\.\-_]+$
|
||||
pub struct Components {
|
||||
pub mut:
|
||||
content_descriptors map[string]ContentDescriptorRef @[json: contentDescriptors] // An object to hold reusable Content Descriptor Objects.
|
||||
schemas map[string]SchemaRef // An object to hold reusable Schema Objects.
|
||||
examples map[string]Example // An object to hold reusable Example Objects.
|
||||
links map[string]Link // An object to hold reusable Link Objects.
|
||||
error map[string]Error // An object to hold reusable Error Objects.
|
||||
example_pairing_objects map[string]ExamplePairing @[json: examplePairingObjects] // An object to hold reusable Example Pairing Objects.
|
||||
tags map[string]Tag // An object to hold reusable Tag Objects.
|
||||
}
|
||||
|
||||
type TagRef = Reference | Tag
|
||||
|
||||
// Adds metadata to a single tag that is used by the Method Object.
|
||||
// It is not mandatory to have a Tag Object per tag defined in the Method Object instances.
|
||||
pub struct Tag {
|
||||
name string // The name of the tag.
|
||||
summary string // A short summary of the tag.
|
||||
description string // A verbose explanation for the tag.
|
||||
external_docs ExternalDocs @[json: externalDocs] // Additional external documentation for this tag.
|
||||
}
|
||||
|
||||
// Allows referencing an external resource for extended documentation.
|
||||
pub struct ExternalDocs {
|
||||
description string // A verbose explanation of the target documentation.
|
||||
url string // The URL for the target documentation. Value MUST be in the format of a URL.
|
||||
}
|
||||
|
||||
pub struct Property {
|
||||
pub:
|
||||
description string @[omitempty] // A description of the property.
|
||||
typ string @[json: 'type'; omitempty] // The type of the property.
|
||||
exclusive_minimum int @[json: exclusiveMinimum; omitempty] // If the value of the property is a number, this defines the minimum value allowed.
|
||||
min_items int @[json: minItems; omitempty] // If the value of the property is an array, this defines the minimum items allowed.
|
||||
unique_items bool @[json: uniqueItems; omitempty] // If the value of the property is an array, this determines if the values in the array MUST be unique.
|
||||
}
|
||||
|
||||
// todo: implement specification extensions
|
||||
|
||||
// pub struct Property {
|
||||
// description string
|
||||
// typ string [json: 'type']
|
||||
// exclusive_minimum int [json: exclusiveMinimum]
|
||||
// min_items int [json: minItems]
|
||||
// unique_items bool [json: uniqueItems]
|
||||
// }
|
||||
|
||||
// pub struct Value {
|
||||
// versions []Version
|
||||
// }
|
||||
|
||||
// pub struct Version {
|
||||
// status string
|
||||
// updated time.Time
|
||||
// id string
|
||||
// urls []URL
|
||||
// }
|
||||
|
||||
// pub struct URL {
|
||||
// href string
|
||||
// rel string
|
||||
// }
|
||||
92
lib/schemas/openrpc/parse_example.v
Normal file
92
lib/schemas/openrpc/parse_example.v
Normal file
@@ -0,0 +1,92 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonschema { Reference }
|
||||
import freeflowuniverse.herolib.core.code { Struct, StructField }
|
||||
import json
|
||||
import x.json2
|
||||
|
||||
pub fn parse_example_pairing(text_ string) !ExamplePairing {
|
||||
if !text_.contains('Example:') {
|
||||
return error('no example found fitting format')
|
||||
}
|
||||
mut text := text_.all_after('Example:').trim_space()
|
||||
|
||||
mut pairing := ExamplePairing{}
|
||||
|
||||
if text.contains('assert') {
|
||||
pairing.name = if text.all_before('assert').trim_space() != '' {
|
||||
text.all_before('assert').trim_space()
|
||||
} else {
|
||||
text.all_after('assert').all_before('(').trim_space()
|
||||
}
|
||||
value := text.all_after('==').all_before('//').trim_space()
|
||||
pairing.params = parse_pairing_params(text.all_after('(').all_before(')'))
|
||||
pairing.result = parse_pairing_result(text)
|
||||
description := text.all_after('//').trim_space()
|
||||
}
|
||||
|
||||
return pairing
|
||||
}
|
||||
|
||||
pub fn parse_struct_example(structure Struct) Example {
|
||||
mut val_map := map[string]json2.Any{}
|
||||
for field in structure.fields {
|
||||
example_attr := field.attrs.filter(it.name == 'example')
|
||||
example_val := if example_attr.len == 1 {
|
||||
example_attr[0].arg
|
||||
} else {
|
||||
generate_example_val(field)
|
||||
}
|
||||
val_map[field.name] = example_val
|
||||
}
|
||||
return Example{
|
||||
name: '${structure.name}Example'
|
||||
value: json2.encode(val_map)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_example_val(field StructField) string {
|
||||
return ''
|
||||
}
|
||||
|
||||
pub fn parse_pairing_params(text_ string) []ExampleRef {
|
||||
mut text := text_.trim_space()
|
||||
mut examples := []ExampleRef{}
|
||||
|
||||
if text.is_lower() && !text.contains("'") && !text.contains('"') {
|
||||
examples << Reference{text}
|
||||
} else if text.contains(':') {
|
||||
examples << Example{
|
||||
name: ''
|
||||
value: json.encode(text)
|
||||
}
|
||||
} else {
|
||||
examples1 := text.split(',').map(it.trim_space()).map(ExampleRef(Example{
|
||||
name: ''
|
||||
value: it
|
||||
}))
|
||||
|
||||
examples << examples1
|
||||
}
|
||||
return examples
|
||||
}
|
||||
|
||||
pub fn parse_pairing_result(text_ string) ExampleRef {
|
||||
mut text := text_.trim_space()
|
||||
|
||||
if text.is_lower() {
|
||||
return Reference{text}
|
||||
} else if text.contains(':') {
|
||||
return Example{
|
||||
name: ''
|
||||
value: json.encode(text)
|
||||
}
|
||||
}
|
||||
return Example{}
|
||||
}
|
||||
|
||||
// pub fn parse_example(text string) openrpc.Example {
|
||||
// return Example{
|
||||
|
||||
// }
|
||||
// }
|
||||
58
lib/schemas/openrpc/parse_example_test.v
Normal file
58
lib/schemas/openrpc/parse_example_test.v
Normal file
@@ -0,0 +1,58 @@
|
||||
module openrpc
|
||||
|
||||
import freeflowuniverse.herolib.core.code { Attribute, Struct, StructField, Type }
|
||||
|
||||
const example_txt = "
|
||||
Example: Get pet example.
|
||||
assert some_function('input_string') == 'output_string'
|
||||
"
|
||||
|
||||
// "examples": [
|
||||
// {
|
||||
// "name": "getPetExample",
|
||||
// "description": "get pet example",
|
||||
// "params": [
|
||||
// {
|
||||
// "name": "petId",
|
||||
// "value": 7
|
||||
// }
|
||||
// ],
|
||||
// "result": {
|
||||
// "name": "getPetExampleResult",
|
||||
// "value": {
|
||||
// "name": "fluffy",
|
||||
// "tag": "poodle",
|
||||
// "id": 7
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
fn test_parse_example_pairing() ! {
|
||||
example := parse_example_pairing(openrpc.example_txt)!
|
||||
params := example.params
|
||||
assert params.len == 1
|
||||
param0 := (params[0] as Example)
|
||||
assert param0.value == "'input_string'"
|
||||
}
|
||||
|
||||
const test_struct = Struct{
|
||||
name: 'TestStruct'
|
||||
fields: [
|
||||
StructField{
|
||||
name: 'TestField'
|
||||
typ: Type{
|
||||
symbol: 'int'
|
||||
}
|
||||
attrs: [Attribute{
|
||||
name: 'example'
|
||||
arg: '21'
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn test_parse_struct_example() ! {
|
||||
example := parse_struct_example(openrpc.test_struct)
|
||||
// assert example.name == 'TestStructExample'
|
||||
// panic(example)
|
||||
}
|
||||
96
lib/schemas/openrpc/playground.v
Normal file
96
lib/schemas/openrpc/playground.v
Normal file
@@ -0,0 +1,96 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
// import freeflowuniverse.herolib.develop.gittools
|
||||
import freeflowuniverse.herolib.core.pathlib
|
||||
import freeflowuniverse.herolib.core.texttools
|
||||
import vweb
|
||||
import os
|
||||
|
||||
pub struct Playground {
|
||||
vweb.Context
|
||||
build pathlib.Path @[vweb_global]
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct PlaygroundConfig {
|
||||
pub:
|
||||
dest pathlib.Path @[required]
|
||||
specs []pathlib.Path
|
||||
}
|
||||
|
||||
pub fn export_playground(config PlaygroundConfig) ! {
|
||||
// tw := tailwind.new(
|
||||
// name: 'publisher'
|
||||
// path_build: '${os.dir(@FILE)}'
|
||||
// content_paths: [
|
||||
// '${os.dir(@FILE)}/templates/**/*.html',
|
||||
// ]
|
||||
// )!
|
||||
// css_source := '${os.dir(@FILE)}/templates/css/index.css'
|
||||
// css_dest := '${os.dir(@FILE)}/static/css/index.css'
|
||||
// tw.compile(css_source, css_dest)!
|
||||
// mut gs := gittools.new() or { panic(err) }
|
||||
// mut repo := gs.get_repo(url: 'https://github.com/freeflowuniverse/playground')!
|
||||
|
||||
// playground_dir := repo.get_path()!
|
||||
|
||||
// mut project := npm.new_project(playground_dir)!
|
||||
// project.install()
|
||||
// // export_examples(config.specs, '${playground_dir.path}/src')!
|
||||
// project.build()!
|
||||
// project.export(config.dest)!
|
||||
}
|
||||
|
||||
const build_path = '${os.dir(@FILE)}/playground'
|
||||
|
||||
pub fn new_playground(config PlaygroundConfig) !&Playground {
|
||||
build_dir := pathlib.get_dir(path: openrpc.build_path)!
|
||||
mut pg := Playground{
|
||||
build: build_dir
|
||||
}
|
||||
pg.serve_examples(config.specs) or { return error('failed to serve examples:\n${err}') }
|
||||
pg.mount_static_folder_at('${build_dir.path}/static', '/static')
|
||||
|
||||
mut env_file := pathlib.get_file(path: '${build_dir.path}/env.js')!
|
||||
env_file.write(encode_env(config.specs)!)!
|
||||
pg.serve_static('/env.js', env_file.path)
|
||||
return &pg
|
||||
}
|
||||
|
||||
struct ExampleSpec {
|
||||
name string
|
||||
url string
|
||||
}
|
||||
|
||||
fn encode_env(specs_ []pathlib.Path) !string {
|
||||
mut specs := specs_.clone()
|
||||
mut examples := []ExampleSpec{}
|
||||
|
||||
for mut spec in specs {
|
||||
o := decode(spec.read()!)!
|
||||
name := texttools.name_fix(o.info.title)
|
||||
examples << ExampleSpec{
|
||||
name: name
|
||||
url: '/specs/${name}.json'
|
||||
}
|
||||
}
|
||||
mut examples_str := "window._env_ = { ACTORS: '${json.encode(examples)}' }"
|
||||
return examples_str
|
||||
}
|
||||
|
||||
fn (mut pg Playground) serve_examples(specs_ []pathlib.Path) ! {
|
||||
mut specs := specs_.clone()
|
||||
for mut spec in specs {
|
||||
o := decode(spec.read()!) or {
|
||||
return error('Failed to decode OpenRPC Spec ${spec}:\n${err}')
|
||||
}
|
||||
name := texttools.name_fix(o.info.title)
|
||||
pg.serve_static('/specs/${name}.json', spec.path)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn (mut pg Playground) index() vweb.Result {
|
||||
mut index := pathlib.get_file(path: '${pg.build.path}/index.html') or { panic(err) }
|
||||
return pg.html(index.read() or { panic(err) })
|
||||
}
|
||||
3
lib/schemas/openrpc/testdata/method_plain.v
vendored
Normal file
3
lib/schemas/openrpc/testdata/method_plain.v
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module main
|
||||
|
||||
pub fn method_plain() {}
|
||||
5
lib/schemas/openrpc/testdata/method_with_description.v
vendored
Normal file
5
lib/schemas/openrpc/testdata/method_with_description.v
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
module main
|
||||
|
||||
// method_with_description shows that the parser can parse a method's description
|
||||
// from its comments, even if the comments are multiline.
|
||||
pub fn method_with_description(name string) {}
|
||||
31
lib/schemas/openrpc/testdata/methods.v
vendored
Normal file
31
lib/schemas/openrpc/testdata/methods.v
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
module main
|
||||
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
|
||||
pub fn plain_method() {
|
||||
}
|
||||
|
||||
pub fn method_with_body() {
|
||||
console.print_debug('Example method evoked.')
|
||||
}
|
||||
|
||||
// this method has a comment
|
||||
pub fn method_with_comment() {
|
||||
}
|
||||
|
||||
pub fn method_with_param(param string) {
|
||||
}
|
||||
|
||||
pub fn method_with_return() string {
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct Params {
|
||||
param1 int
|
||||
param2 string
|
||||
param3 []int
|
||||
param4 []string
|
||||
}
|
||||
|
||||
pub fn method_with_params(a Params) {
|
||||
}
|
||||
205
lib/schemas/openrpc/testdata/openrpc.json
vendored
Normal file
205
lib/schemas/openrpc/testdata/openrpc.json
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"openrpc": "1.0.0-rc1",
|
||||
"info": {
|
||||
"version": "1.0.0",
|
||||
"title": "Petstore",
|
||||
"license": {
|
||||
"name": "MIT"
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"name": "localhost",
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
],
|
||||
"methods": [
|
||||
{
|
||||
"name": "list_pets",
|
||||
"summary": "List all pets",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "How many items to return at one time (max 100)",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pets",
|
||||
"description": "A paged array of pets",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pets"
|
||||
}
|
||||
},
|
||||
"errors": [
|
||||
{
|
||||
"code": 100,
|
||||
"message": "pets busy"
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"name": "listPetExample",
|
||||
"description": "List pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "limit",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "listPetResultExample",
|
||||
"value": [
|
||||
{
|
||||
"id": 7,
|
||||
"name": "fluffy",
|
||||
"tag": "poodle"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "create_pet",
|
||||
"summary": "Create a pet",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"name": "newPetName",
|
||||
"description": "Name of pet to create",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "newPetTag",
|
||||
"description": "Pet tag to create",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"examples": [
|
||||
{
|
||||
"name": "createPetExample",
|
||||
"description": "Create pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "newPetName",
|
||||
"value": "fluffy"
|
||||
},
|
||||
{
|
||||
"name": "tag",
|
||||
"value": "poodle"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "listPetResultExample",
|
||||
"value": 7
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"$ref": "#/components/contentDescriptors/PetId"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "get_pet",
|
||||
"summary": "Info for a specific pet",
|
||||
"tags": [
|
||||
{
|
||||
"name": "pets"
|
||||
}
|
||||
],
|
||||
"params": [
|
||||
{
|
||||
"$ref": "#/components/contentDescriptors/PetId"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet",
|
||||
"description": "Expected response to a valid request",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "getPetExample",
|
||||
"description": "get pet example",
|
||||
"params": [
|
||||
{
|
||||
"name": "petId",
|
||||
"value": 7
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "getPetExampleResult",
|
||||
"value": {
|
||||
"name": "fluffy",
|
||||
"tag": "poodle",
|
||||
"id": 7
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"contentDescriptors": {
|
||||
"PetId": {
|
||||
"name": "petId",
|
||||
"required": true,
|
||||
"description": "The id of the pet to retrieve",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PetId"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"PetId": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"Pet": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/components/schemas/PetId"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
lib/schemas/openrpc/testdata/petstore_client/README.md
vendored
Normal file
2
lib/schemas/openrpc/testdata/petstore_client/README.md
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
This is a mock client to the Pet Store JSON-RPC API, described by the PetStore OpenRPC Document.
|
||||
The client has comments that are copied from the PetStore OpenRPC Document to demonstrate that document generation from the client results in a similar OpenRPC Document.
|
||||
19
lib/schemas/openrpc/testdata/petstore_client/client.v
vendored
Normal file
19
lib/schemas/openrpc/testdata/petstore_client/client.v
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
module petstore_client
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc { JsonRpcRequest }
|
||||
import net.websocket
|
||||
|
||||
struct Client {
|
||||
mut:
|
||||
ws_client &websocket.Client
|
||||
}
|
||||
|
||||
pub fn new() !Client {
|
||||
address := 'localhost:8000'
|
||||
ws_client := websocket.new_client(address)!
|
||||
return Client{ws_client}
|
||||
}
|
||||
|
||||
fn (mut client Client) send_rpc[T](rpc JsonRpcRequest[T]) ! {
|
||||
client.ws_client.write_string(rpc.to_json())
|
||||
}
|
||||
36
lib/schemas/openrpc/testdata/petstore_client/methods.v
vendored
Normal file
36
lib/schemas/openrpc/testdata/petstore_client/methods.v
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
module petstore_client
|
||||
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
// get_pets finds pets in the system that the user has access to by tags and within a limit
|
||||
// - tags: tags to filter by
|
||||
// - limit: maximum number of results to return
|
||||
// returns pet_list, all pets from the system, that mathes the tags
|
||||
pub fn (mut client Client) get_pets(tags []string, limit int) []Pet {
|
||||
return []Pet{}
|
||||
}
|
||||
|
||||
@[params]
|
||||
struct NewPet {
|
||||
name string @[required]
|
||||
tag string
|
||||
}
|
||||
|
||||
// create_pet creates a new pet in the store. Duplicates are allowed.
|
||||
// - new_pet: Pet to add to the store.
|
||||
// returns pet, the newly created pet
|
||||
pub fn (mut client Client) create_pet(new_pet NewPet) Pet {
|
||||
return Pet{}
|
||||
}
|
||||
|
||||
// get_pet_by_id gets a pet based on a single ID, if the user has access to the pet
|
||||
// - id: ID of pet to fetch
|
||||
// returns pet, pet response
|
||||
pub fn (mut client Client) get_pet_by_id(id int) Pet {
|
||||
return Pet{}
|
||||
}
|
||||
|
||||
// delete_pet_by_id deletes a single pet based on the ID supplied
|
||||
// - id: ID of pet to delete
|
||||
// returns pet, pet deleted
|
||||
pub fn (mut client Client) delete_pet_by_id(id int) {}
|
||||
8
lib/schemas/openrpc/testdata/petstore_client/model.v
vendored
Normal file
8
lib/schemas/openrpc/testdata/petstore_client/model.v
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
module petstore_client
|
||||
|
||||
// a pet struct that represents a pet
|
||||
struct Pet {
|
||||
name string // name of the pet
|
||||
tag string // a tag of the pet, helps finding pet
|
||||
id int // unique indentifier
|
||||
}
|
||||
132
lib/schemas/openrpc/testdata/petstore_client/openrpc.json
vendored
Normal file
132
lib/schemas/openrpc/testdata/petstore_client/openrpc.json
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"openrpc": "1.0.0",
|
||||
"info": {
|
||||
"title": "PetStore API",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"methods": [
|
||||
{
|
||||
"name": "petstore_client.GetPets",
|
||||
"description": "finds pets in the system that the user has access to by tags and within a limit",
|
||||
"params": [
|
||||
{
|
||||
"name": "tags",
|
||||
"description": "tags to filter by",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "maximum number of results to return",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet_list",
|
||||
"description": "all pets from the system, that mathes the tags",
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/Pet"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "petstore_client.CreatePet",
|
||||
"description": "creates a new pet in the store. Duplicates are allowed.",
|
||||
"params": [
|
||||
{
|
||||
"name": "new_pet",
|
||||
"description": "Pet to add to the store.",
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/NewPet"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet",
|
||||
"description": "the newly created pet",
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/Pet"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "petstore_client.GetPetById",
|
||||
"description": "gets a pet based on a single ID, if the user has access to the pet",
|
||||
"params": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of pet to fetch",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet",
|
||||
"description": "pet response",
|
||||
"schema": {
|
||||
"$ref": "#\/components\/schemas\/Pet"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "petstore_client.DeletePetById",
|
||||
"description": "deletes a single pet based on the ID supplied",
|
||||
"params": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "ID of pet to delete",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"name": "pet",
|
||||
"description": "pet deleted",
|
||||
"schema": {
|
||||
"type": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"components": {
|
||||
"schemas": {
|
||||
"NewPet": {
|
||||
"title": "NewPet",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pet": {
|
||||
"title": "Pet",
|
||||
"description": "a pet struct that represents a pet",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "name of the pet",
|
||||
"type": "string"
|
||||
},
|
||||
"tag": {
|
||||
"description": "a tag of the pet, helps finding pet",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "unique indentifier",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user