You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ra...@apache.org on 2021/01/07 16:39:35 UTC

[sling-org-apache-sling-graphql-core] branch master updated: SLING-10018 As a developer I want to access the selectionset in my fetcher context

This is an automated email from the ASF dual-hosted git repository.

radu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git


The following commit(s) were added to refs/heads/master by this push:
     new 1975b40  SLING-10018 As a developer I want to access the selectionset in my fetcher context
1975b40 is described below

commit 1975b40565e02606e3727591964140b401b50c92
Author: Thierry Ygé <th...@gmail.com>
AuthorDate: Thu Jan 7 17:39:27 2021 +0100

    SLING-10018 As a developer I want to access the selectionset in my fetcher context
    
    * implemented SelectionSet / SelectedField wrappers
---
 .../apache/sling/graphql/api/SelectedField.java    | 65 ++++++++++++++++
 .../org/apache/sling/graphql/api/SelectionSet.java | 77 +++++++++++++++++++
 .../graphql/api/SlingDataFetcherEnvironment.java   |  4 +
 .../org/apache/sling/graphql/api/package-info.java |  2 +-
 .../engine/DataFetchingEnvironmentWrapper.java     |  8 ++
 .../graphql/core/engine/SelectedFieldWrapper.java  | 89 ++++++++++++++++++++++
 .../graphql/core/engine/SelectionSetWrapper.java   | 72 +++++++++++++++++
 .../core/engine/DefaultQueryExecutorTest.java      | 66 ++++++++++++++++
 .../sling/graphql/core/mocks/EchoDataFetcher.java  |  7 ++
 src/test/resources/test-schema.txt                 | 10 +++
 10 files changed, 399 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/apache/sling/graphql/api/SelectedField.java b/src/main/java/org/apache/sling/graphql/api/SelectedField.java
new file mode 100644
index 0000000..b3da627
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/SelectedField.java
@@ -0,0 +1,65 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.api;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+import java.util.List;
+
+/**
+ * Interface to wrap information from <a href="https://javadoc.io/doc/com.graphql-java/graphql-java/latest/graphql/schema/SelectedField.html">GraphQL SelectedField</a>.
+ *
+ * <p>As described in {@link org.apache.sling.graphql.api.SelectionSet SelectionSet}, it is aimed to map the SelectedField to the minimum information
+ * required when processing the query.</p><p>InlineFragment are mapped so that its isInline() is return true.</p>
+ */
+@ProviderType
+public interface SelectedField {
+
+    /**
+     * @return the name as defined in the selection set.
+     */
+    @Nullable
+    String getName();
+
+    /**
+     * @return the sub selected fields.
+     */
+    @NotNull
+    List<SelectedField> getSubSelectedFields();
+
+    /**
+     * @param name the sub selected field name.
+     * @return the object or null if that doesn't exist.
+     */
+    @Nullable
+    SelectedField getSubSelectedField(String name);
+
+    /**
+     * @param name the sub selected field name(s).
+     * @return true if any of the sub selected fields exists.
+     */
+    boolean hasSubSelectedFields(String ...name);
+
+    /**
+     * @return true if this field is an inline (i.e: ... on Something { }).
+     */
+    boolean isInline();
+}
diff --git a/src/main/java/org/apache/sling/graphql/api/SelectionSet.java b/src/main/java/org/apache/sling/graphql/api/SelectionSet.java
new file mode 100644
index 0000000..ccfa765
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/api/SelectionSet.java
@@ -0,0 +1,77 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.api;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+import java.util.List;
+
+/**
+ * Interface to wrap information from <a href="https://javadoc.io/doc/com.graphql-java/graphql-java/latest/graphql/schema/DataFetchingFieldSelectionSet.html">GraphQL DataFetchingFieldSelectionSet</a>.
+ * <p>Mainly it keeps information about fields name that got selected.</p>
+ * <pre>
+ * For example:
+ * {@code
+ *   queryName {
+ *       field1
+ *       field2 {
+ *           ... on Type1 {
+ *               field3
+ *           }
+ *       }
+ *       field4
+ *       field5 {
+ *           field6
+ *           field7 {
+ *               field8
+ *           }
+ *       }
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>Would result in a mapping with corresponding SelectedField(s).</p>
+ * <p><b>field1</b> would be accessible with qualified name "field1"
+ * while <b>field3</b> would be accessible with qualified name "field2/Type1/field3"
+ * and <b>field8</b> would be accessible with qualified name "field5/field7/field8"
+ * </p>
+ * <p><b>Type1</b> would be a SelectedField with isInline() returning true</p>
+ */
+@ProviderType
+public interface SelectionSet {
+
+    /**
+     * @return the immediate list of fields in the selection.
+     */
+    @NotNull
+    List<SelectedField> getFields();
+
+    /**
+     * @return true if the field qualified name exist.
+     */
+    boolean contains(String qualifiedName);
+
+    /**
+     * @return SelectedField for qualified name.
+     */
+    @Nullable
+    SelectedField get(String qualifiedName);
+}
diff --git a/src/main/java/org/apache/sling/graphql/api/SlingDataFetcherEnvironment.java b/src/main/java/org/apache/sling/graphql/api/SlingDataFetcherEnvironment.java
index c557145..29bcf54 100644
--- a/src/main/java/org/apache/sling/graphql/api/SlingDataFetcherEnvironment.java
+++ b/src/main/java/org/apache/sling/graphql/api/SlingDataFetcherEnvironment.java
@@ -20,6 +20,7 @@
 package org.apache.sling.graphql.api;
 
 import java.util.Map;
