Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display JSON and protobuf request and response specification in DocService #4322

Merged
merged 27 commits into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9358084
Display JSON and protobuf request and response specification in `DocS…
ikhoon Jun 28, 2022
95b0c4d
Fix javadoc error
ikhoon Jun 28, 2022
d9c31a0
Remove cruft
ikhoon Jun 28, 2022
9298e47
Fix tests
ikhoon Jul 1, 2022
9f810ae
Address comments by @jrhee17
ikhoon Jul 18, 2022
d279c17
Merge branch 'master' into struct-info-provider
ikhoon Jul 29, 2022
bfb8a03
Fill `DescriptionInfo` using `NamedTypeInfoProvider`
ikhoon Jul 29, 2022
2327fa5
Refactor `DocService` for better readability
ikhoon Jul 30, 2022
bfd6190
Add ExampleSupport
ikhoon Jul 30, 2022
0c91cfd
Fix NPE
ikhoon Aug 1, 2022
b14dff3
Fix failed tests
ikhoon Aug 1, 2022
11ee1ff
Add alias for linking proto messages
ikhoon Aug 1, 2022
fc32b33
lint
ikhoon Aug 1, 2022
00050a9
Fix tests
ikhoon Aug 3, 2022
316fdcd
Add ReflectiveNamedTypeInfoProvider in order to support non-json types
ikhoon Aug 3, 2022
df72d25
Fix a bug
ikhoon Aug 3, 2022
c3f0b63
Checkstyle
ikhoon Aug 3, 2022
97b9d8c
Rename ThriftNameTypeInfoProvider to ThriftNamedTypeInfoProvider
ikhoon Aug 3, 2022
7e6c892
Address comments
ikhoon Aug 8, 2022
b822357
Remove cruft
ikhoon Aug 8, 2022
5b3f3f1
Update Javadoc for alias
ikhoon Aug 11, 2022
f81452b
Use TEnum
ikhoon Aug 16, 2022
61dd8c0
Add tests to check unexpect types
ikhoon Aug 16, 2022
8412738
Fix compile errors
ikhoon Aug 16, 2022
05e11ac
Add comments
ikhoon Aug 17, 2022
fab9b95
Merge branch 'master' into struct-info-provider
ikhoon Sep 8, 2022
53475b1
Fix mistaks in the comments and Javadoc. No code has changed
ikhoon Sep 8, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import static java.util.Objects.requireNonNull;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
Expand All @@ -40,12 +39,13 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
Expand All @@ -68,13 +68,13 @@
import com.linecorp.armeria.server.docs.DocServicePlugin;
import com.linecorp.armeria.server.docs.EndpointInfo;
import com.linecorp.armeria.server.docs.EndpointInfoBuilder;
import com.linecorp.armeria.server.docs.EnumInfo;
import com.linecorp.armeria.server.docs.FieldInfo;
import com.linecorp.armeria.server.docs.FieldInfoBuilder;
import com.linecorp.armeria.server.docs.FieldLocation;
import com.linecorp.armeria.server.docs.FieldRequirement;
import com.linecorp.armeria.server.docs.MethodInfo;
import com.linecorp.armeria.server.docs.NamedTypeInfo;
import com.linecorp.armeria.server.docs.NamedTypeInfoProvider;
import com.linecorp.armeria.server.docs.ServiceInfo;
import com.linecorp.armeria.server.docs.ServiceSpecification;
import com.linecorp.armeria.server.docs.StructInfo;
Expand Down Expand Up @@ -107,10 +107,17 @@ public final class AnnotatedDocServicePlugin implements DocServicePlugin {
static final TypeSignature BINARY = TypeSignature.ofBase("binary");
@VisibleForTesting
static final TypeSignature BEAN = TypeSignature.ofBase("bean");
@VisibleForTesting
static final TypeSignature OBJECT = TypeSignature.ofBase("object");

private static final ObjectWriter objectWriter = JacksonUtil.newDefaultObjectMapper()
.writerWithDefaultPrettyPrinter();

private static final NamedTypeInfoProvider requestJsonNamedTypeInfoProvider =
new JsonNamedTypeInfoProvider(true);
private static final NamedTypeInfoProvider responseJsonNamedTypeInfoProvider =
new JsonNamedTypeInfoProvider(false);

@Override
public String name() {
return "annotated";
Expand All @@ -123,9 +130,11 @@ public String name() {

@Override
public ServiceSpecification generateSpecification(Set<ServiceConfig> serviceConfigs,
DocServiceFilter filter) {
DocServiceFilter filter,
NamedTypeInfoProvider namedTypeInfoProvider) {
requireNonNull(serviceConfigs, "serviceConfigs");
requireNonNull(filter, "filter");
requireNonNull(namedTypeInfoProvider, "namedTypeInfoProvider");

final Map<Class<?>, Set<MethodInfo>> methodInfos = new HashMap<>();
final Map<Class<?>, String> serviceDescription = new HashMap<>();
Expand All @@ -137,12 +146,12 @@ public ServiceSpecification generateSpecification(Set<ServiceConfig> serviceConf
if (!filter.test(name(), className, methodName)) {
return;
}
addMethodInfo(methodInfos, sc.virtualHost().hostnamePattern(), service);
addMethodInfo(methodInfos, sc.virtualHost().hostnamePattern(), service, namedTypeInfoProvider);
addServiceDescription(serviceDescription, service);
}
});

return generate(serviceDescription, methodInfos);
return generate(serviceDescription, methodInfos, namedTypeInfoProvider);
}

private static void addServiceDescription(Map<Class<?>, String> serviceDescription,
Expand All @@ -152,13 +161,14 @@ private static void addServiceDescription(Map<Class<?>, String> serviceDescripti
}

private static void addMethodInfo(Map<Class<?>, Set<MethodInfo>> methodInfos,
String hostnamePattern, AnnotatedService service) {
String hostnamePattern, AnnotatedService service,
NamedTypeInfoProvider namedTypeInfoProvider) {
final Route route = service.route();
final EndpointInfo endpoint = endpointInfo(route, hostnamePattern);
final Method method = service.method();
final String name = method.getName();
final TypeSignature returnTypeSignature = getReturnTypeSignature(method);
final List<FieldInfo> fieldInfos = fieldInfos(service.annotatedValueResolvers());
final List<FieldInfo> fieldInfos = fieldInfos(service.annotatedValueResolvers(), namedTypeInfoProvider);
final Class<?> clazz = service.object().getClass();
route.methods().forEach(
httpMethod -> {
Expand Down Expand Up @@ -225,10 +235,11 @@ private static Set<MediaType> availableMimeTypes(Route route) {
return builder.build();
}

private static List<FieldInfo> fieldInfos(List<AnnotatedValueResolver> resolvers) {
private static List<FieldInfo> fieldInfos(List<AnnotatedValueResolver> resolvers,
NamedTypeInfoProvider namedTypeInfoProvider) {
final ImmutableList.Builder<FieldInfo> fieldInfosBuilder = ImmutableList.builder();
for (AnnotatedValueResolver resolver : resolvers) {
final FieldInfo fieldInfo = fieldInfo(resolver);
final FieldInfo fieldInfo = fieldInfo(resolver, namedTypeInfoProvider);
if (fieldInfo != null) {
fieldInfosBuilder.add(fieldInfo);
}
Expand All @@ -237,7 +248,8 @@ private static List<FieldInfo> fieldInfos(List<AnnotatedValueResolver> resolvers
}

@Nullable
private static FieldInfo fieldInfo(AnnotatedValueResolver resolver) {
private static FieldInfo fieldInfo(AnnotatedValueResolver resolver,
NamedTypeInfoProvider namedTypeInfoProvider) {
final Class<? extends Annotation> annotationType = resolver.annotationType();
if (annotationType == RequestObject.class) {
final BeanFactoryId beanFactoryId = resolver.beanFactoryId();
Expand All @@ -251,10 +263,25 @@ private static FieldInfo fieldInfo(AnnotatedValueResolver resolver) {
if (!resolvers.isEmpty()) {
// Just use the simple name of the bean class as the field name.
return FieldInfo.builder(beanFactoryId.type().getSimpleName(), BEAN,
fieldInfos(resolvers)).build();
fieldInfos(resolvers, namedTypeInfoProvider)).build();
}
} else {
// NamedTypeInfoProvider may provide NamedTypedInfo for the implicit request object.
final Class<?> elementType = resolver.elementType();
NamedTypeInfo namedTypeInfo = namedTypeInfoProvider.newNamedTypeInfo(elementType);
if (namedTypeInfo == null) {
namedTypeInfo = requestJsonNamedTypeInfoProvider.newNamedTypeInfo(elementType);
}
if (namedTypeInfo instanceof StructInfo && !((StructInfo) namedTypeInfo).fields().isEmpty()) {
return FieldInfo.builder(namedTypeInfo.name(), OBJECT,
((StructInfo) namedTypeInfo).fields())
.requirement(resolver.shouldExist() ?
FieldRequirement.REQUIRED : FieldRequirement.OPTIONAL)
.build();
} else {
return FieldInfo.of(elementType.getName(), toTypeSignature(elementType));
minwoox marked this conversation as resolved.
Show resolved Hide resolved
}
}
return null;
}

if (annotationType != Param.class && annotationType != Header.class) {
Expand Down Expand Up @@ -290,10 +317,13 @@ private static FieldInfo fieldInfo(AnnotatedValueResolver resolver) {
return builder.build();
}

@VisibleForTesting
static TypeSignature toTypeSignature(Type type) {
requireNonNull(type, "type");

if (type instanceof JavaType) {
return toTypeSignature((JavaType) type);
}

// The data types defined by the OpenAPI Specification:

if (type == Void.class || type == void.class) {
Expand Down Expand Up @@ -344,6 +374,10 @@ static TypeSignature toTypeSignature(Type type) {
return TypeSignature.ofMap(key, value);
}

if (Optional.class.isAssignableFrom(rawType)) {
return TypeSignature.ofOptional(toTypeSignature(parameterizedType.getActualTypeArguments()[0]));
}

final List<TypeSignature> actualTypes = Stream.of(parameterizedType.getActualTypeArguments())
.map(AnnotatedDocServicePlugin::toTypeSignature)
.collect(toImmutableList());
Expand Down Expand Up @@ -371,7 +405,26 @@ static TypeSignature toTypeSignature(Type type) {
return TypeSignature.ofList(toTypeSignature(clazz.getComponentType()));
}

return TypeSignature.ofBase(clazz.getSimpleName());
return TypeSignature.ofNamed(clazz);
minwoox marked this conversation as resolved.
Show resolved Hide resolved
}

static TypeSignature toTypeSignature(JavaType type) {
if (type.isArrayType() || type.isCollectionLikeType()) {
return TypeSignature.ofList(toTypeSignature(type.getContentType()));
}

if (type.isMapLikeType()) {
final TypeSignature key = toTypeSignature(type.getKeyType());
final TypeSignature value = toTypeSignature(type.getContentType());
return TypeSignature.ofMap(key, value);
}

if (Optional.class.isAssignableFrom(type.getRawClass())) {
return TypeSignature.ofOptional(
toTypeSignature(type.getBindings().getBoundType(0)));
}

return toTypeSignature(type.getRawClass());
}

private static FieldLocation location(AnnotatedValueResolver resolver) {
Expand All @@ -389,7 +442,8 @@ private static FieldLocation location(AnnotatedValueResolver resolver) {

@VisibleForTesting
static ServiceSpecification generate(Map<Class<?>, String> serviceDescription,
Map<Class<?>, Set<MethodInfo>> methodInfos) {
Map<Class<?>, Set<MethodInfo>> methodInfos,
NamedTypeInfoProvider namedTypeInfoProvider) {
final Set<ServiceInfo> serviceInfos = methodInfos
.entrySet().stream()
.map(entry -> {
Expand All @@ -399,33 +453,31 @@ static ServiceSpecification generate(Map<Class<?>, String> serviceDescription,
})
.collect(toImmutableSet());

return ServiceSpecification.generate(serviceInfos, AnnotatedDocServicePlugin::newNamedTypeInfo);
return ServiceSpecification.generate(
serviceInfos, typeSignature -> newNamedTypeInfo(typeSignature, namedTypeInfoProvider));
}

private static NamedTypeInfo newNamedTypeInfo(TypeSignature typeSignature) {
final Class<?> type = (Class<?>) typeSignature.namedTypeDescriptor();
if (type == null) {
private static NamedTypeInfo newNamedTypeInfo(TypeSignature typeSignature, NamedTypeInfoProvider provider) {
final Object typeDescriptor = typeSignature.namedTypeDescriptor();
if (typeDescriptor == null) {
throw new IllegalArgumentException("cannot create a named type from: " + typeSignature);
}

if (type.isEnum()) {
@SuppressWarnings("unchecked")
final Class<? extends Enum<?>> enumType = (Class<? extends Enum<?>>) type;
return new EnumInfo(enumType);
NamedTypeInfo namedTypeInfo = provider.newNamedTypeInfo(typeDescriptor);
if (namedTypeInfo != null) {
return namedTypeInfo;
}

return newStructInfo(type);
}

private static StructInfo newStructInfo(Class<?> structClass) {
final String name = structClass.getName();

final Field[] declaredFields = structClass.getDeclaredFields();
final List<FieldInfo> fields =
Stream.of(declaredFields)
.map(f -> FieldInfo.of(f.getName(), toTypeSignature(f.getGenericType())))
.collect(Collectors.toList());
return new StructInfo(name, fields);
// If the type can be serialized to JSON, try extracting a `StructInfo` using Jackson.
// Don't care about the request object because the `StructInfo` for the request object is processed
// when building the `FieldInfo` of the `MethodInfo`.
namedTypeInfo = responseJsonNamedTypeInfoProvider.newNamedTypeInfo(typeDescriptor);
jrhee17 marked this conversation as resolved.
Show resolved Hide resolved
if (namedTypeInfo != null) {
return namedTypeInfo;
} else {
// An unresolved StructInfo.
return new StructInfo(typeSignature.name(), ImmutableList.of());
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,16 @@ private static Type parameterizedTypeOf(AnnotatedElement element) {
element.getClass().getSimpleName());
}

static boolean isAnnotatedNullable(AnnotatedElement annotatedElement) {
for (Annotation a : annotatedElement.getAnnotations()) {
final String annotationTypeName = a.annotationType().getName();
if (annotationTypeName.endsWith(".Nullable")) {
return true;
}
}
return false;
}

@Nullable
private final Class<? extends Annotation> annotationType;

Expand Down Expand Up @@ -1333,16 +1343,6 @@ private Class<?> getElementType(Type parameterizedType, boolean unwrapOptional)
return toRawType(elementType);
}

private static boolean isAnnotatedNullable(AnnotatedElement annotatedElement) {
for (Annotation a : annotatedElement.getAnnotations()) {
final String annotationTypeName = a.annotationType().getName();
if (annotationTypeName.endsWith(".Nullable")) {
return true;
}
}
return false;
}

@Nullable
private static ParameterizedType getParameterizedElementType(Type parameterizedType) {
if (!(parameterizedType instanceof ParameterizedType)) {
Expand Down
Loading