From b40455158a7b961fe0c96603dd206ffd1debe53f Mon Sep 17 00:00:00 2001 From: Jonathan Bailey Date: Wed, 13 Nov 2019 13:45:40 -0500 Subject: [PATCH] Add callback support --- src/components/Callbacks/Callback.tsx | 26 +++++++++ src/components/Callbacks/CallbackTitle.tsx | 22 ++++++++ src/components/Callbacks/CallbacksList.tsx | 35 +++++++++++++ src/components/Callbacks/styled.elements.ts | 15 ++++++ src/components/ContentItems/ContentItems.tsx | 29 +++++++++-- src/services/models/Callback.ts | 55 ++++++++++++++++++++ src/services/models/Operation.ts | 55 ++++++++++++++++++-- src/services/models/index.ts | 1 + 8 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 src/components/Callbacks/Callback.tsx create mode 100644 src/components/Callbacks/CallbackTitle.tsx create mode 100644 src/components/Callbacks/CallbacksList.tsx create mode 100644 src/components/Callbacks/styled.elements.ts create mode 100644 src/services/models/Callback.ts diff --git a/src/components/Callbacks/Callback.tsx b/src/components/Callbacks/Callback.tsx new file mode 100644 index 00000000..ffec7f6c --- /dev/null +++ b/src/components/Callbacks/Callback.tsx @@ -0,0 +1,26 @@ +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { CallbackModel } from '../../services/models'; +import { CallbackDetailsWrap, StyledCallbackTitle } from '../Callbacks/styled.elements'; + +@observer +export class CallbackView extends React.Component<{ callback: CallbackModel }> { + toggle = () => { + this.props.callback.toggle(); + }; + + render() { + const { name, expanded } = this.props.callback; + + return ( +
+ + {expanded && ( + + {name} + + )} +
+ ); + } +} diff --git a/src/components/Callbacks/CallbackTitle.tsx b/src/components/Callbacks/CallbackTitle.tsx new file mode 100644 index 00000000..f594238e --- /dev/null +++ b/src/components/Callbacks/CallbackTitle.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; + +import { ShelfIcon } from '../../common-elements'; + +export interface CallbackTitleProps { + name: string; + opened?: boolean; + className?: string; + onClick?: () => void; +} + +export class CallbackTitle extends React.PureComponent { + render() { + const { name, opened, className, onClick } = this.props; + return ( +
+ + {name} +
+ ); + } +} diff --git a/src/components/Callbacks/CallbacksList.tsx b/src/components/Callbacks/CallbacksList.tsx new file mode 100644 index 00000000..826cdb2a --- /dev/null +++ b/src/components/Callbacks/CallbacksList.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; +import { CallbackModel } from '../../services/models'; +import styled from '../../styled-components'; +import { CallbackView } from './Callback'; + +const CallbacksHeader = styled.h3` + font-size: 18px; + padding: 0.2em 0; + margin: 3em 0 1.1em; + color: #253137; + font-weight: normal; +`; + +export interface CallbacksListProps { + callbacks: CallbackModel[]; +} + +export class CallbacksList extends React.PureComponent { + render() { + const { callbacks } = this.props; + + if (!callbacks || callbacks.length === 0) { + return null; + } + + return ( +
+ Callbacks + {callbacks.map(callback => { + return ; + })} +
+ ); + } +} diff --git a/src/components/Callbacks/styled.elements.ts b/src/components/Callbacks/styled.elements.ts new file mode 100644 index 00000000..c5abc8ef --- /dev/null +++ b/src/components/Callbacks/styled.elements.ts @@ -0,0 +1,15 @@ +import styled from '../../styled-components'; +import { CallbackTitle } from './CallbackTitle'; + +export const StyledCallbackTitle = styled(CallbackTitle)` + padding: 10px; + border-radius: 2px; + margin-bottom: 4px; + line-height: 1.5em; + background-color: #f2f2f2; + cursor: pointer; +`; + +export const CallbackDetailsWrap = styled.div` + padding: 10px; +`; diff --git a/src/components/ContentItems/ContentItems.tsx b/src/components/ContentItems/ContentItems.tsx index 7545243f..2c34ff70 100644 --- a/src/components/ContentItems/ContentItems.tsx +++ b/src/components/ContentItems/ContentItems.tsx @@ -4,10 +4,19 @@ import * as React from 'react'; import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation'; import { AdvancedMarkdown } from '../Markdown/AdvancedMarkdown'; +import { Operation } from '..'; import { H1, H2, MiddlePanel, Row, Section, ShareLink } from '../../common-elements'; -import { ContentItemModel } from '../../services/MenuBuilder'; +import { ContentItemModel } from '../../services'; import { GroupModel, OperationModel } from '../../services/models'; -import { Operation } from '../Operation/Operation'; +import styled from '../../styled-components'; + +const CallbacksHeader = styled.h3` + font-size: 18px; + padding: 0 40px; + margin: 3em 0 1.1em; + color: #253137; + font-weight: normal; +`; @observer export class ContentItems extends React.Component<{ @@ -18,7 +27,21 @@ export class ContentItems extends React.Component<{ if (items.length === 0) { return null; } - return items.map(item => ); + return items.map(item => { + if (item.type === 'operation' && item.callbacks.length > 0) { + return ( + + + Callbacks: + {item.callbacks.map((callbackIndex, idx) => { + return ; + })} + + ); + } + + return ; + }); } } diff --git a/src/services/models/Callback.ts b/src/services/models/Callback.ts new file mode 100644 index 00000000..502c6e64 --- /dev/null +++ b/src/services/models/Callback.ts @@ -0,0 +1,55 @@ +import { action, observable } from 'mobx'; +import { OpenAPICallback, Referenced } from '../../types'; +import { isOperationName } from '../../utils'; +import { OpenAPIParser } from '../OpenAPIParser'; +import { OperationModel } from './Operation'; + +export class CallbackModel { + @observable + expanded: boolean; + name: string; + paths: Referenced; + operations: OperationModel[] = []; + + constructor( + parser: OpenAPIParser, + name: string, + infoOrRef: Referenced, + options, + ) { + this.name = name; + this.paths = parser.deref(infoOrRef); + parser.exitRef(infoOrRef); + + for (const pathName of Object.keys(this.paths)) { + const path = this.paths[pathName]; + const operations = Object.keys(path).filter(isOperationName); + for (const operationName of operations) { + const operationInfo = path[operationName]; + + const operation = new OperationModel( + parser, + { + ...operationInfo, + pathName, + httpVerb: operationName, + pathParameters: path.parameters || [], + }, + undefined, + options, + true, + this.name, + ); + + this.operations.push(operation); + } + } + + console.log(this.operations); + } + + @action + toggle() { + this.expanded = !this.expanded; + } +} diff --git a/src/services/models/Operation.ts b/src/services/models/Operation.ts index 1aa3a65d..058b6196 100644 --- a/src/services/models/Operation.ts +++ b/src/services/models/Operation.ts @@ -26,6 +26,7 @@ import { import { ContentItemModel, ExtendedOpenAPIOperation } from '../MenuBuilder'; import { OpenAPIParser } from '../OpenAPIParser'; import { RedocNormalizedOptions } from '../RedocNormalizedOptions'; +import { CallbackModel } from './Callback'; import { FieldModel } from './Field'; import { MediaContentModel } from './MediaContent'; import { RequestBodyModel } from './RequestBody'; @@ -77,12 +78,15 @@ export class OperationModel implements IMenuItem { servers: OpenAPIServer[]; security: SecurityRequirementModel[]; extensions: Dict; + isCallback: boolean; constructor( private parser: OpenAPIParser, private operationSpec: ExtendedOpenAPIOperation, parent: GroupModel | undefined, private options: RedocNormalizedOptions, + isCallback: boolean = false, + callbackEventName: string | undefined = undefined, ) { this.pointer = JsonPointer.compile(['paths', operationSpec.pathName, operationSpec.httpVerb]); @@ -93,7 +97,6 @@ export class OperationModel implements IMenuItem { ? parent.id + this.pointer : this.pointer; - this.name = getOperationSummary(operationSpec); this.description = operationSpec.description; this.parent = parent; this.externalDocs = operationSpec.externalDocs; @@ -103,20 +106,50 @@ export class OperationModel implements IMenuItem { this.deprecated = !!operationSpec.deprecated; this.operationId = operationSpec.operationId; this.path = operationSpec.pathName; + this.isCallback = isCallback; + this.codeSamples = operationSpec['x-code-samples'] || []; + + if (this.isCallback) { + // NOTE: Use callback's event name as the view label, not the operationID. + this.name = callbackEventName || getOperationSummary(operationSpec); + // NOTE: Callbacks by default should not inherit the specification's global `security` definition. + // Can be defined individually per-callback in the specification. Defaults to none. + this.security = (operationSpec.security || []).map( + security => new SecurityRequirementModel(security, parser), + ); + } else { + this.name = getOperationSummary(operationSpec); + this.security = (operationSpec.security || parser.spec.security || []).map( + security => new SecurityRequirementModel(security, parser), + ); + + const requestBodyContent = this.requestBody && this.requestBody.content; + if (requestBodyContent && requestBodyContent.hasSample) { + const insertInx = Math.min(this.codeSamples.length, options.payloadSampleIdx); + + this.codeSamples = [ + ...this.codeSamples.slice(0, insertInx), + { + lang: 'payload', + label: 'Payload', + source: '', + requestBodyContent, + }, + ...this.codeSamples.slice(insertInx), + ]; + } const pathInfo = parser.byRef( JsonPointer.compile(['paths', operationSpec.pathName]), ); + // NOTE: Callbacks by default will inherit the specification's global `servers` definition. + // In many cases, this may be undesirable. Override individually in the specification to remedy this. this.servers = normalizeServers( parser.specUrl, operationSpec.servers || (pathInfo && pathInfo.servers) || parser.spec.servers || [], ); - this.security = (operationSpec.security || parser.spec.security || []).map( - security => new SecurityRequirementModel(security, parser), - ); - if (options.showExtensions) { this.extensions = extractExtensions(operationSpec, options.showExtensions); } @@ -224,4 +257,16 @@ export class OperationModel implements IMenuItem { ); }); } + + @memoize + get callbacks() { + return Object.keys(this.operationSpec.callbacks || []).map(callbackEventName => { + return new CallbackModel( + this.parser, + callbackEventName, + this.operationSpec.callbacks![callbackEventName], + this.options, + ); + }); + } } diff --git a/src/services/models/index.ts b/src/services/models/index.ts index 65006e79..b373b392 100644 --- a/src/services/models/index.ts +++ b/src/services/models/index.ts @@ -1,3 +1,4 @@ +export * from './Callback'; export * from '../SpecStore'; export * from './Group.model'; export * from './Operation';