From 53edca779e37a2cdf61944a80f8a17c968e3326a Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 3 Jun 2022 10:37:10 -0700 Subject: [PATCH] Add support for CodeSection named parameters This commit allows getters of any currently associated CodeSection to be used as named parameters in AbstractCodeWriter templates. The context bag is checked first, and if not found, methods of the associated CodeSection, if present, are checked (x() and then getX()). --- .../smithy/utils/AbstractCodeWriter.java | 43 +++++++++++++++- .../amazon/smithy/utils/CodeWriterTest.java | 49 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java index beef352b01d..ca70466927b 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/AbstractCodeWriter.java @@ -15,6 +15,7 @@ package software.amazon.smithy.utils; +import java.lang.reflect.Method; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; @@ -149,7 +150,8 @@ * *

Named parameters

* - *

Named parameters are parameters that take a value from the context of + *

Named parameters are parameters that take a value from the context bag of + * the current state or using getters of the {@link CodeSection} associated with * the current state. They take the following form {@code $:}, * where {@code } is a string that starts with a lowercase letter, * followed by any number of {@code [A-Za-z0-9_#$.]} characters, and @@ -164,6 +166,13 @@ * // Outputs: "a b" * } * + *

The context bag is checked first, and then if the parameter is not found, + * getters of the currently associated CodeSection are checked. If a getter is + * found that matches the key exactly, then that getter is invoked and used as + * the named parameter. If a getter is found that matches + * "get" + uppercase_first_letter(key), then that getter is used as the named + * parameter. + * *

Escaping interpolation

* *

You can escape the "$" character using two "$$". @@ -1888,7 +1897,37 @@ public T removeContext(String key) { * @return Returns the associated value or null if not present. */ public Object getContext(String key) { - return currentState.context.peek().get(key); + CodeSection section = currentState.sectionValue; + Map currentContext = currentState.context.peek(); + if (currentContext.containsKey(key)) { + return currentContext.get(key); + } else if (section != null) { + Method method = findContextMethod(section, key); + if (method != null) { + try { + return method.invoke(section); + } catch (ReflectiveOperationException e) { + String message = String.format( + "Unable to get context '%s' from a matching method of the current CodeSection: %s %s", + key, + e.getCause() != null ? e.getCause().getMessage() : e.getMessage(), + getDebugInfo()); + throw new RuntimeException(message, e); + } + } + } + return null; + } + + private Method findContextMethod(CodeSection section, String key) { + for (Method method : section.getClass().getMethods()) { + if (method.getName().equals(key) || method.getName().equals("get" + StringUtils.capitalize(key))) { + if (!method.getReturnType().equals(Void.TYPE)) { + return method; + } + } + } + return null; } /** diff --git a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java index 99ac40e4c23..af0faf97085 100644 --- a/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java +++ b/smithy-utils/src/test/java/software/amazon/smithy/utils/CodeWriterTest.java @@ -1416,6 +1416,55 @@ public void filteringAllStackFramesEmitsNoStackComment() { assertThat(writer.toString(), equalTo("Hello\n")); } + @Test + public void canAccessCodeSectionGettersFromTemplates() { + SimpleCodeWriter writer = new SimpleCodeWriter(); + writer.pushState(new MySection()); + writer.write("${foo:L}: ${ten:L}... ${nope:L}."); + writer.popState(); + + assertThat(writer.toString(), equalTo("foo: 10... .\n")); + } + + @Test + public void providesContextWhenBadGetterIsCalled() { + RuntimeException e = Assertions.assertThrows(RuntimeException.class, () -> { + SimpleCodeWriter writer = new SimpleCodeWriter(); + writer.pushState(new MySection()); + writer.write("${bad:L}"); + }); + + assertThat(e.getMessage(), containsString("Unable to get context 'bad' from a matching method of the current " + + "CodeSection: This was bad! ")); + // The debug info contains the class name of the section. + assertThat(e.getMessage(), containsString(MySection.class.getCanonicalName())); + } + + @Test + public void namedContextValuesOverrideSectionGetters() { + SimpleCodeWriter writer = new SimpleCodeWriter(); + writer.pushState(new MySection()); + writer.putContext("bad", "ok actually"); + writer.write("${foo:L}: ${bad:L}"); + writer.popState(); + + assertThat(writer.toString(), equalTo("foo: ok actually\n")); + } + + private static final class MySection implements CodeSection { + public String getFoo() { + return "foo"; + } + + public int getTen() { + return 10; + } + + public String bad() { + throw new RuntimeException("This was bad!"); + } + } + private static final class MyCustomWriter extends AbstractCodeWriter { // Ensure that subclass methods are automatically filtered out as irrelevant frames. @Override