+
 import org.apache.sling.api.resource.Resource;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.annotation.versioning.ProviderType;
@@ -64,4 +65,7 @@ public interface SlingDataFetcherEnvironment {
     /** @return the source, if set by the schema directive */
     @Nullable
     String getFetcherSource();
+
+    /** @return the selectionSet, mandatory in a graphql query */
+    SelectionSet getSelectionSet();
 }
diff --git a/src/main/java/org/apache/sling/graphql/api/package-info.java b/src/main/java/org/apache/sling/graphql/api/package-info.java
index e5d4b27..39a0bdc 100644
--- a/src/main/java/org/apache/sling/graphql/api/package-info.java
+++ b/src/main/java/org/apache/sling/graphql/api/package-info.java
@@ -21,6 +21,6 @@
   * This package contains APIs which are independent of
   * a specific implementation of the underlying graphQL engine.
   */
-@Version("3.2.0")
+@Version("3.3.0")
 package org.apache.sling.graphql.api;
 import org.osgi.annotation.versioning.Version;
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/DataFetchingEnvironmentWrapper.java b/src/main/java/org/apache/sling/graphql/core/engine/DataFetchingEnvironmentWrapper.java
index 560013c..a98d416 100644
--- a/src/main/java/org/apache/sling/graphql/core/engine/DataFetchingEnvironmentWrapper.java
+++ b/src/main/java/org/apache/sling/graphql/core/engine/DataFetchingEnvironmentWrapper.java
@@ -22,6 +22,7 @@ package org.apache.sling.graphql.core.engine;
 import java.util.Map;
 
 import org.apache.sling.api.resource.Resource;
+import org.apache.sling.graphql.api.SelectionSet;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
 
 import graphql.schema.DataFetchingEnvironment;
