<commit_before>/* jshint undef: true, strict:false, trailing:false, unused:false */
/* global require, exports, console, process, module, L, angular */

angular
	.module('wellbeing', ['ng','indx','infinite-scroll'])
	// .directive('dayview', function() {
	// 	return {
	// 		restrict:'E',
	// 		scope:{ day:'=' },
	// 		templateUrl:'/apps/wellbeing/templates/day.html'
	// 		controller:function($scope) {}
	// 	};
	// })
	.directive('locmap', function() {
		return {
			restrict:'E',
			replace:true,
			scope:{ location:'=' },
			template:'

',
			link:function(scope, element, attribute) {
				var lat = scope.location.peek('latitude'), lon = scope.location.peek('longitude');
				scope.map = L.map(element[0]).setView([lat, lon], 13);

				element.attr('data-latitude', lat);
				element.attr('data-longitude', lon);
				element.attr('data-id', scope.location.id);

				L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
				    attribution: '© OpenStreetMap contributors'
				}).addTo(scope.map);

					// add a marker in the given location, attach some popup content to it and open the popup
				L.marker([lat, lon]).addTo(scope.map)
					    .bindPopup(scope.location.peek('name') ? scope.location.peek('name') : scope.location.id)
					    .openPopup();
			},
			controller:function($scope) {
			}
		};
	}).controller('main', function($scope, client, utils) {
		var u = utils;
		$scope.days = [];
		$scope.locations = [];
		var all_locs = [];
		var box;

		// var genDay = function(date) {
		// 	return {
		// 		date: date,
		// 		locations: get_things(date, {type: "location"}),
		// 		activities: get_things(date, {type:'activity'}))
		// 	};
		// };

		// $scope.genDay = function() {
		// 	var last_day = $scope.days.slice($scope.days.length - 1),
		// 		late_date = last_day && last_day.date || new Date();

		// 	genDay(last_date);
		// };

		var genLocs = function() {
			console.log('genlocs >> ', $scope.locations.length, all_locs.length);
			if ($scope.locations.length < all_locs.length) {
				$scope.locations.push(all_locs[$scope.locations.length]);
			}
		};
		$scope.$watch('user + box', function() {
			if (!$scope.user) { console.log('no user :( '); return; }
			$scope.days = [];
			box = client.store.getBox($scope.box).then(function(_box) {
				window.box = _box;
				box = _box;
				console.log('querying ----------- > ');
				box.query({type:'location'}).then(function(things) {
					console.log('things');
					u.safeApply($scope, function() {
						console.log("I GOT THE THINGS >> ", things);
						all_locs = things;
						$scope.locations = things;
					});
				}).fail(function(bail) { console.log('fail querying ', bail); });
				// $scope.genDay();
			}).fail(function(bail) { console.error(bail); });
		});

	});<commit_msg>update<commit_after>/* jshint undef: true, strict:false, trailing:false, unused:false */
/* global require, exports, console, process, module, L, angular */

angular
	.module('wellbeing', ['ng','indx','infinite-scroll'])
	// .directive('dayview', function() {
	// 	return {
	// 		restrict:'E',
	// 		scope:{ day:'=' },
	// 		templateUrl:'/apps/wellbeing/templates/day.html'
	// 		controller:function($scope) {}
	// 	};
	// })
	.directive('locmap', function() {
		return {
			restrict:'E',
			replace:true,
			scope:{ location:'=' },
			template:'

',
			link:function(scope, element, attribute) {
				var lat = scope.location.peek('latitude'), lon = scope.location.peek('longitude');
				scope.map = L.map(element[0]).setView([lat, lon], 18);

				element.attr('data-latitude', lat);
				element.attr('data-longitude', lon);
				element.attr('data-id', scope.location.id);

				L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
				    attribution: '© OpenStreetMap contributors'
				}).addTo(scope.map);

					// add a marker in the given location, attach some popup content to it and open the popup
				L.marker([lat, lon]).addTo(scope.map);
					    // .bindPopup(scope.location.peek('name') ? scope.location.peek('name') : scope.location.id)
					    // .openPopup();
			},
			controller:function($scope) {
			}
		};
	}).controller('main', function($scope, client, utils) {
		var u = utils;
		$scope.days = [];
		$scope.locations = [];
		var all_locs = [];
		var box;

		// var genDay = function(date) {
		// 	return {
		// 		date: date,
		// 		locations: get_things(date, {type: "location"}),
		// 		activities: get_things(date, {type:'activity'}))
		// 	};
		// };

		// $scope.genDay = function() {
		// 	var last_day = $scope.days.slice($scope.days.length - 1),
		// 		late_date = last_day && last_day.date || new Date();

		// 	genDay(last_date);
		// };

		var genLocs = function() {
			console.log('genlocs >> ', $scope.locations.length, all_locs.length);
			if ($scope.locations.length < all_locs.length) {
				$scope.locations.push(all_locs[$scope.locations.length]);
			}
		};
		$scope.$watch('user + box', function() {
			if (!$scope.user) { console.log('no user :( '); return; }
			$scope.days = [];
			box = client.store.getBox($scope.box).then(function(_box) {
				window.box = _box;
				box = _box;
				console.log('querying ----------- > ');
				box.query({type:'location'}).then(function(things) {
					console.log('things');
					u.safeApply($scope, function() {
						console.log("I GOT THE THINGS >> ", things);
						all_locs = things;
						$scope.locations = things;
					});
				}).fail(function(bail) { console.log('fail querying ', bail); });
				// $scope.genDay();
			}).fail(function(bail) { console.error(bail); });
		});

	});<|endoftext|>


<commit_before>import VisComponent from '../../VisComponent';
import VegaChart from '../../VisComponent/mixin/VegaChart';
import spec from './spec.json';

export default class SurvivalPlot extends VegaChart(VisComponent, spec) {
  static get options () {
    return [
      {
        name: 'data',
        type: 'table',
        format: 'objectlist'
      },
      {
        name: 'time',
        type: 'string',
        format: 'text',
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['number', 'integer']
        }
      },
      {
        name: 'censor',
        type: 'string',
        format: 'text',
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['integer', 'boolean']
        }
      },
      {
        name: 'group',
        type: 'string',
        format: 'text',
        optional: true,
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['string', 'date', 'number', 'integer', 'boolean']
        }
      }
    ];
  }

  constructor (el, options) {
    super(el, options);

    // Sort the events by time.
    options.data.sort((x, y) => x[options.time] - y[options.time]);

    // If there is no 'group' parameter, fake one up.
    if (!options.group) {
      options.group = 'group';
      options.data.forEach(d => d.group = 0);
    }

    // Collect the grouping values, and count how many belong to each group.
    let groups = {};
    options.data.forEach(d => {
      const key = d[options.group];
      if (!groups[key]) {
        groups[key] = {
          survivors: 1
        };
      }

      groups[key].survivors++;
    });

    // Append a dummy value to the start of the dataset, one per group.
    const dummies = Object.keys(groups).map(g => {
      let x = {
        time: 0
      };

      x[options.group] = g;

      return x;
    });
    options.data = [...dummies, ...options.data];

    // Compute the number of survivors at each event. A patient is assumed to
    // have not survived, unless the reason for the event is "right censoring"
    // (aka, censor == 1).
    options.data.forEach(d => {
      if (d[options.censor] !== 0) {
        groups[d[options.group]].survivors--;
      }
      d.survivors = groups[d[options.group]].survivors;
    });
  }
}
<commit_msg>fix(SurvivalPlot): use Vega update API to properly update processed data<commit_after>import VisComponent from '../../VisComponent';
import VegaChart from '../../VisComponent/mixin/VegaChart';
import spec from './spec.json';

export default class SurvivalPlot extends VegaChart(VisComponent, spec) {
  static get options () {
    return [
      {
        name: 'data',
        type: 'table',
        format: 'objectlist'
      },
      {
        name: 'time',
        type: 'string',
        format: 'text',
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['number', 'integer']
        }
      },
      {
        name: 'censor',
        type: 'string',
        format: 'text',
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['integer', 'boolean']
        }
      },
      {
        name: 'group',
        type: 'string',
        format: 'text',
        optional: true,
        domain: {
          mode: 'field',
          from: 'data',
          fieldTypes: ['string', 'date', 'number', 'integer', 'boolean']
        }
      }
    ];
  }

  constructor (el, options) {
    super(el, options);

    // Sort the events by time.
    options.data.sort((x, y) => x[options.time] - y[options.time]);

    // If there is no 'group' parameter, fake one up.
    if (!options.group) {
      options.group = 'group';
      options.data.forEach(d => d.group = 0);
    }

    // Collect the grouping values, and count how many belong to each group.
    let groups = {};
    options.data.forEach(d => {
      const key = d[options.group];
      if (!groups[key]) {
        groups[key] = {
          survivors: 1
        };
      }

      groups[key].survivors++;
    });

    // Append a dummy value to the start of the dataset, one per group.
    const dummies = Object.keys(groups).map(g => {
      let x = {
        time: 0
      };

      x[options.group] = g;

      return x;
    });
    options.data = [...dummies, ...options.data];

    // Compute the number of survivors at each event. A patient is assumed to
    // have not survived, unless the reason for the event is "right censoring"
    // (aka, censor == 1).
    options.data.forEach(d => {
      if (d[options.censor] !== 0) {
        groups[d[options.group]].survivors--;
      }
      d.survivors = groups[d[options.group]].survivors;
    });

    this.chart.then(chart => {
      let data = chart.data('table');
      data.remove(() => true);
      data.insert(options.data);

      chart.update();
    });
  }
}
<|endoftext|>


<commit_before>import _ from 'lodash';
import {createSimpleScope, createOverrideContext} from 'bcx-expression-evaluator';
import valueEvaluator from './value-evaluator';

export function isBlank(v) {
  if (_.isNull(v) || _.isUndefined(v) || _.isNaN(v)) return true;
  if (_.isString(v)) {
    if (_.trim(v).length === 0) return true;
  } else if (_.isNumber(v)) {
    return false;
  } else if (_.isArray(v) || _.isPlainObject(v)) {
    if (_.isEmpty(v)) return true;
  }
}

