apiaryio/fury-adapter-swagger

View on GitHub
src/parser.js

Summary

Maintainability
F
1 wk
Test Coverage
// The main Swagger parsing component that outputs refract.

import _ from 'lodash';
import { sep } from 'path';
import yaml from 'js-yaml';
import contentTypeModule from 'content-type';
import mediaTyper from 'media-typer';
import SwaggerParser from 'swagger-parser';
import ZSchema from 'z-schema';
import annotations from './annotations';
import { bodyFromSchema, bodyFromFormParameter } from './generator';
import uriTemplate from './uri-template';
import { baseLink, origin } from './link';
import { pushHeader, pushHeaderObject } from './headers';
import Ast from './ast';
import { DataStructureGenerator, idForDataStructure } from './schema';
import { convertSchema, convertSchemaDefinitions } from './json-schema';
import { FORM_CONTENT_TYPE, isValidContentType, isJsonContentType, isTextContentType, isMultiPartFormData, isFormURLEncoded, hasBoundary, parseBoundary } from './media-type';


// Provide a `nextTick` function that either is Node's nextTick or a fallback
// for browsers
function nextTick(cb) {
  if (process && process.nextTick) {
    process.nextTick(cb);
  } else {
    cb();
  }
}

// Test whether a key is a special Swagger extension.
function isExtension(value, key) {
  return _.startsWith(key, 'x-');
}

// The parser holds state about the current parsing environment and converts
// the input Swagger into Refract elements. The `parse` function is its main
// interface.
export default class Parser {
  constructor({ minim, source, generateSourceMap }) {
    // Parser options
    this.minim = minim;
    this.source = source;
    this.generateSourceMap = generateSourceMap;

    // Global scheme requirements
    this.globalSchemes = [];

    // Loaded, dereferenced Swagger API
    this.swagger = null;
    // Refract parse result
    this.result = null;
    // Refract API category
    this.api = null;
    // State of the current parsing path
    this.path = [];
    // Current resource group, if any
    this.group = null;
  }

  parse(done) {
    const {
      Category, ParseResult, SourceMap,
    } = this.minim.elements;
    const swaggerParser = new SwaggerParser();

    this.result = new ParseResult();

    // First, we load the YAML if it is a string, and handle any errors.
    let loaded;
    try {
      loaded = _.isString(this.source) ? yaml.safeLoad(this.source) : this.source;
    } catch (err) {
      // Temporarily disable generateSourceMap while handling error
      // This is because while handling this error we may try to generate
      // source map which further tries to parse YAML to get source
      // maps which causes another warning and raise conditions where we throw
      // an error back to the caller.
      const { generateSourceMap } = this;
      this.generateSourceMap = false;

      this.createAnnotation(
        annotations.CANNOT_PARSE, null,
        (err.reason || 'Problem loading the input'),
      );

      if (err.mark) {
        this.result.first.attributes.set('sourceMap', [
          new SourceMap([[err.mark.position, 1]]),
        ]);
      }

      this.generateSourceMap = generateSourceMap;
      return done(new Error(err.message), this.result);
    }

    if (!_.isObject(loaded)) {
      this.createAnnotation(
        annotations.CANNOT_PARSE, null,
        ('Swagger document is not an object'),
      );

      return done(null, this.result);
    }

    // Some sane defaults since these are sometimes left out completely
    if (loaded.info === undefined) {
      loaded.info = {};
    }

    if (loaded.paths === undefined) {
      loaded.paths = {};
    }

    // Next, we dereference and validate the loaded Swagger object. Any schema
    // violations get converted into annotations with source maps.
    const swaggerOptions = {
      dereference: {
        circular: 'ignore',
      },
      resolve: {
        external: false,
      },
    };

    // Swagger parser is mutating the given input and dereferencing.
    // Let's give it no changes to screw the original and give it a deep copy
    const referencedSwagger = JSON.parse(JSON.stringify(loaded));
    this.referencedSwagger = referencedSwagger;

    return swaggerParser.validate(loaded, swaggerOptions, (err) => {
      const swagger = swaggerParser.api;
      this.swagger = swaggerParser.api;

      if (err) {
        if (this.swagger === undefined) {
          return done(err, this.result);
        }

        // Non-fatal errors, so let us try and create annotations for them and
        // continue with the parsing as best we can.
        if (err.details) {
          const queue = [err.details];

          while (queue.length) {
            _.forEach(queue[0], (item) => {
              this.createAnnotation(annotations.VALIDATION_ERROR, item.path, item.message);

              if (item.inner) {
                // TODO: I am honestly not sure what the correct behavior is
                // here. Some items will have within them a tree of other items,
                // some of which might contain more info (but it's unclear).
                // Do we treat them as their own error or do something else?
                queue.push(item.inner);
              }
            });

            queue.shift();
          }

          return done(new Error(err.message), this.result);
        }

        // Maybe there is some information in the error itself? Let's check
        // whether it is a messed up reference!
        let location = null;

        // Workaround for https://github.com/APIDevTools/json-schema-ref-parser/pull/80#issuecomment-436041573
        const message = err.message.replace(`${process.cwd()}${sep}`, '');
        const matches = message.match(/\$ref pointer "(.*?)"/);

        if (matches) {
          location = [this.source.indexOf(matches[1]), matches[1].length];
        }

        const annotation = this.createAnnotation(annotations.VALIDATION_ERROR, null, message);

        if (location !== null) {
          annotation.attributes.set('sourceMap', [
            new SourceMap([location]),
          ]);
        }

        return done(new Error(err.message), this.result);
      }

      try {
        // Root API Element
        this.api = new Category();
        this.api.classes.push('api');
        this.result.push(this.api);

        // By default there are no groups, just the root API element
        this.group = this.api;

        this.handleSwaggerInfo();
        this.handleSwaggerHost();
        this.handleSwaggerAuth();

        this.handleExternalDocs(this.api, swagger.externalDocs);

        this.validateProduces(this.swagger.produces);
        this.validateConsumes(this.swagger.consumes);

        this.definitions = convertSchemaDefinitions(referencedSwagger.definitions);

        const complete = () => {
          this.handleSwaggerVendorExtensions(this.api, swagger.paths);

          if (this.definitions) {
            this.withPath('definitions', () => {
              this.handleSwaggerDefinitions(referencedSwagger.definitions);
            });
          }

          return done(null, this.result);
        };

        // Swagger has a paths object to loop through that describes resources
        // We will run each path on it's own tick since it may take some time
        // and we want to ensure that other events in the event queue are not
        // held up.
        const paths = _.omitBy(swagger.paths, isExtension);
        let pendingPaths = Object.keys(paths).length;

        if (pendingPaths === 0) {
          // If there are no paths, let's go ahead and call the callback.
          return complete();
        }

        return _.forEach(paths, (pathValue, href) => {
          nextTick(() => {
            this.handleSwaggerPath(pathValue, href);
            pendingPaths -= 1;

            if (pendingPaths === 0) {
              // Last path, let's call the completion callback.
              complete();
            }
          });
        });
      } catch (exception) {
        this.createAnnotation(
          annotations.UNCAUGHT_ERROR, null,
          'There was a problem converting the Swagger document',
        );

        return done(exception, this.result);
      }
    });
  }

