schema-builder.js 5.32 KB
// Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-swagger
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';

// Globalization
var g = require('strong-globalize')();

var assert = require('assert');
var typeConverter = require('./type-converter');

var TYPES_PRIMITIVE = [
  'boolean',
  'integer',
  'number',
  'null',
  'string',
  'object',
  'array',
];

var KEY_TRANSLATIONS = {
  // LDL : Swagger
  min: 'minimum',
  max: 'maximum',
  length: 'maxLength',
};

var SWAGGER_DATA_TYPE_FIELDS = [
  'format',
  'default',
  'enum',
  'minimum',
  'minItems',
  'minLength',
  'maximum',
  'maxItems',
  'maxLength',
  'uniqueItems',
  'pattern',
];

/**
 * Build a Swagger Schema Object and/or Parameter Object from LoopBack
 * type descriptor.
 *
 * @param {String|Function|Array|Object} ldlDef The loopback type to convert,
 *  the value should be one of the following:
 *   - a string value (type name), e.g. `'string'` or `'MyModel'`
 *   - a constructor function, e.g. `String` or `MyModel`
 *   - an array of a single item in `lbType` format
 *   - an object containing a `type` property with string/function/array value
 *     and validation fields like `length` or `max`
 * @param {TypeRegistry} typeRegistry The registry of known types and models.
 * @returns {Object} Swagger Schema Object that can be used as `schema` field
 *   or as a base for Parameter Object.
 */
exports.buildFromLoopBackType = function(ldlDef, typeRegistry) {
  assert(!!typeRegistry, 'typeRegistry is a required parameter');

  // Normalize non-object values to object format `{ type: XYZ }`
  if (typeof ldlDef === 'string' || typeof ldlDef === 'function') {
    ldlDef = { type: ldlDef };
  } else if (Array.isArray(ldlDef)) {
    ldlDef = { type: ldlDef };
  }

  if (!ldlDef.type) {
    ldlDef = { type: 'any' };
  }

  var schema = exports.buildMetadata(ldlDef);

  var ldlType = ldlDef.type;
  if (ldlType === 'object' && ldlDef.model) {
    ldlType = ldlDef.model;
  }
  ldlType = exports.getLdlTypeName(ldlType);

  if (Array.isArray(ldlType)) {
    var itemLdl = ldlType[0] || 'any';
    var itemSchema = exports.buildFromLoopBackType(itemLdl, typeRegistry);
    schema.type = 'array';
    schema.items = itemSchema;
    return schema;
  }

  if (ldlType === 'object' && typeof ldlDef.type === 'object') {
    var obj = {};
    for (var prop in ldlDef.type) {
      obj[prop] = exports.buildFromLoopBackType(ldlDef.type[prop], typeRegistry);
    }
    schema.type = 'object';
    schema.properties = obj;
    return schema;
  }

  var ldlTypeLowerCase = ldlType.toLowerCase();
  switch (ldlTypeLowerCase) {
    case 'date':
      schema.type = 'string';
      schema.format = 'date-time';
      break;
    case 'buffer':
      schema.type = 'string';
      schema.format = 'byte';
      break;
    case 'number':
      schema.type = 'number';
      schema.format = schema.format || 'double'; // All JS numbers are doubles
      break;
    case 'any':
      schema.$ref = typeRegistry.reference('x-any');
      break;
    default:
      if (exports.isPrimitiveType(ldlTypeLowerCase)) {
        schema.type = ldlTypeLowerCase;
      } else {
        // TODO - register anonymous types
        schema.$ref = typeRegistry.reference(ldlType);
      }
  }
  return schema;
};

/**
 * @param {String|Function|Array|Object} ldlType LDL type
 * @returns {String|Array} Type name
 */
exports.getLdlTypeName = function(ldlType) {
  // Value "array" is a shortcut for `['any']`
  if (ldlType === 'array') {
    return ['any'];
  }

  if (typeof ldlType === 'string') {
    var arrayMatch = ldlType.match(/^\[(.*)\]$/);
    return arrayMatch ? [arrayMatch[1]] : ldlType;
  }

  if (typeof ldlType === 'function') {
    return ldlType.modelName || ldlType.name;
  }

  if (Array.isArray(ldlType)) {
    return ldlType;
  }

  if (typeof ldlType === 'object') {
    // Anonymous objects, they are allowed e.g. in accepts/returns definitions
    // TODO(bajtos) Build a named schema for this anonymous object
    return 'object';
  }

  if (ldlType === undefined) {
    return 'any';
  }

  var msg = g.f('Warning: unknown LDL type %j, using "{{any}}" instead', ldlType);
  console.error(msg);
  return 'any';
};

/**
 * Convert validations and other metadata from LDL format to Swagger format.
 * @param {Object} ldlDef LDL property/argument definition,
 * for example `{ type: 'string', maxLength: 64 }`.
 * @return {Object} Metadata in Swagger format.
 */
exports.buildMetadata = function(ldlDef) {
  var result = {};
  var key;

  for (key in KEY_TRANSLATIONS) {
    if (key in ldlDef) {
      // Skip null as swagger 2.x UI does not support it
      // https://github.com/swagger-api/swagger-spec/issues/229
      if (ldlDef[key] != null) {
        result[KEY_TRANSLATIONS[key]] = ldlDef[key];
      }
    }
  }

  /* eslint-disable one-var */
  for (var ix in SWAGGER_DATA_TYPE_FIELDS) {
    key = SWAGGER_DATA_TYPE_FIELDS[ix];
    if (key in ldlDef)
      result[key] = ldlDef[key];
  }
  /* eslint-enable one-var */
  if (ldlDef.description) {
    result.description = typeConverter.convertText(ldlDef.description);
  } else if (ldlDef.doc) {
    result.description = typeConverter.convertText(ldlDef.doc);
  }

  return result;
};

exports.isPrimitiveType = function(typeName) {
  return TYPES_PRIMITIVE.indexOf(typeName.toLowerCase()) !== -1;
};