export const ifTransformer = function (rule) {
  const _if = _.get(rule, 'if');
  const subRule = _.omit(rule, 'if');

  let rules = [];
  // if condition is false, skip the real validation
  rules.push({validate: "skipImmediatelyIf", value: `!(${_if})`});

  if (_.has(subRule, 'group') &&
      _.isArray(subRule.group)) {
    rules.push(... subRule.group);
  } else {
    rules.push(subRule);
  }

  return rules;
};

// TODO: do we need to support switch: a_function?
export const switchTransformer = function (rule, validate) {
  const _switch = _.get(rule, 'switch');
  const cases = _.get(rule, 'cases');

  const validator = scope => {
    // make a guess whether user try to use nested validation or plain validation
    let nestedPath;
    if (_.isObjectLike(valueEvaluator('$value')(scope))) {
      nestedPath = valueEvaluator('$propertyPath')(scope);
      // console.log(' nestedPath:'+nestedPath);
    }

    const switchEvaluator = valueEvaluator(nestedPath ? `${nestedPath}.${_switch}` : _switch);

    const _case = switchEvaluator(scope);
    const matchCase = cases[_case];
    // console.log('  case: '+_case);
    // console.log('  matchCase: '+JSON.stringify(matchCase));
    // console.log('  scope: ' + JSON.stringify(scope, null, 2));
    if (!matchCase) return;

    return validate(matchCase, nestedPath)(scope);
  };

  validator.readyToUse = true;
  return validator;
};

export const forEachTester = function (rule) {
  if (!_.has(rule, 'foreach')) return;
  if (!_.isEmpty(_.omit(rule, ['foreach', 'key']))) return;
  return true;
};

export const forEachTransformer = function (rule, validate) {
  const foreachRulesMap = _.get(rule, 'foreach');
  let foreachRulesMapFunc;
  if (_.isFunction(foreachRulesMap)) {
    foreachRulesMapFunc = valueEvaluator(foreachRulesMap);
  } else if (_.isArray(foreachRulesMap)) {
    foreachRulesMapFunc = scope => _.map(foreachRulesMap, r =>
      _.isFunction(r) ? valueEvaluator(r)(scope) : r
    );
  }

  const _key = _.get(rule, 'key', '$index');
  const keyEvaluator = valueEvaluator(_key);

  const validator = scope => {
    let errors = {};
    const enumerable = valueEvaluator('$value')(scope);
    const length = _.size(enumerable);
    _.each(enumerable, (item, index) => {
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(item, overrideContext);

      let neighbours = _.filter(enumerable, (v, i) => i !== index);

      _.merge(newOverrideContext, {
        $value: item,
        $propertyPath: '', // initial propertyPath
        $neighbours: neighbours,
        $index: index,
        $first: index === 0,
        $last: (index === length - 1),
      });

      const newScope = {
        bindingContext: item,
        overrideContext: newOverrideContext
      };

      const key = keyEvaluator(newScope);
      const result = validate(foreachRulesMapFunc ?
                              foreachRulesMapFunc(newScope) :
                              foreachRulesMap)(newScope);

      if (result.isValid === false) {
        errors[key] = result.errors;
      }
    });

    return {isValid: _.isEmpty(errors), errors};
  };

  validator.readyToUse = true;
  return validator;
};

// The order of transformers and validators is very important.
// It is the order that resolveValidator finds a match.
export const standardTransformers = [

  // if a rule contains {if: 'expression'}
  // transform it into a chain of validators
  // support two forms:
  //
  // group form:
  //   {if: 'expression', group: [ /* validators */]}
  //
  // single form:
  //   {if: 'expression', validate: 'name', ...}
  //
  // note: 'if' is a reserved word in javascript but not in bcx-expression
  // be less surprising but verbose, write "_.isString($this['if'])"
  ["_.isString(if) && !_.isEmpty(_.omit($this, 'if'))", ifTransformer],

  // switch
  //
  // {
  //   switch: 'expression',
  //   cases: {
  //     "matchingA": {validate: 'name', ...},
  //     "matchingB": [ /* validators */ ]
  //   }
  // }
  //
  // note: 'switch' is a reserved word in javascript but not in bcx-expression
  // be less surprising but verbose, write "_.isString($this['switch'])"
  ["_.isString(switch) && _.isObjectLike(cases) && _.isEmpty(_.omit($this, 'switch', 'cases'))", switchTransformer],

  // foreach
  //
  // {
  //   foreach: ...rules,
  //   key: optional key expression or function
  // }
  //
  [forEachTester, forEachTransformer],

  // transform regex
  ["_.isRegExp($this)", rule => ({validate: "isTrue", value: rule})],
  ["_.isRegExp(validate)", rule => ({validate: "isTrue", value: rule.validate})],

  // transform "isBlank"
  ["$this === 'isBlank'", () => ({validate: "isBlank"})],

  // transform "notBlank"
  ["$this === 'notBlank'", () => ({validate: "notBlank"})],

  // transform "mandatory"
  ["$this === 'mandatory'", () => ({validate: "mandatory"})],

  // transform "notMandatory"
  ["$this === 'notMandatory'", () => ({validate: "notMandatory"})],

  // transform "email"
  ["$this === 'email'", () => ({validate: "email"})],

  // transform "unique"
  ["$this === 'unique'", () => ({validate: "unique"})],
];

export const standardValidators = [
  //validators implemented in func
  ["isTrue", v => v ? null : "must be true"],

  ["passImmediatelyIf", v => v ?
                             // stop the chain if passed
                             {isValid: true, break: true} :
                             // continue, never fail
                             null],

  ["skipImmediatelyIf", v => v ?
                             // skip rest if passed
                             // isValid is not true, but null
                             {isValid: null, break: true} :
                             // continue, never fail
                             null],

  ["failImmediatelyIf", v => v ?
                             // stop the chain if failed
                             {isValid: false, break: true} :
                             // continue
                             null],

  // all other validators are in form of rewrite or composition
  ["isFalse", {validate: "isTrue", value: "!$value", message: "must be false"}],
  ["isBlank", {validate: "isTrue", value: isBlank, message: "must be blank"}],
  ["notBlank", {validate: "isFalse", value: isBlank, message: "must not be blank"}],

  ["mandatory", {validate: "failImmediatelyIf", value: isBlank, message: "must not be empty"}],
  ["notMandatory", {validate: "skipImmediatelyIf", value: isBlank}],

  // {validate: 'number', integer: true, min: 0, max: 10, greaterThan: 0, lessThan: 10, even: true, /* odd: true */}
  ["number", [
    {validate: "isTrue", value: "_.isNumber($value)", message: "must be a number", stopValidationChainIfFail: true},
    // option {integer: true}
    {if: "$integer", validate: "isTrue", value: "_.isInteger($value)", message: "must be an integer", stopValidationChainIfFail: true},
    // option {min: aNumber}
    {if: "_.isNumber($min)", validate: "isTrue", value: "$value >= $min", message: "must be at least ${$min}"},
    // option {greaterThan: aNumber}
    {if: "_.isNumber($greaterThan)", validate: "isTrue", value: "$value > $greaterThan", message: "must be greater than ${$greaterThan}"},
    // option {max: aNumber}
    {if: "_.isNumber($max)", validate: "isTrue", value: "$value <= $max", message: "must be no more than ${$max}"},
    // option {lessThan: aNumber}
    {if: "_.isNumber($lessThan)", validate: "isTrue", value: "$value < $lessThan", message: "must be less than ${$lessThan}"},
    // option {even: true}
    {if: "$even", validate: "isTrue", value: "$value % 2 === 0", message: "must be an even number"},
    // option {odd: true}
    {if: "$odd", validate: "isTrue", value: "$value % 2 === 1", message: "must be an odd number"}
  ]],

  // {validate: 'string', minLength: 4, maxLength: 8}
  ["string", [
    {validate: "isTrue", value: "_.isString($value)", message: "must be a string", stopValidationChainIfFail: true},
    {if: "$minLength", validate: "isTrue", value: "_.size($value) >= $minLength", message: "must have at least ${$minLength} characters"},
    {if: "$maxLength", validate: "isTrue", value: "_.size($value) <= $maxLength", message: "must be no more than ${$maxLength} characters"}
  ]],

  // {validate: 'within', items: [ ... ]}
  ["within", {validate: "isTrue", value: "_.includes($items, $value)", message: "must be one of ${_.join($items, ', ')}"}],

  // {validate: 'notIn', items: [ ... ]}
  ["notIn", {validate: "isFalse", value: "_.includes($items, $value)", message: "must not be one of ${_.join($items, ', ')}"}],

  // {validate: 'contain', item: obj, /* or items: [...] */}
  ["contain", [
    {if: "$item", validate: "isTrue", value: "_.includes($value, $item)", message: "must contain ${$item}"},
    {if: "$items", validate: "isBlank", value: "_.difference($items, $value)", message: "missing ${_.difference($items, $value).join(', ')}"},
  ]],

  // {validate: 'password', minLength: 4, maxLength: 8, alphabet: true, mixCase: true, digit: true, specialChar: true}
  ["password", [
    // min/maxLength options would be passed through in scope, do not need explicit passing to string validator
    {validate: 'string'},
    // {validate: /[a-z]/i} is a shortcut to
    // {validate: "isTrue", value: /[a-z]/i}
    {if: '$alphabet', validate: /[a-z]/i, message: 'must contain alphabet letter'},
    // [/[a-z]/, /[A-Z]/] is a shortcut to
    // [
    //   {validate: "isTrue", value: /[a-z]/},
    //   {validate: "isTrue", value: /[A-Z]/}
    // ]
    {if: '$mixCase', group: [/[a-z]/, /[A-Z]/], message: 'must contain both lower case and upper case letters'},
    {if: '$digit', validate: /[0-9]/, message: 'must contain number'},
    {if: '$specialChar', validate: /[!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>\/?]/, message: 'must contain special character (like !@$%)'},
  ]],

  // email regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
  ["email", {validate: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
             message: "not a valid email"}],

  // unique. need to access neighbours
  // option items is evaluated from current scope
  // neighbours could be simple value (when no propertyPath)
  ["unique", {validate: "notIn", "items.bind": "$propertyPath ? _.map($neighbours, $propertyPath) : $neighbours", message: "must be unique"}],
];

