diff --git a/qctl-integration-tests/src/test/resources/fixtures/templates/test-computed-vars/template/src/main/java/$packagePath/App.java b/qctl-integration-tests/src/test/resources/fixtures/templates/test-computed-vars/template/src/main/java/__packagePath__/App.java similarity index 100% rename from qctl-integration-tests/src/test/resources/fixtures/templates/test-computed-vars/template/src/main/java/$packagePath/App.java rename to qctl-integration-tests/src/test/resources/fixtures/templates/test-computed-vars/template/src/main/java/__packagePath__/App.java diff --git a/qctl-integration-tests/src/test/resources/fixtures/templates/test-full-substitution/template/src/main/java/$packagePath/App.java b/qctl-integration-tests/src/test/resources/fixtures/templates/test-full-substitution/template/src/main/java/__packagePath__/App.java similarity index 100% rename from qctl-integration-tests/src/test/resources/fixtures/templates/test-full-substitution/template/src/main/java/$packagePath/App.java rename to qctl-integration-tests/src/test/resources/fixtures/templates/test-full-substitution/template/src/main/java/__packagePath__/App.java diff --git a/qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/$packagePath/.gitkeep b/qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/__packagePath__/.gitkeep similarity index 100% rename from qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/$packagePath/.gitkeep rename to qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/__packagePath__/.gitkeep diff --git a/qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/$packagePath/App.java b/qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/__packagePath__/App.java similarity index 100% rename from qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/$packagePath/App.java rename to qctl-integration-tests/src/test/resources/fixtures/templates/test-transforms/template/src/main/java/__packagePath__/App.java diff --git a/qctl-qqq/src/main/java/io/qrun/qctl/qqq/template/TemplateEngine.java b/qctl-qqq/src/main/java/io/qrun/qctl/qqq/template/TemplateEngine.java index 4d3f45b..5476d8d 100644 --- a/qctl-qqq/src/main/java/io/qrun/qctl/qqq/template/TemplateEngine.java +++ b/qctl-qqq/src/main/java/io/qrun/qctl/qqq/template/TemplateEngine.java @@ -20,6 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -29,6 +30,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.qrun.qctl.qqq.error.SuggestionEngine; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; @@ -68,6 +70,7 @@ public class TemplateEngine private final VelocityEngine velocityEngine; private final StringTool stringTool; + private final SuggestionEngine suggestionEngine; @@ -80,6 +83,7 @@ public class TemplateEngine { this.velocityEngine = createVelocityEngine(); this.stringTool = new StringTool(); + this.suggestionEngine = new SuggestionEngine(); } @@ -400,7 +404,13 @@ public class TemplateEngine public String renderPath(String path, Map variables) throws TemplateRenderException { - return renderContent(path, variables); + ///////////////////////////////////////////////////////////////////////// + // Convert __VARNAME__ placeholders to $VARNAME for Velocity. // + // This allows templates to use __VARNAME__ in paths, which is safer // + // for shells and build tools that might interpret $ as a variable. // + ///////////////////////////////////////////////////////////////////////// + String normalizedPath = path.replaceAll("__([a-zA-Z][a-zA-Z0-9]*)__", "\\$$1"); + return renderContent(normalizedPath, variables); } @@ -600,8 +610,12 @@ public class TemplateEngine String fullRef = matcher.group(0); if(template.contains(fullRef)) { - throw new TemplateRenderException( - "Undefined variable: " + varName); + ////////////////////////////////////////////////////////////// + // Collect available variables and find similar ones // + ////////////////////////////////////////////////////////////// + List available = collectAvailableVariables(context); + List suggestions = suggestionEngine.findSimilar(varName, available); + throw new TemplateRenderException(varName, suggestions, available); } } } @@ -609,6 +623,32 @@ public class TemplateEngine + /*************************************************************************** + * Collect all available variable names from the context. + * + * @param context Velocity context + * @return list of variable names + * @since 0.2.0 + ***************************************************************************/ + private List collectAvailableVariables(VelocityContext context) + { + List variables = new ArrayList<>(); + for(Object key : context.getKeys()) + { + String name = key.toString(); + ///////////////////////////////////////////////////////////////////// + // Skip internal tools like $str // + ///////////////////////////////////////////////////////////////////// + if(!"str".equals(name)) + { + variables.add(name); + } + } + return variables; + } + + + /*************************************************************************** * Resolve the template source directory. *