TemplateProcessor.java

1
package com.renomad.minum.templating;
2
3
4
import java.util.*;
5
6
/**
7
 * This class provides methods for working with templates.
8
 * <p>
9
 * The first step is to write a template.  Here is an example:
10
 * </p>
11
 * <pre>
12
 * Hello, my name is {{name}}
13
 * </pre>
14
 * <p>
15
 * Then, feed that string into the {@link #buildProcessor} method, like
16
 * this:
17
 * </p>
18
 * <pre>
19
 * {@code
20
 *   String input = "Hello, my name is {{name}}"
21
 *   TemplateProcessor helloProcessor = TemplateProcessor.buildProcessor(input);
22
 * }
23
 * </pre>
24
 * <p>
25
 * The returned value ("helloProcessor") can be rendered with different values. For
26
 * example:
27
 * </p>
28
 * <pre>
29
 * {@code
30
 *   Map<String,String> myMap = Map.of("name", "Susanne");
31
 *   String fullyRenderedString = helloProcessor.renderTemplate(myMap);
32
 * }
33
 * </pre>
34
 * <p>
35
 *     The result is:
36
 * </p>
37
 * <pre>
38
 *     {@code Hello, my name is Susanne}
39
 * </pre>
40
 */
41
public final class TemplateProcessor {
42
43
    private final List<TemplateSection> templateSections;
44
45
    /**
46
     * Instantiate a new object with a list of {@link TemplateSection}.
47
     */
48
    private TemplateProcessor(List<TemplateSection> templateSections) {
49
        this.templateSections = templateSections;
50
    }
51
52
    /**
53
     * Given a map of key names -> value, render a template.
54
     */
55
    public String renderTemplate(Map<String, String> myMap) {
56
        // This indicates the count of usages of each key
57
        Map <String, Integer> usageMap = new HashMap<>();
58
        List<String> parts = new ArrayList<>();
59
        for (TemplateSection templateSection : templateSections) {
60
            RenderingResult result = templateSection.render(myMap);
61
            parts.add(result.renderedSection());
62
            String appliedKey = result.appliedKey();
63 1 1. renderTemplate : negated conditional → KILLED
            if (appliedKey != null) {
64
                usageMap.merge(appliedKey, 1, Integer::sum);
65
            }
66
        }
67
        Set<String> unusedKeys = new HashSet<>(myMap.keySet());
68
        unusedKeys.removeIf(usageMap.keySet()::contains);
69
70 1 1. renderTemplate : negated conditional → KILLED
        if (!unusedKeys.isEmpty()) {
71
            throw new TemplateRenderException("No corresponding key in template found for these keys: " + String.join(", ", unusedKeys));
72
        }
73 1 1. renderTemplate : replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED
        return String.join("",parts);
74
    }
75
76
    /**
77
     * Builds a {@link TemplateProcessor} from a string
78
     * containing a proper template.  Templated values
79
     * are surrounded by double-curly-braces, i.e. {{foo}} or {{ foo }}
80
     */
81
    public static TemplateProcessor buildProcessor(String template) {
82
        // this value holds the entire template after processing, comprised
83
        // of an ordered list of TemplateSections
84
        var tSections = new ArrayList<TemplateSection>();
85
86
        // these values are used for logging and setting proper indentation
87
        int rowNumber = 1;
88
        int columnNumber = 1;
89
        // this value records the indent of the beginning of template keys,
90
        // so we can properly indent the values later.
91
        int startOfKey = 0;
92
93
        StringBuilder builder = new StringBuilder();
94
        // this flag is to help us understand whether we are currently reading the
95
        // name of a template literal.
96
        // e.g. in the case of hello {{ name }}, "name" is the literal.
97
        boolean isInsideTemplateKeyLiteral = false;
98 2 1. buildProcessor : changed conditional boundary → KILLED
2. buildProcessor : negated conditional → KILLED
        for (int i = 0; i < template.length(); i++) {
99
            char charAtCursor = template.charAt(i);
100
101 1 1. buildProcessor : negated conditional → KILLED
            if (justArrivedInside(template, charAtCursor, i)) {
102
                isInsideTemplateKeyLiteral = true;
103
                startOfKey = columnNumber;
104 1 1. buildProcessor : Changed increment from 1 to -1 → TIMED_OUT
                i += 1;
105
                builder = processSectionInside(builder, tSections);
106 1 1. buildProcessor : negated conditional → KILLED
            } else if (justArrivedOutside(template, charAtCursor, i, isInsideTemplateKeyLiteral)) {
107
                isInsideTemplateKeyLiteral = false;
108 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                i += 1;
109
                builder = processSectionOutside(builder, tSections, startOfKey);
110
                startOfKey = 0;
111
            } else {
112
                builder.append(charAtCursor);
113
114
                /*
115
                 if we're at the end of the template, it's our last chance to
116
                 add a substring (we can't be adding to a key, since if we're
117
                 at the end, and it's not a closing brace, it's a malformed
118
                 template.
119
                 */
120 2 1. buildProcessor : Replaced integer subtraction with addition → KILLED
2. buildProcessor : negated conditional → KILLED
                if (i == template.length() - 1) {
121 1 1. buildProcessor : negated conditional → KILLED
                    if (isInsideTemplateKeyLiteral) {
122
                        // if we're exiting this string while inside a template literal, then
123
                        // we're reading a corrupted input, and we should make that clear
124
                        // to our caller.
125 2 1. buildProcessor : negated conditional → KILLED
2. buildProcessor : changed conditional boundary → KILLED
                        String templateSample = template.length() > 10 ? template.substring(0, 10) + "..." : template;
126
                        throw new TemplateParseException(
127
                                "parsing failed for string starting with \"" + templateSample + "\" at line " + rowNumber + " and column " + columnNumber);
128
                    }
129
                    tSections.add(new TemplateSection(null, builder.toString(), 0));
130
                }
131
            }
132
133 1 1. buildProcessor : negated conditional → KILLED
            if (charAtCursor == '\n') {
134 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                rowNumber += 1;
135
                columnNumber = 1;
136
            } else {
137 1 1. buildProcessor : Changed increment from 1 to -1 → KILLED
                columnNumber += 1;
138
            }
139
140
        }
141
142 1 1. buildProcessor : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::buildProcessor → KILLED
        return new TemplateProcessor(tSections);
143
    }
144
145
    static StringBuilder processSectionInside(StringBuilder builder, List<TemplateSection> tSections) {
146 1 1. processSectionInside : negated conditional → KILLED
        if (!builder.isEmpty()) {
147
            tSections.add(new TemplateSection(null, builder.toString(), 0));
148
            builder = new StringBuilder();
149
        }
150 1 1. processSectionInside : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionInside → KILLED
        return builder;
151
    }
152
153
    static StringBuilder processSectionOutside(StringBuilder builder, List<TemplateSection> tSections, int indent) {
154 1 1. processSectionOutside : negated conditional → KILLED
        if (!builder.isEmpty()) {
155
            tSections.add(new TemplateSection(builder.toString().trim(), null, indent));
156
            builder = new StringBuilder();
157
        }
158 1 1. processSectionOutside : replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionOutside → KILLED
        return builder;
159
    }
160
161
    /**
162
     * Just left a template key value.
163
     * <pre>
164
     *     hello {{ world }}
165
     *                ^
166
     *                +------Template key
167
     *
168
     * </pre>
169
     */
170
    static boolean justArrivedOutside(String template, char charAtCursor, int i, boolean isInsideTemplateKeyLiteral) {
171 8 1. justArrivedOutside : negated conditional → KILLED
2. justArrivedOutside : negated conditional → KILLED
3. justArrivedOutside : Replaced integer addition with subtraction → KILLED
4. justArrivedOutside : changed conditional boundary → KILLED
5. justArrivedOutside : negated conditional → KILLED
6. justArrivedOutside : replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedOutside → KILLED
7. justArrivedOutside : Replaced integer addition with subtraction → KILLED
8. justArrivedOutside : negated conditional → KILLED
        return charAtCursor == '}' && (i + 1) < template.length() && template.charAt(i + 1) == '}' && isInsideTemplateKeyLiteral;
172
    }
173
174
    /**
175
     * Just arrived inside a template key value.
176
     * <pre>
177
     *     hello {{ world }}
178
     *                ^
179
     *                +------Template key
180
     *
181
     * </pre>
182
     */
183
    static boolean justArrivedInside(String template, char charAtCursor, int i) {
184 7 1. justArrivedInside : negated conditional → KILLED
2. justArrivedInside : negated conditional → KILLED
3. justArrivedInside : replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedInside → KILLED
4. justArrivedInside : Replaced integer addition with subtraction → KILLED
5. justArrivedInside : negated conditional → KILLED
6. justArrivedInside : Replaced integer addition with subtraction → KILLED
7. justArrivedInside : changed conditional boundary → KILLED
        return charAtCursor == '{' && (i + 1) < template.length() && template.charAt(i + 1) == '{';
185
    }
186
}
187