export function config (validation) {
  _.each(standardTransformers, pair => {
    const [tester, transformer] = pair;
    validation.addTransformer(tester, transformer);
  });

  _.each(standardValidators, pair => {
    const [name, imp] = pair;
    validation.addValidator(name, imp);
  });
}
<commit_msg>remove debug lines.<commit_after>import _ from 'lodash';
import {createSimpleScope, createOverrideContext} from 'bcx-expression-evaluator';
import valueEvaluator from './value-evaluator';

export function isBlank(v) {
  if (_.isNull(v) || _.isUndefined(v) || _.isNaN(v)) return true;
  if (_.isString(v)) {
    if (_.trim(v).length === 0) return true;
  } else if (_.isNumber(v)) {
    return false;
  } else if (_.isArray(v) || _.isPlainObject(v)) {
    if (_.isEmpty(v)) return true;
  }
}

export const ifTransformer = function (rule) {
  const _if = _.get(rule, 'if');
  const subRule = _.omit(rule, 'if');

  let rules = [];
  // if condition is false, skip the real validation
  rules.push({validate: "skipImmediatelyIf", value: `!(${_if})`});

  if (_.has(subRule, 'group') &&
      _.isArray(subRule.group)) {
    rules.push(... subRule.group);
  } else {
    rules.push(subRule);
  }

  return rules;
};

// TODO: do we need to support switch: a_function?
export const switchTransformer = function (rule, validate) {
  const _switch = _.get(rule, 'switch');
  const cases = _.get(rule, 'cases');

  const validator = scope => {
    // make a guess whether user try to use nested validation or plain validation
    let nestedPath;
    if (_.isObjectLike(valueEvaluator('$value')(scope))) {
      nestedPath = valueEvaluator('$propertyPath')(scope);
    }

    const switchEvaluator = valueEvaluator(nestedPath ? `${nestedPath}.${_switch}` : _switch);

    const _case = switchEvaluator(scope);
    const matchCase = cases[_case];
    if (!matchCase) return;

    return validate(matchCase, nestedPath)(scope);
  };

  validator.readyToUse = true;
  return validator;
};

export const forEachTester = function (rule) {
  if (!_.has(rule, 'foreach')) return;
  if (!_.isEmpty(_.omit(rule, ['foreach', 'key']))) return;
  return true;
};

export const forEachTransformer = function (rule, validate) {
  const foreachRulesMap = _.get(rule, 'foreach');
  let foreachRulesMapFunc;
  if (_.isFunction(foreachRulesMap)) {
    foreachRulesMapFunc = valueEvaluator(foreachRulesMap);
  } else if (_.isArray(foreachRulesMap)) {
    foreachRulesMapFunc = scope => _.map(foreachRulesMap, r =>
      _.isFunction(r) ? valueEvaluator(r)(scope) : r
    );
  }

  const _key = _.get(rule, 'key', '$index');
  const keyEvaluator = valueEvaluator(_key);

  const validator = scope => {
    let errors = {};
    const enumerable = valueEvaluator('$value')(scope);
    const length = _.size(enumerable);
    _.each(enumerable, (item, index) => {
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(item, overrideContext);

      let neighbours = _.filter(enumerable, (v, i) => i !== index);

      _.merge(newOverrideContext, {
        $value: item,
        $propertyPath: '', // initial propertyPath
        $neighbours: neighbours,
        $index: index,
        $first: index === 0,
        $last: (index === length - 1),
      });

      const newScope = {
        bindingContext: item,
        overrideContext: newOverrideContext
      };

      const key = keyEvaluator(newScope);
      const result = validate(foreachRulesMapFunc ?
                              foreachRulesMapFunc(newScope) :
                              foreachRulesMap)(newScope);

      if (result.isValid === false) {
        errors[key] = result.errors;
      }
    });

    return {isValid: _.isEmpty(errors), errors};
  };

  validator.readyToUse = true;
  return validator;
};

// The order of transformers and validators is very important.
// It is the order that resolveValidator finds a match.
export const standardTransformers = [

  // if a rule contains {if: 'expression'}
  // transform it into a chain of validators
  // support two forms:
  //
  // group form:
  //   {if: 'expression', group: [ /* validators */]}
  //
  // single form:
  //   {if: 'expression', validate: 'name', ...}
  //
  // note: 'if' is a reserved word in javascript but not in bcx-expression
  // be less surprising but verbose, write "_.isString($this['if'])"
  ["_.isString(if) && !_.isEmpty(_.omit($this, 'if'))", ifTransformer],

  // switch
  //
  // {
  //   switch: 'expression',
  //   cases: {
  //     "matchingA": {validate: 'name', ...},
  //     "matchingB": [ /* validators */ ]
  //   }
  // }
  //
  // note: 'switch' is a reserved word in javascript but not in bcx-expression
  // be less surprising but verbose, write "_.isString($this['switch'])"
  ["_.isString(switch) && _.isObjectLike(cases) && _.isEmpty(_.omit($this, 'switch', 'cases'))", switchTransformer],

  // foreach
  //
  // {
  //   foreach: ...rules,
  //   key: optional key expression or function
  // }
  //
  [forEachTester, forEachTransformer],

  // transform regex
  ["_.isRegExp($this)", rule => ({validate: "isTrue", value: rule})],
  ["_.isRegExp(validate)", rule => ({validate: "isTrue", value: rule.validate})],

  // transform "isBlank"
  ["$this === 'isBlank'", () => ({validate: "isBlank"})],

  // transform "notBlank"
  ["$this === 'notBlank'", () => ({validate: "notBlank"})],

  // transform "mandatory"
  ["$this === 'mandatory'", () => ({validate: "mandatory"})],

  // transform "notMandatory"
  ["$this === 'notMandatory'", () => ({validate: "notMandatory"})],

  // transform "email"
  ["$this === 'email'", () => ({validate: "email"})],

  // transform "unique"
  ["$this === 'unique'", () => ({validate: "unique"})],
];

export const standardValidators = [
  //validators implemented in func
  ["isTrue", v => v ? null : "must be true"],

  ["passImmediatelyIf", v => v ?
                             // stop the chain if passed
                             {isValid: true, break: true} :
                             // continue, never fail
                             null],

  ["skipImmediatelyIf", v => v ?
                             // skip rest if passed
                             // isValid is not true, but null
                             {isValid: null, break: true} :
                             // continue, never fail
                             null],

  ["failImmediatelyIf", v => v ?
                             // stop the chain if failed
                             {isValid: false, break: true} :
                             // continue
                             null],

  // all other validators are in form of rewrite or composition
  ["isFalse", {validate: "isTrue", value: "!$value", message: "must be false"}],
  ["isBlank", {validate: "isTrue", value: isBlank, message: "must be blank"}],
  ["notBlank", {validate: "isFalse", value: isBlank, message: "must not be blank"}],

  ["mandatory", {validate: "failImmediatelyIf", value: isBlank, message: "must not be empty"}],
  ["notMandatory", {validate: "skipImmediatelyIf", value: isBlank}],

  // {validate: 'number', integer: true, min: 0, max: 10, greaterThan: 0, lessThan: 10, even: true, /* odd: true */}
  ["number", [
    {validate: "isTrue", value: "_.isNumber($value)", message: "must be a number", stopValidationChainIfFail: true},
    // option {integer: true}
    {if: "$integer", validate: "isTrue", value: "_.isInteger($value)", message: "must be an integer", stopValidationChainIfFail: true},
    // option {min: aNumber}
    {if: "_.isNumber($min)", validate: "isTrue", value: "$value >= $min", message: "must be at least ${$min}"},
    // option {greaterThan: aNumber}
    {if: "_.isNumber($greaterThan)", validate: "isTrue", value: "$value > $greaterThan", message: "must be greater than ${$greaterThan}"},
    // option {max: aNumber}
    {if: "_.isNumber($max)", validate: "isTrue", value: "$value <= $max", message: "must be no more than ${$max}"},
    // option {lessThan: aNumber}
    {if: "_.isNumber($lessThan)", validate: "isTrue", value: "$value < $lessThan", message: "must be less than ${$lessThan}"},
    // option {even: true}
    {if: "$even", validate: "isTrue", value: "$value % 2 === 0", message: "must be an even number"},
    // option {odd: true}
    {if: "$odd", validate: "isTrue", value: "$value % 2 === 1", message: "must be an odd number"}
  ]],

  // {validate: 'string', minLength: 4, maxLength: 8}
  ["string", [
    {validate: "isTrue", value: "_.isString($value)", message: "must be a string", stopValidationChainIfFail: true},
    {if: "$minLength", validate: "isTrue", value: "_.size($value) >= $minLength", message: "must have at least ${$minLength} characters"},
    {if: "$maxLength", validate: "isTrue", value: "_.size($value) <= $maxLength", message: "must be no more than ${$maxLength} characters"}
  ]],

  // {validate: 'within', items: [ ... ]}
  ["within", {validate: "isTrue", value: "_.includes($items, $value)", message: "must be one of ${_.join($items, ', ')}"}],

  // {validate: 'notIn', items: [ ... ]}
  ["notIn", {validate: "isFalse", value: "_.includes($items, $value)", message: "must not be one of ${_.join($items, ', ')}"}],

  // {validate: 'contain', item: obj, /* or items: [...] */}
  ["contain", [
    {if: "$item", validate: "isTrue", value: "_.includes($value, $item)", message: "must contain ${$item}"},
    {if: "$items", validate: "isBlank", value: "_.difference($items, $value)", message: "missing ${_.difference($items, $value).join(', ')}"},
  ]],

  // {validate: 'password', minLength: 4, maxLength: 8, alphabet: true, mixCase: true, digit: true, specialChar: true}
  ["password", [
    // min/maxLength options would be passed through in scope, do not need explicit passing to string validator
    {validate: 'string'},
    // {validate: /[a-z]/i} is a shortcut to
    // {validate: "isTrue", value: /[a-z]/i}
    {if: '$alphabet', validate: /[a-z]/i, message: 'must contain alphabet letter'},
    // [/[a-z]/, /[A-Z]/] is a shortcut to
    // [
    //   {validate: "isTrue", value: /[a-z]/},
    //   {validate: "isTrue", value: /[A-Z]/}
    // ]
    {if: '$mixCase', group: [/[a-z]/, /[A-Z]/], message: 'must contain both lower case and upper case letters'},
    {if: '$digit', validate: /[0-9]/, message: 'must contain number'},
    {if: '$specialChar', validate: /[!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>\/?]/, message: 'must contain special character (like !@$%)'},
  ]],

  // email regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
  ["email", {validate: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
             message: "not a valid email"}],

  // unique. need to access neighbours
  // option items is evaluated from current scope
  // neighbours could be simple value (when no propertyPath)
  ["unique", {validate: "notIn", "items.bind": "$propertyPath ? _.map($neighbours, $propertyPath) : $neighbours", message: "must be unique"}],
];

