<commit_before>/****************************************
 *     PENN STATE DIRECTORY API         *
 ****************************************/
(function() {
    /* Imports */
    var request = require('request');
    var querystring = require('querystring');
    var cheerio = require('cheerio');

    /* Public object that will contain the public functions that are going to be exported */
    var pub = {};
    /* Private object that  will contain the private functions that will not be exported */
    var priv = {};

    /**
     * psd-api namespace
     *
     */
    var getStudent = function(options, callbackOne) {
        priv.Student = {}, // student
            priv.form = {}, // form
            priv.desc = [], // description elements from the directory page
            priv.data = []; // data elements from the directory page

        /* Selectors used for searching HTML */
        priv.selectors = {
            MATCHES: "b", // the number of MATCHES for each student
            DESC: "th", // the descriptions of the student such as name, email, etc
            DATA: "td" // the data for the descriptions
        };

        /* Generates new query object for the form */
        priv.newQuery = function(firstName, lastName, userID, email) {
            return {
                'cn': firstName,
                'sn': lastName,
                'uid': userID, // name=uid attribute in search page
                'mail': email // name=mail attribute in search page
            };
        };

        /* Validation object that contains the different validation methods */
        priv.validate = {
            /* Validates the passed in parameter options */
            options: function(str) {
                return /^[a-zA-Z]+$/.test(str);
            },

            /* Checks if passed in parameter is an email */
            email: function(str) {
                return str.endsWith("@psu.edu");
            },

            /* Checks if passed in parameter contains a number, which means it is an id */
            /* This validation happens AFTER checking if the string is an email */
            userID: function(str) {
                var isNumber, userID = str.match(/\d+/g);
                if (userID != null) {
                    isNumber = true;
                }
                else {
                    isNumber = false;
                }
                return isNumber;
            },

            /* Determines if the option parameter is an object */
            isobjectValid: function() {
                /* Checks every value in the options object and Incase the string contains non-alphabet characters */
                for (var optKey in options) {
                    /* Validates first and last name */
                    if (optKey === "firstName" || optKey === "lastName") {
                        if ((typeof options[optKey] != "string")) {
                            throw new Error("Invalid Object format!");
                        }
                    }

                    /* Validates the user id */
                    else if (optKey === "userID") {
                        if (priv.validate.userID(options["userID"])) return true;
                        else return false;
                    }

                    /* Validates the email */
                    else if (optKey === "email") {
                        if ((priv.validate.email(options[optKey]) === false)) {
                            throw new Error("Invalid Object format!");
                        }
                    }
                    else {
                        throw new Error("Invalid Object format!");
                    }
                }
                return true;
            }
        };

        /* Fills the student info object  */
        priv.fillStudentObj = function(firstName, lastName, userID, email) {
            priv.Student.firstName = firstName;
            priv.Student.lastName = lastName;
            priv.Student.userID = userID;
            priv.Student.email = email;
        };

        /* Initializes the Student object depending on the passed in string */
        priv.initStrStudent = function() {
            /* Checks if passed in parameter is an email */
            if (priv.validate.email(options)) {
                priv.fillStudentObj("", "", "", options);
            }

            /* Checks if passed in parameter is an id */
            else if (priv.validate.userID(options)) {
                priv.fillStudentObj("", "", options, "");
            }

            /* Passed in parameter is not an object, email, or id, so it must be a name */
            else {
                var input = options.split(" ");
                if (input.length === 2) {
                    priv.fillStudentObj(input[0], input[1]);
                }
                else {
                    priv.fillStudentObj(input[0], "", "", "");
                }
            }
        };

        /* Initializes the student information passed in as options */
        priv.initStudent = function() {
            /* Checks if the passed in is valid object */
            if (typeof options === "object") {
                if (priv.validate.isobjectValid()) {
                    Object.assign(priv.Student, options);
                }
            }

            /* Checks if the passed in parameter is a string */
            else if (typeof options === "string") {
                priv.initStrStudent();
            }
        }();

        /* Form object that will contain the form data that will be sent in the post request */
        priv.form.data = priv.newQuery(priv.Student.firstName, priv.Student.lastName, priv.Student.userID, priv.Student.email);
        priv.form.stringFormData = querystring.stringify(priv.form.data); /* Contains the stringified data */
        priv.form.contentLength = priv.form.stringFormData.length; /* Contains the size of the data that is sent in post request */
        priv.form.dirLink = 'http://www.work.psu.edu/cgi-bin/ldap/ldap_query.cgi'; /* Penn State Directory link for scraping */

        /* Request options that contain the appropriate request headers, url, body, and method for sending the post request */
        priv.form.reqOptions = {
            headers: {
                'Content-Length': priv.form.contentLength,
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            url: priv.form.dirLink,
            body: priv.form.stringFormData,
            method: 'POST'
        };

        /* Removes all line breaks and colons in the data retrieved */
        priv.removeLinBr = function(arr) {
            for (var str in arr) {
                arr[str] = arr[str].trim();
                arr[str] = arr[str].replace(/:/g, '');

                /* Seperates the emails with commas */
                if (priv.validate.email(arr[str])) {
                    arr[str] = arr[str].replace(/\n+/g, ', ');
                }
            }
        };

        /* Finds if there is a match */
        priv.isStudentFound = function($) {
            if ($(priv.selectors.MATCHES).text().indexOf("0 matches", 0) == 0 || options === "") {
                return false;
            } else {
                return true;
            }
        };

        /* Formats the data that is received from the HTML page by removing first entry and line breaks */
        priv.formatData = function() {
            priv.desc.shift();
            priv.removeLinBr(priv.desc);
            priv.removeLinBr(priv.data);
        };

        /* Queries the HTML page for the students information */
        priv.extractData = function($) {
            priv.data = [], priv.desc = []; // reset

            $(priv.selectors.DESC).each(function() {
                priv.desc.push($(this).text());
            });

            $(priv.selectors.DATA).each(function() {
                priv.data.push($(this).text());
            });
        };

        /* Initializes the Student object with the correct properties and values */
        priv.initStudentData = function() {
            priv.Student = {};

            for (var studKey in priv.data) {
                var studProp = priv.desc[studKey];
                priv.Student[studProp] = priv.data[studKey];
            }
        };

        /* Queries the student page for the table headers and table data */
        priv.getStudentInfo = function($) {
            priv.extractData($);
            priv.formatData();
            priv.initStudentData();
        };

        /* Finds the students info on the given HTML page */
        priv.findStudent = function(htmlPage) {
            /* Library used to parse HTML  */
            var $ = cheerio.load(htmlPage);

            /* Checks if the student is found */
            if (priv.isStudentFound($)) {
                priv.getStudentInfo($);
            }
            else {
                if (options != "") console.error("Student was not found!");
                else console.error("Please enter a student!");
            }
        };

        /* Scrapes the students HTML page that is received from the server */
        priv.scrape = function(htmlPage) {
            priv.findStudent(htmlPage);
        };

        /* Callback that executes after the student information is returned */
        var callback = function(err, res, html) {
            if (err) throw new Error('Error in retrieving student informaton');
            priv.scrape(res.body);
            callbackOne(priv.Student);
        };

        /* Sends the request */
        request(priv.form.reqOptions, callback);
    };

    /* Allows you to pass in array of students for searching */
    pub.get = function(input, callbackOne) {
        /* Validates if the number of parameter passed in is 2 and the second parameter is a callback */
        if (arguments.length != 2 || typeof callbackOne != "function") {
            console.error("Please provide the correct number of parameters and a callback function!");
            return;
        }

        /* If an array is passed in then loops through and gets each student */
        if (Array.isArray(input)) {
            for (var student in input) {
                getStudent(input[student], callbackOne);
            }
        }
        else {
            getStudent(input, callbackOne);
        }
    };

    /* EXPORTS the psd-api in node */
    module.exports = pub;
}).call(this);<commit_msg>add initial JSDocs to functions, add questions/suggestions to top of file<commit_after>// Should we change variables/functions named "student" to "person"?
    // This API can be used to search Penn State professors and faculty/staff as well

// I added some JSDoc comments but I was not sure about some of the variable types or the descriptions of the methods
    // Please search for "???" and either edit them to how you like or let me know how they are supposed to look

// I left your normal comments above each JSDoc I added, so you may want to remove them
    // I just copied the comments originally above each function into the description or each JSDoc I added
        // I did describe the parameters of those functions to my best knowledge though
    // I left them in case you wanted to remove the JSDocs

/****************************************
 *     PENN STATE DIRECTORY API         *
 ****************************************/
(function() {
    /* Imports */
    var request = require('request');
    var querystring = require('querystring');
    var cheerio = require('cheerio');

    /* Public object that will contain the public functions that are going to be exported */
    var pub = {};
    /* Private object that  will contain the private functions that will not be exported */
    var priv = {};

    /**
     * psd-api namespace
     *
     */
    var getStudent = function(options, callbackOne) {
        priv.Student = {}, // student
            priv.form = {}, // form
            priv.desc = [], // description elements from the directory page
            priv.data = []; // data elements from the directory page

        /* Selectors used for searching HTML */
        priv.selectors = {
            MATCHES: "b", // the number of MATCHES for each student
            DESC: "th", // the descriptions of the student such as name, email, etc
            DATA: "td" // the data for the descriptions
        };

        /* Generates new query object for the form */
        /**
        * Generates new query object for the form.
        * @function
        * @param {string} firstName - The first name of the person.
        * @param {string} lastName - The last name of the person.
        * @param {string} userID - The user id of the person.
        * @param {string} email - The email of the person.
        */
        priv.newQuery = function(firstName, lastName, userID, email) {
            return {
                'cn': firstName,
                'sn': lastName,
                'uid': userID, // name=uid attribute in search page
                'mail': email // name=mail attribute in search page
            };
        };

        /* Validation object that contains the different validation methods */
        priv.validate = {
            /* Validates the passed in parameter options */
            options: function(str) {
                return /^[a-zA-Z]+$/.test(str);
            },

            /* Checks if passed in parameter is an email */
            email: function(str) {
                return str.endsWith("@psu.edu");
            },

            /* Checks if passed in parameter contains a number, which means it is an id */
            /* This validation happens AFTER checking if the string is an email */
            userID: function(str) {
                var isNumber, userID = str.match(/\d+/g);
                if (userID != null) {
                    isNumber = true;
                }
                else {
                    isNumber = false;
                }
                return isNumber;
            },

            /* Determines if the option parameter is an object */
            isobjectValid: function() {
                /* Checks every value in the options object and Incase the string contains non-alphabet characters */
                for (var optKey in options) {
                    /* Validates first and last name */
                    if (optKey === "firstName" || optKey === "lastName") {
                        if ((typeof options[optKey] != "string")) {
                            throw new Error("Invalid Object format!");
                        }
                    }

                    /* Validates the user id */
                    else if (optKey === "userID") {
                        if (priv.validate.userID(options["userID"])) return true;
                        else return false;
                    }

                    /* Validates the email */
                    else if (optKey === "email") {
                        if ((priv.validate.email(options[optKey]) === false)) {
                            throw new Error("Invalid Object format!");
                        }
                    }
                    else {
                        throw new Error("Invalid Object format!");
                    }
                }
                return true;
            }
        };

        /* Fills the student info object  */
        /**
        * Fills the student info object.
        * @function
        * @param {string} firstName - The first name of the person.
        * @param {string} lastName - The last name of the person.
        * @param {string} userID - The user id of the person.
        * @param {string} email - The email of the person.
        */
        priv.fillStudentObj = function(firstName, lastName, userID, email) {
            priv.Student.firstName = firstName;
            priv.Student.lastName = lastName;
            priv.Student.userID = userID;
            priv.Student.email = email;
        };

        /* Initializes the Student object depending on the passed in string */
        /**
        * Initializes the Student object depending on the passed in string.
        * @function
        */
        priv.initStrStudent = function() {
            /* Checks if passed in parameter is an email */
            if (priv.validate.email(options)) {
                priv.fillStudentObj("", "", "", options);
            }

            /* Checks if passed in parameter is an id */
            else if (priv.validate.userID(options)) {
                priv.fillStudentObj("", "", options, "");
            }

            /* Passed in parameter is not an object, email, or id, so it must be a name */
            else {
                var input = options.split(" ");
                if (input.length === 2) {
                    priv.fillStudentObj(input[0], input[1]);
                }
                else {
                    priv.fillStudentObj(input[0], "", "", "");
                }
            }
        };

        /* Initializes the student information passed in as options */
        /**
        * Initializes the student information passed in as options.
        * @function
        */
        priv.initStudent = function() {
            /* Checks if the passed in is valid object */
            if (typeof options === "object") {
                if (priv.validate.isobjectValid()) {
                    Object.assign(priv.Student, options);
                }
            }

            /* Checks if the passed in parameter is a string */
            else if (typeof options === "string") {
                priv.initStrStudent();
            }
        }();

        /* Form object that will contain the form data that will be sent in the post request */
        priv.form.data = priv.newQuery(priv.Student.firstName, priv.Student.lastName, priv.Student.userID, priv.Student.email);
        priv.form.stringFormData = querystring.stringify(priv.form.data); /* Contains the stringified data */
        priv.form.contentLength = priv.form.stringFormData.length; /* Contains the size of the data that is sent in post request */
        priv.form.dirLink = 'http://www.work.psu.edu/cgi-bin/ldap/ldap_query.cgi'; /* Penn State Directory link for scraping */

        /* Request options that contain the appropriate request headers, url, body, and method for sending the post request */
        priv.form.reqOptions = {
            headers: {
                'Content-Length': priv.form.contentLength,
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            url: priv.form.dirLink,
            body: priv.form.stringFormData,
            method: 'POST'
        };

        /* Removes all line breaks and colons in the data retrieved */
        /**
        * Removes all line breaks and colons in the data retrieved.
        * @function
        * @param {array} arr - The array of strings that make up the response from the POST request???.
        */
        priv.removeLinBr = function(arr) {
            for (var str in arr) {
                arr[str] = arr[str].trim();
                arr[str] = arr[str].replace(/:/g, '');

                /* Seperates the emails with commas */
                if (priv.validate.email(arr[str])) {
                    arr[str] = arr[str].replace(/\n+/g, ', ');
                }
            }
        };

        /* Finds if there is a match */
        /**
        * Finds if there is a match.
        * @function
        * @param {$} $ - ???
        */
        priv.isStudentFound = function($) {
            if ($(priv.selectors.MATCHES).text().indexOf("0 matches", 0) == 0 || options === "") {
                return false;
            } else {
                return true;
            }
        };

        /* Formats the data that is received from the HTML page by removing first entry and line breaks */
        /**
        * Formats the data that is received from the HTML page by removing first entry and line breaks.
        * @function
        */
        priv.formatData = function() {
            priv.desc.shift();
            priv.removeLinBr(priv.desc);
            priv.removeLinBr(priv.data);
        };

        /* Queries the HTML page for the students information */
        /**
        * Queries the HTML page for the students information.
        * @function
        * @param {$} $ - ???
        */
        priv.extractData = function($) {
            priv.data = [], priv.desc = []; // reset

            $(priv.selectors.DESC).each(function() {
                priv.desc.push($(this).text());
            });

            $(priv.selectors.DATA).each(function() {
                priv.data.push($(this).text());
            });
        };

        /* Initializes the Student object with the correct properties and values */
        /**
        * Initializes the Student object with the correct properties and values.
        * @function
        */
        priv.initStudentData = function() {
            priv.Student = {};

            for (var studKey in priv.data) {
                var studProp = priv.desc[studKey];
                priv.Student[studProp] = priv.data[studKey];
            }
        };

        /* Queries the student page for the table headers and table data */
        /**
        * Queries the student page for the table headers and table data.
        * @function
        * @param {$} $ - ???
        */
        priv.getStudentInfo = function($) {
            priv.extractData($);
            priv.formatData();
            priv.initStudentData();
        };

        /* Finds the students info on the given HTML page */
        /**
        * Finds the students info on the given HTML page.
        * @function
        * @param {string} htmlPage - String representing the HTML recieved from the POST request???.
        */
        priv.findStudent = function(htmlPage) {
            /* Library used to parse HTML  */
            var $ = cheerio.load(htmlPage);

            /* Checks if the student is found */
            if (priv.isStudentFound($)) {
                priv.getStudentInfo($);
            }
            else {
                if (options != "") console.error("Student was not found!");
                else console.error("Please enter a student!");
            }
        };

        /* Scrapes the students HTML page that is received from the server */
        /**
        * Scrapes the students HTML page that is received from the server.
        * @function
        * @param {string} htmlPage - String representing the HTML recieved from the POST request???.
        */
        priv.scrape = function(htmlPage) {
            priv.findStudent(htmlPage);
        };

        /* Callback that executes after the student information is returned */
        /**
        * Callback that executes after the student information is returned.
        * @function
        * @param {boolean???} err - Whether or not an error ocurred???.
        * @param {string} res - String representing the HTML recieved from the POST request???.
        * @param {???} html - ???.
        */
        var callback = function(err, res, html) {
            if (err) throw new Error('Error in retrieving student informaton');
            priv.scrape(res.body);
            callbackOne(priv.Student);
        };

        /* Sends the request */
        request(priv.form.reqOptions, callback);
    };

    /* Allows you to pass in array of students for searching */
    /**
    * Allows you to pass in array of students for searching.
    * @function
    * @param {String or Object???} input - Query to search for in the directory.
    * @param {???} callbackOne - Callback function.
    */
    pub.get = function(input, callbackOne) {
        /* Validates if the number of parameter passed in is 2 and the second parameter is a callback */
        if (arguments.length != 2 || typeof callbackOne != "function") {
            console.error("Please provide the correct number of parameters and a callback function!");
            return;
        }

        /* If an array is passed in then loops through and gets each student */
        if (Array.isArray(input)) {
            for (var student in input) {
                getStudent(input[student], callbackOne);
            }
        }
        else {
            getStudent(input, callbackOne);
        }
    };

    /* EXPORTS the psd-api in node */
    module.exports = pub;
}).call(this);<|endoftext|>


<commit_before>import {
  AUTHENTICATE_FAILED,
  AUTHENTICATE_SUCCEEDED,
  INITIALIZE,
  INVALIDATE_SESSION,
  RESTORE,
  RESTORE_FAILED
} from './actionTypes'

const initialState = {
  authenticator: null,
  isAuthenticated: false,
  data: {}
}

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case INITIALIZE:
      const { authenticated: { authenticator, ...data } = {} } = action.payload

      return {
        authenticator,
        data,
        isAuthenticated: false
      }
    case AUTHENTICATE_SUCCEEDED:
      return {
        ...state,
        authenticator: action.meta.authenticator,
        isAuthenticated: true,
        data: action.payload
      }
    case AUTHENTICATE_FAILED:
    case INVALIDATE_SESSION:
    case RESTORE_FAILED:
      return {
        ...state,
        authenticator: null,
        isAuthenticated: false,
        data: {}
      }
    case RESTORE: {
      const { authenticator, ...data } = action.payload

      return {
        ...state,
        authenticator,
        data,
        isAuthenticated: true
      }
    }
    default:
      return state
  }
}

