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

Allow a user choose a path when a Thrift/gRPC service has more than one path #3024

Merged
merged 11 commits into from Oct 1, 2020
Expand Up @@ -342,15 +342,16 @@ private ServiceInfo addServiceExamples(ServiceInfo service) {
// generated by the plugin.
concatAndDedup(exampleHeaders.get(m.name()), m.exampleHeaders()),
concatAndDedup(exampleRequests.get(m.name()), m.exampleRequests()),
examplePaths.get(m.name()),
exampleQueries.get(m.name()),
concatAndDedup(examplePaths.get(m.name()), m.examplePaths()),
concatAndDedup(exampleQueries.get(m.name()), m.exampleQueries()),
kojilin marked this conversation as resolved.
Show resolved Hide resolved
m.httpMethod(), m.docString()))::iterator,
Iterables.concat(service.exampleHeaders(), exampleHeaders.get("")),
service.docString());
}

private static <T> Iterable<T> concatAndDedup(Iterable<T> first, Iterable<T> second) {
return Stream.concat(Streams.stream(first), Streams.stream(second)).distinct()::iterator;
return Stream.concat(Streams.stream(first), Streams.stream(second)).distinct()
.collect(toImmutableList());
}

private DocServiceVfs vfs() {
Expand Down
Expand Up @@ -62,17 +62,6 @@ public final class MethodInfo {
@Nullable
private final String docString;

/**
* Creates a new instance.
*/
public MethodInfo(String name,
TypeSignature returnTypeSignature,
Iterable<FieldInfo> parameters,
Iterable<TypeSignature> exceptionTypeSignatures,
Iterable<EndpointInfo> endpoints) {
this(name, returnTypeSignature, parameters, exceptionTypeSignatures, endpoints, HttpMethod.POST, null);
}

/**
* Creates a new instance.
*/
Expand Down
178 changes: 91 additions & 87 deletions docs-client/src/containers/MethodPage/DebugPage.tsx
Expand Up @@ -28,6 +28,7 @@ import React, {
ChangeEvent,
useCallback,
useEffect,
useMemo,
useReducer,
useState,
} from 'react';
Expand Down Expand Up @@ -133,6 +134,15 @@ const DebugPage: React.FunctionComponent<Props> = ({
const [stickyHeaders, toggleStickyHeaders] = useReducer(toggle, false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [keepDebugResponse, toggleKeepDebugResponse] = useReducer(
toggle,
false,
);

const transport = TRANSPORTS.getDebugTransport(method);
if (!transport) {
throw new Error("This method doesn't have a debug transport.");
}

useEffect(() => {
const urlParams = new URLSearchParams(location.search);
Expand All @@ -145,13 +155,27 @@ const DebugPage: React.FunctionComponent<Props> = ({
}
}

const urlPath =
isAnnotatedService && exactPathMapping
? method.endpoints[0].pathMapping.substring('exact:'.length)
: urlParams.get('endpoint_path') || '';
let urlPath;
if (isAnnotatedService) {
if (exactPathMapping) {
urlPath = method.endpoints[0].pathMapping.substring('exact:'.length);
} else {
urlPath = urlParams.get('endpoint_path') || '';
}
} else {
urlPath =
transport.findDebugMimeTypeEndpoint(
method,
urlParams.get('endpoint_path') || undefined,
)?.pathMapping || '';
}

const urlQueries = isAnnotatedService ? urlParams.get('queries') : '';

setDebugResponse('');
if (!keepDebugResponse) {
setDebugResponse('');
toggleKeepDebugResponse(false);
}
setSnackbarOpen(false);
setRequestBody(urlRequestBody || method.exampleRequests[0] || '');
setAdditionalPath(urlPath || '');
Expand All @@ -162,9 +186,10 @@ const DebugPage: React.FunctionComponent<Props> = ({
isAnnotatedService,
location.search,
match.params,
method.endpoints,
method.exampleRequests,
method,
transport,
useRequestBody,
keepDebugResponse,
]);

/* eslint-disable react-hooks/exhaustive-deps */
Expand Down Expand Up @@ -195,55 +220,6 @@ const DebugPage: React.FunctionComponent<Props> = ({
setSnackbarOpen(false);
}, []);

const validateEndpointPath = useCallback(
(newEndpointPath: string) => {
if (!newEndpointPath) {
throw new Error('You must specify the endpoint path.');
}
const endpoint = method.endpoints[0];
const regexPathPrefix = endpoint.regexPathPrefix;
const originalPath = endpoint.pathMapping;

if (originalPath.startsWith('prefix:')) {
// Prefix path mapping.
const prefix = originalPath.substring('prefix:'.length);
if (!newEndpointPath.startsWith(prefix)) {
throw new Error(
`The path: '${newEndpointPath}' should start with the prefix: ${prefix}`,
);
}
}

if (originalPath.startsWith('regex:')) {
let regexPart;
if (regexPathPrefix) {
// Prefix adding path mapping.
const prefix = regexPathPrefix.substring('prefix:'.length);
if (!newEndpointPath.startsWith(prefix)) {
throw new Error(
`The path: '${newEndpointPath}' should start with the prefix: ${prefix}`,
);
}

// Remove the prefix from the endpointPath so that we can test the regex.
regexPart = newEndpointPath.substring(prefix.length - 1);
} else {
regexPart = newEndpointPath;
}
const regExp = new RegExp(originalPath.substring('regex:'.length));
if (!regExp.test(regexPart)) {
const expectedPath = regexPathPrefix
? `${regexPathPrefix} ${originalPath}`
: originalPath;
throw new Error(
`Endpoint path: ${newEndpointPath} (expected: ${expectedPath})`,
);
}
}
},
[method],
);

const onSelectedQueriesChange = useCallback((selectedQueries: Option) => {
setAdditionalQueries(selectedQueries.value);
}, []);
Expand Down Expand Up @@ -297,37 +273,39 @@ const DebugPage: React.FunctionComponent<Props> = ({
`${window.location.protocol}//${window.location.hostname}` +
`${window.location.port ? `:${window.location.port}` : ''}`;

const transport = TRANSPORTS.getDebugTransport(method);
if (!transport) {
throw new Error("This method doesn't have a debug transport.");
}

const httpMethod = method.httpMethod;
const endpoint = transport.findDebugMimeTypeEndpoint(method);
const path = endpoint.pathMapping;
const body = transport.getCurlBody(
endpoint,
method,
escapeSingleQuote(requestBody),
);
let uri;
let endpoint;

if (isAnnotatedService) {
const queries = additionalQueries;
if (exactPathMapping) {
endpoint = transport.getDebugMimeTypeEndpoint(method);
uri =
`'${host}${escapeSingleQuote(path.substring('exact:'.length))}` +
`'${host}${escapeSingleQuote(
endpoint.pathMapping.substring('exact:'.length),
)}` +
`${queries.length > 0 ? `?${escapeSingleQuote(queries)}` : ''}'`;
} else {
validateEndpointPath(additionalPath);
endpoint = transport.getDebugMimeTypeEndpoint(method, additionalPath);
uri =
`'${host}${escapeSingleQuote(additionalPath)}'` +
`${queries.length > 0 ? `?${escapeSingleQuote(queries)}` : ''}'`;
}
} else if (additionalPath.length > 0) {
endpoint = transport.getDebugMimeTypeEndpoint(method, additionalPath);
uri = `'${host}${escapeSingleQuote(additionalPath)}'`;
} else {
uri = `'${host}${escapeSingleQuote(path)}'`;
endpoint = transport.getDebugMimeTypeEndpoint(method);
uri = `'${host}${escapeSingleQuote(endpoint.pathMapping)}'`;
}

const body = transport.getCurlBody(
endpoint,
method,
escapeSingleQuote(requestBody),
);

headers['content-type'] = transport.getDebugMimeType();
if (process.env.WEBPACK_DEV === 'true') {
headers[docServiceDebug] = 'true';
Expand All @@ -352,12 +330,12 @@ const DebugPage: React.FunctionComponent<Props> = ({
useRequestBody,
additionalHeaders,
method,
transport,
requestBody,
isAnnotatedService,
showSnackbar,
additionalQueries,
exactPathMapping,
validateEndpointPath,
additionalPath,
]);

Expand Down Expand Up @@ -390,12 +368,13 @@ const DebugPage: React.FunctionComponent<Props> = ({
if (!exactPathMapping) {
executedEndpointPath = params.get('endpoint_path') || undefined;
}
} else {
executedEndpointPath = params.get('endpoint_path') || undefined;
}

const headersText = params.get('headers');
const headers = headersText ? JSON.parse(headersText) : {};

const transport = TRANSPORTS.getDebugTransport(method)!;
let executedDebugResponse;
try {
executedDebugResponse = await transport.send(
Expand All @@ -410,10 +389,10 @@ const DebugPage: React.FunctionComponent<Props> = ({
}
setDebugResponse(executedDebugResponse);
},
[useRequestBody, isAnnotatedService, exactPathMapping, method],
[useRequestBody, isAnnotatedService, exactPathMapping, method, transport],
);

const onSubmit = useCallback(() => {
const onSubmit = useCallback(async () => {
setDebugResponse('');

const queries = additionalQueries;
Expand All @@ -439,9 +418,14 @@ const DebugPage: React.FunctionComponent<Props> = ({
params.set('queries', queries);
}
if (!exactPathMapping) {
validateEndpointPath(additionalPath);
transport.getDebugMimeTypeEndpoint(method, additionalPath);
params.set('endpoint_path', additionalPath);
}
} else if (additionalPath.length > 0) {
params.set('endpoint_path', additionalPath);
} else {
// Fall back to default endpoint.
params.delete('endpoint_path');
}

if (headers) {
Expand All @@ -467,9 +451,11 @@ const DebugPage: React.FunctionComponent<Props> = ({

const serializedParams = `?${params.toString()}`;
if (serializedParams !== location.search) {
// executeRequest may throw error before useEffect, we need to avoid useEffect cleanup the debug response.
toggleKeepDebugResponse(true);
history.push(`${location.pathname}${serializedParams}`);
}
executeRequest(params);
await executeRequest(params);
}, [
additionalQueries,
additionalHeaders,
Expand All @@ -481,11 +467,24 @@ const DebugPage: React.FunctionComponent<Props> = ({
isAnnotatedService,
requestBody,
exactPathMapping,
validateEndpointPath,
additionalPath,
history,
method,
transport,
]);

const supportedExamplePaths = useMemo(() => {
if (isAnnotatedService) {
return examplePaths;
}
return transport.listDebugMimeTypeEndpoint(method).map((endpoint) => {
return {
label: endpoint.pathMapping,
value: endpoint.pathMapping,
};
});
}, [examplePaths, method, isAnnotatedService, transport]);

return (
<Section>
<div id="debug-form">
Expand All @@ -507,22 +506,27 @@ const DebugPage: React.FunctionComponent<Props> = ({
.
</Alert>
<EndpointPath
examplePaths={examplePaths}
examplePaths={supportedExamplePaths}
editable={!exactPathMapping}
isAnnotatedService={isAnnotatedService}
endpointPathOpen={endpointPathOpen}
additionalPath={additionalPath}
onEditEndpointPathClick={toggleEndpointPathOpen}
onPathFormChange={onPathFormChange}
onSelectedPathChange={onSelectedPathChange}
/>
<HttpQueryString
exampleQueries={exampleQueries}
additionalQueriesOpen={additionalQueriesOpen}
additionalQueries={additionalQueries}
onEditHttpQueriesClick={toggleAdditionalQueriesOpen}
onQueriesFormChange={onQueriesFormChange}
onSelectedQueriesChange={onSelectedQueriesChange}
/>
{isAnnotatedService && (
<>
<HttpQueryString
exampleQueries={exampleQueries}
additionalQueriesOpen={additionalQueriesOpen}
additionalQueries={additionalQueries}
onEditHttpQueriesClick={toggleAdditionalQueriesOpen}
onQueriesFormChange={onQueriesFormChange}
onSelectedQueriesChange={onSelectedQueriesChange}
/>
</>
)}
<HttpHeaders
exampleHeaders={exampleHeaders}
additionalHeadersOpen={additionalHeadersOpen}
Expand Down