Skip to content

Commit

Permalink
feat: Process Iterable same way as Collection type (#9709)
Browse files Browse the repository at this point in the history
* Process Iterable same way as Collection type
Fixes #9256

* add missed file

* Move IterableEndpoint fixture and test into their own package

The endpoint generator testing setup requires a separate package for
each test to avoid interference between tests. This is because the
generator in the tests emits all classes per package.

* Fix IterableEndpointGenerationTest

Co-authored-by: Nikolai Gorokhov <nikolai@vaadin.com>
Co-authored-by: Anton Platonov <platosha@gmail.com>
  • Loading branch information
3 people committed Feb 24, 2021
1 parent c58d5f1 commit eeffced
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ private boolean isNumberType(ResolvedType type) {
}

private boolean isCollectionType(ResolvedType type) {
return !type.isPrimitive() && isTypeOf(type, Collection.class);
return !type.isPrimitive() && (isTypeOf(type, Collection.class) || isTypeOf(type, Iterable.class));
}

private boolean isMapType(ResolvedType type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import com.vaadin.flow.server.connect.auth.VaadinConnectAccessChecker;
import com.vaadin.flow.server.connect.exception.EndpointException;
import com.vaadin.flow.server.connect.exception.EndpointValidationException;
import com.vaadin.flow.server.connect.generator.endpoints.iterableendpoint.IterableEndpoint;
import com.vaadin.flow.server.connect.generator.endpoints.superclassmethods.PersonEndpoint;
import com.vaadin.flow.server.connect.testendpoint.BridgeMethodTestEndpoint;
import com.vaadin.flow.server.startup.ApplicationConfiguration;
Expand Down Expand Up @@ -283,7 +284,7 @@ public void should_CallEnableCsrf_When_GettingTheAccessChecker() {
ApplicationContext appContext = mockApplicationContext(TEST_ENDPOINT);
VaadinConnectAccessChecker accessChecker = mock(VaadinConnectAccessChecker.class);
Mockito.doReturn(accessChecker).when(appContext).getBean(VaadinConnectAccessChecker.class);

VaadinConnectController controller = new VaadinConnectController(
new ObjectMapper(), mock(EndpointNameChecker.class),
mock(ExplicitNullableTypeChecker.class), appContext);
Expand Down Expand Up @@ -796,7 +797,7 @@ public void should_UseCustomEndpointName_When_ItIsDefined() {
new TestClassWithCustomEndpointName()));

VaadinConnectController vaadinConnectController = createVaadinControllerWithApplicationContext(contextMock);

ResponseEntity<String> response = vaadinConnectController
.serveEndpoint("CustomEndpoint", "testMethod",
createRequestParameters(
Expand Down Expand Up @@ -1132,6 +1133,15 @@ public void should_AllowAccessToPackagePrivateEndpoint_PublicMethods()
assertEquals("\"Hello\"", response.getBody());
}

@Test
public void should_ConvertIterableIntoArray() {
ResponseEntity<?> response = createVaadinController(new IterableEndpoint())
.serveEndpoint("IterableEndpoint", "getFoos", createRequestParameters("{}"), requestMock);

assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("[{\"bar\":\"bar\"},{\"bar\":\"bar\"}]", response.getBody());
}

private void assertEndpointInfoPresent(String responseBody) {
assertTrue(String.format(
"Response body '%s' should have endpoint information in it",
Expand Down Expand Up @@ -1196,7 +1206,7 @@ private <T> VaadinConnectController createVaadinController(T endpoint,

ApplicationContext mockApplicationContext = mockApplicationContext(endpoint);
VaadinConnectController connectController = Mockito.spy(
new VaadinConnectController(vaadinEndpointMapper, endpointNameChecker,
new VaadinConnectController(vaadinEndpointMapper, endpointNameChecker,
explicitNullableTypeChecker, mockApplicationContext)
);
Mockito.doReturn(accessChecker).when(connectController).getAccessChecker(any());
Expand All @@ -1211,7 +1221,7 @@ private VaadinConnectController createVaadinControllerWithoutPrincipal() {

private VaadinConnectController createVaadinControllerWithApplicationContext(
ApplicationContext applicationContext) {
VaadinConnectControllerMockBuilder controllerMockBuilder
VaadinConnectControllerMockBuilder controllerMockBuilder
= new VaadinConnectControllerMockBuilder();
VaadinConnectController vaadinConnectController = controllerMockBuilder
.withObjectMapper(new ObjectMapper())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
Expand Down Expand Up @@ -186,7 +186,11 @@ private Class<?> applyTypeArguments(Type type, HashMap<String,
((GenericArrayType) type).getGenericComponentType(),
typeArguments)).getClass();
} else if (type instanceof TypeVariable) {
return typeArguments.get(((TypeVariable) type).getName());
Class<?> argument = typeArguments
.get(((TypeVariable<?>) type).getName());
if (argument != null) {
return argument;
}
}

return Object.class;
Expand Down Expand Up @@ -288,11 +292,16 @@ private void assertPath(Class<?> testEndpointClass,
expectedEndpointMethod.getGenericParameterTypes();
Class<?>[] parameterTypes =
new Class<?>[genericParameterTypes.length];
List<HashMap<String, Class<?>>> parameterTypeArguments =
new ArrayList<>();
for (int i = 0; i < genericParameterTypes.length; i++) {
parameterTypes[i] = applyTypeArguments(genericParameterTypes[i],
typeArguments);
parameterTypeArguments.add(i, extractTypeArguments(
genericParameterTypes[i], typeArguments));
}
assertRequestSchema(requestSchema, parameterTypes,
parameterTypeArguments,
expectedEndpointMethod.getParameters());
} else {
assertNull(String.format(
Expand All @@ -309,10 +318,11 @@ private void assertPath(Class<?> testEndpointClass,
"Every operation is expected to have a single '200' response",
apiResponse);

Class<?> returnType = applyTypeArguments(
expectedEndpointMethod.getGenericReturnType(), typeArguments);
Type genericReturnType = expectedEndpointMethod.getGenericReturnType();
Class<?> returnType = applyTypeArguments(genericReturnType, typeArguments);
if (returnType != void.class) {
assertSchema(extractSchema(apiResponse.getContent()), returnType);
assertSchema(extractSchema(apiResponse.getContent()), returnType,
extractTypeArguments(genericReturnType, typeArguments));
} else {
assertNull(String.format(
"No response is expected to be present for void method '%s'",
Expand All @@ -325,15 +335,18 @@ private void assertPath(Class<?> testEndpointClass,
}

private void assertRequestSchema(Schema requestSchema,
Class<?>[] parameterTypes, Parameter[] parameters) {
Class<?>[] parameterTypes,
List<HashMap<String, Class<?>>> parameterTypeArguments,
Parameter[] parameters) {
Map<String, Schema> properties = requestSchema.getProperties();
assertEquals(
"Request schema should have the same amount of properties as the corresponding endpoint method parameters number",
parameterTypes.length, properties.size());
int index = 0;
for (Map.Entry<String, Schema> stringSchemaEntry : properties
.entrySet()) {
assertSchema(stringSchemaEntry.getValue(), parameterTypes[index]);
assertSchema(stringSchemaEntry.getValue(), parameterTypes[index],
parameterTypeArguments.get(index));
List requiredList = requestSchema.getRequired();
if (parameters[index].isAnnotationPresent(Nullable.class) ||
Optional.class.isAssignableFrom(parameters[index].getType())) {
Expand Down Expand Up @@ -365,15 +378,17 @@ private void assertComponentSchemas(Map<String, Schema> actualSchemas,
assertNotNull(String.format(
"Expected to have a schema defined for a class '%s'",
expectedSchemaClass), actualSchema);
assertSchema(actualSchema, expectedSchemaClass);
assertSchema(actualSchema, expectedSchemaClass, new HashMap<>());
}
assertEquals("Expected to have all endpoint classes defined in schemas",
schemasCount, actualSchemas.size());
}

private void assertSchema(Schema actualSchema,
Class<?> expectedSchemaClass) {
if (assertSpecificJavaClassSchema(actualSchema, expectedSchemaClass)) {
Class<?> expectedSchemaClass,
HashMap<String, Class<?>> typeArguments) {
if (assertSpecificJavaClassSchema(actualSchema, expectedSchemaClass,
typeArguments)) {
return;
}

Expand All @@ -395,10 +410,15 @@ private void assertSchema(Schema actualSchema,
} else if (actualSchema instanceof ArraySchema) {
if (expectedSchemaClass.isArray()) {
assertSchema(((ArraySchema) actualSchema).getItems(),
expectedSchemaClass.getComponentType());
expectedSchemaClass.getComponentType(),
typeArguments);
} else {
assertTrue(Collection.class
assertTrue(Iterable.class
.isAssignableFrom(expectedSchemaClass));
Type itemType = expectedSchemaClass.getTypeParameters()[0];
assertSchema(((ArraySchema) actualSchema).getItems(),
applyTypeArguments(itemType, typeArguments),
new HashMap<>());
}
} else if (actualSchema instanceof MapSchema) {
assertTrue(Map.class.isAssignableFrom(expectedSchemaClass));
Expand All @@ -418,7 +438,8 @@ private void assertSchema(Schema actualSchema,
for (Schema schema : allOf) {
if (expectedSchemaClass.getCanonicalName()
.equals(schema.getName())) {
assertSchemaProperties(expectedSchemaClass, schema);
assertSchemaProperties(expectedSchemaClass,
typeArguments, schema);
break;
}
}
Expand All @@ -429,7 +450,8 @@ private void assertSchema(Schema actualSchema,
allOf.get(0).getName());
}
} else if (actualSchema instanceof ObjectSchema) {
assertSchemaProperties(expectedSchemaClass, actualSchema);
assertSchemaProperties(expectedSchemaClass, typeArguments,
actualSchema);
} else {
throw new AssertionError(
String.format("Unknown schema '%s' for class '%s'",
Expand All @@ -439,11 +461,19 @@ private void assertSchema(Schema actualSchema,
}

private boolean assertSpecificJavaClassSchema(Schema actualSchema,
Class<?> expectedSchemaClass) {
Class<?> expectedSchemaClass,
HashMap<String, Class<?>> typeArguments) {
if (expectedSchemaClass == Optional.class) {
if (actualSchema instanceof ComposedSchema) {
ComposedSchema actualComposedSchema =
(ComposedSchema) actualSchema;
assertEquals(1,
((ComposedSchema) actualSchema).getAllOf().size());
actualComposedSchema.getAllOf().size());
assertSchema(((ComposedSchema) actualSchema).getAllOf().get(0),
typeArguments
.get(expectedSchemaClass.getTypeParameters()[0]
.getName()),
new HashMap<>());
}
} else if (expectedSchemaClass == Object.class) {
assertNull(actualSchema.getProperties());
Expand All @@ -457,7 +487,7 @@ private boolean assertSpecificJavaClassSchema(Schema actualSchema,
}

private void assertSchemaProperties(Class<?> expectedSchemaClass,
Schema schema) {
HashMap<String, Class<?>> typeArguments, Schema schema) {
int expectedFieldsCount = 0;
Map<String, Schema> properties = schema.getProperties();
assertNotNull(properties);
Expand All @@ -476,7 +506,9 @@ private void assertSchemaProperties(Class<?> expectedSchemaClass,
.get(expectedSchemaField.getName());
assertNotNull(String.format("Property schema is not found %s",
expectedSchemaField.getName()), propertySchema);
assertSchema(propertySchema, expectedSchemaField.getType());
Type type = expectedSchemaField.getGenericType();
assertSchema(propertySchema, expectedSchemaField.getType(),
extractTypeArguments(type, typeArguments));
if (Optional.class
.isAssignableFrom(expectedSchemaField.getType()) ||
expectedSchemaField.isAnnotationPresent(Nullable.class)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2000-2021 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.server.connect.generator.endpoints.iterableendpoint;

import java.util.Arrays;

import com.vaadin.flow.server.connect.Endpoint;
import com.vaadin.flow.server.connect.auth.AnonymousAllowed;

@Endpoint
@AnonymousAllowed
public class IterableEndpoint {

public static class Foo {
public String bar = "bar";
}

public Iterable<Foo> getFoos() {
return Arrays.asList(new Foo(), new Foo());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2000-2021 Vaadin Ltd.
*
* Licensed 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 com.vaadin.flow.server.connect.generator.endpoints.iterableendpoint;

import com.vaadin.flow.server.connect.generator.endpoints.AbstractEndpointGenerationTest;

import org.junit.Test;

import java.util.Collections;

/**
* com.vaadin.flow.server.connect.generator.endpoints.iterableendpoint.IterableEndpointGenerationTest, created on 21/12/2020 23.00
* @author nikolaigorokhov
*/
public class IterableEndpointGenerationTest extends AbstractEndpointGenerationTest {

public IterableEndpointGenerationTest() {
super(Collections.singletonList(IterableEndpoint.class));
}

@Test
public void should_ConvertIterableIntoArrayInTS() {
verifyOpenApiObjectAndGeneratedTs();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* This module is generated from IterableEndpoint.java
* All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
* @module IterableEndpoint
*/

// @ts-ignore
import client from './connect-client.default';
import Foo from './com/vaadin/flow/server/connect/generator/endpoints/iterableendpoint/IterableEndpoint/Foo';

function _getFoos(): Promise<Array<Foo>> {
return client.call('IterableEndpoint', 'getFoos');
}
export {_getFoos as getFoos};

export const IterableEndpoint = Object.freeze({
getFoos: _getFoos,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* This module is generated from com.vaadin.flow.server.connect.generator.endpoints.iterableendpoint.IterableEndpoint.Foo.
* All changes to this file are overridden. Please consider to make changes in the corresponding Java file if necessary.
*/

export default interface Foo {
bar: string;
}

0 comments on commit eeffced

Please sign in to comment.