  // == Internal properties & functions ==

  // Base path (URL) name for the API
  get basePath() {
    return (this.swagger.basePath || '').replace(/[/]+$/, '');
  }

  // Lazy-loaded input AST is made available when we need it. If it can't be
  // loaded, then an annotation is generated with more information about why.
  get ast() {
    if (this.internalAST !== undefined) {
      return this.internalAST;
    }

    if (_.isString(this.source)) {
      try {
        this.internalAST = new Ast(this.source);
      } catch (err) {
        this.internalAST = null;

        let message = 'YAML Syntax Error';
        if (err.problem) {
          message = `${message}: ${err.problem}`;
        }

        const annotation = this.createAnnotation(annotations.AST_UNAVAILABLE, null, message);

        if (err.problem_mark && err.problem_mark.pointer) {
          const SourceMap = this.minim.getElementClass('sourceMap');
          const position = err.problem_mark.pointer;

          annotation.attributes.set('sourceMap', [
            new SourceMap([[position, 1]]),
          ]);
        }
      }
    } else {
      this.internalAST = null;
      this.createAnnotation(
        annotations.AST_UNAVAILABLE, null,
        'Source maps are only available with string input',
      );
    }

    return this.internalAST;
  }

  // This method lets you set the current parsing path and synchronously run
  // a function (e.g. to create an element).
  withPath(...args) {
    let i;
    const originalPath = _.clone(this.path);

    for (i = 0; i < args.length - 1; i += 1) {
      if (args[i] === '..') {
        this.path.pop();
      } else if (args[i] === '.') {
        // do nothing
      } else {
        this.path.push(args[i]);
      }
    }

    args[args.length - 1].bind(this)(this.path);

    this.path = originalPath;
  }

  // This is like `withPath` above, but slices the path before calling by
  // using the first argument as a length (starting at index 0).
  withSlicedPath(...args) {
    const original = this.path.slice(0);

    // First, we slice the path, then call `withPath` and finally reset the path.
    this.path = this.path.slice(0, args[0]);
    this.withPath(...args.slice(1));
    this.path = original;
  }

  handleExternalDocs(element, docs) {
    if (!docs) {
      return;
    }

    baseLink(element, this, 'help', {
      description: docs.description,
      url: docs.url,
      path: this.path.concat(['externalDocs']),
    });
  }

  // Converts the Swagger title and description
  handleSwaggerInfo() {
    const { Copy } = this.minim.elements;

    if (this.swagger.info) {
      this.withPath('info', () => {
        if (this.swagger.info.title) {
          this.withPath('title', () => {
            this.api.title = this.swagger.info.title;

            if (this.generateSourceMap) {
              this.createSourceMap(this.api.meta.get('title'), this.path);
            }

            return this.api.meta.get('title');
          });
        }

        if (this.swagger.info.version) {
          this.withPath('version', () => {
            this.api.attributes.set('version', this.swagger.info.version);

            if (this.generateSourceMap) {
              this.createSourceMap(this.api.attributes.get('version'), this.path);
            }

            return this.api.attributes.get('version');
          });
        }

        if (this.swagger.info.description) {
          this.withPath('description', () => {
            const description = new Copy(this.swagger.info.description);
            this.api.content.push(description);

            if (this.generateSourceMap) {
              this.createSourceMap(description, this.path);
            }

            return description;
          });
        }

        this.handleSwaggerVendorExtensions(this.api, this.swagger.info);
      });
    }
  }

  // Converts the Swagger hostname and schemes to a Refract host metadata entry.
  handleSwaggerHost() {
    const { Member: MemberElement } = this.minim.elements;

    if (this.swagger.host) {
      this.withPath('host', () => {
        let hostname = this.swagger.host;

        if (this.swagger.schemes) {
          if (this.swagger.schemes.length > 1) {
            this.createAnnotation(
              annotations.DATA_LOST, ['schemes'],
              'Only the first scheme will be used to create a hostname',
            );
          }

          hostname = `${this.swagger.schemes[0]}://${hostname}`;
        }

        const metadata = [];
        const member = new MemberElement('HOST', hostname);

        member.meta.set('classes', ['user']);

        if (this.generateSourceMap) {
          this.createSourceMap(member, this.path);
        }

        metadata.push(member);
        this.api.attributes.set('metadata', metadata);

        return member;
      });
    }
  }

