Skip to content

Commit

Permalink
Let escape fields with dot (apidoc#774) (apidoc#1063)
Browse files Browse the repository at this point in the history
* let escape fields with dot (apidoc#774)

* fix optional fields display

* fix issue with objects nesting

* accept both '.' and '[]' for object properties

* fix an issue with json conversion

* fix json/form-data conversion

* fix json object/array conversion

* taking into account @NicolasCARPi remarks

* typo

* json to form-data array of objects conversion

* improve json/form-data conversion

* json conversion for anonymous arrays of objects

* accept only dot notation

* use of brackets for object properties is deprecated

* remove useless eslint comment

* add ifNotObject helper

* Merge branch 'dev' into add_feature_774

* fix ifNotObject helper

---------

Co-authored-by: Emmanuel Saracco <[email protected]>
  • Loading branch information
esaracco and Emmanuel Saracco authored Apr 19, 2023
1 parent c5e60ff commit 49eb49e
Show file tree
Hide file tree
Showing 11 changed files with 307 additions and 39 deletions.
12 changes: 10 additions & 2 deletions example/example.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,20 @@ function getUser () { }
*
* @apiBody {Number} age Age of the User
* @apiBody {String} name=Caroline Name of the User
* @apiBody {Object} extraInfo Date when user was hired
* @apiBody {Date} extraInfo.hireDate Date when user was hired
* @apiBody {Date} extraInfo.hireDateWithDefault=2021-09-01 Date when user was hired with default
* @apiBody {String} extraInfo.nickname Nickname of the user
* @apiBody {String[]} extraInfo.nicknames List of Users nicknames (Array of Strings)
* @apiBody {Boolean} extraInfo.isVegan=true Is the user vegan? (boolean with default)
* @apiBody {Boolean} extraInfo.isAlive Is the user alive? (boolean with no default)
* @apiBody {Object} extraInfo.secrets Secret object
* @apiBody {String} extraInfo.secrets.crush The user secret crush
* @apiBody {Number} extraInfo.secrets.hair=1000 Number of hair of user
* @apiBody {Object[]} extraInfo.secrets.deepSecrets Deep user secrets crush (array of objects)
* @apiBody {String} extraInfo.secrets.deepSecrets.key A deep user secret key
* @apiBody {Number} extraInfo.secrets.deepSecrets.number A deep user secret key
* @apiBody {String} extraInfo.secrets.deepSecrets.name.particle A deep user secret name particle with dot
* @apiBody {Boolean} extraInfo.isAlive Is the user alive? (boolean with no default)
* @apiBody {String} custom.property Custom property with dot
*
* @apiSuccess {Number} id The new Users-ID.
*
Expand Down Expand Up @@ -170,6 +177,7 @@ function createCity () { }
* @apiGroup Category (official)
* @apiDescription Get a category. Sample request on example.com here.
* @apiQuery {Number} id Category ID.
* @apiBody {String} custom.id Custom ID with dot.
*/
function getCategory () { }

Expand Down
2 changes: 2 additions & 0 deletions lib/core/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ Parser.prototype._parseBlockElements = function (indexApiBlocks, detectedElement

if (!elementParser) {
app.log.warn(`parser plugin '${element.name}' not found in block: '${blockIndex}' in file: '${filename}'`);
} else if (element.source.match(/[^\s]\[[^\]]/)) {
app.log.warn(`The use of square brackets for object properties is deprecated. Please use dot notation instead: "${element.source}"`);
} else {
app.log.debug('found @' + element.sourceName + ' in block: ' + blockIndex);

Expand Down
28 changes: 25 additions & 3 deletions lib/core/parsers/api_param.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const unindent = require('../utils/unindent');

let group = '';
const parents = {};

// Search: group, type, optional, fieldname, defaultValue, size, description
// Example: {String{1..4}} [user.name='John Doe'] Users fullname.
Expand Down Expand Up @@ -58,6 +59,17 @@ function _objectValuesToString (obj) {
return str;
}

function _getParentNode (field) {
let i = field.length;
while (i--) {
if (field.charAt(i) === '.') {
const path = field.substring(0, i);
const parentNode = parents[path];
if (parentNode) { return Object.assign({ path: path }, parentNode); }
}
}
}

const parseRegExp = new RegExp(_objectValuesToString(regExp));

const allowedValuesWithDoubleQuoteRegExp = /"[^"]*[^"]"/g;
Expand Down Expand Up @@ -98,13 +110,23 @@ function parse (content, source, defaultGroup) {
// Set global group variable
group = matches[1] || defaultGroup || 'Parameter';

const type = matches[2];
const field = matches[6];
const parentNode = _getParentNode(field);
const isArray = Boolean(type && type.indexOf('[]') !== -1);

// store the parent to assign it to its children later
if (type && type.indexOf('Object') !== -1) { parents[field] = { parentNode, field, type, isArray }; }

return {
group: group,
type: matches[2],
type: type,
size: matches[3],
allowedValues: allowedValues,
optional: !!((matches[5] && matches[5][0] === '[')), // eslint-disable-line no-extra-parens
field: matches[6],
optional: Boolean(matches[5] && matches[5][0] === '['),
parentNode: parentNode,
field: field,
isArray: isArray,
defaultValue: matches[7] || matches[8] || matches[9],
description: unindent(matches[10] || ''),
};
Expand Down
43 changes: 26 additions & 17 deletions template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,9 @@ <h2>{{__ "Query Parameter(s)"}}</h2>
<tbody>
{{#each params}}
<tr>
<td class="code">{{this.field}}&nbsp;&nbsp;
{{#if optional}}
<td class="code">
{{{nestObject this}}}
{{#if this.optional}}
<span class="label optional">{{__ "optional"}}</span>
{{else}}
{{#if ../template.showRequiredLabels}}
Expand Down Expand Up @@ -272,8 +273,9 @@ <h2>{{__ "Request Body"}}</h2>
<tbody>
{{#each params}}
<tr>
<td class="code">{{this.field}}&nbsp;&nbsp;
{{#if optional}}
<td class="code">
{{{nestObject this}}}
{{#if this.optional}}
<span class="label optional">{{__ "optional"}}</span>
{{else}}
{{#if ../template.showRequiredLabels}}
Expand Down Expand Up @@ -322,7 +324,8 @@ <h2>{{__ @key}}</h2>
<tbody>
{{#each this}}
<tr>
<td class="code">{{{splitFill field "." "&nbsp;&nbsp;"}}}
<td class="code">
{{{nestObject this}}}
{{#if optional}}
<span class="label optional">{{__ "optional"}}</span>
{{else}}
Expand Down Expand Up @@ -438,12 +441,13 @@ <h3>{{__ "Parameters"}}</h3>
<div class="{{../id}}-sample-request-param-fields {{../id}}-sample-header-content-type-fields">
{{#each this}}
<div class="form-group">
{{#ifNotObject type}}
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
<div class="input-group">
<div class="input-group-addon">{{{type}}}</div>
{{#if allowedValues}}
<div class="input-group-addon sample-request-select">
<select class="form-control" data-name="{{field}}" data-family="query" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<select class="form-control" data-name="{{dot2bracket this}}" data-family="query" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<option value="" class="empty">&lt;{{__ "No value"}}&gt;</option>
{{#each allowedValues}}
<option {{#ifCond ../defaultValue '===' this}} selected {{/ifCond}}value="{{{removeDblQuotes this}}}">{{{removeDblQuotes this}}}</option>
Expand All @@ -457,14 +461,15 @@ <h3>{{__ "Parameters"}}</h3>
class="{{#ifCond type '!==' 'Boolean'}}form-control{{/ifCond}} sample-request-param"
type="{{setInputType type}}"
value="{{#if defaultValue}}{{ defaultValue }}{{/if}}"
placeholder="{{#if defaultValue}}{{ defaultValue }}{{else}}{{field}}{{/if}}"
data-name="{{field}}"
placeholder="{{#if defaultValue}}{{ defaultValue }}{{/if}}"
data-name="{{dot2bracket this}}"
data-family="query"
data-group="{{@../index}}"
{{#if optional}}data-optional="true"{{/if}}>
</div></div>
{{/if}}
</div>
{{/ifNotObject}}
</div>
{{/each}}
</div>
Expand All @@ -477,12 +482,13 @@ <h3>{{__ "Query Parameters"}}</h3>
<div class="{{../id}}-sample-request-param-fields {{../id}}-sample-header-content-type-fields">
{{#each article.query}}
<div class="form-group">
{{#ifNotObject type}}
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}{{#if optional}} ({{__ "optional"}}){{/if}}</label>
<div class="input-group col-md-6">
<div class="input-group-addon">{{{type}}}</div>
{{#if allowedValues}}
<div class="input-group-addon sample-request-select">
<select class="form-control" data-name="{{field}}" data-family="query" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<select class="form-control" data-name="{{dot2bracket this}}" data-family="query" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<option value="" class="empty">&lt;{{__ "No value"}}&gt;</option>
{{#each allowedValues}}
<option {{#ifCond ../defaultValue '===' this}} selected {{/ifCond}}value="{{{removeDblQuotes this}}}">{{{removeDblQuotes this}}}</option>
Expand All @@ -496,14 +502,15 @@ <h3>{{__ "Query Parameters"}}</h3>
class="{{#ifCond type '!==' 'Boolean'}}form-control{{/ifCond}} sample-request-input"
type="{{setInputType type}}"
value="{{#if defaultValue}}{{ defaultValue }}{{/if}}"
placeholder="{{#if defaultValue}}{{ defaultValue }}{{else}}{{field}}{{/if}}"
data-name="{{field}}"
placeholder="{{#if defaultValue}}{{ defaultValue }}{{/if}}"
data-name="{{dot2bracket this}}"
data-family="query"
data-group="{{@../index}}"
{{#if optional}}data-optional="true"{{/if}}>
</div></div>
{{/if}}
</div>
{{/ifNotObject}}
</div>
{{/each}}
</div>
Expand Down Expand Up @@ -536,12 +543,13 @@ <h3>{{__ "Body"}}</h3>
<div hidden class="col-md-9" id="sample-request-body-form-input-{{this.id}}">
{{#each article.body}}
<div class="form-group">
{{#ifNotObject type}}
<label class="col-md-3 control-label" for="sample-request-param-field-{{field}}">{{field}}</label>
<div class="input-group">
<div class="input-group-addon">{{{type}}}</div>
{{#if allowedValues}}
<div class="input-group-addon sample-request-select">
<select class="form-control" data-name="{{field}}" data-family="body" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<select class="form-control" data-name="{{dot2bracket this}}" data-family="body" data-group="{{@../index}}" {{#if optional}}data-optional="true"{{/if}}>
<option value="" class="empty">&lt;{{__ "No value"}}&gt;</option>
{{#each allowedValues}}
<option {{#ifCond ../defaultValue '===' this}} selected {{/ifCond}}value="{{{removeDblQuotes this}}}">{{{removeDblQuotes this}}}</option>
Expand All @@ -556,14 +564,15 @@ <h3>{{__ "Body"}}</h3>
type="{{setInputType type}}"
value="{{#ifCond type '!==' 'Boolean'}}{{#if defaultValue}}{{ defaultValue }}{{/if}}{{/ifCond}}"
{{#if checked}}checked{{/if}}
placeholder="{{field}}"
placeholder="{{#if defaultValue}}{{ defaultValue }}{{/if}}"
data-family="body"
data-name="{{field}}"
data-name="{{dot2bracket this}}"
data-content-type="form"
{{#if optional}}data-optional="true"{{/if}}>
</div></div>
{{/if}}
</div>
{{/ifNotObject}}
</div>
{{/each}}
</div>
Expand Down Expand Up @@ -931,7 +940,7 @@ <h2>{{__ "Request Body"}}</h2>
{{#if typeSame}}
<tr>
<td class="code">
{{{splitFill source.field "." "&nbsp;&nbsp;"}}}
{{{nestObject source}}}
{{#if source.optional}}
{{#if compare.optional}} <span class="label label-optional">{{__ "optional"}}</span>
{{else}} <span class="label label-optional label-ins">{{__ "optional"}}</span>
Expand Down Expand Up @@ -964,7 +973,7 @@ <h2>{{__ "Request Body"}}</h2>
{{#if typeIns}}
<tr class="ins">
<td class="code">
{{{splitFill source.field "." "&nbsp;&nbsp;"}}}
{{{nestObject source}}}
{{#if source.optional}} <span class="label label-optional label-ins">{{__ "optional"}}</span>{{/if}}
</td>

Expand All @@ -984,7 +993,7 @@ <h2>{{__ "Request Body"}}</h2>
{{#if typeDel}}
<tr class="del">
<td class="code">
{{{splitFill compare.field "." "&nbsp;&nbsp;"}}}
{{{nestObject compare}}}
{{#if compare.optional}} <span class="label label-optional label-del">{{__ "optional"}}</span>{{/if}}
</td>

Expand Down
55 changes: 50 additions & 5 deletions template/src/hb_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,16 @@ export function register () {
return _handlebarsNewlineToBreak(text);
});

/**
* Test if the type is that of an object
*
* @param {String} type Type
* @returns {Boolean}
*/
Handlebars.registerHelper('ifNotObject', function (type, options) {
return type && type.indexOf('Object') !== 0 ? options.fn(this) : options.inverse(this);
});

// Test conditions
// Usage: {{#ifCond var1 '===' var2}}something{{/ifCond}}
Handlebars.registerHelper('ifCond', function (v1, operator, v2, options) {
Expand Down Expand Up @@ -170,11 +180,46 @@ export function register () {
});

/**
*
*/
Handlebars.registerHelper('splitFill', function (value, splitChar, fillChar) {
const splits = value.split(splitChar);
return new Array(splits.length).join(fillChar) + splits[splits.length - 1];
* Convert object dot-notation to form bracket-notation
*
* @param {String} field object
* @returns {String}
*/
Handlebars.registerHelper('dot2bracket', function (entry) {
const { parentNode, field, isArray } = entry;
let ret = '';
if (parentNode) {
let current = entry;
// loop on parents to build full object path
do {
const p = current.parentNode;
if (p.isArray) {
ret = `[]${ret}`;
}
if (p.parentNode) {
ret = `[${p.field.substring(p.parentNode.path.length + 1)}]${ret}`;
} else {
ret = p.field + ret;
}
current = current.parentNode;
} while (current.parentNode);
ret += `[${field.substring(parentNode.path.length + 1)}]`;
} else {
ret = field;
if (isArray) { ret += '[]'; }
}
return ret;
});

/**
* Indent object property based on its nesting level
*
* @param {Object} object
* @param {String} property
*/
Handlebars.registerHelper('nestObject', function (entry) {
const { parentNode, field } = entry;
return parentNode ? '&nbsp;&nbsp;'.repeat(parentNode.path.split('.').length) + field.substring(parentNode.path.length + 1) : field;
});

/**
Expand Down
Loading

0 comments on commit 49eb49e

Please sign in to comment.