Skip to content

Add YAML pre-processing step to strip double quotes from pure Thymeleaf placeholder values #12

@snytkine

Description

@snytkine

Problem

When a YAML field whose expected type is non-string (e.g. integer, boolean) is written with a Thymeleaf placeholder wrapped in double quotes:

assertions:
  - type: status_code_equals
    value: "[[${suite.expected_status}]]"

YAML parses the raw value as the string "[[${suite.expected_status}]]". After Thymeleaf substitutes 201, the YAML still contains:

value: "201"

Jackson deserialises "201" as String, not Integer, which causes type mismatch failures at assertion time. Without double quotes the YAML would correctly yield value: 201 after substitution.

Root cause: authors instinctively quote Thymeleaf placeholders in YAML to satisfy editors/linters, but those quotes survive into post-substitution parsing.

Fix: a pre-processing pass that strips double quotes from YAML values that are entirely a Thymeleaf placeholder — i.e. the entire quoted value starts with the template open-bracket ([[) and ends with the close-bracket (]]).


Behaviour

Given this input line:

  value: "[[${suite.expected_status}]]"

The pre-processor outputs:

  value: [[${suite.expected_status}]]

Given a mixed value (prefix or suffix outside the placeholder), the line is left unchanged:

  url: "/api/[[${suite.version}]]/users"   # NOT stripped — not purely a placeholder

Trailing YAML inline comments must be preserved:

  value: "[[${suite.expected_status}]]"   # expected HTTP status

becomes:

  value: [[${suite.expected_status}]]   # expected HTTP status

Configurable bracket markers

The open- and close-bracket tokens must be defined as named constants (not inlined in the regex) so that switching to a different templating engine later requires changing only those two values:

// Thymeleaf TEXT-mode inline syntax
private static final String PLACEHOLDER_OPEN  = "[[";
private static final String PLACEHOLDER_CLOSE = "]]";

// Future: Pebble expression syntax
// private static final String PLACEHOLDER_OPEN  = "{{";
// private static final String PLACEHOLDER_CLOSE = "}}";

The detection regex is built from these constants at class-initialisation time using Pattern.quote() so that bracket characters with regex meaning are escaped safely.


Implementation

New class: TemplatePreProcessor

Create src/main/java/.../service/TemplatePreProcessor.java (or util/ — place it next to FileLoader). It must be a Spring @Service (or a stateless utility) and thread-safe.

/**
 * Pre-processes a raw YAML template string before Thymeleaf evaluation.
 *
 * <p>Strips double quotes from YAML scalar values that consist entirely of a
 * Thymeleaf inline placeholder (a value whose content starts with {@value #PLACEHOLDER_OPEN}
 * and ends with {@value #PLACEHOLDER_CLOSE}). This prevents YAML from locking a
 * post-substitution integer or boolean into a string type.
 *
 * <p>The open- and close-bracket constants are intentionally named so they can be
 * updated in one place if the project moves to a different template engine.
 *
 * <p>Thread-safety: stateless; the compiled {@link Pattern} is a static final field.
 */
public class TemplatePreProcessor {

    private static final String PLACEHOLDER_OPEN  = "[[";
    private static final String PLACEHOLDER_CLOSE = "]]";

    /** Matches: <indent><key><colon+space>"<open>...<close>"<optional trailing comment>  */
    private static final Pattern QUOTED_PLACEHOLDER = Pattern.compile(
        "^(\\s*[\\w.-]+\\s*:\\s*)\"("
            + Pattern.quote(PLACEHOLDER_OPEN)
            + ".*?"
            + Pattern.quote(PLACEHOLDER_CLOSE)
            + ")\"(\\s*(?:#.*)?)$");

    /**
     * Processes {@code yaml} line by line and removes double quotes that wrap a
     * pure Thymeleaf placeholder value.
     *
     * @param yaml raw YAML content (may be multi-line)
     * @return the transformed YAML string; identical to {@code yaml} when no
     *         matching lines are found
     */
    public String process(String yaml) { ... }
}

Algorithm inside process():

  1. Split the input on newlines (preserve the original line-ending style, or normalise to \n and restore).
  2. For each line apply QUOTED_PLACEHOLDER.matcher(line).
  3. If it matches, replace with group(1) + group(2) + group(3) (key + unquoted placeholder + trailing comment/whitespace).
  4. If it does not match, keep the line unchanged.
  5. Rejoin and return.

Integration in TestSuiteLoader

In both load(Path) and load(Path, SuiteRunContext), call preProcessor.process(templateContent) immediately after Files.readString(filePath) and use the result everywhere downstream — including the value stored in TestSuite.templateContent (which PureJavaTestEngine uses for per-test re-parsing):

// load(Path filePath, SuiteRunContext context)
String rawYaml = Files.readString(filePath);
String templateContent = preProcessor.process(rawYaml);   // ← new step
// ... existing Step 1 / Step 2 Thymeleaf passes use templateContent

Inject TemplatePreProcessor into TestSuiteLoader via constructor injection (GraalVM native image requires constructor injection).


Tests

TemplatePreProcessorTest (new):

Scenario Expected outcome
value: "[[${x}]]" value: [[${x}]]
timeout: "[[${suite.timeout_ms}]]" timeout: [[${suite.timeout_ms}]]
Trailing comment preserved value: [[${x}]] # comment
Mixed value "/api/[[${v}]]" unchanged
No placeholder present value: "hello" unchanged
Multi-line input — only matching lines change all other lines preserved verbatim

TestSuiteLoaderTest (update): add a test case where a suite YAML contains value: "[[${suite.my_int}]]", supplies my_int=42 as a suite variable, and asserts the assertion value field deserialises as Integer 42 (not String "42").


Implementation Notes

  • Pattern is compiled once as a static final field — thread-safe and efficient.
  • Pattern.quote() ensures bracket tokens with special regex meaning (e.g. future {{, }}) are escaped correctly without manual escaping.
  • No runtime reflection — safe for GraalVM native image.
  • Run ./mvnw spotless:apply before committing.
  • All new classes and methods must have JavaDoc.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions