From 18d831a1db6d38b17569f1b9b9da2e5725ae3fbb Mon Sep 17 00:00:00 2001 From: "I.Alfer" Date: Thu, 2 Nov 2023 10:45:44 +0100 Subject: [PATCH] BT-4346 Fauna templating --- build.gradle | 6 + .../fauna/query/template/FaunaTemplate.java | 124 ++++++++++++++++++ .../query/template/TemplatePartType.java | 8 ++ .../query/template/FaunaTemplateTest.java | 78 +++++++++++ 4 files changed, 216 insertions(+) create mode 100644 faunaJava/src/main/java/com/fauna/query/template/FaunaTemplate.java create mode 100644 faunaJava/src/main/java/com/fauna/query/template/TemplatePartType.java create mode 100644 faunaJava/src/test/java/com/fauna/query/template/FaunaTemplateTest.java diff --git a/build.gradle b/build.gradle index af621ed5..59dd960a 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,9 @@ subprojects { project(':faunaJava') { apply plugin: 'java' + compileJava { + options.encoding = 'UTF-8' + } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -37,6 +40,9 @@ project(':faunaJava') { project(':common') { apply plugin: 'java' + compileJava { + options.encoding = 'UTF-8' + } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/faunaJava/src/main/java/com/fauna/query/template/FaunaTemplate.java b/faunaJava/src/main/java/com/fauna/query/template/FaunaTemplate.java new file mode 100644 index 00000000..c0a3a13c --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/template/FaunaTemplate.java @@ -0,0 +1,124 @@ +package com.fauna.query.template; + +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FaunaTemplate implements Iterable { + + private static final char DELIMITER = '$'; + private static final String ID_PATTERN = "[\\p{L}_][\\p{L}\\p{N}_]*"; + private static final Pattern PATTERN; + + static { + String delim = Pattern.quote(String.valueOf(DELIMITER)); + String pattern = String.format( + "%s(?:(?%s)|\\{(?%s)\\}|\\{(?[^}]*))?", + delim, delim, ID_PATTERN + ); + PATTERN = Pattern.compile(pattern, Pattern.COMMENTS); + } + + private final String template; + + public FaunaTemplate(String template) { + this.template = template; + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private final Matcher matcher = PATTERN.matcher(template); + private int curPos = 0; + private boolean foundMatch = false; + + @Override + public boolean hasNext() { + if (curPos < template.length()) { + return true; + } + if (foundMatch) { + return true; + } + return matcher.find(curPos); + } + + @Override + public TemplatePart next() { + if (curPos >= template.length()) { + throw new IllegalStateException("No more elements"); + } + + if (foundMatch || (foundMatch = matcher.find(curPos))) { + int spanStartPos = matcher.start(); + int spanEndPos = matcher.end(); + String invalid = matcher.group("invalid"); + if (invalid != null) { + handleInvalid(matcher.start("invalid")); + } + String escapedPart = matcher.group("escaped"); + String variablePart = matcher.group("braced"); + + TemplatePart part; + if (escapedPart != null) { + String literalPart = template.substring(curPos, spanStartPos) + DELIMITER; + part = new TemplatePart(literalPart, TemplatePartType.LITERAL); + curPos = spanEndPos; + } else if (variablePart != null) { + if (curPos < spanStartPos) { + part = new TemplatePart(template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); + curPos = spanStartPos; + } else { + part = new TemplatePart(variablePart, TemplatePartType.VARIABLE); + curPos = spanEndPos; + } + } else { + part = new TemplatePart(template.substring(curPos, spanStartPos), TemplatePartType.LITERAL); + curPos = spanStartPos; + } + foundMatch = false; // Reset after processing a match + return part; + } else { + TemplatePart part = new TemplatePart(template.substring(curPos), TemplatePartType.LITERAL); + curPos = template.length(); + return part; + } + } + }; + } + + private void handleInvalid(int position) { + String substringUpToPosition = template.substring(0, position); + String[] lines = substringUpToPosition.split("\r?\n"); + int colno, lineno; + if (lines.length == 0) { + colno = 1; + lineno = 1; + } else { + String lastLine = lines[lines.length - 1]; + // Adjust the column number for invalid placeholder + colno = position - (substringUpToPosition.length() - lastLine.length()) - 1; // -1 to exclude the dollar sign + lineno = lines.length; + } + throw new IllegalArgumentException(String.format("Invalid placeholder in template: line %d, col %d", lineno, colno)); + } + + public static class TemplatePart { + private final String part; + private final TemplatePartType type; + + public TemplatePart(String part, TemplatePartType type) { + this.part = part; + this.type = type; + } + + public String getPart() { + return part; + } + + public TemplatePartType getType() { + return type; + } + } + +} diff --git a/faunaJava/src/main/java/com/fauna/query/template/TemplatePartType.java b/faunaJava/src/main/java/com/fauna/query/template/TemplatePartType.java new file mode 100644 index 00000000..f8df7718 --- /dev/null +++ b/faunaJava/src/main/java/com/fauna/query/template/TemplatePartType.java @@ -0,0 +1,8 @@ +package com.fauna.query.template; + +public enum TemplatePartType { + + LITERAL, + VARIABLE + +} diff --git a/faunaJava/src/test/java/com/fauna/query/template/FaunaTemplateTest.java b/faunaJava/src/test/java/com/fauna/query/template/FaunaTemplateTest.java new file mode 100644 index 00000000..327bf70a --- /dev/null +++ b/faunaJava/src/test/java/com/fauna/query/template/FaunaTemplateTest.java @@ -0,0 +1,78 @@ +package com.fauna.query.template; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FaunaTemplateTest { + + @Test + void testTemplate_WithSingleVariable() { + FaunaTemplate template = new FaunaTemplate("let x = ${my_var}"); + List expanded = new ArrayList<>(); + template.forEach(expanded::add); + assertEquals(2, expanded.size()); + assertEquals("let x = ", expanded.get(0).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(0).getType()); + assertEquals("my_var", expanded.get(1).getPart()); + assertEquals(TemplatePartType.VARIABLE, expanded.get(1).getType()); + } + + @Test + void testTemplate_WithDuplicateVariable() { + FaunaTemplate template = new FaunaTemplate("let x = ${my_var}\nlet y = ${my_var}\nx * y"); + List expanded = new ArrayList<>(); + template.forEach(expanded::add); + assertEquals(5, expanded.size()); + assertEquals("let x = ", expanded.get(0).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(0).getType()); + assertEquals("my_var", expanded.get(1).getPart()); + assertEquals(TemplatePartType.VARIABLE, expanded.get(1).getType()); + assertEquals("\nlet y = ", expanded.get(2).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(2).getType()); + assertEquals("my_var", expanded.get(3).getPart()); + assertEquals(TemplatePartType.VARIABLE, expanded.get(3).getType()); + assertEquals("\nx * y", expanded.get(4).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(4).getType()); + } + + @Test + void testTemplate_WithVariableAtStart() { + FaunaTemplate template = new FaunaTemplate("${my_var} { .name }"); + List expanded = new ArrayList<>(); + template.forEach(expanded::add); + assertEquals(2, expanded.size()); + assertEquals("my_var", expanded.get(0).getPart()); + assertEquals(TemplatePartType.VARIABLE, expanded.get(0).getType()); + assertEquals(" { .name }", expanded.get(1).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(1).getType()); + } + + @Test + void testTemplates_WithEscapes() { + FaunaTemplate template = new FaunaTemplate("let x = '$${not_a_var}'"); + List expanded = new ArrayList<>(); + template.forEach(expanded::add); + assertEquals(2, expanded.size()); + assertEquals("let x = '$", expanded.get(0).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(0).getType()); + assertEquals("{not_a_var}'", expanded.get(1).getPart()); + assertEquals(TemplatePartType.LITERAL, expanded.get(1).getType()); + } + + @Test + void testTemplates_WithUnsupportedIdentifiers() { + FaunaTemplate template = new FaunaTemplate("let x = ${かわいい}"); + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + List expanded = new ArrayList<>(); + template.forEach(expanded::add); + }); + String expectedMessage = "Invalid placeholder in template: line 1, col 9"; + String actualMessage = exception.getMessage(); + assertTrue(actualMessage.contains(expectedMessage)); + } + +} \ No newline at end of file