From b052d55486b4d95154fe4c4c612907f6e30c07c9 Mon Sep 17 00:00:00 2001 From: Brendan Abbott Date: Fri, 14 Jul 2017 13:32:36 +1000 Subject: [PATCH] Support externalDocs. Closes #112 --- docs/open-api-v3-support.md | 12 ++++---- .../BodyContent/BodyContent.styles.js | 12 ++------ src/components/BodySchema/BodySchema.js | 1 + .../ContentContainer.styles.js | 10 ++----- .../Description/Description.styles.js | 10 +++---- src/components/Docs/Docs.js | 27 +++++++++++++++++ src/components/Docs/Docs.styles.js | 7 +++++ src/components/Header/Header.js | 9 +++++- src/components/Header/Header.styles.js | 2 +- src/components/Method/Method.styles.js | 2 +- src/components/NavigationTag/NavigationTag.js | 7 +++-- src/components/Page/Page.js | 29 +++++++++++++++++-- src/components/Property/Property.js | 8 +++-- .../SecurityContainer.styles.js | 8 +---- .../ServiceContainer/ServiceContainer.js | 5 +++- src/parser/open-api/schemaParser.js | 4 +++ src/parser/open-api/v3/navigationParser.js | 4 +++ src/parser/open-api/v3/open-api-v3-parser.js | 23 ++++++++++----- .../v3/data/inputs/petstore.false.json | 9 +++++- .../v3/data/outputs/petstore.false.json | 27 +++++++++++++++-- 20 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 src/components/Docs/Docs.js create mode 100644 src/components/Docs/Docs.styles.js diff --git a/docs/open-api-v3-support.md b/docs/open-api-v3-support.md index 0410966..955a7e5 100644 --- a/docs/open-api-v3-support.md +++ b/docs/open-api-v3-support.md @@ -31,7 +31,7 @@ This document outlines this project's support for visualising the [Open API V3][ - [ ] [components](#components-object) - [x] [security](#security-requirement-object) - [x] [tags](#tag-object) -- [ ] [externalDocs](#external-documentation-object) +- [x] [externalDocs](#external-documentation-object) ### [Info](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#info-object) object @@ -104,7 +104,7 @@ This is supported by default as all `$ref` are dereferenced before the definitio - [x] tags - [x] summary - [x] description -- [ ] [externalDocs](#external-documentation-object) +- [x] [externalDocs](#external-documentation-object) - [ ] operationId - [x] [parameters](#parameter-object) - [x] [requestBody](#request-body-object) @@ -116,8 +116,8 @@ This is supported by default as all `$ref` are dereferenced before the definitio ### [External Documentation](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#external-documentation-object) object -- [ ] description -- [ ] url +- [x] description +- [x] url ### [Parameter](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#parameter-object) object @@ -209,7 +209,7 @@ See [parameter](#parameter-object) object. - [x] name - [x] description -- [ ] [externalDocs](#external-documentation-object) +- [x] [externalDocs](#external-documentation-object) ### [Reference](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#reference-object) object @@ -263,7 +263,7 @@ The OpenAPI specification also supports several additional properties from JSON - [ ] readOnly - [ ] writeOnly - [ ] [xml](#xml-object) -- [ ] [externalDocs](#external-documentation-object) +- [x] [externalDocs](#external-documentation-object) - [ ] example - [ ] deprecated diff --git a/src/components/BodyContent/BodyContent.styles.js b/src/components/BodyContent/BodyContent.styles.js index 6c3467e..3ed1151 100644 --- a/src/components/BodyContent/BodyContent.styles.js +++ b/src/components/BodyContent/BodyContent.styles.js @@ -5,14 +5,14 @@ export const styles = createSheet(({ text, backgrounds, sizes }) => ({ 'bodyContent': { display: 'table', width: '100%', - padding: '10px', + padding: '1rem 0', boxSizing: 'border-box' }, 'tabs': { '& > div': { display: 'inline-block', - padding: '10px 20px', + padding: '1rem 2rem', cursor: 'pointer', '&:not($active)': { @@ -23,12 +23,6 @@ export const styles = createSheet(({ text, backgrounds, sizes }) => ({ 'active': { backgroundColor: `${backgrounds.schema}`, - borderRadius: '10px 10px 0 0' - }, - - [`@media (max-width: ${sizes.breakpoint})`]: { - 'bodyContent': { - padding: '10px 0' - } + borderRadius: '1rem 1rem 0 0' } })) diff --git a/src/components/BodySchema/BodySchema.js b/src/components/BodySchema/BodySchema.js index a3ec6c7..1ce2ab9 100644 --- a/src/components/BodySchema/BodySchema.js +++ b/src/components/BodySchema/BodySchema.js @@ -77,6 +77,7 @@ export default class BodySchema extends Component { defaultValue={property.defaultValue} constraints={property.constraints} attributes={property.attributes} + docs={property.docs} onClick={hasSubset ? this.onClick : undefined} isRequired={property.required} isOpen={isOpen} diff --git a/src/components/ContentContainer/ContentContainer.styles.js b/src/components/ContentContainer/ContentContainer.styles.js index 89c5871..aca4394 100644 --- a/src/components/ContentContainer/ContentContainer.styles.js +++ b/src/components/ContentContainer/ContentContainer.styles.js @@ -2,19 +2,15 @@ import { createSheet } from '../../theme' export const styles = createSheet(({ sizes }) => ({ 'contentContainer': { - padding: '20px 0', - - '& h2': { - paddingLeft: '2rem' - } + padding: '0.5rem 2rem', + margin: '1rem 0' }, [`@media (max-width: ${sizes.breakpoint})`]: { 'contentContainer': { - padding: '10px 0', + padding: '1rem 0', '& h2': { - paddingLeft: '1rem', margin: 0 } } diff --git a/src/components/Description/Description.styles.js b/src/components/Description/Description.styles.js index ec60ff5..6537f6d 100644 --- a/src/components/Description/Description.styles.js +++ b/src/components/Description/Description.styles.js @@ -3,10 +3,8 @@ import { createSheet } from '../../theme' export const styles = createSheet(({ text }) => ({ 'description': { - '&:not($inline)': { - color: `${c(text.default).lighten(0.3)}`, - fontSize: '0.9em' - }, + color: `${c(text.default).lighten(0.3)}`, + fontSize: '1rem', '&$inline': { '&, & p': { @@ -17,5 +15,7 @@ export const styles = createSheet(({ text }) => ({ margin: '.5rem 0' } }, - 'inline': {} + 'inline': { + paddingRight: '.2rem' + } })) diff --git a/src/components/Docs/Docs.js b/src/components/Docs/Docs.js new file mode 100644 index 0000000..2b51a38 --- /dev/null +++ b/src/components/Docs/Docs.js @@ -0,0 +1,27 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classNames from 'classnames' + +import Description from '../Description/Description' +import ExternalLink from '../ExternalLink/ExternalLink' +import { styles } from './Docs.styles' + +@styles +export default class Docs extends PureComponent { + render () { + const { url, description, classes } = this.props + + return ( +
+ {description && } + More information +
+ ) + } +} + +Docs.propTypes = { + description: PropTypes.string, + url: PropTypes.string.isRequired, + classes: PropTypes.object +} diff --git a/src/components/Docs/Docs.styles.js b/src/components/Docs/Docs.styles.js new file mode 100644 index 0000000..3c23c93 --- /dev/null +++ b/src/components/Docs/Docs.styles.js @@ -0,0 +1,7 @@ +import { createSheet } from '../../theme' + +export const styles = createSheet(() => ({ + 'docs': { + margin: '0.5rem 0' + } +})) diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js index 0f8306d..ae608d9 100644 --- a/src/components/Header/Header.js +++ b/src/components/Header/Header.js @@ -1,6 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' +import Docs from '../Docs/Docs' import Description from '../Description/Description' import ExternalLink from '../ExternalLink/ExternalLink' import { styles } from './Header.styles' @@ -28,6 +29,8 @@ export default class Header extends PureComponent { {info && info.termsOfService && Terms of Service} + + {info && info.docs && } ) } @@ -88,7 +91,11 @@ Header.propTypes = { info: PropTypes.shape({ contact: PropTypes.object, license: PropTypes.object, - termsOfService: PropTypes.string + termsOfService: PropTypes.string, + docs: PropTypes.shape({ + description: PropTypes.string, + url: PropTypes.string.isRequired + }) }), definitionUrl: PropTypes.string, classes: PropTypes.object diff --git a/src/components/Header/Header.styles.js b/src/components/Header/Header.styles.js index eaacc4d..97a58f4 100644 --- a/src/components/Header/Header.styles.js +++ b/src/components/Header/Header.styles.js @@ -2,7 +2,7 @@ import { createSheet } from '../../theme' export const styles = createSheet(({ borders, sizes }) => ({ 'header': { - padding: '0 20px', + padding: '0 2rem', '& h1': { marginBottom: '.5rem' diff --git a/src/components/Method/Method.styles.js b/src/components/Method/Method.styles.js index 5ae0874..72ab562 100644 --- a/src/components/Method/Method.styles.js +++ b/src/components/Method/Method.styles.js @@ -5,7 +5,7 @@ export const styles = createSheet(({ borders, text, sizes }) => ({ method: { borderBottom: `1px solid ${borders.default}`, margin: '0 1rem 2rem 0', - padding: '1rem 2rem', + padding: '1rem 0', '& > h3': { marginBottom: '15px', diff --git a/src/components/NavigationTag/NavigationTag.js b/src/components/NavigationTag/NavigationTag.js index c9510be..996ec73 100644 --- a/src/components/NavigationTag/NavigationTag.js +++ b/src/components/NavigationTag/NavigationTag.js @@ -1,6 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import classNames from 'classnames' + import Indicator from '../Indicator/Indicator' import NavigationMethod from '../NavigationMethod/NavigationMethod' import Description from '../Description/Description' @@ -89,11 +90,11 @@ export default class NavigationTag extends Component { NavigationTag.propTypes = { title: PropTypes.string.isRequired, + hash: PropTypes.string.isRequired, description: PropTypes.string, methods: PropTypes.array, shouldBeExpanded: PropTypes.bool, onClick: PropTypes.func.isRequired, - hash: PropTypes.string.isRequired, - classes: PropTypes.object, - onClickMethod: PropTypes.func + onClickMethod: PropTypes.func, + classes: PropTypes.object } diff --git a/src/components/Page/Page.js b/src/components/Page/Page.js index 0755294..175724b 100644 --- a/src/components/Page/Page.js +++ b/src/components/Page/Page.js @@ -48,9 +48,17 @@ export default class Page extends Component { /> {security && this.renderSecurity(security)} - {services && services.map( - (service) => - )} + {services && services.map((service) => { + const serviceWithDocs = Object.assign({}, service, this.findTagDocs(service.title, navigation)) + + return ( + + ) + })} @@ -68,6 +76,21 @@ export default class Page extends Component { ) } + findTagDocs (tagHandle, navigation) { + // Find navigation tag that matches given `tagHandle`, and if has docs. + // Note: tag.methods is a way to determine if the navigation has tags at all. + const navigationTag = navigation.find((tag) => (tag.methods && tag.handle === tagHandle && tag.docs)) + + // No tag, no additional docs. + if (!navigationTag) { + return {} + } + + return { + docs: navigationTag.docs + } + } + onToggleNavigation () { const { isNavOpen } = this.state if (isNavOpen) { diff --git a/src/components/Property/Property.js b/src/components/Property/Property.js index 49e36f0..f9333e6 100644 --- a/src/components/Property/Property.js +++ b/src/components/Property/Property.js @@ -1,6 +1,8 @@ import React, { PureComponent } from 'react' import classNames from 'classnames' import PropTypes from 'prop-types' + +import Docs from '../Docs/Docs' import Description from '../Description/Description' import Indicator from '../Indicator/Indicator' import PropertyConstraints from './PropertyConstraints/PropertyConstraints' @@ -47,9 +49,8 @@ export default class Property extends PureComponent { render () { const { - type, title, description, constraints, isRequired, - defaultValue, onClick, isOpen, isLast, attributes, - classes + type, title, description, constraints, docs, defaultValue, attributes, + isRequired, isOpen, isLast, onClick, classes } = this.props const {name, isEnumTrimmed} = this.state @@ -104,6 +105,7 @@ export default class Property extends PureComponent { {enumValues && this.renderEnumValues(enumValues, isEnumTrimmed)} {defaultValue !== undefined && this.renderDefaultValue(defaultValue)} {description &&
} + {docs && } } diff --git a/src/components/SecurityContainer/SecurityContainer.styles.js b/src/components/SecurityContainer/SecurityContainer.styles.js index d16826b..2f49738 100644 --- a/src/components/SecurityContainer/SecurityContainer.styles.js +++ b/src/components/SecurityContainer/SecurityContainer.styles.js @@ -2,7 +2,7 @@ import { createSheet } from '../../theme' export const styles = createSheet(({ backgrounds, borders, sizes }) => ({ 'securityContainer': { - padding: '1rem 2rem', + padding: '1rem 0', borderBottom: `1px solid ${borders.default}` }, @@ -26,11 +26,5 @@ export const styles = createSheet(({ backgrounds, borders, sizes }) => ({ '& > li > span': { fontWeight: 600 } - }, - - [`@media (max-width: ${sizes.breakpoint})`]: { - 'securityContainer': { - padding: '1rem' - } } })) diff --git a/src/components/ServiceContainer/ServiceContainer.js b/src/components/ServiceContainer/ServiceContainer.js index 6d093c8..9baa6ac 100644 --- a/src/components/ServiceContainer/ServiceContainer.js +++ b/src/components/ServiceContainer/ServiceContainer.js @@ -1,5 +1,7 @@ import React, { PureComponent } from 'react' import PropTypes from 'prop-types' + +import Docs from '../Docs/Docs' import Method from '../Method/Method' import { styles } from './ServiceContainer.styles' @@ -7,11 +9,12 @@ import { styles } from './ServiceContainer.styles' export default class ServiceContainer extends PureComponent { render () { const { service, classes, initialSchemaTreeDepth } = this.props - const { title, methods } = service + const { title, docs, methods } = service return (

{title}

+ {docs && } {methods.map( (method) => )} diff --git a/src/parser/open-api/schemaParser.js b/src/parser/open-api/schemaParser.js index e34dad1..4d36949 100644 --- a/src/parser/open-api/schemaParser.js +++ b/src/parser/open-api/schemaParser.js @@ -55,6 +55,10 @@ function getPropertyNode (nodeName, propertyNode, required = false) { outputNode.defaultValue = propertyNode.default } + if (propertyNode.externalDocs) { + outputNode.docs = propertyNode.externalDocs + } + if (propertyNode.additionalProperties === false) { outputNode.additionalProperties = false } diff --git a/src/parser/open-api/v3/navigationParser.js b/src/parser/open-api/v3/navigationParser.js index e7a960d..f1688fe 100644 --- a/src/parser/open-api/v3/navigationParser.js +++ b/src/parser/open-api/v3/navigationParser.js @@ -52,6 +52,10 @@ export function getServicesMethod ({ path, method, request, params, responses }) servicesMethod.description = method.description } + if (method.externalDocs) { + servicesMethod.docs = method.externalDocs + } + if (params) { servicesMethod.parameters = params } diff --git a/src/parser/open-api/v3/open-api-v3-parser.js b/src/parser/open-api/v3/open-api-v3-parser.js index 5d4ccbc..d3aad7b 100644 --- a/src/parser/open-api/v3/open-api-v3-parser.js +++ b/src/parser/open-api/v3/open-api-v3-parser.js @@ -193,10 +193,13 @@ function getUIParametersForLocation (parameters, location) { const resultArray = parameters.filter(parameter => (parameter.in === location)).map(parameter => { const uiParameter = { name: parameter.name, - description: parameter.description, required: parameter.required } + if (parameter.description && parameter.description !== '') { + uiParameter.description = parameter.description + } + // TODO: We set the type to be an array because the Property component // handles this. Property should eventually be split and this won't be // necessary... @@ -226,7 +229,7 @@ function getUIParametersForLocation (parameters, location) { function getUIRequest (description, requestBody = null) { const uiRequest = {} - if (description) { + if (description && description !== '') { uiRequest.description = description } @@ -252,10 +255,12 @@ function getUIResponses (responses) { for (const statusCode in responses) { const response = responses[statusCode] - const uiResponse = { - code: statusCode, - description: response.description + code: statusCode + } + + if (response.description && response.description !== '') { + uiResponse.description = response.description } const mediaType = getMediaType(response.content) @@ -341,12 +346,12 @@ function addTagDetailsToNavigation (navigation, tagDefinitions) { navGroup.handle = navGroup.title navGroup.title = tagDefinition.name - if (tagDefinition.description) { + if (tagDefinition.description && tagDefinition.description !== '') { navGroup.description = tagDefinition.description } if (tagDefinition.externalDocs) { - navGroup.externalDocs = tagDefinition.externalDocs + navGroup.docs = tagDefinition.externalDocs } } } @@ -390,6 +395,10 @@ export default async function getUIReadyDefinition (openApiV3, sortFunc) { delete infoObj.version delete infoObj.description + if (derefOpenApiV3.externalDocs) { + infoObj.docs = derefOpenApiV3.externalDocs + } + const definition = { title: info.title, version: info.version, diff --git a/test/parser/open-api/v3/data/inputs/petstore.false.json b/test/parser/open-api/v3/data/inputs/petstore.false.json index c3f370a..c810158 100644 --- a/test/parser/open-api/v3/data/inputs/petstore.false.json +++ b/test/parser/open-api/v3/data/inputs/petstore.false.json @@ -232,6 +232,10 @@ "summary": "Find pet by ID", "description": "Returns a single pet", "operationId": "getPetById", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + }, "parameters": [ { "name": "petId", @@ -946,7 +950,10 @@ "properties": { "id": { "type": "integer", - "format": "int64" + "format": "int64", + "externalDocs": { + "url": "http://petstore.swagger.io/v2" + } }, "category": { "$ref": "#/components/schemas/Category" diff --git a/test/parser/open-api/v3/data/outputs/petstore.false.json b/test/parser/open-api/v3/data/outputs/petstore.false.json index 9780c20..ca10d0f 100644 --- a/test/parser/open-api/v3/data/outputs/petstore.false.json +++ b/test/parser/open-api/v3/data/outputs/petstore.false.json @@ -10,6 +10,10 @@ "license": { "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "docs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" } }, "navigation": [ @@ -59,7 +63,7 @@ ], "handle": "pet", "description": "Everything about your Pets", - "externalDocs": { + "docs": { "description": "Find out more", "url": "http://swagger.io" } @@ -137,7 +141,7 @@ ], "handle": "user", "description": "Operations about user", - "externalDocs": { + "docs": { "description": "Find out more about our store", "url": "http://swagger.io" } @@ -161,6 +165,9 @@ "required": false, "constraints": { "format": "int64" + }, + "docs": { + "url": "http://petstore.swagger.io/v2" } }, { @@ -286,6 +293,9 @@ "required": false, "constraints": { "format": "int64" + }, + "docs": { + "url": "http://petstore.swagger.io/v2" } }, { @@ -432,6 +442,9 @@ "required": false, "constraints": { "format": "int64" + }, + "docs": { + "url": "http://petstore.swagger.io/v2" } }, { @@ -583,6 +596,9 @@ "required": false, "constraints": { "format": "int64" + }, + "docs": { + "url": "http://petstore.swagger.io/v2" } }, { @@ -711,6 +727,10 @@ "type": "get", "title": "Find pet by ID", "link": "/pet/{petId}/get", + "docs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + }, "request": { "description": "Returns a single pet" }, @@ -727,6 +747,9 @@ "required": false, "constraints": { "format": "int64" + }, + "docs": { + "url": "http://petstore.swagger.io/v2" } }, {