Mutations

63

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

70

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

73

1.1
Location : renderTemplate
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with "" for com/renomad/minum/templating/TemplateProcessor::renderTemplate → KILLED

98

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

101

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

104

1.1
Location : buildProcessor
Killed by : none
Changed increment from 1 to -1 → TIMED_OUT

106

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

108

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

120

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer subtraction with addition → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

121

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

125

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

2.2
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

133

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

134

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

137

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
Changed increment from 1 to -1 → KILLED

142

1.1
Location : buildProcessor
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::buildProcessor → KILLED

146

1.1
Location : processSectionInside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

150

1.1
Location : processSectionInside
Killed by : com.renomad.minum.templating.TemplatingTests
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionInside → KILLED

154

1.1
Location : processSectionOutside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_processSectionOutside_builderEmpty(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

158

1.1
Location : processSectionOutside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_processSectionOutside_builderEmpty(com.renomad.minum.templating.TemplateSectionTests)
replaced return value with null for com/renomad/minum/templating/TemplateProcessor::processSectionOutside → KILLED

171

1.1
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

2.2
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

3.3
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer addition with subtraction → KILLED

4.4
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
changed conditional boundary → KILLED

5.5
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

6.6
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedOutside → KILLED

7.7
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
Replaced integer addition with subtraction → KILLED

8.8
Location : justArrivedOutside
Killed by : com.renomad.minum.templating.TemplatingTests
negated conditional → KILLED

184

1.1
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

2.2
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

3.3
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
replaced boolean return with true for com/renomad/minum/templating/TemplateProcessor::justArrivedInside → KILLED

4.4
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
Replaced integer addition with subtraction → KILLED

5.5
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
negated conditional → KILLED

6.6
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
Replaced integer addition with subtraction → KILLED

7.7
Location : justArrivedInside
Killed by : com.renomad.minum.templating.TemplateSectionTests.test_justArrivedInside(com.renomad.minum.templating.TemplateSectionTests)
changed conditional boundary → KILLED

Active mutators

Tests examined


Report generated by PIT 1.17.0