normalize-machine-def.js 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891
  1. /**
  2. * Module dependencies
  3. */
  4. var util = require('util');
  5. var _ = require('@sailshq/lodash');
  6. var rttc = require('rttc');
  7. var ANCHOR_RULES = require('anchor/accessible/rules');
  8. var validateCodeNameStrict = require('./validate-code-name-strict');
  9. var getIsProductionWithoutDebug = require('./get-is-production-without-debug');
  10. var getMethodName = require('../get-method-name');
  11. var STRIP_COMMENTS_RX = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/mg;
  12. /**
  13. * normalizeMachineDef()
  14. *
  15. * Verify/normalize a (dry) node-machine definition, potentially coercing it a bit.
  16. *
  17. * > Note that this modifies properties in-place, as a direct reference!
  18. * > (This is for performance reasons, but it comes at a price. For an example
  19. * > of what that means, and to get an idea of what to be aware of when building
  20. * > higher-level tools/modules on top of this runner, see node-machine/machine/issues/42)
  21. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  22. * @param {Ref} nmDef [machine definition from implementor-land, MUTATED IN-PLACE!!!]
  23. * @param {String} implementationSniffingTactic
  24. *
  25. * @returns {Array} [implementation problems]
  26. * @of {String}
  27. * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  28. * > This private utility is based on the original implementation from machine@14.x and earlier:
  29. * > • https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/Machine.build.js
  30. * > • https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/private/verify-exit-definition.js
  31. */
  32. module.exports = function normalizeMachineDef(nmDef, implementationSniffingTactic){
  33. // Assert valid usage of this utility.
  34. // > (in production, we skip this stuff to save cycles. We can get away w/ that
  35. // > because we only include this here to provide better error msgs in development
  36. // > anyway)
  37. if (!getIsProductionWithoutDebug()) {
  38. if (!_.isObject(nmDef) || _.isArray(nmDef) || _.isFunction(nmDef)) {
  39. throw new Error('Consistency violation: `nmDef` must be a dictionary. But instead got: '+util.inspect(nmDef, {depth: 5}));
  40. }//•
  41. }//fi
  42. var implProblems = [];
  43. // Check `identity`, attempting to infer it if it is missing.
  44. var originalIdentity = nmDef.identity;
  45. if (nmDef.identity === undefined) {
  46. if (nmDef.friendlyName || nmDef.description) {
  47. nmDef.identity = (nmDef.friendlyName && _.kebabCase(nmDef.friendlyName)) || (nmDef.description && _.kebabCase(nmDef.description)).replace(/[^a-zA-Z0-9]/g, '') || undefined;
  48. }
  49. else {
  50. implProblems.push('Missing `identity`, and could not infer one. (No `friendlyName` or `description` either.)');
  51. // > (Note that in many cases, it is conventional to use a tool like `.pack()` to infer the `identity` from the filename.
  52. // > In those cases, the inferred-from-filename identity takes precedence, but it is still a good idea to make sure the
  53. // > `friendlyName` you choose is consistent.)
  54. }
  55. } else if (!_.isString(nmDef.identity) || nmDef.identity === '') {
  56. implProblems.push('Invalid `identity` property. (Please use a valid, non-empty string.)');
  57. // Wipe the `identity` to avoid breaking the very error message we'll use to display
  58. // this implementation problem in userland.
  59. delete nmDef.identity;
  60. }
  61. // Make sure the `identity` (whether it was derived or explicitly specified)
  62. // won't conflict with anything important.
  63. if (nmDef.identity !== undefined) {
  64. try {
  65. getMethodName(nmDef.identity);
  66. } catch (err) {
  67. switch (err.code) {
  68. case 'E_RESERVED':
  69. implProblems.push(
  70. 'Cannot use '+(originalIdentity?'specified':'derived')+' `identity` '+
  71. '("'+nmDef.identity+'"). '+err.message);
  72. default:
  73. throw err;
  74. }
  75. }
  76. }//fi
  77. // Set `implementationType` to the default, if not provided. Otherwise verify it.
  78. var isStringImplementation = _.isString(nmDef.implementationType) && nmDef.implementationType.match(/^string\:.+$/);
  79. if (nmDef.implementationType === undefined) {
  80. if (!_.isFunction(nmDef.fn)) {
  81. nmDef.implementationType = 'abstract';
  82. } else if (implementationSniffingTactic === 'analog') {
  83. nmDef.implementationType = 'analog';
  84. } else {
  85. var lastishParamIsExits = (function(){
  86. var fnStr = nmDef.fn.toString().replace(STRIP_COMMENTS_RX, '');
  87. var parametersAsString = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')'));
  88. return !! (
  89. parametersAsString.match(/,\s*exits\s*$/) ||
  90. parametersAsString.match(/,\s*exits\s*,\s*(env|meta)\s*$/)
  91. );
  92. })();//†
  93. if (lastishParamIsExits) {
  94. nmDef.implementationType = 'analog';
  95. } else {
  96. nmDef.implementationType = 'classical';
  97. }
  98. }
  99. } else {
  100. var ITPM_PREFIX = 'Invalid `implementationType` property. ';
  101. var CONCRETE_IMPLEMENTATION_TYPES = [
  102. 'abstract',
  103. 'composite',
  104. 'classical',
  105. 'analog'
  106. ];
  107. // Check for misspellings / mixups:
  108. if (_.contains(['analogue', 'fn'], nmDef.implementationType)) {
  109. implProblems.push(ITPM_PREFIX+' (Did you mean \'analog\'?)');
  110. } else if (_.contains(['circuit'], nmDef.implementationType)) {
  111. implProblems.push(ITPM_PREFIX+' (Did you mean \'composite\'?)');
  112. } else if (_.contains(['abstact', 'interface'], nmDef.implementationType)) {
  113. implProblems.push(ITPM_PREFIX+' (Did you mean \'abstract\'?)');
  114. } else if (_.contains(['classic', 'es8AsyncFunction', 'classicJsFunction'], nmDef.implementationType)) {
  115. implProblems.push(ITPM_PREFIX+' (Did you mean \'classical\'?)');
  116. } else {
  117. // Ensure implementationType is recognized.
  118. if (!isStringImplementation && !_.contains(CONCRETE_IMPLEMENTATION_TYPES, nmDef.implementationType)) {
  119. implProblems.push(ITPM_PREFIX+' (If specified, must either be a known, concrete implementation type like '+CONCRETE_IMPLEMENTATION_TYPES.join('/')+', or a code string+language declaration like \'string:js\' or \'string:c\'.)');
  120. }
  121. }
  122. }
  123. // Check `fn`.
  124. if (nmDef.implementationType === 'abstract' && nmDef.fn !== undefined) {
  125. implProblems.push('Should not declare a `fn` since implementation is \'abstract\'.');
  126. } else if (isStringImplementation && !_.isString(nmDef.fn)) {
  127. implProblems.push('Invalid `fn` property. (Should be a string, since implementation is declared as a code string.)');
  128. } else if (nmDef.fn === undefined) {
  129. implProblems.push('Missing the `fn` property.');
  130. } else if (!_.isFunction(nmDef.fn) && (nmDef.implementationType === 'classical' || nmDef.implementationType === 'analog')) {
  131. implProblems.push('Invalid `fn` property. (Please use a valid JavaScript function.)');
  132. }//fi
  133. if (_.isFunction(nmDef.fn)) {
  134. if (nmDef.fn.constructor.name === 'AsyncFunction') {
  135. if (nmDef.sync === true) {
  136. implProblems.push('Invalid `fn` property. (Cannot use `async function` since implementation is declared `sync: true`.)');
  137. }//fi
  138. nmDef._fnIsAsyncFunction = true;
  139. }
  140. }
  141. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  142. // FUTURE: Detect whether we're dealing with an arrow function.
  143. //
  144. // In the future, we may end up logging a warning about `this` probably not working
  145. // as expected. (Thanks @elmigranto and @ljharb! -- https://stackoverflow.com/a/38830947)
  146. // (See also https://github.com/ljharb/is-arrow-function/blob/df0c69e2188c7f2a7773116c370136d646fc193f/index.js)
  147. // ```
  148. // var isArrowFunction = (function(){
  149. // var fnStr = nmDef.fn.toString();
  150. // if (fnStr.length === 0) { return false; }
  151. // var RX_IS_NOT_ARROW_FN = /^\s*function/;
  152. // if (RX_IS_NOT_ARROW_FN.test(fnStr)) { return false; }
  153. // var RX_IS_ARROW_FN_WITH_PARENTHESES = /^\([^)]*\) *=>/;
  154. // if (!RX_IS_ARROW_FN_WITH_PARENTHESES.test(fnStr)) { return false; }
  155. // var RX_IS_ARROW_FN_WITHOUT_PARENTHESES = /^[^=]*=>/;
  156. // if (!RX_IS_ARROW_FN_WITHOUT_PARENTHESES.test(fnStr)) { return false; }
  157. // return true;
  158. // })();//†
  159. //
  160. // if (isArrowFunction) {
  161. // // …
  162. // }//fi
  163. // ```
  164. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  165. // Check `sync`
  166. if (nmDef.sync !== undefined) {
  167. if (!_.isBoolean(nmDef.sync)){
  168. implProblems.push('Invalid `sync` property. (If specified, must be boolean: `true` or `false`.)');
  169. }
  170. }
  171. // Check `timeout`
  172. if (nmDef.timeout !== undefined) {
  173. if (!_.isNumber(nmDef.timeout) || nmDef.timeout <= 0){
  174. implProblems.push('Invalid `timeout` property. (If specified, must be a number greater than zero.)');
  175. }
  176. }
  177. // Check `sideEffects`
  178. if (nmDef.sideEffects !== undefined) {
  179. if (nmDef.sideEffects !== 'cacheable' && nmDef.sideEffects !== 'idempotent' && nmDef.sideEffects !== '') {
  180. implProblems.push('Invalid `sideEffects` property. (If specified, must be \'cacheable\', \'idempotent\', or \'\'.)');
  181. }
  182. }
  183. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  184. // FUTURE: Recognize and check `humanLanguage`, making sure it's a iso-639-2 language code
  185. // (e.g. `humanLanguage: 'eng'`). This prop declares what human language documentation metadata
  186. // is written in.
  187. //
  188. // Reference of iso-639-2 codes:
  189. // https://www.loc.gov/standards/iso639-2/php/code_list.php
  190. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  191. // TOP-LEVEL COMPATIBILITY CHECKS:
  192. // ==========================================
  193. // `id`
  194. if (nmDef.id !== undefined) {
  195. implProblems.push('The `id` property is no longer supported. (Please use `identity` instead.)');
  196. }
  197. // `methodName`/`variableName`
  198. if (nmDef.methodName !== undefined || nmDef.variableName !== undefined) {
  199. implProblems.push('The `methodName`/`variableName` property is no longer supported. (Please use `identity` instead-- a method name will be derived when appropriate.)');
  200. }
  201. // `typeSafe`/`typesafe`
  202. if (nmDef.typeSafe !== undefined || nmDef.typesafe !== undefined) {
  203. implProblems.push('The `typeSafe` property is no longer supported. (Please use new opts in `buildWithCustomUsage()` instead.)');
  204. }
  205. // `cacheable`
  206. if (nmDef.cacheable !== undefined) {
  207. implProblems.push('The `cacheable` property is no longer supported. (Please use `sideEffects: \'cacheable\' instead.)');
  208. }
  209. // `idempotent`
  210. if (nmDef.idempotent !== undefined) {
  211. implProblems.push('The `idempotent` property is no longer supported. (Please use `sideEffects: \'idempotent\' instead.)');
  212. }
  213. // `defaultExit`
  214. if (nmDef.defaultExit !== undefined) {
  215. implProblems.push('Support for `defaultExit` was removed in machine@7.0.0. (Nowadays, you can simply use `success`.)');
  216. }
  217. // `catchallExit`/`catchAllExit`
  218. if (nmDef.catchallExit !== undefined || nmDef.catchAllExit !== undefined) {
  219. implProblems.push('Support for `catchallExit` was removed in machine@7.0.0. (Nowadays, you can simply use `error`.)');
  220. }
  221. // ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗ ██╗███╗ ██╗██████╗ ██╗ ██╗████████╗███████╗
  222. // ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝ ██║████╗ ██║██╔══██╗██║ ██║╚══██╔══╝██╔════╝██╗
  223. // ██║ ███████║█████╗ ██║ █████╔╝ ██║██╔██╗ ██║██████╔╝██║ ██║ ██║ ███████╗╚═╝
  224. // ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ██║██║╚██╗██║██╔═══╝ ██║ ██║ ██║ ╚════██║██╗
  225. // ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗ ██║██║ ╚████║██║ ╚██████╔╝ ██║ ███████║╚═╝
  226. // ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═════╝ ╚═╝ ╚══════╝
  227. //
  228. // > Note that we don't explicitly check `protect`, `contract`, `readOnly`, or other metadata
  229. // > like `description`, `moreInfoUrl`, etc.
  230. // Track inputs with indeterminate data types (to avoid inadvertently causing bad errors from
  231. // subsequent checks-- e.g. for the `like`/`itemOf` check that exits go through)
  232. var inputsWithIndeterminateTypes = [];
  233. // Sanitize input definitions.
  234. if (nmDef.inputs === undefined) {
  235. nmDef.inputs = {};
  236. }
  237. else if (!_.isObject(nmDef.inputs) || _.isArray(nmDef.inputs) || _.isFunction(nmDef.inputs)) {
  238. implProblems.push('Invalid `inputs`. (If specified, must be a dictionary-- i.e. plain JavaScript object like `{}`.)');
  239. }
  240. var inputCodeNames = Object.keys(nmDef.inputs);
  241. _.each(inputCodeNames, function(inputCodeName){
  242. var inputDef = nmDef.inputs[inputCodeName];
  243. var inputProblemPrefix = 'Invalid input definition ("'+inputCodeName+'"). ';
  244. // Make sure this code name won't conflict with anything important.
  245. var reasonCodeNameIsInvalid = validateCodeNameStrict(inputCodeName);
  246. if (reasonCodeNameIsInvalid) {
  247. implProblems.push('Invalid input name ("'+inputCodeName+'"). Please use something else instead, because this '+reasonCodeNameIsInvalid+'.');
  248. }//fi
  249. // Check basic structure.
  250. if (!_.isObject(inputDef) || _.isArray(inputDef) || _.isFunction(inputDef)) {
  251. implProblems.push(inputProblemPrefix+'Must be a dictionary-- i.e. plain JavaScript object like `{}`.');
  252. // Pretend this input def was an empty dictionary and keep going so that the "implProblems"
  253. // returned end up being more useful overall:
  254. inputDef = {};
  255. }
  256. // Check `type` & `example`
  257. // > • If a `type` is given, we verify that it's a valid type.
  258. // > • If an `example` is given with NO `type`, we verify that
  259. // > it is a formal RTTC exemplar.
  260. // > • And if both are provided, then verfy that the `example`
  261. // > is at least a valid hypothetical argin for the given type.
  262. var wasAbleToDeriveValidTypeSchema;
  263. if (inputDef.type !== undefined) {
  264. // ┬ ┬┌─┐┌─┐ ┌─┐─┐ ┬┌─┐┬ ┬┌─┐┬┌┬┐ ╔╦╗╦ ╦╔═╗╔═╗
  265. // ├─┤├─┤└─┐ ├┤ ┌┴┬┘├─┘│ ││ │ │ ║ ╚╦╝╠═╝║╣
  266. // ┴ ┴┴ ┴└─┘ └─┘┴ └─┴ ┴─┘┴└─┘┴ ┴ ╩ ╩ ╩ ╚═╝
  267. if (inputDef.type === 'dictionary' || inputDef.type === 'object' || inputDef.type === '{}'){
  268. implProblems.push(inputProblemPrefix+'Instead of specifying `type: '+util.inspect(inputDef.type,{depth:5})+'`, '+
  269. 'please use `type: {}`. (Or, if this data structure might sometimes contain functions or '+
  270. 'other nested data that isn\'t JSON-compatible, then use `type: \'ref\'` instead.)');
  271. }
  272. else if (inputDef.type === 'array' || inputDef.type === '[]' || _.isEqual(inputDef.type, [])){
  273. implProblems.push(inputProblemPrefix+'Instead of specifying `type: '+util.inspect(inputDef.type,{depth:5})+'`, '+
  274. 'please use `type: [\'ref\']`. Or you might opt to use something more specific-- for example, '+
  275. '`type: [\'number\']` indicates an array of numbers.');
  276. }
  277. else if (inputDef.type === 'function' || inputDef.type === 'lamda' || inputDef.type === 'lambda' || inputDef.type === '->' || inputDef.type === '=>' || _.isFunction(inputDef.type)){
  278. implProblems.push(inputProblemPrefix+'Instead of specifying `type: '+util.inspect(inputDef.type,{depth:5})+'`, '+
  279. 'please use `type: \'ref\'` and provide clarification in this input\'s `description`.');
  280. }
  281. else {
  282. try {
  283. rttc.validateStrict(inputDef.type, undefined);
  284. throw new Error('Consistency violation: Should never make it here. If you\'re seeing this message, you\'ve probably found a bug. Please let us know at https://sailsjs.com/bugs');
  285. } catch (err) {
  286. switch (err.code) {
  287. case 'E_INVALID':
  288. // `undefined` (which we passed in as the 2nd argument above) is always invalid
  289. // in RTTC vs. any type, so we can expect this outcome.
  290. // (We just used it as a pretend value anyway, as a hack to be able to reuse the code
  291. // in rttc.validateStrict().)
  292. wasAbleToDeriveValidTypeSchema = true;
  293. break;
  294. case 'E_UNKNOWN_TYPE':
  295. implProblems.push(inputProblemPrefix+'Unrecognized `type`. (Must be \'string\', \'number\', \'boolean\', \'json\' or \'ref\'. Or set it to a type schema like `[{id:\'number\', name: {givenName: \'Lisa\'}}]`.)');
  296. break;
  297. default:
  298. throw err;
  299. }
  300. }
  301. }
  302. if (wasAbleToDeriveValidTypeSchema && inputDef.example !== undefined) {
  303. // Since a `type` is also specified, just make sure `example` is a valid instance of that provided type schema.
  304. try {
  305. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  306. // FUTURE: If the example _could_ be interpreted as a valid RTTC exemplar schema, then make sure it intersects
  307. // vs. the declared type schema. (Otherwise this can be kinda confusing.) For instance, it would be weird
  308. // to have `type: 'json', example: '==='`. Technically, the string '===' is a valid JSON-compatible value,
  309. // but since it has special meaning, it's kinda weird to allow stuff to be specified this way. (In fact, it
  310. // might be just as valid of an idea to prevent special string notation (*/===/->) altogether when a `type`
  311. // is specified-- this needs more thought.)
  312. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  313. rttc.validateStrict(inputDef.type, inputDef.example);
  314. } catch (err) {
  315. switch (err.code) {
  316. case 'E_INVALID':
  317. implProblems.push(inputProblemPrefix+'Defined with `type: \'' + inputDef.type + '\'`, '+
  318. 'but the specified `example` is not strictly valid for that type. (Since both `type` and `example` '+
  319. 'are provided, they must be compatible.)');
  320. break;
  321. default:
  322. throw err;
  323. }
  324. }
  325. }//fi
  326. } else if (inputDef.example !== undefined){
  327. // ┌┐┌┌─┐ ┌┬┐┬ ┬┌─┐┌─┐ ╦╦ ╦╔═╗╔╦╗ ╔═╗═╗ ╦╔═╗╔╦╗╔═╗╦ ╔═╗
  328. // ││││ │ │ └┬┘├─┘├┤ ║║ ║╚═╗ ║ ║╣ ╔╩╦╝╠═╣║║║╠═╝║ ║╣
  329. // ┘└┘└─┘ ┴ ┴ ┴ └─┘┘ ╚╝╚═╝╚═╝ ╩ ╚═╝╩ ╚═╩ ╩╩ ╩╩ ╩═╝╚═╝
  330. // In the absense of `type`, `example` becomes more formal.
  331. // (It must be an RTTC exemplar which we'll then use to derive a `type`.)
  332. var isValidExemplar;
  333. try {
  334. rttc.validateExemplarStrict(inputDef.example, true);
  335. isValidExemplar = true;
  336. } catch (err) {
  337. switch (err.code) {
  338. case 'E_DEPRECATED_SYNTAX':
  339. case 'E_INVALID_EXEMPLAR':
  340. implProblems.push(inputProblemPrefix+'The specified `example` cannot be unambiguously '+
  341. 'interpreted as a particular data type. Please either change this input\'s `example` '+
  342. 'or provide an additional `type` to clear up the ambiguity.');
  343. break;
  344. default:
  345. throw err;
  346. }
  347. }
  348. if (isValidExemplar){
  349. inputDef.type = rttc.infer(inputDef.example);
  350. wasAbleToDeriveValidTypeSchema = true;
  351. }
  352. } else if (inputDef.isExemplar === true) {
  353. // ┬┌┐┌┌─┐┬ ┬┌┬┐ ┬┌┬┐┌─┐┌─┐┬ ┌─┐ ╦╔═╗ ╔═╗═╗ ╦╔═╗╔╦╗╔═╗╦ ╔═╗╦═╗
  354. // ││││├─┘│ │ │ │ │ └─┐├┤ │ ├┤ ║╚═╗ ║╣ ╔╩╦╝║╣ ║║║╠═╝║ ╠═╣╠╦╝
  355. // ┴┘└┘┴ └─┘ ┴ ┴ ┴ └─┘└─┘┴─┘└ ╩╚═╝ ╚═╝╩ ╚═╚═╝╩ ╩╩ ╩═╝╩ ╩╩╚═
  356. // If this input definition declares itself `isExemplar:true`, but doesn't specify a `type`
  357. // or `example` as a sort of "meta-schema", then use either "json" or "ref" as the `type`.
  358. // Either one works, but since "ref" is preferable for performance, we use it if we can.
  359. // > The only reason we don't use it ALL the time is because it allows a direct reference
  360. // > to the runtime data to be passed into this machine's `fn`. So we can only make this
  361. // > optimization if we're sure that the machine won't mutate the runtime argin provided for
  362. // > this input. But if this input definition has `readOnly: true`, then that means we DO
  363. // > have that guarantee, and so we can safely perform this optimization.
  364. inputDef.type = (inputDef.readOnly) ? 'ref' : 'json';
  365. wasAbleToDeriveValidTypeSchema = true;
  366. } else if (inputDef.custom) {
  367. // ┌┐┌┌─┐ ┌─┐─┐ ┬┌─┐┬ ┬┌─┐┬┌┬┐ ┌┬┐┬ ┬┌─┐┌─┐ ┌─┐┌─┐┬ ┬┌─┐┌┬┐┌─┐ ┌─┐┌┬┐┌─┐ ┌┐ ┬ ┬┌┬┐
  368. // ││││ │ ├┤ ┌┴┬┘├─┘│ ││ │ │ │ └┬┘├─┘├┤ └─┐│ ├─┤├┤ │││├─┤ ├┤ │ │ ├┴┐│ │ │
  369. // ┘└┘└─┘ └─┘┴ └─┴ ┴─┘┴└─┘┴ ┴ ┴ ┴ ┴ └─┘ └─┘└─┘┴ ┴└─┘┴ ┴┴ ┴ └─┘ ┴ └─┘┘ └─┘└─┘ ┴
  370. // ┬┌┬┐ ┬ ┬┌─┐┌─┐ ╔═╗╦ ╦╔═╗╔╦╗╔═╗╔╦╗ ┌─┐┌─┐ ┬┬ ┬┌─┐┌┬┐ ┌─┐┌─┐ ┬ ┬┬┌┬┐┬ ┬ ╦═╗╔═╗╔═╗
  371. // │ │ ├─┤├─┤└─┐ ║ ║ ║╚═╗ ║ ║ ║║║║ └─┐│ │ ││ │└─┐ │ │ ┬│ │ ││││ │ ├─┤ ╠╦╝║╣ ╠╣
  372. // ┴ ┴ ┴ ┴┴ ┴└─┘ ╚═╝╚═╝╚═╝ ╩ ╚═╝╩ ╩┘ └─┘└─┘ └┘└─┘└─┘ ┴ └─┘└─┘ └┴┘┴ ┴ ┴ ┴ ╩╚═╚═╝╚
  373. // (we don't really 100% know for sure that "ref" is the intent, but we still default to
  374. // `type: 'ref'` in this case anyway, just to try to be less annoying)
  375. inputDef.type = 'ref';
  376. } else if (
  377. inputDef.isCreditCard ||
  378. inputDef.isEmail ||
  379. inputDef.isHexColor ||
  380. (inputDef.isIn && _.all(inputDef.isIn, function(item){ return _.isString(item); })) ||
  381. inputDef.isIP ||
  382. inputDef.isURL ||
  383. inputDef.isUUID ||
  384. inputDef.maxLength ||
  385. inputDef.minLength ||
  386. inputDef.regex
  387. ) {
  388. // ┌┐┌┌─┐ ┌─┐─┐ ┬┌─┐┬ ┬┌─┐┬┌┬┐ ┌┬┐┬ ┬┌─┐┌─┐ ┌─┐┌─┐┬ ┬┌─┐┌┬┐┌─┐ ┌─┐┌┬┐┌─┐ ┌┐ ┬ ┬┌┬┐
  389. // ││││ │ ├┤ ┌┴┬┘├─┘│ ││ │ │ │ └┬┘├─┘├┤ └─┐│ ├─┤├┤ │││├─┤ ├┤ │ │ ├┴┐│ │ │
  390. // ┘└┘└─┘ └─┘┴ └─┴ ┴─┘┴└─┘┴ ┴ ┴ ┴ ┴ └─┘ └─┘└─┘┴ ┴└─┘┴ ┴┴ ┴┘ └─┘ ┴ └─┘o┘ └─┘└─┘ ┴
  391. // ┌┐ ┬ ┬ ┌─┐─┐ ┬┌─┐┌┬┐┬┌┐┌┬┌┐┌┌─┐ ┌─┐┌┐┌┌─┐┬ ┬┌─┐┬─┐ ┬─┐┬ ┬┬ ┌─┐┌─┐ ┬ ┬┌─┐ ┌─┐┌─┐┌┐┌
  392. // ├┴┐└┬┘ ├┤ ┌┴┬┘├─┤││││││││││││ ┬ ├─┤││││ ├─┤│ │├┬┘ ├┬┘│ ││ ├┤ └─┐ │││├┤ │ ├─┤│││
  393. // └─┘ ┴ └─┘┴ └─┴ ┴┴ ┴┴┘└┘┴┘└┘└─┘ ┴ ┴┘└┘└─┘┴ ┴└─┘┴└─ ┴└─└─┘┴─┘└─┘└─┘┘ └┴┘└─┘ └─┘┴ ┴┘└┘
  394. // ┌┬┐┌─┐┌┬┐┌─┐┬─┐┌┬┐┬┌┐┌┌─┐ ┌┬┐┬ ┬┌─┐┌┬┐ ┬┌┬┐ ╔╦╗╦ ╦╔═╗╔╦╗ ╔╗ ╔═╗ ╔═╗ ╔═╗╔╦╗╦═╗╦╔╗╔╔═╗
  395. // ││├┤ │ ├┤ ├┬┘│││││││├┤ │ ├─┤├─┤ │ │ │ ║║║║ ║╚═╗ ║ ╠╩╗║╣ ╠═╣ ╚═╗ ║ ╠╦╝║║║║║ ╦
  396. // ─┴┘└─┘ ┴ └─┘┴└─┴ ┴┴┘└┘└─┘ ┴ ┴ ┴┴ ┴ ┴ ┴ ┴ ╩ ╩╚═╝╚═╝ ╩ ╚═╝╚═╝ ╩ ╩ ╚═╝ ╩ ╩╚═╩╝╚╝╚═╝o
  397. // (not even "ref" or "json"! i.e. to be valid, any argin would HAVE to be a string.)
  398. inputDef.type = 'string';
  399. } else if (
  400. inputDef.max ||
  401. inputDef.min ||
  402. inputDef.isInteger
  403. ) {
  404. // ┌┐┌┌─┐ ┌─┐─┐ ┬┌─┐┬ ┬┌─┐┬┌┬┐ ┌┬┐┬ ┬┌─┐┌─┐ ┌─┐┌─┐┬ ┬┌─┐┌┬┐┌─┐ ┌─┐┌┬┐┌─┐ ┌┐ ┬ ┬┌┬┐
  405. // ││││ │ ├┤ ┌┴┬┘├─┘│ ││ │ │ │ └┬┘├─┘├┤ └─┐│ ├─┤├┤ │││├─┤ ├┤ │ │ ├┴┐│ │ │
  406. // ┘└┘└─┘ └─┘┴ └─┴ ┴─┘┴└─┘┴ ┴ ┴ ┴ ┴ └─┘ └─┘└─┘┴ ┴└─┘┴ ┴┴ ┴┘ └─┘ ┴ └─┘o┘ └─┘└─┘ ┴
  407. // ┌┐ ┬ ┬ ┌─┐─┐ ┬┌─┐┌┬┐┬┌┐┌┬┌┐┌┌─┐ ┌─┐┌┐┌┌─┐┬ ┬┌─┐┬─┐ ┬─┐┬ ┬┬ ┌─┐┌─┐ ┬ ┬┌─┐ ┌─┐┌─┐┌┐┌
  408. // ├┴┐└┬┘ ├┤ ┌┴┬┘├─┤││││││││││││ ┬ ├─┤││││ ├─┤│ │├┬┘ ├┬┘│ ││ ├┤ └─┐ │││├┤ │ ├─┤│││
  409. // └─┘ ┴ └─┘┴ └─┴ ┴┴ ┴┴┘└┘┴┘└┘└─┘ ┴ ┴┘└┘└─┘┴ ┴└─┘┴└─ ┴└─└─┘┴─┘└─┘└─┘┘ └┴┘└─┘ └─┘┴ ┴┘└┘
  410. // ┌┬┐┌─┐┌┬┐┌─┐┬─┐┌┬┐┬┌┐┌┌─┐ ┌┬┐┬ ┬┌─┐┌┬┐ ┬┌┬┐ ╔╦╗╦ ╦╔═╗╔╦╗ ╔╗ ╔═╗ ╔═╗ ╔╗╔╦ ╦╔╦╗╔╗ ╔═╗╦═╗
  411. // ││├┤ │ ├┤ ├┬┘│││││││├┤ │ ├─┤├─┤ │ │ │ ║║║║ ║╚═╗ ║ ╠╩╗║╣ ╠═╣ ║║║║ ║║║║╠╩╗║╣ ╠╦╝
  412. // ─┴┘└─┘ ┴ └─┘┴└─┴ ┴┴┘└┘└─┘ ┴ ┴ ┴┴ ┴ ┴ ┴ ┴ ╩ ╩╚═╝╚═╝ ╩ ╚═╝╚═╝ ╩ ╩ ╝╚╝╚═╝╩ ╩╚═╝╚═╝╩╚═o
  413. // (not even "ref" or "json"! i.e. to be valid, any argin would HAVE to be a number.)
  414. inputDef.type = 'number';
  415. } else {
  416. implProblems.push(inputProblemPrefix+'Must have `type`, or at least some more information. (If you aren\'t sure what to use, just go with `type: \'ref\'.)');
  417. }//fi
  418. // If the data type could not be determined, then track this input.
  419. if (!wasAbleToDeriveValidTypeSchema) {
  420. inputsWithIndeterminateTypes.push(inputCodeName);
  421. }
  422. // Check `isExemplar`.
  423. if (inputDef.isExemplar !== undefined){
  424. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  425. // FUTURE: Maybe remove support for `isExemplar` in favor of the recently-
  426. // proposed "isType" prop? Note that this would require some changes in
  427. // other tooling though...
  428. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  429. if (!_.isBoolean(inputDef.isExemplar)){
  430. implProblems.push(inputProblemPrefix+'Invalid `isExemplar` property. (If specified, must be boolean: `true` or `false`.)');
  431. }
  432. }
  433. // Check `required`.
  434. if (inputDef.required !== undefined){
  435. if (!_.isBoolean(inputDef.required)){
  436. implProblems.push(inputProblemPrefix+'Invalid `required` property. (If specified, must be boolean: `true` or `false`.)');
  437. }
  438. }
  439. // Check `allowNull`.
  440. if (inputDef.allowNull !== undefined){
  441. if (!_.isBoolean(inputDef.allowNull)){
  442. implProblems.push(inputProblemPrefix+'Invalid `allowNull` property. (If specified, must be boolean: `true` or `false`.)');
  443. }
  444. else if (inputDef.allowNull === true && inputDef.required === true) {
  445. implProblems.push(inputProblemPrefix+'Defined with both `allowNull: true` and `required: true`... but that wouldn\'t make any sense. (These settings are mutually exclusive.)');
  446. }
  447. else if (inputDef.allowNull === true && inputDef.isExemplar === true) {
  448. implProblems.push(inputProblemPrefix+'Defined with both `allowNull: true` and `isExemplar: true`... but that wouldn\'t make any sense. (These settings are mutually exclusive.)');
  449. }
  450. if (wasAbleToDeriveValidTypeSchema && (inputDef.type === 'json' || inputDef.type === 'ref')) {
  451. implProblems.push(inputProblemPrefix+
  452. 'Defined with `allowNull: true`, which is unnecessary in conjunction with this input\'s '+
  453. 'declared `type`, "'+inputDef.type+'". (With this type restriction, `null` would always '+
  454. 'be valid anyway.)');
  455. }
  456. }
  457. // Check `defaultsTo`.
  458. if (inputDef.defaultsTo !== undefined){
  459. if (inputDef.required === true) {
  460. implProblems.push('The `defaultsTo` property cannot be used in conjunction with `required: true`. (Only optional inputs may have a default value.)');
  461. } else if (inputDef.allowNull === true && _.isNull(inputDef.defaultsTo)) {
  462. // Always tolerate `defaultsTo: null` if this input has `allowNull: true`
  463. } else if (wasAbleToDeriveValidTypeSchema) {
  464. try {
  465. rttc.validateStrict(inputDef.type, inputDef.defaultsTo);
  466. } catch (err) {
  467. switch (err.code) {
  468. case 'E_INVALID':
  469. implProblems.push(
  470. inputProblemPrefix+'Invalid `defaultsTo` property. (Input is defined with '+
  471. '`type: \'' + inputDef.type + '\'`, but the specified `defaultsTo` is not valid for '+
  472. 'that type.)');
  473. break;
  474. default:
  475. throw err;
  476. }
  477. }
  478. }
  479. }
  480. // Check anchor validation rules.
  481. // (Note that, at this point, we know we have a valid `type` at least.)
  482. _.each(ANCHOR_RULES, function(ruleInfo, ruleName){
  483. // Ignore unspecified rules.
  484. // > Note that if the rule is `undefined`, we still process it-- so long as
  485. // > it is specified. (This is to match the behavior of how these rules are
  486. // > validated against at runtime, where we run the validation if the key
  487. // > is present, regardless of whether or not it is `undefined`.)
  488. if (!inputDef.hasOwnProperty(ruleName)) {
  489. return;
  490. }
  491. // Special exception for the "custom" rule, which works with _anything_.
  492. if (ruleName === 'custom') { return; }
  493. // But otherwise, we check that the input's `type` is supported:
  494. if (wasAbleToDeriveValidTypeSchema && !_.contains(ruleInfo.expectedTypes, inputDef.type)) {
  495. implProblems.push(inputProblemPrefix+'Cannot use `'+ruleName+'` with that type of input.');
  496. }
  497. // And check that the configuration of the rule is valid.
  498. if (!_.isFunction(ruleInfo.checkConfig)) { throw new Error('Consistency violation: Rule is missing `checkConfig` function! (Could an out-of-date dependency still be installed? (To resolve, try running `rm -rf node_modules && rm package-lock.json && npm install`. If that doesn\'t work, please report this at https://sailsjs.com/bugs.)'); }
  499. var ruleConfigError = ruleInfo.checkConfig(inputDef[ruleName]);
  500. if (ruleConfigError) {
  501. implProblems.push(inputProblemPrefix+'Configuration for `'+ruleName+'` is invalid: ' + ruleConfigError);
  502. }
  503. });//∞
  504. // Check `readOnly`.
  505. if (inputDef.readOnly !== undefined){
  506. if (!_.isBoolean(inputDef.readOnly)){
  507. implProblems.push(inputProblemPrefix+'Invalid `readOnly` property. (If specified, must be boolean: `true` or `false`.)');
  508. }
  509. }
  510. // Check `protect`.
  511. if (inputDef.protect !== undefined){
  512. if (!_.isBoolean(inputDef.protect)){
  513. implProblems.push(inputProblemPrefix+'Invalid `protect` property. (If specified, must be boolean: `true` or `false`.)');
  514. }
  515. }
  516. // COMMON TYPO CHECKS:
  517. // ==========================================
  518. // `defaultsto` (lowercase)
  519. if (inputDef.defaultsto !== undefined){
  520. implProblems.push(inputProblemPrefix+'Unrecognized property, "defaultsto". Did you mean `defaultsTo`? (camelcase, with a capital "T")');
  521. }
  522. // `allownull` (lowercase)
  523. if (inputDef.allownull !== undefined){
  524. implProblems.push(inputProblemPrefix+'Unrecognized property, "allownull". Did you mean `allowNull`? (camelcase, with a capital "N")');
  525. }
  526. // `outputType`/`outputExample`
  527. if (inputDef.outputType !== undefined || inputDef.outputExample !== undefined) {
  528. implProblems.push(inputProblemPrefix+'`outputType` and `outputExample` are not supported for inputs (only exits). (Tip: This error usually surfaces from mistakes when copying/pasting.)');
  529. }
  530. // INPUT DEF COMPATIBILITY CHECKS:
  531. // ==========================================
  532. // `id`
  533. if (inputDef.id !== undefined) {
  534. implProblems.push(inputProblemPrefix+'`id` is no longer supported. (Please use the key within `inputs`-- aka the "input code name"-- instead.)');
  535. }
  536. // `like`/`itemOf`/`getExample`
  537. if (inputDef.like !== undefined || inputDef.itemOf !== undefined || inputDef.getExample !== undefined) {
  538. implProblems.push(inputProblemPrefix+'`like`, `itemOf`, and `getExample` are not supported for inputs (only exits).');
  539. }
  540. // `typeclass`
  541. if (inputDef.typeclass !== undefined) {
  542. implProblems.push(inputProblemPrefix+'`typeclass` is no longer supported. (Please use `type` instead.)');
  543. }
  544. // `validate`
  545. if (inputDef.validate !== undefined) {
  546. implProblems.push(inputProblemPrefix+'`validate` is no longer supported. (Please use `custom` instead.)');
  547. }
  548. });//∞ </each input>
  549. // ██████╗██╗ ██╗███████╗ ██████╗██╗ ██╗ ███████╗██╗ ██╗██╗████████╗███████╗
  550. // ██╔════╝██║ ██║██╔════╝██╔════╝██║ ██╔╝ ██╔════╝╚██╗██╔╝██║╚══██╔══╝██╔════╝██╗
  551. // ██║ ███████║█████╗ ██║ █████╔╝ █████╗ ╚███╔╝ ██║ ██║ ███████╗╚═╝
  552. // ██║ ██╔══██║██╔══╝ ██║ ██╔═██╗ ██╔══╝ ██╔██╗ ██║ ██║ ╚════██║██╗
  553. // ╚██████╗██║ ██║███████╗╚██████╗██║ ██╗ ███████╗██╔╝ ██╗██║ ██║ ███████║╚═╝
  554. // ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝
  555. //
  556. // > Note that we don't explicitly check metadata like `description`, `moreInfoUrl`, etc.
  557. // Sanitize exit definitions.
  558. if (nmDef.exits === undefined) {
  559. nmDef.exits = {};
  560. }
  561. else if (!_.isObject(nmDef.exits) || _.isArray(nmDef.exits) || _.isFunction(nmDef.exits)) {
  562. implProblems.push('Invalid `exits`. (If specified, must be a dictionary-- i.e. plain JavaScript object like `{}`.)');
  563. }
  564. var exitCodeNames = Object.keys(nmDef.exits);
  565. _.each(exitCodeNames, function (exitCodeName){
  566. var exitDef = nmDef.exits[exitCodeName];
  567. var exitProblemPrefix = 'Invalid exit definition ("'+exitCodeName+'"). ';
  568. // Make sure this code name won't conflict with anything important.
  569. var reasonCodeNameIsInvalid = validateCodeNameStrict(exitCodeName);
  570. if (reasonCodeNameIsInvalid) {
  571. implProblems.push('Invalid exit name ("'+exitCodeName+'"). Please use something else instead, because this '+reasonCodeNameIsInvalid+'.');
  572. }//fi
  573. if (!_.isObject(exitDef) || _.isArray(exitDef) || _.isFunction(exitDef)) {
  574. implProblems.push(exitProblemPrefix+'Must be a dictionary-- i.e. plain JavaScript object like `{}`.');
  575. // Pretend this exit def was an empty dictionary and keep going so that the "implProblems"
  576. // returned end up being more useful overall:
  577. exitDef = {};
  578. }//•
  579. // Check `outputType` / `outputExample` / `like` / `itemOf` / `getExample`
  580. //
  581. // > (Note that `like`, `itemOf`, and `getExample` resolution is still pretty generic here--
  582. // > when `.exec()` is called, this is taken further to use the runtime values. At this point,
  583. // > we're just validating that the provided stuff in the definition is meaningful and relevant.)
  584. // >
  585. // > For reference, the original implementation:
  586. // > • https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/private/verify-exit-definition.json
  587. // > • https://github.com/node-machine/machine/blob/3cbdf60f7754ef47688320d370ef543eb27e36f0/lib/Machine.build.js
  588. var outputDeclarationStyles = _.intersection(Object.keys(exitDef), [
  589. 'outputType',
  590. 'outputExample',
  591. 'like',
  592. 'itemOf',
  593. 'getExample'
  594. ]);
  595. // Now that we've counted the number of output declarations, we can analyze:
  596. // • If 0, this exit is void (has no guaranteed output).
  597. // • If 1, this exit has exactly one style of output declaration.
  598. // • If exit has both "outputType" and "outputExample", then that's technically OK.
  599. // It just means that this exit has two (valid) co-existing styles of output declaration.
  600. // (In this case, we interpret the output example as merely advisory.)
  601. //
  602. // But otherwise, this exit has conflicting output declarations.
  603. // And that's not ok:
  604. if (outputDeclarationStyles.length > 1 && !(outputDeclarationStyles.length === 2 && _.contains(outputDeclarationStyles, 'outputType') && _.contains(outputDeclarationStyles, 'outputExample'))) {
  605. implProblems.push(exitProblemPrefix+'Conflicting output declarations: '+outputDeclarationStyles+'. Please use one or the other.');
  606. }
  607. // If this is the error exit, it should either (A) not declare any kind of output or (B) declare a `'ref'` output (i.e. an Error instance)
  608. // (keep in mind the error exit is shared by built-in functionality such as argin validation, timeouts, uncaught exception catching, and more)
  609. else if (exitCodeName === 'error' && outputDeclarationStyles.length >= 1 && exitDef.outputType !== 'ref' && exitDef.outputExample !== '===') {
  610. implProblems.push(exitProblemPrefix+'It\'s best not to declare a specific output type for the "error" exit at all-- but if you have no other option, then please use `outputType: \'ref\'`.');
  611. }
  612. // Otherwise the basic structure makes sense, so we'll dive in a bit deeper:
  613. else {
  614. // Assuming everything checks out, we then check the output declaration(s) for correctness:
  615. if (exitDef.outputType !== undefined) {
  616. if (exitDef.outputType === 'dictionary' || exitDef.outputType === 'object' || exitDef.outputType === '{}'){
  617. implProblems.push(exitProblemPrefix+'Instead of specifying `outputType: '+util.inspect(exitDef.outputType,{depth:5})+'`, '+
  618. 'please use `outputType: {}`. (Or, if this data structure might sometimes contain functions or '+
  619. 'other nested data that isn\'t JSON-compatible, then use `outputType: \'ref\'` instead.)');
  620. }
  621. else if (exitDef.outputType === 'array' || exitDef.outputType === '[]' || _.isEqual(exitDef.outputType, [])){
  622. implProblems.push(exitProblemPrefix+'Instead of specifying `outputType: '+util.inspect(exitDef.outputType,{depth:5})+'`, '+
  623. 'please use `outputType: [\'ref\']`. Or you might opt to use something more specific-- for example, '+
  624. '`outputType: [\'number\']` indicates that this exit should yield an array of numbers.');
  625. }
  626. else if (exitDef.outputType === 'function' || exitDef.outputType === 'lamda' || exitDef.outputType === 'lambda' || exitDef.outputType === '->' || exitDef.outputType === '=>' || _.isFunction(exitDef.outputType)){
  627. implProblems.push(exitProblemPrefix+'Instead of specifying `outputType: '+util.inspect(exitDef.outputType,{depth:5})+'`, '+
  628. 'please use `outputType: \'ref\'` and provide clarification in this exit\'s `outputDescription`.');
  629. }
  630. else {
  631. // Verify we've got a valid type schema.
  632. try {
  633. rttc.validateStrict(exitDef.outputType, undefined);
  634. } catch (err) {
  635. switch (err.code) {
  636. case 'E_INVALID':
  637. // `undefined` (which we passed in as the 2nd argument above) is always invalid
  638. // in RTTC vs. any type, so we can expect this outcome.
  639. // (We just used it as a pretend value anyway, as a hack to be able to reuse the code
  640. // in rttc.validateStrict().)
  641. break;
  642. case 'E_UNKNOWN_TYPE':
  643. implProblems.push(exitProblemPrefix+'Unrecognized `outputType`. (Must be \'string\', \'number\', \'boolean\', \'json\' or \'ref\'. Or set it to a type schema like `{total: \'number\', entries: [{}]}` or `[{id:\'number\', name: {givenName: \'Lisa\'}}]`.)');
  644. break;
  645. default:
  646. throw err;
  647. }
  648. }
  649. }
  650. // Since an `outputType` is also specified, just make sure `outputExample` is a valid instance of that provided type schema.
  651. if (exitDef.outputExample !== undefined) {
  652. try {
  653. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  654. // FUTURE: If the example _could_ be interpreted as a valid RTTC exemplar schema, then make sure it is valid
  655. // vs. the declared type schema. (Otherwise this can be kinda confusing.) For instance, it would be weird
  656. // to have `type: 'json', example: '==='`. Technically, the string '===' is a valid JSON-compatible value,
  657. // but since it has special meaning, it's kinda weird to allow stuff to be specified this way. (In fact, it
  658. // might be just as valid of an idea to prevent special string notation (*/===/->) altogether when a `type`
  659. // is specified-- this needs more thought.)
  660. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  661. rttc.validateStrict(exitDef.outputType, exitDef.outputExample);
  662. } catch (err) {
  663. switch (err.code) {
  664. case 'E_INVALID':
  665. implProblems.push(exitProblemPrefix+'Defined with `outputType: \'' + exitDef.outputType + '\'`, '+
  666. 'but the specified `outputExample` is not strictly valid for that type. (Since both `outputType` and `outputExample` '+
  667. 'are provided, they must be compatible.)');
  668. break;
  669. default:
  670. throw err;
  671. }
  672. }
  673. }//fi
  674. } else if (exitDef.outputExample !== undefined) {
  675. // In the absense of `outputType`, `outputExample` becomes more formal.
  676. // (It must be an RTTC exemplar which we'll then use to derive an `outputType`.)
  677. try {
  678. rttc.validateExemplarStrict(exitDef.outputExample, true);
  679. } catch (err) {
  680. switch (err.code) {
  681. case 'E_DEPRECATED_SYNTAX':
  682. case 'E_INVALID_EXEMPLAR':
  683. implProblems.push(exitProblemPrefix+'The specified `outputExample` cannot be unambiguously '+
  684. 'interpreted as a particular data type. Please either change this exit\'s `outputExample` '+
  685. 'or provide an additional `outputType` to clear up the ambiguity.');
  686. break;
  687. default:
  688. throw err;
  689. }
  690. }
  691. exitDef.outputType = rttc.infer(exitDef.outputExample);
  692. } else if (exitDef.like !== undefined || exitDef.itemOf !== undefined) {
  693. var declarationStyle = exitDef.itemOf ? 'itemOf' : 'like';
  694. var referencedInputCodeName = exitDef.itemOf || exitDef.like;
  695. var referencedInput = nmDef.inputs[referencedInputCodeName];
  696. if (!referencedInput) {
  697. implProblems.push(exitProblemPrefix+'If specified, `'+declarationStyle+'` should refer to one of the declared inputs. But no such input (`'+referencedInputCodeName+'`) exists.');
  698. } else if (declarationStyle === 'itemOf' && !_.contains(inputsWithIndeterminateTypes, referencedInputCodeName)) {
  699. if (!_.isArray(referencedInput.type)) {
  700. implProblems.push(exitProblemPrefix+'If specified, `'+declarationStyle+'` should refer to an input that accepts only arrays. But the referenced input (`'+referencedInputCodeName+'`) accepts '+rttc.getNounPhrase(_.isString(referencedInput.type)? referencedInput.type : 'dictionary', {plural: true})+'.');
  701. }
  702. }
  703. } else if (exitDef.getExample !== undefined) {
  704. if (!_.isFunction(exitDef.getExample)) {
  705. implProblems.push(exitProblemPrefix+'If specified, `getExample` should be a function.');
  706. }
  707. }
  708. }
  709. // EXIT DEF COMMON MISTAKES CHECK:
  710. // ==========================================
  711. // `defaultsTo`
  712. if (exitDef.defaultsTo !== undefined) {
  713. implProblems.push(exitProblemPrefix+'`defaultsTo` is not supported for exits. (If you want this to be the default output, then it\'s up to you to be sure and send this value through this exit in your implementation.)');
  714. // FUTURE: Consider adding support for this (Useful for redirects in MaA, for example.)
  715. // But we would want to name it something more explicit-- e.g. `outputDefaultsTo`
  716. }
  717. // `type`
  718. if (exitDef.type !== undefined) {
  719. implProblems.push(exitProblemPrefix+'`type` is not supported for exits. (For clarity, please use `outputType`.)');
  720. }
  721. // `example`
  722. if (exitDef.example !== undefined) {
  723. implProblems.push(exitProblemPrefix+'`example` is not supported for exits. (For clarity, please use `outputExample`.)');
  724. }
  725. // `isExemplar`
  726. if (exitDef.isExemplar !== undefined) {
  727. implProblems.push(exitProblemPrefix+'`isExemplar` is not supported for exits. Are you sure that\'s what you actually meant?');
  728. }
  729. // `allowNull`
  730. if (exitDef.allowNull !== undefined) {
  731. implProblems.push(exitProblemPrefix+'`allowNull` is not supported for exits. Use `outputType: \'ref\'` or `outputType: \'json\'` if you want to tolerate a `null` values as results from this exit.');
  732. }
  733. // `protect`
  734. if (exitDef.protect !== undefined) {
  735. implProblems.push(exitProblemPrefix+'`protect` is not supported for exits. (If you have a use case that would benefit from this, please open a discussion in the newsgroup.)');
  736. }
  737. // `readOnly`
  738. if (exitDef.readOnly !== undefined) {
  739. implProblems.push(exitProblemPrefix+'`readOnly` is not supported for exits. (If you have a use case that would benefit from this, please open a discussion in the newsgroup.)');
  740. }
  741. // anchor validation rules
  742. _.each(Object.keys(ANCHOR_RULES), function(ruleName){
  743. if (exitDef[ruleName]) {
  744. implProblems.push(exitProblemPrefix+'`'+ruleName+'` is not supported for exits. (If you are interested in validating implementation vs. interface using validations like these, please open up a discussion in the newsgroup.)');
  745. }
  746. });//∞
  747. // EXIT DEF COMPATIBILITY CHECKS:
  748. // ==========================================
  749. // `id`
  750. if (exitDef.id !== undefined) {
  751. implProblems.push(exitProblemPrefix+'`id` is no longer supported. (Please use the key within `exits`-- aka the "exit code name"-- instead.)');
  752. }
  753. // `typeclass`
  754. if (exitDef.typeclass !== undefined) {
  755. implProblems.push(exitProblemPrefix+'`typeclass` is no longer supported. (Please use `outputType` instead.)');
  756. }
  757. // `validate`
  758. if (exitDef.validate !== undefined) {
  759. implProblems.push(exitProblemPrefix+'The `validate` function is not supported anymore.');
  760. }
  761. });//∞ </each exit>
  762. // If "error" & "success" weren't provided in the definition, then add them.
  763. // > Note: This is actually the recommended usage-- there's no reason to explicitly
  764. // > specify an exit unless you actually need to customize it.
  765. if (!nmDef.exits.error){
  766. nmDef.exits.error = { description: 'An unexpected error occurred.' };
  767. }
  768. if (!nmDef.exits.success){
  769. nmDef.exits.success = { description: 'Done.' };
  770. }
  771. return implProblems;
  772. };