@@ -35,12 +36,14 @@ class DataFetchingEnvironmentWrapper implements SlingDataFetcherEnvironment {
     private final Resource currentResource;
     private final String options;
     private final String source;
+    private final SelectionSet selectionSet;
 
     DataFetchingEnvironmentWrapper(DataFetchingEnvironment env, Resource currentResource, String options, String source) {
         this.env = env;
         this.currentResource = currentResource;
         this.options = options;
         this.source = source;
+        this.selectionSet = new SelectionSetWrapper(env.getSelectionSet());
     }
 
     @Override
@@ -77,4 +80,9 @@ class DataFetchingEnvironmentWrapper implements SlingDataFetcherEnvironment {
     public String getFetcherSource() {
         return source;
     }
+
+    @Override
+    public SelectionSet getSelectionSet() {
+        return selectionSet;
+    }
 }
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
new file mode 100644
index 0000000..6ca27a3
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
@@ -0,0 +1,89 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.engine;
+
+import graphql.language.Field;
+import graphql.language.InlineFragment;
+import graphql.language.Selection;
+import graphql.language.SelectionSet;
+import org.apache.sling.graphql.api.SelectedField;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Implement a wrapper for GraphQL SelectedField.
+ */
+public class SelectedFieldWrapper implements SelectedField {
+
+    private String name;
+    private boolean isInline;
+    private Map<String, SelectedField> subFieldMap = new HashMap<>();
+    private List<SelectedField> subFields;
+
+    public SelectedFieldWrapper(Selection selection) {
+        SelectionSet selectionSet = null;
+        if (selection instanceof InlineFragment) {
+            InlineFragment inline = (InlineFragment) selection;
+            this.name = inline.getTypeCondition().getName();
+            this.isInline = true;
+            selectionSet = inline.getSelectionSet();
+        }
+        if (selection instanceof Field) {
+            Field subField = (Field) selection;
+            this.name = subField.getName();
+            selectionSet = subField.getSelectionSet();
+        }
+        if (selectionSet != null) {
+            selectionSet.getSelections().forEach(s -> {
+                SelectedFieldWrapper wrappedField = new SelectedFieldWrapper(s);
+                subFieldMap.put(wrappedField.getName(), wrappedField);
+            });
+        }
+        subFields = subFieldMap.values().stream().collect(Collectors.toList());
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public List<SelectedField> getSubSelectedFields() {
+        return subFields;
+    }
+
+    @Override
+    public SelectedField getSubSelectedField(String name) {
+        return subFieldMap.get(name);
+    }
+
+    @Override
+    public boolean hasSubSelectedFields(String... name) {
+        return Arrays.stream(name).anyMatch(subFieldMap::containsKey);
+    }
+
+    @Override
+    public boolean isInline() {
+        return isInline;
+    }
+}
diff --git a/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java b/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java
new file mode 100644
index 0000000..40f0bfd
--- /dev/null
+++ b/src/main/java/org/apache/sling/graphql/core/engine/SelectionSetWrapper.java
@@ -0,0 +1,72 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements.  See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership.  The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License.  You may obtain a copy of the License at
+ ~
+ ~   http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied.  See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package org.apache.sling.graphql.core.engine;
+
+import graphql.schema.DataFetchingFieldSelectionSet;
+import org.apache.sling.graphql.api.SelectedField;
+import org.apache.sling.graphql.api.SelectionSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implement a wrapper for GraphQL DataFetchingFieldSelectionSet.
+ */
+public class SelectionSetWrapper implements SelectionSet {
+
+    private List<SelectedField> fields = new ArrayList<>();
+
+    private Map<String, SelectedField> fieldsMap = new HashMap<>();
+
+    public SelectionSetWrapper(@Nullable DataFetchingFieldSelectionSet selectionSet) {
+        if (selectionSet != null) {
+            selectionSet.get().getSubFieldsList().forEach(s -> fields.add(new SelectedFieldWrapper(s.getSingleField())));
+            initFlatMap(fields, "");
+        }
+    }
+
+    private void initFlatMap(List<SelectedField> parentList, String qualifiedPath) {
+        parentList.forEach(s -> {
+           String qualifiedName = qualifiedPath + s.getName();
+           fieldsMap.put(qualifiedName, s);
+           initFlatMap(s.getSubSelectedFields(), qualifiedName + "/");
+        });
+    }
+
+    @Override
+    @NotNull
+    public List<SelectedField> getFields() {
+        return Collections.unmodifiableList(fields);
+    }
+
+    @Override
+    public boolean contains(String qualifiedName) {
+        return fieldsMap.containsKey(qualifiedName);
+    }
+
+    @Override
+    public SelectedField get(String qualifiedName) {
+        return fieldsMap.get(qualifiedName);
+    }
+}
diff --git a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
index 76488df..130c942 100644
--- a/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
+++ b/src/test/java/org/apache/sling/graphql/core/engine/DefaultQueryExecutorTest.java
@@ -23,8 +23,11 @@ import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Objects;
 
 import org.apache.sling.graphql.api.SchemaProvider;
+import org.apache.sling.graphql.api.SelectionSet;
+import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingGraphQLException;
 import org.apache.sling.graphql.api.engine.QueryExecutor;
 import org.apache.sling.graphql.api.engine.ValidationResult;
@@ -39,6 +42,7 @@ import org.apache.sling.graphql.core.mocks.TypeTestDTO;
 import org.apache.sling.graphql.core.mocks.UnionTypeResolver;
 import org.junit.Test;
 import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
 import org.osgi.framework.ServiceRegistration;
 
 import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
@@ -71,6 +75,7 @@ public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "test/static", new EchoDataFetcher(staticData));
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "test/fortyTwo", new EchoDataFetcher(42));
         TestUtil.registerSlingDataFetcher(context.bundleContext(), "sling/digest", new DigestDataFetcher());
+        TestUtil.registerSlingDataFetcher(context.bundleContext(), "combined/fetcher", new EchoDataFetcher(unionData));
     }
 
     @Test
@@ -188,4 +193,65 @@ public class DefaultQueryExecutorTest extends ResourceQueryTestBase {
         assertThat(json, hasJsonPath("$.data.unionFetcher.items[0].testingArgument", equalTo("1, 2, 3")));
         assertThat(json, hasJsonPath("$.data.unionFetcher.items[1].path", equalTo(resource.getPath())));
     }
