diff --git a/package.json b/package.json index 4fc680a..c50378f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@stoplight/mosaic-code-viewer": "^1.32", "hash-color-material": "^1.1.3", "lodash": "^4.17.15", + "openapi-url-resolver": "^1.0.8", "qs": "^6.11.1", "react-markdown": "^8.0.7", "react-syntax-highlighter": "^15.4.3" diff --git a/src/InteractiveMethod/InteractiveMethod.tsx b/src/InteractiveMethod/InteractiveMethod.tsx index 9817779..851d486 100644 --- a/src/InteractiveMethod/InteractiveMethod.tsx +++ b/src/InteractiveMethod/InteractiveMethod.tsx @@ -1,36 +1,60 @@ -import React, {createRef, useEffect} from "react"; +import React, { createRef, useEffect, useState } from 'react'; import validator from '@rjsf/validator-ajv8'; -import { IconButtonProps,ArrayFieldTemplateProps, RJSFSchema, UiSchema, ArrayFieldTemplateItemType } from '@rjsf/utils'; -import { ContentDescriptorObject, ExampleObject, ExamplePairingObject, MethodObject} from '@open-rpc/meta-schema'; -import traverse from "@json-schema-tools/traverse"; +import { + IconButtonProps, + ArrayFieldTemplateProps, + RJSFSchema, + UiSchema, + ArrayFieldTemplateItemType, +} from '@rjsf/utils'; +import { + ContentDescriptorObject, + ExampleObject, + ExamplePairingObject, + MethodObject, + OpenrpcDocument, +} from '@open-rpc/meta-schema'; +import traverse from '@json-schema-tools/traverse'; import Form from '@rjsf/core'; import ArrayFieldTemplate from '../ArrayFieldTemplate/ArrayFieldTemplate'; import ArrayFieldItemTemplate from '../ArrayFieldItemTemplate/ArrayFieldItemTemplate'; -import FieldErrorTemplate from "../FieldErrorTemplate/FieldErrorTemplate"; -import FieldTemplate from "../FieldTemplate/FieldTemplate"; -import ObjectFieldTemplate from "../ObjectFieldTemplate/ObjectFieldTemplate"; +import FieldErrorTemplate from '../FieldErrorTemplate/FieldErrorTemplate'; +import FieldTemplate from '../FieldTemplate/FieldTemplate'; +import ObjectFieldTemplate from '../ObjectFieldTemplate/ObjectFieldTemplate'; const qs = require('qs'); +const openapiUrlResolver = require('openapi-url-resolver'); const { useHistory, useLocation } = require('@docusaurus/router'); const uiSchema: UiSchema = { 'ui:description': '', - "ui:submitButtonOptions": { - "norender": true, - } + 'ui:submitButtonOptions': { + norender: true, + }, }; interface Props { method: MethodObject; + openrpcDocument: OpenrpcDocument; selectedExamplePairing?: ExamplePairingObject; + requestTemplate: Record; components?: { - CodeBlock: React.FC<{children: string, className?: string}>; + CodeBlock: React.FC<{ + children: JSX.Element | JSX.Element[] | string | undefined; + className?: string; + }>; }; } function AddButton(props: IconButtonProps) { const { icon, iconType, ...btnProps } = props; return ( - + ); } @@ -40,10 +64,17 @@ function RemoveButton(props: IconButtonProps) { ...btnProps.style, minWidth: '35px', maxWidth: '36px', - border: undefined + border: undefined, }; return ( - + ); } function MoveUpButton(props: IconButtonProps) { @@ -54,7 +85,14 @@ function MoveUpButton(props: IconButtonProps) { maxWidth: '36px', }; return ( - + ); } @@ -66,7 +104,14 @@ function MoveDownButton(props: IconButtonProps) { maxWidth: '36px', }; return ( - + ); } @@ -77,13 +122,13 @@ interface ParamProps { formData: any; } const InteractiveMethodParam: React.FC = (props) => { - - const {param, refref} = props; - const [metamaskInstalled, setMetamaskInstalled] = React.useState(false); + const { param, refref } = props; + const [metamaskInstalled, setMetamaskInstalled] = + React.useState(false); const schema = traverse( param.schema, - (s ) => { + (s) => { if (typeof s === 'boolean') { return s; } @@ -91,7 +136,7 @@ const InteractiveMethodParam: React.FC = (props) => { s.summary = undefined; return s; }, - { mutable: false } + { mutable: false }, ); schema.title = undefined; useEffect(() => { @@ -112,14 +157,19 @@ const InteractiveMethodParam: React.FC = (props) => { ArrayFieldTemplate, FieldErrorTemplate, FieldTemplate, - ButtonTemplates:{ AddButton, RemoveButton, MoveUpButton, MoveDownButton }, - ObjectFieldTemplate + ButtonTemplates: { + AddButton, + RemoveButton, + MoveUpButton, + MoveDownButton, + }, + ObjectFieldTemplate, }} onChange={props.onChange} liveValidate={metamaskInstalled} /> ); -} +}; const InteractiveMethod: React.FC = (props) => { const history = useHistory(); @@ -129,9 +179,9 @@ const InteractiveMethod: React.FC = (props) => { value: string, defaultEncoder: any, _: string, - type: "key" | "value", + type: 'key' | 'value', ) => { - if (type === "key") { + if (type === 'key') { return defaultEncoder(value); } if (/^(\d+|\d*\.\d+)$/.test(value)) { @@ -153,32 +203,59 @@ const InteractiveMethod: React.FC = (props) => { return value; } }, - }) - const {method, components, selectedExamplePairing} = props; - const [requestParams, setRequestParams] = React.useState(queryString || {}); - const [executionResult, setExecutionResult] = React.useState(); - const [metamaskInstalled, setMetamaskInstalled] = React.useState(false); + }); + const { method, components, selectedExamplePairing } = props; + const [requestParams, setRequestParams] = useState(queryString || {}); + const [executionResult, setExecutionResult] = useState(); + const [metamaskInstalled, setMetamaskInstalled] = + React.useState(false); + const [selectedTab, setSelectedTab] = useState('js'); + const [selectedServer, setSelectedServer] = useState( + undefined, + ); const formRefs = method.params.map(() => createRef()); + props.openrpcDocument.servers = [ + { + name: 'foo', + url: 'https://foo.bar', + }, + { + name: 'bar', + url: 'https://bar.foo', + }, + ]; + const [servers, setServers] = useState( + openapiUrlResolver.resolve(props.openrpcDocument), + ); + const [newServer, setNewServer] = useState(''); + + const handleAddServer = () => { + setServers((val) => [...val, newServer]); + setSelectedServer(newServer); + setNewServer(''); + }; useEffect(() => { if (!selectedExamplePairing || Object.keys(queryString).length > 0) { return; } - const defaultFormData = selectedExamplePairing?.params.reduce((memo: any, exampleObject, i) => { - const ex = exampleObject as ExampleObject; - memo[(method.params[i] as ContentDescriptorObject).name] = ex.value; - return memo; - }, {}) + const defaultFormData = selectedExamplePairing?.params.reduce( + (memo: any, exampleObject, i) => { + const ex = exampleObject as ExampleObject; + memo[(method.params[i] as ContentDescriptorObject).name] = ex.value; + return memo; + }, + {}, + ); setRequestParams(defaultFormData); }, [selectedExamplePairing]); - const handleChange = (change: any, i: number) => { setRequestParams((val: any) => { const newVal = { ...val, [(method.params[i] as ContentDescriptorObject).name]: change.formData, - } + }; history.replace({ search: qs.stringify(newVal, { encode: false }), }); @@ -187,10 +264,32 @@ const InteractiveMethod: React.FC = (props) => { }; const methodCall = { + jsonrpc: '2.0', + id: 1, method: method.name, - params: method.paramStructure === "by-name" ? requestParams : method.params.map((p, i) => requestParams[(p as ContentDescriptorObject).name] || undefined), + params: + method.paramStructure === 'by-name' + ? requestParams + : method.params.map( + (p, i) => + requestParams[(p as ContentDescriptorObject).name] || undefined, + ), }; + const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; + + async function awaitFromString(handler: string) { + return new Promise((resolve, reject) => { + new AsyncFunction( + 'resolve', + 'reject', + `try { + const res = await ${handler}; + resolve(res); + } catch (e) { reject(e); }`, + )(resolve, reject); + }); + } const handleExec = async () => { // loop over refs @@ -199,84 +298,166 @@ const InteractiveMethod: React.FC = (props) => { }); try { - const response = await (window as any).ethereum.request(methodCall); + const req = processTemplateRequest(props.requestTemplate.js); + console.log('req', req); + const response = await awaitFromString(req); + console.log('response', response); setExecutionResult(response); } catch (e) { - setExecutionResult(e); + if ((e as unknown as any).code) { + setExecutionResult(e); + } else { + setExecutionResult((e as unknown as any).message); + } } }; - const jsCode = `await window.ethereum.request(${JSON.stringify(methodCall, null, " ")});`; - useEffect(() => { const installed = !!(window as any)?.ethereum; setMetamaskInstalled(installed); }, []); + const addIndent = (str: string, indentLevel: number, skipFirst = true) => { + return str + .split('\n') + .map((line, index: number) => { + if (index === 0 && skipFirst) { + return line; + } + return ' '.repeat(indentLevel) + line; + }) + .join('\n'); + }; + + const processTemplateRequest = (template: string | undefined) => { + if (template === undefined) { + return ''; + } + let returns = ''; + returns = template.replace( + '${jsonRpcRequest}', + addIndent(JSON.stringify(methodCall, null, 2), 4), + ); + returns = returns.replace('${serverUrl}', selectedServer || ''); + return returns; + }; + return (
- {!metamaskInstalled && + {servers && servers.length > 0 && (
-
- Install MetaMask for your platform and refresh the page. The interactive features in this documentation require installing MetaMask. +

Servers

+ setNewServer(e.target.value)} + placeholder="Add a new server" + /> + +
+
-
- } - {method.params.length > 0 && - <> + )} + {!metamaskInstalled && (
-

Params

-
- {method.params.map((p, i) => ( - <> -

{(p as ContentDescriptorObject).name}

- handleChange(change, i)} - param={p as ContentDescriptorObject} /> - - ))} +
+ Install MetaMask for your platform and refresh the page. The + interactive features in this documentation require installing{' '} + + MetaMask + + .
+
-
- - } + )} + {method.params.length > 0 && ( + <> +
+

Params

+
+ {method.params.map((p, i) => ( + <> +

{(p as ContentDescriptorObject).name}

+ handleChange(change, i)} + param={p as ContentDescriptorObject} + /> + + ))} +
+
+
+ + )}

