Repository: PebbleTemplates/pebble
Pull request #241 (2016-11-16 12:24:18)

DynamicAttributeProvider
This is a feature I suggest and that would be really useful for me. It consists in a `DynamicAttributeProvider` interface which, when implemented by an object, tells the expression parser that this object is able to provide attributes _dynamically_, given their names and the potential arguments.

For example :

    public class MyObject implements DynamicAttributeProvider {

        @Override
        public boolean canProvideDynamicAttribute(Object attributeName) {
            
            if("firstname".equals(attributeName)) {
                return true;
            }
            return false;
        }

        @Override
        public Object getDynamicAttribute(Object attributeName, Object[] argumentValues) {
            
            if("firstname".equals(attributeName)) {
                return "John";
            }
            return null;
        }

        public String getSurname() {
            return "Doe";
        }
    }

Then `"{{ object.firstname }} {{ object.surname }}"` => **John Doe**

-------------------------

So, prior to check for :

- `getXXX `method
- `isXXX `method
- `hasXXX `method
- `XXX `method
- `XXX `field

the parser will call `DynamicAttributeProvider#canProvideDynamicAttribute(attributeName)` to see if the current object is able (and wants) to generate the attribute by itself. If so, the `DynamicAttributeProvider#getDynamicAttribute(attributeName, argumentValues)` method will be called.

This feature will help me since I do have a solution for issue #230 , but it involves adding a duplicate of all the `Map<String, Object>` parameters for the template to a special object and then add this object itself to the `Map`. With this feature, I wouln't have to duplicate anything.

Those changes shouldn't impact any existing code.


Patch:
diff --git a/src/main/java/com/mitchellbosecke/pebble/extension/DynamicAttributeProvider.java b/src/main/java/com/mitchellbosecke/pebble/extension/DynamicAttributeProvider.java
new file mode 100644
index 00000000..6d696b5a
--- /dev/null
+++ b/src/main/java/com/mitchellbosecke/pebble/extension/DynamicAttributeProvider.java
@@ -0,0 +1,21 @@
+package com.mitchellbosecke.pebble.extension;
+
+/**
+ * When an object implements this interface, it tells
+ * the expression parser that it is able to provide
+ * dynamic attributes.
+ */
+public interface DynamicAttributeProvider {
+    
+    /**
+     * Returns <code>true</code> if the attribute can be
+     * provided given the specified name.
+     */
+    boolean canProvideDynamicAttribute(Object attributeName);
+    
+    /**
+     * Returns the attribute given the specified name and arguments.
+     */
+    Object getDynamicAttribute(Object attributeName, Object[] argumentValues);
+
+}
diff --git a/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java b/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
index 9717d149..9748bc31 100644
--- a/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
+++ b/src/main/java/com/mitchellbosecke/pebble/node/expression/GetAttributeExpression.java
@@ -17,6 +17,7 @@ package com.mitchellbosecke.pebble.node.expression;
 import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException;
+import com.mitchellbosecke.pebble.extension.DynamicAttributeProvider;
 import com.mitchellbosecke.pebble.extension.NodeVisitor;
 import com.mitchellbosecke.pebble.node.ArgumentsNode;
 import com.mitchellbosecke.pebble.node.PositionalArgumentNode;
@@ -35,8 +36,9 @@ import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * Used to get an attribute from an object. It will look up attributes in the
- * following order: map entry, array item, list item, get method, is method, has
- * method, public method, public field.
+ * following order: map entry, array item, list item,
+ * {@link DynamicAttributeProvider}, get method, is method, has method, public method,
+ * public field.
  *
  * @author Mitchell
  */
