Skip to content
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

Add support for CodeSection named parameters #1256

Merged
merged 1 commit into from
Jun 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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