export default reducer
<commit_msg>Export selectors from reducer<commit_after>import {
  AUTHENTICATE_FAILED,
  AUTHENTICATE_SUCCEEDED,
  INITIALIZE,
  INVALIDATE_SESSION,
  RESTORE,
  RESTORE_FAILED
} from './actionTypes'

const initialState = {
  authenticator: null,
  isAuthenticated: false,
  data: {}
}

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case INITIALIZE:
      const { authenticated: { authenticator, ...data } = {} } = action.payload

      return {
        authenticator,
        data,
        isAuthenticated: false
      }
    case AUTHENTICATE_SUCCEEDED:
      return {
        ...state,
        authenticator: action.meta.authenticator,
        isAuthenticated: true,
        data: action.payload
      }
    case AUTHENTICATE_FAILED:
    case INVALIDATE_SESSION:
    case RESTORE_FAILED:
      return {
        ...state,
        authenticator: null,
        isAuthenticated: false,
        data: {}
      }
    case RESTORE: {
      const { authenticator, ...data } = action.payload

      return {
        ...state,
        authenticator,
        data,
        isAuthenticated: true
      }
    }
    default:
      return state
  }
}

export default reducer

export const getData = state => state.data
export const getIsAuthenticated = state => state.isAuthenticated
export const getAuthenticator = state => state.authenticator
<|endoftext|>


