Skip to content
Open
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
40 changes: 40 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Repository Guidelines

## Project Structure & Module Organization

- Multi‑module Maven build. Key modules: `compiler` (core), `javascript`, `handlebar`, `scala-extensions/*`, `mustache-maven-plugin`, `example`, `benchmarks`.
- Standard layout per module: `src/main/java|resources`, `src/test/java|resources` (Scala tests in `scala-extensions-*/src/test/scala`).
- Examples live under `example/src/main`, benchmarks under `benchmarks/src/main` and `benchmarks/src/main/resources`.

## Build, Test, and Development Commands

- Build all modules: `mvn -q package` (use `-DskipTests` to skip tests).
- Run all tests: `mvn -q test`.
- Build/test a single module (with deps): `mvn -q -pl compiler -am test`.
- Clean: `mvn -q clean`.
- Optional coverage (if configured): `mvn -q test jacoco:report`.

## Coding Style & Naming Conventions

- Language: Java (and some Scala in `scala-extensions`). Follow the existing two‑space indent and K&R braces in Java sources.
- Packages: lowercase; Classes: `PascalCase`; methods/fields: `lowerCamelCase`.
- Tests end with `*Test` and mirror source package structure.
- Keep imports organized; avoid unused code; prefer immutability (`final`) where sensible. No strict linter is enforced—match nearby style.

## Testing Guidelines

- Frameworks: JUnit 4 for Java tests; Scala tests use JUnit annotations under `scala-extensions`.
- Add tests for new features and bug fixes—focus on `compiler` behavior and edge cases (whitespace, partials, recursion limits).
- Run a single test: `mvn -q -Dtest=JavascriptObjectHandlerTest test` (adjust class name as needed).

## Commit & Pull Request Guidelines

- Commits: concise, imperative subject; reference issues when relevant (e.g., `ISSUE #257: clarify dynamic partials`).
- PRs must include: clear description, linked issues, tests or rationale for test impact, and any doc/example updates.
- CI hygiene: ensure `mvn -q package` and `mvn -q test` pass locally. Avoid breaking public APIs in `com.github.mustachejava` without discussion.
- If rendering output changes, include sample template and before/after output in the PR.

## Agent-Specific Instructions

- Keep changes scoped to the smallest relevant module. Do not alter public APIs lightly—preserve backwards compatibility and spec‑compliant whitespace behavior.
- Update README/examples only when behavior changes; otherwise keep diffs minimal.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.github.mustachejava.Binding;
import com.github.mustachejava.Code;
import com.github.mustachejava.MustacheException;
import com.github.mustachejava.ObjectHandler;
import com.github.mustachejava.TemplateContext;
import com.github.mustachejava.reflect.guards.ClassGuard;
Expand All @@ -18,6 +19,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

import static java.util.Collections.singletonList;

