<commit_before>import {
  Api,
  Exp,
  Logger,
  NotificationCode,
  UserSettings,
  MessageCode,
} from 'xdl';

import _ from 'lodash';
import os from 'os';
import path from 'path';
import React, { PropTypes } from 'react';
import LoadingIndicator from 'react-loading-indicator';

import Commands from './Commands';
import StyleConstants from './StyleConstants';
import SharedStyles from './Styles';
import * as IdentifierRules from 'xde/utils/IdentifierRules';

const ICON_SIZE = 120;
const MAX_PROJECT_LENGTH = 44;
const HALF_MAX_PROJECT_LENGTH = 20;

class NewProjectModal extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      isLoading: true,
      projectName: 'my-new-project',
      projectDirectory: null,
      errorMessage: null,
      loadingMessage: null,
      notificationType: null,
      showRetryPrompt: false,
      progress: null,
      templates: null,
      selectedTemplate: null,
    };
  }

  static propTypes = {
    onClose: PropTypes.func.isRequired,
    onSelectProject: PropTypes.func.isRequired,
  };

  _currentRequestID = 0;

  componentDidMount() {
    Logger.notifications.addStream({
      stream: {
        write: chunk => {
          switch (chunk.code) {
            case NotificationCode.PROGRESS:
              this.setState({
                loadingMessage: chunk.msg,
                notificationType: NotificationCode.PROGRESS,
              });
              break;
            case NotificationCode.DOWNLOAD:
              this.setState({
                loadingMessage: 'Downloading... ' + chunk.msg + '%',
                progress: chunk.msg,
                notificationType: NotificationCode.DOWNLOAD,
              });
              break;
            case NotificationCode.RETRY_DOWNLOAD:
              this.setState({
                showRetryPrompt: true,
              });
              break;
          }
        },
      },
      type: 'raw',
    });

    this._loadTemplatesAsync();
  }

  _loadTemplatesAsync = async () => {
    let versions = await Api.versionsAsync();
    let dir = await UserSettings.getAsync('defaultNewProjectDir', os.homedir());

    this.setState({
      isLoading: false,
      templates: versions.templates,
      selectedTemplate: versions.templates[0],
      projectDirectory: dir,
    });
  };

  render() {
    return (

        {this.state.isLoading ? this._renderLoading() : this._renderForm()}

    );
  }

  _selectTemplate = template => {
    this.setState({
      selectedTemplate: template,
    });
  };

  _renderTemplate = template => {
    let isSelected = this.state.selectedTemplate.id === template.id;

    return (

 this._selectTemplate(template)}>

          {template.name}

    );
  };

  _renderForm = () => {
    let { templates, selectedTemplate } = this.state;

    if (!templates) {
      return
;
    }

    return (
      <form
        name="newProject"
        style={Styles.form}
        onSubmit={this._onSubmitNewProject}>

          Choose a template for your project

            {_.map(templates, this._renderTemplate)}

          {selectedTemplate.description}

          Choose a project name and directory

        <input
          autoFocus
          type="text"
          className="form-control"
          style={Styles.input}
          ref="projectName"
          onChange={this._onProjectNameChange}
          value={this.state.projectName}
        />

          <input
            disabled
            type="text"
            className="form-control"
            style={{ color: StyleConstants.colorSubtitle }}
            ref="projectDirectory"
            value={this._getShortProjectDirectory()}
          />

            ...

        {this._renderErrors()}

          <button
            onClick={this._onClickCancel}
            type="button"
            style={Styles.cancelButton}>

Cancel

          </button>
          <button type="submit" style={Styles.submitButton}>

Create

          </button>

      </form>
    );
  };

  _renderErrors = () => {
    if (this.state.errorMessage) {
      return (

{this.state.errorMessage}

      );
    } else {
      return null;
    }
  };

  _renderLoading = () => {
    let downloading = this.state.notificationType === NotificationCode.DOWNLOAD;
    return (

        {downloading
          ?

          : <LoadingIndicator
              color={{
                red: 17,
                green: 114,
                blue: 182,
                alpha: 1.0,
              }}
              segmentWidth={6}
              segmentLength={15}
              spacing={9}
            />}
        {this.state.loadingMessage &&

            {this.state.loadingMessage}

}
        {this.state.showRetryPrompt
          ?

                {MessageCode.DOWNLOAD_IS_SLOW}

              <button
                onClick={this._onClickRetry}
                type="button"
                style={Styles.retryButton}>

Retry

              </button>

          :
}

    );
  };

  _onProjectNameChange = event => {
    let newValue = IdentifierRules.normalizeProjectNameWhileTyping(
      event.target.value
    );
    let errorMessage = null;
    if (newValue.length === 0) {
      errorMessage = 'Project name cannot be blank';
    }

    this.setState({
      projectName: newValue,
      errorMessage,
    });
  };

  _getShortProjectDirectory = () => {
    let { projectName, projectDirectory } = this.state;
    let dir = path.join(projectDirectory, projectName);
    if (dir.length < MAX_PROJECT_LENGTH) {
      return dir;
    } else {
      return `${dir.substr(0, HALF_MAX_PROJECT_LENGTH)}...${dir.substr(dir.length - HALF_MAX_PROJECT_LENGTH)}`;
    }
  };

  _onClickChangeProjectDirectoryAsync = async () => {
    try {
      let directory = await Commands.getDirectoryAsync(
        this.state.projectDirectory
      );
      if (!directory) {
        return;
      }

      await UserSettings.setAsync('defaultNewProjectDir', directory);
      this.setState({
        projectDirectory: directory,
      });
    } catch (e) {}
  };

  _onClickCancel = event => {
    event.preventDefault();
    this.props.onClose();
  };

  _onClickRetry = async event => {
    Exp.clearXDLCacheAsync();
    this.setState({ showRetryPrompt: false });
    this._onSubmitNewProject(event);
  };

  _onSubmitNewProject = async event => {
    event.preventDefault();
    const requestID = this._currentRequestID + 1;
    this._currentRequestID = requestID;

    if (this.state.projectName.length === 0) {
      this.setState({
        errorMessage: 'Project name cannot be blank',
      });
      return;
    } else {
      this.setState({
        errorMessage: null,
        loadingMessage: null,
        isLoading: true,
      });
    }

    try {
      let templateDownload = await Exp.downloadTemplateApp(
        this.state.selectedTemplate.id,
        this.state.projectDirectory,
        {
          name: this.state.projectName,
          progressFunction: progress => {
            if (this._currentRequestID === requestID) {
              Logger.notifications.info(
                { code: NotificationCode.DOWNLOAD },
                Math.round(progress * 100)
              );
            }
          },
          retryFunction: () =>
            Logger.notifications.info(
              { code: NotificationCode.RETRY_DOWNLOAD },
              MessageCode.DOWNLOAD_IS_SLOW
            ),
        }
      );

      if (this._currentRequestID !== requestID) {
        return;
      }

      let projectRoot = await Exp.extractTemplateApp(
        templateDownload.starterAppPath,
        templateDownload.name,
        templateDownload.root
      );
      if (!projectRoot) {
        this.setState({
          isLoading: false,
        });
        return;
      }

      await this.props.onSelectProject(projectRoot);
      this.props.onClose();
    } catch (e) {
      Exp.clearXDLCacheAsync();
      this.setState({
        isLoading: false,
        errorMessage: e.message,
      });
    }
  };
}