@@ -86,10 +88,18 @@ public class GetAttributeExpression implements Expression<Object> {
 
         Object result = null;
 
-        Object[] argumentValues = null;
+        Object[] argumentValues = this.getArgumentValues(self, context);
 
         Member member = object == null ? null : this.memberCache.get(new MemberCacheKey(object.getClass(), attributeName));
 
+        // check if the object is able to provide the attribute dynamically
+        if(object != null && object instanceof DynamicAttributeProvider) {
+            DynamicAttributeProvider dynamicAttributeProvider = (DynamicAttributeProvider) object;
+            if(dynamicAttributeProvider.canProvideDynamicAttribute(attributeName)) {
+                return dynamicAttributeProvider.getDynamicAttribute(attributeNameValue, argumentValues);
+            }
+        }
+
         if (object != null && member == null) {
 
             /*
@@ -152,7 +162,6 @@ public class GetAttributeExpression implements Expression<Object> {
              * turn args into an array of types and an array of values in order
              * to use them for our reflection calls
              */
-            argumentValues = this.getArgumentValues(self, context);
             Class<?>[] argumentTypes = new Class<?>[argumentValues.length];
 
             for (int i = 0; i < argumentValues.length; i++) {
@@ -172,9 +181,6 @@ public class GetAttributeExpression implements Expression<Object> {
         }
 
         if (object != null && member != null) {
-            if (argumentValues == null) {
-                argumentValues = this.getArgumentValues(self, context);
-            }
             result = this.invokeMember(object, member, argumentValues);
         } else if (context.isStrictVariables()) {
             if (object == null) {


Test patch:
diff --git a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
index 11ab378a..bfe8864e 100644
--- a/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
+++ b/src/test/java/com/mitchellbosecke/pebble/GetAttributeTest.java
@@ -11,6 +11,7 @@ package com.mitchellbosecke.pebble;
 import com.mitchellbosecke.pebble.error.AttributeNotFoundException;
 import com.mitchellbosecke.pebble.error.PebbleException;
 import com.mitchellbosecke.pebble.error.RootAttributeNotFoundException;
+import com.mitchellbosecke.pebble.extension.DynamicAttributeProvider;
 import com.mitchellbosecke.pebble.loader.StringLoader;
 import com.mitchellbosecke.pebble.template.PebbleTemplate;
 
@@ -20,6 +21,7 @@ import java.io.IOException;
 import java.io.StringWriter;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -581,6 +583,83 @@ public class GetAttributeTest extends AbstractTest {
         }
     }
 
+    public class DynamicAttributeProviderObject implements DynamicAttributeProvider {
+
+        public String getSurname() {
+            return "Doe";
+        }
+
+        public String name = "Invalid";
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public boolean canProvideDynamicAttribute(Object attributeName) {
+
+            if("name".equals(attributeName)) {
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public Object getDynamicAttribute(Object attributeName, Object[] argumentValues) {
+
+            if("name".equals(attributeName)) {
+
+                String name = "Steve";
+                if(argumentValues != null && argumentValues.length > 0) {
+                    name += " " + Arrays.toString(argumentValues);
+                }
+                return name;
+            }
+
+            return "Invalid";
+        }
+    }
+
+    @Test
+    public void testAttributeProviderSimple() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();
+
+        PebbleTemplate template = pebble.getTemplate("hello {{ object.name }} {{ object.surname }}{{ object.invalidAttr }}");
+        Map<String, Object> context = new HashMap<>();
+        context.put("object", new DynamicAttributeProviderObject());
+
+        Writer writer = new StringWriter();
+        template.evaluate(writer, context);
+        assertEquals("hello Steve Doe", writer.toString());
+    }
+
+    @Test
+    public void testAttributeProviderEmptyArguments() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(true).build();
+
+        PebbleTemplate template = pebble.getTemplate("hello {{ object.name() }}");
+        Map<String, Object> context = new HashMap<>();
+        context.put("object", new DynamicAttributeProviderObject());
+
+        Writer writer = new StringWriter();
+        template.evaluate(writer, context);
+        assertEquals("hello Steve", writer.toString());
+    }
+
+    @Test
+    public void testAttributeProviderWithArguments() throws PebbleException, IOException {
+        PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(true).build();
+
+        PebbleTemplate template = pebble.getTemplate("hello {{ object.name('abc', (40 + 2)) }}");
+        Map<String, Object> context = new HashMap<>();
+        context.put("object", new DynamicAttributeProviderObject());
+
+        Writer writer = new StringWriter();
+        template.evaluate(writer, context);
+        assertEquals("hello Steve [abc, 42]", writer.toString());
+    }
+
+
+
     @Test
     public void testAttributePrimitiveAccessWithEmptyMap() throws Exception {
         PebbleEngine pebble = new PebbleEngine.Builder().loader(new StringLoader()).strictVariables(false).build();