  // Conver api key name into Refract elements
  apiKeyName(element, apiKey) {
    const { Member: MemberElement } = this.minim.elements;
    let config;

    if (apiKey.in === 'query') {
      config = 'queryParameterName';
    } else if (apiKey.in === 'header') {
      config = 'httpHeaderName';
    }

    const member = new MemberElement(config, apiKey.name);

    if (this.generateSourceMap) {
      this.createSourceMap(member, this.path.concat(['name']));
    }

    element.content.push(member);
  }

  // Convert Oauth2 flow into Refract elements
  oauthGrantType(element, flow) {
    const { Member: MemberElement } = this.minim.elements;
    let grantType = flow;

    if (flow === 'password') {
      grantType = 'resource owner password credentials';
    } else if (flow === 'application') {
      grantType = 'client credentials';
    } else if (flow === 'accessCode') {
      grantType = 'authorization code';
    }

    const member = new MemberElement('grantType', grantType);

    if (this.generateSourceMap) {
      this.createSourceMap(member, this.path.concat(['flow']));
    }

    element.content.push(member);
  }

  // Convert OAuth2 scopes into Refract elements
  oauthScopes(element, items) {
    const {
      Member: MemberElement,
      Array: ArrayElement,
      String: StringElement,
    } = this.minim.elements;

    const scopes = new ArrayElement();
    let descriptions = null;
    let scopesList = items;

    if (_.isObject(items) && !_.isArray(items)) {
      descriptions = Object.values(items);
      scopesList = Object.keys(items);
    }

    // If value is not an empty array, then they are scopes
    _.forEach(scopesList, (scopeName, index) => {
      const scope = new StringElement(scopeName);

      if (descriptions) {
        scope.description = descriptions[index];

        if (this.generateSourceMap) {
          this.createSourceMap(scope.meta.get('description'), this.path.concat([scopeName]));
        }
      }

      if (this.generateSourceMap) {
        const value = descriptions ? scopeName : index;
        this.createSourceMap(scope, this.path.concat([value]));
      }

      scopes.content.push(scope);
    });

    if (scopes.length) {
      element.content.push(new MemberElement('scopes', scopes));
    }
  }

  // Conver OAuth2 transition information into Refract elements
  oauthTransitions(element, oauth) {
    const { Transition } = this.minim.elements;

    if (oauth.authorizationUrl) {
      const transition = new Transition();

      transition.relation = 'authorize';
      transition.href = oauth.authorizationUrl;

      if (this.generateSourceMap) {
        this.createSourceMap(transition.attributes.get('href'), this.path.concat(['authorizationUrl']));
        this.createSourceMap(transition.attributes.get('relation'), this.path.concat(['authorizationUrl']));
      }

      element.content.push(transition);
    }

    if (oauth.tokenUrl) {
      const transition = new Transition();

      transition.relation = 'token';
      transition.href = oauth.tokenUrl;

      if (this.generateSourceMap) {
        this.createSourceMap(transition.attributes.get('href'), this.path.concat(['tokenUrl']));
        this.createSourceMap(transition.attributes.get('relation'), this.path.concat(['tokenUrl']));
      }

      element.content.push(transition);
    }
  }

  // Convert a Swagger auth object into Refract elements.
  handleSwaggerAuth() {
    const { Category, AuthScheme } = this.minim.elements;
    const schemes = [];

    if (this.swagger.securityDefinitions) {
      _.keys(this.swagger.securityDefinitions).forEach((name) => {
        this.withPath('securityDefinitions', name, () => {
          const item = this.swagger.securityDefinitions[name];
          const element = new AuthScheme();

          switch (item.type) {
            case 'basic':
              element.element = 'Basic Authentication Scheme';
              break;

            case 'apiKey':
              element.element = 'Token Authentication Scheme';
              this.apiKeyName(element, item);
              break;

            case 'oauth2':
              element.element = 'OAuth2 Scheme';
              this.oauthGrantType(element, item.flow);

              if (item.scopes) {
                this.withPath('scopes', () => {
                  this.oauthScopes(element, item.scopes);
                });
              }

              this.oauthTransitions(element, item);
              break;

            default:
              break;
          }

          element.id = name;

          if (this.generateSourceMap) {
            this.createSourceMap(element.meta.get('id'), this.path);
          }

          if (item['x-summary']) {
            element.title = item['x-summary'];

            if (this.generateSourceMap) {
              this.createSourceMap(element.meta.get('title'), this.path.concat(['x-summary']));
            }
          }

          if (item.description) {
            element.description = item.description;

            if (this.generateSourceMap) {
              this.createSourceMap(element.meta.get('description'), this.path.concat(['description']));
            }
          }

          schemes.push(element);

          this.handleSwaggerVendorExtensions(element, item);
        });
      });
    }

    if (schemes.length) {
      const category = new Category();

      category.meta.set('classes', ['authSchemes']);
      category.content = schemes;

      this.api.content.push(category);
    }

    if (!this.swagger.security) {
      return;
    }

    this.handleSwaggerSecurity(this.swagger.security, this.globalSchemes);
  }

  handleSwaggerSecurity(security, schemes) {
    const { AuthScheme } = this.minim.elements;

    _.forEach(security, (item, index) => {
      _.keys(item).forEach((name) => {
        this.withPath('security', index, name, () => {
          const element = new AuthScheme();

          // If value is not an empty array, then they are scopes
          this.oauthScopes(element, item[name]);

          if (this.generateSourceMap) {
            this.createSourceMap(element, this.path);
          }

          element.element = name;
          schemes.push(element);
        });
      });
    });
  }

