From 0a8b78aca535ba43362f92988ce9e1c052022cbf Mon Sep 17 00:00:00 2001 From: Greg Gibeling Date: Wed, 10 Jan 2024 16:39:42 -0800 Subject: [PATCH] G2-1500 Support null named command arguments --- .../converter/dumb/DumbCommandConverter.java | 57 ++++++++++++------- .../converter/dumb/HDumbCommandConverter.java | 11 ++-- .../gearbox/command/converter/dumb/Named.java | 2 + .../dumb/TestDumbCommandConverter.java | 15 ++++- 4 files changed, 57 insertions(+), 28 deletions(-) diff --git a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/DumbCommandConverter.java b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/DumbCommandConverter.java index edd9498..6cc09b4 100644 --- a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/DumbCommandConverter.java +++ b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/DumbCommandConverter.java @@ -4,7 +4,9 @@ import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import java.util.stream.Stream; import com.g2forge.alexandria.annotations.note.Note; @@ -15,6 +17,7 @@ import com.g2forge.alexandria.command.invocation.format.ICommandFormat; import com.g2forge.alexandria.command.stdio.StandardIO; import com.g2forge.alexandria.java.core.enums.EnumException; +import com.g2forge.alexandria.java.core.error.HError; import com.g2forge.alexandria.java.core.error.NotYetImplementedError; import com.g2forge.alexandria.java.core.helpers.HCollection; import com.g2forge.alexandria.java.core.marker.ISingleton; @@ -118,8 +121,13 @@ protected static class ArgumentContext { builder.add(ArgumentContext.class, Boolean.class, bool); builder.add(ArgumentContext.class, Boolean.TYPE, bool); builder.fallback((c, v) -> { - if (v == null) HDumbCommandConverter.set(c, c.getArgument(), null); - else throw new IllegalArgumentException(String.format("Parameter %1$s cannot be converted to a command line argument because the type of \"2$s\" (%3$s) is unknown. Please consider implementing %4$s.", c.getArgument().getName(), v, v.getClass(), IArgumentRenderer.class.getSimpleName())); + if (v == null) { + final ISubject subject = c.getArgument().getMetadata(); + if (subject.isPresent(Working.class)) return; + if (subject.isPresent(Environment.class)) return; + if (subject.isPresent(EnvPath.class)) return; + HDumbCommandConverter.set(c, c.getArgument(), null); + } else throw new IllegalArgumentException(String.format("Parameter %1$s cannot be converted to a command line argument because the type of \"2$s\" (%3$s) is unknown. Please consider implementing %4$s.", c.getArgument().getName(), v, v.getClass(), IArgumentRenderer.class.getSimpleName())); }); }).build(); @@ -175,11 +183,13 @@ public ProcessInvocation apply(ProcessInvocation processInvocation, Me if (returnTypeRef.getErasedType().isAssignableFrom(Void.class) || returnTypeRef.getErasedType().isAssignableFrom(Void.TYPE)) commandInvocationBuilder.io(StandardIO.builder().standardInput(InheritRedirect.create()).standardOutput(InheritRedirect.create()).standardError(InheritRedirect.create()).build()); } - // Compute the command name + // Compute the command name & initial arguments commandInvocationBuilder.clearArguments(); final Command command = Metadata.getStandard().of(methodInvocation.getMethod()).get(Command.class); - if (command != null) Stream.of(command.value()).forEach(commandInvocationBuilder::argument); - else commandInvocationBuilder.argument(methodInvocation.getMethod().getName()); + final List commandArguments; + if (command != null) commandArguments = HCollection.asList(command.value()); + else commandArguments = HCollection.asList(methodInvocation.getMethod().getName()); + commandArguments.forEach(commandInvocationBuilder::argument); // Compute the result generator if (processInvocation.getResultSupplier() == null) { @@ -189,24 +199,31 @@ public ProcessInvocation apply(ProcessInvocation processInvocation, Me // Generate the command & environment from the method arguments final Parameter[] parameters = methodInvocation.getMethod().getParameters(); + final List throwables = new ArrayList<>(); for (int i = 0; i < parameters.length; i++) { - final Object value = methodInvocation.getArguments().get(i); - final IMethodArgument methodArgument = new MethodArgument(value, parameters[i]); - - final IArgumentRenderer argumentRenderer = methodArgument.getMetadata().get(IArgumentRenderer.class); - if (argumentRenderer != null) { - if (methodArgument.getMetadata().isPresent(Environment.class)) throw new NotYetImplementedError("Parameters with custom argument renderers cannot be used as environment variables (yet)!"); - @SuppressWarnings({ "unchecked", "rawtypes" }) - final List arguments = argumentRenderer.render((IMethodArgument) methodArgument); - commandInvocationBuilder.arguments(arguments); - } else { - final ArgumentContext argumentContext = new ArgumentContext(commandInvocationBuilder, environmentBuilder, methodArgument); - ARGUMENT_BUILDER.accept(argumentContext, value); + // Convert all the parameters and collect any exceptions, so that the final exception report is comprehensive + try { + final Object value = methodInvocation.getArguments().get(i); + final IMethodArgument methodArgument = new MethodArgument(value, parameters[i]); + + final IArgumentRenderer argumentRenderer = methodArgument.getMetadata().get(IArgumentRenderer.class); + if (argumentRenderer != null) { + if (methodArgument.getMetadata().isPresent(Environment.class)) throw new NotYetImplementedError("Parameters with custom argument renderers cannot be used as environment variables (yet)!"); + @SuppressWarnings({ "unchecked", "rawtypes" }) + final List arguments = argumentRenderer.render((IMethodArgument) methodArgument); + commandInvocationBuilder.arguments(arguments); + } else { + final ArgumentContext argumentContext = new ArgumentContext(commandInvocationBuilder, environmentBuilder, methodArgument); + ARGUMENT_BUILDER.accept(argumentContext, value); + } + + final Constant constant = methodArgument.getMetadata().get(Constant.class); + if ((constant != null) && (constant.value() != null)) commandInvocationBuilder.arguments(HCollection.asList(constant.value())); + } catch (Throwable throwable) { + throwables.add(throwable); } - - final Constant constant = methodArgument.getMetadata().get(Constant.class); - if ((constant != null) && (constant.value() != null)) commandInvocationBuilder.arguments(HCollection.asList(constant.value())); } + if (!throwables.isEmpty()) throw HError.withSuppressed(new RuntimeException(String.format("Failed to convert parameters to arguments for %1$s", commandArguments.stream().collect(Collectors.joining(" ")))), throwables); processInvocationBuilder.commandInvocation(commandInvocationBuilder.environment(environmentBuilder.build().simplify()).build()); return processInvocationBuilder.build(); diff --git a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/HDumbCommandConverter.java b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/HDumbCommandConverter.java index f73a56c..e7aeccd 100644 --- a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/HDumbCommandConverter.java +++ b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/HDumbCommandConverter.java @@ -27,13 +27,14 @@ public static void set(ArgumentContext argumentContext, IMethodArgument argum // Handle named arguments final Named named = metadata.get(Named.class); if (named != null) { - if (value == null) throw new NullPointerException("Named argument values cannot be null (though they can be the string spelling \"null\")!"); + if (value == null) { + if (named.skipNull()) return; + else throw new NullPointerException("Named argument values cannot be null (though they can be the string spelling \"null\")!"); + } if (!named.joined()) { command.argument(named.value()); command.argument(value); - } else { - command.argument(named.value() + value); - } + } else command.argument(named.value() + value); return; } @@ -41,7 +42,7 @@ public static void set(ArgumentContext argumentContext, IMethodArgument argum // Handle environment variables final Environment environment = metadata.get(Environment.class); if (environment != null) { - // Null environment values, do nothing + // Null environment values, do nothing if (value != null) argumentContext.getEnvironment().modifier(environment.value(), prior -> value); return; } diff --git a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/Named.java b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/Named.java index 8cda639..b43fadd 100644 --- a/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/Named.java +++ b/gb-command/src/main/java/com/g2forge/gearbox/command/converter/dumb/Named.java @@ -16,4 +16,6 @@ public String value(); public boolean joined() default true; + + public boolean skipNull() default false; } diff --git a/gb-command/src/test/java/com/g2forge/gearbox/command/converter/dumb/TestDumbCommandConverter.java b/gb-command/src/test/java/com/g2forge/gearbox/command/converter/dumb/TestDumbCommandConverter.java index 56a723a..fd54894 100644 --- a/gb-command/src/test/java/com/g2forge/gearbox/command/converter/dumb/TestDumbCommandConverter.java +++ b/gb-command/src/test/java/com/g2forge/gearbox/command/converter/dumb/TestDumbCommandConverter.java @@ -85,6 +85,10 @@ public interface IStringNamed extends ITestCommandInterface { public String method(@Named("name=") String argument); } + public interface IStringNamedSkipNull extends ITestCommandInterface { + public String method(@Named(value = "name=", skipNull = true) String argument); + } + public interface IStringNamedNonJoined extends ITestCommandInterface { public String method(@Named(value = "name", joined = false) String argument); } @@ -184,7 +188,7 @@ public void pathWorking() { assertCommand(IPathWorking.class, x -> x.method(Paths.get("A")), VoidResultSupplier.class, Paths.get("A"), "method"); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = RuntimeException.class) public void stringArrayNamed() { assertCommand(IStringArrayNamed.class, x -> x.method("A", "B"), StringResultSupplier.class, null); } @@ -209,7 +213,12 @@ public void stringNamedNonJoined() { @Test public void stringNamedNull() { final IStringNamed proxy = createProxy(IStringNamed.class); - HAssert.assertException(NullPointerException.class, () -> proxy.method(null)); + HAssert.assertException(RuntimeException.class, () -> proxy.method(null)); + } + + @Test + public void stringNamedNullSkip() { + assertCommand(IStringNamedSkipNull.class, x -> x.method(null), StringResultSupplier.class, null, "method"); } @Test @@ -220,6 +229,6 @@ public void stringValue() { @Test public void stringValueNull() { final IStringValue proxy = createProxy(IStringValue.class); - HAssert.assertException(NullPointerException.class, () -> proxy.method(null)); + HAssert.assertException(RuntimeException.class, () -> proxy.method(null)); } }