let Styles = {
  container: {
    fontFamily: 'Verdana',
    padding: StyleConstants.gutterLg,
    overflowWrap: 'break-word',

    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  loadingContainer: {
    padding: StyleConstants.gutterLg,
    minWidth: 250,

    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  form: {
    width: 400,
    display: 'flex',
    flexDirection: 'column',
  },
  templatesContainer: {
    display: 'flex',
    flexDirection: 'row',
    //alignItems: 'center',
    //justifyContent: 'center',
    marginTop: 5,
    marginBottom: 10,
    overflowX: 'auto',
    backgroundColor: '#f1f1f1',
    borderRadius: 3,
    paddingTop: 5,
    paddingBottom: 5,
  },
  templateContainer: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 20,
    marginRight: 20,
    marginTop: 10,
    marginBottom: 10,
    paddingLeft: 20,
    paddingRight: 20,
    paddingTop: StyleConstants.gutterMd * 2,
    paddingBottom: StyleConstants.gutterMd,
    borderRadius: 4,
    cursor: 'pointer',
  },
  largeText: {
    fontWeight: 'bold',
    color: StyleConstants.colorText,
    fontSize: StyleConstants.fontSizeLg,
    paddingBottom: StyleConstants.gutterSm,
  },
  mediumText: {
    color: StyleConstants.colorSubtitle,
    fontSize: StyleConstants.fontSizeMd,
  },
  loadingText: {
    paddingTop: StyleConstants.gutterLg,
    color: StyleConstants.colorText,
    fontSize: StyleConstants.fontSizeMd,
  },
  buttonsContainer: {
    marginTop: 10,
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  cancelButton: {
    border: 'none',
    borderRadius: 5,
    color: 'black',
    padding: StyleConstants.gutterSm,
    width: '40%',

    backgroundColor: StyleConstants.colorCancel,
  },
  submitButton: {
    border: 'none',
    borderRadius: 5,
    color: 'white',
    padding: StyleConstants.gutterSm,
    width: '40%',

    backgroundColor: StyleConstants.colorPrimary,
  },
  retryButton: {
    border: 'none',
    borderRadius: 5,
    color: 'white',
    padding: StyleConstants.gutterSm,
    marginTop: 20,
    width: '40%',
    backgroundColor: StyleConstants.colorPrimary,
  },
  input: {
    marginBottom: StyleConstants.gutterMd,
  },
  icon: {
    width: ICON_SIZE,
    height: ICON_SIZE,
    marginBottom: StyleConstants.gutterMd,
    borderStyle: 'solid',
    borderWidth: 1,
    borderColor: 'black',
  },
  progress: {
    backgroundColor: StyleConstants.colorPrimary,
    height: 30,
  },
  progressBar: {
    backgroundColor: StyleConstants.colorLightBackground,
    height: 30,
    width: 200,
  },
};

export default NewProjectModal;
<commit_msg>Api/ApiV2 now using Axios instead of Request<commit_after>import {
  Api,
  Exp,
  Logger,
  NotificationCode,
  UserSettings,
  MessageCode,
} from 'xdl';

import _ from 'lodash';
import os from 'os';
import path from 'path';
import React, { PropTypes } from 'react';
import LoadingIndicator from 'react-loading-indicator';

import Commands from './Commands';
import StyleConstants from './StyleConstants';
import SharedStyles from './Styles';
import * as IdentifierRules from 'xde/utils/IdentifierRules';

const ICON_SIZE = 120;
const MAX_PROJECT_LENGTH = 44;
const HALF_MAX_PROJECT_LENGTH = 20;

class NewProjectModal extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      isLoading: true,
      projectName: 'my-new-project',
      projectDirectory: null,
      errorMessage: null,
      loadingMessage: null,
      notificationType: null,
      showRetryPrompt: false,
      progress: null,
      templates: null,
      selectedTemplate: null,
    };
  }

  static propTypes = {
    onClose: PropTypes.func.isRequired,
    onSelectProject: PropTypes.func.isRequired,
  };

  _currentRequestID = 0;

  componentDidMount() {
    Logger.notifications.addStream({
      stream: {
        write: chunk => {
          switch (chunk.code) {
            case NotificationCode.PROGRESS:
              this.setState({
                loadingMessage: chunk.msg,
                notificationType: NotificationCode.PROGRESS,
              });
              break;
            case NotificationCode.DOWNLOAD:
              this.setState({
                loadingMessage: 'Downloading... ' + chunk.msg + '%',
                progress: chunk.msg,
                notificationType: NotificationCode.DOWNLOAD,
              });
              break;
            case NotificationCode.RETRY_DOWNLOAD:
              this.setState({
                showRetryPrompt: true,
              });
              break;
          }
        },
      },
      type: 'raw',
    });

    this._loadTemplatesAsync();
  }

  _loadTemplatesAsync = async () => {
    let versions = await Api.versionsAsync();
    let dir = await UserSettings.getAsync('defaultNewProjectDir', os.homedir());

    this.setState({
      isLoading: false,
      templates: versions.templates,
      selectedTemplate: versions.templates[0],
      projectDirectory: dir,
    });
  };

  render() {
    return (

        {this.state.isLoading ? this._renderLoading() : this._renderForm()}

    );
  }

  _selectTemplate = template => {
    this.setState({
      selectedTemplate: template,
    });
  };

  _renderTemplate = template => {
    let isSelected = this.state.selectedTemplate.id === template.id;

    return (

 this._selectTemplate(template)}>

          {template.name}

    );
  };

  _renderForm = () => {
    let { templates, selectedTemplate } = this.state;

    if (!templates) {
      return
;
    }

    return (
      <form
        name="newProject"
        style={Styles.form}
        onSubmit={this._onSubmitNewProject}>

          Choose a template for your project

            {_.map(templates, this._renderTemplate)}

          {selectedTemplate.description}

          Choose a project name and directory

        <input
          autoFocus
          type="text"
          className="form-control"
          style={Styles.input}
          ref="projectName"
          onChange={this._onProjectNameChange}
          value={this.state.projectName}
        />

          <input
            disabled
            type="text"
            className="form-control"
            style={{ color: StyleConstants.colorSubtitle }}
            ref="projectDirectory"
            value={this._getShortProjectDirectory()}
          />

            ...

        {this._renderErrors()}

          <button
            onClick={this._onClickCancel}
            type="button"
            style={Styles.cancelButton}>

Cancel

          </button>
          <button type="submit" style={Styles.submitButton}>

Create

          </button>

      </form>
    );
  };

  _renderErrors = () => {
    if (this.state.errorMessage) {
      return (

{this.state.errorMessage}

      );
    } else {
      return null;
    }
  };

  _renderLoading = () => {
    let downloading = this.state.notificationType === NotificationCode.DOWNLOAD;
    return (

        {downloading
          ?

          : <LoadingIndicator
              color={{
                red: 17,
                green: 114,
                blue: 182,
                alpha: 1.0,
              }}
              segmentWidth={6}
              segmentLength={15}
              spacing={9}
            />}
        {this.state.loadingMessage &&

            {this.state.loadingMessage}

}
        {this.state.showRetryPrompt
          ?

                {MessageCode.DOWNLOAD_IS_SLOW}

              <button
                onClick={this._onClickRetry}
                type="button"
                style={Styles.retryButton}>

Retry

              </button>

          :
}

    );
  };

  _onProjectNameChange = event => {
    let newValue = IdentifierRules.normalizeProjectNameWhileTyping(
      event.target.value
    );
    let errorMessage = null;
    if (newValue.length === 0) {
      errorMessage = 'Project name cannot be blank';
    }

    this.setState({
      projectName: newValue,
      errorMessage,
    });
  };

  _getShortProjectDirectory = () => {
    let { projectName, projectDirectory } = this.state;
    let dir = path.join(projectDirectory, projectName);
    if (dir.length < MAX_PROJECT_LENGTH) {
      return dir;
    } else {
      return `${dir.substr(0, HALF_MAX_PROJECT_LENGTH)}...${dir.substr(dir.length - HALF_MAX_PROJECT_LENGTH)}`;
    }
  };

  _onClickChangeProjectDirectoryAsync = async () => {
    try {
      let directory = await Commands.getDirectoryAsync(
        this.state.projectDirectory
      );
      if (!directory) {
        return;
      }

      await UserSettings.setAsync('defaultNewProjectDir', directory);
      this.setState({
        projectDirectory: directory,
      });
    } catch (e) {}
  };

  _onClickCancel = event => {
    event.preventDefault();
    this.props.onClose();
  };

  _onClickRetry = async event => {
    Exp.clearXDLCacheAsync();
    this.setState({ showRetryPrompt: false });
    this._onSubmitNewProject(event);
  };

  _onSubmitNewProject = async event => {
    event.preventDefault();
    const requestID = this._currentRequestID + 1;
    this._currentRequestID = requestID;

    if (this.state.projectName.length === 0) {
      this.setState({
        errorMessage: 'Project name cannot be blank',
      });
      return;
    } else {
      this.setState({
        errorMessage: null,
        loadingMessage: null,
        isLoading: true,
      });
    }

    try {
      let templateDownload = await Exp.downloadTemplateApp(
        this.state.selectedTemplate.id,
        this.state.projectDirectory,
        {
          name: this.state.projectName,
          progressFunction: progress => {
            if (this._currentRequestID === requestID) {
              Logger.notifications.info(
                { code: NotificationCode.DOWNLOAD },
                progress
              );
            }
          },
          retryFunction: () =>
            Logger.notifications.info(
              { code: NotificationCode.RETRY_DOWNLOAD },
              MessageCode.DOWNLOAD_IS_SLOW
            ),
        }
      );

      if (this._currentRequestID !== requestID) {
        return;
      }

      let projectRoot = await Exp.extractTemplateApp(
        templateDownload.starterAppPath,
        templateDownload.name,
        templateDownload.root
      );
      if (!projectRoot) {
        this.setState({
          isLoading: false,
        });
        return;
      }

      await this.props.onSelectProject(projectRoot);
      this.props.onClose();
    } catch (e) {
      Exp.clearXDLCacheAsync();
      this.setState({
        isLoading: false,
        errorMessage: e.message,
      });
    }
  };
}

