apiary-preprocessor
Version:
This tool automatically updates responses for Apiary markdown files.
339 lines (325 loc) • 11.1 kB
JavaScript
'use strict';
var Document = require('md-doc'),
DocumentExtension = Document.Extension,
ContentType = Document.ContentType,
Content = Document.Content,
_ = require('lodash'),
regexUtils = require('./regex.utils'),
jsonLint = require("jsonlint"),
jsonStringifyPrettyCompact = require("json-stringify-pretty-compact");
var HEADERS = prefix('Headers'), HEADERS_SHORT = pfx('head'),
BODY = prefix('Body'), BODY_SHORT = pfx('body'),
SECTION = prefix('Section'), SECTION_SHORT = pfx('sect'),
REQUEST = prefix('Request'), REQUEST_SHORT = pfx('req'),
RESPONSE = prefix('Response'), RESPONSE_SHORT = pfx('res');
function sortObject(object) {
if (typeof object !== 'object')
return object;
var temp = {};
var keys = [];
if (Array.isArray(object)) {
return object.map(element=>sortObject(element));
}
for (var key in object) {
if (object.hasOwnProperty(key)) {
keys.push(key);
}
}
keys.sort();
for (var index in keys) {
if (keys.hasOwnProperty(index)) {
temp[keys[index]] = sortObject(object[keys[index]]);
}
}
return temp;
}
function jsonStringify(object, options) {
return jsonStringifyPrettyCompact(sortObject(object), options);
}
function pfx(name) {
return 'api-' + name;
}
function prefix(name) {
return 'Apiary ' + name;
}
function processHeaders(headers) {
var _headers = {};
if (typeof headers === 'string') {
headers = headers.split('\n');
for (var i = 0; i < headers.length; i++) {
var header = headers[i].split(':');
if (header && header.length === 2) {
_headers[header[0].trim()] = header[1].trim();
}
}
} else {
_headers = headers;
}
return _headers;
}
function getBodyContent() {
var bodyContent = this.find('|' + BODY_SHORT)[0];
if (!bodyContent) {
bodyContent = Content.create(getContentType(BODY), '\n + Body\n\n', this);
this.contents.push(bodyContent);
}
return bodyContent;
}
function getHeadersContent() {
var headersContent = this.find('|' + HEADERS_SHORT)[0];
if (!headersContent) {
headersContent = Content.create(getContentType(HEADERS), '\n + Headers\n\n', this);
this.contents.push(headersContent);
}
return headersContent;
}
var endOfSubsection = /(?:\n {0,4}[^ ])/;
function section(indent) {
// Blah [SOMETHING url]
var header = /(.*)\[([^ \n]*) ?(.*)]/;
return new ContentType({
match: regexUtils.replace(
/\n#{_indent_}[^#]_header_((?:\n|.*)*?)(?=(?:\n#{_indent_}[^#].*)|$)/
)(/_indent_/g, String(indent))(/_header_/g, header)(),
name: SECTION,
short: SECTION_SHORT,
isBlock: true,
validAt: ['Document', 'Section', SECTION],
on: function (match) {
var section = new Content(this, '\n' + match[4].trim());
//Prepare level and header
section.level = indent;
section.name = match[1];
var req = {};
if (match[2].match(/^((POST)|(GET)|(DELETE)|(PUT)|(HEADER))$/)) {
req.method = match[2];
if (match[3].length > 0) {
req.address = match[3];
}
} else {
req.address = match[2];
}
section.request = req;
return section;
},
getText: function () {
var level = '', req;
for (var i = 0; i < this.level; i++) {
level += '#';
}
if (this.request.method) {
req = this.request.method + (this.request.address ? (' ' + this.request.address) : '');
} else if (this.request.address) {
req = this.request.address;
}
var name = this.name.trim();
if (name.length > 0) {
name += ' ';
}
return '\n' + level + ' ' + name + '[' + req + ']' + '\n\n' + this.getContentText();
},
getHtml: function () {
var req = (this.request.method ? this.request.method + ' ' : '') + this.request.address;
return '<h' + this.level + '>' + this.name + ' <b>' + req + '</b>' + '</h' + this.level + '><div>' + this.getContentHtml() + '</div>';
}
});
}
var contentTypes = [
section(1),
section(2),
section(3),
section(4),
section(5),
section(6),
ContentType.ListTypedSection(/\+/, /\n\n(?=[^ ])(?!\+ *Response)/, REQUEST, 'Request', REQUEST_SHORT, function (headerParams) {
var params = headerParams.match(/.*\((.*)\)|(.*)/);
this.contentType = params[1];
if (typeof this.contents === 'string') {
this.contents = [
Content.create(getContentType(BODY), '\n + Body\n\n' + this.contents, this)
];
} else {
var hasBody = _.find(this.contents, element=>element.type.name === BODY);
if (!hasBody) {
var body = this.contents[0];
if (body.type.name === 'Text Block') {
this.contents[0] = Content.create(getContentType(BODY), '\n + Body\n\n' + body.contents, this);
} else {
this.contents.splice(0, 0, Content.create(getContentType(BODY), '\n + Body\n\n', this));
}
}
}
this.getBodyContent = getBodyContent;
this.getHeadersContent = getHeadersContent;
}).setGetText(function () {
var description = this.contents.filter(element=> {
var t = element.type.short;
return t !== HEADERS_SHORT && t !== BODY_SHORT && t !== RESPONSE_SHORT
}),
headers = this.getHeadersContent(),
body = this.getBodyContent(),
responses = this.find('|' + RESPONSE_SHORT);
var text = '\n\n+ Request' + (this.contentType ? (' (' + this.contentType + ')') : '') + '\n\n';
description.forEach(description=> {
text += description.getText()
});
text += '\n';
if (headers) {
var headersText = headers.getText();
if (headersText) {
text += '\n' + headersText + '\n';
}
}
if (body) {
var bodyText = body.getText();
if (bodyText) {
text += '\n' + body.getText() + '\n';
}
}
responses.forEach(response=> {
text += response.getText()
});
return text;
}).setValidChildren([RESPONSE, HEADERS, BODY]),
ContentType.ListTypedSection(/\+/, true, RESPONSE, 'Response', RESPONSE_SHORT, function (headerParams) {
var params = headerParams.match(/(?:(.*)\((.*)\))|(.*)/);
//Response code and type
if (params[3]) {
this.code = parseInt(params[3].trim());
} else {
this.code = parseInt(params[1].trim());
this.contentType = params[2];
}
if (typeof this.contents === 'string') {
this.contents = [
Content.create(getContentType(BODY), '\n + Body\n\n' + this.contents, this)
];
}
this.getBodyContent = getBodyContent;
this.getHeadersContent = getHeadersContent;
})
.setGetText(function () {
var description = this.contents.filter(element=> {
var t = element.type.short;
return t !== HEADERS_SHORT && t !== BODY_SHORT && t !== RESPONSE_SHORT
}),
headers = this.getHeadersContent(),
body = this.getBodyContent();
var text = '\n+ Response';
if (this.code) {
text += ' ' + this.code;
} else {
text += ' 200';
}
if (this.contentType) {
text += ' (' + this.contentType + ')';
}
text += '\n';
description.forEach(description=> {
text += description.getText()
});
text += '\n';
if (headers) {
text += '\n' + headers.getText() + '\n';
}
if (body) {
text += '\n' + body.getText() + '\n';
}
return text;
})
.setValidChildren([HEADERS, BODY]),
ContentType.ListTypedSection(/\+/, endOfSubsection, HEADERS, 'Headers', HEADERS_SHORT, function () {
this.getHeaders = function () {
return this._headers;
};
this.setHeaders = function (headers) {
this._headers = processHeaders(headers);
};
this.setHeaders(this.contents);
this.contents = '';
}).setParseContent(false)
.setValidAt([REQUEST, RESPONSE])
.setGetText(function () {
var text = '', whitespace = ' ', headers = this.getHeaders();
for (var name in headers) {
if (headers.hasOwnProperty(name)) {
text += whitespace + name + ': ' + headers[name] + '\n';
}
}
if (text === '') {
return '';
}
return '\n + Headers\n\n' + text + '\n\n';
}),
ContentType.ListTypedSection(/\+/, endOfSubsection, BODY, 'Body', BODY_SHORT, function () {
this.rawBody = ('\n' + this.contents).replace(/\n {12}/g, '\n').replace(/\n {8}/g, '\n').substring(1);
this.contents = '';
this.getBody = function () {
var body;
switch (this.parent.contentType) {
case 'application/json':
try {
body = jsonLint.parse(this.rawBody);
} catch (e) {
var parent = this.parents(SECTION_SHORT)[0],
what = parent ? parent.name : 'Unknown';
var err = new Error('Malformed ' + this.parent.type.name + ' JSON Body at ' + what + '\n\n' + e.message);
delete err.stack;
throw err;
}
break;
default:
body = this.rawBody;
break;
}
return body;
};
this.setBody = function (body) {
if (typeof body !== 'string') {
switch (this.parent.contentType) {
case 'application/json':
this.rawBody = jsonStringify(body, { indent: 4 });
break;
default:
this.rawBody = String(body);
}
} else {
this.rawBody = body;
}
};
})
.setParseContent(false)
.setGetText(function () {
var contentType = this.parent.contentType, body = this.getBody();
switch (contentType) {
case 'application/json':
body = jsonStringify(body, { indent: 4 });
break;
}
if (!body) {
return '';
}
return '\n + Body\n\n' + (body.replace(/^|\n/g, '\n ')) + '\n';
}).setValidAt([REQUEST, RESPONSE])
];
function getContentType(name) {
return _.find(contentTypes, element=>element.name === name);
}
exports = module.exports = new DocumentExtension('Apiary', {
onDocumentHeaderRead: function (document, callback) {
var format = document.headers['FORMAT'];
if (!format) {
return callback(new Error('FORMAT is missing.'));
}
if (format !== '1A') {
return callback(new Error('Only "1A" format is supported. Got "' + format + '"'));
}
callback();
}
}, contentTypes, 0);
exports.HEADERS = HEADERS_SHORT;
exports.BODY = BODY_SHORT;
exports.SECTION = SECTION_SHORT;
exports.REQUEST = REQUEST_SHORT;
exports.RESPONSE = RESPONSE_SHORT;
exports.processHeaders = processHeaders;