<commit_before>import reducerUtils from './utils/reducerUtils';
import constants, {prefix} from './constants';
import getIn from '@mzvonar/getin';
import setIn from '@mzvonar/setin';
import mergeIn from '@mzvonar/mergein';
import {validateInput} from './validators';

const isMRFAction = action => (
    action &&
    action.type &&
    action.type.length > prefix.length &&
    action.type.substring(0, prefix.length) === prefix
);

const createEmptyInputState = (config, initialValue, initialErrors) => {
    const state = {
        pristine: true,
        dirty: false,
        touched: false,
        valid: true,
        errors: null,
        validate: config.validate || [],
        asyncValidation: false,
        asyncErrors: [],
        initialErrors: initialErrors && (Object.keys(initialErrors).length > 0 ? initialErrors : null),
        value: typeof initialValue !== 'undefined' ? initialValue : config.value
    };

    if(config.required === true) {
        state.validate.unshift('required');
    }

    return state;
};

const createEmtpyFormState = (state) => Object.assign({
    pristine: true,
    valid: true,
    touched: false,
    submitting: false,
    submitted: false,
    submitSuccess: false,
    asyncValidation: null,
    initialValues: {},
    initialFormErrors: null,
    initialInputErrors: null,
    submitError: false,
    submitErrorMessages: null
}, state);

function registerForm(state, form, config = {}) {
    return setIn(state, [form], createEmtpyFormState({
        initialValues: config.initialValues || {},
        initialFormErrors: config.initialFormErrors && (config.initialFormErrors.length > 0 ? config.initialFormErrors : null),
        initialInputErrors: config.initialInputErrors
    }));
}

function removeForm(state, form) {
    // return null;
    const newState = Object.assign({}, state);
    delete newState[form];

    return newState;
}

function getAllValues(inputs) {
    const values = {};

    for(const key in inputs) {
        /* istanbul ignore else */
        if(Object.prototype.hasOwnProperty.call(inputs, key)) {
            values[key] = inputs[key].value;
        }
    }

    return values;
}

function registerInput(state, name, config, initialValue, initialErrors) {
    const input = getIn(state, ['inputs', name], createEmptyInputState(config, initialValue, initialErrors));
    const errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));

    input.errors = errors.length > 0 ? errors : null;
    input.valid = !errors || errors.length === 0;

    return setIn(state, ['inputs', name], input);
}

// TODO: remove input cant remove inout from state, it has to only mark it as disabled

function handleInputChange(state, touch, name, value) {
    const input = getIn(state, ['inputs', name]);

    const newValue = (value === '' && input.initialValue === undefined) ? undefined : value;
    // const errors = validateInput(input, newValue, getAllValues(getIn(state, 'inputs')));
    // const valid = !errors || errors.length === 0;

    return mergeIn(state, ['inputs', name], {
        // valid,
        // errors: errors.length > 0 ? errors : null,
        pristine: false,
        dirty: true,
        touched: touch ? true : input.touched,
        value: newValue,
        asyncErrors: []
    });
}

function handleInputBlur(state, touch, name) {
    const input = getIn(state, ['inputs', name]);

    let valid = input.valid;
    let errors = input.errors || [];

    // if(touch) {
    //     errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
    //     valid = errors.length === 0;
    // }

    return mergeIn(state, ['inputs', name], {
        valid,
        // errors: errors.length > 0 ? errors : null,
        touched: touch ? true : input.touched
    });
}

function handleFormChange(state, validate) {
    let formValid = true;
    let pristine = true;
    let touched = false;

    for(const key in state.inputs) {
        /* istanbul ignore else */
        if(Object.prototype.hasOwnProperty.call(state.inputs, key)) {
            const input = state.inputs[key];

            let valid;
            if(validate) {
                const errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
                valid = errors.length === 0;

                state = mergeIn(state, ['inputs', key], {
                    valid: valid,
                    errors: errors.length > 0 ? errors : null,
                });
            }
            else {
                valid = input.valid;
            }

            if(!valid) {
                formValid = false;
            }

            if(!input.pristine) {
                pristine = false;
            }
            if(input.touched) {
                touched = true;
            }
        }
    }

    return mergeIn(state, {
        valid: formValid,
        pristine,
        touched
    });
}

function handleSubmitSuccess(state) {
    // In case form is already removed
    /* istanbul ignore else */
    //if(!state) {
    //    return state;
    //}

    return mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: true,
        valid: true,
        submitError: false,
        submitErrorObject: null
    });
}

function handleSubmitError(state, error) {
    let errorMessages = null;

    const errorType = Object.prototype.toString.call(error);
    if(errorType === '[object String]') {
        errorMessages = [error];
    }
    else if(errorType === '[object Array]') {
        errorMessages = error;
    }

    let formValid = state.valid;
    if(error && error.inputs) {
        for(const key in error.inputs) {
            /* istanbul ignore else */
            if(Object.prototype.hasOwnProperty.call(error.inputs, key)) {
                /* istanbul ignore else */
                if(getIn(state, ['inputs', key])) {
                    let asyncErrors = error.inputs[key];
                    formValid = false;

                    /* istanbul ignore else */
                    if(Object.prototype.toString.call(asyncErrors) !== '[object Array]') {
                        asyncErrors = [asyncErrors];
                    }

                    state = mergeIn(state, ['inputs', key], {
                        asyncErrors: asyncErrors,
                        valid: false
                    });
                }
            }
        }
    }

    state = mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: false,
        submitError: true,
        submitErrorMessages: errorMessages,
        valid: formValid
    });

    return state;
}

function handleAsyncValidateStart(state, name) {
    let newState = setIn(state, 'asyncValidation', true);

    if(name) {
       newState = setIn(newState, ['inputs', name, 'asyncValidation'], true);
    }

    return newState;
}

function handleAsyncValidateFinished(state, name, errors) {
    let newState = setIn(state, 'asyncValidation', false);

    if(errors) {
        if(name) {
            errors = {
                [name]: errors[name]
            };
        }

        for(const key in errors) {
            /* istanbul ignore else */
            if(Object.prototype.hasOwnProperty.call(errors, key) && getIn(newState, ['inputs', key])) {
                let inputErrors = errors[key];
                if(inputErrors) {
                    if(typeof inputErrors === 'string') {
                        inputErrors = [inputErrors];
                    }
                    if(Object.prototype.toString.call(inputErrors) !== '[object Array]') {
                        throw new Error(`Errors should be of type string or array. ${typeof inputErrors} was given`);
                    }
                }
                else {
                    inputErrors = [];
                }

                newState = mergeIn(newState, ['inputs', key], {
                    asyncValidation: false,
                    asyncErrors: inputErrors,
                    valid: inputErrors.length !== 0 ? false : getIn(newState, ['inputs', key, 'valid'])
                });
            }

        }
    }

    return newState;
}

const reducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_INPUT:
            return handleFormChange(registerInput(state, action.payload.name, action.payload.config, action.payload.initialValue, action.payload.initialErrors));

        case constants.INPUT_CHANGE:
            return handleFormChange(handleInputChange(state, action.meta.touch, action.meta.name, action.payload.value), action.meta.touch);

        case constants.INPUT_BLUR:
            return handleFormChange(handleInputBlur(state, action.meta.touch, action.meta.name), true);

        case constants.SUBMITTING:
            return mergeIn(state, {
                submitting: true,
                submitError: null
            });

        case constants.SUBMIT_SUCCESS:
            return handleSubmitSuccess(state);

        case constants.SUBMIT_ERROR:
            return handleSubmitError(state, action.payload);

        case constants.ASYNC_VALIDATE_START:
            return handleAsyncValidateStart(state, action.meta.name);

        case constants.ASYNC_VALIDATE_FINISHED:
            return handleFormChange(handleAsyncValidateFinished(state, action.meta.name, action.payload));

        default:
            return state;
    }
};

const commonReducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_FORM:
            return registerForm(state, action.meta.form, action.payload);

        case constants.REMOVE_FORM:
            if(action.meta && action.meta.form) {
                return removeForm(state, action.meta.form);
            }
            else {
                return state;
            }
            break;

        default:
            return state;
    }
};

const byForm = reducer => (state = {}, action = {}) => {
    const form = action && action.meta && action.meta.form;
    if (!form || !isMRFAction(action)) {
        return state
    }

    const formState = state[form];

    if(!formState) {
        return state;
    }

    const result = reducer(formState, action);

    return result === formState ? state : setIn(state, form, result);
};

export default reducerUtils.composeReducers(byForm(reducer), commonReducer);<commit_msg>reverted back validation for changed and blurred input<commit_after>import reducerUtils from './utils/reducerUtils';
import constants, {prefix} from './constants';
import getIn from '@mzvonar/getin';
import setIn from '@mzvonar/setin';
import mergeIn from '@mzvonar/mergein';
import {validateInput} from './validators';

const isMRFAction = action => (
    action &&
    action.type &&
    action.type.length > prefix.length &&
    action.type.substring(0, prefix.length) === prefix
);

const createEmptyInputState = (config, initialValue, initialErrors) => {
    const state = {
        pristine: true,
        dirty: false,
        touched: false,
        valid: true,
        errors: null,
        validate: config.validate || [],
        asyncValidation: false,
        asyncErrors: [],
        initialErrors: initialErrors && (Object.keys(initialErrors).length > 0 ? initialErrors : null),
        value: typeof initialValue !== 'undefined' ? initialValue : config.value
    };

    if(config.required === true) {
        state.validate.unshift('required');
    }

    return state;
};

const createEmtpyFormState = (state) => Object.assign({
    pristine: true,
    valid: true,
    touched: false,
    submitting: false,
    submitted: false,
    submitSuccess: false,
    asyncValidation: null,
    initialValues: {},
    initialFormErrors: null,
    initialInputErrors: null,
    submitError: false,
    submitErrorMessages: null
}, state);

function registerForm(state, form, config = {}) {
    return setIn(state, [form], createEmtpyFormState({
        initialValues: config.initialValues || {},
        initialFormErrors: config.initialFormErrors && (config.initialFormErrors.length > 0 ? config.initialFormErrors : null),
        initialInputErrors: config.initialInputErrors
    }));
}

function removeForm(state, form) {
    // return null;
    const newState = Object.assign({}, state);
    delete newState[form];

    return newState;
}