  handleSwaggerTransitionAuth(methodValue) {
    const schemes = [];

    if (!methodValue.security) {
      return this.globalSchemes;
    }

    this.handleSwaggerSecurity(methodValue.security, schemes);

    return schemes;
  }

  handleSwaggerDefinitions(definitions) {
    const { Category } = this.minim.elements;
    const generator = new DataStructureGenerator(this.minim, this.referencedSwagger);
    const dataStructures = new Category();
    dataStructures.classes.push('dataStructures');

    _.forEach(definitions, (schema, key) => {
      this.withPath(key, () => {
        try {
          const dataStructure = generator.generateDataStructure(schema);

          if (dataStructure) {
            dataStructure.content.id = idForDataStructure(`#/definitions/${key}`);

            if (this.generateSourceMap) {
              this.createSourceMap(dataStructure, this.path);
            }

            dataStructures.push(dataStructure);
          }
        } catch (error) {
          // TODO: Expose errors once feature is more-complete
        }
      });
    });

    if (dataStructures.length > 0) {
      this.api.push(dataStructures);
    }
  }

  // Convert a Swagger path into a Refract resource.
  handleSwaggerPath(pathValue, href) {
    const { Copy, Resource } = this.minim.elements;
    const resource = new Resource();

    this.withPath('paths', href, () => {
      // Provide users with a way to add a title to a resource in Swagger
      if (pathValue['x-summary']) {
        this.withPath('x-summary', () => {
          resource.title = String(pathValue['x-summary']);

          if (this.generateSourceMap) {
            this.createSourceMap(resource.meta.get('title'), this.path);
          }

          return resource.meta.get('title');
        });
      }

      // Provide users a way to add a description to a resource in Swagger
      if (pathValue['x-description']) {
        this.withPath('x-description', () => {
          const resourceDescription = new Copy(pathValue['x-description']);
          resource.push(resourceDescription);

          if (this.generateSourceMap) {
            this.createSourceMap(resourceDescription, this.path);
          }

          return resourceDescription;
        });
      }

      if (this.useResourceGroups()) {
        this.updateResourceGroup(pathValue['x-group-name']);
      }

      this.group.content.push(resource);

      const pathObjectParameters = pathValue.parameters || [];
      const resourceHrefVariables = this.createHrefVariables(pathObjectParameters);

      if (resourceHrefVariables) {
        resource.hrefVariables = resourceHrefVariables;
      }

      // Set the resource-wide URI template, which can further be overridden
      // by individual transition URI templates. When creating a transition
      // below, we *only* set the transition URI template if it differs from
      // the one we've generated here.
      resource.href = uriTemplate(this.basePath, href, pathObjectParameters);

      if (this.generateSourceMap) {
        this.createSourceMap(resource.attributes.get('href'), this.path);
      }

      const relevantMethods = _.chain(pathValue)
        .omit('parameters', '$ref')
        .omitBy(isExtension)
        .value();

      // Each path is an object with methods as properties
      _.forEach(relevantMethods, (methodValue, method) => {
        this.handleSwaggerMethod(resource, href, pathObjectParameters, methodValue, method);
      });

      this.handleSwaggerVendorExtensions(resource, pathValue);

      return resource;
    });
  }

  // Converts all unknown Swagger vendor extensions from an object into a API Element extension
  handleSwaggerVendorExtensions(element, object) {
    const extensions = _.chain(object)
      .pickBy(isExtension)
      .omit('x-description', 'x-summary', 'x-group-name')
      .value();

    if (Object.keys(extensions).length > 0) {
      const { Link, Extension } = this.minim.elements;

      const profileLink = new Link();
      profileLink.relation = 'profile';
      profileLink.href = 'https://help.apiary.io/profiles/api-elements/vendor-extensions/';

      const extension = new Extension(extensions);
      extension.links = [profileLink];
      element.content.push(extension);
    }
  }

  // Convert a Swagger method into a Refract transition.
  handleSwaggerMethod(resource, href, resourceParams, methodValue, method) {
    const { Copy, Transition } = this.minim.elements;
    const transition = new Transition();

    resource.content.push(transition);

    this.withPath(method, () => {
      const schemes = this.handleSwaggerTransitionAuth(methodValue);

      this.validateProduces(methodValue.produces);
      this.validateConsumes(methodValue.consumes);

      this.handleExternalDocs(transition, methodValue.externalDocs);

      const transitionParams = methodValue.parameters || [];

      const queryParams = transitionParams.filter(parameter => parameter.in === 'query');

      // Here we generate a URI template specific to this transition. If it
      // is different from the resource URI template, then we set the
      // transition's `href` attribute.
      const hrefForTransition = uriTemplate(this.basePath, href, resourceParams, queryParams);

      if (hrefForTransition !== resource.href.toValue()) {
        transition.href = hrefForTransition;
      }

      if (methodValue.summary) {
        this.withPath('summary', () => {
          transition.title = methodValue.summary;

          if (this.generateSourceMap) {
            this.createSourceMap(transition.meta.get('title'), this.path);
          }

          return transition.meta.get('title');
        });
      }

      if (methodValue.description) {
        this.withPath('description', () => {
          const description = new Copy(methodValue.description);
          transition.push(description);

          if (this.generateSourceMap) {
            this.createSourceMap(description, this.path);
          }

          return description;
        });
      }

      if (methodValue.operationId) {
        // TODO: Add a source map?
        transition.id = methodValue.operationId;
      }

      // For each uriParameter, create an hrefVariable
      const methodHrefVariables = this.createHrefVariables(transitionParams);
      if (methodHrefVariables) {
        transition.hrefVariables = methodHrefVariables;
      }

      // Currently, default responses are not supported in API Description format
      const relevantResponses = _.chain(methodValue.responses)
        .omit('default')
        .omitBy(isExtension)
        .value();

      if (methodValue.responses && methodValue.responses.default) {
        this.withPath('responses', 'default', (path) => {
          this.createAnnotation(
            annotations.DATA_LOST, path,
            'Default response is not yet supported',
          );
        });
      }

      if (_.keys(relevantResponses).length === 0) {
        if (transitionParams.filter(p => p.in === 'body').length) {
          // Create an empty successful response so that the request/response
          // pair gets properly generated. In the future we may want to
          // refactor the code below as this is a little weird.
          relevantResponses.null = {};
        } else {
          this.createTransaction(transition, method, schemes);
        }
      }

      // Transactions are created for each response in the document
      _.forEach(relevantResponses, (responseValue, statusCode) => {
        this.handleSwaggerResponse(
          transition, method, methodValue,
          transitionParams, responseValue, statusCode,
          schemes, resourceParams,
        );
      });

      this.handleSwaggerVendorExtensions(transition, methodValue);

      return transition;
    });
  }

