Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,38 +80,42 @@ void echoMethodWrongNumberOfArgs() throws IOException {
assertEquals(expectedErrorCode, exception.jsonRpcCode);
}

@Test
void helpForHelpMethod() throws IOException {
var expectedResultSubstring = "Displays detailed help text for the specified method.";
try (var client = new DefaultRpcClient(endpoint, "", "")) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use @BeforeEach to initialize client for each test. Use @AfterEach to call client.close(). This will make the individual tests more readable by eliminating the call to DefaultRpcClient and the try block.

String result = (String) client.send("help", "help");
assertTrue(result.contains(expectedResultSubstring));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we don't need to match the entire text of the result, but we should verify that the result contains the method name and the correct argument name (and optional parens) So that's three asserts per test:

  1. contains method name
  2. contains parameter names and optionality info
  3. contains descriptive text

}
}

@Test
void helpMethod() throws IOException {
var expectedResult = """
echo message
help
stop
""";
void helpForEchoMethod() throws IOException {
var expectedResultSubstring = "Returns the provided message exactly as it was sent.";
try (var client = new DefaultRpcClient(endpoint, "", "")) {
String result = (String) client.send("help");
assertEquals(expectedResult, result);
String result = (String) client.send("help", "echo");
assertTrue(result.contains(expectedResultSubstring));
}
}

/*
* 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(exception.getMessage().startsWith(expectedErrorMessagePrefix));
assertEquals(expectedErrorCode, exception.jsonRpcCode);
void helpForStopMethod() throws IOException {
var expectedResultSubstring = "Initiates the shutdown process of the JSON-RPC server.";
try (var client = new DefaultRpcClient(endpoint, "", "")) {
String result = (String) client.send("help", "stop");
assertTrue(result.contains(expectedResultSubstring));
}
}

@Test
void helpMethodNoArg() throws IOException {
var expectedResult = "echo message\n" +
"help (method)\n" +
"stop ";
try (var client = new DefaultRpcClient(endpoint, "", "")) {
String result = (String) client.send("help");
assertEquals(expectedResult, result);
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,60 @@
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
*/
public class EchoJsonRpcService extends AbstractJsonRpcService implements Closeable {
private static final Logger log = LoggerFactory.getLogger(EchoJsonRpcService.class);
private static final Map<String, Method> methods = JsonRpcServiceWrapper.reflect(MethodHandles.lookup().lookupClass());
private static final String helpString =
"echo message\n" +
"help\n" +
"stop\n";
private static final Map<String, JsonRpcHelp> helpMap = Map.of(
"echo", new JsonRpcHelp("message",
"Usage:\n"
+ " echo <message>\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 <method>\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 helpString = createHelpString(EchoJsonRpcService::createHelpStringLine);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member should probably be renamed something like allMethodsHelpString or similar and have a JavaDoc comment explaining what it is.


private final JsonRpcShutdownService shutdownService;

Expand All @@ -50,14 +92,31 @@ 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<String> echo(String message) {
log.debug("EchoJsonRpcService: echo {}", message);
return result(message);
}

public CompletableFuture<String> help() {
/**
* Get detailed help information for a given command.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or all commands.

* @param method: A string containing the method name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... or null for all commands.

* @return A string containing help information
*/
public CompletableFuture<String> help(String method) {
log.debug("EchoJsonRpcService: help");
return result(helpString);
if (method == null) {
return result(helpString);
}
if (helpMap.containsKey(method)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use an else if here, just to be a little more clear that exactly one of the three conditions will be true.

return result(helpMap.get(method).detail());
} else {
return result("Method not found.\n" + helpString);
}
}

/**
Expand All @@ -70,4 +129,26 @@ public CompletableFuture<String> stop() {
String message = shutdownService.stopServer();
return result(message);
}

/**
* Create a string containing the name of each method and it's parameters in alphabetical order.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No apostrophe on "it's"

* @param formatFunction A function determining how to format each line
* @return A string containing the name and parameters of each method
*/
private static String createHelpString(Function<Map.Entry<String, JsonRpcHelp>, 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<String, JsonRpcHelp> entry) {
return entry.getKey() + " " + entry.getValue().summary();
}

}
Loading