refactor: integrate heromodels RPC with heroserver
- Integrate heromodels RPC as a handler within heroserver - Update API endpoint to use standard JSON-RPC format - Add generated curl examples with copy button to docs - Improve error handling to return JSON-RPC errors - Simplify heromodels server example script
This commit is contained in:
1
examples/hero/heromodels/.gitignore
vendored
Normal file
1
examples/hero/heromodels/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
heroserver_example
|
||||
17
examples/hero/heromodels/heroserver_example.vsh
Executable file
17
examples/hero/heromodels/heroserver_example.vsh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals -no-skip-unused run
|
||||
|
||||
import freeflowuniverse.herolib.hero.heromodels.rpc
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
import time
|
||||
|
||||
fn main() {
|
||||
// Start the server in a background thread
|
||||
spawn fn () {
|
||||
rpc.start(port: 8080) or { panic('Failed to start HeroModels server: ${err}') }
|
||||
}()
|
||||
|
||||
// Keep the main thread alive
|
||||
for {
|
||||
time.sleep(time.second)
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals -no-skip-unused run
|
||||
|
||||
import json
|
||||
import freeflowuniverse.herolib.hero.heromodels.rpc
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.hero.heromodels
|
||||
import time
|
||||
|
||||
fn main() {
|
||||
println('Starting RPC server on port 9090...')
|
||||
|
||||
// Start the server in a background thread
|
||||
spawn fn () {
|
||||
rpc.start(http_port: 9090) or { panic('Failed to start RPC server: ${err}') }
|
||||
}()
|
||||
|
||||
// Wait for server to start
|
||||
time.sleep(time.second * 2)
|
||||
println('Server started, now testing with some requests...')
|
||||
|
||||
// Create a calendar object to test with
|
||||
mut mydb := heromodels.new()!
|
||||
mut my_calendar := mydb.calendar.new(
|
||||
color: '#FF0000'
|
||||
timezone: 'UTC'
|
||||
is_public: true
|
||||
events: []u32{}
|
||||
)!
|
||||
my_calendar.name = 'Test Calendar'
|
||||
my_calendar.description = 'A test calendar for RPC'
|
||||
|
||||
// Test the calendar_set RPC method
|
||||
request := jsonrpc.new_request('calendar_set', json.encode(my_calendar))
|
||||
println('Sending request: ${request}')
|
||||
|
||||
// TODO: Add HTTP client to actually send the request to localhost:9090
|
||||
// For now, just show what would be sent
|
||||
|
||||
// Keep the server running
|
||||
println('Server is running on http://localhost:9090')
|
||||
println('You can test it with curl or other HTTP clients')
|
||||
println('Press Ctrl+C to stop the server')
|
||||
|
||||
// Keep main thread alive so server continues running
|
||||
for {
|
||||
time.sleep(time.second * 10)
|
||||
println('Server still running...')
|
||||
}
|
||||
}
|
||||
@@ -31,8 +31,7 @@ if http_port == 0 {
|
||||
|
||||
curl -X POST -H "Content-Type: application/json" -d \'\{"jsonrpc":"2.0","method":"comment_set","params":{"comment":"Hello world!","parent":0,"author":42},"id":1\}\' http://localhost:9933
|
||||
|
||||
'
|
||||
)
|
||||
')
|
||||
}
|
||||
|
||||
rpc.start(http_port: http_port)!
|
||||
rpc.start(port: http_port)!
|
||||
|
||||
@@ -1,68 +1,93 @@
|
||||
module rpc
|
||||
|
||||
import freeflowuniverse.herolib.schemas.openrpc
|
||||
import freeflowuniverse.herolib.hero.heroserver
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
import freeflowuniverse.herolib.ui.console
|
||||
import os
|
||||
|
||||
const openrpc_path = os.join_path(os.dir(@FILE), 'openrpc.json')
|
||||
|
||||
// Create a new heromodels handler for heroserver
|
||||
pub fn new_heromodels_handler() !&openrpc.Handler {
|
||||
mut handler := openrpc.new_handler(openrpc_path)!
|
||||
|
||||
// Register all comment methods
|
||||
handler.register_procedure_handle('comment_get', comment_get)
|
||||
handler.register_procedure_handle('comment_set', comment_set)
|
||||
handler.register_procedure_handle('comment_delete', comment_delete)
|
||||
handler.register_procedure_handle('comment_list', comment_list)
|
||||
|
||||
// Register all calendar methods
|
||||
handler.register_procedure_handle('calendar_get', calendar_get)
|
||||
handler.register_procedure_handle('calendar_set', calendar_set)
|
||||
handler.register_procedure_handle('calendar_delete', calendar_delete)
|
||||
handler.register_procedure_handle('calendar_list', calendar_list)
|
||||
|
||||
// Register all calendar event methods
|
||||
handler.register_procedure_handle('calendar_event_get', calendar_event_get)
|
||||
handler.register_procedure_handle('calendar_event_set', calendar_event_set)
|
||||
handler.register_procedure_handle('calendar_event_delete', calendar_event_delete)
|
||||
handler.register_procedure_handle('calendar_event_list', calendar_event_list)
|
||||
|
||||
// Register all chat group methods
|
||||
handler.register_procedure_handle('chat_group_get', chat_group_get)
|
||||
handler.register_procedure_handle('chat_group_set', chat_group_set)
|
||||
handler.register_procedure_handle('chat_group_delete', chat_group_delete)
|
||||
handler.register_procedure_handle('chat_group_list', chat_group_list)
|
||||
|
||||
// Register all chat message methods
|
||||
handler.register_procedure_handle('chat_message_get', chat_message_get)
|
||||
handler.register_procedure_handle('chat_message_set', chat_message_set)
|
||||
handler.register_procedure_handle('chat_message_delete', chat_message_delete)
|
||||
handler.register_procedure_handle('chat_message_list', chat_message_list)
|
||||
|
||||
// Register all group methods
|
||||
handler.register_procedure_handle('group_get', group_get)
|
||||
handler.register_procedure_handle('group_set', group_set)
|
||||
handler.register_procedure_handle('group_delete', group_delete)
|
||||
handler.register_procedure_handle('group_list', group_list)
|
||||
|
||||
// Register all project issue methods
|
||||
handler.register_procedure_handle('project_issue_get', project_issue_get)
|
||||
handler.register_procedure_handle('project_issue_set', project_issue_set)
|
||||
handler.register_procedure_handle('project_issue_delete', project_issue_delete)
|
||||
handler.register_procedure_handle('project_issue_list', project_issue_list)
|
||||
|
||||
// Register all project methods
|
||||
handler.register_procedure_handle('project_get', project_get)
|
||||
handler.register_procedure_handle('project_set', project_set)
|
||||
handler.register_procedure_handle('project_delete', project_delete)
|
||||
handler.register_procedure_handle('project_list', project_list)
|
||||
|
||||
// Register all user methods
|
||||
handler.register_procedure_handle('user_get', user_get)
|
||||
handler.register_procedure_handle('user_set', user_set)
|
||||
handler.register_procedure_handle('user_delete', user_delete)
|
||||
handler.register_procedure_handle('user_list', user_list)
|
||||
|
||||
return &handler
|
||||
}
|
||||
|
||||
// Start heromodels server using heroserver
|
||||
@[params]
|
||||
pub struct ServerArgs {
|
||||
pub mut:
|
||||
socket_path string = '/tmp/heromodels'
|
||||
http_port int // if 0, no http server will be started
|
||||
port int = 8080
|
||||
host string = 'localhost'
|
||||
}
|
||||
|
||||
pub fn start(args ServerArgs) ! {
|
||||
mut openrpc_handler := openrpc.new_handler(openrpc_path)!
|
||||
// Create a new heroserver instance
|
||||
mut server := heroserver.new(port: args.port, host: args.host)!
|
||||
|
||||
openrpc_handler.register_procedure_handle('comment_get', comment_get)
|
||||
openrpc_handler.register_procedure_handle('comment_set', comment_set)
|
||||
openrpc_handler.register_procedure_handle('comment_delete', comment_delete)
|
||||
openrpc_handler.register_procedure_handle('comment_list', comment_list)
|
||||
// Create and register the heromodels handler
|
||||
handler := new_heromodels_handler()!
|
||||
server.register_handler('heromodels', handler)!
|
||||
|
||||
openrpc_handler.register_procedure_handle('calendar_get', calendar_get)
|
||||
openrpc_handler.register_procedure_handle('calendar_set', calendar_set)
|
||||
openrpc_handler.register_procedure_handle('calendar_delete', calendar_delete)
|
||||
openrpc_handler.register_procedure_handle('calendar_list', calendar_list)
|
||||
console.print_green('Documentation available at: http://${args.host}:${args.port}/doc/heromodels/')
|
||||
console.print_green('HeroModels API available at: http://${args.host}:${args.port}/api/heromodels')
|
||||
|
||||
openrpc_handler.register_procedure_handle('calendar_event_get', calendar_event_get)
|
||||
openrpc_handler.register_procedure_handle('calendar_event_set', calendar_event_set)
|
||||
openrpc_handler.register_procedure_handle('calendar_event_delete', calendar_event_delete)
|
||||
openrpc_handler.register_procedure_handle('calendar_event_list', calendar_event_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('chat_group_get', chat_group_get)
|
||||
openrpc_handler.register_procedure_handle('chat_group_set', chat_group_set)
|
||||
openrpc_handler.register_procedure_handle('chat_group_delete', chat_group_delete)
|
||||
openrpc_handler.register_procedure_handle('chat_group_list', chat_group_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('chat_message_get', chat_message_get)
|
||||
openrpc_handler.register_procedure_handle('chat_message_set', chat_message_set)
|
||||
openrpc_handler.register_procedure_handle('chat_message_delete', chat_message_delete)
|
||||
openrpc_handler.register_procedure_handle('chat_message_list', chat_message_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('group_get', group_get)
|
||||
openrpc_handler.register_procedure_handle('group_set', group_set)
|
||||
openrpc_handler.register_procedure_handle('group_delete', group_delete)
|
||||
openrpc_handler.register_procedure_handle('group_list', group_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('project_issue_get', project_issue_get)
|
||||
openrpc_handler.register_procedure_handle('project_issue_set', project_issue_set)
|
||||
openrpc_handler.register_procedure_handle('project_issue_delete', project_issue_delete)
|
||||
openrpc_handler.register_procedure_handle('project_issue_list', project_issue_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('project_get', project_get)
|
||||
openrpc_handler.register_procedure_handle('project_set', project_set)
|
||||
openrpc_handler.register_procedure_handle('project_delete', project_delete)
|
||||
openrpc_handler.register_procedure_handle('project_list', project_list)
|
||||
|
||||
openrpc_handler.register_procedure_handle('user_get', user_get)
|
||||
openrpc_handler.register_procedure_handle('user_set', user_set)
|
||||
openrpc_handler.register_procedure_handle('user_delete', user_delete)
|
||||
openrpc_handler.register_procedure_handle('user_list', user_list)
|
||||
|
||||
if args.http_port != 0 {
|
||||
openrpc.start_http_server(openrpc_handler, port: args.http_port)!
|
||||
} else {
|
||||
openrpc.start_unix_server(openrpc_handler, socket_path: args.socket_path)!
|
||||
}
|
||||
// Start the server
|
||||
server.start()!
|
||||
}
|
||||
|
||||
@@ -31,7 +31,14 @@ pub fn comment_get(request Request) !Response {
|
||||
}
|
||||
|
||||
mut mydb := heromodels.new()!
|
||||
comment := mydb.comments.get(payload.id)!
|
||||
comment := mydb.comments.get(payload.id) or {
|
||||
// Return proper JSON-RPC error instead of panicking
|
||||
return jsonrpc.new_error(request.id, jsonrpc.RPCError{
|
||||
code: -32000 // Server error
|
||||
message: 'Comment not found'
|
||||
data: 'Comment with ID ${payload.id} does not exist'
|
||||
})
|
||||
}
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(comment))
|
||||
}
|
||||
@@ -59,14 +66,28 @@ pub fn comment_delete(request Request) !Response {
|
||||
}
|
||||
|
||||
mut mydb := heromodels.new()!
|
||||
mydb.comments.delete(payload.id)!
|
||||
mydb.comments.delete(payload.id) or {
|
||||
// Return proper JSON-RPC error instead of panicking
|
||||
return jsonrpc.new_error(request.id, jsonrpc.RPCError{
|
||||
code: -32000 // Server error
|
||||
message: 'Comment not found'
|
||||
data: 'Comment with ID ${payload.id} does not exist or could not be deleted'
|
||||
})
|
||||
}
|
||||
|
||||
return new_response_true(request.id) // return true as jsonrpc (bool)
|
||||
}
|
||||
|
||||
pub fn comment_list(request Request) !Response {
|
||||
mut mydb := heromodels.new()!
|
||||
comments := mydb.comments.list()!
|
||||
comments := mydb.comments.list() or {
|
||||
// Return proper JSON-RPC error instead of panicking
|
||||
return jsonrpc.new_error(request.id, jsonrpc.RPCError{
|
||||
code: -32000 // Server error
|
||||
message: 'Failed to list comments'
|
||||
data: 'Error occurred while retrieving comments: ${err}'
|
||||
})
|
||||
}
|
||||
|
||||
return jsonrpc.new_response(request.id, json.encode(comments))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ module heroserver
|
||||
|
||||
import json
|
||||
import veb
|
||||
import freeflowuniverse.herolib.schemas.jsonrpc
|
||||
|
||||
@['/auth/:action']
|
||||
pub fn (mut server HeroServer) auth_handler(mut ctx Context, action string) !veb.Result {
|
||||
@@ -11,7 +12,9 @@ pub fn (mut server HeroServer) auth_handler(mut ctx Context, action string) !veb
|
||||
return ctx.request_error('Invalid JSON format')
|
||||
}
|
||||
server.register(request.pubkey)!
|
||||
return ctx.json({'status': 'success'})
|
||||
return ctx.json({
|
||||
'status': 'success'
|
||||
})
|
||||
}
|
||||
'authreq' {
|
||||
request := json.decode(AuthRequest, ctx.req.data) or {
|
||||
@@ -33,17 +36,31 @@ pub fn (mut server HeroServer) auth_handler(mut ctx Context, action string) !veb
|
||||
}
|
||||
}
|
||||
|
||||
@['/api/:handler_type/:method_name']
|
||||
pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string, method_name string) veb.Result {
|
||||
session_key := ctx.get_header(.authorization) or {
|
||||
return ctx.request_error('Missing session key in Authorization header')
|
||||
}.replace('Bearer ', '')
|
||||
@['/api/:handler_type'; post]
|
||||
pub fn (mut server HeroServer) api_handler(mut ctx Context, handler_type string) veb.Result {
|
||||
// TODO: For now, skip authentication for testing
|
||||
// session_key := ctx.get_header(.authorization) or {
|
||||
// return ctx.request_error('Missing session key in Authorization header')
|
||||
// }.replace('Bearer ', '')
|
||||
|
||||
// Validate session
|
||||
mut session := server.validate_session(session_key) or {
|
||||
return ctx.request_error('Invalid session')
|
||||
// // Validate session
|
||||
// mut session := server.validate_session(session_key) or {
|
||||
// return ctx.request_error('Invalid session')
|
||||
// }
|
||||
|
||||
// Get the registered handler
|
||||
mut handler := server.handlers[handler_type] or {
|
||||
return ctx.request_error('Handler not found: ${handler_type}')
|
||||
}
|
||||
|
||||
// For now, simplified response
|
||||
return ctx.json({'result': 'success'})
|
||||
// Parse JSON-RPC request
|
||||
request := jsonrpc.decode_request(ctx.req.data) or {
|
||||
return ctx.request_error('Invalid JSON-RPC request: ${err}')
|
||||
}
|
||||
|
||||
// Handle the request using the OpenRPC handler
|
||||
response := handler.handle(request) or { return ctx.server_error('Handler error: ${err}') }
|
||||
|
||||
// Return the JSON-RPC response
|
||||
return ctx.json(response)
|
||||
}
|
||||
@@ -31,6 +31,7 @@ pub mut:
|
||||
example_call string
|
||||
example_response string
|
||||
endpoint_url string
|
||||
curl_example string // New field for curl command
|
||||
}
|
||||
|
||||
// DocParam represents a parameter or result in the documentation
|
||||
@@ -110,16 +111,24 @@ pub fn doc_spec_from_openrpc(openrpc_spec openrpc.OpenRPC, handler_type string)
|
||||
example_call := generate_example_call(doc_params)
|
||||
example_response := generate_example_response(doc_result)
|
||||
|
||||
doc_method := DocMethod{
|
||||
// Generate JSON-RPC example call
|
||||
jsonrpc_call := generate_jsonrpc_example_call(method.name, doc_params)
|
||||
|
||||
mut doc_method := DocMethod{
|
||||
name: method.name
|
||||
summary: method.summary
|
||||
description: method.description
|
||||
params: doc_params
|
||||
result: doc_result
|
||||
endpoint_url: '/api/${handler_type}/${method.name}'
|
||||
endpoint_url: '/api/${handler_type}'
|
||||
example_call: example_call
|
||||
example_response: example_response
|
||||
curl_example: '' // Will be set later with proper base URL
|
||||
}
|
||||
|
||||
// Generate curl example with localhost as default using JSON-RPC format
|
||||
doc_method.curl_example = generate_curl_example_jsonrpc(method.name, doc_params,
|
||||
'http://localhost:8080', handler_type)
|
||||
doc_spec.methods << doc_method
|
||||
}
|
||||
|
||||
@@ -232,3 +241,41 @@ fn create_auth_info() AuthDocInfo {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// generate_jsonrpc_example_call creates a complete JSON-RPC request example
|
||||
fn generate_jsonrpc_example_call(method_name string, params []DocParam) string {
|
||||
params_obj := if params.len == 0 {
|
||||
'{}'
|
||||
} else {
|
||||
mut call_parts := []string{}
|
||||
for param in params {
|
||||
call_parts << '"${param.name}": ${param.example}'
|
||||
}
|
||||
'{\n ${call_parts.join(',\n ')}\n }'
|
||||
}
|
||||
|
||||
return '{\n "jsonrpc": "2.0",\n "method": "${method_name}",\n "params": ${params_obj},\n "id": 1\n}'
|
||||
}
|
||||
|
||||
// generate_curl_example_jsonrpc creates a curl command with proper JSON-RPC format
|
||||
fn generate_curl_example_jsonrpc(method_name string, params []DocParam, base_url string, handler_name string) string {
|
||||
endpoint := '${base_url}/api/${handler_name}'
|
||||
jsonrpc_request := generate_jsonrpc_example_call(method_name, params)
|
||||
|
||||
mut curl_cmd := 'curl -X POST ${endpoint} \\\n'
|
||||
curl_cmd += ' -H "Content-Type: application/json" \\\n'
|
||||
curl_cmd += ' -d \'${jsonrpc_request}\''
|
||||
|
||||
return curl_cmd
|
||||
}
|
||||
|
||||
// generate_curl_example creates a curl command for the given method (legacy)
|
||||
pub fn generate_curl_example(method DocMethod, base_url string, handler_name string) string {
|
||||
endpoint := '${base_url}/api/${handler_name}'
|
||||
|
||||
mut curl_cmd := 'curl -X POST ${endpoint} \\\n'
|
||||
curl_cmd += ' -H "Content-Type: application/json" \\\n'
|
||||
curl_cmd += ' -d \'${method.example_call}\''
|
||||
|
||||
return curl_cmd
|
||||
}
|
||||
|
||||
@@ -65,6 +65,44 @@
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.curl-section {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.copy-button:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
.copy-button.copied {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.curl-command {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
padding-right: 4rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -255,6 +293,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Curl Example -->
|
||||
<div class="curl-section">
|
||||
<h6>Curl Command:</h6>
|
||||
<button class="copy-button" onclick="copyToClipboard('curl-${method.name}', this)">
|
||||
Copy
|
||||
</button>
|
||||
<pre class="curl-command" id="curl-${method.name}">${method.curl_example}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@end
|
||||
@@ -346,6 +393,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Curl Example -->
|
||||
<div class="curl-section">
|
||||
<h6>Curl Command:</h6>
|
||||
<button class="copy-button" onclick="copyToClipboard('curl-${method.name}', this)">
|
||||
📋 Copy
|
||||
</button>
|
||||
<pre class="curl-command" id="curl-${method.name}">${method.curl_example}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@end
|
||||
@@ -366,6 +422,61 @@
|
||||
<!-- 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>
|
||||
|
||||
<!-- Copy to Clipboard Functionality -->
|
||||
<script>
|
||||
function copyToClipboard(elementId, button) {
|
||||
const element = document.getElementById(elementId);
|
||||
const text = element.textContent;
|
||||
|
||||
// Use the modern Clipboard API
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showCopySuccess(button);
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
fallbackCopyTextToClipboard(text, button);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
fallbackCopyTextToClipboard(text, button);
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopyTextToClipboard(text, button) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
textArea.style.top = "0";
|
||||
textArea.style.left = "0";
|
||||
textArea.style.position = "fixed";
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
const successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
showCopySuccess(button);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fallback: Oops, unable to copy', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
function showCopySuccess(button) {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = 'Copied!';
|
||||
button.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('copied');
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -145,5 +145,8 @@ pub fn (handler Handler) handle(request Request) !Response {
|
||||
}
|
||||
|
||||
// Execute the procedure handler with the request payload
|
||||
return procedure_func(request) or { panic(err) }
|
||||
return procedure_func(request) or {
|
||||
// Return proper JSON-RPC error instead of panicking
|
||||
return new_error(request.id, internal_error)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user