  // Returns all of the content types for a request
  // Request content types include all consumes types
  // Returns `[null]` when there are no content types
  gatherRequestContentTypes(methodValue) {
    const contentTypes = (methodValue.consumes || this.swagger.consumes || [])
      .filter(isValidContentType);

    if (contentTypes.length === 0) {
      return [null];
    }

    return contentTypes;
  }

  // Returns all of the content types for a response
  // Response content types include all example types OR the first JSON content type
  // Returns `[null]` when there are no content types
  gatherResponseContentTypes(methodValue, examples) {
    let contentTypes = [];

    if (examples && Object.keys(examples).length > 0) {
      contentTypes = Object.keys(examples);
    } else {
      const produces = (methodValue.produces || this.swagger.produces || []);
      const jsonContentTypes = produces.filter(isJsonContentType);

      if (jsonContentTypes.length > 0) {
        contentTypes = [jsonContentTypes[0]];
      }
    }

    contentTypes = contentTypes.filter(isValidContentType);

    if (contentTypes.length === 0) {
      return [null];
    }

    return contentTypes;
  }

  // Convert a Swagger response & status code into Refract transactions.
  handleSwaggerResponse(
    transition, method, methodValue, transitionParams,
    responseValue, statusCode, schemes, resourceParams,
  ) {
    const requestContentTypes = this.gatherRequestContentTypes(methodValue);
    const responseContentTypes = this
      .gatherResponseContentTypes(methodValue, responseValue.examples);

    responseContentTypes.forEach((responseContentType) => {
      let responseBody;

      if (responseContentType && responseValue.examples &&
          responseValue.examples[responseContentType]) {
        responseBody = responseValue.examples[responseContentType];
      }

      requestContentTypes.forEach((requestContentType) => {
        const transaction = this.createTransaction(transition, method, schemes);

        this.handleSwaggerExampleRequest(
          transaction, methodValue, transitionParams,
          resourceParams, requestContentType, responseContentType, responseBody === undefined,
        );

        this.handleSwaggerExampleResponse(
          transaction, methodValue, responseValue,
          statusCode, responseBody, responseContentType,
        );
      });
    });
  }

  // Convert a Swagger example into a Refract request.
  handleSwaggerExampleRequest(
    transaction, methodValue, transitionParams, resourceParams,
    requestContentType, responseContentType, contentTypeFromProduces,
  ) {
    let contentType = requestContentType;
    const { request } = transaction;

    this.withPath(() => {
      const consumeIsJson = contentType && isJsonContentType(contentType);
      const consumeIsMultipartFormData = contentType && isMultiPartFormData(contentType);

      if (consumeIsMultipartFormData && !hasBoundary(contentType)) {
        // When multipart/form-data conntent type doesn't have a boundary
        // add a default one `BOUNDARY` which hopefully isn't found
        // in an example content. `parseBoundary` will provide the default.
        contentType += `; boundary=${parseBoundary(contentType)}`;
      }

      if (contentType) {
        pushHeader('Content-Type', contentType, request, this, 'consumes-content-type');
      }

      if (responseContentType) {
        if (contentTypeFromProduces) {
          pushHeader('Accept', responseContentType, request, this, 'produces-accept');
        } else {
          pushHeader('Accept', responseContentType, request, this);
        }
      }

      const formParams = [];
      let formParamsSchema = { type: 'object', properties: {}, required: [] };

      const parametersGenerator = {};

      _.forEach([
        [resourceParams, '..'],
        [transitionParams, '.'],
      ], (parameters) => {
        _.forEach(parameters[0], (param, index) => {
          switch (param.in) {
            case 'header':
              _.set(parametersGenerator, [param.in, param.name], _.bind(this.withPath, this, parameters[1], 'parameters', index, () => {
                pushHeaderObject(param.name, param, request, this);
              }));
              break;

            case 'body': {
              let bodyIsPrimitive = false;

              _.set(parametersGenerator, [param.in, param.name], _.bind(this.withPath, this, parameters[1], 'parameters', index, () => {
                if (param['x-example']) {
                  this.withPath('x-example', () => {
                    this.createAnnotation(
                      annotations.VALIDATION_ERROR, this.path,
                      'The \'x-example\' property isn\'t allowed for body parameters - use \'schema.example\' instead',
                    );
                  });
                }

                if (param.schema) {
                  if (param.schema.format === 'binary') {
                    return;
                  }

                  bodyIsPrimitive = (param.schema.type && ['string', 'boolean', 'number'].includes(param.schema.type));
                }

                this.withPath('schema', () => {
                  const pushBody = (consumeIsJson ||
                      (bodyIsPrimitive && isTextContentType(contentType)));
                  this.pushAssets(param.schema, request, contentType, pushBody);
                });
              }));
              break;
            }

            case 'formData':
              _.set(parametersGenerator, [param.in, param.name], _.bind(this.withPath, this, parameters[1], 'parameters', index, () => {
                this.formDataParameterCheck(param);
                formParamsSchema = bodyFromFormParameter(param, formParamsSchema);
                const member = this.convertParameterToMember(param);
                formParams.push(member);
              }));
              break;

            default:
          }
        });
      });

      _.forEach(parametersGenerator, (paramType) => {
        _.forEach(paramType, (invoke) => {
          invoke();
        });
      });

      if (!contentType || consumeIsMultipartFormData || isFormURLEncoded(contentType)) {
        this.generateFormParameters(formParams, formParamsSchema, request, contentType);
      }

      // Using form parameters instead of body? We will convert those to
      // data structures and will generate form-urlencoded body.
      return request;
    });
  }

