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';