Skip to content

Commit

Permalink
Regex based name transformations for options in Mixins to allow for M…
Browse files Browse the repository at this point in the history
…ixin reuse in the same command.

Addresses issue remkop#2310

	# Please enter the commit message for your changes. Lines starting
  • Loading branch information
Alex Turc committed Jun 25, 2024
1 parent b03121b commit 00e4649
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 1 deletion.
66 changes: 66 additions & 0 deletions docs/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9595,6 +9595,72 @@ class MyApp : Runnable {
}
----

==== Reusing a Mixin within the same command

Here is an example on how a Mixin can be reused in the same command.
Option names within the mixin can be transformed using regular expressions to create a command line without
name collisions.

.Java
[source,java,role="primary"]
----
public class DbParams {

@Option(names={"--url"})
private String url;

@Option(names={"--user"})
private String user;

@Option(names={"--pass"})
private String pass;
}

@Command(name = "compare", description = "Example reuse with @Mixin annotation.")
public class MyCommand {

// Adds the options defined in DbParams to this command with names transformed.
// The options exposed by this Mixin will be --firstDb.url, --firstDb.user, --firstDb.pass
@Mixin(optionNameTransformations = {"^--(.*)$", "--firstDb.$1"})
private DbParams firstDb;

// Adds the options defined in DbParams to this command with names transformed.
// The options exposed by this Mixin will be --secondDb.url, --secondDb.user, --secondDb.pass
@Mixin(optionNameTransformations = {"^--(.*)$", "--secondDb.$1"})
private DbParams secondDb;
}
----

.Kotlin
[source,kotlin,role="secondary"]
----
public class DbParams {

@Option(names = ["--url"])
private url: String

@Option(names = ["--user"])
private user: String

@Option(names = ["--pass"])
private pass: String
}

@Command(name = "compare", description = ["Example reuse with @Mixin annotation."])
public class MyCommand {

// Adds the options defined in DbParams to this command with names transformed.
// The options exposed by this Mixin will be --firstDb.url, --firstDb.user, --firstDb.pass
@Mixin(optionNameTransformations = ["^--(.*)$", "--firstDb.$1"])
var firstDb: DbParams

// Adds the options defined in DbParams to this command with name transformed.
// The options exposed by this Mixin will be --secondDb.url, --secondDb.user, --secondDb.pass
@Mixin(optionNameTransformations = ["^--(.*)$", "--secondDb.$1"])
var secondDb: DbParams
}
----

==== Accessing the Mixee from a Mixin