Request

- {components && components.CodeBlock && {jsCode}} - {!components?.CodeBlock && -
-             
-               {jsCode}
-             
-           
- } + {Object.keys(props.requestTemplate).length !== 1 && ( +
    + {Object.keys(props.requestTemplate).map((key) => { + return ( +
  • setSelectedTab(key)} + > + {key} +
  • + ); + })} +
+ )} + <> + {components && components.CodeBlock && ( + + {processTemplateRequest(props.requestTemplate[selectedTab])} + + )} +
- {executionResult !== undefined &&
-

Response

-
- {components && components.CodeBlock && {JSON.stringify(executionResult, null, ' ')}} - {!components?.CodeBlock && -
-             
-               {JSON.stringify(executionResult, null, '  ')}
-             
-           
- } + {executionResult !== undefined && ( +
+

Response

+
+ {components && components.CodeBlock && ( + + {JSON.stringify(executionResult, null, ' ')} + + )} + {!components?.CodeBlock && ( +
+                {JSON.stringify(executionResult, null, '  ')}
+              
+ )} +
-
} + )}
-
); - -} +}; export default InteractiveMethod; diff --git a/yarn.lock b/yarn.lock index a52dd1f..2c98ca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1083,6 +1083,7 @@ __metadata: jest-transform-css: ^6.0.1 json-schema-ref-parser: ^7.0.1 lodash: ^4.17.15 + openapi-url-resolver: ^1.0.8 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.3.0 qs: ^6.11.1 @@ -7450,6 +7451,13 @@ __metadata: languageName: node linkType: hard +"openapi-url-resolver@npm:^1.0.8": + version: 1.0.8 + resolution: "openapi-url-resolver@npm:1.0.8" + checksum: 26ab7a6103f44b5c9435540e4dcd18e8103453ab597f05e38be897bf89953b817841416cf3e219fb4b4add14174550e07a20fbb7cb30cc972948fd210360944a + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3"