Expand All @@ -31,6 +33,17 @@
public class ReflectionObjectHandler extends BaseObjectHandler {
private static final Wrapper[] EMPTY_WRAPPERS = new Wrapper[0];
private static final Guard[] EMPTY_GUARDS = new Guard[0];
private static final Wrapper CALLABLE_WRAPPER = scopes -> {
Object scope = scopes.get(0);
if (!(scope instanceof Callable)) {
throw new GuardException();
}
try {
return ((Callable<?>) scope).call();
} catch (Exception e) {
throw new MustacheException("Failed to invoke callable while resolving dotted name", e);
}
};

protected static final Method MAP_METHOD;

Expand Down Expand Up @@ -92,6 +105,10 @@ public Wrapper find(String name, final List<Object> scopes) {
try {
// Pull out the next level from the coerced scope
scope = coerce(wrapper.call(ObjectHandler.makeList(coerce(scope))));
while (scope instanceof Callable) {
wrappers.add(CALLABLE_WRAPPER);
scope = coerce(CALLABLE_WRAPPER.call(ObjectHandler.makeList(scope)));
}
} catch (GuardException e) {
throw new AssertionError(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.function.Function;

import static org.junit.Assert.assertEquals;

Expand Down Expand Up @@ -59,6 +61,86 @@ public void testIncompleteObjectPath() {
public void testAlmostCompleteObjectPath() {
testMiss(objectModel, LAST_ELEMENT_MISS_TEMPLATE);
}

@Test
public void testDottedSectionDoesNotPushIntermediateScope() throws Exception {
Map<String, Object> a = map("x", "A", "b", map());
Map<String, Object> model = map("a", a, "x", "ROOT");

assertEquals("ROOT", render(compile("{{#a.b}}{{x}}{{/a.b}}"), model));
}

@Test
public void testDottedSectionPrefersFinalScope() throws Exception {
Map<String, Object> bar = map("baz", "from bar");
Map<String, Object> foo = map("bar", bar, "baz", "from foo");

assertEquals("from bar", render(compile("{{#foo.bar}}{{baz}}{{/foo.bar}}"), map("foo", foo)));
}

@Test
public void testDottedSectionPreservesContextPrecedence() throws Exception {
Map<String, Object> a = map("b", map());
Map<String, Object> model = map("a", a, "c", true);

assertEquals("", render(compile("{{#a.b.c}}ERROR{{/a.b.c}}"), model));
}

@Test
public void testBrokenDottedSectionChainRendersNothing() throws Exception {
assertEquals("", render(compile("{{#a.b.c}}ERROR{{/a.b.c}}"), map("a", map())));
}

@Test
public void testLiteralDottedKeyDoesNotPushIntermediateScope() throws Exception {
Mustache mustache = compile("{{#foo.bar}}{{baz}}{{/foo.bar}}");
Map<String, Object> literalOnly = map("foo.bar", map("value", true), "baz", "root");
Map<String, Object> collision = map(
"foo.bar", map("value", true),
"foo", map("baz", "wrong intermediate"),
"baz", "root");

assertEquals("root", render(mustache, literalOnly));
assertEquals("root", render(mustache, collision));
}

@Test
public void testInvertedDottedSectionBehaviorIsUnchanged() throws Exception {
Mustache mustache = compile("{{^foo.bar}}missing{{/foo.bar}}");

assertEquals("", render(mustache, map("foo", map("bar", true))));
assertEquals("missing", render(mustache, map("foo", map())));
assertEquals("", render(mustache, map("foo.bar", true)));
}

@Test
public void testCallableIntermediateMatchesNestedSections() throws Exception {
Callable<Object> foo = () -> map(
"bar", (Function<String, String>) text -> "baz");
Map<String, Object> model = map("foo", foo);

String dotted = render(compile("{{#foo.bar}}quux{{/foo.bar}}"), model);
String nested = render(compile("{{#foo}}{{#bar}}quux{{/bar}}{{/foo}}"), model);

assertEquals("baz", dotted);
assertEquals(nested, dotted);
}

@Test
public void testNullCallableBreaksDottedSectionChain() throws Exception {
Callable<Object> foo = () -> null;

assertEquals("", render(compile("{{#foo.bar}}ERROR{{/foo.bar}}"), map("foo", foo)));
}

@Test
public void testCallableInMiddleOfThreePartName() throws Exception {
Callable<Object> b = () -> map("c", map("fromC", "c"), "fromB", "b");
Map<String, Object> a = map("b", b, "fromA", "a");

assertEquals("c", render(compile(
"{{#a.b.c}}{{fromC}}{{/a.b.c}}"), map("a", a)));
}

private void testMiss(Object model, String template) {
Mustache mustache = compile(template);
Expand All @@ -72,6 +154,20 @@ private Mustache compile(String template) {
Reader reader = new StringReader(template);
return factory.compile(reader, "template");
}

private String render(Mustache mustache, Object model) throws Exception {
StringWriter writer = new StringWriter();
mustache.execute(writer, model).close();
return writer.toString();
}

private static Map<String, Object> map(Object... entries) {
Map<String, Object> map = new HashMap<>();
for (int i = 0; i < entries.length; i += 2) {
map.put((String) entries[i], entries[i + 1]);
}
return map;
}


}
Loading