From 5b760ca3bc05d517a8e5c9fbd4eb22a07e773d50 Mon Sep 17 00:00:00 2001 From: kojilin Date: Thu, 3 Sep 2020 21:14:14 +0900 Subject: [PATCH] Separate endpoint page, only dropdown for rpc. Disable query for rpc. --- .../src/containers/MethodPage/DebugPage.tsx | 152 +++++++----------- .../containers/MethodPage/EndpointPath.tsx | 84 ++++++---- .../src/lib/transports/annotated-http.ts | 56 ++++++- .../src/lib/transports/grpc-unframed.ts | 2 +- docs-client/src/lib/transports/thrift.ts | 2 +- docs-client/src/lib/transports/transport.ts | 57 ++++++- examples/thrift/src/main/thrift/hello.thrift | 12 +- 7 files changed, 223 insertions(+), 142 deletions(-) diff --git a/docs-client/src/containers/MethodPage/DebugPage.tsx b/docs-client/src/containers/MethodPage/DebugPage.tsx index 7b61df0f6bf..4d9a1e981b3 100644 --- a/docs-client/src/containers/MethodPage/DebugPage.tsx +++ b/docs-client/src/containers/MethodPage/DebugPage.tsx @@ -134,6 +134,15 @@ const DebugPage: React.FunctionComponent = ({ 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); @@ -146,13 +155,27 @@ const DebugPage: React.FunctionComponent = ({ } } - 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'), + )?.pathMapping || ''; + } + const urlQueries = isAnnotatedService ? urlParams.get('queries') : ''; - setDebugResponse(''); + if (!keepDebugResponse) { + setDebugResponse(''); + toggleKeepDebugResponse(false); + } setSnackbarOpen(false); setRequestBody(urlRequestBody || method.exampleRequests[0] || ''); setAdditionalPath(urlPath || ''); @@ -163,9 +186,10 @@ const DebugPage: React.FunctionComponent = ({ isAnnotatedService, location.search, match.params, - method.endpoints, - method.exampleRequests, + method, + transport, useRequestBody, + keepDebugResponse, ]); /* eslint-disable react-hooks/exhaustive-deps */ @@ -196,55 +220,6 @@ const DebugPage: React.FunctionComponent = ({ 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); }, []); @@ -298,35 +273,30 @@ const DebugPage: React.FunctionComponent = ({ `${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; let uri; let endpoint; if (isAnnotatedService) { - endpoint = transport.findDebugMimeTypeEndpoint(method); const queries = additionalQueries; if (exactPathMapping) { + endpoint = transport.getDebugMimeTypeEndpoint(method); uri = `'${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.findDebugMimeTypeEndpoint(method, additionalPath); + endpoint = transport.getDebugMimeTypeEndpoint(method, additionalPath); uri = `'${host}${escapeSingleQuote(additionalPath)}'`; } else { - endpoint = transport.findDebugMimeTypeEndpoint(method); + endpoint = transport.getDebugMimeTypeEndpoint(method); uri = `'${host}${escapeSingleQuote(endpoint.pathMapping)}'`; } @@ -360,12 +330,12 @@ const DebugPage: React.FunctionComponent = ({ useRequestBody, additionalHeaders, method, + transport, requestBody, isAnnotatedService, showSnackbar, additionalQueries, exactPathMapping, - validateEndpointPath, additionalPath, ]); @@ -405,7 +375,6 @@ const DebugPage: React.FunctionComponent = ({ const headersText = params.get('headers'); const headers = headersText ? JSON.parse(headersText) : {}; - const transport = TRANSPORTS.getDebugTransport(method)!; let executedDebugResponse; try { executedDebugResponse = await transport.send( @@ -420,10 +389,10 @@ const DebugPage: React.FunctionComponent = ({ } setDebugResponse(executedDebugResponse); }, - [useRequestBody, isAnnotatedService, exactPathMapping, method], + [useRequestBody, isAnnotatedService, exactPathMapping, method, transport], ); - const onSubmit = useCallback(() => { + const onSubmit = useCallback(async () => { setDebugResponse(''); const queries = additionalQueries; @@ -449,7 +418,7 @@ const DebugPage: React.FunctionComponent = ({ params.set('queries', queries); } if (!exactPathMapping) { - validateEndpointPath(additionalPath); + transport.getDebugMimeTypeEndpoint(method, additionalPath); params.set('endpoint_path', additionalPath); } } else if (additionalPath.length > 0) { @@ -482,9 +451,11 @@ const DebugPage: React.FunctionComponent = ({ 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, @@ -496,30 +467,20 @@ const DebugPage: React.FunctionComponent = ({ isAnnotatedService, requestBody, exactPathMapping, - validateEndpointPath, additionalPath, history, + method, + transport, ]); const supportedExamplePaths = useMemo(() => { if (isAnnotatedService) { return examplePaths; } - const transport = TRANSPORTS.getDebugTransport(method); - if (!transport) { - throw new Error("This method doesn't have a debug transport."); - } return examplePaths.filter((path) => - method.endpoints.some((endpoint) => { - return ( - endpoint.pathMapping === path.value && - endpoint.availableMimeTypes.some((mimeType) => { - return transport.supportsMimeType(mimeType); - }) - ); - }), + transport.findDebugMimeTypeEndpoint(method, path.value), ); - }, [examplePaths, method, isAnnotatedService]); + }, [examplePaths, method, isAnnotatedService, transport]); return (
@@ -544,20 +505,25 @@ const DebugPage: React.FunctionComponent = ({ - + {isAnnotatedService && ( + <> + + + )} ) => void; } -const EndpointPath: React.FunctionComponent = (props) => ( - <> - - - - {props.endpointPathOpen && ( - <> - {props.examplePaths.length > 0 && ( - <> - - - - )} - - - - )} - -); +const EndpointPath: React.FunctionComponent = (props) => { + return ( + <> + + + + {props.endpointPathOpen && ( + <> + {props.isAnnotatedService ? ( + <> + {props.examplePaths.length > 0 && ( + <> + + + + )} + + + + ) : ( + <> + + + + )} + + )} + + ); +}; export default React.memo(EndpointPath); diff --git a/docs-client/src/lib/transports/annotated-http.ts b/docs-client/src/lib/transports/annotated-http.ts index 349617be433..7578d1f1841 100644 --- a/docs-client/src/lib/transports/annotated-http.ts +++ b/docs-client/src/lib/transports/annotated-http.ts @@ -14,7 +14,7 @@ * under the License. */ -import { Method } from '../specification'; +import { Endpoint, Method } from '../specification'; import prettify from '../json-prettify'; import Transport from './transport'; @@ -30,6 +30,58 @@ export default class AnnotatedHttpTransport extends Transport { return ANNOTATED_HTTP_MIME_TYPE; } + protected validatePath(endpoint: Endpoint, path: string): { error?: string } { + const regexPathPrefix = endpoint.regexPathPrefix; + const originalPath = endpoint.pathMapping; + + if (originalPath.startsWith(`exact:`)) { + const exact = originalPath.substring('exact:'.length); + if (path !== exact) { + return { + error: `The path: '${path}' must be equal to: ${exact}`, + }; + } + } + + if (originalPath.startsWith('prefix:')) { + // Prefix path mapping. + const prefix = originalPath.substring('prefix:'.length); + if (!path.startsWith(prefix)) { + return { + error: `The path: '${path}' 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 (!path.startsWith(prefix)) { + return { + error: `The path: '${path}' should start with the prefix: ${prefix}`, + }; + } + + // Remove the prefix from the endpointPath so that we can test the regex. + regexPart = path.substring(prefix.length - 1); + } else { + regexPart = path; + } + const regExp = new RegExp(originalPath.substring('regex:'.length)); + if (!regExp.test(regexPart)) { + const expectedPath = regexPathPrefix + ? `${regexPathPrefix} ${originalPath}` + : originalPath; + return { + error: `Endpoint path: ${path} (expected: ${expectedPath})`, + }; + } + } + return {}; + } + protected async doSend( method: Method, headers: { [name: string]: string }, @@ -37,7 +89,7 @@ export default class AnnotatedHttpTransport extends Transport { endpointPath?: string, queries?: string, ): Promise { - const endpoint = this.findDebugMimeTypeEndpoint(method); + const endpoint = this.getDebugMimeTypeEndpoint(method); const hdrs = new Headers(); hdrs.set('content-type', ANNOTATED_HTTP_MIME_TYPE); diff --git a/docs-client/src/lib/transports/grpc-unframed.ts b/docs-client/src/lib/transports/grpc-unframed.ts index 8cc46f349b8..2678853d2d9 100644 --- a/docs-client/src/lib/transports/grpc-unframed.ts +++ b/docs-client/src/lib/transports/grpc-unframed.ts @@ -40,7 +40,7 @@ export default class GrpcUnframedTransport extends Transport { if (!bodyJson) { throw new Error('A gRPC request must have body.'); } - const endpoint = this.findDebugMimeTypeEndpoint(method, endpointPath); + const endpoint = this.getDebugMimeTypeEndpoint(method, endpointPath); const hdrs = new Headers(); hdrs.set('content-type', GRPC_UNFRAMED_MIME_TYPE); diff --git a/docs-client/src/lib/transports/thrift.ts b/docs-client/src/lib/transports/thrift.ts index fd130741672..faa66026ee2 100644 --- a/docs-client/src/lib/transports/thrift.ts +++ b/docs-client/src/lib/transports/thrift.ts @@ -49,7 +49,7 @@ export default class ThriftTransport extends Transport { if (!bodyJson) { throw new Error('A Thrift request must have body.'); } - const endpoint = this.findDebugMimeTypeEndpoint(method, endpointPath); + const endpoint = this.getDebugMimeTypeEndpoint(method, endpointPath); const thriftMethod = ThriftTransport.thriftMethod(endpoint, method); diff --git a/docs-client/src/lib/transports/transport.ts b/docs-client/src/lib/transports/transport.ts index 6515f10aa7b..bcd67db5031 100644 --- a/docs-client/src/lib/transports/transport.ts +++ b/docs-client/src/lib/transports/transport.ts @@ -52,15 +52,43 @@ export default abstract class Transport { } public findDebugMimeTypeEndpoint( + method: Method, + endpointPath?: string | null, + ): Endpoint | undefined { + return method.endpoints.find((ep) => { + return ( + ep.availableMimeTypes.includes(this.getDebugMimeType()) && + (!endpointPath || !this.validatePath(ep, endpointPath).error) + ); + }); + } + + public getDebugMimeTypeEndpoint( method: Method, endpointPath?: string, ): Endpoint { - const endpoint = method.endpoints.find( - (ep) => - ep.availableMimeTypes.includes(this.getDebugMimeType()) && - (endpointPath === undefined || endpointPath === ep.pathMapping), - ); + // Provide better error message to the UI if there is only one endpoint. + const targetEndpoints = this.listDebugMimeTypeEndpoint(method); + if (targetEndpoints.length === 1) { + if (endpointPath) { + const errorMsg = this.validatePath(targetEndpoints[0], endpointPath) + .error; + if (errorMsg) { + throw new Error(errorMsg); + } + } + return targetEndpoints[0]; + } + + // General error message if not found. + const endpoint = this.findDebugMimeTypeEndpoint(method, endpointPath); if (!endpoint) { + if (endpointPath) { + throw new Error( + `Endpoint does not support debug transport. MimeType: ${this.getDebugMimeType()}, Supported paths: + ${targetEndpoints.map((ep) => ep.pathMapping).join()}`, + ); + } throw new Error( `Endpoint does not support debug transport. MimeType: ${this.getDebugMimeType()}`, ); @@ -76,6 +104,19 @@ export default abstract class Transport { return body; } + /** + * Checking if the endpoint's path supports target path. + * Default implementation is suitable for RPC, using endpoint.pathMapping === path. + */ + protected validatePath(endpoint: Endpoint, path: string): { error?: string } { + if (endpoint.pathMapping !== path) { + return { + error: `The path: '${path}' must be equal to ${endpoint.pathMapping}`, + }; + } + return {}; + } + protected abstract doSend( method: Method, headers: { [name: string]: string }, @@ -83,4 +124,10 @@ export default abstract class Transport { endpointPath?: string, queries?: string, ): Promise; + + private listDebugMimeTypeEndpoint(method: Method): Endpoint[] { + return method.endpoints.filter((endpoint) => + endpoint.availableMimeTypes.includes(this.getDebugMimeType()), + ); + } } diff --git a/examples/thrift/src/main/thrift/hello.thrift b/examples/thrift/src/main/thrift/hello.thrift index 64d95af1ded..5093632f603 100644 --- a/examples/thrift/src/main/thrift/hello.thrift +++ b/examples/thrift/src/main/thrift/hello.thrift @@ -1,11 +1,5 @@ namespace java example.armeria.thrift -service HelloService { - HelloReply hello(1:HelloRequest request) - HelloReply lazyHello (1:HelloRequest request) - HelloReply blockingHello (1:HelloRequest request) -} - struct HelloRequest { 1: required string name; } @@ -13,3 +7,9 @@ struct HelloRequest { struct HelloReply { 1: required string message; } + +service HelloService { + HelloReply hello(1:HelloRequest request) + HelloReply lazyHello (1:HelloRequest request) + HelloReply blockingHello (1:HelloRequest request) +}