function getAllValues(inputs) {
    const values = {};

    for(const key in inputs) {
        /* istanbul ignore else */
        if(Object.prototype.hasOwnProperty.call(inputs, key)) {
            values[key] = inputs[key].value;
        }
    }

    return values;
}

function registerInput(state, name, config, initialValue, initialErrors) {
    const input = getIn(state, ['inputs', name], createEmptyInputState(config, initialValue, initialErrors));
    const errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));

    input.errors = errors.length > 0 ? errors : null;
    input.valid = !errors || errors.length === 0;

    return setIn(state, ['inputs', name], input);
}

// TODO: remove input cant remove inout from state, it has to only mark it as disabled

function handleInputChange(state, touch, name, value) {
    const input = getIn(state, ['inputs', name]);

    const newValue = (value === '' && input.initialValue === undefined) ? undefined : value;

    const errors = validateInput(input, newValue, getAllValues(getIn(state, 'inputs')));
    const valid = !errors || errors.length === 0;

    return mergeIn(state, ['inputs', name], {
        valid,
        errors: errors.length > 0 ? errors : null,
        pristine: false,
        dirty: true,
        touched: touch ? true : input.touched,
        value: newValue,
        asyncErrors: []
    });
}

function handleInputBlur(state, touch, name) {
    const input = getIn(state, ['inputs', name]);

    let valid = input.valid;
    let errors = input.errors || [];

    if(touch) {
        errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
        valid = errors.length === 0;
    }

    return mergeIn(state, ['inputs', name], {
        valid,
        errors: errors.length > 0 ? errors : null,
        touched: touch ? true : input.touched
    });
}

function handleFormChange(state, validate) {
    let formValid = true;
    let pristine = true;
    let touched = false;

    for(const key in state.inputs) {
        /* istanbul ignore else */
        if(Object.prototype.hasOwnProperty.call(state.inputs, key)) {
            const input = state.inputs[key];

            let valid;
            if(validate) {
                const errors = validateInput(input, input.value, getAllValues(getIn(state, 'inputs')));
                valid = errors.length === 0;

                state = mergeIn(state, ['inputs', key], {
                    valid: valid,
                    errors: errors.length > 0 ? errors : null,
                });
            }
            else {
                valid = input.valid;
            }

            if(!valid) {
                formValid = false;
            }

            if(!input.pristine) {
                pristine = false;
            }
            if(input.touched) {
                touched = true;
            }
        }
    }

    return mergeIn(state, {
        valid: formValid,
        pristine,
        touched
    });
}

function handleSubmitSuccess(state) {
    // In case form is already removed
    /* istanbul ignore else */
    //if(!state) {
    //    return state;
    //}

    return mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: true,
        valid: true,
        submitError: false,
        submitErrorObject: null
    });
}

function handleSubmitError(state, error) {
    let errorMessages = null;

    const errorType = Object.prototype.toString.call(error);
    if(errorType === '[object String]') {
        errorMessages = [error];
    }
    else if(errorType === '[object Array]') {
        errorMessages = error;
    }

    let formValid = state.valid;
    if(error && error.inputs) {
        for(const key in error.inputs) {
            /* istanbul ignore else */
            if(Object.prototype.hasOwnProperty.call(error.inputs, key)) {
                /* istanbul ignore else */
                if(getIn(state, ['inputs', key])) {
                    let asyncErrors = error.inputs[key];
                    formValid = false;

                    /* istanbul ignore else */
                    if(Object.prototype.toString.call(asyncErrors) !== '[object Array]') {
                        asyncErrors = [asyncErrors];
                    }

                    state = mergeIn(state, ['inputs', key], {
                        asyncErrors: asyncErrors,
                        valid: false
                    });
                }
            }
        }
    }

    state = mergeIn(state, {
        submitting: false,
        submitted: true,
        submitSuccess: false,
        submitError: true,
        submitErrorMessages: errorMessages,
        valid: formValid
    });

    return state;
}

function handleAsyncValidateStart(state, name) {
    let newState = setIn(state, 'asyncValidation', true);

    if(name) {
       newState = setIn(newState, ['inputs', name, 'asyncValidation'], true);
    }

    return newState;
}

function handleAsyncValidateFinished(state, name, errors) {
    let newState = setIn(state, 'asyncValidation', false);

    if(errors) {
        if(name) {
            errors = {
                [name]: errors[name]
            };
        }

        for(const key in errors) {
            /* istanbul ignore else */
            if(Object.prototype.hasOwnProperty.call(errors, key) && getIn(newState, ['inputs', key])) {
                let inputErrors = errors[key];
                if(inputErrors) {
                    if(typeof inputErrors === 'string') {
                        inputErrors = [inputErrors];
                    }
                    if(Object.prototype.toString.call(inputErrors) !== '[object Array]') {
                        throw new Error(`Errors should be of type string or array. ${typeof inputErrors} was given`);
                    }
                }
                else {
                    inputErrors = [];
                }

                newState = mergeIn(newState, ['inputs', key], {
                    asyncValidation: false,
                    asyncErrors: inputErrors,
                    valid: inputErrors.length !== 0 ? false : getIn(newState, ['inputs', key, 'valid'])
                });
            }

        }
    }

    return newState;
}

const reducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_INPUT:
            return handleFormChange(registerInput(state, action.payload.name, action.payload.config, action.payload.initialValue, action.payload.initialErrors));

        case constants.INPUT_CHANGE:
            return handleFormChange(handleInputChange(state, action.meta.touch, action.meta.name, action.payload.value), action.meta.touch || !getIn(state, ['inputs', action.meta.name, 'valid']));

        case constants.INPUT_BLUR:
            return handleFormChange(handleInputBlur(state, action.meta.touch, action.meta.name), true);

        case constants.SUBMITTING:
            return mergeIn(state, {
                submitting: true,
                submitError: null
            });

        case constants.SUBMIT_SUCCESS:
            return handleSubmitSuccess(state);

        case constants.SUBMIT_ERROR:
            return handleSubmitError(state, action.payload);

        case constants.ASYNC_VALIDATE_START:
            return handleAsyncValidateStart(state, action.meta.name);

        case constants.ASYNC_VALIDATE_FINISHED:
            return handleFormChange(handleAsyncValidateFinished(state, action.meta.name, action.payload));

        default:
            return state;
    }
};

const commonReducer = (state, action) => {
    switch(action.type) {
        case constants.REGISTER_FORM:
            return registerForm(state, action.meta.form, action.payload);

        case constants.REMOVE_FORM:
            if(action.meta && action.meta.form) {
                return removeForm(state, action.meta.form);
            }
            else {
                return state;
            }
            break;

        default:
            return state;
    }
};

const byForm = reducer => (state = {}, action = {}) => {
    const form = action && action.meta && action.meta.form;
    if (!form || !isMRFAction(action)) {
        return state
    }

    const formState = state[form];

    if(!formState) {
        return state;
    }

    const result = reducer(formState, action);

    return result === formState ? state : setIn(state, form, result);
};

export default reducerUtils.composeReducers(byForm(reducer), commonReducer);<|endoftext|>


<commit_before>/**
 * @file Require implementation
 * @since 0.2.2
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfPathJoin*/ // Join all arguments together and normalize the resulting path
/*global _gpfRequireLoad*/ // Load the resource
/*exported _gpfRequireAllocate*/ // Allocate a new require context with the proper methods
/*#endif*/

/* this is globally used as the current context in this module */
/*jshint -W040*/
/*eslint-disable no-invalid-this*/

_gpfErrorDeclare("require", {

    /**
     * ### Summary
     *
     * Invalid {@link gpf.require.configure} option
     *
     * ### Description
     *
     * This error is triggered whenever an option passed to {@link gpf.require.configure} is not recognized.
     * Please check the {@link gpf.typedef.requireOptions} documentation.
     * @since 0.2.2
     */
    invalidRequireConfigureOption:
        "Invalid configuration option"
});

/**
 * @namespace gpf.require
 * @description Root namespace for the modularization helpers.
 * @since 0.2.2
 */

/**
 * @typedef gpf.typedef.requireOptions
 * @property {String} [base] Base path used to resolve names
 * @property {Object} [cache] Inject names into the require cache
 * @property {Boolean} [clearCache=false] When set, the require cache is first cleared
 * @since 0.2.2
 */

/**
 * @typedef gpf.typedef._requireContext
 * @property {String} base Base path used to resolve names
 * @property {Object} cache Dictionary of loaded requires
 * @since 0.2.2
 */

/**
 * Valuate the option priority to have them executed in the proper order
 *
 * @param {String} name option name
 * @return {Number} Option priority
 * @since 0.2.2
 */
function _gpfRequireOptionPriority (name) {
    if ("clearCache" === name) {
        return -1;
    }
    return 1;
}

/**
 *  Dictionary of option name to function handling the option
 * @type {Object}
 * @since 0.2.2
 */
var _gpfRequireOptionHandler = {

    base: function (base) {
        this.base = base;
    },

    cache: function (cache) {
        _gpfArrayForEach(Object.keys(cache), function (name) {
            this.cache[name] = Promise.resolve(cache[name]);
        }, this);
    },

    clearCache: function () {
        this.cache = {};
    }

};

/**
 * Configure the {@link gpf.require} layer
 *
 * @param {gpf.typedef.requireOptions} options Options to configure
 * @since 0.2.2
 */
function _gpfRequireConfigure (options) {
    var me = this;
    _gpfArrayForEach(Object.keys(options).sort(function (key1, key2) {
        // Some keys must be processed first
        return _gpfRequireOptionPriority(key1) - _gpfRequireOptionPriority(key2);
    }), function (key) {
        (_gpfRequireOptionHandler[key] || gpf.Error.invalidRequireConfigureOption).call(me, options[key]);
    }, me);
}

/**
 * Resolves the resource name according to current require context.
 *
 * @param {String} name Relative resource name
 * @return {String} Resolved name
 * @since 0.2.2
 *
 */
function _gpfRequireResolve (name) {
    return _gpfPathJoin(this.base, name);
}

function _gpfRequireDocumentStack (reason, name) {
    if (!Array.isArray(reason.requires)) {
        reason.requires = [];
    }
    reason.requires.push(name);
}

