-
Notifications
You must be signed in to change notification settings - Fork 394
Introduce new command builder DSL #1211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| root = true | ||
|
|
||
| [*.{java,xml,gradle}] | ||
| indent_style = tab | ||
| indent_size = 4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,11 +13,13 @@ | |
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.springframework.shell.core.command; | ||
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.function.Consumer; | ||
|
|
||
| import org.springframework.shell.core.commands.AbstractCommand; | ||
|
|
||
| /** | ||
| * @author Eric Bottard | ||
|
|
@@ -74,4 +76,70 @@ default List<String> getAliases() { | |
| */ | ||
| ExitStatus execute(CommandContext commandContext) throws Exception; | ||
|
|
||
| /** | ||
| * Creates and returns a new instance of a {@code Builder} for defining and | ||
| * constructing commands. | ||
| * <p> | ||
| * The builder allows customization of command properties such as name, description, | ||
| * group, help text, aliases, and execution logic. | ||
| * @return a new {@code Builder} instance for configuring and creating commands | ||
| */ | ||
| static Builder builder() { | ||
| return new DefaultCommandBuilder(); | ||
| } | ||
|
|
||
| /** | ||
| * Builder for creating command. | ||
| */ | ||
| interface Builder { | ||
|
|
||
| /** | ||
| * Set the name of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder name(String name); | ||
|
|
||
| /** | ||
| * Set the description of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder description(String description); | ||
|
|
||
| /** | ||
| * Set the help of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder help(String help); | ||
|
|
||
| /** | ||
| * Set the group of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder group(String group); | ||
|
|
||
| /** | ||
| * Set the aliases of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder aliases(String... aliases); | ||
|
|
||
| /** | ||
| * Set the aliases of the command. | ||
| * @return this builder | ||
| */ | ||
| Builder aliases(List<String> aliases); | ||
|
||
|
|
||
| /** | ||
| * Set command execution logic. | ||
| * @return this builder | ||
| */ | ||
| Builder execute(Consumer<CommandContext> commandExecutor); | ||
|
|
||
| /** | ||
| * Build the {@link AbstractCommand}. | ||
| */ | ||
| AbstractCommand build(); | ||
|
||
|
|
||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package org.springframework.shell.core.command; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.function.Consumer; | ||
|
|
||
| import org.jspecify.annotations.Nullable; | ||
|
|
||
| import org.springframework.shell.core.commands.AbstractCommand; | ||
| import org.springframework.shell.core.commands.adapter.ConsumerCommandAdapter; | ||
| import org.springframework.util.Assert; | ||
| import static org.springframework.shell.core.command.Command.*; | ||
|
|
||
| /** | ||
| * Default implementation of {@link Builder}. | ||
| * | ||
| * @author Piotr Olaszewski | ||
| */ | ||
| class DefaultCommandBuilder implements Builder { | ||
|
|
||
| private @Nullable String name; | ||
|
|
||
| private @Nullable String description; | ||
|
|
||
| private String group = ""; | ||
|
|
||
| private String help = ""; | ||
|
|
||
| private @Nullable List<String> aliases; | ||
|
|
||
| private @Nullable Consumer<CommandContext> commandContextConsumer; | ||
|
|
||
| @Override | ||
| public Builder name(String name) { | ||
| this.name = name; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder description(String description) { | ||
| this.description = description; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder help(String help) { | ||
| this.help = help; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder group(String group) { | ||
| this.group = group; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder aliases(String... aliases) { | ||
| this.aliases = Arrays.asList(aliases); | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder aliases(List<String> aliases) { | ||
| this.aliases = aliases; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public Builder execute(Consumer<CommandContext> commandExecutor) { | ||
| this.commandContextConsumer = commandExecutor; | ||
| return this; | ||
| } | ||
|
|
||
| @Override | ||
| public AbstractCommand build() { | ||
| ConsumerCommandAdapter abstractCommand = initCommand(); | ||
|
|
||
| if (aliases != null) { | ||
| abstractCommand.setAliases(aliases); | ||
| } | ||
|
|
||
| return abstractCommand; | ||
| } | ||
|
|
||
| private ConsumerCommandAdapter initCommand() { | ||
|
||
| Assert.hasText(name, "'name' must be specified"); | ||
| Assert.hasText(description, "description"); | ||
| Assert.notNull(commandContextConsumer, "'commandExecutor' must not be null"); | ||
|
|
||
| return new ConsumerCommandAdapter(name, description, group, help, commandContextConsumer); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| @NullMarked | ||
| package org.springframework.shell.core.commands.adapter; | ||
|
|
||
| import org.jspecify.annotations.NullMarked; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,13 +3,20 @@ | |
| import java.util.List; | ||
|
|
||
| import org.jline.terminal.Terminal; | ||
| import org.jline.utils.AttributedStringBuilder; | ||
|
|
||
| import org.springframework.context.ApplicationContext; | ||
| import org.springframework.context.annotation.AnnotationConfigApplicationContext; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.shell.core.ShellRunner; | ||
| import org.springframework.shell.core.command.CommandContext; | ||
| import org.springframework.shell.core.command.annotation.*; | ||
| import org.springframework.shell.core.command.annotation.Argument; | ||
| import org.springframework.shell.core.command.annotation.Arguments; | ||
| import org.springframework.shell.core.command.annotation.Command; | ||
| import org.springframework.shell.core.command.annotation.EnableCommand; | ||
| import org.springframework.shell.core.command.annotation.Option; | ||
| import org.springframework.shell.core.commands.AbstractCommand; | ||
| import static org.jline.utils.AttributedStyle.*; | ||
|
|
||
| @EnableCommand(SpringShellApplication.class) | ||
| public class SpringShellApplication { | ||
|
|
@@ -43,6 +50,26 @@ public void sayYo(CommandContext commandContext) { | |
| terminal.writer().println("Yo there! what's up?"); | ||
| } | ||
|
|
||
| @Bean | ||
| public AbstractCommand sayGoodMorning() { | ||
| return org.springframework.shell.core.command.Command.builder() | ||
| .name("good-morning") | ||
| .description("Say good morning") | ||
| .group("greetings") | ||
| .help("A command that greets the user with 'Good morning!'") | ||
| .execute(commandContext -> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about using the overloaded method that takes a That would be a good example to show how to print styled output without dealing with terminal details. |
||
| String ansiString = new AttributedStringBuilder().append("Good morning ") | ||
| .append("Sir", BOLD.foreground(GREEN)) | ||
| .append("!") | ||
| .toAnsi(); | ||
|
|
||
| Terminal terminal = commandContext.terminal(); | ||
| terminal.writer().println(ansiString); | ||
| terminal.flush(); | ||
| }) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public HelloCommand sayHello() { | ||
| return new HelloCommand(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the Builder should be an interface with a default implementation (creating a custom user-defined builder is not very likely). There are of course good reasons to make a concept configurable/swappable by defining it in an interface/contract, but for builders I am not sure we need that (at least for the most typical average usage). For example, the
RetryPolicyin Spring Framework defines a builder in the interface, and it is a static inner class and not an interface + default implementation: https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java.So to keep things simple, can you please follow the same approach? Thank you upfront.