Skip to content

Commit

Permalink
Add support for CodeSection named parameters
Browse files Browse the repository at this point in the history
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()).
  • Loading branch information
mtdowling authored and Michael Dowling committed Jun 4, 2022
1 parent 3664330 commit e72486e
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -149,7 +150,8 @@
*
* <h3>Named parameters</h3>
*
* <p>Named parameters are parameters that take a value from the context of
* <p>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 $<variable>:<formatter>},
* where {@code <variable>} is a string that starts with a lowercase letter,
* followed by any number of {@code [A-Za-z0-9_#$.]} characters, and
Expand All @@ -164,6 +166,13 @@
* // Outputs: "a b"
* }</pre>
*
* <p>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.
*
* <h3>Escaping interpolation</h3>
*
* <p>You can escape the "$" character using two "$$".
Expand Down Expand Up @@ -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<String, Object> 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyCustomWriter> {
// Ensure that subclass methods are automatically filtered out as irrelevant frames.
@Override
Expand Down

0 comments on commit e72486e

Please sign in to comment.