/**
 * Get the cached resource or load it
 *
 * @param {String} name Resource name
 * @return {Promise<*>} Resource association
 * @since 0.2.2
 */
function _gpfRequireGet (name) {
    var me = this,
        promise;
    if (me.cache[name]) {
        return me.cache[name];
    }
    promise = _gpfRequireLoad.call(me, name);
    me.cache[name] = promise;
    return promise["catch"](function (reason) {
        _gpfRequireDocumentStack(reason, name);
        return Promise.reject(reason);
    });
}

/**
 * Defines a new module by executing the factory function with the specified dependent resources,
 * see {@tutorial REQUIRE}
 *
 *
 * @param {Object} dependencies Dictionary of dependencies, the keys are preserved while passing the result
 * dictionary to the factory function
 * @param {*} factory Can be either:
 * * A factory function executed when all resources are resolved, the first parameter will be a dictionary
 *   with all dependencies indexed by their name (as initially specified in the dependencies parameter).
 *   The result of the factory function will be cached as the result of this resource
 * * Any value that will be cached as the result of this resource
 * @return {Promise<*>} Resolved with the factory function result or the object
 * @since 0.2.2
 */
function _gpfRequireDefine (dependencies, factory) {
    var me = this,
        promises = [],
        keys = Object.keys(dependencies);
    _gpfArrayForEach(keys, function (key) {
        promises.push(_gpfRequireGet.call(me, _gpfRequireResolve.call(me, dependencies[key])));
    }, me);
    return Promise.all(promises)
        .then(function (resources) {
            var result,
                require;
            if ("function" === typeof factory) {
                require = {};
                _gpfArrayForEach(keys, function (key, index) {
                    require[key] = resources[index];
                });
                result = factory(require);
            } else {
                result = factory;
            }
            return result;
        });
}

/**
 * Allocate a new require context with the proper methods
 *
 * @param {Object} parentContext Context to inherit from
 * @param {gpf.typedef.requireOptions} [options] Options to configure
 * @return {Object} Containing {@link gpf.require.define}, {@link gpf.require.resolve} and {@link gpf.require.configure}
 * @since 0.2.2
 */
function _gpfRequireAllocate (parentContext, options) {
    var context = Object.create(parentContext), // cache content is shared but other properties are protected
        require = {};
    require.define = _gpfRequireDefine.bind(context);
    require.resolve = _gpfRequireResolve.bind(context);
    require.configure = _gpfRequireConfigure.bind(context);
    if (options) {
        require.configure(options);
    }
    return require;
}

gpf.require = _gpfRequireAllocate({
    base: "",
    cache: {}
});

/**
 * @method gpf.require.define
 * @gpf:sameas _gpfRequireDefine
 * @since 0.2.2
 */

/**
 * @method gpf.require.configure
 * @gpf:sameas _gpfRequireConfigure
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 *
 * @example
Injecting in the cache

 * var cache = {};
 * cache[gpf.require.resolve("data.json")] = {};
 * gpf.require.configure({
 *   clearCache: true,
 *   cache: cache
 * });
 */