export function config (validation) {
  _.each(standardTransformers, pair => {
    const [tester, transformer] = pair;
    validation.addTransformer(tester, transformer);
  });

  _.each(standardValidators, pair => {
    const [name, imp] = pair;
    validation.addValidator(name, imp);
  });
}
<|endoftext|>


<commit_before>// Copyright (c) 2012, David Haglund
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the copyright holder nor the names of its
//       contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

// TODO: Create a new class for the fetching of the user timeline.
// TODO: Visualize the data with d3.js
// TODO: User workers for the calculations
// TODO: count all hashtags in the tweets
// TODO: count all the user_mentions in the tweets
$(document).ready(function () {
    var user_info_url = 'https://api.twitter.com/1/users/show.json?callback=?&include_entities=true&screen_name=',
        user_timeline_url_base = ('https://api.twitter.com/1/statuses/user_timeline.json'
                            + '?callback=?'
                            + '&count=100'
                            + '&trim_user=1'
                            + '&include_entities=1'
                            + '&include_rts=1'
                            + '&contributor_details=1'
                            + '&screen_name='),
        request_number = 0,
        tweets = [],
        max_id = BigInteger(0),
        since_id = BigInteger(0),
        twitter_username = '';

    function getLocalStorageKey(username) {
        return 'user=' + username.toLowerCase();
    }

    function saveTweetsToLocalStorage() {
        try {
            localStorage[getLocalStorageKey(twitter_username)] = JSON.stringify(tweets);
        } catch(e) {
            console.log("Local storage is full, failed to store tweets for " + twitter_username);
        }
    }

    function addTweetsToTag(tag) {
        $.each(tweets, function (i, tweet) {
            $(tag).append('
'+ tweet.text +'
');
        });
    }

    function addMaxIdIfPresent(current_url) {
        var result = current_url;
        if (!max_id.isZero()) {
            result += '&max_id=' + max_id.prev().toString();
        }
        return result;
    }

    function addSinceIdIfPresent(current_url) {
        var result = current_url;
        if (!since_id.isZero()) {
            result += "&since_id=" + since_id.next().toString();
        }
        return result;
    }

    function makeSuccessFunction(current_request_number, base_url) {
        return (function (response) {
            if (current_request_number === request_number) {
                if (response.length > 0) { // did get new data
                    if (since_id.isZero()) { // a response with older tweets
                        tweets = tweets.concat(response);
                    } else if (max_id.isZero()) { // a response with newer tweets
                        tweets = response.concat(tweets);
                    } else { // need to sort!
                        tweets = tweets.concat(response);
                        tweets.sort(function (a, b) {
                            return BigInteger(a.id_str).compare(BigInteger(b.id_str));
                        });
                    }
                    max_id = BigInteger(response[response.length - 1].id_str); // The oldest is last
                    fetchUserTimeline(current_request_number, base_url);
                } else {
                    saveTweetsToLocalStorage();
                    addTweetsToTag('#tweets');
                }
            } else {
                console.log('Received response from old search!', current_request_number, request_number);
            }
        });
    }

    function error(d, msg) {
        console.log('Failed to fetch user timeline: ' + msg);
    }

    function makeRequest(current_request_number, base_url, request_url) {
        $.jsonp({
            url: request_url,
            success: makeSuccessFunction(current_request_number, base_url),
            error: error
        });
    }

    function fetchUserTimeline(current_request_number, base_url) {
        var current_url = base_url;

        // https://dev.twitter.com/docs/working-with-timelines
        current_url = addMaxIdIfPresent(current_url);
        current_url = addSinceIdIfPresent(current_url);
        makeRequest(current_request_number, base_url, current_url);
    }

    function fetchUserTweets(current_request_number) {
        var local_storage_key = getLocalStorageKey(twitter_username),
            current_url = user_timeline_url_base + twitter_username;
        if (localStorage[local_storage_key]) {
            tweets = JSON.parse(localStorage[local_storage_key]);
            console.log('Found ' + tweets.length + ' tweets in localStorage!');
            if (tweets.length > 0) {
                // set since_id to only fetch new feeds
                since_id = BigInteger(tweets[0].id_str);
            }
        }

        $('#tweets').html('');
        fetchUserTimeline(current_request_number, current_url);
    }

    function insertLabeledText(selector, label, text) {
        if (text !== '') {
            selector.append('
' + label + '

' + text + '
');
        }
    }

    $('#userSearch').submit(function () {
        var current_request_number;
        request_number += 1;
        current_request_number = request_number;

        // Reset the internal state, for the new request
        tweets = [];
        max_id = BigInteger(0);
        since_id = BigInteger(0);
        twitter_username = $('#twitter_username_query').val().toLowerCase();

        $('#twitterResults').html('');

        $.jsonp({
            url: user_info_url + twitter_username,
            success: function (response) {
                user_info = $('#user_info');
                user_info.html('');
                insertLabeledText(user_info, 'Name', response.name);
                insertLabeledText(user_info, 'Screen name', '@' + response.screen_name);
                insertLabeledText(user_info, 'Id', response.id);
                insertLabeledText(user_info, 'Location', response.location);
                insertLabeledText(user_info, 'Followers', response.followers_count);
                insertLabeledText(user_info, 'Following', response.friends_count);
                insertLabeledText(user_info, 'Favorites', response.favourites_count);
                insertLabeledText(user_info, 'Tweets', response.statuses_count);
                insertLabeledText(user_info, 'Created', response.created_at);

                $('#user_image').html('');
                $('#user_description').html('
' + response.description + '
');

                fetchUserTweets(current_request_number);
            },
            error: function (d, msg) {
                console.log('error!');
            }
        });
        return false;
    });
});
<commit_msg>Changed the variable names to camelCase<commit_after>// Copyright (c) 2012, David Haglund
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the copyright holder nor the names of its
//       contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

// TODO: Create a new class for the fetching of the user timeline.
// TODO: Visualize the data with d3.js
// TODO: User workers for the calculations
// TODO: count all hashtags in the tweets
// TODO: count all the user_mentions in the tweets
$(document).ready(function () {
    var userInfoUrl = 'https://api.twitter.com/1/users/show.json?callback=?&include_entities=true&screen_name=',
        userTimelineUrlBase = ('https://api.twitter.com/1/statuses/user_timeline.json'
                            + '?callback=?'
                            + '&count=100'
                            + '&trim_user=1'
                            + '&include_entities=1'
                            + '&include_rts=1'
                            + '&contributor_details=1'
                            + '&screen_name='),
        requestNumber = 0,
        tweets = [],
        maxId = BigInteger(0),
        sinceId = BigInteger(0),
        twitterUsername = '';

    function getLocalStorageKey(username) {
        return 'user=' + username.toLowerCase();
    }

    function saveTweetsToLocalStorage() {
        try {
            localStorage[getLocalStorageKey(twitterUsername)] = JSON.stringify(tweets);
        } catch(e) {
            console.log("Local storage is full, failed to store tweets for " + twitterUsername);
        }
    }

    function addTweetsToTag(tag) {
        $.each(tweets, function (i, tweet) {
            $(tag).append('
'+ tweet.text +'
');
        });
    }

    function addMaxIdIfPresent(currentUrl) {
        var result = currentUrl;
        if (!maxId.isZero()) {
            result += '&max_id=' + maxId.prev().toString();
        }
        return result;
    }

    function addSinceIdIfPresent(currentUrl) {
        var result = currentUrl;
        if (!sinceId.isZero()) {
            result += "&since_id=" + sinceId.next().toString();
        }
        return result;
    }

    function makeSuccessFunction(currentRequestNumber, baseUrl) {
        return (function (response) {
            if (currentRequestNumber === requestNumber) {
                if (response.length > 0) { // did get new data
                    if (sinceId.isZero()) { // a response with older tweets
                        tweets = tweets.concat(response);
                    } else if (maxId.isZero()) { // a response with newer tweets
                        tweets = response.concat(tweets);
                    } else { // need to sort!
                        tweets = tweets.concat(response);
                        tweets.sort(function (a, b) {
                            return BigInteger(a.id_str).compare(BigInteger(b.id_str));
                        });
                    }
                    maxId = BigInteger(response[response.length - 1].id_str); // The oldest is last
                    fetchUserTimeline(currentRequestNumber, baseUrl);
                } else {
                    saveTweetsToLocalStorage();
                    addTweetsToTag('#tweets');
                }
            } else {
                console.log('Received response from old search!', currentRequestNumber, requestNumber);
            }
        });
    }

    function error(d, msg) {
        console.log('Failed to fetch user timeline: ' + msg);
    }

    function makeRequest(currentRequestNumber, baseUrl, requestUrl) {
        $.jsonp({
            url: requestUrl,
            success: makeSuccessFunction(currentRequestNumber, baseUrl),
            error: error
        });
    }

    function fetchUserTimeline(currentRequestNumber, baseUrl) {
        var currentUrl = baseUrl;

        // https://dev.twitter.com/docs/working-with-timelines
        currentUrl = addMaxIdIfPresent(currentUrl);
        currentUrl = addSinceIdIfPresent(currentUrl);
        makeRequest(currentRequestNumber, baseUrl, currentUrl);
    }

    function fetchUserTweets(currentRequestNumber) {
        var localStorageKey = getLocalStorageKey(twitterUsername),
            currentUrl = userTimelineUrlBase + twitterUsername;
        if (localStorage[localStorageKey]) {
            tweets = JSON.parse(localStorage[localStorageKey]);
            console.log('Found ' + tweets.length + ' tweets in localStorage!');
            if (tweets.length > 0) {
                // set sinceId to only fetch new feeds
                sinceId = BigInteger(tweets[0].id_str);
            }
        }

        $('#tweets').html('');
        fetchUserTimeline(currentRequestNumber, currentUrl);
    }

    function insertLabeledText(selector, label, text) {
        if (text !== '') {
            selector.append('
' + label + '

' + text + '
');
        }
    }

    $('#userSearch').submit(function () {
        var currentRequestNumber;
        requestNumber += 1;
        currentRequestNumber = requestNumber;

        // Reset the internal state, for the new request
        tweets = [];
        maxId = BigInteger(0);
        sinceId = BigInteger(0);
        twitterUsername = $('#twitter_username_query').val().toLowerCase();

        $.jsonp({
            url: userInfoUrl + twitterUsername,
            success: function (response) {
                userInfo = $('#user_info');
                userInfo.html('');
                insertLabeledText(userInfo, 'Name', response.name);
                insertLabeledText(userInfo, 'Screen name', '@' + response.screen_name);
                insertLabeledText(userInfo, 'Id', response.id);
                insertLabeledText(userInfo, 'Location', response.location);
                insertLabeledText(userInfo, 'Followers', response.followers_count);
                insertLabeledText(userInfo, 'Following', response.friends_count);
                insertLabeledText(userInfo, 'Favorites', response.favourites_count);
                insertLabeledText(userInfo, 'Tweets', response.statuses_count);
                insertLabeledText(userInfo, 'Created', response.created_at);

                $('#user_image').html('');
                $('#user_description').html('
' + response.description + '
');

                fetchUserTweets(currentRequestNumber);
            },
            error: function (d, msg) {
                console.log('error!');
            }
        });
        return false;
    });
});
<|endoftext|>


<commit_before>// Meiosis

import flyd from "flyd";
import m from "mithril";
import * as R from "ramda";
import objectPath from "object-path";
import meiosisTracer from "meiosis-tracer";

const meiosis = (initialModel) => {
  const components = {};
  const propose = flyd.stream();

  const receive = (model, proposal) => Object.keys(components)
    .map(key => components[key])
    .map(R.prop("receive"))
    .reduce((model, rcv) => rcv(model, proposal), model);

  const model = flyd.scan(receive, initialModel, propose);

  return {
    components,
    model,
    propose
  };
};

// Util

const pipeIn = function() {
  return R.pipe.apply(R, Array.prototype.slice.call(arguments, 1))(arguments[0]);
};

const nestComponent = function(component, path) {
  return {
    receive: component.receive && ((model, proposal) => {
      component.receive(objectPath.get(model, path), proposal);
      return model;
    }),
    view: component.view && (model => component.view((component.state || R.identity)(objectPath.get(model, path)))),
    initialModel: component.initialModel
  };
};

// Credit: source: https://github.com/jayrbolton/flyd-zip
const zip = sources => {
  var withIdxs = R.addIndex(R.map)(function(s, i) {
    return flyd.map(function(v) {
      return [v, i];
    }, s);
  }, sources);

  var buffer = [];

  return flyd.combine(function() {
    var changes = R.last(arguments);

    R.map(R.apply(function(val, idx) {
      buffer[idx] = val;
    }), R.map(R.call, changes));

    if (R.filter(function(n) {
      return n !== undefined;
    }, buffer).length === sources.length) {
      var _self = arguments[arguments.length - 2];
      _self(buffer);
      buffer = [];
    }
  }, withIdxs);
};

// Counter

const counterComponent = (propose, id) => {
  const remove = !!id;
  id = id || "counter_" + String(new Date().getTime());

  const events = propose => ({
    onIncrease: _evt => propose({ counterId: id, add:  1 }),
    onDecrease: _evt => propose({ counterId: id, add: -1 }),
    onRemove: _evt => propose({ counterId: id, removeCounter: true })
  });

  const createView = events => model => m("div",
    m("span", "Counter: " + model.counter + " " + (model.even ? "Even" : "Odd")),
    m("button.btn.btn-primary", { onclick: events.onIncrease }, "Increase"),
    m("button.btn.btn-default", { onclick: events.onDecrease }, "Decrease"),
    remove ? m("button.btn.btn-danger", { onclick: events.onRemove }, "Remove") : null);

  const initialModel = { counter: 0 };

  const receive = (model, proposal) => {
    if (proposal.counterId === id && proposal.add) {
      model.counter += proposal.add;
    }
    return model;
  };

  const view = pipeIn(propose, events, createView);

  const state = model => Object.assign({}, model, { even: model.counter % 2 === 0 });

  return { initialModel, receive, view, state };
};

const initialModel = { counter: 0, counterIds: [], countersById: {} };
const { propose, model, components } = meiosis(initialModel);
const counter = counterComponent(propose);
components["counter"] = counter;

const nextAction = (model, _proposal) => {
  if (model.counter > 0 && model.counter % 10 === 0) {
    propose({ add: 2 });
  }
};

flyd.on(pair => nextAction(pair[0], pair[1]), zip([model, propose]));

const events = propose => ({
  onAddCounter: _evt => propose({ addCounter: true })
});

const createView = events => model => m("div",
  components["counter"].view(model),
  m("button.btn.btn-primary", { onclick: events.onAddCounter }, "Add Counter"),
  model.counterIds.map(id => components[id]).map(component => component.view(model)));

const view = pipeIn(propose, events, createView);

const counterContainer = {
  receive: (model, proposal) => {
    if (proposal.addCounter) {
      const id = "counter_" + String(new Date().getTime());
      const counter = nestComponent(counterComponent(propose, id), "countersById." + id);
      model.countersById[id] = counter.initialModel;
      model.counterIds.push(id);
      components[id] = counter;
    }
    else if (proposal.removeCounter) {
      const id = proposal.counterId;
      delete components[id];
      delete model.countersById[id];
      model.counterIds.splice(model.counterIds.indexOf(id), 1);
    }
    return model;
  }
};
components["counterContainer"] = counterContainer;

const element = document.getElementById("app");
flyd.on(model => m.render(element, view(model)), flyd.map(counter.state, model));

const createComponent = component => {
  components["tracer"] = component;
};
const renderRoot = model => m.render(element, view(model));
renderRoot.initialModel = initialModel;
renderRoot.state = R.identity;

meiosisTracer(createComponent, renderRoot, "#tracer");
<commit_msg>More experimentation.<commit_after>// Meiosis

import flyd from "flyd";
import m from "mithril";
import * as R from "ramda";
import objectPath from "object-path";
import meiosisTracer from "meiosis-tracer";

const meiosis = (initialModel, receive) => {
  const propose = flyd.stream();

  const model = flyd.scan(receive, initialModel, propose);

  return {
    model,
    propose
  };
};

// Util

const pipeIn = function() {
  return R.pipe.apply(R, Array.prototype.slice.call(arguments, 1))(arguments[0]);
};

const nestComponent = function(component, path) {
  return {
    receive: component.receive && ((model, proposal) => {
      component.receive(objectPath.get(model, path), proposal);
      return model;
    }),
    view: component.view && (model => component.view((component.state || R.identity)(objectPath.get(model, path)))),
    initialModel: component.initialModel
  };
};

// Credit: source: https://github.com/jayrbolton/flyd-zip
const zip = sources => {
  var withIdxs = R.addIndex(R.map)(function(s, i) {
    return flyd.map(function(v) {
      return [v, i];
    }, s);
  }, sources);

  var buffer = [];

  return flyd.combine(function() {
    var changes = R.last(arguments);

    R.map(R.apply(function(val, idx) {
      buffer[idx] = val;
    }), R.map(R.call, changes));

    if (R.filter(function(n) {
      return n !== undefined;
    }, buffer).length === sources.length) {
      var _self = arguments[arguments.length - 2];
      _self(buffer);
      buffer = [];
    }
  }, withIdxs);
};

// Counter

const counterComponent = (propose, id) => {
  const remove = !!id;
  id = id || "counter_" + String(new Date().getTime());

  const events = propose => ({
    onIncrease: _evt => propose({ counterId: id, add:  1 }),
    onDecrease: _evt => propose({ counterId: id, add: -1 }),
    onRemove: _evt => propose({ counterId: id, removeCounter: true })
  });

  const createView = events => model => m("div",
    m("span", "Counter: " + model.counter + " " + (model.even ? "Even" : "Odd")),
    m("button.btn.btn-primary", { onclick: events.onIncrease }, "Increase"),
    m("button.btn.btn-default", { onclick: events.onDecrease }, "Decrease"),
    remove ? m("button.btn.btn-danger", { onclick: events.onRemove }, "Remove") : null);

  const initialModel = { counter: 0 };

  const receive = (model, proposal) => {
    if (proposal.counterId === id && proposal.add) {
      model.counter += proposal.add;
    }
    return model;
  };

  const view = pipeIn(propose, events, createView);

  const state = model => Object.assign({}, model, { even: model.counter % 2 === 0 });

  return { initialModel, receive, view, state };
};

const initialModel = { counter: 0, counterIds: [], countersById: {} };

const components = {};
const additionalComponents = [];

const receive = (model, proposal) => Object.keys(components)
  .map(key => components[key])
  .concat(additionalComponents)
  .map(R.prop("receive"))
  .reduce((model, rcv) => rcv(model, proposal), model);

const { propose, model } = meiosis(initialModel, receive);
const counter = counterComponent(propose);
components["counter"] = counter;

const nextAction = (model, _proposal) => {
  if (model.counter > 0 && model.counter % 10 === 0) {
    propose({ add: 2 });
  }
};

flyd.on(pair => nextAction(pair[0], pair[1]), zip([model, propose]));

const events = propose => ({
  onAddCounter: _evt => propose({ addCounter: true })
});

const createView = events => model => m("div",
  components["counter"].view(model),
  m("button.btn.btn-primary", { onclick: events.onAddCounter }, "Add Counter"),
  model.counterIds.map(id => components[id]).map(component => component.view(model)));

const view = pipeIn(propose, events, createView);

const counterContainer = {
  receive: (model, proposal) => {
    if (proposal.addCounter) {
      const id = "counter_" + String(new Date().getTime());
      const counter = nestComponent(counterComponent(propose, id), "countersById." + id);
      model.countersById[id] = counter.initialModel;
      model.counterIds.push(id);
      components[id] = counter;
    }
    else if (proposal.removeCounter) {
      const id = proposal.counterId;
      delete components[id];
      delete model.countersById[id];
      model.counterIds.splice(model.counterIds.indexOf(id), 1);
    }
    return model;
  }
};
components["counterContainer"] = counterContainer;

const element = document.getElementById("app");
flyd.on(model => m.render(element, view(model)), flyd.map(counter.state, model));

const createComponent = component => {
  additionalComponents.push(component);
};
const renderRoot = model => m.render(element, view(model));
renderRoot.initialModel = initialModel;
renderRoot.state = R.identity;

meiosisTracer(createComponent, renderRoot, "#tracer");
<|endoftext|>


<commit_before>/*
 * jQuery MultiSelect UI Widget Filtering Plugin 1.1pre
 * Copyright (c) 2010 Eric Hynds
 *
 * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
 *
 * Depends:
 *   - jQuery UI MultiSelect widget
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
*/
(function($){
	$.widget("ech.multiselectfilter", {

		options: {
			label: "Filter:",
			width: null, /* override default width set in css file (px). null will inherit */
			placeholder: "Enter keywords"
		},

		_create: function(){
			var self = this,
				opts = this.options,
				instance = (this.instance = $(this.element).data("multiselect")),

				// store header; add filter class so the close/check all/uncheck all links can be positioned correctly
				header = (this.header = instance.menu.find(".ui-multiselect-header").addClass("ui-multiselect-hasfilter")),

				// wrapper elem
				wrapper = (this.wrapper = $('
'+(opts.label.length ? opts.label : '')+'<input placeholder="'+opts.placeholder+'" type="search"' + (/\d/.test(opts.width) ? 'style="width:'+opts.width+'px"' : '') + ' />
').prependTo( this.header ));

			// reference to the actual inputs
			this.inputs = instance.menu.find(":checkbox, :radio");

			// build the input box
			this.input = wrapper
			.find("input")
			.bind("keydown", function( e ){
				// prevent the enter key from submitting the form / closing the widget
				if( e.which === 13 ){
					e.preventDefault();
				}
			})
			.bind("keyup", $.proxy(self._handler, self) )
			.bind("click", function(){
				if( !this.value.length ){
					self._handler();
				}
			});

			// cache input values for searching
			this.updateCache();

			// each list item
			this.rows = instance.menu.find(".ui-multiselect-checkboxes li:not(.ui-multiselect-optgroup-label)");

			// rewrite internal _toggleChecked fn so that when checkAll/uncheckAll is fired,
			// only the currently filtered elements are checked
			instance._toggleChecked = function(flag, group){
				var $inputs = (group && group.length) ?
						group :
						this.labels.find('input'),

					// do not include hidden elems if the menu isn't open.
					selector = self.instance._isOpen ?
						":disabled, :hidden" :
						":disabled";

				// toggle checked
				$inputs.not( selector ).attr('checked', flag);

				// update text
				this.update();

				// figure out which option tags need to be selected
				var values = $inputs.map(function(){
					return this.value;
				}).get();

				// select option tags
				this.element.find('option').filter(function(){
					return !this.disabled && $.inArray(this.value, values) > -1;
				}).attr({ 'selected':flag, 'aria-selected':flag });
			};
		},

		// thx for the logic here ben alman
		_handler: function( e ){
			var term = $.trim( this.input[0].value.toLowerCase() ),

				// speed up references
				rows = this.rows, inputs = this.inputs, cache = this.cache;

			if( !term ){
				rows.show();
			} else {
				rows.hide();

				var regex = new RegExp(term.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'gi');
				this._trigger( "filter", e, $.map(cache, function(v,i){
					if( v.search(regex) !== -1 ){
						rows.eq(i).show();
						return inputs.get(i);
					}

					return null;
				}));
			}
		},

		updateCache: function(){
			this.cache = this.element.children().map(function(){
				var self = $(this);

				// account for optgroups
				if( this.tagName.toLowerCase() === "optgroup" ){
					self = self.children();
				}

				// see _create() in jquery.multiselect.js
				if( !self.val().length ){
					return null;
				}

				return self.map(function(){
					return this.innerHTML.toLowerCase();
				}).get();
			}).get();
		},

		widget: function(){
			return this.wrapper;
		},

		destroy: function(){
			$.Widget.prototype.destroy.call( this );
			this.input.val('').trigger("keyup");
			this.wrapper.remove();
		}
	});
})(jQuery);
<commit_msg>combined 3 bind() calls into 1<commit_after>/*
 * jQuery MultiSelect UI Widget Filtering Plugin 1.1pre
 * Copyright (c) 2010 Eric Hynds
 *
 * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
 *
 * Depends:
 *   - jQuery UI MultiSelect widget
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
*/
(function($){
	$.widget("ech.multiselectfilter", {

		options: {
			label: "Filter:",
			width: null, /* override default width set in css file (px). null will inherit */
			placeholder: "Enter keywords"
		},

		_create: function(){
			var self = this,
				opts = this.options,
				instance = (this.instance = $(this.element).data("multiselect")),

				// store header; add filter class so the close/check all/uncheck all links can be positioned correctly
				header = (this.header = instance.menu.find(".ui-multiselect-header").addClass("ui-multiselect-hasfilter")),

				// wrapper elem
				wrapper = (this.wrapper = $('
'+(opts.label.length ? opts.label : '')+'<input placeholder="'+opts.placeholder+'" type="search"' + (/\d/.test(opts.width) ? 'style="width:'+opts.width+'px"' : '') + ' />
').prependTo( this.header ));

			// reference to the actual inputs
			this.inputs = instance.menu.find(":checkbox, :radio");

			// build the input box
			this.input = wrapper
			.find("input")
			.bind({
				keydown: function( e ){
					// prevent the enter key from submitting the form / closing the widget
					if( e.which === 13 ){
						e.preventDefault();
					}
				},
				keyup: function(){
					self._handler();
				},
				click: function(){
					if( !this.value.length ){
						self._handler();
					}
				}
			});

			// cache input values for searching
			this.updateCache();

			// each list item
			this.rows = instance.menu.find(".ui-multiselect-checkboxes li:not(.ui-multiselect-optgroup-label)");

			// rewrite internal _toggleChecked fn so that when checkAll/uncheckAll is fired,
			// only the currently filtered elements are checked
			instance._toggleChecked = function(flag, group){
				var $inputs = (group && group.length) ?
						group :
						this.labels.find('input'),

					// do not include hidden elems if the menu isn't open.
					selector = self.instance._isOpen ?
						":disabled, :hidden" :
						":disabled";

				// toggle checked
				$inputs.not( selector ).attr('checked', flag);

				// update text
				this.update();

				// figure out which option tags need to be selected
				var values = $inputs.map(function(){
					return this.value;
				}).get();

				// select option tags
				this.element.find('option').filter(function(){
					return !this.disabled && $.inArray(this.value, values) > -1;
				}).attr({ 'selected':flag, 'aria-selected':flag });
			};
		},

		// thx for the logic here ben alman
		_handler: function( e ){
			var term = $.trim( this.input[0].value.toLowerCase() ),

				// speed up references
				rows = this.rows, inputs = this.inputs, cache = this.cache;

			if( !term ){
				rows.show();
			} else {
				rows.hide();

				var regex = new RegExp(term.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), 'gi');
				this._trigger( "filter", e, $.map(cache, function(v,i){
					if( v.search(regex) !== -1 ){
						rows.eq(i).show();
						return inputs.get(i);
					}

					return null;
				}));
			}
		},

		updateCache: function(){
			this.cache = this.element.children().map(function(){
				var self = $(this);

				// account for optgroups
				if( this.tagName.toLowerCase() === "optgroup" ){
					self = self.children();
				}

				// see _create() in jquery.multiselect.js
				if( !self.val().length ){
					return null;
				}

				return self.map(function(){
					return this.innerHTML.toLowerCase();
				}).get();
			}).get();
		},

		widget: function(){
			return this.wrapper;
		},

		destroy: function(){
			$.Widget.prototype.destroy.call( this );
			this.input.val('').trigger("keyup");
			this.wrapper.remove();
		}
	});
})(jQuery);
<|endoftext|>


<commit_before>import _ from 'lodash';
import {createOverrideContext} from 'bcx-expression-evaluator';
import valueEvaluator from './value-evaluator';

export function isBlank(v) {
  if (_.isNull(v) || _.isUndefined(v) || _.isNaN(v)) return true;
  if (_.isString(v)) {
    if (_.trim(v).length === 0) return true;
  } else if (_.isArray(v) || _.isPlainObject(v)) {
    if (_.isEmpty(v)) return true;
  }
}

export const ifTester = function (rule) {
  if (!_.has(rule, 'if')) return false;
  return _.isString(rule.if) && !_.isEmpty(_.omit(rule, 'if'));
};

export const ifTransformer = function (rule) {
  const _if = _.get(rule, 'if');
  const subRule = _.omit(rule, 'if');

  let rules = [];
  // if condition is false, skip the real validation
  rules.push({validate: "skipImmediatelyIf", value: `!(${_if})`});

  if (_.has(subRule, 'group') &&
      _.isArray(subRule.group)) {
    rules.push(... subRule.group);
  } else {
    rules.push(subRule);
  }

  return rules;
};

export const switchTester = function (rule) {
  if (!_.has(rule, 'switch')) return false;
  if (!_.has(rule, 'cases')) return false;
  if (!_.isEmpty(_.omit(rule, ['switch', 'cases']))) return false;
  return (_.isString(rule.switch) || _.isFunction(rule.switch)) &&
    _.isObjectLike(rule.cases);
};

export const switchTransformer = function (rule, validate, inPropertyName) {
  const _switch = _.get(rule, 'switch');
  const cases = _.get(rule, 'cases');
  const switchEvaluator = valueEvaluator(_switch);

  const precompiledPlain = _.mapValues(cases, (rules, _case) => validate(rules));
  const precompiledNested = _.mapValues(cases, (rules, _case) => validate(rules, inPropertyName));

  const validator = scope => {
    // make a guess whether user try to use nested validation or plain validation
    const value = scope.overrideContext.$value;
    let precompiled;

    if (_.isObjectLike(value)) {
      // in nested object
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(value, overrideContext);
      newOverrideContext.$value = value;
      const newScope = {
        bindingContext: value,
        overrideContext: newOverrideContext
      };

      precompiled = precompiledNested[switchEvaluator(newScope)];
    } else {
      // normal switch
      precompiled = precompiledPlain[switchEvaluator(scope)];
    }

    return precompiled && precompiled(scope);
  };

  validator.readyToUse = true;
  return validator;
};

export const forEachTester = function (rule) {
  if (!_.has(rule, 'foreach')) return false;
  if (!_.isEmpty(_.omit(rule, ['foreach', 'key']))) return false;
  if (rule.key && !_.isString(rule.key) && !_.isFunction(rule.key)) return false;
  return true;
};

export const forEachTransformer = function (rule, validate /*, inPropertyName*/) {
  const foreachRulesMap = _.get(rule, 'foreach');
  let foreachRulesMapFunc;
  if (_.isFunction(foreachRulesMap)) {
    foreachRulesMapFunc = valueEvaluator(foreachRulesMap);
  } else if (_.isArray(foreachRulesMap) && _.some(foreachRulesMap, _.isFunction)) {
    foreachRulesMapFunc = scope => _.map(foreachRulesMap, r =>
      _.isFunction(r) ? valueEvaluator(r)(scope) : r
    );
  }

  // don't pass inPropertyName to underneath validators,
  // they work in new overrideContext with propertyPath null.
  const precompiled = !foreachRulesMapFunc && validate(foreachRulesMap);

  const _key = _.get(rule, 'key', '$index');
  const keyEvaluator = valueEvaluator(_key);

  const validator = scope => {
    let errors = {};
    const enumerable = scope.overrideContext.$value;
    const length = _.size(enumerable);
    _.each(enumerable, (item, index) => {
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(item, overrideContext);

      let neighbours = _.filter(enumerable, (v, i) => i !== index);

      _.merge(newOverrideContext, {
        $value: item,
        $propertyPath: null, // initial propertyPath
        $neighbours: neighbours,
        $neighbourValues: neighbours,
        $index: index,
        $first: index === 0,
        $last: (index === length - 1),
      });

      const newScope = {
        bindingContext: item,
        overrideContext: newOverrideContext
      };

      const key = keyEvaluator(newScope);
      const result = (precompiled || validate(foreachRulesMapFunc(newScope)))(newScope);

      if (result.isValid === false) {
        errors[key] = result.errors;
      }
    });

    return {isValid: _.isEmpty(errors), errors};
  };

  validator.readyToUse = true;
  return validator;
};

export function config (validation) {

// Transformers

  // The order of transformers and validators is very important.
  // It is the order that resolveValidator finds a match.

  // if a rule contains {if: 'expression'}
  // transform it into a chain of validators
  // support two forms:
  //
  // group form:
  //   {if: 'expression', group: [ /* validators */]}
  //
  // single form:
  //   {if: 'expression', validate: 'name', ...}
  //
  validation.addTransformer(ifTester, ifTransformer);

  // switch
  //
  // on single field:
  //   {
  //     switch: 'expression',
  //     cases: {
  //       "matchingA": {validate: 'name', ...},
  //       "matchingB": [ /* validators */ ]
  //     }
  //   }
  //
  // on nested object:
  //   {
  //     switch: 'expression',
  //     cases: {
  //       "matchingA": {field1: {validate: 'name', ...}, field2: [ /* validators */ ],
  //       "matchingB": {/*...*/}
  //     }
  //   }
  //
  validation.addTransformer(switchTester, switchTransformer);

  // foreach
  //
  // {
  //   foreach: ...rules,
  //   key: optional key expression or function
  // }
  //
  validation.addTransformer(forEachTester, forEachTransformer);

  // transform regex
  validation.addTransformer(
    _.isRegExp,
    rule => ({validate: "isTrue", value: rule, message: 'invalid format'})
  );

  validation.addTransformer(
    r => _.isRegExp(r && r.validate),
    rule => ({validate: "isTrue", value: rule.validate, message: 'invalid format'})
  );

// Validators

  // validators implemented in functions.

  validation.addValidator("isTrue", v => v ? null : "must be true");
  validation.addValidator("isFalse", v => v ? "must be false" : null);

  validation.addValidator(
    "passImmediatelyIf",
    v => v ?
    // stop the chain if passed
    {isValid: true, break: true} :
    // continue, never fail
    null
  );

  validation.addValidator(
    "skipImmediatelyIf",
    v => v ?
    // skip rest if passed
    // isValid is not true, but null
    {isValid: null, break: true} :
    // continue, never fail
    null
  );

  validation.addValidator(
    "failImmediatelyIf",
    v => v ?
    // stop the chain if failed
    {isValid: false, break: true} :
    // continue
    null
  );

  // all other validators are in form of composition.

  validation.addValidator("isBlank", {validate: "isTrue", value: isBlank, message: "must be blank"});
  validation.addValidator("notBlank", {validate: "isFalse", value: isBlank, message: "must not be blank"});

  validation.addValidator("mandatory", {validate: "failImmediatelyIf", value: isBlank, message: "must not be empty"});
  validation.addValidator("notMandatory", {validate: "skipImmediatelyIf", value: isBlank});

  // {validate: 'number', integer: true, min: 0, max: 10, greaterThan: 0, lessThan: 10, even: true, /* odd: true */}
  validation.addValidator("number", [
    {validate: "isTrue", value: v => _.isNumber(v), message: "must be a number", stopValidationChainIfFail: true},
    // option {integer: true}
    {if: "$integer", validate: "isTrue", value: v => _.isInteger(v), message: "must be an integer", stopValidationChainIfFail: true},
    // option {min: aNumber}
    {if: "_.isNumber($min)", validate: "isTrue", value: "$value >= $min", message: "must be at least ${$min}"},
    // option {greaterThan: aNumber}
    {if: "_.isNumber($greaterThan)", validate: "isTrue", value: "$value > $greaterThan", message: "must be greater than ${$greaterThan}"},
    // option {max: aNumber}
    {if: "_.isNumber($max)", validate: "isTrue", value: "$value <= $max", message: "must be no more than ${$max}"},
    // option {lessThan: aNumber}
    {if: "_.isNumber($lessThan)", validate: "isTrue", value: "$value < $lessThan", message: "must be less than ${$lessThan}"},
    // option {even: true}
    {if: "$even", validate: "isTrue", value: v => v % 2 === 0, message: "must be an even number"},
    // option {odd: true}
    {if: "$odd", validate: "isTrue", value: v => v % 2 === 1, message: "must be an odd number"}
  ]);

  // {validate: 'string', minLength: 4, maxLength: 8}
  validation.addValidator("string", [
    {validate: "isTrue", value: v => _.isString(v), message: "must be a string", stopValidationChainIfFail: true},
    {if: "$minLength", validate: "isTrue", value: "_.size($value) >= $minLength", message: "must have at least ${$minLength} characters"},
    {if: "$maxLength", validate: "isTrue", value: "_.size($value) <= $maxLength", message: "must be no more than ${$maxLength} characters"}
  ]);

  // {validate: 'within', items: [ ... ]}
  validation.addValidator("within", {validate: "isTrue", value: "_.includes($items, $value)", message: "must be one of ${_.join($items, ', ')}"});

  // {validate: 'notIn', items: [ ... ]}
  validation.addValidator("notIn", {validate: "isFalse", value: "_.includes($items, $value)", message: "must not be one of ${_.join($items, ', ')}"});

  // {validate: 'contain', item: obj, /* or items: [...] */}
  validation.addValidator("contain", [
    {if: "$item", validate: "isTrue", value: "_.includes($value, $item)", message: "must contain ${$item}"},
    {if: "$items", validate: "isBlank", value: "_.difference($items, $value)", message: "missing ${_.difference($items, $value).join(', ')}"},
  ]);

  // {validate: 'password', minLength: 4, maxLength: 8, alphabet: true, mixCase: true, digit: true, specialChar: true}
  validation.addValidator("password", [
    // min/maxLength options would be passed through in scope, do not need explicit passing to string validator
    {validate: 'string'},
    {if: '$alphabet', validate: "isTrue", value: /[a-z]/i, message: 'must contain alphabet letter'},
    {if: '$mixCase', group: [{validate: "isTrue", value: /[a-z]/}, {validate: "isTrue", value: /[A-Z]/}], message: 'must contain both lower case and upper case letters'},
    {if: '$digit', validate: /[0-9]/, message: 'must contain number'},
    {if: '$specialChar', validate: /[!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>\/?]/, message: 'must contain special character (like !@$%)'},
  ]);

  // email regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
  validation.addValidator("email", {validate: "isTrue", value: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
             message: "not a valid email"});

  // unique. need to access neighbours
  // option items is evaluated from current scope
  validation.addValidator("unique", {validate: "notIn", "items.bind": "$neighbourValues", message: "must be unique"});
}
<commit_msg>remove unused variable.<commit_after>import _ from 'lodash';
import {createOverrideContext} from 'bcx-expression-evaluator';
import valueEvaluator from './value-evaluator';

export function isBlank(v) {
  if (_.isNull(v) || _.isUndefined(v) || _.isNaN(v)) return true;
  if (_.isString(v)) {
    if (_.trim(v).length === 0) return true;
  } else if (_.isArray(v) || _.isPlainObject(v)) {
    if (_.isEmpty(v)) return true;
  }
}

export const ifTester = function (rule) {
  if (!_.has(rule, 'if')) return false;
  return _.isString(rule.if) && !_.isEmpty(_.omit(rule, 'if'));
};

export const ifTransformer = function (rule) {
  const _if = _.get(rule, 'if');
  const subRule = _.omit(rule, 'if');

  let rules = [];
  // if condition is false, skip the real validation
  rules.push({validate: "skipImmediatelyIf", value: `!(${_if})`});

  if (_.has(subRule, 'group') &&
      _.isArray(subRule.group)) {
    rules.push(... subRule.group);
  } else {
    rules.push(subRule);
  }

  return rules;
};

export const switchTester = function (rule) {
  if (!_.has(rule, 'switch')) return false;
  if (!_.has(rule, 'cases')) return false;
  if (!_.isEmpty(_.omit(rule, ['switch', 'cases']))) return false;
  return (_.isString(rule.switch) || _.isFunction(rule.switch)) &&
    _.isObjectLike(rule.cases);
};

export const switchTransformer = function (rule, validate, inPropertyName) {
  const _switch = _.get(rule, 'switch');
  const cases = _.get(rule, 'cases');
  const switchEvaluator = valueEvaluator(_switch);

  const precompiledPlain = _.mapValues(cases, rules => validate(rules));
  const precompiledNested = _.mapValues(cases, rules => validate(rules, inPropertyName));

  const validator = scope => {
    // make a guess whether user try to use nested validation or plain validation
    const value = scope.overrideContext.$value;
    let precompiled;

    if (_.isObjectLike(value)) {
      // in nested object
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(value, overrideContext);
      newOverrideContext.$value = value;
      const newScope = {
        bindingContext: value,
        overrideContext: newOverrideContext
      };

      precompiled = precompiledNested[switchEvaluator(newScope)];
    } else {
      // normal switch
      precompiled = precompiledPlain[switchEvaluator(scope)];
    }

    return precompiled && precompiled(scope);
  };

  validator.readyToUse = true;
  return validator;
};

export const forEachTester = function (rule) {
  if (!_.has(rule, 'foreach')) return false;
  if (!_.isEmpty(_.omit(rule, ['foreach', 'key']))) return false;
  if (rule.key && !_.isString(rule.key) && !_.isFunction(rule.key)) return false;
  return true;
};

export const forEachTransformer = function (rule, validate /*, inPropertyName*/) {
  const foreachRulesMap = _.get(rule, 'foreach');
  let foreachRulesMapFunc;
  if (_.isFunction(foreachRulesMap)) {
    foreachRulesMapFunc = valueEvaluator(foreachRulesMap);
  } else if (_.isArray(foreachRulesMap) && _.some(foreachRulesMap, _.isFunction)) {
    foreachRulesMapFunc = scope => _.map(foreachRulesMap, r =>
      _.isFunction(r) ? valueEvaluator(r)(scope) : r
    );
  }

  // don't pass inPropertyName to underneath validators,
  // they work in new overrideContext with propertyPath null.
  const precompiled = !foreachRulesMapFunc && validate(foreachRulesMap);

  const _key = _.get(rule, 'key', '$index');
  const keyEvaluator = valueEvaluator(_key);

  const validator = scope => {
    let errors = {};
    const enumerable = scope.overrideContext.$value;
    const length = _.size(enumerable);
    _.each(enumerable, (item, index) => {
      const {overrideContext} = scope;
      let newOverrideContext = createOverrideContext(item, overrideContext);

      let neighbours = _.filter(enumerable, (v, i) => i !== index);

      _.merge(newOverrideContext, {
        $value: item,
        $propertyPath: null, // initial propertyPath
        $neighbours: neighbours,
        $neighbourValues: neighbours,
        $index: index,
        $first: index === 0,
        $last: (index === length - 1),
      });

      const newScope = {
        bindingContext: item,
        overrideContext: newOverrideContext
      };

      const key = keyEvaluator(newScope);
      const result = (precompiled || validate(foreachRulesMapFunc(newScope)))(newScope);

      if (result.isValid === false) {
        errors[key] = result.errors;
      }
    });

    return {isValid: _.isEmpty(errors), errors};
  };

  validator.readyToUse = true;
  return validator;
};

export function config (validation) {

// Transformers

  // The order of transformers and validators is very important.
  // It is the order that resolveValidator finds a match.

  // if a rule contains {if: 'expression'}
  // transform it into a chain of validators
  // support two forms:
  //
  // group form:
  //   {if: 'expression', group: [ /* validators */]}
  //
  // single form:
  //   {if: 'expression', validate: 'name', ...}
  //
  validation.addTransformer(ifTester, ifTransformer);

  // switch
  //
  // on single field:
  //   {
  //     switch: 'expression',
  //     cases: {
  //       "matchingA": {validate: 'name', ...},
  //       "matchingB": [ /* validators */ ]
  //     }
  //   }
  //
  // on nested object:
  //   {
  //     switch: 'expression',
  //     cases: {
  //       "matchingA": {field1: {validate: 'name', ...}, field2: [ /* validators */ ],
  //       "matchingB": {/*...*/}
  //     }
  //   }
  //
  validation.addTransformer(switchTester, switchTransformer);

  // foreach
  //
  // {
  //   foreach: ...rules,
  //   key: optional key expression or function
  // }
  //
  validation.addTransformer(forEachTester, forEachTransformer);

  // transform regex
  validation.addTransformer(
    _.isRegExp,
    rule => ({validate: "isTrue", value: rule, message: 'invalid format'})
  );

  validation.addTransformer(
    r => _.isRegExp(r && r.validate),
    rule => ({validate: "isTrue", value: rule.validate, message: 'invalid format'})
  );

// Validators

  // validators implemented in functions.

  validation.addValidator("isTrue", v => v ? null : "must be true");
  validation.addValidator("isFalse", v => v ? "must be false" : null);

  validation.addValidator(
    "passImmediatelyIf",
    v => v ?
    // stop the chain if passed
    {isValid: true, break: true} :
    // continue, never fail
    null
  );

  validation.addValidator(
    "skipImmediatelyIf",
    v => v ?
    // skip rest if passed
    // isValid is not true, but null
    {isValid: null, break: true} :
    // continue, never fail
    null
  );

  validation.addValidator(
    "failImmediatelyIf",
    v => v ?
    // stop the chain if failed
    {isValid: false, break: true} :
    // continue
    null
  );

  // all other validators are in form of composition.

  validation.addValidator("isBlank", {validate: "isTrue", value: isBlank, message: "must be blank"});
  validation.addValidator("notBlank", {validate: "isFalse", value: isBlank, message: "must not be blank"});

  validation.addValidator("mandatory", {validate: "failImmediatelyIf", value: isBlank, message: "must not be empty"});
  validation.addValidator("notMandatory", {validate: "skipImmediatelyIf", value: isBlank});

  // {validate: 'number', integer: true, min: 0, max: 10, greaterThan: 0, lessThan: 10, even: true, /* odd: true */}
  validation.addValidator("number", [
    {validate: "isTrue", value: v => _.isNumber(v), message: "must be a number", stopValidationChainIfFail: true},
    // option {integer: true}
    {if: "$integer", validate: "isTrue", value: v => _.isInteger(v), message: "must be an integer", stopValidationChainIfFail: true},
    // option {min: aNumber}
    {if: "_.isNumber($min)", validate: "isTrue", value: "$value >= $min", message: "must be at least ${$min}"},
    // option {greaterThan: aNumber}
    {if: "_.isNumber($greaterThan)", validate: "isTrue", value: "$value > $greaterThan", message: "must be greater than ${$greaterThan}"},
    // option {max: aNumber}
    {if: "_.isNumber($max)", validate: "isTrue", value: "$value <= $max", message: "must be no more than ${$max}"},
    // option {lessThan: aNumber}
    {if: "_.isNumber($lessThan)", validate: "isTrue", value: "$value < $lessThan", message: "must be less than ${$lessThan}"},
    // option {even: true}
    {if: "$even", validate: "isTrue", value: v => v % 2 === 0, message: "must be an even number"},
    // option {odd: true}
    {if: "$odd", validate: "isTrue", value: v => v % 2 === 1, message: "must be an odd number"}
  ]);

  // {validate: 'string', minLength: 4, maxLength: 8}
  validation.addValidator("string", [
    {validate: "isTrue", value: v => _.isString(v), message: "must be a string", stopValidationChainIfFail: true},
    {if: "$minLength", validate: "isTrue", value: "_.size($value) >= $minLength", message: "must have at least ${$minLength} characters"},
    {if: "$maxLength", validate: "isTrue", value: "_.size($value) <= $maxLength", message: "must be no more than ${$maxLength} characters"}
  ]);

  // {validate: 'within', items: [ ... ]}
  validation.addValidator("within", {validate: "isTrue", value: "_.includes($items, $value)", message: "must be one of ${_.join($items, ', ')}"});

  // {validate: 'notIn', items: [ ... ]}
  validation.addValidator("notIn", {validate: "isFalse", value: "_.includes($items, $value)", message: "must not be one of ${_.join($items, ', ')}"});

  // {validate: 'contain', item: obj, /* or items: [...] */}
  validation.addValidator("contain", [
    {if: "$item", validate: "isTrue", value: "_.includes($value, $item)", message: "must contain ${$item}"},
    {if: "$items", validate: "isBlank", value: "_.difference($items, $value)", message: "missing ${_.difference($items, $value).join(', ')}"},
  ]);

  // {validate: 'password', minLength: 4, maxLength: 8, alphabet: true, mixCase: true, digit: true, specialChar: true}
  validation.addValidator("password", [
    // min/maxLength options would be passed through in scope, do not need explicit passing to string validator
    {validate: 'string'},
    {if: '$alphabet', validate: "isTrue", value: /[a-z]/i, message: 'must contain alphabet letter'},
    {if: '$mixCase', group: [{validate: "isTrue", value: /[a-z]/}, {validate: "isTrue", value: /[A-Z]/}], message: 'must contain both lower case and upper case letters'},
    {if: '$digit', validate: /[0-9]/, message: 'must contain number'},
    {if: '$specialChar', validate: /[!@#$%^&*()\-_=+\[\]{}\\|;:'",<.>\/?]/, message: 'must contain special character (like !@$%)'},
  ]);

  // email regex from https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
  validation.addValidator("email", {validate: "isTrue", value: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
             message: "not a valid email"});

  // unique. need to access neighbours
  // option items is evaluated from current scope
  validation.addValidator("unique", {validate: "notIn", "items.bind": "$neighbourValues", message: "must be unique"});
}
<|endoftext|>