This is an automated email from the ASF dual-hosted git repository. bdelacretaz 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 beebfd5 Use schema directives to define fetchers, instead of structured comments beebfd5 is described below commit beebfd554f29d0d0eaaad923af2577bff1fd16e3 Author: Bertrand Delacretaz <bdelacre...@apache.org> AuthorDate: Wed Jun 3 12:50:40 2020 +0200 Use schema directives to define fetchers, instead of structured comments --- README.md | 51 +++++++------- .../graphql/core/engine/GraphQLResourceQuery.java | 48 +++++++------ .../graphql/core/schema/DataFetcherDefinition.java | 37 +++------- .../core/schema/DataFetcherDefinitionTest.java | 81 ---------------------- .../core/schema/DataFetcherSelectorTest.java | 10 +-- .../core/schema/DataFetcherServiceRankingTest.java | 16 ++--- .../core/schema/SchemaDescriptionsTest.java | 4 +- .../core/schema/TestDataFetcherProvider.java | 8 +-- .../apps/graphql/test/one/GQLschema.jsp | 15 ++-- .../apps/graphql/test/two/GQLschema.jsp | 15 ++-- .../apps/graphql/test/two/testing.GQLschema.jsp | 18 +++-- src/test/resources/test-schema-selected-foryou.txt | 20 +++--- src/test/resources/test-schema.txt | 44 ++++-------- 13 files changed, 126 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index 1168dd0..9d695bf 100644 --- a/README.md +++ b/README.md @@ -56,41 +56,38 @@ The default provider makes an internal Sling request with for the current Resour This allows the Sling script/servlet resolution mechanism and its script engines to be used to generate schemas dynamically, taking request selectors into account. -## DataFetcher selection with Schema annotations +## DataFetcher selection with Schema Directives -The GraphQL schemas used by this module can be enhanced with comments +The GraphQL schemas used by this module can be enhanced using +[schema directives](http://spec.graphql.org/June2018/#sec-Language.Directives) that select specific `DataFetcher` to return the appropriate data. A default `DataFetcher` is used for types and fields which have no such annotation. -Comments starting with `## fetch` specify data fetchers using the following syntax: +Here's a simple example, the test code has more: - ## fetch:<namespace>/<name>/<options> <source> - -Where `<namespace>` selects a source (OSGi service) of `DataFetcher`, `<name>` selects -a specific fetcher from that source, `<options>` can optionally be used to adapt the -behavior of the fetcher according to its own specification and `<source>` can optionally -be used to tell the fetcher which field or object to select in its input. - -Here's an example of such an annotated schema. + # This directive maps fields to our Sling data fetchers + directive @fetcher( + name : String, + options : String = "", + source : String = "" + ) on FIELD_DEFINITION type Query { - ## fetch:test/echo - currentResource : SlingResource - ## fetch:test/static - staticContent: Test + withTestingSelector : TestData @fetcher(name:"test/pipe") } - type SlingResource { - path: String - resourceType: String - ## fetch:test/digest/md5 path - pathMD5: String - - ## fetch:test/digest/sha-256 path - pathSHA256: String + type TestData { + farenheit: Int @fetcher(name:"test/pipe" options:"farenheit") + } + +For now, the names of those `DataFetcher`s are in the form + + <namespace>/<name> + +Where `<namespace>` selects a source (OSGi service) of `DataFetcher` and `<name>` +selects a specific fetcher from that source. - ## fetch:test/digest/md5 resourceType - resourceTypeMD5: String - } - type Test { test: Boolean } +The `<options>` and `<source>` arguments of the directive are used by some of those +`DataFetcher` according to their own specification. See this module's tests +for examples. \ No newline at end of file diff --git a/src/main/java/org/apache/sling/graphql/core/engine/GraphQLResourceQuery.java b/src/main/java/org/apache/sling/graphql/core/engine/GraphQLResourceQuery.java index edc9a3f..e12aebd 100644 --- a/src/main/java/org/apache/sling/graphql/core/engine/GraphQLResourceQuery.java +++ b/src/main/java/org/apache/sling/graphql/core/engine/GraphQLResourceQuery.java @@ -22,9 +22,12 @@ package org.apache.sling.graphql.core.engine; import javax.script.ScriptException; import graphql.ExecutionInput; -import graphql.language.Comment; +import graphql.language.Argument; +import graphql.language.Directive; import graphql.language.FieldDefinition; import graphql.language.ObjectTypeDefinition; +import graphql.language.StringValue; + import org.apache.sling.api.resource.Resource; import org.apache.sling.graphql.api.SchemaProvider; import org.apache.sling.graphql.core.schema.DataFetcherDefinition; @@ -49,6 +52,11 @@ import java.util.Map; /** Run a GraphQL query in the context of a Sling Resource */ public class GraphQLResourceQuery { + public static final String FETCHER_DIRECTIVE = "fetcher"; + public static final String FETCHER_NAME = "name"; + public static final String FETCHER_OPTIONS = "options"; + public static final String FETCHER_SOURCE = "source"; + private final Logger log = LoggerFactory.getLogger(getClass()); public ExecutionResult executeQuery(SchemaProvider schemaProvider, DataFetcherSelector fetchersSelector, @@ -128,29 +136,25 @@ public class GraphQLResourceQuery { return builder.build(); } - private DataFetcher<Object> getDataFetcher(FieldDefinition field, DataFetcherSelector fetchers, - Resource r) throws IOException { - List<Comment> comments = field.getComments(); - for (Comment comment : comments) { - - String commentStr = comment.getContent(); - if (commentStr.startsWith("#")) { - commentStr = commentStr.substring(1).trim(); - - try { - DataFetcherDefinition def = new DataFetcherDefinition(commentStr); - DataFetcher<Object> fetcher = fetchers.getDataFetcherForType(def, r); - if (fetcher != null) { - return fetcher; - } else { - log.warn("No data fetcher registered for {}", def.toString()); - } - } catch (IllegalArgumentException iae) { - throw new IOException("Invalid fetcher definition", iae); - } - } + private String getDirectiveArgumentValue(Directive d, String name) { + final Argument a = d.getArgument(name); + if(a != null && a.getValue() instanceof StringValue) { + return ((StringValue)a.getValue()).getValue(); } return null; } + private DataFetcher<Object> getDataFetcher(FieldDefinition field, DataFetcherSelector fetchers, Resource r) throws IOException { + DataFetcher<Object> result = null; + final Directive d =field.getDirective(FETCHER_DIRECTIVE); + if(d != null) { + final DataFetcherDefinition def = new DataFetcherDefinition( + getDirectiveArgumentValue(d, FETCHER_NAME), + getDirectiveArgumentValue(d, FETCHER_OPTIONS), + getDirectiveArgumentValue(d, FETCHER_SOURCE) + ); + result = fetchers.getDataFetcherForType(def, r); + } + return result; + } } diff --git a/src/main/java/org/apache/sling/graphql/core/schema/DataFetcherDefinition.java b/src/main/java/org/apache/sling/graphql/core/schema/DataFetcherDefinition.java index 3a3d738..062a3dd 100644 --- a/src/main/java/org/apache/sling/graphql/core/schema/DataFetcherDefinition.java +++ b/src/main/java/org/apache/sling/graphql/core/schema/DataFetcherDefinition.java @@ -20,39 +20,22 @@ package org.apache.sling.graphql.core.schema; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +// TODO Since moving to schema directives, we don't really need this class anymore... public class DataFetcherDefinition { public final String fetcherNamespace; public final String fetcherName; public final String fetcherOptions; public final String fetcherSourceExpression; - /** Definitions are formatted like - * fetch:test/digest:sha512/$.path - */ - private static final Pattern REGEXP = Pattern.compile("fetch\\:(\\w+)/(\\w+)(/(\\S+))?( +(.*))?"); - - /** Creates a definition from a formatted String like - * - */ - public DataFetcherDefinition(String fetcherDef) { - if(fetcherDef == null) { - throw new IllegalArgumentException("Invalid input: " + fetcherDef); - } - final Matcher m = REGEXP.matcher(fetcherDef); - if(!m.matches()) { - throw new IllegalArgumentException("Input does not match " + REGEXP + ": " + fetcherDef); + public DataFetcherDefinition(String nameSpaceAndName, String options, String source) throws IllegalArgumentException { + final String [] parts = nameSpaceAndName.split("/"); + if(parts.length != 2) { + throw new IllegalArgumentException("Expected a namespace/name String, got " + nameSpaceAndName); } - fetcherNamespace = m.group(1); - fetcherName = m.group(2); - fetcherOptions = optional(m.group(4)); - fetcherSourceExpression = optional(m.group(6)); - } - - private static final String optional(String input) { - return input == null ? "" : input.trim(); + fetcherNamespace = parts[0]; + fetcherName = parts[1]; + fetcherOptions = options; + fetcherSourceExpression = source; } public String getFetcherNamespace() { @@ -79,4 +62,4 @@ public class DataFetcherDefinition { fetcherNamespace, fetcherName, fetcherOptions, fetcherSourceExpression); } -} +} \ No newline at end of file diff --git a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherDefinitionTest.java b/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherDefinitionTest.java deleted file mode 100644 index 3348d1d..0000000 --- a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherDefinitionTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.schema; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -@RunWith(Parameterized.class) -public class DataFetcherDefinitionTest { - - private final String input; - private final String expected; - private final Class<?> failureClass; - - @Parameters(name="{0}") - public static Collection<Object[]> data() { - final List<Object []> result = new ArrayList<>(); - - result.add(new Object[] { "fetch:test/withOptions/sha512,armored(UTF-8) $.path", "test#withOptions#sha512,armored(UTF-8)#$.path" }); - result.add(new Object[] { "fetch:namespace2/FetcherOption/upperCase", "namespace2#FetcherOption#upperCase#" }); - result.add(new Object[] { "fetch:namespace2/FetcherExpression \t sqrt(42)/3.4", "namespace2#FetcherExpression##sqrt(42)/3.4" }); - result.add(new Object[] { "fetch:namespace2/noOptions", "namespace2#noOptions##" }); - result.add(new Object[] { "wrongPrefix:namespace2/noOptions", IllegalArgumentException.class }); - result.add(new Object[] { "nimportequoi", IllegalArgumentException.class }); - result.add(new Object[] { "", IllegalArgumentException.class }); - result.add(new Object[] { null, IllegalArgumentException.class }); - - return result; - } - - public DataFetcherDefinitionTest(String input, Object expected) { - this.input = input; - if(expected instanceof String) { - this.expected = (String)expected; - this.failureClass = null; - } else { - this.expected = null; - this.failureClass = (Class<?>)expected; - } - } - - @Test - public void testMatch() throws Exception { - if(failureClass == null) { - final DataFetcherDefinition d = new DataFetcherDefinition(input); - assertEquals("DataFetcherDefinition#" + expected, d.toString()); - } else { - try { - new DataFetcherDefinition(input); - fail("Expecting a " + failureClass.getName()); - } catch(Throwable t) { - assertEquals("Expecting a " + failureClass.getName(), failureClass, t.getClass()); - } - } - } -} diff --git a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherSelectorTest.java b/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherSelectorTest.java index cfb0cb5..72e373b 100644 --- a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherSelectorTest.java +++ b/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherSelectorTest.java @@ -43,10 +43,10 @@ public class DataFetcherSelectorTest { @Test public void testGetDataFetcher() throws IOException { final DataFetcherSelector s = new DataFetcherSelector(context.bundleContext()); - assertFetcher(s, "fetch:ns1/name1", "DF#ns1#name1"); - assertFetcher(s, "fetch:ns1/name2", "DF#ns1#name2"); - assertFetcher(s, "fetch:ns2/name2", "DF#ns2#name2"); - assertFetcher(s, "fetch:ns2/othername", null); - assertFetcher(s, "fetch:otherns/name2", null); + assertFetcher(s, "ns1/name1", "DF#ns1#name1"); + assertFetcher(s, "ns1/name2", "DF#ns1#name2"); + assertFetcher(s, "ns2/name2", "DF#ns2#name2"); + assertFetcher(s, "ns2/othername", null); + assertFetcher(s, "otherns/name2", null); } } \ No newline at end of file diff --git a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherServiceRankingTest.java b/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherServiceRankingTest.java index 9d6231e..9bab263 100644 --- a/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherServiceRankingTest.java +++ b/src/test/java/org/apache/sling/graphql/core/schema/DataFetcherServiceRankingTest.java @@ -36,30 +36,30 @@ public class DataFetcherServiceRankingTest { @Test public void testDataFetcherRankings() throws IOException { final DataFetcherSelector s = new DataFetcherSelector(context.bundleContext()); - final String fetcherDef = "fetch:ns/name"; + final String nsAndName = "ns/name"; final String ns = "ns"; final String name = "name"; // Verify that given the same namespace and name we always get // the DataFetcher which has the lowest service ranking. - assertFetcher(s, fetcherDef, null); + assertFetcher(s, nsAndName, null); final ServiceRegistration<?> reg42 = new TestDataFetcherProvider(ns, name, 42).register(context.bundleContext()); - assertFetcher(s, fetcherDef, "DF#ns#name#42"); + assertFetcher(s, nsAndName, "DF#ns#name#42"); final ServiceRegistration<?> reg40 = new TestDataFetcherProvider(ns, name, 40).register(context.bundleContext()); - assertFetcher(s, fetcherDef, "DF#ns#name#40"); + assertFetcher(s, nsAndName, "DF#ns#name#40"); final ServiceRegistration<?> reg43 = new TestDataFetcherProvider(ns, name, 43).register(context.bundleContext()); - assertFetcher(s, fetcherDef, "DF#ns#name#40"); + assertFetcher(s, nsAndName, "DF#ns#name#40"); reg42.unregister(); - assertFetcher(s, fetcherDef, "DF#ns#name#40"); + assertFetcher(s, nsAndName, "DF#ns#name#40"); reg40.unregister(); - assertFetcher(s, fetcherDef, "DF#ns#name#43"); + assertFetcher(s, nsAndName, "DF#ns#name#43"); reg43.unregister(); - assertFetcher(s, fetcherDef, null); + assertFetcher(s, nsAndName, null); } } diff --git a/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java b/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java index 3b25042..d81348d 100644 --- a/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java +++ b/src/test/java/org/apache/sling/graphql/core/schema/SchemaDescriptionsTest.java @@ -123,7 +123,7 @@ public class SchemaDescriptionsTest { @Test public void verifyTypesDescriptions() { - assertTypeDescription("Query", " A blank line without leading hash does NOT dissociate the comments from the type that follows. GraphQL Schema used for our tests annotated with our fetch: definitions"); + assertTypeDescription("Query", "GraphQL Schema used for our tests"); assertTypeDescription("SlingResource", "SlingResource, for our tests"); assertTypeDescription("Test", "null"); } @@ -133,7 +133,7 @@ public class SchemaDescriptionsTest { assertFieldDescription("Query", "staticContent", "Test some static values"); assertFieldDescription("SlingResource", "pathMD5", "null"); assertFieldDescription("SlingResource", "pathSHA256", "SHA256 digest of the path"); - assertFieldDescription("SlingResource", "failure", "# fetch:failure/fail Failure message - the above fetch: statement is included in the description as it's not separated by an empty comment."); + assertFieldDescription("SlingResource", "failure", "Failure message"); assertFieldDescription("Test", "test", "null"); } } \ No newline at end of file diff --git a/src/test/java/org/apache/sling/graphql/core/schema/TestDataFetcherProvider.java b/src/test/java/org/apache/sling/graphql/core/schema/TestDataFetcherProvider.java index 1a86e90..7d08751 100644 --- a/src/test/java/org/apache/sling/graphql/core/schema/TestDataFetcherProvider.java +++ b/src/test/java/org/apache/sling/graphql/core/schema/TestDataFetcherProvider.java @@ -78,12 +78,12 @@ class TestDataFetcherProvider implements DataFetcherProvider { return ctx.registerService(DataFetcherProvider.class, this, props); } - static void assertFetcher(DataFetcherSelector s, String def, String expected) throws IOException { - final DataFetcher<Object> f = s.getDataFetcherForType(new DataFetcherDefinition(def), null); + static void assertFetcher(DataFetcherSelector s, String nsAndName, String expected) throws IOException { + final DataFetcher<Object> f = s.getDataFetcherForType(new DataFetcherDefinition(nsAndName, null, null), null); if(expected == null) { - assertNull("Expected null DataFetcher for " + def, f); + assertNull("Expected null DataFetcher for " + nsAndName, f); } else { - assertNotNull("Expected non-null DataFetcher for " + def, f); + assertNotNull("Expected non-null DataFetcher for " + nsAndName, f); assertEquals(expected, f.toString()); } } diff --git a/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp b/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp index ad09f7e..c1a00bb 100644 --- a/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp +++ b/src/test/resources/initial-content/apps/graphql/test/one/GQLschema.jsp @@ -17,16 +17,15 @@ * under the License. --%> -<%-- -Generating the schemas in JSP might not be the best way -but it works for these initial tests - we might create -a "passthrough" script engine, or one that extracts the -additional DataFetcher information that we need. ---%> +# This directive maps fields to our Sling data fetchers +directive @fetcher( + name : String, + options : String = "", + source : String = "" +) on FIELD_DEFINITION type Query { - ## fetch:test/pipe $ - scriptedSchemaResource : SlingResource + scriptedSchemaResource : SlingResource @fetcher(name:"test/pipe" source:"$") } type SlingResource { diff --git a/src/test/resources/initial-content/apps/graphql/test/two/GQLschema.jsp b/src/test/resources/initial-content/apps/graphql/test/two/GQLschema.jsp index 3731fde..ec3fb81 100644 --- a/src/test/resources/initial-content/apps/graphql/test/two/GQLschema.jsp +++ b/src/test/resources/initial-content/apps/graphql/test/two/GQLschema.jsp @@ -17,16 +17,15 @@ * under the License. --%> -<%-- -Generating the schemas in JSP might not be the best way -but it works for these initial tests - we might create -a "passthrough" script engine, or one that extracts the -additional DataFetcher information that we need. ---%> +# This directive maps fields to our Sling data fetchers +directive @fetcher( + name : String, + options : String = "", + source : String = "" +) on FIELD_DEFINITION type Query { - ## fetch:test/pipe $ - currentResource : SlingResource + currentResource : SlingResource @fetcher(name:"test/pipe" source:"$") } type SlingResource { diff --git a/src/test/resources/initial-content/apps/graphql/test/two/testing.GQLschema.jsp b/src/test/resources/initial-content/apps/graphql/test/two/testing.GQLschema.jsp index 61633a0..b0cf1da 100644 --- a/src/test/resources/initial-content/apps/graphql/test/two/testing.GQLschema.jsp +++ b/src/test/resources/initial-content/apps/graphql/test/two/testing.GQLschema.jsp @@ -17,19 +17,17 @@ * under the License. --%> -<%-- -Generating the schemas in JSP might not be the best way -but it works for these initial tests - we might create -a "passthrough" script engine, or one that extracts the -additional DataFetcher information that we need. ---%> +# This directive maps fields to our Sling data fetchers +directive @fetcher( + name : String, + options : String = "", + source : String = "" +) on FIELD_DEFINITION type Query { - ## fetch:test/pipe - withTestingSelector : TestData + withTestingSelector : TestData @fetcher(name:"test/pipe") } type TestData { - ## fetch:test/pipe/farenheit - farenheit: Int + farenheit: Int @fetcher(name:"test/pipe" options:"farenheit") } \ No newline at end of file diff --git a/src/test/resources/test-schema-selected-foryou.txt b/src/test/resources/test-schema-selected-foryou.txt index 0f00020..0c22a9d 100644 --- a/src/test/resources/test-schema-selected-foryou.txt +++ b/src/test/resources/test-schema-selected-foryou.txt @@ -15,18 +15,20 @@ # * specific language governing permissions and limitations # * under the License. -# GraphQL Schema used for our tests -# annotated with our fetch: definitions +# This directive maps fields to our Sling data fetchers +directive @fetcher( + name : String, + options : String = "", + source : String = "" +) on FIELD_DEFINITION +# GraphQL Schema used for our tests +# when specific request selectors are used type Query { - ## fetch:echoNS/echo - currentResource : SlingResource + currentResource : SlingResource @fetcher(name:"echoNS/echo") } type SlingResource { - ## fetch:test/fortyTwo - fortyTwo: Int - - ## fetch:test/fortyTwo - path: Int + fortyTwo: Int @fetcher(name:"test/fortyTwo") + path: Int @fetcher(name:"test/fortyTwo") } \ No newline at end of file diff --git a/src/test/resources/test-schema.txt b/src/test/resources/test-schema.txt index 0821e3f..1e8f419 100644 --- a/src/test/resources/test-schema.txt +++ b/src/test/resources/test-schema.txt @@ -15,25 +15,19 @@ # * specific language governing permissions and limitations # * under the License. -# Note the use of empty comment lines, like the one that follows -# this, to dissociate comments from the types or fields -# that follow. -# - -# A blank line without leading hash does NOT dissociate the comments -# from the type that follows. +# This directive maps fields to our Sling data fetchers +directive @fetcher( + name : String, + options : String = "", + source : String = "" +) on FIELD_DEFINITION # GraphQL Schema used for our tests -# annotated with our fetch: definitions type Query { - ## fetch:echoNS/echo - # - currentResource : SlingResource + currentResource : SlingResource @fetcher(name:"echoNS/echo") - ## fetch:test/static - # # Test some static values - staticContent: Test + staticContent: Test @fetcher(name:"test/static") } # This should be omitted from the SlingResource type description @@ -43,28 +37,18 @@ type SlingResource { path: String resourceType: String - ## fetch:test/digest/md5 path - # - pathMD5: String + pathMD5: String @fetcher(name:"test/digest" options:"md5" source:"path") - ## fetch:test/digest/sha-256 path - # # SHA256 digest of the path - pathSHA256: String + pathSHA256: String @fetcher(name:"test/digest" options:"sha-256" source:"path") - ## fetch:test/digest/md5 resourceType - # # MD5 digest of the resource type - resourceTypeMD5: String + resourceTypeMD5: String @fetcher(name:"test/digest" options:"md5" source:"resourceType") - ## fetch:echoNS/echo/null - # - nullValue: String + nullValue: String @fetcher(name:"echoNS/echo" options:"null") - ## fetch:failure/fail - # Failure message - the above fetch: statement is included in the - # description as it's not separated by an empty comment. - failure: String + # Failure message + failure: String @fetcher(name:"failure/fail") } type Test {