/**
 * @method gpf.require.resolve
 * @gpf:sameas _gpfRequireResolve
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 */
<commit_msg>Adds preload option (#259)<commit_after>/**
 * @file Require implementation
 * @since 0.2.2
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfPathJoin*/ // Join all arguments together and normalize the resulting path
/*global _gpfRequireLoad*/ // Load the resource
/*exported _gpfRequireAllocate*/ // Allocate a new require context with the proper methods
/*#endif*/

/* this is globally used as the current context in this module */
/*jshint -W040*/
/*eslint-disable no-invalid-this*/

_gpfErrorDeclare("require", {

    /**
     * ### Summary
     *
     * Invalid {@link gpf.require.configure} option
     *
     * ### Description
     *
     * This error is triggered whenever an option passed to {@link gpf.require.configure} is not recognized.
     * Please check the {@link gpf.typedef.requireOptions} documentation.
     * @since 0.2.2
     */
    invalidRequireConfigureOption:
        "Invalid configuration option"
});

/**
 * @namespace gpf.require
 * @description Root namespace for the modularization helpers.
 * @since 0.2.2
 */

/**
 * @typedef gpf.typedef.requireOptions
 * @property {String} [base] Base path used to resolve names
 * @property {Object} [cache] Inject names into the require cache
 * @property {Boolean} [clearCache=false] When set, the require cache is first cleared
 * @property {Object} [preload] Inject names into the loading cache
 * @since 0.2.2
 */

/**
 * @typedef gpf.typedef._requireContext
 * @property {String} base Base path used to resolve names
 * @property {Object} cache Dictionary of loaded requires
 * @since 0.2.2
 */

/**
 * Valuate the option priority to have them executed in the proper order
 *
 * @param {String} name option name
 * @return {Number} Option priority
 * @since 0.2.2
 */
function _gpfRequireOptionPriority (name) {
    if ("clearCache" === name) {
        return -1;
    }
    return 1;
}

/**
 *  Dictionary of option name to function handling the option
 * @type {Object}
 * @since 0.2.2
 */
var _gpfRequireOptionHandler = {

    base: function (base) {
        this.base = base;
    },

    cache: function (cache) {
        _gpfArrayForEach(Object.keys(cache), function (name) {
            this.cache[name] = Promise.resolve(cache[name]);
        }, this);
    },

    clearCache: function () {
        this.cache = {};
    },

    preload: function (cache) {
        _gpfArrayForEach(Object.keys(cache), function (name) {
            this.preload[name] = cache[name];
        }, this);
    }

};

/**
 * Configure the {@link gpf.require} layer
 *
 * @param {gpf.typedef.requireOptions} options Options to configure
 * @since 0.2.2
 */
function _gpfRequireConfigure (options) {
    var me = this;
    _gpfArrayForEach(Object.keys(options).sort(function (key1, key2) {
        // Some keys must be processed first
        return _gpfRequireOptionPriority(key1) - _gpfRequireOptionPriority(key2);
    }), function (key) {
        (_gpfRequireOptionHandler[key] || gpf.Error.invalidRequireConfigureOption).call(me, options[key]);
    }, me);
}

/**
 * Resolves the resource name according to current require context.
 *
 * @param {String} name Relative resource name
 * @return {String} Resolved name
 * @since 0.2.2
 *
 */
function _gpfRequireResolve (name) {
    return _gpfPathJoin(this.base, name);
}

function _gpfRequireDocumentStack (reason, name) {
    if (!Array.isArray(reason.requires)) {
        reason.requires = [];
    }
    reason.requires.push(name);
}

/**
 * Get the cached resource or load it
 *
 * @param {String} name Resource name
 * @return {Promise<*>} Resource association
 * @since 0.2.2
 */
function _gpfRequireGet (name) {
    var me = this,
        promise;
    if (me.cache[name]) {
        return me.cache[name];
    }
    promise = _gpfRequireLoad.call(me, name);
    me.cache[name] = promise;
    return promise["catch"](function (reason) {
        _gpfRequireDocumentStack(reason, name);
        return Promise.reject(reason);
    });
}

/**
 * Defines a new module by executing the factory function with the specified dependent resources,
 * see {@tutorial REQUIRE}
 *
 *
 * @param {Object} dependencies Dictionary of dependencies, the keys are preserved while passing the result
 * dictionary to the factory function
 * @param {*} factory Can be either:
 * * A factory function executed when all resources are resolved, the first parameter will be a dictionary
 *   with all dependencies indexed by their name (as initially specified in the dependencies parameter).
 *   The result of the factory function will be cached as the result of this resource
 * * Any value that will be cached as the result of this resource
 * @return {Promise<*>} Resolved with the factory function result or the object
 * @since 0.2.2
 */
function _gpfRequireDefine (dependencies, factory) {
    var me = this,
        promises = [],
        keys = Object.keys(dependencies);
    _gpfArrayForEach(keys, function (key) {
        promises.push(_gpfRequireGet.call(me, _gpfRequireResolve.call(me, dependencies[key])));
    }, me);
    return Promise.all(promises)
        .then(function (resources) {
            var result,
                require;
            if ("function" === typeof factory) {
                require = {};
                _gpfArrayForEach(keys, function (key, index) {
                    require[key] = resources[index];
                });
                result = factory(require);
            } else {
                result = factory;
            }
            return result;
        });
}

/**
 * Allocate a new require context with the proper methods
 *
 * @param {Object} parentContext Context to inherit from
 * @param {gpf.typedef.requireOptions} [options] Options to configure
 * @return {Object} Containing {@link gpf.require.define}, {@link gpf.require.resolve} and {@link gpf.require.configure}
 * @since 0.2.2
 */
function _gpfRequireAllocate (parentContext, options) {
    var context = Object.create(parentContext), // cache content is shared but other properties are protected
        require = {};
    require.define = _gpfRequireDefine.bind(context);
    require.resolve = _gpfRequireResolve.bind(context);
    require.configure = _gpfRequireConfigure.bind(context);
    if (options) {
        require.configure(options);
    }
    return require;
}

gpf.require = _gpfRequireAllocate({
    base: "",
    cache: {},
    preload: {}
});

/**
 * @method gpf.require.define
 * @gpf:sameas _gpfRequireDefine
 * @since 0.2.2
 */

/**
 * @method gpf.require.configure
 * @gpf:sameas _gpfRequireConfigure
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 *
 * @example
Injecting in the cache

 * var cache = {};
 * cache[gpf.require.resolve("data.json")] = {};
 * gpf.require.configure({
 *   clearCache: true,
 *   cache: cache
 * });
 */

/**
 * @method gpf.require.resolve
 * @gpf:sameas _gpfRequireResolve
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 */
<|endoftext|>


<commit_before>/**
 * @file Require implementation
 * @since 0.2.2
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfErrorDeclare*/ // Declare new gpf.Error names
/*global _gpfPathJoin*/ // Join all arguments together and normalize the resulting path
/*global _gpfRequireLoad*/ // Load the resource
/*global _gpfRequireConfigure*/ // Configure the {@link gpf.require} layer
/*exported _gpfRequireAllocate*/ // Allocate a new require context with the proper methods
/*#endif*/

/* this is globally used as the current context in this module */
/*jshint -W040*/
/*eslint-disable no-invalid-this*/

/**
 * @typedef gpf.typedef._requireContext
 * @property {String} base Base path used to resolve names
 * @property {Object} cache Dictionary of loaded requires
 * @property {Object} preload Dictionary of preloaded requires
 * @property {gpf.typedef.requirePreprocessFunc} preprocess Preprocess function
 * @since 0.2.2
 */

/**
 * Resolves the resource name according to current require context.
 *
 * @param {String} name Relative resource name
 * @return {String} Resolved name
 * @since 0.2.2
 *
 */
function _gpfRequireResolve (name) {
    return _gpfPathJoin(this.base, name);
}

function _gpfRequireDocumentStack (reason, name) {
    if (!Array.isArray(reason.requires)) {
        reason.requires = [];
    }
    reason.requires.push(name);
}

/**
 * Get the cached resource or load it
 *
 * @param {String} name Resource name
 * @return {Promise<*>} Resource association
 * @since 0.2.2
 */
function _gpfRequireGet (name) {
    var me = this,
        promise;
    if (me.cache[name]) {
        return me.cache[name];
    }
    promise = _gpfRequireLoad.call(me, name);
    me.cache[name] = promise;
    return promise["catch"](function (reason) {
        _gpfRequireDocumentStack(reason, name);
        return Promise.reject(reason);
    });
}

/**
 * Defines a new module by executing the factory function with the specified dependent resources,
 * see {@tutorial REQUIRE}
 *
 *
 * @param {Object} dependencies Dictionary of dependencies, the keys are preserved while passing the result
 * dictionary to the factory function
 * @param {*} factory Can be either:
 * * A factory function executed when all resources are resolved, the first parameter will be a dictionary
 *   with all dependencies indexed by their name (as initially specified in the dependencies parameter).
 *   The result of the factory function will be cached as the result of this resource
 * * Any value that will be cached as the result of this resource
 * @return {Promise<*>} Resolved with the factory function result or the object
 * @since 0.2.2
 */
function _gpfRequireDefine (dependencies, factory) {
    var me = this,
        promises = [],
        keys = Object.keys(dependencies);
    _gpfArrayForEach(keys, function (key) {
        promises.push(_gpfRequireGet.call(me, _gpfRequireResolve.call(me, dependencies[key])));
    }, me);
    return Promise.all(promises)
        .then(function (resources) {
            var result,
                require;
            if (typeof factory === "function") {
                require = {};
                _gpfArrayForEach(keys, function (key, index) {
                    require[key] = resources[index];
                });
                result = factory(require);
            } else {
                result = factory;
            }
            return result;
        });
}

/**
 * Allocate a new require context with the proper methods
 *
 * @param {Object} parentContext Context to inherit from
 * @param {gpf.typedef.requireOptions} [options] Options to configure
 * @return {Object} Containing {@link gpf.require.define}, {@link gpf.require.resolve} and {@link gpf.require.configure}
 * @since 0.2.2
 */
function _gpfRequireAllocate (parentContext, options) {
    var context = Object.create(parentContext), // cache content is shared but other properties are protected
        require = {};
    require.define = _gpfRequireDefine.bind(context);
    require.resolve = _gpfRequireResolve.bind(context);
    require.configure = _gpfRequireConfigure.bind(context);
    if (options) {
        require.configure(options);
    }
    return require;
}

gpf.require = _gpfRequireAllocate({
    base: "",
    cache: {},
    preload: {},
    preprocess: function (resource) {
        return Promise.resolve(resource);
    }
});

/**
 * @method gpf.require.define
 * @gpf:sameas _gpfRequireDefine
 * @since 0.2.2
 */

/**
 * @method gpf.require.resolve
 * @gpf:sameas _gpfRequireResolve
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 */
<commit_msg>Documentation (#306)<commit_after>/**
 * @file Require implementation
 * @since 0.2.2
 */
/*#ifndef(UMD)*/
"use strict";
/*global _gpfArrayForEach*/ // Almost like [].forEach (undefined are also enumerated)
/*global _gpfPathJoin*/ // Join all arguments together and normalize the resulting path
/*global _gpfRequireConfigure*/ // Configure the {@link gpf.require} layer
/*global _gpfRequireLoad*/ // Load the resource
/*exported _gpfRequireAllocate*/ // Allocate a new require context with the proper methods
/*#endif*/

/* this is globally used as the current context in this module */
/*jshint -W040*/
/*eslint-disable no-invalid-this*/

/**
 * @typedef gpf.typedef._requireContext
 * @property {String} base Base path used to resolve names
 * @property {Object} cache Dictionary of loaded requires
 * @property {Object} preload Dictionary of preloaded requires
 * @property {gpf.typedef.requirePreprocessFunc} preprocess Preprocess function
 * @since 0.2.2
 */

/**
 * Resolves the resource name according to current require context.
 *
 * @param {String} name Relative resource name
 * @return {String} Resolved name
 * @since 0.2.2
 *
 */
function _gpfRequireResolve (name) {
    return _gpfPathJoin(this.base, name);
}

function _gpfRequireDocumentStack (reason, name) {
    if (!Array.isArray(reason.requires)) {
        reason.requires = [];
    }
    reason.requires.push(name);
}

/**
 * Get the cached resource or load it
 *
 * @param {String} name Resource name
 * @return {Promise<*>} Resource association
 * @since 0.2.2
 */
function _gpfRequireGet (name) {
    var me = this,
        promise;
    if (me.cache[name]) {
        return me.cache[name];
    }
    promise = _gpfRequireLoad.call(me, name);
    me.cache[name] = promise;
    return promise["catch"](function (reason) {
        _gpfRequireDocumentStack(reason, name);
        return Promise.reject(reason);
    });
}

/**
 * Defines a new module by executing the factory function with the specified dependent resources,
 * see {@tutorial REQUIRE}
 *
 *
 * @param {Object} dependencies Dictionary of dependencies, the keys are preserved while passing the result
 * dictionary to the factory function
 * @param {*} factory Can be either:
 * * A factory function executed when all resources are resolved, the first parameter will be a dictionary
 *   with all dependencies indexed by their name (as initially specified in the dependencies parameter).
 *   The result of the factory function will be cached as the result of this resource
 * * Any value that will be cached as the result of this resource
 * @return {Promise<*>} Resolved with the factory function result or the object
 * @since 0.2.2
 */
function _gpfRequireDefine (dependencies, factory) {
    var me = this,
        promises = [],
        keys = Object.keys(dependencies);
    _gpfArrayForEach(keys, function (key) {
        promises.push(_gpfRequireGet.call(me, _gpfRequireResolve.call(me, dependencies[key])));
    }, me);
    return Promise.all(promises)
        .then(function (resources) {
            var result,
                require;
            if (typeof factory === "function") {
                require = {};
                _gpfArrayForEach(keys, function (key, index) {
                    require[key] = resources[index];
                });
                result = factory(require);
            } else {
                result = factory;
            }
            return result;
        });
}

/**
 * Allocate a new require context with the proper methods
 *
 * @param {Object} parentContext Context to inherit from
 * @param {gpf.typedef.requireOptions} [options] Options to configure
 * @return {Object} Containing {@link gpf.require.define}, {@link gpf.require.resolve} and {@link gpf.require.configure}
 * @since 0.2.2
 */
function _gpfRequireAllocate (parentContext, options) {
    var context = Object.create(parentContext), // cache content is shared but other properties are protected
        require = {};
    require.define = _gpfRequireDefine.bind(context);
    require.resolve = _gpfRequireResolve.bind(context);
    require.configure = _gpfRequireConfigure.bind(context);
    if (options) {
        require.configure(options);
    }
    return require;
}

gpf.require = _gpfRequireAllocate({
    base: "",
    cache: {},
    preload: {},
    preprocess: function (resource) {
        return Promise.resolve(resource);
    }
});

/**
 * @method gpf.require.define
 * @gpf:sameas _gpfRequireDefine
 * @since 0.2.2
 */

/**
 * @method gpf.require.resolve
 * @gpf:sameas _gpfRequireResolve
 * @since 0.2.2
 *
 * @example
Setting the base path

 * gpf.require.configure({
 *   base: "/test/require"
 * });
 * assert(gpf.require.resolve("file.js") === "/test/require/file.js");
 */
<|endoftext|>


<commit_before>function resolve({
  columns,
  method = () => rowData => rowData,
  indexKey = '_index'
}) {
  if (!columns) {
    throw new Error('resolve - Missing columns!');
  }

  return (rows = []) => {
    const methodsByColumn = columns.map(column => method({ column }));

    return rows.map((rowData, rowIndex) => {
      let ret = {
        [indexKey]: rowIndex,
        ...rowData
      };

      methodsByColumn.forEach((boundMethod) => {
        ret = boundMethod(ret);
      });

      // TODO: What's the purpose of this?
      delete ret.undefined;
      return ret;
    });
  };
}

export default resolve;
<commit_msg>Remove undefined property reset.<commit_after>function resolve({
  columns,
  method = () => rowData => rowData,
  indexKey = '_index'
}) {
  if (!columns) {
    throw new Error('resolve - Missing columns!');
  }

  return (rows = []) => {
    const methodsByColumn = columns.map(column => method({ column }));

    return rows.map((rowData, rowIndex) => {
      let ret = {
        [indexKey]: rowIndex,
        ...rowData
      };

      methodsByColumn.forEach((boundMethod) => {
        ret = boundMethod(ret);
      });

      return ret;
    });
  };
}

export default resolve;
<|endoftext|>


<commit_before>
/**
 *
 * sclang - boots a supercollider language interpreter process
 *  and enables stdin/out and system level event responders
 *
 *  SuperCollider comes with an executable called sclang
 *  which can be communicated with via stdin/stdout
 *  or via OSC.
 *
 * methods:
 *   boot  - boot an sclang process
 *   write - write to stdin of the sclang process
 *   quit
 *
 */

var
  _ = require('underscore'),
  EventEmitter = require('events').EventEmitter,
  spawn = require('child_process').spawn,
  path = require('path'),
  Logger = require('./logger'),
  Q = require('q'),
  uuid = require('node-uuid'),
  join = require('path').join,
  yaml = require('js-yaml'),
  temp = require('temp'),
  fs   = require('fs'),
  untildify = require('untildify');

import {SclangIO, STATES} from './sclang-io';

export default class SCLang extends EventEmitter {

  /*
   * @param {object} options - sclang command line options
   */
  constructor(options) {
    super();
    this.options = options || {};
    this.process = null;
    this.log = new Logger(this.options.debug, this.options.echo);
    this.log.dbug(this.options);
    this.stateWatcher = this.makeStateWatcher();
  }

  /**
   * build args for sclang
   *
   *   -d <path>                      Set runtime directory
   *   -D                             Enter daemon mode (no input)
   *   -g <memory-growth>[km]         Set heap growth (default 256k)
   *   -h                             Display this message and exit
   *   -l <path>                      Set library configuration file
   *   -m <memory-space>[km]          Set initial heap size (default 2m)
   *   -r                             Call Main.run on startup
   *   -s                             Call Main.stop on shutdown
   *   -u <network-port-number>       Set UDP listening port (default 57120)
   *   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default "none")
   *   -a                             Standalone mode
   */
  args(options) {
    var o = [];
    if (options.executeFile) {
      o.push(options.executeFile);
    }
    o.push('-u', options.langPort);
    if (options.config) {
      o.push('-l', options.config);
    }
    return o;
  }

  /**
   * makeSclangConfig
   *
   * make sclang_config.yaml as a temporary file
   * with the supplied values
   *
   * This is the config file that sclang reads, specifying
   * includePaths and excludePaths
   *
   * @param {object} config - options to write to file
   * @returns {Promise} resolving with the path of the temp config file
   */
  makeSclangConfig(config) {
    /**
      write options as yaml to a temp file
      and return the path
    **/
    var deferred = Q.defer();
    var str = yaml.safeDump(config, {indent: 4});

    temp.open('sclang-config', function(err, info) {
      if (err) {
        return deferred.reject(err);
      }
      fs.write(info.fd, str);
      fs.close(info.fd, function(err) {
        if (err) {
          return deferred.reject(err);
        }
        deferred.resolve(info.path);
      });
    });

    return deferred.promise;
  }

  /**
   * boot
   *
   * start sclang as a subprocess
   *
   * @returns {Promise}
   */
  boot() {
    this.setState(STATES.BOOTING);

    // merge supercollider.js options with any sclang_conf
    var config = this.sclangConfigOptions(this.options);

    return this.makeSclangConfig(config)
      .then((configPath) => {
        return this.spawnProcess(this.options.sclang, _.extend({}, this.options, {config: configPath}));
      });
  }

  /**
   * spawnProcess - starts the sclang executable
   *
   * sets this.process
   * adds state listeners
   *
   * @param {string} execPath - path to sclang
   * @param {object} commandLineOptions - options for the command line
   *                filtered with this.args so it will only include values
   *                that sclang uses.
   * @returns {Promise}
   *     resolves null on successful boot and compile
   *     rejects on failure to boot or failure to compile the class library
   */
  spawnProcess(execPath, commandLineOptions) {
    var deferred = Q.defer();

    this.process = spawn(execPath, this.args(commandLineOptions),
      {
        cwd: path.dirname(execPath)
      });

    var bootListener = (state) => {
      if (state === STATES.READY) {
        deferred.resolve();
        this.removeListener('state', bootListener);
      } else if (state === STATES.COMPILE_ERROR) {
        deferred.reject(this.stateWatcher.compileErrors);
        this.removeListener('state', bootListener);
        // probably should remove all listeners
      }
    };

    // temporary listener until booted ready or compileError
    // that removes itself
    this.addListener('state', bootListener);

    // long term listeners
    this.installListeners(this.process, Boolean(this.options.stdin));
    return deferred.promise;
  }

  /**
   * sclangConfigOptions
   *
   * Builds the options that will be written to the config file that is read by sclang
   * If supercolliderjs-conf specifies a sclang_conf path
   * then this is read and any includePaths and excludePaths are merged
   *
   * throws error if config cannot be read
   *
   * @param {object} options - supercolliderJs options
   * @returns {object} - sclang_config variables
   */
  sclangConfigOptions(options) {
    var
      runtimeIncludePaths = [
        path.resolve(__dirname, '../sc-classes')
      ],
      sclang_conf = {},
      config = {};

    if (options.sclang_conf) {
      try {
        sclang_conf = yaml.safeLoad(fs.readFileSync(untildify(options.sclang_conf), 'utf8'));
      } catch (e) {
        this.log.err(e);
        throw 'Cannot open or read specified sclang_conf ' + options.sclang_conf;
      }
    }

    return {
      includePaths: _.union(sclang_conf.includePaths, options.includePaths, runtimeIncludePaths),
      excludePaths: _.union(sclang_conf.excludePaths, options.excludePaths),
      postInlineWarning: _.isUndefined(options.postInlineWarnings) ?
        Boolean(sclang_conf.postInlineWarnings) :
        Boolean(options.postInlineWarnings)
    };
  }

  makeStateWatcher() {
    var stateWatcher = new SclangIO(this);
    for (let name of ['interpreterLoaded', 'error', 'stdout', 'state']) {
      stateWatcher.on(name, (...args) => {
        this.emit(name, ...args);
      });
    }
    return stateWatcher;
  }

  /**
    * listen to events from process and pipe stdio to the stateWatcher
    */
  installListeners(subprocess, listenToStdin) {
    if (listenToStdin) {
      // stdin of the global top level nodejs process
      process.stdin.setEncoding('utf8');
      process.stdin.on('data', (chunk) => {
        if (chunk) {
          this.write(chunk, null, true);
        }
      });
    }
    subprocess.stdout.on('data', (data) => {
      this.stateWatcher.parse(String(data));
    });
    subprocess.stderr.on('data', (data) => {
      var error = String(data);
      this.log.stderr(error);
      this.emit('stderr', error);
    });
    subprocess.on('error', (err) => {
      this.log.err('ERROR:' + err, 'error');
      this.emit('stderr', err);
    });
    subprocess.on('close', (code, signal) => {
      this.log.dbug('close ' + code + signal);
      this.emit('exit', code);
      this.setState(null);
    });
    subprocess.on('exit', (code, signal) => {
      this.log.dbug('exit ' + code + signal);
      this.emit('exit', code);
      this.setState(null);
    });
    subprocess.on('disconnect', () => {
      this.log.dbug('disconnect');
      this.emit('exit');
      this.setState(null);
    });
  }

  /**
   * write
   *
   * send a raw string to sclang to be interpreted
   * callback is called after write is complete
   */
  write(chunk, callback, noEcho) {
    if (!noEcho) {
      this.log.stdin(chunk);
    }
    this.process.stdin.write(chunk, 'UTF-8');
    // escape character means execute the currently accumulated command line as SC code
    this.process.stdin.write('\x0c', null, callback);
  }

  /**
    * storeSclangConf
    */
  storeSclangConf() {
    // store the original configuration path
    // so that it can be accessed by the modified Quarks methods
    // to store into the correct conf file
    if (this.options.sclang_conf) {
      var configPath = path.resolve(untildify(this.options.sclang_conf));
      var setConfigPath = 'SuperColliderJS.sclangConf = "' + configPath + '";\n\n';
      return this.interpret(setConfigPath, null, true, true, true);
    } else {
      return Promise.resolve(this);
    }
  }

  /**
   * interpret
   *
   * evaluates code in sclang
   * and returns a promise
   *
   * resolves promise with result or rejects with error as JSON
   *
   * @param {String} code
   *        source code to evaluate
   * @param {String} nowExecutingPath
            set thisProcess.nowExecutingPath
   *        for use in a REPL to evaluate text in a file
   *        and let sclang know what file it is executing.
   * @param {Boolean} asString
   *        return result .asString for post window
   *        otherwise returns result as a JSON object
   * @param {Boolean} postErrors
   *        call error.reportError on any errors
   *        which posts call stack, receiver, args, etc
   * @param {Boolean} getBacktrace
   *        return full backtrace
   * @returns {Promise}
   */
  interpret(code, nowExecutingPath, asString, postErrors, getBacktrace) {
    var deferred = Q.defer();
    var escaped = code
      .replace(/[\n\r]/g, '__NL__')
      .replace(/\\/g, '__SLASH__')
      .replace(/\"/g, '\\"');
    var guid = uuid.v1();

    var args = [
      '"' + guid + '"',
      '"' + escaped + '"',
      nowExecutingPath ? '"' + nowExecutingPath + '"' : 'nil',
      asString ? 'true' : 'false',
      postErrors ? 'true' : 'false',
      getBacktrace ? 'true' : 'false'
    ].join(',');

    this.stateWatcher.registerCall(guid, deferred);
    this.write('SuperColliderJS.interpret(' + args + ');', null, true);
    return deferred.promise;
  }

  /**
   * @private
   */
  setState(state) {
    this.stateWatcher.setState(state);
  }

  quit() {
    var deferred = Q.defer();
    var cleanup = () => {
      this.process = null;
      this.setState(null);
      deferred.resolve();
    };
    if (this.process) {
      this.process.once('exit', cleanup);
      // request a polite shutdown
      this.process.kill('SIGINT');
      setTimeout(() => {
        // 3.6.6 doesn't fully respond to SIGINT
        // but SIGTERM causes it to crash
        if (this.process) {
          this.process.kill('SIGTERM');
          cleanup();
        }
      }, 250);
    } else {
      cleanup();
    }
    return deferred.promise;
  }
}

/**
  * alternate constructor
  * resolves options, boots and loads interpreter
  *
  * remember to add a .fail handler to the returned
  * Promise so that you catch any propagated errors
  *
  * @param {object} sclang options
  * @returns: {Promise}
  */
SCLang.boot = function(options) {
  var resolveOptions = require('./resolveOptions');

  return resolveOptions(options.config, options).then(function(options) {

    var sclang = new SCLang(options);

    return sclang.boot()
      .then(function() {
        return sclang.initInterpreter();
      });
  });

};
<commit_msg>sclang: pass ide flag -i supercolliderjs<commit_after>
/**
 *
 * sclang - boots a supercollider language interpreter process
 *  and enables stdin/out and system level event responders
 *
 *  SuperCollider comes with an executable called sclang
 *  which can be communicated with via stdin/stdout
 *  or via OSC.
 *
 * methods:
 *   boot  - boot an sclang process
 *   write - write to stdin of the sclang process
 *   quit
 *
 */

var
  _ = require('underscore'),
  EventEmitter = require('events').EventEmitter,
  spawn = require('child_process').spawn,
  path = require('path'),
  Logger = require('./logger'),
  Q = require('q'),
  uuid = require('node-uuid'),
  join = require('path').join,
  yaml = require('js-yaml'),
  temp = require('temp'),
  fs   = require('fs'),
  untildify = require('untildify');

import {SclangIO, STATES} from './sclang-io';

export default class SCLang extends EventEmitter {

  /*
   * @param {object} options - sclang command line options
   */
  constructor(options) {
    super();
    this.options = options || {};
    this.process = null;
    this.log = new Logger(this.options.debug, this.options.echo);
    this.log.dbug(this.options);
    this.stateWatcher = this.makeStateWatcher();
  }

  /**
   * build args for sclang
   *
   *   -d <path>                      Set runtime directory
   *   -D                             Enter daemon mode (no input)
   *   -g <memory-growth>[km]         Set heap growth (default 256k)
   *   -h                             Display this message and exit
   *   -l <path>                      Set library configuration file
   *   -m <memory-space>[km]          Set initial heap size (default 2m)
   *   -r                             Call Main.run on startup
   *   -s                             Call Main.stop on shutdown
   *   -u <network-port-number>       Set UDP listening port (default 57120)
   *   -i <ide-name>                  Specify IDE name (for enabling IDE-specific class code, default "none")
   *   -a                             Standalone mode
   */
  args(options) {
    var o = [];
    o.push('-i', 'supercolliderjs');
    if (options.executeFile) {
      o.push(options.executeFile);
    }
    o.push('-u', options.langPort);
    if (options.config) {
      o.push('-l', options.config);
    }
    return o;
  }

  /**
   * makeSclangConfig
   *
   * make sclang_config.yaml as a temporary file
   * with the supplied values
   *
   * This is the config file that sclang reads, specifying
   * includePaths and excludePaths
   *
   * @param {object} config - options to write to file
   * @returns {Promise} resolving with the path of the temp config file
   */
  makeSclangConfig(config) {
    /**
      write options as yaml to a temp file
      and return the path
    **/
    var deferred = Q.defer();
    var str = yaml.safeDump(config, {indent: 4});

    temp.open('sclang-config', function(err, info) {
      if (err) {
        return deferred.reject(err);
      }
      fs.write(info.fd, str);
      fs.close(info.fd, function(err) {
        if (err) {
          return deferred.reject(err);
        }
        deferred.resolve(info.path);
      });
    });

    return deferred.promise;
  }

  /**
   * boot
   *
   * start sclang as a subprocess
   *
   * @returns {Promise}
   */
  boot() {
    this.setState(STATES.BOOTING);

    // merge supercollider.js options with any sclang_conf
    var config = this.sclangConfigOptions(this.options);

    return this.makeSclangConfig(config)
      .then((configPath) => {
        return this.spawnProcess(this.options.sclang, _.extend({}, this.options, {config: configPath}));
      });
  }

  /**
   * spawnProcess - starts the sclang executable
   *
   * sets this.process
   * adds state listeners
   *
   * @param {string} execPath - path to sclang
   * @param {object} commandLineOptions - options for the command line
   *                filtered with this.args so it will only include values
   *                that sclang uses.
   * @returns {Promise}
   *     resolves null on successful boot and compile
   *     rejects on failure to boot or failure to compile the class library
   */
  spawnProcess(execPath, commandLineOptions) {
    var deferred = Q.defer();

    this.process = spawn(execPath, this.args(commandLineOptions),
      {
        cwd: path.dirname(execPath)
      });

    var bootListener = (state) => {
      if (state === STATES.READY) {
        deferred.resolve();
        this.removeListener('state', bootListener);
      } else if (state === STATES.COMPILE_ERROR) {
        deferred.reject(this.stateWatcher.compileErrors);
        this.removeListener('state', bootListener);
        // probably should remove all listeners
      }
    };

    // temporary listener until booted ready or compileError
    // that removes itself
    this.addListener('state', bootListener);

    // long term listeners
    this.installListeners(this.process, Boolean(this.options.stdin));
    return deferred.promise;
  }

  /**
   * sclangConfigOptions
   *
   * Builds the options that will be written to the config file that is read by sclang
   * If supercolliderjs-conf specifies a sclang_conf path
   * then this is read and any includePaths and excludePaths are merged
   *
   * throws error if config cannot be read
   *
   * @param {object} options - supercolliderJs options
   * @returns {object} - sclang_config variables
   */
  sclangConfigOptions(options) {
    var
      runtimeIncludePaths = [
        path.resolve(__dirname, '../sc-classes')
      ],
      sclang_conf = {},
      config = {};

    if (options.sclang_conf) {
      try {
        sclang_conf = yaml.safeLoad(fs.readFileSync(untildify(options.sclang_conf), 'utf8'));
      } catch (e) {
        this.log.err(e);
        throw 'Cannot open or read specified sclang_conf ' + options.sclang_conf;
      }
    }

    return {
      includePaths: _.union(sclang_conf.includePaths, options.includePaths, runtimeIncludePaths),
      excludePaths: _.union(sclang_conf.excludePaths, options.excludePaths),
      postInlineWarning: _.isUndefined(options.postInlineWarnings) ?
        Boolean(sclang_conf.postInlineWarnings) :
        Boolean(options.postInlineWarnings)
    };
  }

  makeStateWatcher() {
    var stateWatcher = new SclangIO(this);
    for (let name of ['interpreterLoaded', 'error', 'stdout', 'state']) {
      stateWatcher.on(name, (...args) => {
        this.emit(name, ...args);
      });
    }
    return stateWatcher;
  }

  /**
    * listen to events from process and pipe stdio to the stateWatcher
    */
  installListeners(subprocess, listenToStdin) {
    if (listenToStdin) {
      // stdin of the global top level nodejs process
      process.stdin.setEncoding('utf8');
      process.stdin.on('data', (chunk) => {
        if (chunk) {
          this.write(chunk, null, true);
        }
      });
    }
    subprocess.stdout.on('data', (data) => {
      this.stateWatcher.parse(String(data));
    });
    subprocess.stderr.on('data', (data) => {
      var error = String(data);
      this.log.stderr(error);
      this.emit('stderr', error);
    });
    subprocess.on('error', (err) => {
      this.log.err('ERROR:' + err, 'error');
      this.emit('stderr', err);
    });
    subprocess.on('close', (code, signal) => {
      this.log.dbug('close ' + code + signal);
      this.emit('exit', code);
      this.setState(null);
    });
    subprocess.on('exit', (code, signal) => {
      this.log.dbug('exit ' + code + signal);
      this.emit('exit', code);
      this.setState(null);
    });
    subprocess.on('disconnect', () => {
      this.log.dbug('disconnect');
      this.emit('exit');
      this.setState(null);
    });
  }

  /**
   * write
   *
   * send a raw string to sclang to be interpreted
   * callback is called after write is complete
   */
  write(chunk, callback, noEcho) {
    if (!noEcho) {
      this.log.stdin(chunk);
    }
    this.process.stdin.write(chunk, 'UTF-8');
    // escape character means execute the currently accumulated command line as SC code
    this.process.stdin.write('\x0c', null, callback);
  }

  /**
    * storeSclangConf
    */
  storeSclangConf() {
    // store the original configuration path
    // so that it can be accessed by the modified Quarks methods
    // to store into the correct conf file
    if (this.options.sclang_conf) {
      var configPath = path.resolve(untildify(this.options.sclang_conf));
      var setConfigPath = 'SuperColliderJS.sclangConf = "' + configPath + '";\n\n';
      return this.interpret(setConfigPath, null, true, true, true);
    } else {
      return Promise.resolve(this);
    }
  }

  /**
   * interpret
   *
   * evaluates code in sclang
   * and returns a promise
   *
   * resolves promise with result or rejects with error as JSON
   *
   * @param {String} code
   *        source code to evaluate
   * @param {String} nowExecutingPath
            set thisProcess.nowExecutingPath
   *        for use in a REPL to evaluate text in a file
   *        and let sclang know what file it is executing.
   * @param {Boolean} asString
   *        return result .asString for post window
   *        otherwise returns result as a JSON object
   * @param {Boolean} postErrors
   *        call error.reportError on any errors
   *        which posts call stack, receiver, args, etc
   * @param {Boolean} getBacktrace
   *        return full backtrace
   * @returns {Promise}
   */
  interpret(code, nowExecutingPath, asString, postErrors, getBacktrace) {
    var deferred = Q.defer();
    var escaped = code
      .replace(/[\n\r]/g, '__NL__')
      .replace(/\\/g, '__SLASH__')
      .replace(/\"/g, '\\"');
    var guid = uuid.v1();

    var args = [
      '"' + guid + '"',
      '"' + escaped + '"',
      nowExecutingPath ? '"' + nowExecutingPath + '"' : 'nil',
      asString ? 'true' : 'false',
      postErrors ? 'true' : 'false',
      getBacktrace ? 'true' : 'false'
    ].join(',');

    this.stateWatcher.registerCall(guid, deferred);
    this.write('SuperColliderJS.interpret(' + args + ');', null, true);
    return deferred.promise;
  }

  /**
   * @private
   */
  setState(state) {
    this.stateWatcher.setState(state);
  }

  quit() {
    var deferred = Q.defer();
    var cleanup = () => {
      this.process = null;
      this.setState(null);
      deferred.resolve();
    };
    if (this.process) {
      this.process.once('exit', cleanup);
      // request a polite shutdown
      this.process.kill('SIGINT');
      setTimeout(() => {
        // 3.6.6 doesn't fully respond to SIGINT
        // but SIGTERM causes it to crash
        if (this.process) {
          this.process.kill('SIGTERM');
          cleanup();
        }
      }, 250);
    } else {
      cleanup();
    }
    return deferred.promise;
  }
}

/**
  * alternate constructor
  * resolves options, boots and loads interpreter
  *
  * remember to add a .fail handler to the returned
  * Promise so that you catch any propagated errors
  *
  * @param {object} sclang options
  * @returns: {Promise}
  */
SCLang.boot = function(options) {
  var resolveOptions = require('./resolveOptions');

  return resolveOptions(options.config, options).then(function(options) {

    var sclang = new SCLang(options);

    return sclang.boot()
      .then(function() {
        return sclang.initInterpreter();
      });
  });

};
<|endoftext|>