let Styles = {
  container: {
    fontFamily: 'Verdana',
    padding: StyleConstants.gutterLg,
    overflowWrap: 'break-word',

    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  loadingContainer: {
    padding: StyleConstants.gutterLg,
    minWidth: 250,

    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
  },
  form: {
    width: 400,
    display: 'flex',
    flexDirection: 'column',
  },
  templatesContainer: {
    display: 'flex',
    flexDirection: 'row',
    //alignItems: 'center',
    //justifyContent: 'center',
    marginTop: 5,
    marginBottom: 10,
    overflowX: 'auto',
    backgroundColor: '#f1f1f1',
    borderRadius: 3,
    paddingTop: 5,
    paddingBottom: 5,
  },
  templateContainer: {
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
    justifyContent: 'center',
    marginLeft: 20,
    marginRight: 20,
    marginTop: 10,
    marginBottom: 10,
    paddingLeft: 20,
    paddingRight: 20,
    paddingTop: StyleConstants.gutterMd * 2,
    paddingBottom: StyleConstants.gutterMd,
    borderRadius: 4,
    cursor: 'pointer',
  },
  largeText: {
    fontWeight: 'bold',
    color: StyleConstants.colorText,
    fontSize: StyleConstants.fontSizeLg,
    paddingBottom: StyleConstants.gutterSm,
  },
  mediumText: {
    color: StyleConstants.colorSubtitle,
    fontSize: StyleConstants.fontSizeMd,
  },
  loadingText: {
    paddingTop: StyleConstants.gutterLg,
    color: StyleConstants.colorText,
    fontSize: StyleConstants.fontSizeMd,
  },
  buttonsContainer: {
    marginTop: 10,
    width: '100%',
    display: 'flex',
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  cancelButton: {
    border: 'none',
    borderRadius: 5,
    color: 'black',
    padding: StyleConstants.gutterSm,
    width: '40%',

    backgroundColor: StyleConstants.colorCancel,
  },
  submitButton: {
    border: 'none',
    borderRadius: 5,
    color: 'white',
    padding: StyleConstants.gutterSm,
    width: '40%',

    backgroundColor: StyleConstants.colorPrimary,
  },
  retryButton: {
    border: 'none',
    borderRadius: 5,
    color: 'white',
    padding: StyleConstants.gutterSm,
    marginTop: 20,
    width: '40%',
    backgroundColor: StyleConstants.colorPrimary,
  },
  input: {
    marginBottom: StyleConstants.gutterMd,
  },
  icon: {
    width: ICON_SIZE,
    height: ICON_SIZE,
    marginBottom: StyleConstants.gutterMd,
    borderStyle: 'solid',
    borderWidth: 1,
    borderColor: 'black',
  },
  progress: {
    backgroundColor: StyleConstants.colorPrimary,
    height: 30,
  },
  progressBar: {
    backgroundColor: StyleConstants.colorLightBackground,
    height: 30,
    width: 200,
  },
};

export default NewProjectModal;
<|endoftext|>


<commit_before>import {nest} from 'd3'
import {dimensionLabelTitle} from './labels'

export function queryResultNester(result, nesting = []) {
  const _nesting = nesting ? nesting : []
  const nester = nest()

  _nesting.forEach(level => {
    const variables = level.variables
    const name = level.name
    const keyNest = nester.key(item => item[level.name])
    if (variables.length > 0) {
      keyNest.sortKeys((a, b) => variables.indexOf(b) - variables.indexOf(a))
    }
  })

  // Calculate a total for all leaf nodes, to use in maxVal for scaling
  // Also divide the number by 100 if the unit is percent
  let maxVal = 0
  nester.rollup(leaves => {
    leaves.total = 0
    leaves.forEach(leaf => {
      leaves.total += leaf.value
    })
    if (leaves.total > maxVal) {
      maxVal = leaves.total
    }
    return leaves
  })

  const res = nester.entries(result)
  res.maxValue = maxVal
  return res
}

export function nestedQueryResultLabelizer(result, nesting = []) {

  const labelIt = (node, depth = 0) => {
    node.title = dimensionLabelTitle(nesting[depth], node.key)
    if (node.values instanceof Array) {
      node.values.forEach(item => labelIt(item, depth + 1))
    }
  }

  result.forEach(node => labelIt(node))

  return result
}
<commit_msg>Fix lint error<commit_after>import {nest} from 'd3'
import {dimensionLabelTitle} from './labels'

export function queryResultNester(result, nesting = []) {
  const _nesting = nesting ? nesting : []
  const nester = nest()

  _nesting.forEach(level => {
    const variables = level.variables
    const name = level.name
    const keyNest = nester.key(item => item[name])
    if (variables.length > 0) {
      keyNest.sortKeys((a, b) => variables.indexOf(b) - variables.indexOf(a))
    }
  })

  // Calculate a total for all leaf nodes, to use in maxVal for scaling
  // Also divide the number by 100 if the unit is percent
  let maxVal = 0
  nester.rollup(leaves => {
    leaves.total = 0
    leaves.forEach(leaf => {
      leaves.total += leaf.value
    })
    if (leaves.total > maxVal) {
      maxVal = leaves.total
    }
    return leaves
  })

  const res = nester.entries(result)
  res.maxValue = maxVal
  return res
}

export function nestedQueryResultLabelizer(result, nesting = []) {

  const labelIt = (node, depth = 0) => {
    node.title = dimensionLabelTitle(nesting[depth], node.key)
    if (node.values instanceof Array) {
      node.values.forEach(item => labelIt(item, depth + 1))
    }
  }

  result.forEach(node => labelIt(node))

  return result
}
<|endoftext|>


<commit_before>/**
* Validator is an engine for HTML forms elements.
* @name Validator
* @class Validator
* @augments ch.Widget
* @requires ch.Condition
* @memberOf ch
* @param {Object} conf Object with configuration properties.
* @param {Object} conf.conditions Object with conditions.
* @returns itself
* @factorized
* @see ch.Widget
* @see ch.Condition
*/
(function (window, $, ch) {
	'use strict';

	if (window.ch === undefined) {
		throw new window.Error('Expected ch namespace defined.');
	}

	var setTimeout = window.setTimeout;

	function Validator($el, conf) {
		/**
		* Reference to a internal component instance, saves all the information and configuration properties.
		* @protected
		* @name ch.Validator#that
		* @type Object
		*/
		var that = this;

		that.$element = $el;
		that.element = $el[0];
		that.type = 'validator';
		var conf = conf || {};

		conf = ch.util.clone(conf);
		that.conf = conf;

	/**
	* Inheritance
	*/
		that = ch.Widget.call(that);
		that.parent = ch.util.clone(that);

	/**
	* Private Members
	*/
		var conditions = (function(){
			var c = {}; // temp collection

			var condition = ch.Condition.call(that["public"], conf.condition);

			c[condition.name] = condition;

			// return all the configured conditions
			return c;
		})(); // Love this ;)

		var validate = function(value) {

			if (!that.enabled) { return true; }

			var condition, tested, empty, val, message, required = conditions["required"];

			// Avoid fields that aren't required when they are empty or de-activated
			if (!required && value === "" && that.active === false) { return {"status": true}; }

			if (that.enabled && (!that.active || value !== "" || required)) {
				/**
				* Triggers before start validation process.
				* @name ch.Validator#beforeValidate
				* @event
				* @public
				* @exampleDescription
				* @example
				* widget.on("beforeValidate",function(){
				*	submitButton.disable();
				* });
				*/
				// old callback system
				that.callbacks('beforeValidate');
				// new callback
				that.trigger("beforeValidate");

				// for each condition
				for (condition in conditions){

					val = ((condition === "required") ? that.element : value.toLowerCase());
					// this is the validation
					tested = test.call(this, condition, val);

					// return false if any test fails,
					if (!tested) {

						/**
						* Triggers when an error occurs on the validation process.
						* @name ch.Validator#error
						* @event
						* @public
						* @exampleDescription
						* @example
						* widget.on("error",function(event, condition){
						*	errorModal.show();
						* });
						*/
						// old callback system
						that.callbacks('onError', condition);
						// new callback
						that.trigger("error", condition);

						that.active = true;

						// stops the proccess
						//return false;
						return {
							"status": false,
							"condition": condition,
							"msg": conditions[condition].message
						}
					};
				}
			}

			// Status OK (with previous error)
			if (that.active || !that.enabled) {
				// Public status OK
				that.active = false;
			}

			/**
			* Triggers when the validation process ends.
			* @name ch.Validator#afterValidate
			* @event
			* @public
			* @exampleDescription
			* @example
			* widget.on("afterValidate",function(){
			*	submitButton.disable();
			* });
			*/
			// old callback system
			that.callbacks('afterValidate');
			// new callback
			that.trigger("afterValidate");

			// It's all good ;)
			//return true;
			return {
				"status": true
			}
		}

		/**
		* Test a condition looking for error.
		* @private
		* @name ch.Validator#test
		* @see ch.Condition
		*/
		var test = function(condition, value){

			if (value === "" && condition !== "required") { return true };

			var isOk = false,
				// this is the validation
				validation = this || window,
				condition = conditions[condition];

			isOk = condition.test.call(validation, value);

			return isOk;

		};

	/**
	* Protected Members
	*/

		/**
		* Flag that let you know if there's a validation going on.
		* @protected
		* @name ch.Validator#active
		* @type boolean
		*/
		that.active = false;

		/**
		* Flag that let you know if the all conditions are enabled or not.
		* @protected
		* @name ch.Validator#enabled
		* @type boolean
		*/
		that.enabled = true;

	/**
	*	Public Members
	*/

		/**
		* @borrows ch.Widget#uid as ch.TabNavigator#uid
		*/

		/**
		* This public property defines the component type. All instances are saved into a 'map', grouped by its type. You can reach for any or all of the components from a specific type with 'ch.instances'.
		* @public
		* @name ch.Validator#type
		* @type String
		*/
		that["public"].type = "validator";

		/**
		* This public Map saves all the validation configurations from this instance.
		* @public
		* @name ch.Validator#conditions
		* @type object
		*/
		that["public"].conditions = conditions;

		/**
		* Active is a boolean property that let you know if there's a validation going on.
		* @public
		* @function
		* @name ch.Validator#isActive
		* @returns itself
		*/
		that["public"].isActive = function() {
			return that.active;
		};

		/**
		* Let you keep chaining methods.
		* @public
		* @function
		* @name ch.Validator#and
		* @returns Object
		*/
		that["public"].and = function(){
			return that.$element;
		};

		/**
		* Merge its conditions with a new conditions of another instance with the same trigger.
		* @public
		* @function
		* @name ch.Validator#extend
		* @returns itself
		*/
		that["public"].extend = function(condition){

			conditions[condition.name] = ch.Condition.call(that["public"], condition);

			return that["public"];
		};

		/**
		* Clear all active validations.
		* @public
		* @function
		* @name ch.Validator#clear
		* @returns itself
		*/
		that["public"].clear = function() {
			that.active = false;

			return that["public"];
		};

		/**
		* Runs all configured conditions and returns an object with a status value, condition name and a message.
		* @public
		* @function
		* @name ch.Validator#validate
		* @returns Status Object
		*/
		that["public"].validate = function(value){
			// this is the validation
			return validate.call(this, value);
		}

		/**
		* Turn on Validator engine or an specific condition.
		* @public
		* @name enable
		* @name ch.Validator#enable
		* @returns itself
		*/
		that["public"].enable = function(condition){
			if (condition && conditions[condition]){
				// Enable specific condition
				conditions[condition].enable();
			} else {
				// enable all
				that.enabled = true;
				for (condition in conditions){
					conditions[condition].enable();
				}
			}
			return that["public"];
		}

		/**
		* Turn on Validator engine or an specific condition.
		* @public
		* @name disable
		* @name ch.Validator#disable
		* @returns itself
		*/
		that["public"].disable = function(condition){
			if (condition && conditions[condition]){
				// disable specific condition
				conditions[condition].disable();
			} else {
				// disable all
				that.enabled = false;
				for (condition in conditions){
					conditions[condition].disable();
				}
			}
			return that["public"];
		}

	/**
	*	Default event delegation
	*/
		/**
		* Triggers when the component is ready to use.
		* @name ch.Validator#ready
		* @event
		* @public
		* @exampleDescription Following the first example, using widget as modal's instance controller:
		* @example
		* widget.on("ready",function(){
		*	this.show();
		* });
		*/
		that.trigger("ready");

		return that;
	}

	Validator.prototype.name = 'validator';
	Validator.prototype.constructor = Validator;

	ch.factory(Validator);

}(this, this.jQuery, this.ch));<commit_msg>GH-901 deleted.<commit_after><|endoftext|>


<commit_before>import Emitter from 'component-emitter';
import errors from './errors';
import { Pricing } from './pricing';
import { normalize } from '../util/normalize';
import decimalize from '../util/decimalize';
import { FIELDS } from './token';

const debug = require('debug')('recurly:apple-pay');

const APPLE_PAY_API_VERSION = 2;
const APPLE_PAY_ADDRESS_MAP = {
  first_name: 'givenName',
  last_name: 'familyName',
  address1: 'addressLines',
  address2: 'addressLines',
  city: 'locality',
  state: 'administrativeArea',
  postal_code: 'postalCode',
  country: 'countryCode'
};

const I18N = {
  authorizationLineItemLabel: 'Card Authorization (Temporary)',
  subtotalLineItemLabel: 'Subtotal',
  discountLineItemLabel: 'Discount',
  taxLineItemLabel: 'Tax',
  giftCardLineItemLabel: 'Gift card'
};

/**
 * Instantiation factory
 *
 * @param  {Object} options
 * @return {ApplePay}
 */
export function factory (options) {
  return new ApplePay(Object.assign({}, options, { recurly: this }));
}

/**
 * Initializes an Apple Pay session.
 *
 * @param {Object} options
 * @param {Recurly} options.recurly
 * @param {String} options.country
 * @param {String} options.currency
 * @param {String} options.label label to display to customers in the Apple Pay dialogue
 * @param {String} options.total transaction total in format '1.00'
 * @param {HTMLElement} [options.form] to provide additional customer data
 * @param {Pricing} [options.pricing] to provide transaction total from Pricing
 * @constructor
 * @public
 */

class ApplePay extends Emitter {
  constructor (options) {
    super();

    this._ready = false;
    this.config = {
      i18n: I18N
    };
    this.once('ready', () => this._ready = true);

    // Detect whether Apple Pay is available
    if (!window.ApplePaySession) {
      this.initError = this.error('apple-pay-not-supported');
    } else if (!window.ApplePaySession.canMakePayments()) {
      this.initError = this.error('apple-pay-not-available');
    }

    if (!this.initError) {
      this.configure(options);
    }
  }

  /**
   * @return {ApplePaySession} Bound Apple Pay session
   * @private
   */
  get session () {
    if (this._session) return this._session;

    debug('Creating new Apple Pay session');

    this.addAuthorizationLineItem();

    let session = new window.ApplePaySession(APPLE_PAY_API_VERSION, {
      countryCode: this.config.country,
      currencyCode: this.config.currency,
      supportedNetworks: this.config.supportedNetworks,
      merchantCapabilities: this.config.merchantCapabilities,
      requiredBillingContactFields: ['postalAddress'],
      requiredShippingContactFields: this.config.requiredShippingContactFields,
      total: this.totalLineItem
    });

    session.onvalidatemerchant = this.onValidateMerchant.bind(this);
    session.onshippingcontactselected = this.onShippingContactSelected.bind(this);
    session.onshippingmethodselected = this.onShippingMethodSelected.bind(this);
    session.onpaymentmethodselected = this.onPaymentMethodSelected.bind(this);
    session.onpaymentauthorized = this.onPaymentAuthorized.bind(this);
    session.oncancel = this.onCancel.bind(this);

    return this._session = session;
  }

  /**
   * @return {Array} subtotal line items for display on payment sheet
   * @private
   */
  get lineItems () {
    // Clone configured line items
    return [].concat(this.config.lineItems);
  }

  /**
   * @return {Object} total cost line item
   * @private
   */
  get totalLineItem () {
    return lineItem(this.config.label, this.config.total);
  }

  /**
   * @return {Object} total cost line item indicating a finalized line item
   * @private
   */
  get finalTotalLineItem () {
    return Object.assign({}, this.totalLineItem, { type: 'final' });
  }

  /**
   * Used when the total price is zero, to circumvent Apple Pay
   * zero amount authorization limitation
   * @return {Object} card authorization line item, $1.00
   * @private
   */
  get authorizationLineItem () {
    return lineItem(this.config.i18n.authorizationLineItemLabel, 1.00);
  }

  /**
   * Initialized state callback registry
   *
   * @param  {Function} cb callback
   * @public
   */
  ready (cb) {
    if (this._ready) cb();
    else this.once('ready', cb);
  }

  /**
   * Configures a new instance
   *
   * @param  {Object} options
   * @emit 'ready'
   * @private
   */
  configure (options) {
    if ('label' in options) this.config.label = options.label;
    else return this.initError = this.error('apple-pay-config-missing', { opt: 'label' });

    if ('form' in options) this.config.form = options.form;

    // Initialize with no line items
    this.config.lineItems = [];

    if ('recurly' in options) this.recurly = options.recurly;
    else return this.initError = this.error('apple-pay-factory-only');

    if ('i18n' in options) Object.assign(this.config.i18n, options.i18n);

    // Listen for pricing changes to update totals and currency
    const { pricing } = options;
    if (pricing instanceof Pricing) {
      this.config.pricing = pricing;
      pricing.on('change', () => this.onPricingChange());
      if (pricing.hasPrice) this.onPricingChange();
    } else if ('total' in options) {
      this.config.total = options.total;
    } else {
      return this.initError = this.error('apple-pay-config-missing', { opt: 'total' });
    }

    this.recurly.request.get({
      route: '/apple_pay/info',
      done: this.applyRemoteConfig(options)
    });
  }

  /**
   * Assigns ApplePay configuration from site config
   * @param  {object} options
   * @private
   */
  applyRemoteConfig (options) {
    return (err, info) => {
      if (err) return this.initError = this.error(err);

      if ('countries' in info && ~info.countries.indexOf(options.country)) this.config.country = options.country;
      else return this.initError = this.error('apple-pay-config-invalid', { opt: 'country', set: info.countries });

      if ('currencies' in info && ~info.currencies.indexOf(options.currency)) this.config.currency = options.currency;
      else return this.initError = this.error('apple-pay-config-invalid', { opt: 'currency', set: info.currencies });

      this.config.merchantCapabilities = info.merchantCapabilities || [];
      this.config.supportedNetworks = info.supportedNetworks || [];
      this.config.requiredShippingContactFields = options.requiredShippingContactFields || [];

      this.emit('ready');
    };
  }

  /**
   * Begins Apple Pay transaction
   *
   * @public
   */
  begin () {
    if (this.initError) return this.error('apple-pay-init-error', { err: this.initError });
    delete this._session;
    this.session.begin();
  }

  /**
   * Conditionally adds an authorization line item in order to circumvent Apple Pay
   * zero-amount authorization limitation
   *
   * @private
   */
  addAuthorizationLineItem () {
    if (parseFloat(this.config.total) > 0) return;
    this.config.lineItems.push(this.authorizationLineItem);
    this.config.total = this.authorizationLineItem.amount;
  }

  /**
   * Updates line items and total price on pricing module changes
   *
   * @param  {Object} price Pricing.price
   * @private
   */
  onPricingChange () {
    const { pricing } = this.config;

    let lineItems = this.config.lineItems = [];
    this.config.total = pricing.totalNow;

    if (!pricing.hasPrice) return;
    let taxAmount = pricing.price.now.taxes || pricing.price.now.tax;

    lineItems.push(lineItem(this.config.i18n.subtotalLineItemLabel, pricing.subtotalPreDiscountNow));

    if (+pricing.price.now.discount) {
      lineItems.push(lineItem(this.config.i18n.discountLineItemLabel, -pricing.price.now.discount));
    }

    if (+taxAmount) {
      lineItems.push(lineItem(this.config.i18n.taxLineItemLabel, taxAmount));
    }

    if (+pricing.price.now.giftCard) {
      lineItems.push(lineItem(this.config.i18n.giftCardLineItemLabel, -pricing.price.now.giftCard));
    }

    this.config.lineItems = lineItems;
  }

  /**
   * Validates the merchant session
   *
   * @param  {Event} event ApplePayValidateMerchantEvent
   * @return {[type]} [description]
   * @private
   */
  onValidateMerchant (event) {
    debug('Validating Apple Pay merchant session', event);

    const validationURL = event.validationURL;

    this.recurly.request.post({
      route: '/apple_pay/start',
      data: { validationURL },
      done: (err, merchantSession) => {
        if (err) return this.error(err);
        this.session.completeMerchantValidation(merchantSession);
      }
    });
  }

  /**
   * Handles payment method selection
   *
   * @param  {Event} event
   * @private
   */
  onPaymentMethodSelected (event) {
    debug('Payment method selected', event);
    this.session.completePaymentMethodSelection(this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles shipping contact selection. Not utilized
   *
   * @param  {Event} event
   * @private
   */
  onShippingContactSelected (event) {
    const status = this.session.STATUS_SUCCESS;
    const newShippingMethods = [];

    this.emit('shippingContactSelected', event);

    this.session.completeShippingContactSelection(status, newShippingMethods, this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles shipping method selection. Not utilized
   *
   * @param  {Event} event
   * @private
   */
  onShippingMethodSelected (event) {
    const status = this.session.STATUS_SUCCESS;

    this.emit('shippingMethodSelected', event);

    this.session.completeShippingMethodSelection(status, this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles payment authorization. Submits payment token and
   * emits the resultant authorization token
   *
   * @param  {Event} event
   * @emit 'token'
   * @private
   */
  onPaymentAuthorized (event) {
    debug('Payment authorization received', event);

    let data = {};

    if (this.config.form) {
      data = normalize(this.config.form, FIELDS, { parseCard: false }).values;
    }

    this.emit('paymentAuthorized', event);

    this.mapPaymentData(data, event.payment);

    this.recurly.request.post({
      route: '/apple_pay/token',
      data,
      done: (err, token) => {
        if (err) {
          this.session.completePayment(this.session.STATUS_FAILURE);
          return this.error('apple-pay-payment-failure', err);
        }

        debug('Token received', token);

        this.session.completePayment(this.session.STATUS_SUCCESS);
        this.emit('authorized', event);
        this.emit('token', token);
      }
    });
  }

  /**
   * Handles customer cancelation. Passes on the event
   *
   * @param  {Event} event
   * @emit 'cancel'
   * @private
   */
  onCancel (event) {
    debug('User canceled Apple Pay payment', event);

    this.emit('cancel', event);
  }

  /**
   * Creates and emits a RecurlyError
   *
   * @param  {...Mixed} params to be passed to the Recurlyerror factory
   * @return {RecurlyError}
   * @emit 'error'
   * @private
   */
  error (...params) {
    let err = params[0] instanceof Error ? params[0] : errors(...params);
    this.emit('error', err);
    return err;
  }

  /**
   * Maps data from the Apple Pay token into the inputs
   * object that is sent to RA for tokenization
   *
   * @private
   */
  mapPaymentData (inputs, data) {
    inputs.paymentData = data.token.paymentData;
    inputs.paymentMethod = data.token.paymentMethod;

    if (!data.billingContact) return;
    if (FIELDS.some(field => inputs[field])) return;

    FIELDS.forEach(field => {
      if (!APPLE_PAY_ADDRESS_MAP[field]) return;

      let tokenData = data.billingContact[APPLE_PAY_ADDRESS_MAP[field]];

      // address lines are an array from Apple Pay
      if (field === 'address1') tokenData = tokenData[0];
      else if (field === 'address2') tokenData = tokenData[1];

      inputs[field] = tokenData;
    });
  }
}

/**
 * Builds an ApplePayLineItem
 * @param  {String} label
 * @param  {Number} amount
 * @return {object}
 */
function lineItem (label = '', amount = 0) {
  return { label, amount: decimalize(amount) };
}
<commit_msg>Sends requested currency in `GET /apple_pay/info` requests<commit_after>import Emitter from 'component-emitter';
import errors from './errors';
import { Pricing } from './pricing';
import { normalize } from '../util/normalize';
import decimalize from '../util/decimalize';
import { FIELDS } from './token';

const debug = require('debug')('recurly:apple-pay');

const APPLE_PAY_API_VERSION = 2;
const APPLE_PAY_ADDRESS_MAP = {
  first_name: 'givenName',
  last_name: 'familyName',
  address1: 'addressLines',
  address2: 'addressLines',
  city: 'locality',
  state: 'administrativeArea',
  postal_code: 'postalCode',
  country: 'countryCode'
};

const I18N = {
  authorizationLineItemLabel: 'Card Authorization (Temporary)',
  subtotalLineItemLabel: 'Subtotal',
  discountLineItemLabel: 'Discount',
  taxLineItemLabel: 'Tax',
  giftCardLineItemLabel: 'Gift card'
};

/**
 * Instantiation factory
 *
 * @param  {Object} options
 * @return {ApplePay}
 */
export function factory (options) {
  return new ApplePay(Object.assign({}, options, { recurly: this }));
}

/**
 * Initializes an Apple Pay session.
 *
 * @param {Object} options
 * @param {Recurly} options.recurly
 * @param {String} options.country
 * @param {String} options.currency
 * @param {String} options.label label to display to customers in the Apple Pay dialogue
 * @param {String} options.total transaction total in format '1.00'
 * @param {HTMLElement} [options.form] to provide additional customer data
 * @param {Pricing} [options.pricing] to provide transaction total from Pricing
 * @constructor
 * @public
 */

class ApplePay extends Emitter {
  constructor (options) {
    super();

    this._ready = false;
    this.config = {
      i18n: I18N
    };
    this.once('ready', () => this._ready = true);

    // Detect whether Apple Pay is available
    if (!window.ApplePaySession) {
      this.initError = this.error('apple-pay-not-supported');
    } else if (!window.ApplePaySession.canMakePayments()) {
      this.initError = this.error('apple-pay-not-available');
    }

    if (!this.initError) {
      this.configure(options);
    }
  }

  /**
   * @return {ApplePaySession} Bound Apple Pay session
   * @private
   */
  get session () {
    if (this._session) return this._session;

    debug('Creating new Apple Pay session');

    this.addAuthorizationLineItem();

    let session = new window.ApplePaySession(APPLE_PAY_API_VERSION, {
      countryCode: this.config.country,
      currencyCode: this.config.currency,
      supportedNetworks: this.config.supportedNetworks,
      merchantCapabilities: this.config.merchantCapabilities,
      requiredBillingContactFields: ['postalAddress'],
      requiredShippingContactFields: this.config.requiredShippingContactFields,
      total: this.totalLineItem
    });

    session.onvalidatemerchant = this.onValidateMerchant.bind(this);
    session.onshippingcontactselected = this.onShippingContactSelected.bind(this);
    session.onshippingmethodselected = this.onShippingMethodSelected.bind(this);
    session.onpaymentmethodselected = this.onPaymentMethodSelected.bind(this);
    session.onpaymentauthorized = this.onPaymentAuthorized.bind(this);
    session.oncancel = this.onCancel.bind(this);

    return this._session = session;
  }

  /**
   * @return {Array} subtotal line items for display on payment sheet
   * @private
   */
  get lineItems () {
    // Clone configured line items
    return [].concat(this.config.lineItems);
  }

  /**
   * @return {Object} total cost line item
   * @private
   */
  get totalLineItem () {
    return lineItem(this.config.label, this.config.total);
  }

  /**
   * @return {Object} total cost line item indicating a finalized line item
   * @private
   */
  get finalTotalLineItem () {
    return Object.assign({}, this.totalLineItem, { type: 'final' });
  }

  /**
   * Used when the total price is zero, to circumvent Apple Pay
   * zero amount authorization limitation
   * @return {Object} card authorization line item, $1.00
   * @private
   */
  get authorizationLineItem () {
    return lineItem(this.config.i18n.authorizationLineItemLabel, 1.00);
  }

  /**
   * Initialized state callback registry
   *
   * @param  {Function} cb callback
   * @public
   */
  ready (cb) {
    if (this._ready) cb();
    else this.once('ready', cb);
  }

  /**
   * Configures a new instance
   *
   * @param  {Object} options
   * @emit 'ready'
   * @private
   */
  configure (options) {
    if ('label' in options) this.config.label = options.label;
    else return this.initError = this.error('apple-pay-config-missing', { opt: 'label' });

    if ('form' in options) this.config.form = options.form;

    // Initialize with no line items
    this.config.lineItems = [];

    if ('recurly' in options) this.recurly = options.recurly;
    else return this.initError = this.error('apple-pay-factory-only');

    if ('i18n' in options) Object.assign(this.config.i18n, options.i18n);

    // Listen for pricing changes to update totals and currency
    const { pricing } = options;
    if (pricing instanceof Pricing) {
      this.config.pricing = pricing;
      pricing.on('change', () => this.onPricingChange());
      if (pricing.hasPrice) this.onPricingChange();
    } else if ('total' in options) {
      this.config.total = options.total;
    } else {
      return this.initError = this.error('apple-pay-config-missing', { opt: 'total' });
    }

    this.recurly.request.get({
      route: '/apple_pay/info',
      data: { currency: options.currency },
      done: this.applyRemoteConfig(options)
    });
  }

  /**
   * Assigns ApplePay configuration from site config
   * @param  {object} options
   * @private
   */
  applyRemoteConfig (options) {
    return (err, info) => {
      if (err) return this.initError = this.error(err);

      if ('countries' in info && ~info.countries.indexOf(options.country)) this.config.country = options.country;
      else return this.initError = this.error('apple-pay-config-invalid', { opt: 'country', set: info.countries });

      if ('currencies' in info && ~info.currencies.indexOf(options.currency)) this.config.currency = options.currency;
      else return this.initError = this.error('apple-pay-config-invalid', { opt: 'currency', set: info.currencies });

      this.config.merchantCapabilities = info.merchantCapabilities || [];
      this.config.supportedNetworks = info.supportedNetworks || [];
      this.config.requiredShippingContactFields = options.requiredShippingContactFields || [];

      this.emit('ready');
    };
  }

  /**
   * Begins Apple Pay transaction
   *
   * @public
   */
  begin () {
    if (this.initError) return this.error('apple-pay-init-error', { err: this.initError });
    delete this._session;
    this.session.begin();
  }

  /**
   * Conditionally adds an authorization line item in order to circumvent Apple Pay
   * zero-amount authorization limitation
   *
   * @private
   */
  addAuthorizationLineItem () {
    if (parseFloat(this.config.total) > 0) return;
    this.config.lineItems.push(this.authorizationLineItem);
    this.config.total = this.authorizationLineItem.amount;
  }

  /**
   * Updates line items and total price on pricing module changes
   *
   * @param  {Object} price Pricing.price
   * @private
   */
  onPricingChange () {
    const { pricing } = this.config;

    let lineItems = this.config.lineItems = [];
    this.config.total = pricing.totalNow;

    if (!pricing.hasPrice) return;
    let taxAmount = pricing.price.now.taxes || pricing.price.now.tax;

    lineItems.push(lineItem(this.config.i18n.subtotalLineItemLabel, pricing.subtotalPreDiscountNow));

    if (+pricing.price.now.discount) {
      lineItems.push(lineItem(this.config.i18n.discountLineItemLabel, -pricing.price.now.discount));
    }

    if (+taxAmount) {
      lineItems.push(lineItem(this.config.i18n.taxLineItemLabel, taxAmount));
    }

    if (+pricing.price.now.giftCard) {
      lineItems.push(lineItem(this.config.i18n.giftCardLineItemLabel, -pricing.price.now.giftCard));
    }

    this.config.lineItems = lineItems;
  }

  /**
   * Validates the merchant session
   *
   * @param  {Event} event ApplePayValidateMerchantEvent
   * @return {[type]} [description]
   * @private
   */
  onValidateMerchant (event) {
    debug('Validating Apple Pay merchant session', event);

    const validationURL = event.validationURL;

    this.recurly.request.post({
      route: '/apple_pay/start',
      data: { validationURL },
      done: (err, merchantSession) => {
        if (err) return this.error(err);
        this.session.completeMerchantValidation(merchantSession);
      }
    });
  }

  /**
   * Handles payment method selection
   *
   * @param  {Event} event
   * @private
   */
  onPaymentMethodSelected (event) {
    debug('Payment method selected', event);
    this.session.completePaymentMethodSelection(this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles shipping contact selection. Not utilized
   *
   * @param  {Event} event
   * @private
   */
  onShippingContactSelected (event) {
    const status = this.session.STATUS_SUCCESS;
    const newShippingMethods = [];

    this.emit('shippingContactSelected', event);

    this.session.completeShippingContactSelection(status, newShippingMethods, this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles shipping method selection. Not utilized
   *
   * @param  {Event} event
   * @private
   */
  onShippingMethodSelected (event) {
    const status = this.session.STATUS_SUCCESS;

    this.emit('shippingMethodSelected', event);

    this.session.completeShippingMethodSelection(status, this.finalTotalLineItem, this.lineItems);
  }

  /**
   * Handles payment authorization. Submits payment token and
   * emits the resultant authorization token
   *
   * @param  {Event} event
   * @emit 'token'
   * @private
   */
  onPaymentAuthorized (event) {
    debug('Payment authorization received', event);

    let data = {};

    if (this.config.form) {
      data = normalize(this.config.form, FIELDS, { parseCard: false }).values;
    }

    this.emit('paymentAuthorized', event);

    this.mapPaymentData(data, event.payment);

    this.recurly.request.post({
      route: '/apple_pay/token',
      data,
      done: (err, token) => {
        if (err) {
          this.session.completePayment(this.session.STATUS_FAILURE);
          return this.error('apple-pay-payment-failure', err);
        }

        debug('Token received', token);

        this.session.completePayment(this.session.STATUS_SUCCESS);
        this.emit('authorized', event);
        this.emit('token', token);
      }
    });
  }

  /**
   * Handles customer cancelation. Passes on the event
   *
   * @param  {Event} event
   * @emit 'cancel'
   * @private
   */
  onCancel (event) {
    debug('User canceled Apple Pay payment', event);

    this.emit('cancel', event);
  }

  /**
   * Creates and emits a RecurlyError
   *
   * @param  {...Mixed} params to be passed to the Recurlyerror factory
   * @return {RecurlyError}
   * @emit 'error'
   * @private
   */
  error (...params) {
    let err = params[0] instanceof Error ? params[0] : errors(...params);
    this.emit('error', err);
    return err;
  }

  /**
   * Maps data from the Apple Pay token into the inputs
   * object that is sent to RA for tokenization
   *
   * @private
   */
  mapPaymentData (inputs, data) {
    inputs.paymentData = data.token.paymentData;
    inputs.paymentMethod = data.token.paymentMethod;

    if (!data.billingContact) return;
    if (FIELDS.some(field => inputs[field])) return;

    FIELDS.forEach(field => {
      if (!APPLE_PAY_ADDRESS_MAP[field]) return;

      let tokenData = data.billingContact[APPLE_PAY_ADDRESS_MAP[field]];

      // address lines are an array from Apple Pay
      if (field === 'address1') tokenData = tokenData[0];
      else if (field === 'address2') tokenData = tokenData[1];

      inputs[field] = tokenData;
    });
  }
}

/**
 * Builds an ApplePayLineItem
 * @param  {String} label
 * @param  {Number} amount
 * @return {object}
 */
function lineItem (label = '', amount = 0) {
  return { label, amount: decimalize(amount) };
}
<|endoftext|>


<commit_before>'use strict';

const Util = require('util');

const Diff = require('diff');

const internals = {
    width: 50
};

internals.stringify = (obj, indent) => {

    return Util.inspect(obj, { compact: !indent, depth: Infinity, maxArrayLength: Infinity, breakLength: 80, sorted: true });
};

exports = module.exports = internals.Reporter = function (options) {

    this.settings = options;
    this.count = 0;
    this.last = [];

    this.colors = internals.colors(options.colors);
};

internals.Reporter.prototype.start = function (notebook) {

};

internals.Reporter.prototype.test = function (test) {

    if (this.settings.progress === 0) {
        return;
    }

    if (this.settings['silent-skips'] && (test.skipped || test.todo)) {
        return;
    }

    if (this.settings.progress === 1) {

        // ..x....x.-..

        if (!this.count) {
            this.report('\n  ');
        }

        this.count++;

        if ((this.count - 1) % internals.width === 0) {
            this.report('\n  ');
        }

        this.report(test.err ? this.colors.red('x') : (test.skipped || test.todo ? this.colors.magenta('-') : '.'));
    }
    else {

        // Can't be fully covereed but it's ok
        // $lab:coverage:off$
        const check = process.platform === 'win32' ? '\u221A' : '\u2714';
        const asterisk = process.platform === 'win32' ? '\u00D7' : '\u2716';
        // $lab:coverage:on$

        // Verbose (Spec reporter)

        for (let i = 0; i < test.path.length; ++i) {
            if (test.path[i] !== this.last[i] || (i > 0 && test.path[i - 1] !== this.last[i - 1])) {
                this.report(internals.spacer(i * 2) + test.path[i] + '\n');
            }
        }

        this.last = test.path;

        const spacer = internals.spacer(test.path.length * 2);
        if (test.err) {
            this.report(spacer + this.colors.red(asterisk + ' ' + test.id + ') ' + test.relativeTitle) + '\n');
        }
        else {
            const symbol = test.skipped || test.todo ? this.colors.magenta('-') : this.colors.green(check);
            const assertion = test.assertions === undefined ? '' : ' and ' + test.assertions + ' assertions';
            this.report(spacer + symbol + ' ' + this.colors.gray(test.id + ') ' + test.relativeTitle +
                ' (' + test.duration + ' ms' + assertion + ')') + '\n');
        }
    }
};

internals.spacer = function (length) {

    return new Array(length + 1).join(' ');
};

internals.Reporter.prototype.end = function (notebook) {

    if (this.settings.progress) {
        this.report('\n\n');
    }

    // Colors

    const red = this.colors.red;
    const green = this.colors.green;
    const gray = this.colors.gray;
    const yellow = this.colors.yellow;
    const whiteRedBg = this.colors.whiteRedBg;
    const blackGreenBg = this.colors.blackGreenBg;
    const magenta = this.colors.magenta;

    // Tests

    const notes = notebook.tests.filter(internals.filterNotes);

    const failures = notebook.tests.filter(internals.filterFailures);

    const skipped = notebook.tests.filter(internals.filterSkipped);

    let output = '';
    const totalTests = notebook.tests.length - skipped.length;

    const errors = notebook.errors || [];
    if (errors.length) {
        output += 'Test script errors:\n\n';
        errors.forEach((err = {}) => {

            output += red(err.message) + '\n';
            if (err.stack) {
                const stack = err.stack.slice(err.stack.indexOf('\n') + 1)
                    .replace(/^/gm, '  ')
                    .split('\n')
                    .filter(internals.filterNodeModules)               // Remove node_modules files
                    .slice(0, 5)                                       // Show only first 5 stack lines
                    .join('\n');

                output += gray(stack) + '\n';
            }

            output += '\n';
        });
        output += red('There were ' + errors.length + ' test script error(s).') + '\n\n';
    }

    if (failures.length) {
        output += 'Failed tests:\n\n';

        for (let i = 0; i < failures.length; ++i) {
            const test = failures[i];
            const message = test.err.message || '';

            output += '  ' + test.id + ') ' + test.title + ':\n\n';

            // Actual vs Expected

            if (test.err.actual !== undefined &&
                test.err.expected !== undefined) {

                const actual = internals.stringify(test.err.actual, true);
                const expected = internals.stringify(test.err.expected, true);

                output += '      ' + whiteRedBg('actual') + ' ' + blackGreenBg('expected') + '\n\n      ';

                const comparison = Diff.diffWords(actual, expected);
                for (let j = 0; j < comparison.length; ++j) {
                    const item = comparison[j];
                    const value = item.value;
                    const lines = value.split('\n');
                    for (let k = 0; k < lines.length; ++k) {
                        if (k) {
                            output += '\n      ';
                        }

                        if (item.added || item.removed) {
                            output += item.added ? blackGreenBg(lines[k]) : whiteRedBg(lines[k]);
                        }
                        else {
                            output += lines[k];
                        }
                    }
                }

                output += '\n\n      ' + yellow(message);
                output += '\n\n';
            }
            else {
                output += '      ' + red(message) + '\n\n';
            }

            if (test.err.at) {
                output += gray('      at ' + test.err.at.filename + ':' + test.err.at.line + ':' + test.err.at.column) + '\n';
            }
            else if (!test.timeout &&
                test.err.stack) {
                let stack = test.err.stack;
                if (stack.indexOf(message) !== -1) {
                    stack = stack.slice(stack.indexOf(message) + message.length + 1);
                }

                output += gray(stack.replace(/^/gm, '  ')) + '\n';
            }

            if (test.err.data) {
                const isObject = typeof test.err.data === 'object' && !Array.isArray(test.err.data);
                let errorData = internals.stringify(test.err.data, isObject);
                if (isObject) {
                    errorData = errorData.split('\n').slice(1, -1).join('\n');
                }

                output += gray('\n      Additional error data:\n' + errorData.replace(/^/gm, '        ')) + '\n';
            }

            output += '\n';
        }

        output += '\n' + red(failures.length + ' of ' + totalTests + ' tests failed');
    }
    else {
        output += green(totalTests + ' tests complete');
    }

    if (skipped.length) {
        output += magenta(` (${skipped.length} skipped)`);
    }

    output += '\n';
    output += 'Test duration: ' + notebook.ms + ' ms\n';

    // Assertions

    if (notebook.assertions !== undefined) {
        output += 'Assertions count: ' + notebook.assertions + ' (verbosity: ' + (notebook.assertions / totalTests).toFixed(2) + ')\n';
    }

    // Leaks

    if (notebook.leaks) {
        if (notebook.leaks.length) {
            output += red('The following leaks were detected:' + notebook.leaks.join(', ')) + '\n';
        }
        else {
            output += green('No global variable leaks detected') + '\n';
        }
    }

    if (notebook.shuffle) {
        output += 'Randomized with seed: ' + notebook.seed + '. Use --shuffle --seed ' + notebook.seed + ' to run tests in same order again.\n';
    }

    // Coverage

    const coverage = notebook.coverage;
    if (coverage) {
        const status = 'Coverage: ' + coverage.percent.toFixed(2) + '%';

        output += coverage.percent === 100 ? green(status) : red(status + ' (' + (coverage.sloc - coverage.hits) + '/' + coverage.sloc + ')');
        if (coverage.percent < 100) {
            coverage.files.forEach((file) => {

                let missingLines;
                if (file.sourcemaps) {
                    const missingLinesByFile = {};
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines = missingLinesByFile[line.originalFilename] = missingLinesByFile[line.originalFilename] || [];
                            missingLines.push(line.originalLine);
                        }
                    });

                    const files = Object.keys(missingLinesByFile);
                    if (files.length) {
                        output += red('\n' + file.filename + ' missing coverage from file(s):');
                        files.forEach((filename) => {

                            output += red('\n\t' + filename + ' on line(s): ' + missingLinesByFile[filename].join(', '));
                        });
                    }
                }
                else {
                    missingLines = [];
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines.push(parseInt(lineNumber, 10));
                        }
                    });

                    if (missingLines.length) {
                        // Lines missing coverage are reported as a list of
                        // spans, e.g. "1, 3-8, 10, 13-15".
                        const missingLinesReport = [];
                        const span = {
                            start: missingLines[0],
                            end: missingLines[0]
                        };
                        for (let i = 1; i <= missingLines.length; ++i) {
                            const line = missingLines[i];
                            if (line === span.end + 1) {
                                // Extend the current span.
                                span.end = line;
                            }
                            else {
                                // Flush current span to output.
                                if (span.start === span.end) {
                                    missingLinesReport.push(span.start);
                                }
                                else if (span.start + 1 === span.end) {
                                    missingLinesReport.push(span.start);
                                    missingLinesReport.push(span.end);
                                }
                                else {
                                    missingLinesReport.push(span.start + '-' + span.end);
                                }

                                // Start a new span.
                                span.start = line;
                                span.end = line;
                            }
                        }

                        output += yellow('\n' + file.filename + ' missing coverage on line(s): ' + missingLinesReport.join(', '));
                    }
                }
            });

            if (coverage.percent < this.settings.threshold) {
                output += red('\nCode coverage below threshold: ' + coverage.percent.toFixed(2) + ' < ' + this.settings.threshold);
            }
        }

        output += '\n';
    }

    const lint = notebook.lint;
    if (lint) {
        output += 'Linting results:';

        let hasErrors = false;
        lint.lint.forEach((entry) => {

            // Don't show anything if there aren't issues
            if (!entry.errors || !entry.errors.length) {
                return;
            }

            hasErrors = true;
            output += gray('\n\t' + entry.filename + ':');
            entry.errors.forEach((err) => {

                output += (err.severity === 'ERROR' ? red : yellow)('\n\t\tLine ' + err.line + ': ' + err.message);
            });
        });

        if (!hasErrors) {
            output += green(' No issues\n');
        }
    }

    if (notes.length) {
        output += '\n\nTest notes:\n';
        notes.forEach((test) => {

            output += gray(test.relativeTitle) + '\n';
            test.notes.forEach((note) => {

                output += yellow(`\t* ${note}`);
            });
        });
    }

    output += '\n';
    this.report(output);
};

internals.color = function (name, code, enabled) {

    if (enabled && Array.isArray(code)) {
        const color = '\u001b[' + code[0] + ';' + code[1] + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }
    else if (enabled) {
        const color = '\u001b[' + code + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }

    return function (text) {

        return text;
    };
};

internals.colors = function (enabled) {

    if (enabled === null) {
        enabled = require('supports-color').stdout;
    }

    const codes = {
        black: 0,
        gray: 90,
        red: 31,
        green: 32,
        yellow: 33,
        magenta: 35,
        redBg: 41,
        greenBg: 42,
        whiteRedBg: [37, 41],
        blackGreenBg: [30, 42]
    };

    const colors = {};
    const names = Object.keys(codes);
    for (let i = 0; i < names.length; ++i) {
        const name = names[i];
        colors[name] = internals.color(name, codes[name], enabled);
    }

    return colors;
};

internals.filterNotes = function (test) {

    return test.notes && test.notes.length;
};

internals.filterFailures = function (test) {

    return !!test.err;
};

internals.filterNodeModules = function (line) {

    return !(/\/node_modules\//.test(line));
};

internals.filterSkipped = function (test) {

    return test.skipped || test.todo;
};
<commit_msg>node 8 fix. For #915<commit_after>'use strict';

const Util = require('util');

const Diff = require('diff');

const internals = {
    width: 50
};

internals.stringify = function (obj) {

    // $lab:coverage:off$
    if (process.version[1] !== '1') {
        obj = internals.sortObject(obj);
    }
    // $lab:coverage:on$

    return Util.inspect(obj, { compact: false, depth: Infinity, maxArrayLength: Infinity, breakLength: 80, sorted: true });
};

// $lab:coverage:off$
internals.sortObject = (obj, _seen) => {

    if (!obj ||
        typeof obj !== 'object') {

        return obj;
    }

    const seen = _seen || new Map();

    if (seen.has(obj)) {
        return seen.get(obj);
    }

    const output = Array.isArray(obj) ? [] : {};
    seen.set(obj, output);

    const keys = Object.keys(obj).sort();
    for (const key of keys) {
        const value = obj[key];
        output[key] = internals.sortObject(value, seen);
    }

    return output;
};
// $lab:coverage:on$

exports = module.exports = internals.Reporter = function (options) {

    this.settings = options;
    this.count = 0;
    this.last = [];

    this.colors = internals.colors(options.colors);
};

internals.Reporter.prototype.start = function (notebook) {

};

internals.Reporter.prototype.test = function (test) {

    if (this.settings.progress === 0) {
        return;
    }

    if (this.settings['silent-skips'] && (test.skipped || test.todo)) {
        return;
    }

    if (this.settings.progress === 1) {

        // ..x....x.-..

        if (!this.count) {
            this.report('\n  ');
        }

        this.count++;

        if ((this.count - 1) % internals.width === 0) {
            this.report('\n  ');
        }

        this.report(test.err ? this.colors.red('x') : (test.skipped || test.todo ? this.colors.magenta('-') : '.'));
    }
    else {
        // $lab:coverage:off$
        const check = process.platform === 'win32' ? '\u221A' : '\u2714';
        const asterisk = process.platform === 'win32' ? '\u00D7' : '\u2716';
        // $lab:coverage:on$

        // Verbose (Spec reporter)

        for (let i = 0; i < test.path.length; ++i) {
            if (test.path[i] !== this.last[i] || (i > 0 && test.path[i - 1] !== this.last[i - 1])) {
                this.report(internals.spacer(i * 2) + test.path[i] + '\n');
            }
        }

        this.last = test.path;

        const spacer = internals.spacer(test.path.length * 2);
        if (test.err) {
            this.report(spacer + this.colors.red(asterisk + ' ' + test.id + ') ' + test.relativeTitle) + '\n');
        }
        else {
            const symbol = test.skipped || test.todo ? this.colors.magenta('-') : this.colors.green(check);
            const assertion = test.assertions === undefined ? '' : ' and ' + test.assertions + ' assertions';
            this.report(spacer + symbol + ' ' + this.colors.gray(test.id + ') ' + test.relativeTitle +
                ' (' + test.duration + ' ms' + assertion + ')') + '\n');
        }
    }
};

internals.spacer = function (length) {

    return new Array(length + 1).join(' ');
};

internals.Reporter.prototype.end = function (notebook) {

    if (this.settings.progress) {
        this.report('\n\n');
    }

    // Colors

    const red = this.colors.red;
    const green = this.colors.green;
    const gray = this.colors.gray;
    const yellow = this.colors.yellow;
    const whiteRedBg = this.colors.whiteRedBg;
    const blackGreenBg = this.colors.blackGreenBg;
    const magenta = this.colors.magenta;

    // Tests

    const notes = notebook.tests.filter(internals.filterNotes);

    const failures = notebook.tests.filter(internals.filterFailures);

    const skipped = notebook.tests.filter(internals.filterSkipped);

    let output = '';
    const totalTests = notebook.tests.length - skipped.length;

    const errors = notebook.errors || [];
    if (errors.length) {
        output += 'Test script errors:\n\n';
        errors.forEach((err = {}) => {

            output += red(err.message) + '\n';
            if (err.stack) {
                const stack = err.stack.slice(err.stack.indexOf('\n') + 1)
                    .replace(/^/gm, '  ')
                    .split('\n')
                    .filter(internals.filterNodeModules)               // Remove node_modules files
                    .slice(0, 5)                                       // Show only first 5 stack lines
                    .join('\n');

                output += gray(stack) + '\n';
            }

            output += '\n';
        });
        output += red('There were ' + errors.length + ' test script error(s).') + '\n\n';
    }

    if (failures.length) {
        output += 'Failed tests:\n\n';

        for (let i = 0; i < failures.length; ++i) {
            const test = failures[i];
            const message = test.err.message || '';

            output += '  ' + test.id + ') ' + test.title + ':\n\n';

            // Actual vs Expected

            if (test.err.actual !== undefined &&
                test.err.expected !== undefined) {

                const actual = internals.stringify(test.err.actual);
                const expected = internals.stringify(test.err.expected);

                output += '      ' + whiteRedBg('actual') + ' ' + blackGreenBg('expected') + '\n\n      ';

                const comparison = Diff.diffWords(actual, expected);
                for (let j = 0; j < comparison.length; ++j) {
                    const item = comparison[j];
                    const value = item.value;
                    const lines = value.split('\n');
                    for (let k = 0; k < lines.length; ++k) {
                        if (k) {
                            output += '\n      ';
                        }

                        if (item.added || item.removed) {
                            output += item.added ? blackGreenBg(lines[k]) : whiteRedBg(lines[k]);
                        }
                        else {
                            output += lines[k];
                        }
                    }
                }

                output += '\n\n      ' + yellow(message);
                output += '\n\n';
            }
            else {
                output += '      ' + red(message) + '\n\n';
            }

            if (test.err.at) {
                output += gray('      at ' + test.err.at.filename + ':' + test.err.at.line + ':' + test.err.at.column) + '\n';
            }
            else if (!test.timeout &&
                test.err.stack) {
                let stack = test.err.stack;
                if (stack.indexOf(message) !== -1) {
                    stack = stack.slice(stack.indexOf(message) + message.length + 1);
                }

                output += gray(stack.replace(/^/gm, '  ')) + '\n';
            }

            if (test.err.data) {
                const isObject = typeof test.err.data === 'object' && !Array.isArray(test.err.data);
                let errorData = internals.stringify(test.err.data);

                if (Array.isArray(test.err.data)) {
                    errorData = errorData.replace(/\n\s*/g, ' ');
                }

                if (isObject) {
                    errorData = errorData.split('\n').slice(1, -1).join('\n');
                }

                output += gray('\n      Additional error data:\n' + errorData.replace(/^/gm, '        ')) + '\n';
            }

            output += '\n';
        }

        output += '\n' + red(failures.length + ' of ' + totalTests + ' tests failed');
    }
    else {
        output += green(totalTests + ' tests complete');
    }

    if (skipped.length) {
        output += magenta(` (${skipped.length} skipped)`);
    }

    output += '\n';
    output += 'Test duration: ' + notebook.ms + ' ms\n';

    // Assertions

    if (notebook.assertions !== undefined) {
        output += 'Assertions count: ' + notebook.assertions + ' (verbosity: ' + (notebook.assertions / totalTests).toFixed(2) + ')\n';
    }

    // Leaks

    if (notebook.leaks) {
        if (notebook.leaks.length) {
            output += red('The following leaks were detected:' + notebook.leaks.join(', ')) + '\n';
        }
        else {
            output += green('No global variable leaks detected') + '\n';
        }
    }

    if (notebook.shuffle) {
        output += 'Randomized with seed: ' + notebook.seed + '. Use --shuffle --seed ' + notebook.seed + ' to run tests in same order again.\n';
    }

    // Coverage

    const coverage = notebook.coverage;
    if (coverage) {
        const status = 'Coverage: ' + coverage.percent.toFixed(2) + '%';

        output += coverage.percent === 100 ? green(status) : red(status + ' (' + (coverage.sloc - coverage.hits) + '/' + coverage.sloc + ')');
        if (coverage.percent < 100) {
            coverage.files.forEach((file) => {

                let missingLines;
                if (file.sourcemaps) {
                    const missingLinesByFile = {};
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines = missingLinesByFile[line.originalFilename] = missingLinesByFile[line.originalFilename] || [];
                            missingLines.push(line.originalLine);
                        }
                    });

                    const files = Object.keys(missingLinesByFile);
                    if (files.length) {
                        output += red('\n' + file.filename + ' missing coverage from file(s):');
                        files.forEach((filename) => {

                            output += red('\n\t' + filename + ' on line(s): ' + missingLinesByFile[filename].join(', '));
                        });
                    }
                }
                else {
                    missingLines = [];
                    Object.keys(file.source).forEach((lineNumber) => {

                        const line = file.source[lineNumber];
                        if (line.miss) {
                            missingLines.push(parseInt(lineNumber, 10));
                        }
                    });

                    if (missingLines.length) {
                        // Lines missing coverage are reported as a list of
                        // spans, e.g. "1, 3-8, 10, 13-15".
                        const missingLinesReport = [];
                        const span = {
                            start: missingLines[0],
                            end: missingLines[0]
                        };
                        for (let i = 1; i <= missingLines.length; ++i) {
                            const line = missingLines[i];
                            if (line === span.end + 1) {
                                // Extend the current span.
                                span.end = line;
                            }
                            else {
                                // Flush current span to output.
                                if (span.start === span.end) {
                                    missingLinesReport.push(span.start);
                                }
                                else if (span.start + 1 === span.end) {
                                    missingLinesReport.push(span.start);
                                    missingLinesReport.push(span.end);
                                }
                                else {
                                    missingLinesReport.push(span.start + '-' + span.end);
                                }

                                // Start a new span.
                                span.start = line;
                                span.end = line;
                            }
                        }

                        output += yellow('\n' + file.filename + ' missing coverage on line(s): ' + missingLinesReport.join(', '));
                    }
                }
            });

            if (coverage.percent < this.settings.threshold) {
                output += red('\nCode coverage below threshold: ' + coverage.percent.toFixed(2) + ' < ' + this.settings.threshold);
            }
        }

        output += '\n';
    }

    const lint = notebook.lint;
    if (lint) {
        output += 'Linting results:';

        let hasErrors = false;
        lint.lint.forEach((entry) => {

            // Don't show anything if there aren't issues
            if (!entry.errors || !entry.errors.length) {
                return;
            }

            hasErrors = true;
            output += gray('\n\t' + entry.filename + ':');
            entry.errors.forEach((err) => {

                output += (err.severity === 'ERROR' ? red : yellow)('\n\t\tLine ' + err.line + ': ' + err.message);
            });
        });

        if (!hasErrors) {
            output += green(' No issues\n');
        }
    }

    if (notes.length) {
        output += '\n\nTest notes:\n';
        notes.forEach((test) => {

            output += gray(test.relativeTitle) + '\n';
            test.notes.forEach((note) => {

                output += yellow(`\t* ${note}`);
            });
        });
    }

    output += '\n';
    this.report(output);
};

internals.color = function (name, code, enabled) {

    if (enabled && Array.isArray(code)) {
        const color = '\u001b[' + code[0] + ';' + code[1] + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }
    else if (enabled) {
        const color = '\u001b[' + code + 'm';
        return function (text) {

            return color + text + '\u001b[0m';
        };
    }

    return function (text) {

        return text;
    };
};

internals.colors = function (enabled) {

    if (enabled === null) {
        enabled = require('supports-color').stdout;
    }

    const codes = {
        black: 0,
        gray: 90,
        red: 31,
        green: 32,
        yellow: 33,
        magenta: 35,
        redBg: 41,
        greenBg: 42,
        whiteRedBg: [37, 41],
        blackGreenBg: [30, 42]
    };

    const colors = {};
    const names = Object.keys(codes);
    for (let i = 0; i < names.length; ++i) {
        const name = names[i];
        colors[name] = internals.color(name, codes[name], enabled);
    }

    return colors;
};

internals.filterNotes = function (test) {

    return test.notes && test.notes.length;
};

internals.filterFailures = function (test) {

    return !!test.err;
};

internals.filterNodeModules = function (line) {

    return !(/\/node_modules\//.test(line));
};

internals.filterSkipped = function (test) {

    return test.skipped || test.todo;
};
<|endoftext|>


<commit_before>import Rx from 'rx'
const merge = Rx.Observable.merge
const of = Rx.Observable.of
const scase = Rx.Observable.case

import {exists,getExtension,getNameAndExtension,isValidFile, isEmpty} from './utils'

import {postProcessMesh,geometryFromBuffers} from './meshUtils'
import {meshTools} from 'glView-helpers'
const centerMesh         = meshTools.centerMesh

import {equals, cond, T, always} from 'ramda'
import {combineLatestObj} from './obsUtils'
import {mergeData} from './modelUtils'
import assign from 'fast.js/object/assign'//faster object.assign

//import parse as stlParser,  {outputs} from 'stlParser'
import * as stlParser from 'usco-stl-parser'
import * as objParser from 'usco-obj-parser'
import * as threemfParser from 'usco-3mf-parser'

function makeParsers(){
  //other
  let parsers = {}
  parsers["stl"] = stlParser.default
  parsers["obj"] = objParser.default
  parsers["3mf"] = threemfParser.default
  //console.log(".inputDataType",parsers["stl"].inputDataType)
  return parsers
}
const parsers = makeParsers()

function postProcessParsedData(data){
  //TODO: unify parsers' returned data/api
  console.log("postProcessMesh",data)
  let mesh = undefined
  if("objects" in data){
    //for 3mf , etc
    console.log("data",data)
    mesh = data.objects['1']
    mesh = geometryFromBuffers(mesh)
    mesh = postProcessMesh(mesh)
    mesh = centerMesh(mesh)

  }else{
    mesh = data
    mesh = geometryFromBuffers(mesh)
    mesh = postProcessMesh(mesh)
    mesh = centerMesh(mesh)
  }

  return mesh
}

function fetch(drivers, sourceNames=["http","desktop"]){
  const chosenDrivers = sourceNames
    .map(function(name){
      return drivers[name]
    })

  const fetched$ = merge(...chosenDrivers)
    .filter(res$ => res$.request.type === 'resource')
    .flatMap(data => {
      const responseWrapper$ = data.catch(e=>{
        console.log("caught error in fetching data",e)
        return Rx.Observable.empty()
      })
      const request$  = of(data.request)
      const response$ = responseWrapper$.pluck("response")
      const progress$ = responseWrapper$.pluck("progress")

      return combineLatestObj({response$, request$, progress$})//.materialize()//FIXME: still do not get this one
    })
    .share()

  return fetched$
}

function parse(fetched$){
  const parseBase$ = fetched$
    .filter(data=>(data.response !== undefined && data.progress === undefined))
    .distinctUntilChanged(d=>d.request.uri,equals)
    //.debounce(10)
    .shareReplay(1)

  const parsed$ = parseBase$
    .map(function(data){
      const uri = data.request.uri
      const {name,ext} = getNameAndExtension(uri)
      return {uri, data:data.response, ext, name}
    })
    //actual parsing part
    .filter(data=>parsers[data.ext]!==undefined)//does parser exist?
    .flatMap(function({uri, data, ext, name}){
      const parseOptions = {useWorker:true}

      const parse    = parsers[ext]
      const parsedObs$ = parse(data, parseOptions)
        //.do(e=>console.log("parsing data",e))
        .doOnError(e=>console.log("error in parse",e))

      const data$  = parsedObs$
        .filter(e=> e.progress === undefined)//seperate out progress data
        .map(postProcessParsedData)

      const progress$ = parsedObs$
        .filter(e=> e.progress !== undefined)//keep ONLY progress data
        .pluck("progress")
        .distinctUntilChanged()
        .startWith(0)

      const meta$    = of({uri, ext, name})

      return combineLatestObj({meta$, data$, progress$})
    })
    .shareReplay(1)

  return parsed$
}

function computeCombinedProgress(fetched$, parsed$){
  const fetchToParseRatio = 0.95

  function preProcess(selector,data$){
    return data$
      .map(selector)
      .distinctUntilChanged()
      .filter(d=>exists(d.progress))
  }

  //we merge fetch information with parse information
  const progress$ = merge(
    preProcess(f=>({id:f.request.uri,progress:f.progress}), fetched$)
      .map(e=>({id:e.id, fetched:e.progress*fetchToParseRatio}))

    ,preProcess(f=>({id:f.meta.uri,progress:f.progress}), parsed$)
      .map(e=>({id:e.id, parsed:e.progress*(1-fetchToParseRatio)}))
  )

  const combinedProgress$ = progress$.scan(function(combined,entry){
    const fetched  = entry.fetched || fetchToParseRatio
    const parsed   = entry.parsed || 0
    const progress = fetched + parsed

    combined.entries[entry.id]  = progress

    let totalProgress = Object.keys(combined.entries)
      .reduce(function(acc,cur){
        return acc + combined.entries[cur]
      },0)

    totalProgress /= Object.keys(combined.entries).length
    combined.totalProgress = totalProgress

    return combined
  },{entries:{},totalProgress:0})
  .pluck("totalProgress")
  .distinctUntilChanged(null, equals)
  .debounce(10)

  return combinedProgress$
}

export function resources(drivers){
  const fetched$ = fetch(drivers)
  const parsed$  = parse(fetched$)
  const combinedProgress$ = computeCombinedProgress( fetched$, parsed$ )

  return {
    combinedProgress$
    , parsed$
  }
}

  /*const fn = cond([
    [equals(0),   always('water freezes at 0°C')],
    [equals(100), always('water boils at 100°C')],
    [T,           temp => 'nothing special happens at ' + temp + '°C']
  ])

  console.log( fn(0) ) //=> 'water freezes at 0°C'
  console.log( fn(50) ) //=> 'nothing special happens at 50°C'
  console.log( fn(100) ) //=> 'water boils at 100°C'*/

<commit_msg>feature(): - experimenting with different outputs from assetManager to handle more complex data types<commit_after>import Rx from 'rx'
const merge = Rx.Observable.merge
const of = Rx.Observable.of
const scase = Rx.Observable.case

import {exists,getExtension,getNameAndExtension,isValidFile, isEmpty} from './utils'

import {postProcessMesh,geometryFromBuffers} from './meshUtils'
import {meshTools} from 'glView-helpers'
const centerMesh         = meshTools.centerMesh

import {equals, cond, T, always} from 'ramda'
import {combineLatestObj} from './obsUtils'
import {mergeData} from './modelUtils'
import assign from 'fast.js/object/assign'//faster object.assign

//import parse as stlParser,  {outputs} from 'stlParser'
import * as stlParser from 'usco-stl-parser'
import * as objParser from 'usco-obj-parser'
import * as threemfParser from 'usco-3mf-parser'

function makeParsers(){
  //other
  let parsers = {}
  parsers["stl"] = stlParser.default
  parsers["obj"] = objParser.default
  parsers["3mf"] = threemfParser.default
  //console.log(".inputDataType",parsers["stl"].inputDataType)
  return parsers
}
const parsers = makeParsers()

function postProcessParsedData(data){
  //TODO: unify parsers' returned data/api
  console.log("postProcessMesh/data",data)
  let mesh = undefined
  if("objects" in data){
    //for 3mf , etc
    console.log("data",data)
    mesh = data.objects['1']
    mesh = geometryFromBuffers(mesh)
    mesh = postProcessMesh(mesh)
    mesh = centerMesh(mesh)

    let typesMetaHash = {}
    let typesMeshes   = []
    let typesMeta = []
    for(let objectId in data.objects){
      //console.log("objectId",objectId, data.objects[objectId])
      let item  = data.objects[objectId]

      let meta = {id:item.id, name:item.name}
      typesMeta.push(meta)
      typesMetaHash[item.id] = meta

      mesh = geometryFromBuffers(item)
      mesh = postProcessMesh(mesh)
      mesh = centerMesh(mesh)
      typesMeshes.push({id:item.id, mesh})
    }

    //now for the instances data
    let instMeta = []
    let instTransforms = []
    data.build.map(function(item,index){

      instMeta.push( { instUid:index, typeUid: typesMetaHash[item.objectid].id} )//TODO : auto generate name
      if('transforms' in item ){
        instTransforms.push({instUid:index, transforms:item.transforms})
      }
    })
    console.log("typesMeta",typesMeta,"instMeta",instMeta,"instTransforms",instTransforms)

    return {typesMeshes, typesMeta, instMeta ,instTransforms}

  }else{
    mesh = data
    mesh = geometryFromBuffers(mesh)
    mesh = postProcessMesh(mesh)
    mesh = centerMesh(mesh)

    let typesMeshes   = [{id:undefined, mesh}]

    return {typesMeshes}
  }

  return mesh
}

function fetch(drivers, sourceNames=["http","desktop"]){
  const chosenDrivers = sourceNames
    .map(function(name){
      return drivers[name]
    })

  const fetched$ = merge(...chosenDrivers)
    .filter(res$ => res$.request.type === 'resource')
    .flatMap(data => {
      const responseWrapper$ = data.catch(e=>{
        console.log("caught error in fetching data",e)
        return Rx.Observable.empty()
      })
      const request$  = of(data.request)
      const response$ = responseWrapper$.pluck("response")
      const progress$ = responseWrapper$.pluck("progress")

      return combineLatestObj({response$, request$, progress$})//.materialize()//FIXME: still do not get this one
    })
    .share()

  return fetched$
}

function parse(fetched$){
  const parseBase$ = fetched$
    .filter(data=>(data.response !== undefined && data.progress === undefined))
    .distinctUntilChanged(d=>d.request.uri,equals)
    //.debounce(10)
    .shareReplay(1)

  const parsed$ = parseBase$
    .map(function(data){
      const uri = data.request.uri
      const {name,ext} = getNameAndExtension(uri)
      return {uri, data:data.response, ext, name}
    })
    //actual parsing part
    .filter(data=>parsers[data.ext]!==undefined)//does parser exist?
    .flatMap(function({uri, data, ext, name}){
      const parseOptions = {useWorker:true}

      const parse    = parsers[ext]
      const parsedObs$ = parse(data, parseOptions)
        //.do(e=>console.log("parsing data",e))
        .doOnError(e=>console.log("error in parse",e))

      const data$  = parsedObs$
        .filter(e=> e.progress === undefined)//seperate out progress data
        .map(postProcessParsedData)

      const progress$ = parsedObs$
        .filter(e=> e.progress !== undefined)//keep ONLY progress data
        .pluck("progress")
        .distinctUntilChanged()
        .startWith(0)

      const meta$    = of({uri, ext, name})

      return combineLatestObj({meta$, data$, progress$})
    })
    .shareReplay(1)

  return parsed$
}

function computeCombinedProgress(fetched$, parsed$){
  const fetchToParseRatio = 0.95

  function preProcess(selector,data$){
    return data$
      .map(selector)
      .distinctUntilChanged()
      .filter(d=>exists(d.progress))
  }

  //we merge fetch information with parse information
  const progress$ = merge(
    preProcess(f=>({id:f.request.uri,progress:f.progress}), fetched$)
      .map(e=>({id:e.id, fetched:e.progress*fetchToParseRatio}))

    ,preProcess(f=>({id:f.meta.uri,progress:f.progress}), parsed$)
      .map(e=>({id:e.id, parsed:e.progress*(1-fetchToParseRatio)}))
  )

  const combinedProgress$ = progress$.scan(function(combined,entry){
    const fetched  = entry.fetched || fetchToParseRatio
    const parsed   = entry.parsed || 0
    const progress = fetched + parsed

    combined.entries[entry.id]  = progress

    let totalProgress = Object.keys(combined.entries)
      .reduce(function(acc,cur){
        return acc + combined.entries[cur]
      },0)

    totalProgress /= Object.keys(combined.entries).length
    combined.totalProgress = totalProgress

    return combined
  },{entries:{},totalProgress:0})
  .pluck("totalProgress")
  .distinctUntilChanged(null, equals)
  .debounce(10)

  return combinedProgress$
}

export function resources(drivers){
  const fetched$ = fetch(drivers)
  const parsed$  = parse(fetched$)
  const combinedProgress$ = computeCombinedProgress( fetched$, parsed$ )

  return {
    combinedProgress$
    , parsed$
  }
}

  /*const fn = cond([
    [equals(0),   always('water freezes at 0°C')],
    [equals(100), always('water boils at 100°C')],
    [T,           temp => 'nothing special happens at ' + temp + '°C']
  ])

  console.log( fn(0) ) //=> 'water freezes at 0°C'
  console.log( fn(50) ) //=> 'nothing special happens at 50°C'
  console.log( fn(100) ) //=> 'water boils at 100°C'*/

<|endoftext|>


<commit_before>'use strict';

module.exports = require('../FusebillResource').extend({
  path: 'coupons',
  includeBasic: ['create', 'list', 'update', 'retrieve', 'del'],
});

<commit_msg>added validate coupon<commit_after>'use strict';

module.exports = require('../FusebillResource').extend({
  path: 'coupons',
  includeBasic: ['create', 'list', 'update', 'retrieve', 'del'],

  validate: fusebillMethod({
    method: 'POST',
    path: '/Validate'
  }),
});

<|endoftext|>