Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support definitions that don't have tags. Closes #140 #144

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ You need to specify a url for the `Open API v3` file, e.g. <http://localhost:820

The following configuration options apply to the Lincoln component:

| property | required | type | description |
| --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `definitionUrl` | ✔ | string | CORS-enabled URL to Open API v3 definition to render. Supports JSON or YAML. |
| `navSort` | | enum | `alpha` which sorts by HTTP method, then path or `false`, which will display paths as defined. Defaults to `false`. |
| `validate` | | boolean | If `true`, uses [Mermade](https://openapi-converter.herokuapp.com/) to validate definition. Defaults to `false`. |
| property | required | type | description |
| --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `definitionUrl` | ✔ | string | CORS-enabled URL to Open API v3 definition to render. Supports JSON or YAML. |
| `navSort` | | enum | This property applies when your definition uses `tags`. `alpha` which sorts by HTTP method, then path or `false`, which will display paths as defined. Defaults to `false`. |
| `validate` | | boolean | If `true`, uses [Mermade](https://openapi-converter.herokuapp.com/) to validate definition. Defaults to `false`. |

## Building & Deployment

Expand Down
2 changes: 1 addition & 1 deletion docs/open-api-v3-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ This is supported by default as all `$ref` are dereferenced before the definitio

### [Operation](https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#operation-object) object

- [x] tags - Currently *required* for this project.
- [x] tags
- [x] summary
- [x] description
- [ ] [externalDocs](#external-documentation-object)
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@
"babel-regenerator-runtime": "^6.5.0",
"babel-runtime": "^6.23.0",
"css-loader": "^0.28.4",
"eslint": "^4.0.0",
"eslint": "^4.1.0",
"eslint-config-standard": "^10.2.1",
"eslint-config-standard-react": "^5.0.0",
"eslint-plugin-import": "^2.3.0",
"eslint-plugin-node": "^5.0.0",
"eslint-plugin-import": "^2.6.0",
"eslint-plugin-node": "^5.1.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^7.0.1",
"eslint-plugin-standard": "^3.0.1",
Expand Down
13 changes: 9 additions & 4 deletions src/components/Navigation/Navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types'
import isEqual from 'lodash/isEqual'
import NavigationTag from '../NavigationTag/NavigationTag'
import NavigationMethod from '../NavigationMethod/NavigationMethod'
import { styles } from './Navigation.styles'

@styles
Expand Down Expand Up @@ -30,18 +31,22 @@ export default class Navigation extends Component {
return (
<nav className={classes.navigation}>
{navigation && navigation.map((tag) => {
let shouldBeExpanded = false
if (expandedTags.includes(tag.title)) {
shouldBeExpanded = true
// Handle a navigation that doesn't require tags.
if (!tag.methods) {
const isActive = (`#${tag.link}` === location.hash)
return (
<NavigationMethod key={tag.link} method={tag} isActive={isActive} isOpen />
)
}

// Otherwise the navigation is grouped by tag.
return (
<NavigationTag
key={tag.title}
title={tag.title}
description={tag.description}
methods={tag.methods}
shouldBeExpanded={shouldBeExpanded}
shouldBeExpanded={expandedTags.includes(tag.title)}
onClick={this.onClick}
hash={hash}
/>
Expand Down
4 changes: 4 additions & 0 deletions src/components/Navigation/Navigation.styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export const styles = createSheet(({ backgrounds }) => ({

'& > div + div': {
borderTop: '1px solid #444'
},

'& > a': {
padding: '.7rem 1rem'
}
}
}))
60 changes: 60 additions & 0 deletions src/parser/open-api/v3/navigationParser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Return the permalink for the given `path` and `methodType`. Note this is
* not the link to the actual path, but it's a unique identifier to help
* with deeplinking from UI applications.
*
* @todo Look at supporting `operationId` which does this purpose.
* @param {string} path
* @param {string} methodType
*/
function getPermalink (path, methodType) {
return `${path}/${methodType}`
}

/**
* Given the `path`, `method` and optionally the `tag`, construct
* an object that represents the navigation method.
*
* @param {string} path
* @param {object} method
* @param {object} tag
* @return {object}
*/
export function getNavigationMethod (path, method, tag) {
return {
type: method.type,
title: method.summary,
link: getPermalink(path, method.type)
}
}

/**
* Construct the object used to render the method in the body of the renderer.
* This should represent all the information to create a request and receive
* a response for the given `method`.
*
* @param {string} path
* @param {object} method
* @param {object} request
* @param {object} params
* @param {object} responses
*/
export function getServicesMethod ({ path, method, request, params, responses }) {
const servicesMethod = {
type: method.type,
title: method.summary,
link: getPermalink(path, method.type),
request,
responses
}

if (method.description) {
servicesMethod.description = method.description
}

if (params) {
servicesMethod.parameters = params
}

return servicesMethod
}
174 changes: 102 additions & 72 deletions src/parser/open-api/v3/open-api-v3-parser.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,123 @@
import refParser from 'json-schema-ref-parser'
import { getSecurityDefinitions, getUISecurity } from './securityParser'
import { getNavigationMethod, getServicesMethod } from './navigationParser'
import getUIReadySchema from '../schemaParser'

/**
* Construct navigation and services ready to be consumed by the UI
*
* @param {Object} paths
* @param {Array} apiSecurity
* @param {Object} securityDefinitions
* @return {{navigation: [], services: []}}
*/
function getUINavigationAndServices ({ paths, apiSecurity = [], securityDefinitions }) {
const { navigationMethods: navigation, servicesMethods } =
buildNavigationAndServices(paths, apiSecurity, securityDefinitions)

// Need to wrap up the methods to be individual services blocks.
// This simplifies the component logic.
const services = servicesMethods.map(method => {
return {
title: method.title,
methods: [ method ]
}
})

return {navigation, services}
}

/**
* Construct navigation and services ready to be consumed by the UI using tags.
* This will group the paths by the logical tags.
*
* @param {Array} tags
* @param {Object} paths
* @param {Array} apiSecurity
* @param {Object} securityDefinitions
* @param {Function} sortFunc
* @return {{navigation: [], services: []}}
*/
function getUINavigationAndServices ({ tags, paths, apiSecurity = [], securityDefinitions, sortFunc }) {
function getUINavigationAndServicesByTags ({ tags, paths, apiSecurity = [], securityDefinitions, sortFunc }) {
const navigation = []
const services = []
const isFunc = typeof sortFunc === 'function'

for (let i = 0; i < tags.length; i++) {
for (let i = 0, tagLength = tags.length; i < tagLength; i++) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to add, it would be nice to integrate benchmarks in our tests, for regressions in the parser (or not-regressions!).

Below is an example benchmark test:

export function benchmark (timeout, fn) {
  const timeoutAt = Date.now() + timeout
  let iterations = 0

  do {
    fn()
    ++iterations
  } while (Date.now() <= timeoutAt)

  return iterations
}

it('benchmark', () => {
  const base = 2000 // Keep updating this manually, but for CI?
  // In future you'd want to import the previous stable version as a dev dep (maybe git commit hash?) and test against that

  const current = benchmark(500, () => {
    doSomeParsing('foo')
  })

  const percentDiff = Math.round(((current - base) / base) * 100)

  console.dir(`doSomeParsing: ${base} -> ${current} (%${percentDiff})`)

  expect(percentDiff).toBeGreaterThan(20) // It must be 20 percent faster
}

In theory iterations will scale across systems, but the baseline will be awkward.

Copy link
Contributor Author

@brendo brendo Jun 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, we should track this as a separate ticket. I raised #146

const tag = tags[i]
const navigationMethods = []
const servicesMethods = []
const pathIds = Object.keys(paths)

for (let j = 0; j < pathIds.length; j++) {
const pathId = pathIds[j]
const path = paths[pathId]
const methodTypes = Object.keys(path)
const exclusionFunc = (method) => method.tags.includes(tag) === false
const { navigationMethods, servicesMethods } =
buildNavigationAndServices(paths, apiSecurity, securityDefinitions, exclusionFunc)

for (let k = 0; k < methodTypes.length; k++) {
const methodType = methodTypes[k]
const method = path[methodType]
navigation.push({
title: tag,
methods: isFunc ? navigationMethods.sort(sortFunc) : navigationMethods
})

if (!method.tags.includes(tag)) {
continue
}
services.push({
title: tag,
methods: isFunc ? servicesMethods.sort(sortFunc) : servicesMethods
})
}

const link = pathId + '/' + methodType
const navigationMethod = {
type: methodType,
title: method.summary,
link
}
navigationMethods.push(navigationMethod)

const uiRequest = getUIRequest(method.description, method.requestBody)
const uiResponses = getUIResponses(method.responses)
const servicesMethod = {
type: methodType,
title: method.summary,
link,
request: uiRequest,
responses: uiResponses
}
return { navigation, services }
}

if (method.description) {
servicesMethod.description = method.description
}
/**
* Build the navigation and services from the given paths.
* Optionally run an `exclusionFunc` which if returns true, will skip
* the path from being included in the result.
*
* @param {Object} paths
* @param {Array} apiSecurity
* @param {Object} securityDefinitions
* @param {Function} exclusionFunc
* @return {{navigation: [], services: []}}
*/
function buildNavigationAndServices (paths, apiSecurity, securityDefinitions, exclusionFunc) {
const pathIds = Object.keys(paths)
const navigationMethods = []
const servicesMethods = []
const isFunc = typeof exclusionFunc === 'function'

for (let j = 0, pathIdLength = pathIds.length; j < pathIdLength; j++) {
const pathId = pathIds[j]
const path = paths[pathId]
const methodTypes = Object.keys(path)

for (let k = 0, methodLength = methodTypes.length; k < methodLength; k++) {
const methodType = methodTypes[k]
const method = Object.assign({ type: methodType }, path[methodType])

// Should this be included in the output?
if (isFunc && exclusionFunc(method)) {
continue
}

// Security can be declared per method, or globally for the entire API.
if (method.security) {
servicesMethod.security = getUISecurity(method.security, securityDefinitions)
} else if (apiSecurity.length) {
servicesMethod.security = getUISecurity(apiSecurity, securityDefinitions)
}
// Add the navigation item
navigationMethods.push(getNavigationMethod(pathId, method))

const uiParameters = getUIParameters(method.parameters)
if (uiParameters) {
servicesMethod.parameters = uiParameters
}
// Construct the full method object
const servicesMethod = getServicesMethod({
path: pathId,
method,
request: getUIRequest(method.description, method.requestBody),
params: getUIParameters(method.parameters),
responses: getUIResponses(method.responses)
})

servicesMethods.push(servicesMethod)
// Security can be declared per method, or globally for the entire API.
if (method.security) {
servicesMethod.security = getUISecurity(method.security, securityDefinitions)
} else if (apiSecurity.length) {
servicesMethod.security = getUISecurity(apiSecurity, securityDefinitions)
}
}

navigation.push({
title: tag,
methods: typeof sortFunc === 'function' ? navigationMethods.sort(sortFunc) : navigationMethods
})

services.push({
title: tag,
methods: typeof sortFunc === 'function' ? servicesMethods.sort(sortFunc) : servicesMethods
})
servicesMethods.push(servicesMethod)
}
}

return {navigation, services}
return { navigationMethods, servicesMethods }
}

/**
Expand Down Expand Up @@ -277,6 +310,10 @@ function getTags (paths) {
for (const methodKey in path) {
const method = path[methodKey]

if (Array.isArray(method.tags) === false) {
continue
}

method.tags.forEach(tag => {
if (!tagCollection.includes(tag)) {
tagCollection.push(tag)
Expand Down Expand Up @@ -334,23 +371,16 @@ export default async function getUIReadyDefinition (openApiV3, sortFunc) {
const info = derefOpenApiV3.info
const paths = derefOpenApiV3.paths
const apiSecurity = derefOpenApiV3.security || []

// Get tags from the paths
const tags = getTags(paths)

// Get security definitions
const securityDefinitions = getSecurityDefinitions(derefOpenApiV3)

// Construction navigation and services
const {navigation, services} = getUINavigationAndServices({
tags,
paths,
apiSecurity,
securityDefinitions,
sortFunc
})
// Construct navigation and services, which differs depending on
// if the definition utilises tags or not.
const tags = getTags(paths)
const {navigation, services} = (tags.length)
? getUINavigationAndServicesByTags({ tags, paths, apiSecurity, securityDefinitions, sortFunc })
: getUINavigationAndServices({ paths, apiSecurity, securityDefinitions })

// If we have tag information, let's add it to the navigation
// If we have top-level tag descriptions, add it to the navigation
if (derefOpenApiV3.tags) {
addTagDetailsToNavigation(navigation, derefOpenApiV3.tags)
}
Expand Down
Loading