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

Enable jsonschema support in debug page for annotated too #5253

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -22,6 +22,7 @@
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.isReturnTypeNothing;
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.kFunctionGenericReturnType;
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.kFunctionReturnType;
import static com.linecorp.armeria.server.docs.FieldLocation.BODY;
import static com.linecorp.armeria.server.docs.FieldLocation.HEADER;
import static com.linecorp.armeria.server.docs.FieldLocation.PATH;
import static com.linecorp.armeria.server.docs.FieldLocation.QUERY;
Expand Down Expand Up @@ -53,6 +54,7 @@
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.ImmutableSet;

import com.linecorp.armeria.common.HttpMethod;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.internal.common.JacksonUtil;
Expand Down Expand Up @@ -171,17 +173,45 @@ private static void addMethodInfo(Map<Class<?>, Set<MethodInfo>> methodInfos,
final Method method = service.method();
final int overloadId = service.overloadId();
final TypeSignature returnTypeSignature = getReturnTypeSignature(method);
final List<FieldInfo> fieldInfos = fieldInfos(service.annotatedValueResolvers());
route.methods().forEach(
httpMethod -> {
final MethodInfo methodInfo = new MethodInfo(
serviceClass.getName(), method.getName(), overloadId, returnTypeSignature,
fieldInfos, ImmutableList.of(),
ImmutableList.of(endpoint), httpMethod,
AnnotatedServiceFactory.findDescription(method));

methodInfos.computeIfAbsent(serviceClass, unused -> new HashSet<>()).add(methodInfo);
});
List<FieldInfo> fieldInfos = fieldInfos(service.annotatedValueResolvers());
boolean useParamaterAsRoot = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
boolean useParamaterAsRoot = false;
boolean useParameterAsRoot = false;

// Support debug form schema when there is a body parameter.
for (int i = 0; i < fieldInfos.size(); i++) {
final FieldInfo field = fieldInfos.get(i);
// Currently body parameters are generally UNSPECIFIED.
if (field.location() != UNSPECIFIED && field.location() != BODY) {
continue;
}
useParamaterAsRoot = true;
if (i != 0) {
// Move root parameter to front.
final ImmutableList.Builder<FieldInfo> moved =
ImmutableList.builderWithExpectedSize(fieldInfos.size());
moved.add(field);
moved.addAll(fieldInfos.subList(0, i));
moved.addAll(fieldInfos.subList(i + 1, fieldInfos.size()));
fieldInfos = moved.build();
}
}

for (HttpMethod httpMethod : route.methods()) {
final MethodInfo methodInfo = new MethodInfo(
serviceClass.getName(), method.getName(),
returnTypeSignature,
fieldInfos,
useParamaterAsRoot,
ImmutableList.of(),
ImmutableList.of(endpoint),
ImmutableList.of(),
ImmutableList.of(),
ImmutableList.of(),
ImmutableList.of(),
httpMethod,
AnnotatedServiceFactory.findDescription(method),
overloadId);

methodInfos.computeIfAbsent(serviceClass, unused -> new HashSet<>()).add(methodInfo);
}
}

private static TypeSignature getReturnTypeSignature(Method method) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ private ObjectNode generate(MethodInfo methodInfo) {
methodFields = ImmutableList.of();
} else {
methodFields = structInfo.fields();
root.put("useParamaterAsRoot", true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
root.put("useParamaterAsRoot", true);
root.put("useParameterAsRoot", true);

}
visited.put(signature, currentPath);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,27 @@ public MethodInfo(String serviceName, String name,
);
}

/**
* Creates a new instance.
*/
public MethodInfo(String serviceName, String name,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added yet more craziness to this file. Hopefully not for this PR, but probably need to get a builder in

TypeSignature returnTypeSignature,
Iterable<FieldInfo> parameters,
boolean useParameterAsRoot,
Iterable<TypeSignature> exceptionTypeSignatures,
Iterable<EndpointInfo> endpoints,
Iterable<HttpHeaders> exampleHeaders,
Iterable<String> exampleRequests,
Iterable<String> examplePaths,
Iterable<String> exampleQueries,
HttpMethod httpMethod,
DescriptionInfo descriptionInfo) {
this(name, returnTypeSignature, parameters, useParameterAsRoot,
exceptionTypeSignatures, endpoints, exampleHeaders,
exampleRequests, examplePaths, exampleQueries, httpMethod, descriptionInfo,
createId(serviceName, name, 0, httpMethod));
}

/**
* Creates a new instance.
*/
Expand All @@ -112,11 +133,12 @@ public MethodInfo(String serviceName, String name,
Iterable<String> examplePaths,
Iterable<String> exampleQueries,
HttpMethod httpMethod,
DescriptionInfo descriptionInfo) {
DescriptionInfo descriptionInfo,
int overloadId) {
this(name, returnTypeSignature, parameters, useParameterAsRoot,
exceptionTypeSignatures, endpoints, exampleHeaders,
exampleRequests, examplePaths, exampleQueries, httpMethod, descriptionInfo,
createId(serviceName, name, 0, httpMethod));
createId(serviceName, name, overloadId, httpMethod));
}