Sometimes you need to write a Mixin class that accesses the mixee (the command where it is mixed in).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
class MixinInfo {
private final String mixinName;
private final String[] optionNameTransformations;
private final IAnnotatedElement annotatedElement;
private final VariableElement element;
private final CommandSpec mixin;
Expand All @@ -29,6 +30,7 @@ public MixinInfo(VariableElement element, CommandSpec mixin) {
name = element.getSimpleName().toString();
}
this.mixinName = name;
this.optionNameTransformations = element.getAnnotation(CommandLine.Mixin.class).optionNameTransformations();
Element targetType = element.getEnclosingElement();
int position = -1;
if (EnumSet.of(ElementKind.METHOD, ElementKind.CONSTRUCTOR).contains(targetType.getKind())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ public String getMixinName() {
return empty(annotationName) ? getName() : annotationName;
}

public String[] getOptionNameTransformations() {
return getAnnotation(CommandLine.Mixin.class).optionNameTransformations();
}

static String propertyName(String methodName) {
if (methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { return decapitalize(methodName.substring(3)); }
return decapitalize(methodName);
Expand Down
44 changes: 43 additions & 1 deletion src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import picocli.CommandLine.Help.Ansi.IStyle;
import picocli.CommandLine.Help.Ansi.Style;
Expand Down Expand Up @@ -4467,6 +4469,12 @@ public enum ScopeType {
* If not specified the name of the annotated field is used.
* @return a String to register the mixin object with, or an empty String if the name of the annotated field should be used */
String name() default "";
/** Optionally specify regex based transformations for all options in a mixin.
* The array contains sequences of regex expression followed by a replacement expression.
* @return an array of transformations {@code {regex, replacement, regex, replacement, ...}}.
* @see java.util.regex.Pattern
* @see java.util.regex.Matcher#replaceFirst(String) */
String[] optionNameTransformations() default {};
}
/**
* Fields annotated with {@code @Spec} will be initialized with the {@code CommandSpec} for the command the field is part of. Example usage:
Expand Down Expand Up @@ -7427,6 +7435,36 @@ public CommandSpec mixinStandardHelpOptions(boolean newValue) {
* @return this CommandSpec for method chaining */
public CommandSpec withToString(String newValue) { this.toString = newValue; return this; }

public CommandSpec withOptionNameTransformations(String[] transformations) {
if (!options.isEmpty() && transformations.length > 0) {
List<OptionSpec> optionsClone = new ArrayList<>(options);
Pattern[] patterns = new Pattern[transformations.length / 2];
String[] replacements = new String[transformations.length / 2];
for (int i = 0; i < transformations.length; i += 2) {
patterns[i] = Pattern.compile(transformations[i]);
replacements[i] = transformations[i + 1];
}
optionsClone.forEach(
option -> {
remove(option);
String[] transformedNames = new String[option.names.length];
for (int i = 0; i < option.names.length; i++) {
transformedNames[i] = option.names[i];
for (int j = 0; j < patterns.length; j++) {
Matcher matcher = patterns[j].matcher(option.names[i]);
if (matcher.matches()) {
transformedNames[i] = matcher.replaceFirst(replacements[j]);
break;
}
}
}
addOption(option.toBuilder().names(transformedNames).build());
}
);
}
return this;
}

/**
* Updates the following attributes from the specified {@code @Command} annotation:
* aliases, {@link ParserSpec#separator() parser separator}, command name, version, help command,
Expand Down Expand Up @@ -11347,6 +11385,7 @@ public interface IAnnotatedElement {
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
String getName();
String getMixinName();
String[] getOptionNameTransformations();
boolean isArgSpec();
boolean isOption();
boolean isParameter();
Expand Down Expand Up @@ -11539,6 +11578,9 @@ public String getMixinName() {
String annotationName = getAnnotation(Mixin.class).name();
return empty(annotationName) ? getName() : annotationName;
}
public String[] getOptionNameTransformations() {
return getAnnotation(Mixin.class).optionNameTransformations();
}
static String propertyName(String methodName) {
if (methodName.length() > 3 && (methodName.startsWith("get") || methodName.startsWith("set"))) { return decapitalize(methodName.substring(3)); }
return decapitalize(methodName);
Expand Down Expand Up @@ -12058,7 +12100,7 @@ private static CommandSpec buildMixinForMember(IAnnotatedElement member, IFactor
member.setter().set(userObject);
}
CommandSpec result = CommandSpec.forAnnotatedObject(userObject, factory);
return result.withToString(member.getToString());
return result.withOptionNameTransformations(member.getOptionNameTransformations()).withToString(member.getToString());
} catch (InitializationException ex) {
throw ex;
} catch (Exception ex) {
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/picocli/MixinTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.io.File;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Map;

import static org.junit.Assert.*;
Expand Down Expand Up @@ -1100,4 +1101,23 @@ public void testIssue1836CommandAliasOnMixin() {
Help help = new Help(new App_Issue1836());
assertEquals("list, ls", help.commandList().trim());
}

@Test
public void testOptionNameTransformations() {
class ReusableMixin {
@Option(names = "--url")
String url;
}
class Application {
@Mixin(optionNameTransformations = {"^--(.*)$", "--firstDb.$1"})
ReusableMixin firstDb;
@Mixin(optionNameTransformations = {"^--(.*)$", "--secondDb.$1"})
ReusableMixin secondDb;
}
Application application = new Application();
CommandLine commandLine = new CommandLine(application, new InnerClassFactory(this));
List<OptionSpec> options = commandLine.getCommandSpec().options();
assertArrayEquals(options.get(0).names(), new String[]{"--firstDb.url"});
assertArrayEquals(options.get(1).names(), new String[]{"--secondDb.url"});
}
}
1 change: 1 addition & 0 deletions src/test/java/picocli/ModelArgSpecTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ private static class AnnotatedImpl implements CommandLine.Model.IAnnotatedElemen
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) { return null;}
public String getName() {return name;}
public String getMixinName() {return null;}
public String[] getOptionNameTransformations() {return null;}
public boolean isArgSpec() {return false;}
public boolean isOption() {return false;}
public boolean isParameter() {return false;}
Expand Down

0 comments on commit 00e4649

Please sign in to comment.