  generateFormParameters(parameters, schema, request, contentType) {
    if (_.isEmpty(parameters)) {
      return;
    }

    const { DataStructure, Object: ObjectElement } = this.minim.elements;

    if (!contentType) {
      // No content type was provided, lets default to first form
      pushHeader('Content-Type', FORM_CONTENT_TYPE, request, this, 'form-data-content-type');
    }

    const jsonSchema = convertSchema(schema, { definitions: this.definitions },
        this.referencedSwagger);
    bodyFromSchema(jsonSchema, request, this, contentType || FORM_CONTENT_TYPE);

    // Generating data structure
    const dataStructure = new DataStructure();

    // A form is essentially an object with key/value members
    const dataObject = new ObjectElement();

    _.forEach(parameters, (param) => {
      dataObject.content.push(param);
    });

    dataStructure.content = dataObject;
    request.content.push(dataStructure);
  }

  formDataParameterCheck(param) {
    if (param.type === 'array') {
      this.createAnnotation(
        annotations.DATA_LOST, this.path,
        'Arrays in form parameters are not fully supported yet',
      );

      return;
    }

    if (param.allowEmptyValue) {
      this.createAnnotation(
        annotations.DATA_LOST, this.path,
        'The allowEmptyValue flag is not fully supported yet',
      );
    }
  }

  // Convert a Swagger example into a Refract response.
  handleSwaggerExampleResponse(
    transaction, methodValue, responseValue,
    statusCode, responseBody, contentType,
  ) {
    const { Asset, Copy } = this.minim.elements;
    const { response } = transaction;

    this.withPath('responses', statusCode, () => {
      if (responseValue.description) {
        const description = new Copy(responseValue.description);
        response.content.push(description);

        if (this.generateSourceMap) {
          this.createSourceMap(description, this.path.concat(['description']));
        }
      }

      if (contentType) {
        if (responseValue.examples && responseValue.examples[contentType]) {
          this.withPath('examples', contentType, () => {
            pushHeader('Content-Type', contentType, response, this);
          });
        } else {
          pushHeader('Content-Type', contentType, response, this, 'produces-content-type');
        }
      }

      const isJsonResponse = isJsonContentType(contentType);

      if (responseValue.headers) {
        this.updateHeaders(response, responseValue.headers);
      }

      this.withPath('examples', () => {
        // Responses can have bodies
        if (responseBody !== undefined) {
          this.withPath(contentType, () => {
            let formattedResponseBody = responseBody;

            if (typeof responseBody !== 'string') {
              formattedResponseBody = JSON.stringify(responseBody, null, 2);
            }

            const bodyAsset = new Asset(formattedResponseBody);
            bodyAsset.classes.push('messageBody');

            if (this.generateSourceMap) {
              this.createSourceMap(bodyAsset, this.path);
            }

            response.content.push(bodyAsset);
          });
        }

        // Responses can have schemas in Swagger
        const exampleSchema = responseValue.examples && responseValue.examples.schema;
        const schema = responseValue.schema || exampleSchema;

        if (schema && schema.format !== 'binary') {
          let args;

          if (responseValue.examples && responseValue.examples.schema) {
            args = [5, 'examples', 'schema'];
          } else {
            args = [5, 'schema'];
          }

          this.withSlicedPath(...args.concat([() => {
            this.pushAssets(schema, response, contentType,
                            isJsonResponse && responseBody === undefined);
          }]));
        }

        if (statusCode !== 'null') {
          response.statusCode = statusCode;

          if (this.generateSourceMap) {
            this.createSourceMap(response.attributes.get('statusCode'), this.path.slice(0, -1));
          }
        }
      });


      this.handleSwaggerVendorExtensions(response, responseValue);

      return response;
    });
  }

  // Takes in an `payload` element and a list of Swagger headers. Adds
  // the Swagger headers to the headers element in the payload
  updateHeaders(payload, httpHeaders) {
    _.forEach(_.keys(httpHeaders), (headerName) => {
      if (Object.prototype.hasOwnProperty.call(httpHeaders, headerName)) {
        // eslint-disable-next-line no-loop-func
        this.withPath('headers', headerName, () => {
          pushHeaderObject(headerName, httpHeaders[headerName], payload, this);
        });
      }
    });
  }

  // Test whether tags can be treated as resource groups, and if so it sets a
  // group name for each resource (used later to create groups).
  useResourceGroups() {
    const tags = [];

    if (this.swagger.paths) {
      _.forEach(this.swagger.paths, (path) => {
        let tag = null;

        if (path) {
          const operations = _.omitBy(path, isExtension);

          // eslint-disable-next-line consistent-return
          _.forEach(operations, (operation) => {
            if (operation.tags && operation.tags.length) {
              if (operation.tags.length > 1) {
                // Too many tags... each resource can only be in one group!
                return false;
              }

              if (tag === null) {
                [tag] = operation.tags;
              } else if (tag !== operation.tags[0]) {
                // Non-matching tags... can't have a resource in multiple groups!
                return false;
              }
            }
          });
        }

        if (tag) {
          // eslint-disable-next-line no-param-reassign
          path['x-group-name'] = tag;
          tags.push(tag);
        }
      });
    }

    return tags.length > 0;
  }

