coerce-exemplar.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /**
  2. * Module dependencies
  3. */
  4. var _ = require('@sailshq/lodash');
  5. var typeInfo = require('./type-info');
  6. var dehydrate = require('./dehydrate');
  7. var union = require('./union');
  8. /**
  9. * coerceExemplar()
  10. *
  11. * Convert a normal JavaScript value into the _most specific_ RTTC exemplar
  12. * which would accept it.... more or less.
  13. *
  14. * > --------------------------------------------------------------------------------
  15. * > WARNING: While this logic is OK at understanding functions, it's not really
  16. * > smart enough to figure out cases where it would need refs. It dehydrates
  17. * > everything first, meaning it ensures everything is JSON compatible by ripping
  18. * > out circular references and replacing replacing streams, buffers, and
  19. * > RttcRefPlaceholder instances with `null`. It also rips out `undefined` array
  20. * > items, and dictionary keys with `undefined` values (meaning that there are
  21. * > never any _nested_ `undefined`s by the time it gets around to building the
  22. * > exemplar.) And finally, it converts all Dates, RegExps, and Error instances
  23. * > to strings.
  24. * > --------------------------------------------------------------------------------
  25. *
  26. * @param {Anything} value
  27. * A normal JavaScript value.
  28. *
  29. * @param {Boolean} allowSpecialSyntax
  30. * If set, string literals which look like special RTTC exemplar syntax (->, *, and ===)
  31. * take on their traditional symbolism; meaning they will NOT be "exemplified"-- that is,
  32. * replaced with other strings: ('an arrow symbol', 'a star symbol', and '3 equal signs').
  33. * > WARNING: Use with care! Remember other things need to be exemplified too!
  34. * > (e.g. consider the `null` literal)
  35. * @default false
  36. *
  37. * @param {Boolean} treatTopLvlUndefinedAsRef
  38. * If set, if the provided value is `undefined`, it will be treated as a ref.
  39. * Note that this purely for backwards compatibility, since as of rttc@9.3.0,
  40. * the `===` exemplar no longer accepts `undefined`; and its base value is now `null`.
  41. * > The default value for this flag will be changed to `false` in a future release.
  42. * @default true
  43. *
  44. * @param {boolean} useStrict
  45. * If set, the pattern exemplar for any multi-item arrays within the provided value will be
  46. * determined by coercing and unioning the array items using strict validation rules (e.g.
  47. * `32 ∪ 'foo' <=> '*'`). Also do not squish Infinity/-Infinity/NaN.
  48. * Otherwise, loose validation rules will be used instead (e.g. `32 ∪ 'foo' <=> 'foo`),
  49. * and Infinity/-Infinity/NaN will be squished to 0.
  50. * > The default value for this flag will be changed to `false` in a future release.
  51. * @default true
  52. *
  53. * @returns {JSON}
  54. * An RTTC exemplar.
  55. */
  56. module.exports = function coerceExemplar (value, allowSpecialSyntax, treatTopLvlUndefinedAsRef, useStrict) {
  57. // Default `treatTopLvlUndefinedAsRef` to `true`.
  58. // (will be changed to false in a future release)
  59. if (_.isUndefined(treatTopLvlUndefinedAsRef)) { treatTopLvlUndefinedAsRef = true; }
  60. // Default `useStrict` to `true`.
  61. // (will be changed to false in a future release)
  62. if (_.isUndefined(useStrict)) { useStrict = true; }
  63. // If the provided value is `undefined` at the top level...
  64. if (_.isUndefined(value)) {
  65. // If `treatTopLvlUndefinedAsRef` is enabled,
  66. if (treatTopLvlUndefinedAsRef) {
  67. // then use `===` (note that this approach isn't quite 1-to-1 with reality,
  68. // since `===` doesn't accept `undefined` values as of rttc@9.3.0.)
  69. return typeInfo('ref').getExemplar();
  70. }
  71. // Otherwise: treat it as if it were `null`, and use `*`.
  72. else { return typeInfo('json').getExemplar(); }
  73. }
  74. // Dehydrate the wanna-be exemplar to avoid circular recursion
  75. // (but allow null, and don't stringify functions, and --
  76. // if `useStrict` is enabled-- allow NaN/Infinity/-Infinity)
  77. value = dehydrate(value, true, true, !!useStrict);
  78. // Next, iterate over the value and coerce it into a valid rttc exemplar.
  79. return (function _recursivelyCoerceExemplar(valuePart){
  80. // `null` becomes '*'
  81. if (_.isNull(valuePart)) {
  82. return typeInfo('json').getExemplar();
  83. }
  84. // `Infinity`, `-Infinity`, and `NaN` SHOULD become '==='
  85. // (because they are not JSON-compatible, and not "number"s, by rttc's definition)
  86. // ...unless `useStrict` is disabled, in which case they should become `0`.
  87. if (valuePart === Infinity || valuePart === -Infinity || _.isNaN(valuePart)) {
  88. if (useStrict) {
  89. return typeInfo('ref').getExemplar();
  90. } else {
  91. return 0;
  92. }
  93. }
  94. // functions become '->'
  95. else if (_.isFunction(valuePart)) {
  96. return typeInfo('lamda').getExemplar();
  97. }
  98. // and strings which resemble potentially-ambiguous exemplars
  99. // become their own exemplar description instead (because all of
  100. // the exemplar descriptions are strings, which is what we want)
  101. else if (typeInfo('json').isExemplar(valuePart)) {
  102. return allowSpecialSyntax ? valuePart : typeInfo('json').getExemplarDescription();
  103. }
  104. else if (typeInfo('ref').isExemplar(valuePart)) {
  105. return allowSpecialSyntax ? valuePart : typeInfo('ref').getExemplarDescription();
  106. }
  107. else if (typeInfo('lamda').isExemplar(valuePart)) {
  108. return allowSpecialSyntax ? valuePart : typeInfo('lamda').getExemplarDescription();
  109. }
  110. // arrays need a recursive step
  111. else if (_.isArray(valuePart)) {
  112. // empty arrays (`[]`) just become generic JSON array exemplars (`[]` aka `['*']`):
  113. if (valuePart.length === 0) {
  114. // Note:
  115. // In a future version of RTTC, this will be modified to
  116. // return `['*']` instead of `[]`.
  117. return valuePart;
  118. }
  119. // NON-empty arrays (e.g. `[1,2,3]`) become pattern array exemplars (e.g. `[1]`):
  120. else {
  121. // To do this, we recursively call `rttc.coerceExemplar()` on each of the normal
  122. // items in the array, then `rttc.union()` them all together and use the resulting
  123. // exemplar as our deduced pattern.
  124. var pattern = _.reduce(valuePart.slice(1), function (patternSoFar, item) {
  125. patternSoFar = union(patternSoFar, _recursivelyCoerceExemplar(item), true, useStrict);
  126. // meaning of `rttc.union()` flags, in order:
  127. // • `true` (yes these are exemplars)
  128. // • `true` (yes, use strict validation rules to prevent confusion)
  129. return patternSoFar;
  130. }, _recursivelyCoerceExemplar(valuePart[0]));
  131. //--------------------------------------------------------------------------------
  132. // Note:
  133. // If the narrowest common schema for the pattern is "===" (ref), that means
  134. // that, as a whole, the exemplar is `['===']`. That makes it effectively
  135. // heterogeneous, as well as indicating that its items are not necessarily
  136. // JSON-compatible, and that they may even be mutable references.
  137. //--------------------------------------------------------------------------------
  138. return [
  139. pattern
  140. ];
  141. }
  142. }
  143. // dictionaries need a recursive step too
  144. else if (_.isObject(valuePart)) {
  145. // Empty dictionaries (`{}`) just become generic dictionaries (`{}`),
  146. // and NON-EMPTY dictionaries (e.g. `{foo: ['bar','baz'] }`) become
  147. // faceted dictionary exemplars (`{foo:'bar'}`)
  148. return _.reduce(_.keys(valuePart), function (dictSoFar, key) {
  149. var subValue = valuePart[key];
  150. dictSoFar[key] = _recursivelyCoerceExemplar(subValue); // <= recursive step
  151. return dictSoFar;
  152. }, {});
  153. }
  154. // Finally, if none of the special cases above apply, this valuePart is already
  155. // good to go, so just return it.
  156. return valuePart;
  157. })(value);
  158. };