...
This commit is contained in:
110
lib/hero/heroserver/api_handler.v
Normal file
110
lib/hero/heroserver/api_handler.v
Normal file
@@ -0,0 +1,110 @@
|
||||
module heroserver
|
||||
|
||||
import net.http
|
||||
import json
|
||||
import veb
|
||||
|
||||
// Setup API routes
|
||||
pub fn (mut server HeroServer) setup_api_routes() ! {
|
||||
// Authentication endpoints
|
||||
server.app.register_controller[AuthController, Context]('/auth', mut &AuthController{server: server})!
|
||||
|
||||
// API endpoints for each handler type
|
||||
for handler_type, _ in server.handlers {
|
||||
controller := &APIController{
|
||||
server: server
|
||||
handler_type: handler_type
|
||||
}
|
||||
server.app.register_controller[APIController, Context]('/api/${handler_type}', mut controller)!
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication controller
|
||||
pub struct AuthController {
|
||||
mut:
|
||||
server &HeroServer
|
||||
}
|
||||
|
||||
@[post; '/register']
|
||||
pub fn (mut controller AuthController) register(mut ctx Context) veb.Result {
|
||||
// Parse JSON request
|
||||
request := json.decode(RegisterRequest, ctx.req.data) or {
|
||||
return ctx.request_error('Invalid JSON format')
|
||||
}
|
||||
|
||||
// Register public key
|
||||
controller.server.register(request.pubkey) or {
|
||||
return ctx.request_error('Registration failed: ${err}')
|
||||
}
|
||||
|
||||
return ctx.json({'status': 'success'})
|
||||
}
|
||||
|
||||
@[post; '/authreq']
|
||||
pub fn (mut controller AuthController) auth_request(mut ctx Context) veb.Result {
|
||||
// Parse JSON request
|
||||
request := json.decode(AuthRequest, ctx.req.data) or {
|
||||
return ctx.request_error('Invalid JSON format')
|
||||
}
|
||||
|
||||
// Generate challenge
|
||||
response := controller.server.auth_request(request.pubkey) or {
|
||||
return ctx.request_error('Auth request failed: ${err}')
|
||||
}
|
||||
|
||||
return ctx.json(response)
|
||||
}
|
||||
|
||||
@[post; '/auth']
|
||||
pub fn (mut controller AuthController) auth_submit(mut ctx Context) veb.Result {
|
||||
// Parse JSON request
|
||||
request := json.decode(AuthSubmitRequest, ctx.req.data) or {
|
||||
return ctx.request_error('Invalid JSON format')
|
||||
}
|
||||
|
||||
// Verify and create session
|
||||
response := controller.server.auth_submit(request.pubkey, request.signature) or {
|
||||
return ctx.request_error('Authentication failed: ${err}')
|
||||
}
|
||||
|
||||
return ctx.json(response)
|
||||
}
|
||||
|
||||
// API controller for specific handler types
|
||||
pub struct APIController {
|
||||
mut:
|
||||
server &HeroServer
|
||||
handler_type string
|
||||
}
|
||||
|
||||
@[post; '/:method']
|
||||
pub fn (mut controller APIController) handle_api_call(mut ctx Context, method string) veb.Result {
|
||||
// Extract session key from header
|
||||
session_key := ctx.get_header(.authorization) or {
|
||||
return ctx.request_error('Missing session key in Authorization header')
|
||||
}.replace('Bearer ', '')
|
||||
|
||||
// Validate session
|
||||
controller.server.validate_session(session_key) or {
|
||||
return ctx.server_error('Invalid or expired session: ${err}')
|
||||
}
|
||||
|
||||
// Get handler
|
||||
handler := controller.server.handlers[controller.handler_type] or {
|
||||
return ctx.server_error('Handler not found')
|
||||
}
|
||||
|
||||
// Parse request parameters
|
||||
params := if ctx.req.data.len > 0 {
|
||||
json.decode(map[string]string, ctx.req.data) or { map[string]string{} }
|
||||
} else {
|
||||
map[string]string{}
|
||||
}
|
||||
|
||||
// Call handler method
|
||||
result := handler.call_method(method, params) or {
|
||||
return ctx.server_error('Method call failed: ${err}')
|
||||
}
|
||||
|
||||
return ctx.json(result)
|
||||
}
|
||||
107
lib/hero/heroserver/auth.v
Normal file
107
lib/hero/heroserver/auth.v
Normal file
@@ -0,0 +1,107 @@
|
||||
module heroserver
|
||||
|
||||
import crypto.md5
|
||||
import crypto.rand
|
||||
import time
|
||||
import encoding.base64
|
||||
|
||||
// Active challenges storage
|
||||
mut challenges := map[string]AuthChallenge{}
|
||||
|
||||
// Register a public key (currently just validates format)
|
||||
pub fn (mut server HeroServer) register(pubkey string) ! {
|
||||
// Validate public key format
|
||||
if pubkey.len < 10 {
|
||||
return error('Invalid public key format')
|
||||
}
|
||||
|
||||
// For now, just return success
|
||||
// In future versions, could store registered keys
|
||||
}
|
||||
|
||||
// Request authentication challenge
|
||||
pub fn (mut server HeroServer) auth_request(pubkey string) !AuthResponse {
|
||||
// Generate random challenge data
|
||||
random_bytes := rand.bytes(32)!
|
||||
challenge_data := '${pubkey}:${random_bytes.hex()}:${time.now().unix}'
|
||||
|
||||
// Create MD5 hash of challenge
|
||||
challenge := md5.hexhash(challenge_data)
|
||||
|
||||
// Store challenge with expiration
|
||||
challenges[pubkey] = AuthChallenge{
|
||||
pubkey: pubkey
|
||||
challenge: challenge
|
||||
created_at: time.now()
|
||||
expires_at: time.now().add_seconds(300) // 5 minute expiry
|
||||
}
|
||||
|
||||
return AuthResponse{
|
||||
challenge: challenge
|
||||
}
|
||||
}
|
||||
|
||||
// Submit signed challenge for authentication
|
||||
pub fn (mut server HeroServer) auth_submit(pubkey string, signature string) !AuthSubmitResponse {
|
||||
// Get stored challenge
|
||||
challenge_data := challenges[pubkey] or {
|
||||
return error('No active challenge for this public key')
|
||||
}
|
||||
|
||||
// Check if challenge expired
|
||||
if time.now() > challenge_data.expires_at {
|
||||
challenges.delete(pubkey)
|
||||
return error('Challenge expired')
|
||||
}
|
||||
|
||||
// Verify signature using HeroCrypt
|
||||
// Note: We need the verification key, which should be derived from pubkey
|
||||
// For now, assume pubkey is the verification key in correct format
|
||||
is_valid := server.crypto_client.verify(pubkey, challenge_data.challenge, signature)!
|
||||
|
||||
if !is_valid {
|
||||
return error('Invalid signature')
|
||||
}
|
||||
|
||||
// Generate session key
|
||||
session_data := '${pubkey}:${time.now().unix}:${rand.bytes(16)!.hex()}'
|
||||
session_key := md5.hexhash(session_data)
|
||||
|
||||
// Create session
|
||||
session := Session{
|
||||
session_key: session_key
|
||||
pubkey: pubkey
|
||||
created_at: time.now()
|
||||
last_activity: time.now()
|
||||
expires_at: time.now().add_seconds(3600 * 24) // 24 hour session
|
||||
}
|
||||
|
||||
// Store session
|
||||
server.sessions[session_key] = session
|
||||
|
||||
// Clean up challenge
|
||||
challenges.delete(pubkey)
|
||||
|
||||
return AuthSubmitResponse{
|
||||
session_key: session_key
|
||||
}
|
||||
}
|
||||
|
||||
// Validate session key
|
||||
pub fn (mut server HeroServer) validate_session(session_key string) !Session {
|
||||
mut session := server.sessions[session_key] or {
|
||||
return error('Invalid session key')
|
||||
}
|
||||
|
||||
// Check if session expired
|
||||
if time.now() > session.expires_at {
|
||||
server.sessions.delete(session_key)
|
||||
return error('Session expired')
|
||||
}
|
||||
|
||||
// Update last activity
|
||||
session.last_activity = time.now()
|
||||
server.sessions[session_key] = session
|
||||
|
||||
return session
|
||||
}
|
||||
38
lib/hero/heroserver/doc_handler.v
Normal file
38
lib/hero/heroserver/doc_handler.v
Normal file
@@ -0,0 +1,38 @@
|
||||
module heroserver
|
||||
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
// Documentation controller
|
||||
pub struct DocController {
|
||||
mut:
|
||||
server &HeroServer
|
||||
handler_type string
|
||||
}
|
||||
|
||||
@[get; '/']
|
||||
pub fn (mut controller DocController) show_docs(mut ctx Context) veb.Result {
|
||||
// Get handler
|
||||
handler := controller.server.handlers[controller.handler_type] or {
|
||||
return ctx.not_found()
|
||||
}
|
||||
|
||||
// Get OpenRPC specification
|
||||
spec := handler.get_openrpc_spec()
|
||||
|
||||
// Render template
|
||||
html_content := $tmpl('templates/doc.html')
|
||||
|
||||
return ctx.html(html_content)
|
||||
}
|
||||
|
||||
// Setup documentation routes
|
||||
pub fn (mut server HeroServer) setup_doc_routes() ! {
|
||||
for handler_type, _ in server.handlers {
|
||||
controller := &DocController{
|
||||
server: server
|
||||
handler_type: handler_type
|
||||
}
|
||||
server.app.register_controller[DocController, Context]('/doc/${handler_type}', mut controller)!
|
||||
}
|
||||
}
|
||||
59
lib/hero/heroserver/factory.v
Normal file
59
lib/hero/heroserver/factory.v
Normal file
@@ -0,0 +1,59 @@
|
||||
module heroserver
|
||||
|
||||
import freeflowuniverse.herolib.crypt.herocrypt
|
||||
import veb
|
||||
|
||||
// Create a new HeroServer instance
|
||||
pub fn new(config HeroServerConfig) !&HeroServer {
|
||||
// Initialize crypto client
|
||||
mut crypto_client := if c := config.crypto_client {
|
||||
c
|
||||
} else {
|
||||
herocrypt.new_default()!
|
||||
}
|
||||
|
||||
// Create VEB app
|
||||
mut app := &veb.StaticHandler{}
|
||||
|
||||
mut server := &HeroServer{
|
||||
port: config.port
|
||||
host: config.host
|
||||
crypto_client: crypto_client
|
||||
sessions: map[string]Session{}
|
||||
handlers: map[string]openrpc.OpenRPCHandler{}
|
||||
app: app
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// Register an OpenRPC handler
|
||||
pub fn (mut server HeroServer) register_handler(handler_type string, handler openrpc.OpenRPCHandler) ! {
|
||||
server.handlers[handler_type] = handler
|
||||
}
|
||||
|
||||
// Start the server
|
||||
pub fn (mut server HeroServer) start() ! {
|
||||
// Setup routes
|
||||
server.setup_routes()!
|
||||
|
||||
// Start VEB server
|
||||
veb.run_at[HeroServer, Context](mut server,
|
||||
host: server.host,
|
||||
port: server.port
|
||||
)!
|
||||
}
|
||||
|
||||
// Context struct for VEB
|
||||
pub struct Context {
|
||||
veb.Context
|
||||
}
|
||||
|
||||
// Add to HeroServer struct
|
||||
pub fn (mut server HeroServer) setup_routes() ! {
|
||||
// Setup authentication routes
|
||||
server.setup_api_routes()!
|
||||
|
||||
// Setup documentation routes
|
||||
server.setup_doc_routes()!
|
||||
}
|
||||
85
lib/hero/heroserver/model.v
Normal file
85
lib/hero/heroserver/model.v
Normal file
@@ -0,0 +1,85 @@
|
||||
module heroserver
|
||||
|
||||
import freeflowuniverse.herolib.crypt.herocrypt
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import time
|
||||
import net.http
|
||||
|
||||
// Main server configuration
|
||||
@[params]
|
||||
pub struct HeroServerConfig {
|
||||
pub mut:
|
||||
port int = 9977
|
||||
host string = 'localhost'
|
||||
// Optional crypto client, will create default if not provided
|
||||
crypto_client ?&herocrypt.HeroCrypt
|
||||
|
||||
}
|
||||
|
||||
// Main server struct
|
||||
pub struct HeroServer {
|
||||
mut:
|
||||
port int
|
||||
host string
|
||||
crypto_client &herocrypt.HeroCrypt
|
||||
sessions map[string]Session // sessionkey -> Session
|
||||
handlers map[string]openrpc.OpenRPCHandler // handlertype -> handler
|
||||
challenges map[string]AuthChallenge
|
||||
pub mut:
|
||||
app &veb.StaticHandler
|
||||
challenges map[string]AuthChallenge
|
||||
}
|
||||
|
||||
// Authentication challenge data
|
||||
pub struct AuthChallenge {
|
||||
pub mut:
|
||||
pubkey string
|
||||
challenge string // unique hashed challenge
|
||||
created_at time.Time
|
||||
expires_at time.Time
|
||||
}
|
||||
|
||||
// Active session data
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
session_key string
|
||||
pubkey string
|
||||
created_at time.Time
|
||||
last_activity time.Time
|
||||
expires_at time.Time
|
||||
}
|
||||
|
||||
// Authentication request structures
|
||||
pub struct RegisterRequest {
|
||||
pub:
|
||||
pubkey string
|
||||
}
|
||||
|
||||
pub struct AuthRequest {
|
||||
pub:
|
||||
pubkey string
|
||||
}
|
||||
|
||||
pub struct AuthResponse {
|
||||
pub:
|
||||
challenge string
|
||||
}
|
||||
|
||||
pub struct AuthSubmitRequest {
|
||||
pub:
|
||||
pubkey string
|
||||
signature string // signed challenge
|
||||
}
|
||||
|
||||
pub struct AuthSubmitResponse {
|
||||
pub:
|
||||
session_key string
|
||||
}
|
||||
|
||||
// API request wrapper
|
||||
pub struct APIRequest {
|
||||
pub:
|
||||
session_key string
|
||||
method string
|
||||
params map[string]string
|
||||
}
|
||||
55
lib/hero/heroserver/readme.md
Normal file
55
lib/hero/heroserver/readme.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# HeroServer
|
||||
|
||||
HeroServer is a secure web server built in V, designed for public key-based authentication and serving OpenRPC APIs and their documentation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Public Key Authentication**: Secure access using cryptographic signatures.
|
||||
- **OpenRPC Integration**: Serve APIs defined with the OpenRPC specification.
|
||||
- **Automatic Documentation**: Generates HTML documentation from your OpenRPC schemas.
|
||||
- **Session Management**: Manages authenticated user sessions.
|
||||
- **Extensible**: Register multiple, independent handlers for different API groups.
|
||||
|
||||
## Usage
|
||||
|
||||
```v
|
||||
import freeflowuniverse.herolib.hero.heroserver
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
|
||||
fn main() {
|
||||
// 1. Create a new server instance
|
||||
mut server := heroserver.new(port: 8080)!
|
||||
|
||||
// 2. Create and register your OpenRPC handlers
|
||||
// These handlers must conform to the `openrpc.OpenRPCHandler` interface.
|
||||
calendar_handler := create_calendar_handler() // Your implementation
|
||||
server.register_handler('calendar', calendar_handler)!
|
||||
|
||||
task_handler := create_task_handler() // Your implementation
|
||||
server.register_handler('tasks', task_handler)!
|
||||
|
||||
// 3. Start the server
|
||||
server.start()! // This call blocks and starts serving requests
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
- **API Calls**: `POST /api/{handler_type}/{method_name}`
|
||||
- **Documentation**: `GET /doc/{handler_type}/`
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
1. **Register Public Key**: `POST /auth/register`
|
||||
- Body: `{"pubkey": "your_public_key"}`
|
||||
2. **Request Challenge**: `POST /auth/authreq`
|
||||
- Body: `{"pubkey": "your_public_key"}`
|
||||
- Returns a unique challenge string.
|
||||
3. **Submit Signature**: `POST /auth/auth`
|
||||
- Sign the challenge from step 2 with your private key.
|
||||
- Body: `{"pubkey": "your_public_key", "signature": "your_signature"}`
|
||||
- Returns a session key.
|
||||
|
||||
All subsequent API calls must include the session key in the `Authorization` header:
|
||||
|
||||
`Authorization: Bearer {session_key}`
|
||||
13
lib/hero/heroserver/session.v
Normal file
13
lib/hero/heroserver/session.v
Normal file
@@ -0,0 +1,13 @@
|
||||
module heroserver
|
||||
|
||||
import time
|
||||
|
||||
// Active session data
|
||||
pub struct Session {
|
||||
pub mut:
|
||||
session_key string
|
||||
pubkey string
|
||||
created_at time.Time
|
||||
last_activity time.Time
|
||||
expires_at time.Time
|
||||
}
|
||||
163
lib/hero/heroserver/templates/doc.html
Normal file
163
lib/hero/heroserver/templates/doc.html
Normal file
@@ -0,0 +1,163 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@{spec.info.title} - API Documentation</title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
.method-card {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.param-table {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.code-block {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.object-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-4">
|
||||
<!-- Header -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1 class="display-4">@{spec.info.title}</h1>
|
||||
<p class="lead">@{spec.info.description}</p>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Authentication Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
<h5>Authentication Required</h5>
|
||||
<p>All API endpoints require authentication using a session key obtained through the authentication flow:</p>
|
||||
<ol>
|
||||
<li>Register your public key: <code>POST /auth/register</code></li>
|
||||
<li>Request authentication challenge: <code>POST /auth/authreq</code></li>
|
||||
<li>Sign challenge and submit: <code>POST /auth/auth</code></li>
|
||||
<li>Use returned session key in <code>Authorization: Bearer {session_key}</code> header</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group methods by root object -->
|
||||
@for root_object, methods in spec.methods_by_object() {
|
||||
<div class="object-section">
|
||||
<h2 id="@{root_object.name_fix()}">@{root_object}</h2>
|
||||
|
||||
@if root_object_desc := spec.get_object_description(root_object) {
|
||||
<p class="text-muted">@{root_object_desc}</p>
|
||||
}
|
||||
|
||||
@for method in methods {
|
||||
<div class="card method-card">
|
||||
<div class="card-header">
|
||||
<h4 class="mb-0">
|
||||
<span class="badge bg-primary me-2">POST</span>
|
||||
<code>/api/@{controller.handler_type}/@{method.name}</code>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>@{method.summary}</p>
|
||||
|
||||
@if method.description.len > 0 {
|
||||
<div class="mb-3">
|
||||
<h6>Description</h6>
|
||||
<p>@{method.description}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Parameters -->
|
||||
@if method.params.len > 0 {
|
||||
<div class="mb-3">
|
||||
<h6>Parameters</h6>
|
||||
<table class="table table-sm param-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for param in method.params {
|
||||
<tr>
|
||||
<td><code>@{param.name}</code></td>
|
||||
<td>@{param.schema.type_}</td>
|
||||
<td>@{param.required ? 'Yes' : 'No'}</td>
|
||||
<td>@{param.description}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Return Type -->
|
||||
@if method.result {
|
||||
<div class="mb-3">
|
||||
<h6>Returns</h6>
|
||||
<p><strong>Type:</strong> @{method.result.schema.type_}</p>
|
||||
@if method.result.description.len > 0 {
|
||||
<p>@{method.result.description}</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Example -->
|
||||
@{example_call, example_response := method.example()}
|
||||
@if example_call.len > 0 {
|
||||
<div class="mb-3">
|
||||
<h6>Example Request</h6>
|
||||
<div class="code-block">
|
||||
<pre><code>curl -X POST \
|
||||
'http://localhost:8080/api/@{controller.handler_type}/@{method.name}' \
|
||||
-H 'Authorization: Bearer YOUR_SESSION_KEY' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '@{example_call}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if example_response.len > 0 {
|
||||
<div class="mb-3">
|
||||
<h6>Example Response</h6>
|
||||
<div class="code-block">
|
||||
<pre><code>@{example_response}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="mt-5 py-4 bg-light">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0">Generated from OpenRPC specification</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
9
lib/schemas/jsonschema/custom.v
Normal file
9
lib/schemas/jsonschema/custom.v
Normal file
@@ -0,0 +1,9 @@
|
||||
module jsonschema
|
||||
|
||||
pub fn (schema Schema) type_() string {
|
||||
return schema.typ.str()
|
||||
}
|
||||
|
||||
pub fn (schema Schema) example_value() string {
|
||||
return ''
|
||||
}
|
||||
51
lib/schemas/openrpc/custom.v
Normal file
51
lib/schemas/openrpc/custom.v
Normal file
@@ -0,0 +1,51 @@
|
||||
module openrpc
|
||||
|
||||
import json
|
||||
|
||||
// In the OpenRPC specification struct
|
||||
pub fn (spec OpenRPC) methods_by_object() map[string][]Method {
|
||||
mut grouped := map[string][]Method{}
|
||||
|
||||
for method in spec.methods {
|
||||
// Extract root object from method name (e.g., "calendar.create" -> "calendar")
|
||||
parts := method.name.split('.')
|
||||
root_object := if parts.len > 1 { parts[0] } else { 'general' }
|
||||
|
||||
if root_object !in grouped {
|
||||
grouped[root_object] = []Method{}
|
||||
}
|
||||
grouped[root_object] << method
|
||||
}
|
||||
|
||||
return grouped
|
||||
}
|
||||
|
||||
pub fn (spec OpenRPC) get_object_description(object_name string) string {
|
||||
// Return description for object if available
|
||||
// Implementation depends on how objects are defined in the spec
|
||||
return ''
|
||||
}
|
||||
|
||||
pub fn (spec OpenRPC) name_fix() string {
|
||||
return spec.info.title.replace(' ', '_').to_lower()
|
||||
}
|
||||
|
||||
// In Method struct
|
||||
pub fn (method Method) example() (string, string) {
|
||||
// Generate example call and response
|
||||
// This should create realistic JSON examples based on the method schema
|
||||
|
||||
mut example_params := map[string]string{}
|
||||
for param in method.params {
|
||||
example_params[param.name] = param.schema.example_value()
|
||||
}
|
||||
|
||||
example_call := json.encode(example_params)
|
||||
example_response := if method.result {
|
||||
method.result.schema.example_value()
|
||||
} else {
|
||||
'{"result": "success"}'
|
||||
}
|
||||
|
||||
return example_call, example_response
|
||||
}
|
||||
Reference in New Issue
Block a user