VYPR
High severityNVD Advisory· Published Sep 1, 2020· Updated Sep 11, 2023

fury-adapter-swagger allows arbitrary file read from system

CVE-2016-1000249

Description

fury-adapter-swagger from version 0.2.0 until version 0.9.7 has a weakness that allows an attacker to read arbitrary files off of the system. This can be used to read sensitive data, or to cause a denial of service condition by attempting to read something like /dev/zero.

Proof of

Concept:

---
swagger: '2.0'
info:
  title: Read local files
  version: '1.0'

paths:
  /foo:
    get:
      responses:
        200:
          description: Some description
          examples:
            text/html:
              example:
                $ref: '/etc/passwd'

Recommendation

Upgrade to version 0.9.7 or later.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
fury-adapter-swaggernpm
>= 0.2.0, < 0.9.70.9.7

Patches

2
777e2d68f035

fix(dereferencing): Prevent dereferencing external assets (#89)

5 files changed · +128 2
  • CHANGELOG.md+6 0 modified
    @@ -1,3 +1,9 @@
    +# 0.9.7 - 2016-10-25
    +
    +## Bug Fixes
    +
    +- Prevents dereferencing external assets such as local files.
    +
     # 0.9.5 - 2016-09-01
     
     ## Bug Fixes
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "fury-adapter-swagger",
    -  "version": "0.9.6",
    +  "version": "0.9.7",
       "description": "Swagger 2.0 parser for Fury.js",
       "main": "./lib/adapter.js",
       "tonicExampleFilename": "tonic-example.js",
    
  • src/parser.js+6 1 modified
    @@ -90,7 +90,12 @@ export default class Parser {
     
         // Next, we dereference and validate the loaded Swagger object. Any schema
         // violations get converted into annotations with source maps.
    -    swaggerParser.validate(loaded, (err) => {
    +    const swaggerOptions = {
    +      '$refs': {
    +        external: false,
    +      },
    +    };
    +    swaggerParser.validate(loaded, swaggerOptions, (err) => {
           const swagger = swaggerParser.api;
           this.swagger = swaggerParser.api;
     
    
  • test/fixtures/external-dereferencing.json+101 0 added
    @@ -0,0 +1,101 @@
    +{
    +  "element": "parseResult",
    +  "meta": {},
    +  "attributes": {},
    +  "content": [
    +    {
    +      "element": "category",
    +      "meta": {
    +        "classes": [
    +          "api"
    +        ],
    +        "title": "Dereferencing a local file"
    +      },
    +      "attributes": {},
    +      "content": [
    +        {
    +          "element": "resource",
    +          "meta": {},
    +          "attributes": {
    +            "href": "/"
    +          },
    +          "content": [
    +            {
    +              "element": "transition",
    +              "meta": {},
    +              "attributes": {},
    +              "content": [
    +                {
    +                  "element": "httpTransaction",
    +                  "meta": {},
    +                  "attributes": {},
    +                  "content": [
    +                    {
    +                      "element": "httpRequest",
    +                      "meta": {},
    +                      "attributes": {
    +                        "method": "GET"
    +                      },
    +                      "content": []
    +                    },
    +                    {
    +                      "element": "httpResponse",
    +                      "meta": {},
    +                      "attributes": {
    +                        "headers": {
    +                          "element": "httpHeaders",
    +                          "meta": {},
    +                          "attributes": {},
    +                          "content": [
    +                            {
    +                              "element": "member",
    +                              "meta": {},
    +                              "attributes": {},
    +                              "content": {
    +                                "key": {
    +                                  "element": "string",
    +                                  "meta": {},
    +                                  "attributes": {},
    +                                  "content": "Content-Type"
    +                                },
    +                                "value": {
    +                                  "element": "string",
    +                                  "meta": {},
    +                                  "attributes": {},
    +                                  "content": "application/json"
    +                                }
    +                              }
    +                            }
    +                          ]
    +                        },
    +                        "statusCode": "200"
    +                      },
    +                      "content": [
    +                        {
    +                          "element": "copy",
    +                          "meta": {},
    +                          "attributes": {},
    +                          "content": "dereference package.json"
    +                        },
    +                        {
    +                          "element": "asset",
    +                          "meta": {
    +                            "classes": [
    +                              "messageBody"
    +                            ]
    +                          },
    +                          "attributes": {},
    +                          "content": "{\n  \"example\": {\n    \"$ref\": \"package.json\"\n  }\n}"
    +                        }
    +                      ]
    +                    }
    +                  ]
    +                }
    +              ]
    +            }
    +          ]
    +        }
    +      ]
    +    }
    +  ]
    +}
    \ No newline at end of file
    
  • test/fixtures/external-dereferencing.yaml+14 0 added
    @@ -0,0 +1,14 @@
    +swagger: "2.0"
    +info:
    +  title: Dereferencing a local file
    +  version: v2
    +paths:
    +  /:
    +    get:
    +      responses:
    +        200:
    +          description: dereference package.json
    +          examples:
    +            application/json:
    +              example:
    +                $ref: 'package.json'
    
f4407e3a5323

Refactor code

https://github.com/apiaryio/fury-adapter-swaggerDaniel G. TaylorDec 11, 2015via ghsa
8 files changed · +924 798
  • Changelog.md+11 1 modified
    @@ -1,6 +1,16 @@
    +# Unreleased
    +
    +- Code refactoring
    +
    +  - Parsing is now handled by a class to more easily share state between various methods. This lays the framework for more refactoring and code cleanup.
    +  - The parser shared state tracks the path of the component currently being parsed, which makes it easier to split pieces of the parser into separate methods.
    +  - YAML AST lookup paths are now arrays of strings rather than strings of period-separated components, which made escaping very difficult.
    +  - Rather than guessing the YAML type, the AST lookup now checks and handles it accordingly. Paths like `'foo.bar.0.baz'` would fail previously and now work (but are passed as `['foo', 'bar', '0', 'baz']`).
    +  - Underscore is now replaced with Lodash for consistency with other modules.
    +
     # 0.6.0 - 2015-12-14
     
    -- Invalid YAML now returns error
    +  - Invalid YAML now returns error
     
     # 0.5.3 - 2015-12-11
     
    
  • package.json+1 1 modified
    @@ -19,8 +19,8 @@
       "dependencies": {
         "babel-runtime": "^5.8.20",
         "js-yaml": "^3.4.2",
    +    "lodash": "^3.10.1",
         "swagger-parser": "^3.3.0",
    -    "underscore": "^1.8.3",
         "yaml-js": "^0.1.3"
       },
       "devDependencies": {
    
  • src/adapter.js+5 791 modified
    @@ -1,34 +1,5 @@
    -import _ from 'underscore';
    -import buildUriTemplate from './uri-template';
    -import SwaggerParser from 'swagger-parser';
    -import yaml from 'js-yaml';
    -import yamlAst from 'yaml-js';
    -
    -// These describe the type of annotations that are produced by this parser
    -// and assigns a unique code to each one. Downstream applications can use this
    -// code to group similar types of annotations together.
    -const ANNOTATIONS = {
    -  CANNOT_PARSE: {
    -    type: 'error',
    -    code: 1,
    -    fragment: 'yaml-parser',
    -  },
    -  AST_UNAVAILABLE: {
    -    type: 'warning',
    -    code: 2,
    -    fragment: 'yaml-parser',
    -  },
    -  DATA_LOST: {
    -    type: 'warning',
    -    code: 3,
    -    fragment: 'refract-not-supported',
    -  },
    -  VALIDATION_ERROR: {
    -    type: 'warning',
    -    code: 4,
    -    fragment: 'swagger-validation',
    -  },
    -};
    +import _ from 'lodash';
    +import Parser from './parser';
     
     export const name = 'swagger';
     
    @@ -44,769 +15,12 @@ export function detect(source) {
         : source.swagger === '2.0');
     }
     
    -// Test whether a key is a special Swagger extension.
    -function isExtension(value, key) {
    -  return key.indexOf('x-') === 0;
    -}
    -
    -// 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).
    -function useResourceGroups(api) {
    -  const tags = [];
    -
    -  if (api.paths) {
    -    _.each(api.paths, (path) => {
    -      let tag = null;
    -
    -      if (path) {
    -        _.each(path, (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[0];
    -            } else if (tag !== operation.tags[0]) {
    -              // Non-matching tags... can't have a resource in multiple groups!
    -              return false;
    -            }
    -          }
    -        });
    -      }
    -
    -      if (tag) {
    -        path['x-group-name'] = tag;
    -        tags.push(tag);
    -      }
    -    });
    -  }
    -
    -  return tags.length > 0;
    -}
    -
    -// Look up a position in the original source based on a JSON path, for
    -// example 'paths./test.get.responses.200'
    -function getPosition(ast, path) {
    -  const pieces = _.isArray(path) ? path.splice(0) : path.split('.');
    -  let end;
    -  let node = ast;
    -  let piece = pieces.shift();
    -  let start;
    -
    -  while (piece) {
    -    let newNode = null;
    -    let index = null;
    -
    -    // If a piece ends with an array index, then we need to make sure we fetch
    -    // that specific item from the value array.
    -    const match = piece.match(/(.*)\[([0-9])+\]$/);
    -    if (match) {
    -      piece = match[1];
    -      index = parseInt(match[2], 10);
    -    }
    -
    -    for (const subNode of node.value) {
    -      if (subNode[0] && subNode[0].value === piece) {
    -        if (pieces.length) {
    -          newNode = subNode[1];
    -          if (index !== null) {
    -            newNode = newNode.value[index];
    -          }
    -        } else {
    -          // This is the last item!
    -          if (index !== null) {
    -            newNode = subNode[1].value[index];
    -            start = newNode.start_mark.pointer;
    -            end = newNode.end_mark.pointer;
    -          } else {
    -            newNode = subNode[0];
    -            start = subNode[0].start_mark.pointer;
    -            end = subNode[1].end_mark.pointer;
    -          }
    -        }
    -        break;
    -      } else if (subNode[0] && subNode[0].value === '$ref') {
    -        if (subNode[1].value.indexOf('#') === 0) {
    -          // This is an internal reference! First, we reset the node to the
    -          // root of the document, shift the ref item off the pieces stack
    -          // and then add the referenced path to the pieces.
    -          const refPaths = subNode[1].value.substr(2).split('/');
    -          newNode = ast;
    -          Array.prototype.unshift.apply(pieces, refPaths.concat([piece]));
    -          break;
    -        } else {
    -          console.log(`External reference ${subNode[1].value} not supported for source maps!`);
    -        }
    -      }
    -    }
    -
    -    if (newNode) {
    -      node = newNode;
    -    } else {
    -      return null;
    -    }
    -
    -    piece = pieces.shift();
    -  }
    -
    -  return {start, end};
    -}
    -
    -// Make a new source map for the given element
    -function makeSourceMap(SourceMap, ast, element, path) {
    -  const position = getPosition(ast, path);
    -  if (position) {
    -    element.attributes.set('sourceMap', [
    -      new SourceMap([[position.start, position.end - position.start]]),
    -    ]);
    -  }
    -}
    -
    -// Make a new annotation for the given path and message
    -function makeAnnotation(Annotation, Link, SourceMap, ast, result, info, path, message) {
    -  const annotation = new Annotation(message);
    -  annotation.classes.push(info.type);
    -  annotation.code = info.code;
    -  result.content.push(annotation);
    -
    -  if (info.fragment) {
    -    const link = new Link();
    -    link.relation = 'origin';
    -    link.href = `http://docs.apiary.io/validations/swagger#${info.fragment}`;
    -    annotation.links.push(link);
    -  }
    -
    -  if (ast && path) {
    -    const position = getPosition(ast, path);
    -    if (position && !isNaN(position.start) && !isNaN(position.end)) {
    -      annotation.attributes.set('sourceMap', [
    -        new SourceMap([[position.start, position.end - position.start]]),
    -      ]);
    -    }
    -  }
    -}
    -
    -function convertParameterToElement(minim, generateSourceMap, setupSourceMap, parameter, path, setAttributes = false) {
    -  const StringElement = minim.getElementClass('string');
    -  const NumberElement = minim.getElementClass('number');
    -  const BooleanElement = minim.getElementClass('boolean');
    -  const ArrayElement = minim.getElementClass('array');
    -
    -  let element;
    -
    -  // Convert from Swagger types to Minim elements
    -  if (parameter.type === 'string') {
    -    element = new StringElement('');
    -  } else if (parameter.type === 'integer' || parameter.type === 'number') {
    -    element = new NumberElement();
    -  } else if (parameter.type === 'boolean') {
    -    element = new BooleanElement();
    -  } else if (parameter.type === 'array') {
    -    element = new ArrayElement();
    -
    -    if (parameter.items) {
    -      element.content = [convertParameterToElement(minim, generateSourceMap,
    -        setupSourceMap, parameter.items, `${path}.items`, true)];
    -    }
    -  } else {
    -    // Default to a string in case we get a type we haven't seen
    -    element = new StringElement('');
    -  }
    -
    -  if (generateSourceMap) {
    -    setupSourceMap(element, path);
    -  }
    -
    -  if (setAttributes) {
    -    if (parameter.description) {
    -      element.description = parameter.description;
    -
    -      if (generateSourceMap) {
    -        setupSourceMap(element.meta.get('description'), `${path}.description`);
    -      }
    -    }
    -
    -    if (parameter.required) {
    -      element.attributes.set('typeAttributes', ['required']);
    -    }
    -
    -    if (parameter.default !== undefined) {
    -      element.attributes.set('default', parameter.default);
    -    }
    -  }
    -
    -  return element;
    -}
    -
    -function convertParameterToMember(minim, generateSourceMap, setupSourceMap, parameter, path) {
    -  const MemberElement = minim.getElementClass('member');
    -  const memberValue = convertParameterToElement(minim, generateSourceMap,
    -    setupSourceMap, parameter, path);
    -
    -  // TODO: Update when Minim has better support for elements as values
    -  // should be: new MemberType(parameter.name, memberValue);
    -  const member = new MemberElement(parameter.name);
    -  member.content.value = memberValue;
    -
    -  if (generateSourceMap) {
    -    setupSourceMap(member, path);
    -  }
    -
    -  if (parameter.description) {
    -    member.description = parameter.description;
    -
    -    if (generateSourceMap) {
    -      setupSourceMap(member.meta.get('description'), `${path}.description`);
    -    }
    -  }
    -
    -  if (parameter.required) {
    -    member.attributes.set('typeAttributes', ['required']);
    -  }
    -
    -  // If there is a default, it is set on the member value instead of the member
    -  // element itself because the default value applies to the value.
    -  if (parameter.default) {
    -    memberValue.attributes.set('default', parameter.default);
    -  }
    -
    -  return member;
    -}
    -
    -function createAssetFromJsonSchema(minim, jsonSchema) {
    -  const Asset = minim.getElementClass('asset');
    -  const schemaAsset = new Asset(JSON.stringify(jsonSchema));
    -  schemaAsset.classes.push('messageBodySchema');
    -  schemaAsset.attributes.set('contentType', 'application/schema+json');
    -
    -  return schemaAsset;
    -}
    -
    -function createTransaction(minim, transition, method) {
    -  const HttpTransaction = minim.getElementClass('httpTransaction');
    -  const HttpRequest = minim.getElementClass('httpRequest');
    -  const HttpResponse = minim.getElementClass('httpResponse');
    -  const transaction = new HttpTransaction();
    -  transaction.content = [new HttpRequest(), new HttpResponse()];
    -
    -  if (transition) {
    -    transition.content.push(transaction);
    -  }
    -
    -  if (method) {
    -    transaction.request.attributes.set('method', method.toUpperCase());
    -  }
    -
    -  return transaction;
    -}
    -
     /*
      * Parse Swagger 2.0 into Refract elements
      */
    -export function parse({minim, source, generateSourceMap}, done) {
    -  // TODO: Will refactor this once API Description namespace is stable
    -  // Leaving as large block of code until then
    -  const Annotation = minim.getElementClass('annotation');
    -  const Asset = minim.getElementClass('asset');
    -  const Copy = minim.getElementClass('copy');
    -  const Category = minim.getElementClass('category');
    -  const DataStructure = minim.getElementClass('dataStructure');
    -  const HrefVariables = minim.getElementClass('hrefVariables');
    -  const HttpHeaders = minim.getElementClass('httpHeaders');
    -  const Link = minim.getElementClass('link');
    -  const MemberElement = minim.getElementClass('member');
    -  const ObjectElement = minim.getElementClass('object');
    -  const ParseResult = minim.getElementClass('parseResult');
    -  const Resource = minim.getElementClass('resource');
    -  const SourceMap = minim.getElementClass('sourceMap');
    -  const Transition = minim.getElementClass('transition');
    -
    -  const parser = new SwaggerParser();
    -  const parseResult = new ParseResult();
    -
    -  let loaded;
    -  try {
    -    loaded = _.isString(source) ? yaml.safeLoad(source) : source;
    -  } catch (err) {
    -    makeAnnotation(Annotation, Link, SourceMap, null, parseResult,
    -      ANNOTATIONS.CANNOT_PARSE, null, (err.reason || 'Problem loading the input'));
    -
    -    if (err.mark) {
    -      parseResult.first().attributes.set('sourceMap', [
    -        new SourceMap([[err.mark.position, 1]]),
    -      ]);
    -    }
    -
    -    return done(new Error(err.message), parseResult);
    -  }
    -
    -  let ast = null;
    -  if (_.isString(source)) {
    -    // TODO: Could we lazy-load the AST here? Seems like a waste of time if
    -    //       we load it but don't wind up using it.
    -    try {
    -      ast = yamlAst.compose(source);
    -    } catch (err) {
    -      makeAnnotation(Annotation, Link, SourceMap, null, parseResult,
    -        ANNOTATIONS.AST_UNAVAILABLE, null,
    -        'Input AST could not be composed, so source maps will not be available');
    -    }
    -  } else {
    -    makeAnnotation(Annotation, Link, SourceMap, null, parseResult,
    -      ANNOTATIONS.AST_UNAVAILABLE, null,
    -      'Source maps are only available with string input');
    -  }
    -
    -  // Some sane defaults since these are sometimes left out completely
    -  if (loaded.info === undefined) {
    -    loaded.info = {};
    -  }
    -
    -  if (loaded.paths === undefined) {
    -    loaded.paths = {};
    -  }
    -
    -  // Parse and validate the Swagger document!
    -  parser.validate(loaded, (err) => {
    -    const swagger = parser.api;
    -
    -    if (err) {
    -      if (swagger === undefined) {
    -        return done(err, parseResult);
    -      }
    -
    -      // 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) {
    -          for (const item of queue[0]) {
    -            makeAnnotation(Annotation, Link, SourceMap, ast, parseResult,
    -              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();
    -        }
    -      }
    -    }
    -
    -    const basePath = (swagger.basePath || '').replace(/[/]+$/, '');
    -    const setupSourceMap = makeSourceMap.bind(makeSourceMap, SourceMap, ast);
    -    const setupAnnotation = makeAnnotation.bind(makeAnnotation, Annotation,
    -      Link, SourceMap, ast, parseResult);
    -    const paramToElement = convertParameterToMember.bind(
    -      convertParameterToElement, minim, generateSourceMap, setupSourceMap);
    -
    -    const api = new Category();
    -    parseResult.push(api);
    -
    -    // Root API Element
    -    api.classes.push('api');
    -
    -    if (swagger.info) {
    -      if (swagger.info.title) {
    -        api.meta.set('title', swagger.info.title);
    -
    -        if (generateSourceMap && ast) {
    -          setupSourceMap(api.meta.get('title'), 'info.title');
    -        }
    -      }
    -
    -      if (swagger.info.description) {
    -        api.content.push(new Copy(swagger.info.description));
    -
    -        if (generateSourceMap && ast) {
    -          setupSourceMap(api.content[api.content.length - 1], 'info.description');
    -        }
    -      }
    -    }
    -
    -    if (swagger.host) {
    -      let hostname = swagger.host;
    -
    -      if (swagger.schemes) {
    -        if (swagger.schemes.length > 1) {
    -          setupAnnotation(ANNOTATIONS.DATA_LOST, 'schemes',
    -            'Only the first scheme will be used to create a hostname');
    -        }
    -
    -        hostname = `${swagger.schemes[0]}://${hostname}`;
    -      }
    -
    -      api.attributes.set('meta', {});
    -      const meta = api.attributes.get('meta');
    -      const member = new MemberElement('HOST', hostname);
    -      member.meta.set('classes', ['user']);
    -
    -      if (generateSourceMap && ast) {
    -        setupSourceMap(member, 'host');
    -      }
    -
    -      meta.content.push(member);
    -    }
    -
    -    if (swagger.securityDefinitions) {
    -      setupAnnotation(ANNOTATIONS.DATA_LOST, 'securityDefinitions',
    -        'Authentication information is not yet supported');
    -    }
    -
    -    if (swagger.security) {
    -      setupAnnotation(ANNOTATIONS.DATA_LOST, 'security',
    -        'Authentication information is not yet supported');
    -    }
    -
    -    if (swagger.externalDocs) {
    -      setupAnnotation(ANNOTATIONS.DATA_LOST, 'externalDocs',
    -        'External documentation is not yet supported');
    -    }
    -
    -    const useGroups = useResourceGroups(swagger);
    -    let group = api;
    -
    -    // Swagger has a paths object to loop through
    -    // The key is the href
    -    _.each(_.omit(swagger.paths, isExtension), (pathValue, href) => {
    -      const resource = new Resource();
    -
    -      if (generateSourceMap && ast) {
    -        setupSourceMap(resource, `paths.${href}`);
    -      }
    -
    -      // Provide users with a way to add a title to a resource in Swagger
    -      if (pathValue['x-summary']) {
    -        resource.title = pathValue['x-summary'];
    -      }
    -
    -      // Provide users a way to add a description to a resource in Swagger
    -      if (pathValue['x-description']) {
    -        const resourceDescription = new Copy(pathValue['x-description']);
    -        resource.push(resourceDescription);
    -      }
    -
    -      if (useGroups) {
    -        const groupName = pathValue['x-group-name'];
    -
    -        if (groupName) {
    -          group = api.find((el) => el.element === 'category' && el.classes.contains('resourceGroup') && el.title === groupName).first();
    -
    -          if (!group) {
    -            group = new Category();
    -            group.title = groupName;
    -            group.classes.push('resourceGroup');
    -
    -            if (swagger.tags && swagger.tags.forEach) {
    -              swagger.tags.forEach((tag) => {
    -                // TODO: Check for external docs here?
    -                if (tag.name === groupName && tag.description) {
    -                  group.content.push(new Copy(tag.description));
    -                }
    -              });
    -            }
    -
    -            api.content.push(group);
    -          }
    -        }
    -      }
    -
    -      group.content.push(resource);
    -
    -      const pathObjectParameters = pathValue.parameters || [];
    -
    -      // TODO: Currently this only supports URI parameters for `path` and `query`.
    -      // It should add support for `body` parameters as well.
    -      if (pathObjectParameters.length > 0) {
    -        resource.hrefVariables = new HrefVariables();
    -
    -        pathObjectParameters.forEach((parameter, index) => {
    -          if (parameter.in === 'query' || parameter.in === 'path') {
    -            const paramPath = `paths.${href}.parameters[${index}]`;
    -            const member = paramToElement(parameter, paramPath);
    -            if (generateSourceMap && ast) {
    -              setupSourceMap(member, paramPath);
    -            }
    -            resource.hrefVariables.content.push(member);
    -          } else if (parameter.in === 'body') {
    -            setupAnnotation(ANNOTATIONS.DATA_LOST,
    -              `paths.${href}.parameters[${index}]`,
    -              'Path-level body parameters are not yet supported');
    -          }
    -        });
    -      }
    -
    -      // TODO: Handle parameters on a resource level
    -      // See https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#path-item-object
    -      const relevantPaths = _.chain(pathValue)
    -        .omit('parameters', '$ref')
    -        .omit(isExtension)
    -        .value();
    -
    -      // Each path is an object with methods as properties
    -      _.each(relevantPaths, (methodValue, method) => {
    -        const transition = new Transition();
    -        resource.content.push(transition);
    -
    -        if (generateSourceMap && ast) {
    -          setupSourceMap(transition, `paths.${href}.${method}`);
    -        }
    -
    -        if (methodValue.externalDocs) {
    -          setupAnnotation(ANNOTATIONS.DATA_LOST,
    -            `paths.${href}.${method}.externalDocs`,
    -            'External documentation is not yet supported');
    -        }
    -
    -        const methodValueParameters = methodValue.parameters || [];
    -
    -        const queryParameters = methodValueParameters.filter((parameter) => {
    -          return parameter.in === 'query';
    -        });
    -
    -        // URI parameters are for query and path variables
    -        const uriParameters = methodValueParameters.filter((parameter) => {
    -          return parameter.in === 'query' || parameter.in === 'path';
    -        });
    -
    -        // Body parameters are ones that define JSON Schema
    -        const bodyParameters = methodValueParameters.filter((parameter) => {
    -          return parameter.in === 'body';
    -        });
    -
    -        // Form parameters are send as encoded form data in the body
    -        const formParameters = methodValueParameters.filter((parameter) => {
    -          return parameter.in === 'formData';
    -        });
    -
    -        const hrefForResource = buildUriTemplate(basePath, href, pathObjectParameters, queryParameters);
    -        resource.attributes.set('href', hrefForResource);
    -
    -        if (methodValue.summary) {
    -          transition.meta.set('title', methodValue.summary);
    -
    -          if (generateSourceMap && ast) {
    -            const title = transition.meta.get('title');
    -            setupSourceMap(title, `paths.${href}.${method}.summary`);
    -          }
    -        }
    -
    -        if (methodValue.description) {
    -          const description = new Copy(methodValue.description);
    -          transition.push(description);
    -
    -          if (generateSourceMap && ast) {
    -            setupSourceMap(description, `paths.${href}.${method}.description`);
    -          }
    -        }
    -
    -        if (methodValue.operationId) {
    -          transition.attributes.set('relation', methodValue.operationId);
    -        }
    -
    -        // For each uriParameter, create an hrefVariable
    -        if (uriParameters.length > 0) {
    -          transition.hrefVariables = new HrefVariables();
    -
    -          uriParameters.forEach((parameter) => {
    -            let paramPath;
    -            if (generateSourceMap && ast) {
    -              const index = methodValueParameters.indexOf(parameter);
    -              paramPath = `paths.${href}.${method}.parameters[${index}]`;
    -            }
    -            transition.hrefVariables.content.push(
    -              paramToElement(parameter, paramPath));
    -          });
    -        }
    -
    -        // Currently, default responses are not supported in API Description format
    -        const relevantResponses = _.chain(methodValue.responses)
    -          .omit('default')
    -          .omit(isExtension)
    -          .value();
    -
    -        if (methodValue.responses && methodValue.responses.default) {
    -          setupAnnotation(ANNOTATIONS.DATA_LOST,
    -            `paths.${href}.${method}.responses.default`,
    -            'Default response is not yet supported');
    -        }
    -
    -        if (_.keys(relevantResponses).length === 0) {
    -          if (bodyParameters.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 {
    -            createTransaction(minim, transition, method);
    -          }
    -        }
    -
    -        // Transactions are created for each response in the document
    -        _.each(relevantResponses, (responseValue, statusCode) => {
    -          let examples = {
    -            '': undefined,
    -          };
    -
    -          if (responseValue.examples) {
    -            examples = responseValue.examples;
    -          }
    -
    -          examples = _.omit(examples, 'schema');
    -
    -          _.each(examples, (responseBody, contentType) => {
    -            const transaction = createTransaction(minim, transition, method);
    -            const request = transaction.request;
    -            const response = transaction.response;
    -
    -            if (generateSourceMap && ast) {
    -              setupSourceMap(transaction,
    -                `paths.${href}.${method}.responses.${statusCode}`);
    -              setupSourceMap(request, `paths.${href}.${method}`);
    -
    -              if (statusCode) {
    -                setupSourceMap(response, `paths.${href}.${method}.responses.${statusCode}`);
    -              }
    -            }
    -
    -            if (responseValue.description) {
    -              const description = new Copy(responseValue.description);
    -              response.content.push(description);
    -              if (generateSourceMap && ast) {
    -                setupSourceMap(description, `paths.${href}.${method}.responses.${statusCode}.description`);
    -              }
    -            }
    -
    -            const headers = new HttpHeaders();
    -
    -            if (contentType) {
    -              headers.push(new MemberElement(
    -                'Content-Type', contentType
    -              ));
    -
    -              if (generateSourceMap && ast) {
    -                setupSourceMap(headers.content[headers.content.length - 1], `paths.${href}.${method}.responses.${statusCode}.examples.${contentType}`);
    -              }
    -
    -              response.headers = headers;
    -            }
    -
    -            if (responseValue.headers) {
    -              for (const headerName in responseValue.headers) {
    -                if (responseValue.headers.hasOwnProperty(headerName)) {
    -                  const header = responseValue.headers[headerName];
    -                  let value = '';
    -
    -                  // Choose the first available option
    -                  if (header.enum) {
    -                    value = header.enum[0];
    -                  }
    -
    -                  if (header.default) {
    -                    value = header.default;
    -                  }
    -
    -                  const member = new MemberElement(headerName, value);
    -
    -                  if (generateSourceMap && ast) {
    -                    setupSourceMap(member, `paths.${href}.${method}.responses.${statusCode}.headers.${headerName}`);
    -                  }
    -
    -                  if (header.description) {
    -                    member.meta.set('description', header.description);
    -
    -                    if (generateSourceMap && ast) {
    -                      setupSourceMap(member.meta.get('description'), `paths.${href}.${method}.responses.${statusCode}.headers.${headerName}.description`);
    -                    }
    -                  }
    -
    -                  headers.push(member);
    -                }
    -              }
    -
    -              response.headers = headers;
    -            }
    -
    -            // Body parameters define request schemas
    -            _.each(bodyParameters, (bodyParameter) => {
    -              const schemaAsset = createAssetFromJsonSchema(minim, bodyParameter.schema);
    -              request.content.push(schemaAsset);
    -            });
    -
    -            // Using form parameters instead of body? We will convert those to
    -            // data structures.
    -            if (formParameters.length) {
    -              const dataStructure = new DataStructure();
    -              // A form is essentially an object with key/value members
    -              const dataObject = new ObjectElement();
    -
    -              _.each(formParameters, (param) => {
    -                let paramPath;
    -                if (generateSourceMap && ast) {
    -                  const index = methodValueParameters.indexOf(param);
    -                  paramPath = `paths.${href}.${method}.parameters[${index}]`;
    -                }
    -                dataObject.content.push(paramToElement(param, paramPath));
    -              });
    -
    -              dataStructure.content = dataObject;
    -              request.content.push(dataStructure);
    -            }
    -
    -            // Responses can have bodies
    -            if (responseBody !== undefined) {
    -              let formattedResponseBody = responseBody;
    -
    -              if (typeof(responseBody) !== 'string') {
    -                formattedResponseBody = JSON.stringify(responseBody, null, 2);
    -              }
    -
    -              const bodyAsset = new Asset(formattedResponseBody);
    -              bodyAsset.classes.push('messageBody');
    -              if (generateSourceMap && ast) {
    -                setupSourceMap(bodyAsset, `paths.${href}.${method}.responses.${statusCode}.examples.${contentType}`);
    -              }
    -              response.content.push(bodyAsset);
    -            }
    -
    -            // Responses can have schemas in Swagger
    -            const schema = responseValue.schema || (responseValue.examples && responseValue.examples.schema);
    -            if (schema) {
    -              const schemaAsset = createAssetFromJsonSchema(minim, schema);
    -              if (generateSourceMap && ast) {
    -                let schemaPath = `paths.${href}.${method}.responses.${statusCode}`;
    -                if (responseValue.examples && responseValue.examples.schema) {
    -                  schemaPath += '.examples.schema';
    -                } else {
    -                  schemaPath += '.schema';
    -                }
    -                setupSourceMap(schemaAsset, schemaPath);
    -              }
    -              response.content.push(schemaAsset);
    -            }
    -
    -            // TODO: Decide what to do with request hrefs
    -            // If the URI is templated, we don't want to add it to the request
    -            // if (uriParameters.length === 0) {
    -            //   request.attributes.href = href;
    -            // }
    -
    -            if (statusCode !== 'null') {
    -              response.attributes.set('statusCode', statusCode);
    -            }
    -          });
    -        });
    -      });
    -    });
    -
    -    done(null, parseResult);
    -  });
    +export function parse(options, done) {
    +  const parser = new Parser(options);
    +  parser.parse(done);
     }
     
     export default {name, mediaTypes, detect, parse};
    
  • src/ast.js+81 0 added
    @@ -0,0 +1,81 @@
    +// A module for dealing with YAML syntax trees and looking up source map
    +// location information.
    +
    +import _ from 'lodash';
    +import yamlAst from 'yaml-js';
    +
    +export default class Ast {
    +  constructor(source) {
    +    this.root = yamlAst.compose(source);
    +  }
    +
    +  // Look up a position in the original source based on a JSON path, for
    +  // example ['paths', '/test', 'get', 'responses', '200']. Also supported
    +  // is using a string ('paths./test.get') but it does not understand any
    +  // escaping.
    +  getPosition(path) {
    +    const pieces = _.isArray(path) ? [].concat(path) : path.split('.');
    +    let end;
    +    let node = this.root;
    +    let piece = pieces.shift();
    +    let start;
    +
    +    if (!node) {
    +      return null;
    +    }
    +
    +    while (piece !== undefined) {
    +      let newNode = null;
    +
    +      if (node.tag === 'tag:yaml.org,2002:map') {
    +        // This is a may / object with key:value pairs.
    +        for (const subNode of node.value) {
    +          if (subNode[0] && subNode[0].value === piece) {
    +            newNode = subNode[1];
    +
    +            if (!pieces.length) {
    +              // This is the last item!
    +              start = subNode[0].start_mark.pointer;
    +              end = subNode[1].end_mark.pointer;
    +            }
    +            break;
    +          } else if (subNode[0] && subNode[0].value === '$ref') {
    +            if (subNode[1].value.indexOf('#') === 0) {
    +              // This is an internal reference! First, we reset the node to the
    +              // root of the document, shift the ref item off the pieces stack
    +              // and then add the referenced path to the pieces.
    +              const refPaths = subNode[1].value.substr(2).split('/');
    +              newNode = this.root;
    +              Array.prototype.unshift.apply(pieces, refPaths.concat([piece]));
    +              break;
    +            } else {
    +              // TODO: Communicate this in some other way?
    +              console.log(`External reference ${subNode[1].value} not supported for source maps!`);
    +            }
    +          }
    +        }
    +      } else if (node.tag === 'tag:yaml.org,2002:seq') {
    +        // This is a sequence, i.e. array. Access it by index.
    +        newNode = node.value[piece];
    +
    +        if (!pieces.length) {
    +          // This is the last item!
    +          start = newNode.start_mark.pointer;
    +          end = newNode.end_mark.pointer;
    +        }
    +      } else {
    +        // Unknown piece, which will just return no source map.
    +      }
    +
    +      if (newNode) {
    +        node = newNode;
    +      } else {
    +        return null;
    +      }
    +
    +      piece = pieces.shift();
    +    }
    +
    +    return {start, end};
    +  }
    +}
    
  • src/parser.js+782 0 added
    @@ -0,0 +1,782 @@
    +// The main Swagger parsing component that outputs refract.
    +
    +import _ from 'lodash';
    +import Ast from './ast';
    +import buildUriTemplate from './uri-template';
    +import SwaggerParser from 'swagger-parser';
    +import yaml from 'js-yaml';
    +
    +// These describe the type of annotations that are produced by this parser
    +// and assigns a unique code to each one. Downstream applications can use this
    +// code to group similar types of annotations together.
    +const ANNOTATIONS = {
    +  CANNOT_PARSE: {
    +    type: 'error',
    +    code: 1,
    +    fragment: 'yaml-parser',
    +  },
    +  AST_UNAVAILABLE: {
    +    type: 'warning',
    +    code: 2,
    +    fragment: 'yaml-parser',
    +  },
    +  DATA_LOST: {
    +    type: 'warning',
    +    code: 3,
    +    fragment: 'refract-not-supported',
    +  },
    +  VALIDATION_ERROR: {
    +    type: 'warning',
    +    code: 4,
    +    fragment: 'swagger-validation',
    +  },
    +};
    +
    +// Test whether a key is a special Swagger extension.
    +function isExtension(value, key) {
    +  return key.indexOf('x-') === 0;
    +}
    +
    +// 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;
    +
    +    // 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 {
    +      Copy, Category, Member: MemberElement, ParseResult, SourceMap,
    +    } = this.minim.elements;
    +
    +    const swaggerParser = new SwaggerParser();
    +    const parseResult = new ParseResult();
    +    this.result = parseResult;
    +
    +    let loaded;
    +    try {
    +      loaded = _.isString(this.source) ? yaml.safeLoad(this.source) : this.source;
    +    } catch (err) {
    +      this.makeAnnotation(ANNOTATIONS.CANNOT_PARSE, null,
    +        (err.reason || 'Problem loading the input'));
    +
    +      if (err.mark) {
    +        parseResult.first().attributes.set('sourceMap', [
    +          new SourceMap([[err.mark.position, 1]]),
    +        ]);
    +      }
    +
    +      return done(new Error(err.message), parseResult);
    +    }
    +
    +    // Some sane defaults since these are sometimes left out completely
    +    if (loaded.info === undefined) {
    +      loaded.info = {};
    +    }
    +
    +    if (loaded.paths === undefined) {
    +      loaded.paths = {};
    +    }
    +
    +    // Parse and validate the Swagger document!
    +    swaggerParser.validate(loaded, (err) => {
    +      const swagger = swaggerParser.api;
    +      this.swagger = swaggerParser.api;
    +
    +      if (err) {
    +        if (swagger === undefined) {
    +          return done(err, parseResult);
    +        }
    +
    +        // 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) {
    +            for (const item of queue[0]) {
    +              this.makeAnnotation(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();
    +          }
    +        }
    +      }
    +
    +      const api = new Category();
    +      this.api = api;
    +      parseResult.push(api);
    +
    +      // Root API Element
    +      api.classes.push('api');
    +
    +      if (swagger.info) {
    +        this.path.push('info');
    +
    +        if (swagger.info.title) {
    +          api.meta.set('title', swagger.info.title);
    +
    +          if (this.generateSourceMap) {
    +            this.makeSourceMap(api.meta.get('title'), this.path.concat(['title']));
    +          }
    +        }
    +
    +        if (swagger.info.description) {
    +          api.content.push(new Copy(swagger.info.description));
    +
    +          if (this.generateSourceMap) {
    +            this.makeSourceMap(api.content[api.content.length - 1],
    +              this.path.concat(['description']));
    +          }
    +        }
    +
    +        this.path.pop();
    +      }
    +
    +      if (swagger.host) {
    +        let hostname = swagger.host;
    +
    +        if (swagger.schemes) {
    +          if (swagger.schemes.length > 1) {
    +            this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['schemes'],
    +              'Only the first scheme will be used to create a hostname');
    +          }
    +
    +          hostname = `${swagger.schemes[0]}://${hostname}`;
    +        }
    +
    +        api.attributes.set('meta', {});
    +        const meta = api.attributes.get('meta');
    +        const member = new MemberElement('HOST', hostname);
    +        member.meta.set('classes', ['user']);
    +
    +        if (this.generateSourceMap) {
    +          this.makeSourceMap(member, ['host']);
    +        }
    +
    +        meta.content.push(member);
    +      }
    +
    +      if (swagger.securityDefinitions) {
    +        this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['securityDefinitions'],
    +          'Authentication information is not yet supported');
    +      }
    +
    +      if (swagger.security) {
    +        this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['security'],
    +          'Authentication information is not yet supported');
    +      }
    +
    +      if (swagger.externalDocs) {
    +        this.makeAnnotation(ANNOTATIONS.DATA_LOST, ['externalDocs'],
    +          'External documentation is not yet supported');
    +      }
    +
    +      this.group = api;
    +
    +      // Swagger has a paths object to loop through
    +      // The key is the href
    +      _.each(_.omit(swagger.paths, isExtension), (pathValue, href) => {
    +        this.handleSwaggerPath(pathValue, href);
    +      });
    +
    +      done(null, parseResult);
    +    });
    +  }
    +
    +  // == Internal properties & functions ==
    +
    +  handleSwaggerPath(pathValue, href) {
    +    const {
    +      Asset, Copy, Category, DataStructure, HrefVariables, HttpHeaders,
    +      Member: MemberElement, Object: ObjectElement, Resource, Transition,
    +    } = this.minim.elements;
    +    const resource = new Resource();
    +
    +    this.path.push('paths');
    +    this.path.push(href);
    +
    +    if (this.generateSourceMap) {
    +      this.makeSourceMap(resource, this.path);
    +    }
    +
    +    // Provide users with a way to add a title to a resource in Swagger
    +    if (pathValue['x-summary']) {
    +      resource.title = pathValue['x-summary'];
    +    }
    +
    +    // Provide users a way to add a description to a resource in Swagger
    +    if (pathValue['x-description']) {
    +      const resourceDescription = new Copy(pathValue['x-description']);
    +      resource.push(resourceDescription);
    +    }
    +
    +    if (this.useResourceGroups()) {
    +      const groupName = pathValue['x-group-name'];
    +
    +      if (groupName) {
    +        this.group = this.api.find((el) => el.element === 'category' && el.classes.contains('resourceGroup') && el.title === groupName).first();
    +
    +        if (!this.group) {
    +          this.group = new Category();
    +          this.group.title = groupName;
    +          this.group.classes.push('resourceGroup');
    +
    +          if (this.swagger.tags && this.swagger.tags.forEach) {
    +            this.swagger.tags.forEach((tag) => {
    +              // TODO: Check for external docs here?
    +              if (tag.name === groupName && tag.description) {
    +                this.group.content.push(new Copy(tag.description));
    +              }
    +            });
    +          }
    +
    +          this.api.content.push(this.group);
    +        }
    +      }
    +    }
    +
    +    this.group.content.push(resource);
    +
    +    const pathObjectParameters = pathValue.parameters || [];
    +
    +    // TODO: Currently this only supports URI parameters for `path` and `query`.
    +    // It should add support for `body` parameters as well.
    +    if (pathObjectParameters.length > 0) {
    +      resource.hrefVariables = new HrefVariables();
    +
    +      pathObjectParameters.forEach((parameter, index) => {
    +        this.path.push('parameters');
    +        this.path.push(index);
    +
    +        if (parameter.in === 'query' || parameter.in === 'path') {
    +          const member = this.convertParameterToMember(parameter, this.path);
    +          if (this.generateSourceMap) {
    +            this.makeSourceMap(member, this.path);
    +          }
    +          resource.hrefVariables.content.push(member);
    +        } else if (parameter.in === 'body') {
    +          this.makeAnnotation(ANNOTATIONS.DATA_LOST, this.path,
    +            'Path-level body parameters are not yet supported');
    +        }
    +
    +        this.path.pop();
    +        this.path.pop();
    +      });
    +    }
    +
    +    const relevantMethods = _.chain(pathValue)
    +      .omit('parameters', '$ref')
    +      .omit(isExtension)
    +      .value();
    +
    +    // Each path is an object with methods as properties
    +    _.each(relevantMethods, (methodValue, method) => {
    +      const transition = new Transition();
    +      resource.content.push(transition);
    +
    +      this.path.push(method);
    +
    +      if (this.generateSourceMap) {
    +        this.makeSourceMap(transition, this.path);
    +      }
    +
    +      if (methodValue.externalDocs) {
    +        this.makeAnnotation(ANNOTATIONS.DATA_LOST,
    +          this.path.concat(['externalDocs']),
    +          'External documentation is not yet supported');
    +      }
    +
    +      const methodValueParameters = methodValue.parameters || [];
    +
    +      const queryParameters = methodValueParameters.filter((parameter) => {
    +        return parameter.in === 'query';
    +      });
    +
    +      // URI parameters are for query and path variables
    +      const uriParameters = methodValueParameters.filter((parameter) => {
    +        return parameter.in === 'query' || parameter.in === 'path';
    +      });
    +
    +      // Body parameters are ones that define JSON Schema
    +      const bodyParameters = methodValueParameters.filter((parameter) => {
    +        return parameter.in === 'body';
    +      });
    +
    +      // Form parameters are send as encoded form data in the body
    +      const formParameters = methodValueParameters.filter((parameter) => {
    +        return parameter.in === 'formData';
    +      });
    +
    +      const basePath = (this.swagger.basePath || '').replace(/[/]+$/, '');
    +      const hrefForResource = buildUriTemplate(basePath, href, pathObjectParameters, queryParameters);
    +      resource.attributes.set('href', hrefForResource);
    +
    +      if (methodValue.summary) {
    +        transition.meta.set('title', methodValue.summary);
    +
    +        if (this.generateSourceMap) {
    +          const title = transition.meta.get('title');
    +          this.makeSourceMap(title, this.path.concat(['summary']));
    +        }
    +      }
    +
    +      if (methodValue.description) {
    +        const description = new Copy(methodValue.description);
    +        transition.push(description);
    +
    +        if (this.generateSourceMap) {
    +          this.makeSourceMap(description, this.path.concat(['description']));
    +        }
    +      }
    +
    +      if (methodValue.operationId) {
    +        transition.attributes.set('relation', methodValue.operationId);
    +      }
    +
    +      // For each uriParameter, create an hrefVariable
    +      if (uriParameters.length > 0) {
    +        transition.hrefVariables = new HrefVariables();
    +
    +        this.path.push('parameters');
    +
    +        uriParameters.forEach((parameter) => {
    +          const index = methodValueParameters.indexOf(parameter);
    +          this.path.push(index);
    +          transition.hrefVariables.content.push(
    +            this.convertParameterToMember(parameter, this.path));
    +          this.path.pop();
    +        });
    +
    +        this.path.pop();
    +      }
    +
    +      // Currently, default responses are not supported in API Description format
    +      const relevantResponses = _.chain(methodValue.responses)
    +        .omit('default')
    +        .omit(isExtension)
    +        .value();
    +
    +      if (methodValue.responses && methodValue.responses.default) {
    +        this.path.push('responses');
    +        this.path.push('default');
    +        this.makeAnnotation(ANNOTATIONS.DATA_LOST, this.path,
    +          'Default response is not yet supported');
    +        this.path.pop();
    +        this.path.pop();
    +      }
    +
    +      if (_.keys(relevantResponses).length === 0) {
    +        if (bodyParameters.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);
    +        }
    +      }
    +
    +      // Transactions are created for each response in the document
    +      _.each(relevantResponses, (responseValue, statusCode) => {
    +        let examples = {
    +          '': undefined,
    +        };
    +
    +        this.path.push('responses');
    +        this.path.push(statusCode);
    +
    +        if (responseValue.examples) {
    +          examples = responseValue.examples;
    +        }
    +
    +        examples = _.omit(examples, 'schema');
    +
    +        _.each(examples, (responseBody, contentType) => {
    +          const transaction = this.createTransaction(transition, method);
    +          const request = transaction.request;
    +          const response = transaction.response;
    +
    +          if (this.generateSourceMap) {
    +            this.makeSourceMap(transaction, this.path);
    +            this.makeSourceMap(request, this.path.slice(0, 3));
    +
    +            if (statusCode) {
    +              this.makeSourceMap(response, this.path);
    +            }
    +          }
    +
    +          if (responseValue.description) {
    +            const description = new Copy(responseValue.description);
    +            response.content.push(description);
    +            if (this.generateSourceMap) {
    +              this.makeSourceMap(description, this.path.concat(['description']));
    +            }
    +          }
    +
    +          const headers = new HttpHeaders();
    +
    +          if (contentType) {
    +            headers.push(new MemberElement(
    +              'Content-Type', contentType
    +            ));
    +
    +            if (this.generateSourceMap) {
    +              this.makeSourceMap(headers.content[headers.content.length - 1], this.path.concat(['examples',  contentType]));
    +            }
    +
    +            response.headers = headers;
    +          }
    +
    +          if (responseValue.headers) {
    +            response.headers = this.createHeaders(headers, responseValue.headers);
    +          }
    +
    +          // Body parameters define request schemas
    +          _.each(bodyParameters, (bodyParameter) => {
    +            const schemaAsset = this.createAssetFromJsonSchema(
    +              bodyParameter.schema);
    +            request.content.push(schemaAsset);
    +          });
    +
    +          // Using form parameters instead of body? We will convert those to
    +          // data structures.
    +          if (formParameters.length) {
    +            const dataStructure = new DataStructure();
    +            // A form is essentially an object with key/value members
    +            const dataObject = new ObjectElement();
    +
    +            _.each(formParameters, (param) => {
    +              const index = methodValueParameters.indexOf(param);
    +              dataObject.content.push(this.convertParameterToMember(param, this.path.slice(0, 3).concat(['parameters', index])));
    +            });
    +
    +            dataStructure.content = dataObject;
    +            request.content.push(dataStructure);
    +          }
    +
    +          this.path.push('examples');
    +
    +          // Responses can have bodies
    +          if (responseBody !== undefined) {
    +            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.path.push(contentType);
    +              this.makeSourceMap(bodyAsset, this.path);
    +              this.path.pop();
    +            }
    +            response.content.push(bodyAsset);
    +          }
    +
    +          // Responses can have schemas in Swagger
    +          const schema = responseValue.schema || (responseValue.examples && responseValue.examples.schema);
    +          if (schema) {
    +            const schemaAsset = this.createAssetFromJsonSchema(schema);
    +            if (this.generateSourceMap) {
    +              let schemaPath = this.path.slice(0, 5);
    +              if (responseValue.examples && responseValue.examples.schema) {
    +                schemaPath = schemaPath.concat(['examples', 'schema']);
    +              } else {
    +                schemaPath = schemaPath.concat(['schema']);
    +              }
    +              this.makeSourceMap(schemaAsset, schemaPath);
    +            }
    +            response.content.push(schemaAsset);
    +          }
    +
    +          if (statusCode !== 'null') {
    +            response.attributes.set('statusCode', statusCode);
    +          }
    +
    +          this.path.pop();
    +        });
    +
    +        this.path.pop();
    +        this.path.pop();
    +      });
    +
    +      this.path.pop();
    +      this.path.pop();
    +    });
    +
    +    this.path.pop();
    +  }
    +
    +  // Takes in an `httpHeaders` element and a list of Swagger headers. Adds
    +  // the Swagger headers to the element and then returns the modified element.
    +  createHeaders(element, headers) {
    +    const {Member: MemberElement} = this.minim.elements;
    +
    +    this.path.push('headers');
    +
    +    for (const headerName in headers) {
    +      if (headers.hasOwnProperty(headerName)) {
    +        const header = headers[headerName];
    +        let value = '';
    +
    +        // Choose the first available option
    +        if (header.enum) {
    +          value = header.enum[0];
    +        }
    +
    +        if (header.default) {
    +          value = header.default;
    +        }
    +
    +        const member = new MemberElement(headerName, value);
    +
    +        if (this.generateSourceMap) {
    +          this.makeSourceMap(member, this.path.concat([headerName]));
    +        }
    +
    +        if (header.description) {
    +          member.meta.set('description', header.description);
    +
    +          if (this.generateSourceMap) {
    +            this.makeSourceMap(member.meta.get('description'), this.path.concat(['description']));
    +          }
    +        }
    +
    +        element.push(member);
    +      }
    +    }
    +
    +    this.path.pop();
    +
    +    return element;
    +  }
    +
    +  // 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._ast !== undefined) {
    +      return this._ast;
    +    }
    +
    +    if (_.isString(this.source)) {
    +      try {
    +        this._ast = new Ast(this.source);
    +      } catch (err) {
    +        this._ast = null;
    +        this.makeAnnotation(ANNOTATIONS.AST_UNAVAILABLE, null,
    +          'Input AST could not be composed, so source maps will not be available');
    +      }
    +    } else {
    +      this._ast = null;
    +      this.makeAnnotation(ANNOTATIONS.AST_UNAVAILABLE, null,
    +        'Source maps are only available with string input');
    +    }
    +
    +    return this._ast;
    +  }
    +
    +  // 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) {
    +      _.each(this.swagger.paths, (path) => {
    +        let tag = null;
    +
    +        if (path) {
    +          _.each(path, (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[0];
    +              } else if (tag !== operation.tags[0]) {
    +                // Non-matching tags... can't have a resource in multiple groups!
    +                return false;
    +              }
    +            }
    +          });
    +        }
    +
    +        if (tag) {
    +          path['x-group-name'] = tag;
    +          tags.push(tag);
    +        }
    +      });
    +    }
    +
    +    return tags.length > 0;
    +  }
    +
    +  // Make a new source map for the given element
    +  makeSourceMap(element, path) {
    +    if (this.ast) {
    +      const SourceMap = this.minim.getElementClass('sourceMap');
    +      const position = this.ast.getPosition(path);
    +      if (position && !isNaN(position.start) && !isNaN(position.end)) {
    +        element.attributes.set('sourceMap', [
    +          new SourceMap([[position.start, position.end - position.start]]),
    +        ]);
    +      }
    +    }
    +  }
    +
    +  // Make a new annotation for the given path and message
    +  makeAnnotation(info, path, message) {
    +    const {Annotation, Link} = 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) {
    +      const link = new Link();
    +      link.relation = 'origin';
    +      link.href = `http://docs.apiary.io/validations/swagger#${info.fragment}`;
    +      annotation.links.push(link);
    +    }
    +
    +    if (path && this.ast) {
    +      this.makeSourceMap(annotation, path);
    +    }
    +  }
    +
    +  convertParameterToElement(parameter, path, setAttributes = false) {
    +    const {
    +      Array: ArrayElement, Boolean: BooleanElement, Number: NumberElement,
    +      String: StringElement,
    +    } = this.minim.elements;
    +    let element;
    +
    +    // Convert from Swagger types to Minim elements
    +    if (parameter.type === 'string') {
    +      element = new StringElement('');
    +    } else if (parameter.type === 'integer' || parameter.type === 'number') {
    +      element = new NumberElement();
    +    } else if (parameter.type === 'boolean') {
    +      element = new BooleanElement();
    +    } else if (parameter.type === 'array') {
    +      element = new ArrayElement();
    +
    +      if (parameter.items) {
    +        element.content = [this.convertParameterToElement(
    +          parameter.items, (path || []).concat(['items']), true)];
    +      }
    +    } else {
    +      // Default to a string in case we get a type we haven't seen
    +      element = new StringElement('');
    +    }
    +
    +    if (this.generateSourceMap) {
    +      this.makeSourceMap(element, path);
    +    }
    +
    +    if (setAttributes) {
    +      if (parameter.description) {
    +        element.description = parameter.description;
    +
    +        if (this.generateSourceMap) {
    +          this.makeSourceMap(element.meta.get('description'), path.concat(['description']));
    +        }
    +      }
    +
    +      if (parameter.required) {
    +        element.attributes.set('typeAttributes', ['required']);
    +      }
    +
    +      if (parameter.default !== undefined) {
    +        element.attributes.set('default', parameter.default);
    +      }
    +    }
    +
    +    return element;
    +  }
    +
    +  convertParameterToMember(parameter, path) {
    +    const MemberElement = this.minim.getElementClass('member');
    +    const memberValue = this.convertParameterToElement(parameter, path);
    +
    +    // TODO: Update when Minim has better support for elements as values
    +    // should be: new MemberType(parameter.name, memberValue);
    +    const member = new MemberElement(parameter.name);
    +    member.content.value = memberValue;
    +
    +    if (this.generateSourceMap) {
    +      this.makeSourceMap(member, path);
    +    }
    +
    +    if (parameter.description) {
    +      member.description = parameter.description;
    +
    +      if (this.generateSourceMap) {
    +        this.makeSourceMap(member.meta.get('description'),
    +          path.concat(['description']));
    +      }
    +    }
    +
    +    if (parameter.required) {
    +      member.attributes.set('typeAttributes', ['required']);
    +    }
    +
    +    // If there is a default, it is set on the member value instead of the member
    +    // element itself because the default value applies to the value.
    +    if (parameter.default) {
    +      memberValue.attributes.set('default', parameter.default);
    +    }
    +
    +    return member;
    +  }
    +
    +  createAssetFromJsonSchema(jsonSchema) {
    +    const Asset = this.minim.getElementClass('asset');
    +    const schemaAsset = new Asset(JSON.stringify(jsonSchema));
    +    schemaAsset.classes.push('messageBodySchema');
    +    schemaAsset.attributes.set('contentType', 'application/schema+json');
    +
    +    return schemaAsset;
    +  }
    +
    +  createTransaction(transition, method) {
    +    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.attributes.set('method', method.toUpperCase());
    +    }
    +
    +    return transaction;
    +  }
    +}
    
  • src/uri-template.js+1 1 modified
    @@ -1,4 +1,4 @@
    -import _ from 'underscore';
    +import _ from 'lodash';
     
     export default function buildUriTemplate(basePath, href, pathObjectParameters = [], queryParameters = []) {
       if (queryParameters.length > 0 || pathObjectParameters.length > 0) {
    
  • test/fixtures/refract/payload-as-string.json+1 1 modified
    @@ -231,4 +231,4 @@
           ]
         }
       ]
    -}
    +}
    \ No newline at end of file
    
  • test/fixtures/refract/petstore.json+42 3 modified
    @@ -22,7 +22,20 @@
             ]
           },
           "attributes": {
    -        "code": 4
    +        "code": 4,
    +        "sourceMap": [
    +          {
    +            "element": "sourceMap",
    +            "meta": {},
    +            "attributes": {},
    +            "content": [
    +              [
    +                7245,
    +                275
    +              ]
    +            ]
    +          }
    +        ]
           },
           "content": "Data does not match any schemas from 'oneOf'"
         },
    @@ -45,7 +58,20 @@
             ]
           },
           "attributes": {
    -        "code": 4
    +        "code": 4,
    +        "sourceMap": [
    +          {
    +            "element": "sourceMap",
    +            "meta": {},
    +            "attributes": {},
    +            "content": [
    +              [
    +                7245,
    +                275
    +              ]
    +            ]
    +          }
    +        ]
           },
           "content": "Data does not match any schemas from 'oneOf'"
         },
    @@ -68,7 +94,20 @@
             ]
           },
           "attributes": {
    -        "code": 4
    +        "code": 4,
    +        "sourceMap": [
    +          {
    +            "element": "sourceMap",
    +            "meta": {},
    +            "attributes": {},
    +            "content": [
    +              [
    +                7245,
    +                275
    +              ]
    +            ]
    +          }
    +        ]
           },
           "content": "Missing required property: $ref"
         },
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.