diff --git a/consensusj-jrpc-echod/src/test/java/org/consensusj/jsonrpc/daemon/ApplicationTest.java b/consensusj-jrpc-echod/src/test/java/org/consensusj/jsonrpc/daemon/ApplicationTest.java index a517c61ea..e71a13a18 100644 --- a/consensusj-jrpc-echod/src/test/java/org/consensusj/jsonrpc/daemon/ApplicationTest.java +++ b/consensusj-jrpc-echod/src/test/java/org/consensusj/jsonrpc/daemon/ApplicationTest.java @@ -21,6 +21,7 @@ import org.consensusj.jsonrpc.DefaultRpcClient; import org.consensusj.jsonrpc.JsonRpcError; import org.consensusj.jsonrpc.JsonRpcStatusException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,9 +43,17 @@ public class ApplicationTest { URI endpoint; + DefaultRpcClient client; + @BeforeEach void testSetup() { endpoint = URI.create(server.getURI().toString()); + client = new DefaultRpcClient(endpoint, "", ""); + } + + @AfterEach + void testTeardown() { + client.close(); } @Test @@ -60,10 +69,8 @@ void serverStarts() { @Test void echoMethod() throws IOException { var testString = "Hello jrpc-echod!"; - try (var client = new DefaultRpcClient(endpoint, "", "")) { - String result = (String) client.send("echo", testString); - assertEquals(testString, result); - } + String result = (String) client.send("echo", testString); + assertEquals(testString, result); } @Test @@ -73,9 +80,7 @@ void echoMethodWrongNumberOfArgs() throws IOException { var testString = "Hello jrpc-echod!"; JsonRpcStatusException exception = assertThrows(JsonRpcStatusException.class, () -> { - try (var client = new DefaultRpcClient(endpoint, "", "")) { - client.send("echo", testString, testString); - } + client.send("echo", testString, testString); }); assertTrue(Objects.requireNonNull(exception.getMessage()).startsWith(expectedErrorMessagePrefix)); assertEquals(expectedErrorCode, exception.jsonRpcCode); @@ -83,36 +88,47 @@ void echoMethodWrongNumberOfArgs() throws IOException { @Test - void helpMethod() throws IOException { - var expectedResult = """ -echo message -help -stop - """; - try (var client = new DefaultRpcClient(endpoint, "", "")) { - String result = (String) client.send("help"); - assertEquals(expectedResult, result); - } + void helpForHelpMethod() throws IOException { + var expectedMethodNameSubstring = "help "; + var expectedParamNameSubstring = "method (string, optional)"; + var expectedDescriptiveTextSubstring = "Displays detailed help text for the specified method."; + String result = (String) client.send("help", "help"); + assertTrue(result.contains(expectedMethodNameSubstring)); + assertTrue(result.contains(expectedParamNameSubstring)); + assertTrue(result.contains(expectedDescriptiveTextSubstring)); + } - /* - * The help method is currently not fully implemented. It SHOULD allow - * for an argument, and only fail if the argument doesn't match an existing - * command. Once the help method is properly implemented we will need to change - * our tests - */ @Test - void helpMethodOneArg() throws IOException { - int expectedErrorCode = JsonRpcError.Error.INVALID_PARAMS.getCode(); - var expectedErrorMessagePrefix = "Invalid params:"; - JsonRpcStatusException exception = - assertThrows(JsonRpcStatusException.class, () -> { - try (var client = new DefaultRpcClient(endpoint, "", "")) { - client.send("help", "echo"); - } - }); - assertTrue(Objects.requireNonNull(Objects.requireNonNull(exception.getMessage())).startsWith(expectedErrorMessagePrefix)); - assertEquals(expectedErrorCode, exception.jsonRpcCode); + void helpForEchoMethod() throws IOException { + var expectedMethodNameSubstring = "echo "; + var expectedParamNameSubstring = "message (string, required)"; + var expectedDescriptiveTextSubstring = "Returns the provided message exactly as it was sent."; + String result = (String) client.send("help", "echo"); + assertTrue(result.contains(expectedMethodNameSubstring)); + assertTrue(result.contains(expectedParamNameSubstring)); + assertTrue(result.contains(expectedDescriptiveTextSubstring)); + + } + + @Test + void helpForStopMethod() throws IOException { + var expectedMethodNameSubstring = "stop"; + var expectedParamNameSubstring = "None."; + var expectedDescriptiveTextSubstring = "Initiates the shutdown process of the JSON-RPC server."; + String result = (String) client.send("help", "stop"); + assertTrue(result.contains(expectedMethodNameSubstring)); + assertTrue(result.contains(expectedParamNameSubstring)); + assertTrue(result.contains(expectedDescriptiveTextSubstring)); + } + + @Test + void helpMethodNoArg() throws IOException { + var expectedResult = "echo message\n" + + "help (method)\n" + + "stop "; + String result = (String) client.send("help"); + assertEquals(expectedResult, result); } @Test @@ -121,9 +137,7 @@ void invalidMethod() throws IOException { var expectedErrorMessagePrefix = "Method not found:"; JsonRpcStatusException exception = assertThrows(JsonRpcStatusException.class, () -> { - try (var client = new DefaultRpcClient(endpoint, "", "")) { - client.send("invalid"); - } + client.send("invalid"); }); assertTrue(Objects.requireNonNull(exception.getMessage()).startsWith(expectedErrorMessagePrefix)); assertEquals(expectedErrorCode, exception.jsonRpcCode); diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/help/JsonRpcHelp.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/help/JsonRpcHelp.java new file mode 100644 index 000000000..3d3cefde1 --- /dev/null +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/help/JsonRpcHelp.java @@ -0,0 +1,43 @@ +/* + * Copyright 2014-2026 ConsensusJ Developers. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.consensusj.jsonrpc.help; + +/** + * Recordlike object to store help information about a given command + */ +public class JsonRpcHelp { + private final String summary; + private final String detail; + + public JsonRpcHelp(String summary, String detail) { + this.summary = summary; + this.detail = detail; + } + + /** + * @return The summary information for a command. + */ + public String summary() { + return summary; + } + + /** + * @return The detailed information for a command. + */ + public String detail() { + return detail; + } +} diff --git a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/services/EchoJsonRpcService.java b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/services/EchoJsonRpcService.java index 9201c89a5..efee8da4a 100644 --- a/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/services/EchoJsonRpcService.java +++ b/consensusj-jsonrpc/src/main/java/org/consensusj/jsonrpc/services/EchoJsonRpcService.java @@ -25,7 +25,10 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.util.Map; +import org.consensusj.jsonrpc.help.JsonRpcHelp; import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; /** * Simple Echo JSON-RPC Service @@ -33,10 +36,49 @@ public class EchoJsonRpcService extends AbstractJsonRpcService implements Closeable { private static final Logger log = LoggerFactory.getLogger(EchoJsonRpcService.class); private static final Map methods = JsonRpcServiceWrapper.reflect(MethodHandles.lookup().lookupClass()); - private static final String helpString = - "echo message\n" + - "help\n" + - "stop\n"; + private static final Map helpMap = Map.of( + "echo", new JsonRpcHelp("message", + "Usage:\n" + + " echo \n" + + "\n" + + "Description:\n" + + " Returns the provided message exactly as it was sent.\n" + + "\n" + + "Parameters:\n" + + " message (string, required)\n" + + " The text to be echoed back by the server.\n" + + "\n" + + "Example:\n" + + " echo \"hello world\"\n"), + "help", new JsonRpcHelp("(method)", + "Usage:\n" + + " help \n" + + "\n" + + "Description:\n" + + " Displays detailed help text for the specified method.\n" + + " If the method name is not recognized or not given, a list of available\n" + + " methods and their parameters is returned instead.\n" + + "\n" + + "Parameters:\n" + + " method (string, optional)\n" + + " The name of the method to display help for.\n" + + "\n" + + "Example:\n" + + " help echo\n"), + "stop", new JsonRpcHelp("", + "Usage:\n" + + " stop\n" + + "\n" + + "Description:\n" + + " Initiates the shutdown process of the JSON-RPC server.\n" + + " The server will respond to this request before the\n" + + " shutdown completes, allowing the client to receive\n" + + " confirmation of the action.\n" + + "\n" + + "Parameters:\n" + + " None.\n") + ); + private static final String allMethodsHelpString = createAllMethodsHelpString(EchoJsonRpcService::createHelpStringLine); private final JsonRpcShutdownService shutdownService; @@ -50,14 +92,30 @@ public void close() { log.info("Closing"); } + /** + * Echo a given message back to the client. + * @param message: A string containing the message to be echoed + * @return A string containing the echoed message + */ public CompletableFuture echo(String message) { log.debug("EchoJsonRpcService: echo {}", message); return result(message); } - public CompletableFuture help() { + /** + * Get detailed help information for a given command or a summary for all commands. + * @param method: A string containing the method name or null for all commands + * @return A string containing help information + */ + public CompletableFuture help(String method) { log.debug("EchoJsonRpcService: help"); - return result(helpString); + if (method == null) { + return result(allMethodsHelpString); + } else if (helpMap.containsKey(method)) { + return result(helpMap.get(method).detail()); + } else { + return result("Method not found.\n" + allMethodsHelpString); + } } /** @@ -70,4 +128,26 @@ public CompletableFuture stop() { String message = shutdownService.stopServer(); return result(message); } + + /** + * Create a string containing the name of each method and its parameters in alphabetical order. + * @param formatFunction A function determining how to format each line + * @return A string containing the name and parameters of each method + */ + private static String createAllMethodsHelpString(Function, String> formatFunction) { + return helpMap.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(formatFunction) + .collect(Collectors.joining("\n")); + } + + /** + * Default formatter. Appends the method's summary delimited with a space + * @param entry Map entry for a given method from `helpMap` + * @return Formatted line for `helpString` + */ + private static String createHelpStringLine(Map.Entry entry) { + return entry.getKey() + " " + entry.getValue().summary(); + } + } diff --git a/consensusj-jsonrpc/src/test/java/org/consensusj/jsonrpc/services/EchoJsonRpcServiceTest.java b/consensusj-jsonrpc/src/test/java/org/consensusj/jsonrpc/services/EchoJsonRpcServiceTest.java index 55821bdf4..c8620d780 100644 --- a/consensusj-jsonrpc/src/test/java/org/consensusj/jsonrpc/services/EchoJsonRpcServiceTest.java +++ b/consensusj-jsonrpc/src/test/java/org/consensusj/jsonrpc/services/EchoJsonRpcServiceTest.java @@ -42,14 +42,14 @@ void testEcho() { @Test void testHelpSummary() { - String help = service.help(/* null */).join(); - assertEquals("echo message\nhelp\nstop\n", help); + String help = service.help(null).join(); + assertEquals("echo message\nhelp (method)\nstop ", help); } @Test @Disabled void testHelpDetail() { - String help = service.help(/* "help" */).join(); + String help = service.help("help").join(); assertEquals("help help", help); } }