  // Update the current group by either selecting or creating it.
  updateResourceGroup(name) {
    const { Category, Copy } = this.minim.elements;

    if (name) {
      this.group = this.api.find(el => el.element === 'category' && el.classes.contains('resourceGroup') && el.title.toValue() === name).first;

      if (!this.group) {
        // TODO: Source maps for these groups. The problem is that the location
        // may not always make sense. Do we point to the tag description,
        // the resource, or the transition?
        this.group = new Category();
        this.group.title = name;
        this.group.classes.push('resourceGroup');

        if (this.swagger.tags && _.isArray(this.swagger.tags)) {
          _.forEach(this.swagger.tags, (tag) => {
            if (tag.name === name && tag.description) {
              this.group.content.push(new Copy(tag.description));
            }

            this.handleExternalDocs(this.group, tag.externalDocs);
          });
        }

        this.api.content.push(this.group);
      }
    }
  }

  /* eslint-disable class-methods-use-this */
  schemaForParameterValue(parameter) {
    const schema = {
      type: parameter.type,
    };

    if (schema.type === 'integer') {
      schema.type = 'number';
    }

    if (parameter.items) {
      schema.items = parameter.items;
    }

    return schema;
  }

  typeForParameter(parameter) {
    const {
      Array: ArrayElement, Boolean: BooleanElement, Number: NumberElement,
      String: StringElement,
    } = this.minim.elements;

    const types = {
      string: StringElement,
      number: NumberElement,
      integer: NumberElement,
      boolean: BooleanElement,
      array: ArrayElement,
      file: StringElement,
    };

    return types[parameter.type];
  }

  convertValueToElement(value, schema) {
    const validator = new ZSchema();
    let element;

    if (schema.type === 'file') {
      // files don't have types
      return this.minim.toElement(value);
    }

    if (validator.validate(value, schema)) {
      element = this.minim.toElement(value);

      if (this.generateSourceMap) {
        this.createSourceMap(element, this.path);
      }
    } else {
      validator.getLastError().details.forEach((detail) => {
        this.createAnnotation(annotations.VALIDATION_WARNING, this.path, detail.message);
      });

      // Coerce parameter to correct type
      if (schema.type === 'string') {
        if (typeof value === 'number' || typeof value === 'boolean') {
          element = new this.minim.elements.String(String(value));
        }
      }
    }

    return element;
  }

  // Convert a Swagger parameter into a Refract element.
  convertParameterToElement(parameter, setAttributes = false) {
    const { Array: ArrayElement, Enum: EnumElement } = this.minim.elements;

    const Type = this.typeForParameter(parameter);
    const schema = this.schemaForParameterValue(parameter);

    let element = new Type();

    if (parameter['x-example'] !== undefined) {
      this.withPath('x-example', () => {
        const value = this.convertValueToElement(parameter['x-example'], schema);

        if (value) {
          if (parameter.enum) {
            value.attributes.set('typeAttributes', ['fixed']);
          }

          element = value;
        }
      });
    }

    if (parameter.enum) {
      const enumerations = new ArrayElement();

      parameter.enum.forEach((value, index) => {
        this.withPath('enum', index, () => {
          const enumeration = this.convertValueToElement(value, schema);

          if (enumeration) {
            enumeration.attributes.set('typeAttributes', ['fixed']);
            enumerations.push(enumeration);
          }
        });
      });


      if (enumerations.length > 0) {
        // We should only wrap the existing element in an enumeration
        // iff there was valid enumeations. When there is enuerations
        // and they are all invalid, let's discard the enumeration.
        // The user already got a warning about it which is
        // raised from `convertValueToElement`.

        if (element.toValue()) {
          element = new EnumElement(element);
        } else {
          element = new EnumElement();
        }

        element.enumerations = enumerations;
      }
    }

    if (parameter.default) {
      this.withPath('default', () => {
        let value = this.convertValueToElement(parameter.default, schema);

        if (value) {
          if (parameter.enum) {
            value.attributes.set('typeAttributes', ['fixed']);
            value = new EnumElement(value);
          }

          element.attributes.set('default', value);
        }
      });
    }

    if (parameter.type === 'array' && parameter.items && parameter.items.type && element.content.length === 0) {
      this.withPath('items', () => {
        element.content = [this.convertParameterToElement(parameter.items, true)];
      });
    }

    if (this.generateSourceMap) {
      this.createSourceMap(element, this.path);
    }

    if (setAttributes) {
      if (parameter.description) {
        element.description = parameter.description;

        if (this.generateSourceMap) {
          this.createSourceMap(element.meta.get('description'), this.path.concat(['description']));
        }
      }

      if (parameter.required) {
        element.attributes.set('typeAttributes', ['required']);
      }
    }

    return element;
  }

  // Convert a Swagger parameter into a Refract member element for use in an
  // object element (or subclass).
  convertParameterToMember(parameter) {
    const MemberElement = this.minim.getElementClass('member');
    const memberValue = this.convertParameterToElement(parameter);
    const member = new MemberElement(parameter.name, memberValue);

    if (this.generateSourceMap) {
      this.createSourceMap(member, this.path);
    }

    if (parameter.description) {
      member.description = parameter.description;

      if (this.generateSourceMap) {
        this.createSourceMap(member.meta.get('description'), this.path.concat(['description']));
      }
    }

    if (parameter.required) {
      member.attributes.set('typeAttributes', ['required']);
    }

    return member;
  }