MethodInfo(String name, TypeSignature returnTypeSignature,
Expand All @@ -130,7 +152,6 @@ public MethodInfo(String serviceName, String name,

this.returnTypeSignature = requireNonNull(returnTypeSignature, "returnTypeSignature");
this.parameters = ImmutableList.copyOf(requireNonNull(parameters, "parameters"));
assert !useParameterAsRoot || this.parameters.size() == 1;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could tweak the assertion to look for a single root parameter but it seemed like overkill

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: the following is also a valid method that has multiple resolvers, but uses headers/query params instead of the body to fill the bean.

Since the last non-param/query will be used, in the worst case it is possible that wrong autocomplete/recommendations are done.

@Post("/bean2")
public HttpResponse bean2(@RequestObject JsonRequest jsonRequest, CompositeBean compositeBean1) {
    return HttpResponse.of("world");
}

Having said this, I'm not sure if there is a non-trivial way to intelligently find the parameter that should be used as the body.
Let me think about this a little more..

this.useParameterAsRoot = useParameterAsRoot;

this.exceptionTypeSignatures =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static com.linecorp.armeria.internal.server.annotation.AnnotatedDocServicePluginTest.compositeBean;
import static com.linecorp.armeria.server.docs.FieldLocation.PATH;
import static com.linecorp.armeria.server.docs.FieldLocation.QUERY;
import static com.linecorp.armeria.server.docs.FieldRequirement.OPTIONAL;
import static com.linecorp.armeria.server.docs.FieldRequirement.REQUIRED;
import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER;
import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson;
Expand Down Expand Up @@ -65,6 +66,7 @@
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.common.MediaTypeNames;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.UnmodifiableFuture;
import com.linecorp.armeria.internal.server.annotation.AnnotatedDocServicePluginTest.CompositeBean;
import com.linecorp.armeria.internal.testing.TestUtil;
Expand Down Expand Up @@ -336,15 +338,24 @@ private static void addJsonMethodInfo(Map<Class<?>, Set<MethodInfo>> methodInfos
final EndpointInfo endpoint1 = EndpointInfo.builder("*", "exact:/service/json")
.availableMimeTypes(MediaType.JSON_UTF_8)
.build();
final FieldInfo jsonRequest =
// request is moved to front even though it is the middle in the Java definition.
final List<FieldInfo> fields = ImmutableList.of(
FieldInfo.builder("request", TypeSignature.ofStruct(JsonRequest.class))
.requirement(REQUIRED)
.build();
.build(),
FieldInfo.builder("query1", TypeSignature.ofBase("string"))
.location(QUERY)
.requirement(OPTIONAL)
.build(),
FieldInfo.builder("query2", TypeSignature.ofBase("string"))
.location(QUERY)
.requirement(OPTIONAL)
.build());
final MethodInfo methodInfo1 = new MethodInfo(
MyService.class.getName(), "json", 0, STRING, ImmutableList.of(jsonRequest), ImmutableList.of(),
MyService.class.getName(), "json", 0, STRING, fields, ImmutableList.of(),
ImmutableList.of(endpoint1), HttpMethod.POST, DescriptionInfo.empty());
final MethodInfo methodInfo2 = new MethodInfo(
MyService.class.getName(), "json", 0, STRING, ImmutableList.of(jsonRequest), ImmutableList.of(),
MyService.class.getName(), "json", 0, STRING, fields, ImmutableList.of(),
ImmutableList.of(endpoint1), HttpMethod.PUT, DescriptionInfo.empty());
final Set<MethodInfo> methods = methodInfos.computeIfAbsent(MyService.class, unused -> new HashSet<>());
methods.add(methodInfo1);
Expand Down Expand Up @@ -642,7 +653,8 @@ public HttpResponse multi() {
@Path("/json")
@Post
@Put
public String json(JsonRequest request) {
public String json(
@Param @Nullable String query1, JsonRequest request, @Param @Nullable String query2) {
return request.bar;
}

Expand Down
22 changes: 11 additions & 11 deletions docs-client/src/containers/MethodPage/RequestBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ const RequestBody: React.FunctionComponent<Props> = ({
}) => {
const monacoEditor = useMonaco();

const supportsJsonSchema =
const forceJsonSchema =
serviceType === ServiceType.GRPC || serviceType === ServiceType.THRIFT;
useMemo(() => {
if (supportsJsonSchema) {
const schema = jsonSchemas.find((s: any) => s.$id === method.id) || {};

const hasJsonSchema = useMemo(() => {
const schema = jsonSchemas.find((s: any) => s.$id === method.id);
if (schema && (schema.useParamaterAsRoot || forceJsonSchema)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (schema && (schema.useParamaterAsRoot || forceJsonSchema)) {
if (schema && (schema.useParameterAsRoot || forceJsonSchema)) {

monacoEditor?.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
Expand All @@ -71,12 +70,13 @@ const RequestBody: React.FunctionComponent<Props> = ({
},
],
});
} else {
monacoEditor?.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: false,
});
return true;
}
}, [monacoEditor, jsonSchemas, method.id, supportsJsonSchema]);
monacoEditor?.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: false,
});
return false;
}, [monacoEditor, jsonSchemas, method.id, forceJsonSchema]);

return (
<>
Expand Down Expand Up @@ -109,7 +109,7 @@ const RequestBody: React.FunctionComponent<Props> = ({
<Typography variant="body2" paragraph />
<Editor
height="30vh"
language={supportsJsonSchema ? 'json' : undefined}
language={hasJsonSchema ? 'json' : undefined}
theme="vs-light"
options={{
minimap: { enabled: false },
Expand Down
Loading