intersection.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. /**
  8. * intersection()
  9. *
  10. * Given two rttc schemas, return the most specific schema that accepts the shared subset
  11. * of values accepted by both, and that _does not_ accept any values that both schemas would
  12. * accept individually. Formally, this subset is the intersection of A and B (A ∩ B),
  13. * where A is the set of values accepted by `schema0` and B is the set of values accepted by
  14. * `schema1`. If there is no schema that accepts only `A ∩ B` (e.g. 'boolean' ∩ `[{foo:['json']]`)
  15. * then this function will return `null`. Otherwise it will return the schema that precisely
  16. * accepts `A ∩ B`.
  17. *
  18. * @param {*} schema0
  19. * @param {*} schema1
  20. * @param {boolean} isExemplar - if set, the schemas will be treated as exemplars (rather than type schemas)
  21. * @param {boolean} isStrict - if set, the schemas will be intersected using strict validation rules.
  22. * @return {*}
  23. */
  24. module.exports = function intersection (schema0, schema1, isExemplar, isStrict) {
  25. // exemplar-vs-type-schema-agnostic type check helper
  26. function thisSchema(schema){
  27. return {
  28. is: function (){
  29. var acceptableTypes = Array.prototype.slice.call(arguments);
  30. if (!isExemplar) {
  31. return _.contains(acceptableTypes, schema);
  32. }
  33. return _.any(acceptableTypes, function (typeName){
  34. return TYPES[typeName].isExemplar(schema);
  35. });
  36. }
  37. };
  38. }
  39. // Configure two-headed schema cursor and use it to recursively
  40. // determine the schema intersection.
  41. var twoHeadedCursor = buildTwoHeadedSchemaCursor(
  42. // If we pass in `false` as the first argument, it indicates we're traversing
  43. // type schemas rather than exemplars. If `true`, then it's the other way around.
  44. !!isExemplar,
  45. function onFacetDict(schema0, schema1, parentKeyOrIndex, iterateRecursive){
  46. if ( thisSchema(schema1).is('json', 'ref') ) {
  47. return schema0;
  48. }
  49. var isIncompatible;
  50. var intersectedFacetDict = _.reduce(schema0, function (memo, val, key) {
  51. // If schema1 is a faceted dictionary, check to see if it has this key.
  52. // It not, include the key no matter what!
  53. if (_.isObject(schema1) && !_.isArray(schema1) && !_.isEqual(schema1, {})) {
  54. if (_.isUndefined(schema1[key]) || _.isNull(schema1[key])) {
  55. memo[key] = val;
  56. return memo;
  57. }
  58. }
  59. var intersectedFacet = iterateRecursive(key);
  60. if (_.isNull(intersectedFacet)) {
  61. isIncompatible = true;
  62. return memo;
  63. }
  64. memo[key] = intersectedFacet;
  65. return memo;
  66. }, {});
  67. if (isIncompatible) {
  68. return null;
  69. }
  70. // If schema1 is a faceted dictionary, check to see if it has any keys which
  71. // are not in schema0. If so, include them in the intersection.
  72. if (_.isObject(schema1) && !_.isArray(schema1) && !_.isEqual(schema1, {})) {
  73. _.each(schema1, function (val, key){
  74. if (_.isUndefined(schema0[key]) || _.isNull(schema0[key])) {
  75. intersectedFacetDict[key] = val;
  76. }
  77. });
  78. }
  79. return intersectedFacetDict;
  80. },
  81. function onPatternArray(schema0, schema1, parentKeyOrIndex, iterateRecursive){
  82. if ( thisSchema(schema1).is('json', 'ref') ) {
  83. return schema0;
  84. }
  85. if (!_.isArray(schema1)) {
  86. return null;
  87. }
  88. var intersectedPattern = iterateRecursive(0);
  89. if (_.isNull(intersectedPattern)) {
  90. return null;
  91. }
  92. return [ intersectedPattern ];
  93. },
  94. function onGenericDict(schema0, schema1, parentKeyOrIndex){
  95. if ( thisSchema(schema1).is('json', 'ref') ) {
  96. return schema0;
  97. }
  98. if (!_.isArray(schema1) && _.isObject(schema1)) {
  99. return schema1;
  100. }
  101. return null;
  102. },
  103. function onGenericArray(schema0, schema1, parentKeyOrIndex){
  104. if ( thisSchema(schema1).is('json', 'ref') ) {
  105. return schema0;
  106. }
  107. if (_.isArray(schema1)) {
  108. return schema1;
  109. }
  110. return null;
  111. },
  112. function onJson(schema0, schema1, parentKeyOrIndex) {
  113. if (_.isArray(schema1)) {
  114. return schema1;
  115. }
  116. if (_.isObject(schema1)) {
  117. return schema1;
  118. }
  119. if ( thisSchema(schema1).is('ref', 'json') ) {
  120. return schema0;
  121. }
  122. if ( thisSchema(schema1).is('string', 'number', 'boolean') ) {
  123. return schema1;
  124. }
  125. return null;
  126. },
  127. function onRef(schema0, schema1, parentKeyOrIndex) {
  128. return schema1;
  129. },
  130. function onLamda(schema0, schema1, parentKeyOrIndex) {
  131. if ( thisSchema(schema1).is('lamda') ) {
  132. return schema1;
  133. }
  134. if ( thisSchema(schema1).is('ref') ) {
  135. return schema0;
  136. }
  137. return null;
  138. },
  139. function onString(schema0, schema1, parentKeyOrIndex) {
  140. if ( thisSchema(schema1).is('string') ) {
  141. return schema1;
  142. }
  143. if ( thisSchema(schema1).is('json', 'ref') ) {
  144. return schema0;
  145. }
  146. if (!isStrict){
  147. if ( thisSchema(schema1).is('number', 'boolean') ) {
  148. return schema1;
  149. }
  150. }
  151. return null;
  152. },
  153. function onNumber(schema0, schema1, parentKeyOrIndex) {
  154. if ( thisSchema(schema1).is('number') ) {
  155. return schema1;
  156. }
  157. if ( thisSchema(schema1).is('json', 'ref') ) {
  158. return schema0;
  159. }
  160. if (!isStrict){
  161. if ( thisSchema(schema1).is('string') ) {
  162. return schema0;
  163. }
  164. if ( thisSchema(schema1).is('boolean') ) {
  165. return schema1;
  166. }
  167. }
  168. return null;
  169. },
  170. function onBoolean(schema0, schema1, parentKeyOrIndex) {
  171. if ( thisSchema(schema1).is('boolean') ) {
  172. return schema1;
  173. }
  174. if ( thisSchema(schema1).is('json', 'ref') ) {
  175. return schema0;
  176. }
  177. if (!isStrict){
  178. if ( thisSchema(schema1).is('string', 'number') ) {
  179. return schema0;
  180. }
  181. }
  182. return null;
  183. }
  184. );
  185. // If either schema is void, then the intersection will always be the same.
  186. // (only relevant w/ exemplars)
  187. if (isExemplar && _.isNull(schema0) || _.isNull(schema1)) {
  188. return null;
  189. }
  190. // Run the iterator to get the schema intersection.
  191. var result = twoHeadedCursor(schema0, schema1);
  192. if (_.isUndefined(result)) {
  193. throw new Error('Consistency violation: Result from rttc.intersection() should never be `undefined`.');
  194. }
  195. return result;
  196. };