  // Make a new source map for the given element
  createSourceMap(element, path, produceLineColumnAttributes) {
    if (this.ast) {
      const NumberElement = this.minim.elements.Number;
      const SourceMap = this.minim.getElementClass('sourceMap');
      const position = this.ast.getPosition(path);

      // eslint-disable-next-line no-restricted-globals
      if (position && position.start && position.end &&
          !isNaN(position.start.pointer) && !isNaN(position.end.pointer)) {
        const start = new NumberElement(position.start.pointer);
        const end = new NumberElement(position.end.pointer - position.start.pointer);

        if (produceLineColumnAttributes) {
          start.attributes.set('line', position.start.line);
          start.attributes.set('column', position.start.column);
          end.attributes.set('line', position.end.line);
          end.attributes.set('column', position.end.column);
        }

        element.attributes.set('sourceMap', [new SourceMap([[start, end]])]);
      }
    }
  }

  // Make a new annotation for the given path and message
  createAnnotation(info, path, message) {
    const { Annotation } = this.minim.elements;

    const annotation = new Annotation(message);
    annotation.classes.push(info.type);
    annotation.code = info.code;

    this.result.content.push(annotation);

    if (info.fragment) {
      origin(info.fragment, annotation, this);
    }

    if (path && this.ast) {
      this.createSourceMap(annotation, path, true);
    }

    return annotation;
  }

  // Create a new HrefVariables element from a parameter list. Returns either
  // the new HrefVariables element or `undefined`.
  createHrefVariables(params) {
    const { HrefVariables } = this.minim.elements;
    const hrefVariables = new HrefVariables();

    _.forEach(params, (parameter, index) => {
      this.withPath('parameters', index, () => {
        let member;
        const format = parameter.collectionFormat || 'csv';

        // Adding a warning if format is not supported
        if (!['multi', 'csv'].includes(format)) {
          this.createAnnotation(
            annotations.DATA_LOST, this.path,
            `Parameters of collection format '${format}' are not supported`,
          );
        }

        if (parameter.in === 'query' || parameter.in === 'path') {
          member = this.convertParameterToMember(parameter);
          hrefVariables.content.push(member);
        }

        return member;
      });
    });

    return hrefVariables.length ? hrefVariables : undefined;
  }

  pushAssets(schema, payload, contentType, pushBody) {
    let jsonSchema;
    const referencedPathValue = this.referencedPathValue();

    try {
      const root = { definitions: this.definitions };
      jsonSchema = convertSchema(referencedPathValue || schema, root,
                                 this.referencedSwagger);
    } catch (error) {
      this.createAnnotation(annotations.VALIDATION_ERROR, this.path, error.message);
      return;
    }

    if (pushBody) {
      bodyFromSchema(jsonSchema, payload, this, contentType);
    }

    this.pushSchemaAsset(schema, jsonSchema, payload, this.path);
    this.pushDataStructureAsset(referencedPathValue || schema, payload);
  }

  // Create a Refract asset element containing JSON Schema and push into payload
  pushSchemaAsset(schema, jsonSchema, payload, path) {
    const Asset = this.minim.getElementClass('asset');
    const schemaAsset = new Asset(JSON.stringify(jsonSchema));

    schemaAsset.classes.push('messageBodySchema');
    schemaAsset.contentType = 'application/schema+json';

    if (this.generateSourceMap) {
      this.createSourceMap(schemaAsset, path);
    }

    this.handleExternalDocs(schemaAsset, schema.externalDocs);

    payload.content.push(schemaAsset);
  }

  /** Retrieves the value of the current path in the original Swagger document (referenced document)
   */
  referencedPathValue() {
    let value = this.referencedSwagger;

    this.path.forEach((path) => {
      if (value) {
        value = value[path];
      } else {
        value = null;
      }
    });

    return value;
  }

  pushDataStructureAsset(schema, payload) {
    try {
      const generator = new DataStructureGenerator(this.minim, this.referencedSwagger);
      const dataStructure = generator.generateDataStructure(schema);
      if (dataStructure) {
        payload.content.push(dataStructure);
      }
    } catch (exception) {
      // TODO: Expose errors once feature is more-complete
    }
  }

  // Create a new Refract transition element with a blank request and response.
  createTransaction(transition, method, schemes) {
    const { HttpRequest, HttpResponse, HttpTransaction } = this.minim.elements;
    const transaction = new HttpTransaction();
    transaction.content = [new HttpRequest(), new HttpResponse()];

    if (transition) {
      transition.content.push(transaction);
    }

    if (method) {
      transaction.request.method = method.toUpperCase();

      if (this.generateSourceMap) {
        this.createSourceMap(transaction.request.attributes.get('method'), this.path);
      }
    }

    if (schemes.length) {
      transaction.attributes.set('authSchemes', schemes.map(scheme => scheme.clone()));
    }

    return transaction;
  }

  validateProduces(produces) {
    if (produces) {
      this.withPath('produces', () => {
        this.validateContentTypes(produces);
      });
    }
  }

  validateConsumes(consumes) {
    if (consumes) {
      this.withPath('consumes', () => {
        this.validateContentTypes(consumes);
      });
    }
  }

  validateContentTypes(contentTypes) {
    contentTypes.forEach((contentType) => {
      try {
        const { type } = contentTypeModule.parse(contentType);
        mediaTyper.parse(type);
      } catch (e) {
        const index = contentTypes.indexOf(contentType);
        this.withPath(index, () => {
          this.createAnnotation(
            annotations.VALIDATION_WARNING, this.path,
            `Invalid content type '${contentType}', ${e.message}`,
          );
        });
      }
    });
  }
}