Skip to content
Closed
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 @@ -49,6 +49,7 @@
* is shared between threads.
*
* @author Thomas Vitale
* @author Sun Yuhan
* @since 1.0.0
*/
public class StTemplateRenderer implements TemplateRenderer {
Expand Down Expand Up @@ -165,16 +166,25 @@ private Set<String> getInputVariables(ST st) {
else if (token.getType() == STLexer.RDELIM) {
isInsideList = false;
}
// Only add IDs that are not function calls (i.e., not immediately followed by
// Handle regular variables - only add IDs that are at the start of an
// expression
else if (!isInsideList && token.getType() == STLexer.ID) {
// Check if this ID is a function call
boolean isFunctionCall = (i + 1 < tokens.size() && tokens.get(i + 1).getType() == STLexer.LPAREN);
boolean isDotProperty = (i > 0 && tokens.get(i - 1).getType() == STLexer.DOT);
// Only add as variable if:
// - Not a function call
// - Not a built-in function used as property (unless validateStFunctions)
if (!isFunctionCall && (!Compiler.funcs.containsKey(token.getText()) || this.validateStFunctions
|| !(isDotProperty && Compiler.funcs.containsKey(token.getText())))) {
inputVariables.add(token.getText());

// Check if this ID is at the beginning of an expression (not a property
// access)
boolean isAfterDot = (i > 0 && tokens.get(i - 1).getType() == STLexer.DOT);

// Only add IDs that are:
// 1. Not function calls
// 2. Not property values (not preceded by a dot)
// 3. Either not built-in functions or we're validating functions
if (!isFunctionCall && !isAfterDot) {
String varName = token.getText();
if (!Compiler.funcs.containsKey(varName) || this.validateStFunctions) {
inputVariables.add(varName);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,60 @@ void shouldRenderTemplateWithBuiltInFunctions() {
assertThat(result).isEqualTo("Hello!");
}

/**
* Tests that property access syntax like {test.name} is correctly handled. The
* top-level variable 'test' should be identified as required, but 'name' should not.
*/
@Test
void shouldHandlePropertyAccessSyntax() {
StTemplateRenderer renderer = StTemplateRenderer.builder().build();
Map<String, Object> variables = new HashMap<>();
variables.put("test", Map.of("name", "Spring AI"));

String result = renderer.apply("Hello {test.name}!", variables);

assertThat(result).isEqualTo("Hello Spring AI!");
}

/**
* Tests that deep property access syntax like {test.tom.name} is correctly handled.
* Only the top-level variable 'test' should be identified as required.
*/
@Test
void shouldHandleDeepPropertyAccessSyntax() {
StTemplateRenderer renderer = StTemplateRenderer.builder().build();
Map<String, Object> variables = new HashMap<>();
variables.put("test", Map.of("tom", Map.of("name", "Spring AI")));

String result = renderer.apply("Hello {test.tom.name}!", variables);

assertThat(result).isEqualTo("Hello Spring AI!");
}

/**
* Tests validation behavior with property access syntax. Should only require the
* top-level variable, not the property names.
*/
@Test
void shouldValidatePropertyAccessCorrectly() {
StTemplateRenderer renderer = StTemplateRenderer.builder().build();
Map<String, Object> variables = new HashMap<>();
// Only provide the top-level variable, not the properties
variables.put("user", Map.of("profile", Map.of("name", "John")));

// This should work fine since we provide the required top-level variable
String result = renderer.apply("Hello {user.profile.name}!", variables);
assertThat(result).isEqualTo("Hello John!");

// Test with missing top-level variable - should throw exception
Map<String, Object> missingVariables = new HashMap<>();
// Wrong: providing nested variable instead of top-level
missingVariables.put("profile", Map.of("name", "John"));

assertThatThrownBy(() -> renderer.apply("Hello {user.profile.name}!", missingVariables))
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining(
"Not all variables were replaced in the template. Missing variable names are: [user]");
}

}