union.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /**
  2. * Module dependencies
  3. */
  4. var _ = require('@sailshq/lodash');
  5. var buildTwoHeadedSchemaCursor = require('./helpers/build-two-headed-schema-cursor');
  6. var TYPES = require('./helpers/types');
  7. var infer = require('./infer');
  8. var getDefaultExemplar = require('./get-default-exemplar');
  9. /**
  10. * union()
  11. *
  12. * Given two rttc schemas, return the most specific schema that
  13. * would accept the superset of what both schemas accept normally.
  14. *
  15. *
  16. * @param {*} schema0
  17. * @param {*} schema1
  18. * @param {boolean} isExemplar - if set, the schemas will be treated as exemplars (rather than type schemas)
  19. * @param {boolean} isStrict - if set, the schemas will be unioned using strict validation rules.
  20. * @return {*}
  21. */
  22. module.exports = function union (schema0, schema1, isExemplar, isStrict) {
  23. /*
  24. // Type union: (using strict validation rules)
  25. (plan is to not worry about supporting uncertainty at the moment)
  26. // Special cases:
  27. // inside a generic dictionary keypath: act like 'json'
  28. // inside a generic array keypath: act like 'json'
  29. // inside a JSON keypath: act like 'json'
  30. // inside a ref keypath: act like 'ref'
  31. // inside any other keypath: not possible, that's an error (will be caught during stabilization, so we can ignore)
  32. // Types always union with themselves, with an identity result.
  33. 'string' ∪ 'string' <====> 'string'
  34. 'number' ∪ 'number' <====> 'number'
  35. 'boolean' ∪ 'boolean' <====> 'boolean'
  36. 'lamda' ∪ 'lamda' <====> 'lamda'
  37. {} ∪ {} <====> {}
  38. [] ∪ [] <====> []
  39. 'json' ∪ 'json' <====> 'json'
  40. 'ref' ∪ 'ref' <====> 'ref'
  41. // Every type unions with "ref", resulting in "ref"
  42. (anything) ∪ 'ref' <====> 'ref'
  43. // Every type but "lamda" unions with "lamda", resulting in "ref"
  44. 'lamda' ∪ (anything else) <====> 'ref'
  45. // Every type except "ref" and "lamda" unions with "json", resulting in "json"
  46. (anything else) ∪ 'json' <====> 'json'
  47. // Primitive types union with most things to result in "json"
  48. 'string' ∪ 'number' <====> 'json'
  49. 'string' ∪ 'boolean' <====> 'json'
  50. 'string' ∪ 'lamda' <====> 'json'
  51. 'string' ∪ (any dictionary) <====> 'json'
  52. 'string' ∪ (any array) <====> 'json'
  53. 'number' ∪ 'string' <====> 'json'
  54. 'number' ∪ 'boolean' <====> 'json'
  55. 'number' ∪ 'lamda' <====> 'json'
  56. 'number' ∪ (any dictionary) <====> 'json'
  57. 'number' ∪ (any array) <====> 'json'
  58. 'boolean' ∪ 'number' <====> 'json'
  59. 'boolean' ∪ 'string' <====> 'json'
  60. 'boolean' ∪ 'lamda' <====> 'json'
  61. 'boolean' ∪ (any dictionary) <====> 'json'
  62. 'boolean' ∪ (any array) <====> 'json'
  63. // Faceted dictionaries union w/ generic dictionaries to result in generic dictionaries.
  64. {'a': 'boolean'} ∪ {} <====> {}
  65. // Faceted dictionaries union w/ each other, recursively unioning their child properties.
  66. // If a key is missing, the result will be a generic dictionary.
  67. {'a': 'boolean'} ∪ {'a':'string'} <====> {'a': 'json'}
  68. {'a': 'lamda'} ∪ {'a':'string'} <====> {'a': 'ref'}
  69. {'a': 'boolean'} ∪ {'b':'string'} <====> {}
  70. // Patterned arrays union w/ generic arrays to result in generic arrays.
  71. [{'a': 'boolean'}] ∪ [] <====> []
  72. // Patterned arrays union w/ each other, recursively unioning their patterns.
  73. ['string'] ∪ ['number'] <====> ['json']
  74. ['lamda'] ∪ ['boolean'] <====> ['ref']
  75. [[]] ∪ ['number'] <====> ['json']
  76. [[[]]] ∪ ['number'] <====> ['json']
  77. [[[]]] ∪ [['number']] <====> [['json']]
  78. [{a:'boolean'}] ∪ [{a:'string'}] <====> [{'a': 'json'}]
  79. [{a:'boolean'}] ∪ [{b:'string'}] <====> [{}]
  80. [{a:'boolean'}] ∪ [[{b:'string'}]] <====> ['json']
  81. // Exceptions when NOT using strict validation:
  82. 'number' ∪ 'string' <====> 'string'
  83. 'boolean' ∪ 'string' <====> 'string'
  84. 'number' ∪ 'boolean' <====> 'number'
  85. */
  86. // exemplar-vs-type-schema-agnostic type check helper
  87. function thisSchema(schema){
  88. return {
  89. is: function (){
  90. var acceptableTypes = Array.prototype.slice.call(arguments);
  91. if (!isExemplar) {
  92. return _.contains(acceptableTypes, schema);
  93. }
  94. return _.any(acceptableTypes, function (typeName){
  95. return TYPES[typeName].isExemplar(schema);
  96. });
  97. },
  98. containsType: function (){
  99. var searchingForTypes = Array.prototype.slice.call(arguments);
  100. return _.any(searchingForTypes, function (typeName){
  101. if (!_.isObject(schema)) {
  102. return false;
  103. }
  104. if (_.isArray(schema)) {
  105. if (schema.length > 0) {
  106. return false;
  107. }
  108. if (!_.isObject(schema[0])) {
  109. return thisSchema(schema[0]).is(typeName);
  110. }
  111. return thisSchema(schema[0]).containsType(typeName);
  112. }
  113. return _.reduce(schema, function (memo, value, key) {
  114. if (!_.isObject(value)) {
  115. return memo || thisSchema(value).is(typeName);
  116. }
  117. return memo || thisSchema(value).containsType(typeName);
  118. }, false);
  119. });
  120. }
  121. };
  122. }
  123. // exemplar-vs-type-schema-agnostic helper for building return values
  124. function normalizeResult(type){
  125. if (!isExemplar) {
  126. return type;
  127. }
  128. return getDefaultExemplar(type);
  129. }
  130. // Configure two-headed type schema cursor and use it to recursively
  131. // determine the type schema union.
  132. var twoHeadedCursor = buildTwoHeadedSchemaCursor(
  133. // If we pass in `false` as the first argument, it indicates we're traversing
  134. // type schemas rather than exemplars. If `true`, then it's the other way around.
  135. !!isExemplar,
  136. function onFacetDict(schema0, schema1, parentKeyOrIndex, iterateRecursive){
  137. if ( thisSchema(schema1).is('ref', 'lamda') ) {
  138. return normalizeResult('ref');
  139. }
  140. if (_.isArray(schema1) || !_.isObject(schema1)) {
  141. // If `schema1` is a faceted dictionary or patterned array which contains
  142. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  143. // result a `ref` (===).
  144. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  145. return normalizeResult('ref');
  146. }
  147. return normalizeResult('json');
  148. }
  149. var sharedKeys = _.intersection(_.keys(schema0), _.keys(schema1));
  150. // If there are any keys that don't exist in BOTH schemas, we'll just return
  151. // a generic type (ref or {}) as the union. This way coercing a value to the
  152. // unioned schema will never result in data loss (i.e. stripped keys).
  153. var xorKeys = _.difference(_.union(_.keys(schema0), _.keys(schema1)), sharedKeys);
  154. if (xorKeys.length > 0) {
  155. // If either schema is a faceted dictionary or patterned array which contains
  156. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  157. // result a `ref` (===).
  158. if (thisSchema(schema1).containsType('ref', 'lamda') || thisSchema(schema0).containsType('ref', 'lamda')) {
  159. return normalizeResult('ref');
  160. }
  161. return {};
  162. }
  163. return _.reduce(sharedKeys, function (memo, key) {
  164. memo[key] = iterateRecursive(key);
  165. return memo;
  166. }, {});
  167. },
  168. function onPatternArray(schema0, schema1, parentKeyOrIndex, iterateRecursive){
  169. if ( thisSchema(schema1).is('ref', 'lamda') ) {
  170. return normalizeResult('ref');
  171. }
  172. if (!_.isArray(schema1)) {
  173. // If `schema1` is a faceted dictionary or patterned array which contains
  174. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  175. // result a `ref` (===).
  176. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  177. return normalizeResult('ref');
  178. }
  179. return normalizeResult('json');
  180. }
  181. if (_.isEqual(schema1, [])) {
  182. return [];
  183. }
  184. return [ iterateRecursive(0) ];
  185. },
  186. function onGenericDict(schema0, schema1, parentKeyOrIndex){
  187. if ( thisSchema(schema1).is('ref', 'lamda') ) {
  188. return normalizeResult('ref');
  189. }
  190. // If `schema1` is a faceted dictionary or patterned array which contains
  191. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  192. // result a `ref` (===).
  193. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  194. return normalizeResult('ref');
  195. }
  196. if (!_.isArray(schema1) && _.isObject(schema1)) {
  197. return {};
  198. }
  199. return normalizeResult('json');
  200. },
  201. function onGenericArray(schema0, schema1, parentKeyOrIndex){
  202. if ( thisSchema(schema1).is('ref', 'lamda') ) {
  203. return normalizeResult('ref');
  204. }
  205. // If `schema1` is a faceted dictionary or patterned array which contains
  206. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  207. // result a `ref` (===).
  208. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  209. return normalizeResult('ref');
  210. }
  211. if (_.isArray(schema1)) {
  212. return [];
  213. }
  214. return normalizeResult('json');
  215. },
  216. function onJson(schema0, schema1, parentKeyOrIndex) {
  217. if ( thisSchema(schema1).is('ref', 'lamda') ) {
  218. return normalizeResult('ref');
  219. }
  220. // If `schema1` is a faceted dictionary or patterned array which contains
  221. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  222. // result a `ref` (===).
  223. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  224. return normalizeResult('ref');
  225. }
  226. return normalizeResult('json');
  227. },
  228. function onRef(schema0, schema1, parentKeyOrIndex) {
  229. return normalizeResult('ref');
  230. },
  231. function onLamda(schema0, schema1, parentKeyOrIndex) {
  232. if ( thisSchema(schema1).is('lamda') ) {
  233. return normalizeResult('lamda');
  234. }
  235. return normalizeResult('ref');
  236. },
  237. function onString(schema0, schema1, parentKeyOrIndex) {
  238. if ( thisSchema(schema1).is('string') ) {
  239. return schema1;
  240. }
  241. // If `schema1` is a faceted dictionary or patterned array which contains
  242. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  243. // result a `ref` (===).
  244. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  245. return normalizeResult('ref');
  246. }
  247. if (!isStrict){
  248. if ( thisSchema(schema1).is('number', 'boolean') ) {
  249. return schema0;
  250. }
  251. }
  252. if (
  253. thisSchema(schema1).is('number', 'boolean', 'json') ||
  254. _.isArray(schema1) ||
  255. _.isObject(schema1)
  256. ) {
  257. return normalizeResult('json');
  258. }
  259. return normalizeResult('ref');
  260. },
  261. function onNumber(schema0, schema1, parentKeyOrIndex) {
  262. if ( thisSchema(schema1).is('number') ) {
  263. return schema1;
  264. }
  265. // If `schema1` is a faceted dictionary or patterned array which contains
  266. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  267. // result a `ref` (===).
  268. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  269. return normalizeResult('ref');
  270. }
  271. if (!isStrict){
  272. if ( thisSchema(schema1).is('string') ) {
  273. return schema1;
  274. }
  275. if ( thisSchema(schema1).is('boolean') ) {
  276. return schema0;
  277. }
  278. }
  279. if (
  280. thisSchema(schema1).is('string', 'boolean', 'json') ||
  281. _.isArray(schema1) ||
  282. _.isObject(schema1)
  283. ) {
  284. return normalizeResult('json');
  285. }
  286. return normalizeResult('ref');
  287. },
  288. function onBoolean(schema0, schema1, parentKeyOrIndex) {
  289. if ( thisSchema(schema1).is('boolean') ) {
  290. return schema1;
  291. }
  292. // If `schema1` is a faceted dictionary or patterned array which contains
  293. // sub-values of type:` ref` (===) or `lamda` (->), then we must make the
  294. // result a `ref` (===).
  295. if (thisSchema(schema1).containsType('ref', 'lamda')) {
  296. return normalizeResult('ref');
  297. }
  298. if (!isStrict){
  299. if ( thisSchema(schema1).is('string', 'number') ) {
  300. return schema1;
  301. }
  302. }
  303. if (
  304. thisSchema(schema1).is('number', 'string', 'json') ||
  305. _.isArray(schema1) ||
  306. _.isObject(schema1)
  307. ) {
  308. return normalizeResult('json');
  309. }
  310. return normalizeResult('ref');
  311. }
  312. );
  313. // Run the iterator to get the schema union.
  314. var result = twoHeadedCursor(schema0, schema1);
  315. // This makes sure the resulting exemplar won't be `undefined`.
  316. if (isExemplar) {
  317. if (_.isUndefined(result)) {
  318. return TYPES.ref.getExemplar();
  319. }
  320. }
  321. return result;
  322. };