+
+    @Test
+    public void selectionSetTest() throws Exception {
+        queryJSON("{ combinedFetcher { boolValue resourcePath aTest { boolValue test resourcePath } allTests { boolValue test resourcePath } items { ... on Test { testingArgument }  ... on SlingResource { path }} } }");
+
+        // retrieve the service used
+        ServiceReference<?>[] serviceReferences = context.bundleContext().getServiceReferences(SlingDataFetcher.class.getName(), "(name=combined/fetcher)");
+        EchoDataFetcher echoDataFetcher = (EchoDataFetcher) context.bundleContext().getService(serviceReferences[0]);
+
+        // Access the computed SelectionSet
+        SelectionSet selectionSet = echoDataFetcher.getSelectionSet();
+
+        // Assert it contains the expected results
+        String[] expectedQualifiedName = new String[] {
+                "boolValue",
+                "resourcePath",
+                "aTest",
+                "aTest/test",
+                "aTest/boolValue",
+                "aTest/resourcePath",
+                "allTests",
+                "allTests/test",
+                "allTests/boolValue",
+                "allTests/resourcePath",
+                "items",
+                "items/Test",
+                "items/Test/testingArgument",
+                "items/SlingResource",
+                "items/SlingResource/path"
+        };
+        for (String expectedQN : expectedQualifiedName) {
+            assertTrue(selectionSet.contains(expectedQN));
+        }
+
+        String[] expectedNonInlineQNs = new String[] {
+                "boolValue",
+                "resourcePath",
+                "aTest",
+                "aTest/test",
+                "aTest/boolValue",
+                "aTest/resourcePath",
+                "allTests",
+                "allTests/test",
+                "allTests/boolValue",
+                "allTests/resourcePath",
+                "items",
+                "items/Test/testingArgument",
+                "items/SlingResource/path"
+        };
+        for (String expectedNonInlineQN : expectedNonInlineQNs) {
+            assertFalse(Objects.requireNonNull(selectionSet.get(expectedNonInlineQN)).isInline());
+        }
+
+        String[] expectedInlineQNs = new String[] {
+                "items/Test",
+                "items/SlingResource"
+        };
+        for (String expectedInlineQN : expectedInlineQNs) {
+            assertTrue(Objects.requireNonNull(selectionSet.get(expectedInlineQN)).isInline());
+        }
+    }
 }
diff --git a/src/test/java/org/apache/sling/graphql/core/mocks/EchoDataFetcher.java b/src/test/java/org/apache/sling/graphql/core/mocks/EchoDataFetcher.java
index 39d66e9..aed6424 100644
--- a/src/test/java/org/apache/sling/graphql/core/mocks/EchoDataFetcher.java
+++ b/src/test/java/org/apache/sling/graphql/core/mocks/EchoDataFetcher.java
@@ -19,12 +19,14 @@
 
 package org.apache.sling.graphql.core.mocks;
 
+import org.apache.sling.graphql.api.SelectionSet;
 import org.apache.sling.graphql.api.SlingDataFetcher;
 import org.apache.sling.graphql.api.SlingDataFetcherEnvironment;
 
 public class EchoDataFetcher implements SlingDataFetcher<Object> {
 
     private final Object data;
+    private SelectionSet selectionSet;
 
     public EchoDataFetcher(Object data) {
         this.data = data;
@@ -37,6 +39,11 @@ public class EchoDataFetcher implements SlingDataFetcher<Object> {
         } else if(data == null) {
             return e.getCurrentResource();
         }
+        selectionSet = e.getSelectionSet();
         return data;
     }
+
+    public SelectionSet getSelectionSet() {
+        return selectionSet;
+    }
 }
diff --git a/src/test/resources/test-schema.txt b/src/test/resources/test-schema.txt
index e0853a8..9b1527d 100644
--- a/src/test/resources/test-schema.txt
+++ b/src/test/resources/test-schema.txt
@@ -38,6 +38,9 @@ type Query {
 
     # Test union as return type
     unionFetcher: Test2 @fetcher(name:"union/fetcher")
+
+    # Test combined as return type
+    combinedFetcher: Test3 @fetcher(name:"combined/fetcher")
 }
 
 union AllTypes @resolver(name:"union/resolver" source:"AllTypes") = SlingResource | Test
@@ -74,3 +77,10 @@ type Test2 {
     items: [AllTypes]
 }
 
+type Test3 {
+    boolValue: Boolean
+    resourcePath: String
+    aTest: Test
+    allTests: [Test]
+    items: [AllTypes]
+}