<commit_before>#!/usr/bin/env node

const Ajv = require('ajv');
const strictDeepEquals = require('deep-equal');
const fs = require("fs");
const _ = require("lodash");
const path = require("path");
const shell = require("shelljs");
const deepEquals = require("./deepEquals");
const Main = require("../output/Main");
const Samples = require("../output/Samples");
const assert = require("assert");
const { inParallel } = require("./multicore");
const os = require("os");
const exit = require('exit');

//////////////////////////////////////
// Constants
/////////////////////////////////////

function debug(x) {
    if (!process.env.DEBUG) return;
    console.log(x);
    return x;
}

const IS_CI = process.env.CI === "true";
const BRANCH = process.env.TRAVIS_BRANCH;
const IS_BLESSED = ["master"].indexOf(BRANCH) !== -1;
const IS_PUSH = process.env.TRAVIS_EVENT_TYPE === "push";
const IS_PR = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== "false";

const CPUs = IS_CI
    ? 2 /* Travis has only 2 but reports 8 */
    : process.env.CPUs || os.cpus().length;

const QUICKTYPE_CLI = path.resolve("./cli/quicktype.js");

const NODE_BIN = path.resolve("./node_modules/.bin");
process.env.PATH += `:${NODE_BIN}`;

//////////////////////////////////////
// Fixtures
/////////////////////////////////////

const FIXTURES = [
    {
        name: "csharp",
        base: "test/csharp",
        setup: "dotnet restore",
        diffViaSchema: false,
        output: "QuickType.cs",
        test: testCSharp
    },
    {
        name: "golang",
        base: "test/golang",
        diffViaSchema: true,
        output: "quicktype.go",
        test: testGo
    },
    {
        name: "json-schema",
        base: "test/golang",
        diffViaSchema: false,
        output: "schema.json",
        test: testJsonSchema
    },
    {
        name: "elm",
        base: "test/elm",
        setup: "elm-make --yes",
        diffViaSchema: false,
        output: "QuickType.elm",
        test: testElm
    }
].filter(({name}) => !process.env.FIXTURE || name === process.env.FIXTURE);

//////////////////////////////////////
// Go tests
/////////////////////////////////////

const knownUnicodeFails = ["identifiers.json"];
const unicodeWillFail = (sample) => knownUnicodeFails.indexOf(path.basename(sample)) !== -1;

function testGo(sample) {
    if (unicodeWillFail(sample)) {
        console.error(`Skipping golang ${sample} – known to fail`);
        return;
    }

    compareJsonFileToJson({
        expectedFile: sample,
        jsonCommand: `go run main.go quicktype.go < "${sample}"`,
        strict: false
    });
}

//////////////////////////////////////
// C# tests
/////////////////////////////////////

function testCSharp(sample) {
    compareJsonFileToJson({
        expectedFile: sample,
        jsonCommand: `dotnet run "${sample}"`,
        strict: false
    });
}

//////////////////////////////////////
// Elm tests
/////////////////////////////////////

function testElm(sample) {
    if (unicodeWillFail(sample)) {
        console.error(`Skipping elm ${sample} – known to fail`);
        return;
    }

    exec("elm-make Main.elm QuickType.elm --output elm.js");
    exec(`node ./runner.js "${sample}"`)
}

//////////////////////////////////////
// JSON Schema tests
/////////////////////////////////////

function testJsonSchema(sample) {
    let input = JSON.parse(fs.readFileSync(sample));
    let schema = JSON.parse(fs.readFileSync("schema.json"));

    let ajv = new Ajv();
    let valid = ajv.validate(schema, input);
    if (!valid) {
        failWith({
            sample,
            error: "Generated schema does not validate input JSON.",
        });
    }

    // Generate Go from the schema
    exec(`quicktype --srcLang json-schema -o quicktype.go --src schema.json`);

    // Possibly check the output of the Go program against the sample
    if (unicodeWillFail(sample)) {
        console.error("Known to fail - not checking output.");
    } else {
        // Parse the sample with Go generated from its schema, and compare to the sample
        compareJsonFileToJson({
            expectedFile: sample,
            jsonCommand: `go run main.go quicktype.go < "${sample}"`,
            strict: false
        });
    }

    // Generate a schema from the schema, making sure the schemas are the same
    compareJsonFileToJson({
        expectedFile: "schema.json",
        jsonCommand: `quicktype --srcLang json-schema --src schema.json --lang json`,
        strict: true
    });
}

//////////////////////////////////////
// Test driver
/////////////////////////////////////

function failWith(obj) {
    obj.cwd = process.cwd();
    console.error(JSON.stringify(obj, null, "  "));
    throw obj;
}

function exec(s, opts, cb) {
    // We special-case quicktype execution
    s = s.replace(/^quicktype /, `node ${QUICKTYPE_CLI} `);

    debug(s);

    let result = shell.exec(s, opts, cb);
    if (result.code !== 0) {
        console.error(result.stdout);
        console.error(result.stderr);
        failWith({
            command: s,
            code: result.code
        });
    }
    return result;
}

function compareJsonFileToJson({expectedFile, jsonFile, jsonCommand, strict}) {
    debug({expectedFile, jsonFile, jsonCommand, strict});

    let jsonString = jsonFile
        ? fs.readFileSync(jsonFile)
        : exec(jsonCommand, {silent: true}).stdout;

    let givenJSON = JSON.parse(jsonString);
    let expectedJSON = JSON.parse(fs.readFileSync(expectedFile));

    let jsonAreEqual = strict
        ? strictDeepEquals(givenJSON, expectedJSON)
        : deepEquals(expectedJSON, givenJSON, []);

    if (!jsonAreEqual) {
        console.error("Error: Output is not equivalent to input.");
        failWith({
            expectedFile,
            jsonCommand,
            jsonFile
        });
    }
}

function inDir(dir, work) {
    let origin = process.cwd();

    debug(`cd ${dir}`)
    process.chdir(dir);

    work();
    process.chdir(origin);
}

function runFixtureWithSample(fixture, sample) {
    let tmp = path.resolve(os.tmpdir(), require("crypto").randomBytes(8).toString('hex'));
    let sampleAbs = path.resolve(sample);

    let stats = fs.statSync(sampleAbs);
    if (stats.size > 32 * 1024 * 1024) {
        console.error(`* Skipping ${sampleAbs} because it's too large`);
        return;
    }

    shell.cp("-R", fixture.base, tmp);

    inDir(tmp, () => {
        // Generate code from the sample
        exec(`quicktype --src ${sampleAbs} --srcLang json -o ${fixture.output}`);

        fixture.test(sampleAbs);

        if (fixture.diffViaSchema) {
            debug("* Diffing with code generated via JSON Schema");
            // Make a schema
            exec(`quicktype --src ${sampleAbs} --srcLang json -o schema.json`);
            // Quicktype from the schema and compare to expected code
            shell.mv(fixture.output, `${fixture.output}.expected`);
            exec(`quicktype --src schema.json --srcLang json-schema -o ${fixture.output}`);

            // Compare fixture.output to fixture.output.expected
            try {
                exec(`diff -Naur ${fixture.output}.expected ${fixture.output}`);
            } catch ({ command }) {
                // FIXME: Set this to fail once we have it working.  See issue #59.
                console.error(`Command failed, but we're allowing it: ${command}`);
            }
        }
    });

    shell.rm("-rf", tmp);
}

function testAll(samples) {
    // Get an array of all { sample, fixtureName } objects we'll run
    let tests =  _
        .chain(FIXTURES)
        .flatMap((fixture) => samples.map((sample) => {
            return { sample, fixtureName: fixture.name };
        }))
        .shuffle()
        .value();

    inParallel({
        queue: tests,
        workers: CPUs,
        setup: () => {
            FIXTURES.forEach(({ name, base, setup }) => {
                if (!setup) return;
                console.error(`* Setting up ${name} fixture`);
                inDir(base, () => exec(setup, { silent: true }));
            });
        },
        work: ({ sample, fixtureName }, i) => {
            console.error(`* [${i+1}/${tests.length}] ${fixtureName} ${sample}`);

            let fixture = _.find(FIXTURES, { name: fixtureName });
            try {
                runFixtureWithSample(fixture, sample);
            } catch (e) {
                exit(1);
            }
        }
    });
}

function testsInDir(dir) {
    return shell.ls(`${dir}/*.json`);
}

function main(sources) {
    if (sources.length == 0) {
        if (IS_CI && !IS_PR && !IS_BLESSED) {
            return main(testsInDir("app/public/sample/json"));
        } else {
            return main(testsInDir("test/inputs/json"));
        }
    } else if (sources.length == 1 && fs.lstatSync(sources[0]).isDirectory()) {
        return main(testsInDir(sources[0]));
    } else {
        testAll(sources);
    }
}

// skip 2 `node` args
main(process.argv.slice(2));
<commit_msg>How large is the elm fixture?<commit_after>#!/usr/bin/env node

const Ajv = require('ajv');
const strictDeepEquals = require('deep-equal');
const fs = require("fs");
const _ = require("lodash");
const path = require("path");
const shell = require("shelljs");
const deepEquals = require("./deepEquals");
const Main = require("../output/Main");
const Samples = require("../output/Samples");
const assert = require("assert");
const { inParallel } = require("./multicore");
const os = require("os");
const exit = require('exit');

//////////////////////////////////////
// Constants
/////////////////////////////////////

function debug(x) {
    if (!process.env.DEBUG) return;
    console.log(x);
    return x;
}

const IS_CI = process.env.CI === "true";
const BRANCH = process.env.TRAVIS_BRANCH;
const IS_BLESSED = ["master"].indexOf(BRANCH) !== -1;
const IS_PUSH = process.env.TRAVIS_EVENT_TYPE === "push";
const IS_PR = process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== "false";

const CPUs = IS_CI
    ? 2 /* Travis has only 2 but reports 8 */
    : process.env.CPUs || os.cpus().length;

const QUICKTYPE_CLI = path.resolve("./cli/quicktype.js");

const NODE_BIN = path.resolve("./node_modules/.bin");
process.env.PATH += `:${NODE_BIN}`;

//////////////////////////////////////
// Fixtures
/////////////////////////////////////

const FIXTURES = [
    {
        name: "csharp",
        base: "test/csharp",
        setup: "dotnet restore",
        diffViaSchema: false,
        output: "QuickType.cs",
        test: testCSharp
    },
    {
        name: "golang",
        base: "test/golang",
        diffViaSchema: true,
        output: "quicktype.go",
        test: testGo
    },
    {
        name: "json-schema",
        base: "test/golang",
        diffViaSchema: false,
        output: "schema.json",
        test: testJsonSchema
    },
    {
        name: "elm",
        base: "test/elm",
        setup: "elm-make --yes",
        diffViaSchema: false,
        output: "QuickType.elm",
        test: testElm
    }
].filter(({name}) => !process.env.FIXTURE || name === process.env.FIXTURE);

//////////////////////////////////////
// Go tests
/////////////////////////////////////

const knownUnicodeFails = ["identifiers.json"];
const unicodeWillFail = (sample) => knownUnicodeFails.indexOf(path.basename(sample)) !== -1;

function testGo(sample) {
    if (unicodeWillFail(sample)) {
        console.error(`Skipping golang ${sample} – known to fail`);
        return;
    }

    compareJsonFileToJson({
        expectedFile: sample,
        jsonCommand: `go run main.go quicktype.go < "${sample}"`,
        strict: false
    });
}

//////////////////////////////////////
// C# tests
/////////////////////////////////////

function testCSharp(sample) {
    compareJsonFileToJson({
        expectedFile: sample,
        jsonCommand: `dotnet run "${sample}"`,
        strict: false
    });
}

//////////////////////////////////////
// Elm tests
/////////////////////////////////////

function testElm(sample) {
    if (unicodeWillFail(sample)) {
        console.error(`Skipping elm ${sample} – known to fail`);
        return;
    }

    exec("elm-make Main.elm QuickType.elm --output elm.js");
    exec(`node ./runner.js "${sample}"`)
}

//////////////////////////////////////
// JSON Schema tests
/////////////////////////////////////

function testJsonSchema(sample) {
    let input = JSON.parse(fs.readFileSync(sample));
    let schema = JSON.parse(fs.readFileSync("schema.json"));

    let ajv = new Ajv();
    let valid = ajv.validate(schema, input);
    if (!valid) {
        failWith({
            sample,
            error: "Generated schema does not validate input JSON.",
        });
    }

    // Generate Go from the schema
    exec(`quicktype --srcLang json-schema -o quicktype.go --src schema.json`);

    // Possibly check the output of the Go program against the sample
    if (unicodeWillFail(sample)) {
        console.error("Known to fail - not checking output.");
    } else {
        // Parse the sample with Go generated from its schema, and compare to the sample
        compareJsonFileToJson({
            expectedFile: sample,
            jsonCommand: `go run main.go quicktype.go < "${sample}"`,
            strict: false
        });
    }

    // Generate a schema from the schema, making sure the schemas are the same
    compareJsonFileToJson({
        expectedFile: "schema.json",
        jsonCommand: `quicktype --srcLang json-schema --src schema.json --lang json`,
        strict: true
    });
}

//////////////////////////////////////
// Test driver
/////////////////////////////////////

function failWith(obj) {
    obj.cwd = process.cwd();
    console.error(JSON.stringify(obj, null, "  "));
    throw obj;
}

function exec(s, opts, cb) {
    // We special-case quicktype execution
    s = s.replace(/^quicktype /, `node ${QUICKTYPE_CLI} `);

    debug(s);

    let result = shell.exec(s, opts, cb);
    if (result.code !== 0) {
        console.error(result.stdout);
        console.error(result.stderr);
        failWith({
            command: s,
            code: result.code
        });
    }
    return result;
}

function compareJsonFileToJson({expectedFile, jsonFile, jsonCommand, strict}) {
    debug({expectedFile, jsonFile, jsonCommand, strict});

    let jsonString = jsonFile
        ? fs.readFileSync(jsonFile)
        : exec(jsonCommand, {silent: true}).stdout;

    let givenJSON = JSON.parse(jsonString);
    let expectedJSON = JSON.parse(fs.readFileSync(expectedFile));

    let jsonAreEqual = strict
        ? strictDeepEquals(givenJSON, expectedJSON)
        : deepEquals(expectedJSON, givenJSON, []);

    if (!jsonAreEqual) {
        console.error("Error: Output is not equivalent to input.");
        failWith({
            expectedFile,
            jsonCommand,
            jsonFile
        });
    }
}

function inDir(dir, work) {
    let origin = process.cwd();

    debug(`cd ${dir}`)
    process.chdir(dir);

    work();
    process.chdir(origin);
}

function runFixtureWithSample(fixture, sample) {
    let tmp = path.resolve(os.tmpdir(), require("crypto").randomBytes(8).toString('hex'));
    let sampleAbs = path.resolve(sample);

    let stats = fs.statSync(sampleAbs);
    if (stats.size > 32 * 1024 * 1024) {
        console.error(`* Skipping ${sampleAbs} because it's too large`);
        return;
    }

    shell.cp("-R", fixture.base, tmp);

    inDir(tmp, () => {
        // Generate code from the sample
        exec(`quicktype --src ${sampleAbs} --srcLang json -o ${fixture.output}`);

        fixture.test(sampleAbs);

        if (fixture.diffViaSchema) {
            debug("* Diffing with code generated via JSON Schema");
            // Make a schema
            exec(`quicktype --src ${sampleAbs} --srcLang json -o schema.json`);
            // Quicktype from the schema and compare to expected code
            shell.mv(fixture.output, `${fixture.output}.expected`);
            exec(`quicktype --src schema.json --srcLang json-schema -o ${fixture.output}`);

            // Compare fixture.output to fixture.output.expected
            try {
                exec(`diff -Naur ${fixture.output}.expected ${fixture.output}`);
            } catch ({ command }) {
                // FIXME: Set this to fail once we have it working.  See issue #59.
                console.error(`Command failed, but we're allowing it: ${command}`);
            }
        }
    });

    shell.rm("-rf", tmp);
}

function testAll(samples) {
    // Get an array of all { sample, fixtureName } objects we'll run
    let tests =  _
        .chain(FIXTURES)
        .flatMap((fixture) => samples.map((sample) => {
            return { sample, fixtureName: fixture.name };
        }))
        .shuffle()
        .value();

    inParallel({
        queue: tests,
        workers: CPUs,
        setup: () => {
            FIXTURES.forEach(({ name, base, setup }) => {
                if (!setup) return;
                console.error(`* Setting up ${name} fixture`);
                inDir(base, () => exec(setup, { silent: true }));
            });
            exec(`du -h test/elm`);
        },
        work: ({ sample, fixtureName }, i) => {
            console.error(`* [${i+1}/${tests.length}] ${fixtureName} ${sample}`);

            let fixture = _.find(FIXTURES, { name: fixtureName });
            try {
                runFixtureWithSample(fixture, sample);
            } catch (e) {
                exit(1);
            }
        }
    });
}

function testsInDir(dir) {
    return shell.ls(`${dir}/*.json`);
}

function main(sources) {
    if (sources.length == 0) {
        if (IS_CI && !IS_PR && !IS_BLESSED) {
            return main(testsInDir("app/public/sample/json"));
        } else {
            return main(testsInDir("test/inputs/json"));
        }
    } else if (sources.length == 1 && fs.lstatSync(sources[0]).isDirectory()) {
        return main(testsInDir(sources[0]));
    } else {
        testAll(sources);
    }
}

// skip 2 `node` args
main(process.argv.slice(2));
<|endoftext|>


<commit_before>/*jslint node: true  es5: true */

/*
 * #%L
 * %%
 * Copyright (C) 2011 - 2016 BMW Car IT GmbH
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

var log = require("./logging.js").log;
var prettyLog = require("./logging.js").prettyLog;
var joynr = require("joynr");
var LocalStorage = require("node-localstorage").LocalStorage;
var persistencyLocation = "./radioLocalStorageConsumer";
var localStorage = new LocalStorage(persistencyLocation);
var RadioStation;
var Country;
var currentStationSubscriptionId = localStorage.getItem("currentStationSubscriptionId");
var multicastSubscriptionId = localStorage.getItem("multicastSubscriptionId");
var multicastPSubscriptionId = localStorage.getItem("multicastPSubscriptionId");
var subscriptionQosOnChange;

var runDemo = function(radioProxy) {
    prettyLog("ATTRIBUTE GET: currentStation...");
    radioProxy.currentStation.get().catch(function(error) {
        prettyLog("ATTRIBUTE GET: currentStation failed: " + error);
    }).then(function(value) {
        prettyLog("ATTRIBUTE GET: currentStation returned: " + JSON.stringify(value));
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation)...");
        return radioProxy.addFavoriteStation(
            {
                newFavoriteStation : new RadioStation({
                    name : "runDemoFavoriteStation",
                    trafficService : true,
                    country : Country.GERMANY
                })
            }
        );
    }).catch(function(error) {
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation) failed: " + JSON.stringify(error));
    }).then(function() {
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation) returned");
        prettyLog("radioProxy.shuffleStations()...");
        return radioProxy.shuffleStations();
    }).catch(function(error) {
        prettyLog("RPC: radioProxy.shuffleStations() failed: " + JSON.stringify(error));
    }).then(function(value) {
        prettyLog("RPC: radioProxy.shuffleStations() returned");
        prettyLog("ATTRIBUTE GET: currentStation after shuffle...");
        return radioProxy.currentStation.get();
    }).catch(function(error) {
        prettyLog("ATTRIBUTE GET: currentStation failed: " + JSON.stringify(error));
    }).then(function(value) {
        prettyLog("ATTRIBUTE GET: currentStation returned: " + JSON.stringify(value));
    });
};

var runInteractiveConsole =
        function(radioProxy, onDone) {
            var readline = require('readline');
            var rl = readline.createInterface(process.stdin, process.stdout);
            rl.setPrompt('>> ');
            var MODES = {
                HELP : {
                    value : "h",
                    description : "help",
                    options : {}
                },
                QUIT : {
                    value : "q",
                    description : "quit",
                    options : {}
                },
                SHUFFLE_STATIONS : {
                    value : "s",
                    description : "shuffle stations",
                    options : {}
                },
                ADD_FAVORITE_STATION : {
                    value : "a",
                    description : "add a Favorite Station",
                    options : {
                        NAME : "name"
                    }
                },
                SUBSCRIBE : {
                    value : "subscribe",
                    description : "subscribe to current station",
                    options : {}
                },
                MULTICAST : {
                    value : "subscribeMulticast",
                    description : "subscribe to weak signal multicast",
                    options : {}
                },
                MULTICASTP : {
                    value : "subscribeMulticastP",
                    description : "subscribe to weak signal multicast with partition \"GERMANY\"",
                    options : {}
                },
                UNSUBSCRIBE : {
                    value : "unsubscribe",
                    description : "unsubscribe from all subscriptions",
                    options : {}
                },
                GET_CURRENT_STATION : {
                    value : "c",
                    description : "get current station",
                    options : {}
                }
            };

            var showHelp = require("./console_common.js");
            rl.on('line', function(line) {
                var input = line.trim().split(' ');

                function subscribeHelper(settings) {
                    var partitionsString = settings.partitions ? JSON.stringify(settings.partitions) : "";
                    var onReceiveCallback = function onReceiveCallback(value) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.onReceive: "
                                  + JSON.stringify(value));
                    };

                    var onErrorCallback = function onErrorCallback(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.onError: " + error);
                    };

                    var parameters = {
                        subscriptionQos : subscriptionQosOnChange,
                        onReceive : onReceiveCallback,
                        onError : onErrorCallback,
                        partitions : settings.partitions
                    };

                    if (settings.subscriptionId !== undefined) {
                        parameters.subscriptionId = settings.subscriptionId;
                    }
                    return radioProxy[settings.subscribeToName].subscribe(parameters).then(function(subscriptionId) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.done. "
                                  + "Subscription ID: "+ subscriptionId);
                        return subscriptionId;
                    }).catch(function(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.failed: " + JSON.stringify(error));
                        return error;
                    });
                }
                function unsubscribeHelper(settings) {
                    var partitionsString = settings.partitions ? JSON.stringify(settings.partitions) : "";
                    return radioProxy[settings.subscribeToName].unsubscribe({
                        subscriptionId : settings.subscriptionId
                    }).then(function() {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".unsubscribe.done. "
                                  + "Subscription ID: " + settings.subscriptionId);
                        return null;
                    }).catch(function(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".unsubscribe.fail. "
                                  + "Subscription ID: " + settings.subscriptionId + " ERROR: " + error
                        );
                        return null;
                    });
                }
                switch (input[0]) {
                    case MODES.HELP.value:
                        showHelp(MODES);
                        break;
                    case MODES.QUIT.value:
                        rl.close();
                        break;
                    case MODES.ADD_FAVORITE_STATION.value:
                        var newFavoriteStation = new RadioStation({
                            name : input[1] || "",
                            trafficService : true,
                            country : Country.GERMANY
                        });

                        radioProxy.addFavoriteStation({newFavoriteStation: newFavoriteStation})
                            .then(
                                function(returnValue) {
                                    prettyLog("RPC: radioProxy.addFavoriteStation("
                                        + JSON.stringify(newFavoriteStation)
                                        + ") returned: "
                                        + JSON.stringify(returnValue));
                                    return returnValue;
                                }).catch(
                                function(error) {
                                    prettyLog("RPC: radioProxy.addFavoriteStation("
                                        + JSON.stringify(newFavoriteStation)
                                        + ") failed: "
                                        + error);
                                    return error;
                                });
                        break;
                    case MODES.SHUFFLE_STATIONS.value:
                        radioProxy.shuffleStations().then(function() {
                            prettyLog("RPC: radioProxy.shuffleStations returned. ");
                            return null;
                        }).catch(function(error) {
                            prettyLog("RPC: radioProxy.shuffleStations failed: " + JSON.stringify(error));
                            return error;
                       });
                        break;
                    case MODES.SUBSCRIBE.value:
                        subscribeHelper({
                            subscribeToName : "currentStation",
                            subscriptionId : currentStationSubscriptionId
                        }).then(function(suscriptionId) {
                            currentStationSubscriptionId = suscriptionId;
                            localStorage.setItem("currentStationSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.MULTICAST.value:
                        subscribeHelper({
                            subscribeToName : "weakSignal",
                            subscriptionId : multicastSubscriptionId
                        }).then(function(suscriptionId) {
                            multicastSubscriptionId = suscriptionId;
                            localStorage.setItem("multicastSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.MULTICASTP.value:
                        subscribeHelper({
                            subscribeToName : "weakSignal",
                            partitions: [ "GERMANY" ],
                            subscriptionId : multicastPSubscriptionId
                        }).then(function(suscriptionId) {
                            multicastPSubscriptionId = suscriptionId;
                            localStorage.setItem("multicastPSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.UNSUBSCRIBE.value:
                        if (currentStationSubscriptionId !== undefined && currentStationSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "currentStation",
                                subscriptionId: currentStationSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(currentStationSubscriptionId);
                                currentStationSubscriptionId = undefined;
                            });
                        }
                        if (multicastSubscriptionId !== undefined && multicastSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "weakSignal",
                                subscriptionId: multicastSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(multicastSubscriptionId);
                                multicastSubscriptionId = undefined;
                            });
                        }
                        if (multicastPSubscriptionId !== undefined && multicastPSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "weakSignal",
                                partitions: [ "GERMANY" ],
                                subscriptionId: multicastPSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(multicastPSubscriptionId);
                                multicastPSubscriptionId = undefined;
                            });
                        }
                        break;
                    case MODES.GET_CURRENT_STATION.value:
                        radioProxy.currentStation.get().then(function(currentStation) {
                            prettyLog("RPC: radioProxy.getCurrentStation returned: " + JSON.stringify(currentStation));
                        }).catch(function(error) {
                            prettyLog("RPC: radioProxy.getCurrentStation failed: " + error);
                        });
                        break;
                    case '':
                        break;
                    default:
                        log('unknown input: ' + input);
                        break;
                }
                rl.prompt();
            });

            rl.on('close', function() {
                if (onDone) {
                    onDone();
                }
            });

            showHelp(MODES);
            rl.prompt();
        };

if (process.env.domain === undefined) {
    log("please pass a domain as argument");
    process.exit(0);
}
var domain = process.env.domain;
log("domain: " + domain);

var provisioning = require("./provisioning_common.js");

provisioning.persistency = {
    //clearPersistency : true,
    location : persistencyLocation
};

if (process.env.runtime !== undefined) {
    if (process.env.runtime === "inprocess") {
        provisioning.brokerUri = process.env.brokerUri;
        provisioning.bounceProxyBaseUrl = process.env.bounceProxyBaseUrl;
        provisioning.bounceProxyUrl = provisioning.bounceProxyBaseUrl + "/bounceproxy/";
        joynr.selectRuntime("inprocess");
    } else if (process.env.runtime === "websocket") {
        provisioning.ccAddress.host = process.env.cchost;
        provisioning.ccAddress.port = process.env.ccport;
        joynr.selectRuntime("websocket.libjoynr");
    }
}

RadioStation = require("../generated/js/joynr/vehicle/RadioStation");
Country = require("../generated/js/joynr/vehicle/Country");
require("../generated/js/joynr/vehicle/GeoPosition");
var AddFavoriteStationErrorEnum = require("../generated/js/joynr/vehicle/Radio/AddFavoriteStationErrorEnum");
var RadioProxy = require("../generated/js/joynr/vehicle/RadioProxy.js");
joynr.load(provisioning).then(function(loadedJoynr) {
    log("joynr started");
    joynr = loadedJoynr;
    var messagingQos = new joynr.messaging.MessagingQos({
        ttl : 60000
    });

    subscriptionQosOnChange = new joynr.proxy.OnChangeSubscriptionQos({
        minIntervalMs : 50
	});

    joynr.proxyBuilder.build(RadioProxy, {
        domain : domain,
        messagingQos : messagingQos
    }).then(function(radioProxy) {
        log("radio proxy build");
        runDemo(radioProxy);
        runInteractiveConsole(radioProxy, function() {
            log("exiting...");
            process.exit(0);
        });
        return radioProxy;
    }).catch(function(error) {
        log("error running radioProxy: " + error);
    });
    return loadedJoynr;
}).catch(function(error){
    throw error;
});
<commit_msg>[JS] radio-node consumer now shuts down runtime<commit_after>/*jslint node: true  es5: true */

/*
 * #%L
 * %%
 * Copyright (C) 2011 - 2016 BMW Car IT GmbH
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

var log = require("./logging.js").log;
var prettyLog = require("./logging.js").prettyLog;
var joynr = require("joynr");
var LocalStorage = require("node-localstorage").LocalStorage;
var persistencyLocation = "./radioLocalStorageConsumer";
var localStorage = new LocalStorage(persistencyLocation);
var RadioStation;
var Country;
var currentStationSubscriptionId = localStorage.getItem("currentStationSubscriptionId");
var multicastSubscriptionId = localStorage.getItem("multicastSubscriptionId");
var multicastPSubscriptionId = localStorage.getItem("multicastPSubscriptionId");
var subscriptionQosOnChange;

var runDemo = function(radioProxy) {
    prettyLog("ATTRIBUTE GET: currentStation...");
    radioProxy.currentStation.get().catch(function(error) {
        prettyLog("ATTRIBUTE GET: currentStation failed: " + error);
    }).then(function(value) {
        prettyLog("ATTRIBUTE GET: currentStation returned: " + JSON.stringify(value));
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation)...");
        return radioProxy.addFavoriteStation(
            {
                newFavoriteStation : new RadioStation({
                    name : "runDemoFavoriteStation",
                    trafficService : true,
                    country : Country.GERMANY
                })
            }
        );
    }).catch(function(error) {
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation) failed: " + JSON.stringify(error));
    }).then(function() {
        prettyLog("RPC: radioProxy.addFavoriteStation(radioStation) returned");
        prettyLog("radioProxy.shuffleStations()...");
        return radioProxy.shuffleStations();
    }).catch(function(error) {
        prettyLog("RPC: radioProxy.shuffleStations() failed: " + JSON.stringify(error));
    }).then(function(value) {
        prettyLog("RPC: radioProxy.shuffleStations() returned");
        prettyLog("ATTRIBUTE GET: currentStation after shuffle...");
        return radioProxy.currentStation.get();
    }).catch(function(error) {
        prettyLog("ATTRIBUTE GET: currentStation failed: " + JSON.stringify(error));
    }).then(function(value) {
        prettyLog("ATTRIBUTE GET: currentStation returned: " + JSON.stringify(value));
    });
};

var runInteractiveConsole =
        function(radioProxy, onDone) {
            var readline = require('readline');
            var rl = readline.createInterface(process.stdin, process.stdout);
            rl.setPrompt('>> ');
            var MODES = {
                HELP : {
                    value : "h",
                    description : "help",
                    options : {}
                },
                QUIT : {
                    value : "q",
                    description : "quit",
                    options : {}
                },
                SHUFFLE_STATIONS : {
                    value : "s",
                    description : "shuffle stations",
                    options : {}
                },
                ADD_FAVORITE_STATION : {
                    value : "a",
                    description : "add a Favorite Station",
                    options : {
                        NAME : "name"
                    }
                },
                SUBSCRIBE : {
                    value : "subscribe",
                    description : "subscribe to current station",
                    options : {}
                },
                MULTICAST : {
                    value : "subscribeMulticast",
                    description : "subscribe to weak signal multicast",
                    options : {}
                },
                MULTICASTP : {
                    value : "subscribeMulticastP",
                    description : "subscribe to weak signal multicast with partition \"GERMANY\"",
                    options : {}
                },
                UNSUBSCRIBE : {
                    value : "unsubscribe",
                    description : "unsubscribe from all subscriptions",
                    options : {}
                },
                GET_CURRENT_STATION : {
                    value : "c",
                    description : "get current station",
                    options : {}
                }
            };

            var showHelp = require("./console_common.js");
            rl.on('line', function(line) {
                var input = line.trim().split(' ');

                function subscribeHelper(settings) {
                    var partitionsString = settings.partitions ? JSON.stringify(settings.partitions) : "";
                    var onReceiveCallback = function onReceiveCallback(value) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.onReceive: "
                                  + JSON.stringify(value));
                    };

                    var onErrorCallback = function onErrorCallback(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.onError: " + error);
                    };

                    var parameters = {
                        subscriptionQos : subscriptionQosOnChange,
                        onReceive : onReceiveCallback,
                        onError : onErrorCallback,
                        partitions : settings.partitions
                    };

                    if (settings.subscriptionId !== undefined) {
                        parameters.subscriptionId = settings.subscriptionId;
                    }
                    return radioProxy[settings.subscribeToName].subscribe(parameters).then(function(subscriptionId) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.done. "
                                  + "Subscription ID: "+ subscriptionId);
                        return subscriptionId;
                    }).catch(function(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".subscribe.failed: " + JSON.stringify(error));
                        return error;
                    });
                }
                function unsubscribeHelper(settings) {
                    var partitionsString = settings.partitions ? JSON.stringify(settings.partitions) : "";
                    return radioProxy[settings.subscribeToName].unsubscribe({
                        subscriptionId : settings.subscriptionId
                    }).then(function() {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".unsubscribe.done. "
                                  + "Subscription ID: " + settings.subscriptionId);
                        return null;
                    }).catch(function(error) {
                        prettyLog("radioProxy." + settings.subscribeToName + partitionsString + ".unsubscribe.fail. "
                                  + "Subscription ID: " + settings.subscriptionId + " ERROR: " + error
                        );
                        return null;
                    });
                }
                switch (input[0]) {
                    case MODES.HELP.value:
                        showHelp(MODES);
                        break;
                    case MODES.QUIT.value:
                        rl.close();
                        break;
                    case MODES.ADD_FAVORITE_STATION.value:
                        var newFavoriteStation = new RadioStation({
                            name : input[1] || "",
                            trafficService : true,
                            country : Country.GERMANY
                        });

                        radioProxy.addFavoriteStation({newFavoriteStation: newFavoriteStation})
                            .then(
                                function(returnValue) {
                                    prettyLog("RPC: radioProxy.addFavoriteStation("
                                        + JSON.stringify(newFavoriteStation)
                                        + ") returned: "
                                        + JSON.stringify(returnValue));
                                    return returnValue;
                                }).catch(
                                function(error) {
                                    prettyLog("RPC: radioProxy.addFavoriteStation("
                                        + JSON.stringify(newFavoriteStation)
                                        + ") failed: "
                                        + error);
                                    return error;
                                });
                        break;
                    case MODES.SHUFFLE_STATIONS.value:
                        radioProxy.shuffleStations().then(function() {
                            prettyLog("RPC: radioProxy.shuffleStations returned. ");
                            return null;
                        }).catch(function(error) {
                            prettyLog("RPC: radioProxy.shuffleStations failed: " + JSON.stringify(error));
                            return error;
                       });
                        break;
                    case MODES.SUBSCRIBE.value:
                        subscribeHelper({
                            subscribeToName : "currentStation",
                            subscriptionId : currentStationSubscriptionId
                        }).then(function(suscriptionId) {
                            currentStationSubscriptionId = suscriptionId;
                            localStorage.setItem("currentStationSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.MULTICAST.value:
                        subscribeHelper({
                            subscribeToName : "weakSignal",
                            subscriptionId : multicastSubscriptionId
                        }).then(function(suscriptionId) {
                            multicastSubscriptionId = suscriptionId;
                            localStorage.setItem("multicastSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.MULTICASTP.value:
                        subscribeHelper({
                            subscribeToName : "weakSignal",
                            partitions: [ "GERMANY" ],
                            subscriptionId : multicastPSubscriptionId
                        }).then(function(suscriptionId) {
                            multicastPSubscriptionId = suscriptionId;
                            localStorage.setItem("multicastPSubscriptionId", suscriptionId);
                        });
                        break;
                    case MODES.UNSUBSCRIBE.value:
                        if (currentStationSubscriptionId !== undefined && currentStationSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "currentStation",
                                subscriptionId: currentStationSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(currentStationSubscriptionId);
                                currentStationSubscriptionId = undefined;
                            });
                        }
                        if (multicastSubscriptionId !== undefined && multicastSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "weakSignal",
                                subscriptionId: multicastSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(multicastSubscriptionId);
                                multicastSubscriptionId = undefined;
                            });
                        }
                        if (multicastPSubscriptionId !== undefined && multicastPSubscriptionId !== null) {
                            unsubscribeHelper({
                                subscribeToName: "weakSignal",
                                partitions: [ "GERMANY" ],
                                subscriptionId: multicastPSubscriptionId
                            }).then(function() {
                                localStorage.removeItem(multicastPSubscriptionId);
                                multicastPSubscriptionId = undefined;
                            });
                        }
                        break;
                    case MODES.GET_CURRENT_STATION.value:
                        radioProxy.currentStation.get().then(function(currentStation) {
                            prettyLog("RPC: radioProxy.getCurrentStation returned: " + JSON.stringify(currentStation));
                        }).catch(function(error) {
                            prettyLog("RPC: radioProxy.getCurrentStation failed: " + error);
                        });
                        break;
                    case '':
                        break;
                    default:
                        log('unknown input: ' + input);
                        break;
                }
                rl.prompt();
            });

            rl.on('close', function() {
                if (onDone) {
                    onDone();
                }
            });

            showHelp(MODES);
            rl.prompt();
        };

if (process.env.domain === undefined) {
    log("please pass a domain as argument");
    process.exit(0);
}
var domain = process.env.domain;
log("domain: " + domain);

var provisioning = require("./provisioning_common.js");

provisioning.persistency = {
    //clearPersistency : true,
    location : persistencyLocation
};

if (process.env.runtime !== undefined) {
    if (process.env.runtime === "inprocess") {
        provisioning.brokerUri = process.env.brokerUri;
        provisioning.bounceProxyBaseUrl = process.env.bounceProxyBaseUrl;
        provisioning.bounceProxyUrl = provisioning.bounceProxyBaseUrl + "/bounceproxy/";
        joynr.selectRuntime("inprocess");
    } else if (process.env.runtime === "websocket") {
        provisioning.ccAddress.host = process.env.cchost;
        provisioning.ccAddress.port = process.env.ccport;
        joynr.selectRuntime("websocket.libjoynr");
    }
}

RadioStation = require("../generated/js/joynr/vehicle/RadioStation");
Country = require("../generated/js/joynr/vehicle/Country");
require("../generated/js/joynr/vehicle/GeoPosition");
var AddFavoriteStationErrorEnum = require("../generated/js/joynr/vehicle/Radio/AddFavoriteStationErrorEnum");
var RadioProxy = require("../generated/js/joynr/vehicle/RadioProxy.js");
joynr.load(provisioning).then(function(loadedJoynr) {
    log("joynr started");
    joynr = loadedJoynr;
    var messagingQos = new joynr.messaging.MessagingQos({
        ttl : 60000
    });

    subscriptionQosOnChange = new joynr.proxy.OnChangeSubscriptionQos({
        minIntervalMs : 50
	});

    joynr.proxyBuilder.build(RadioProxy, {
        domain : domain,
        messagingQos : messagingQos
    }).then(function(radioProxy) {
        log("radio proxy build");
        runDemo(radioProxy);
        runInteractiveConsole(radioProxy, function() {
            log("exiting...");
            joynr.shutdown().then(function() {
                log("shutdown completed...");
                process.exit(0);
            });
        });
        return radioProxy;
    }).catch(function(error) {
        log("error running radioProxy: " + error);
    });
    return loadedJoynr;
}).catch(function(error){
    throw error;
});
<|endoftext|>


<commit_before>'use strict';

var Sequelize = require('sequelize');
var diff = require('deep-diff').diff;
var jsdiff = require('diff');
var _ = require('lodash');
var helpers = require('./helpers');
var cls = require('continuation-local-storage');

var failHard = false;

exports.init = function (sequelize, optionsArg) {
  // In case that options is being parsed as a readonly attribute.
  var options = _.cloneDeep(optionsArg);
  var defaultAttributes = {
    documentId: 'documentId',
    revisionId: 'revisionId'
  };

  if (optionsArg.defaultAttributes) {
    if (optionsArg.defaultAttributes.documentId) {
      defaultAttributes.documentId = optionsArg.defaultAttributes.documentId;
    }
    if (optionsArg.defaultAttributes.revisionId) {
      defaultAttributes.revisionId = optionsArg.defaultAttributes.revisionId;
    }
  }

  // if no options are passed the function
  if (!options) {
    options = {};
  }
  // enable debug logging
  var debug = false;
  if (options.debug) {
    debug = options.debug;
  }

  // TODO: implement logging option
  var log = options.log || console.log;

  // show the current sequelize and options objects
  if (debug) {
    // log('sequelize object:');
    // log(sequelize);
    log('options object:');
    log(options);
  }

  // attribute name for revision number in the models
  if (!options.revisionAttribute) {
    options.revisionAttribute = "revision";
  }

  // fields we want to exclude from audit trails
  if (!options.exclude) {
    options.exclude = ["id", "createdAt", "updatedAt", "deletedAt", // if the model is paranoid
      "created_at", "updated_at", "deleted_at", options.revisionAttribute];
  }

  // model name for revision table
  if (!options.revisionModel) {
    options.revisionModel = "Revision";
  }

  // model name for revision changes tables
  if (!options.revisionChangeModel) {
    options.revisionChangeModel = "RevisionChange";
  }

  if (!options.enableRevisionChangeModel) {
    options.enableRevisionChangeModel = false;
  }
  // support UUID for postgresql
  if (options.UUID === undefined) {
    options.UUID = false;
  }

  // underscored created and updated attributes
  if (!options.underscored) {
    options.underscored = false;
  }

  if (!options.underscoredAttributes) {
    options.underscoredAttributes = false;
    options.defaultAttributes = defaultAttributes;
  } else {
    options.defaultAttributes = helpers.toUnderscored(defaultAttributes);
  }

  // To track the user that made the changes
  if (!options.userModel) {
    options.userModel = false;
  }

  // full revisions or compressed revisions (track only the difference in models)
  // default: full revisions
  if (!options.enableCompression) {
    options.enableCompression = false;
  }

  // add the column to the database if it doesn't exist
  if (!options.enableMigration) {
    options.enableMigration = false;
  }

  // enable strict diff
  // when true: 10 !== '10'
  // when false: 10 == '10'
  // default: true
  if (!options.enableStrictDiff) {
    options.enableStrictDiff = true;
  }

  var ns;
  if (options.continuationNamespace) {
    ns = cls.getNamespace(options.continuationNamespace);
    if (!ns) {
      ns = cls.createNamespace(options.continuationNamespace);
    }

    if (!options.continuationKey) {
      options.continuationKey = 'userId';
    }

    if (!options.extraDataContinuationKey) {
      options.extraDataContinuationKey = 'extraData';
    }
  }

  if (debug) {
    log('parsed options:');
    log(options);
  }

  // order in which sequelize processes the hooks
  // (1)
  // beforeBulkCreate(instances, options, fn)
  // beforeBulkDestroy(instances, options, fn)
  // beforeBulkUpdate(instances, options, fn)
  // (2)
  // beforeValidate(instance, options, fn)
  // (-)
  // validate
  // (3)
  // afterValidate(instance, options, fn)
  // - or -
  // validationFailed(instance, options, error, fn)
  // (4)
  // beforeCreate(instance, options, fn)
  // beforeDestroy(instance, options, fn)
  // beforeUpdate(instance, options, fn)
  // (-)
  // create
  // destroy
  // update
  // (5)
  // afterCreate(instance, options, fn)
  // afterDestroy(instance, options, fn)
  // afterUpdate(instance, options, fn)
  // (6)
  // afterBulkCreate(instances, options, fn)
  // afterBulkDestroy(instances, options, fn)
  // afterBulkUpdate(instances, options, fn)

  // Extend model prototype with "hasPaperTrail" function
  // Call model.hasPaperTrail() to enable revisions for model
  _.extend(sequelize.Model, {
    hasPaperTrail: function hasPaperTrail() {
      if (debug) {
        log('Enabling paper trail on', this.name);
      }

      this.attributes[options.revisionAttribute] = {
        type: Sequelize.INTEGER,
        defaultValue: 0
      };
      this.revisionable = true;
      this.refreshAttributes();

      if (options.enableMigration) {
        var tableName = this.getTableName();
        sequelize.getQueryInterface().describeTable(tableName).then(function (attributes) {
          if (!attributes[options.revisionAttribute]) {
            if (debug) {
              log('adding revision attribute to the database');
            }
            sequelize.getQueryInterface().addColumn(tableName, options.revisionAttribute, {
              type: Sequelize.INTEGER,
              defaultValue: 0
            }).then(function () {
              return null;
            }).catch(function (err) {
              log('something went really wrong..');
              log(err);
              return null;
            });
          }
          return null;
        });
      }

      this.addHook("beforeCreate", createBeforeHook('create'));
      this.addHook("beforeUpdate", createBeforeHook('update'));
      this.addHook("beforeDestroy", createBeforeHook('destroy'));
      this.addHook("afterCreate", createAfterHook('create'));
      this.addHook("afterUpdate", createAfterHook('update'));
      this.addHook("afterDestroy", createAfterHook('destroy'));

      // create association
      return this.hasMany(sequelize.models[options.revisionModel], {
        foreignKey: options.defaultAttributes.documentId,
        constraints: false,
        scope: {
          model: this.name
        }
      });
    }
  });

  function createBeforeHook(operation) {
    var beforeHook = function beforeHook(instance, opt) {
      if (opt.noPaperTrail) {
        if (debug) {
          log('noPaperTrail opt: is true, not logging');
        }
        return;
      }

      if (debug) {
        log('beforeHook called');
        log('instance:');
        log(instance);
        log('opt:');
        log(opt);
      }

      var destroyOperation = operation === 'destroy';

      if (!destroyOperation && options.enableCompression) {
        var previousVersion = {};
        var currentVersion = {};

        _.forEach(opt.defaultFields, function (a) {
          const previousDataValuesOfA = instance._previousDataValues[a] === undefined ? null : instance._previousDataValues[a];
          previousVersion[a] = instance._previousDataValues[a];
          previousVersion[a] = instance._previousDataValues[a];
          currentVersion[a] = instance.dataValues[a];
          currentVersion[a] = instance.dataValues[a];
        });
      } else {
        var previousVersion = instance._previousDataValues;
        var currentVersion = instance.dataValues;
      }
      // Supported nested models.
      previousVersion = _.omitBy(previousVersion, function (i) {
        return i != null && typeof i === 'object' && !(i instanceof Date);
      });
      previousVersion = _.omit(previousVersion, options.exclude);
      currentVersion = _.omitBy(currentVersion, function (i) {
        return i != null && typeof i === 'object' && !(i instanceof Date);
      });
      currentVersion = _.omit(currentVersion, options.exclude);

      // Disallow change of revision
      instance.set(options.revisionAttribute, instance._previousDataValues[options.revisionAttribute]);

      // Get diffs
      var delta = helpers.calcDelta(previousVersion, currentVersion, options.exclude, options.enableStrictDiff);
      var currentRevisionId = instance.get(options.revisionAttribute);
      if (failHard && !currentRevisionId && opt.type === 'UPDATE') {
        throw new Error('Revision Id was undefined');
      }
      if (debug) {
        log('delta:');
        log(delta);
        log('revisionId', currentRevisionId);
      }

      // Check if all required fields have been provided to the opts / CLS
      if (options.extraDataFields) {
        // get all required field keys as an array
        var requiredFields = _.keys(_.pickBy(options.extraDataFields, function (required) {
          return required;
        }));
        if (requiredFields && requiredFields.length) {
          var extraData = ns && ns.get(options.extraDataContinuationKey) || options.extraData;
          var requiredFieldsProvided = _.filter(requiredFields, function(field) {
            return extraData[field] !== undefined;
          });
          if (requiredFieldsProvided.length !== requiredFields.length) {
            log('Required fields: ', options.extraDataFields, requiredFields);
            log('Required fields provided: ', extraData, requiredFieldsProvided);
            throw new Error('Not all required fields are provided to paper trail!');
          }
        }
      }

      if (destroyOperation || delta && delta.length > 0) {
        var revisionId = (currentRevisionId || 0) + 1;
        instance.set(options.revisionAttribute, revisionId);

        if (!instance.context) {
          instance.context = {};
        }
        instance.context.delta = delta;
      }
      if (debug) {
        log('end of beforeHook');
      }
    };
    return beforeHook;
  }

  function createAfterHook(operation) {
    var afterHook = function afterHook(instance, opt) {
      if (opt.noPaperTrail) {
        if (debug) {
          log('noPaperTrail opt: is true, not logging');
        }
        return;
      }

      if (debug) {
        log('afterHook called');
        log('instance:', instance);
        log('opt:', opt);
        if (ns) {
          log(`CLS ${options.continuationKey}:`, ns.get(options.continuationKey));
          log(`CLS ${options.extraDataFields}:`, ns.get(options.extraDataContinuationKey));
        }
      }

      var destroyOperation = operation === 'destroy';

      if (instance.context && (instance.context.delta && instance.context.delta.length > 0 || destroyOperation)) {
        var Revision = sequelize.model(options.revisionModel);
        if (options.enableRevisionChangeModel) {
          var RevisionChange = sequelize.model(options.revisionChangeModel);
        }
        var delta = instance.context.delta;

        if (!destroyOperation && options.enableCompression) {
          var previousVersion = {};
          var currentVersion = {};

          _.forEach(opt.defaultFields, function (a) {
            previousVersion[a] = instance._previousDataValues[a];
            currentVersion[a] = instance.dataValues[a];
          });
        } else {
          var previousVersion = instance._previousDataValues;
          var currentVersion = instance.dataValues;
        }

        // Supported nested models.
        previousVersion = _.omitBy(previousVersion, function (i) {
          return i != null && typeof i === 'object' && !(i instanceof Date);
        });
        previousVersion = _.omit(previousVersion, options.exclude);
        currentVersion = _.omitBy(currentVersion, function (i) {
          return i != null && typeof i === 'object' && !(i instanceof Date);
        });
        currentVersion = _.omit(currentVersion, options.exclude);

        if (failHard && !ns.get(options.continuationKey)) {
          throw new Error('The CLS continuationKey ' + options.continuationKey + ' was not defined.');
        }

        var document = currentVersion;

        if (options.mysql) {
          document = JSON.stringify(document);
        }

        // Build revision
        var query = {
          model: this.name,
          document: document,
          userId: ns && ns.get(options.continuationKey) || opt['userId'],
          operation: operation
        };
        query[options.defaultAttributes.documentId] = instance.id;

        // Add all extra data fields to the query object
        if (options.extraDataFields) {
          var extraData = ns && ns.get(options.extraDataContinuationKey) || opt['extraData'];
          if (extraData) {
            _.forEach(options.extraDataFields, function (required, field) {
              var value = extraData[field];
              if (debug) {
                log(`Adding extraData field to Revision - ${field} => ${value}`);
              }
              if (!query.hasOwnProperty(field)) {
                query[field] = value;
              } else if (debug) {
                log(`Revision object already has a value at ${field} => ${query[field]}`);
                log('Not overwriting the original value');
              }
            });
          }
        }

        var revision = Revision.build(query);

        revision[options.revisionAttribute] = instance.get(options.revisionAttribute);

        // Save revision
        return revision.save({transaction: opt.transaction}).then(function (revision) {
          // Loop diffs and create a revision-diff for each
          if (options.enableRevisionChangeModel) {
            _.forEach(delta, function (difference) {
              var o = helpers.diffToString(difference.item ? difference.item.lhs : difference.lhs);
              var n = helpers.diffToString(difference.item ? difference.item.rhs : difference.rhs);

              var document = difference;
              var diff = o || n ? jsdiff.diffChars(o, n) : []

              if (options.mysql) {
                document = JSON.stringify(document);
                diff = JSON.stringify(diff)
              }

              var d = RevisionChange.build({
                path: difference.path[0],
                document: document,
                revisionId: revision.id,
                diff: diff
              });

              d.save({transaction: opt.transaction}).then(function (d) {
                // Add diff to revision
                revision['add' + helpers.capitalizeFirstLetter(options.revisionChangeModel)](d);

                return null;
              }).catch(function (err) {
                log('RevisionChange save error');
                log(err);
                throw err;
              });
            });
          }
          return null;
        }).catch(function (err) {
          log('Revision save error');
          log(err);
          throw err;
        });
      }

      if (debug) {
        log('end of afterHook');
      }
    };
    return afterHook;
  };

  return {
    // Return defineModels()
    defineModels: function defineModels(db) {
      var attributes = {
        model: {
          type: Sequelize.TEXT,
          allowNull: false
        },
        document: {
          type: Sequelize.JSON,
          allowNull: false
        },
        operation: Sequelize.STRING(7)
      };

      if (options.mysql) {
        attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
      }

      attributes[options.defaultAttributes.documentId] = {
        type: Sequelize.INTEGER,
        allowNull: false
      };

      attributes[options.revisionAttribute] = {
        type: Sequelize.INTEGER,
        allowNull: false
      };

      if (debug) {
        log('attributes');
        log(attributes);
      }
      if (options.UUID) {
        attributes.id = {
          primaryKey: true,
          type: Sequelize.UUID,
          defaultValue: Sequelize.UUIDV4
        };
        attributes[options.defaultAttributes.documentId].type = Sequelize.UUID;
      }
      // Revision model
      var Revision = sequelize.define(options.revisionModel, attributes, {
        underscored: options.underscored
      });
      Revision.associate = function associate(models) {
        Revision.belongsTo(sequelize.model(options.userModel), options.belongsToUserOptions);
      };

      attributes = {
        path: {
          type: Sequelize.TEXT,
          allowNull: false
        },
        document: {
          type: Sequelize.JSON,
          allowNull: false
        },
        diff: {
          type: Sequelize.JSON,
          allowNull: false
        }
      };
      if (options.mysql) {
        attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
        attributes.diff.type = Sequelize.TEXT('MEDIUMTEXT');
      }
      if (options.UUID) {
        attributes.id = {
          primaryKey: true,
          type: Sequelize.UUID,
          defaultValue: Sequelize.UUIDV4
        };
      }

      if (options.enableRevisionChangeModel) {
        // RevisionChange model
        var RevisionChange = sequelize.define(options.revisionChangeModel, attributes, {
          underscored: options.underscored
        });

        // Set associations
        Revision.hasMany(RevisionChange, {
          foreignKey: options.defaultAttributes.revisionId,
          constraints: false
        });

        RevisionChange.belongsTo(Revision);
        if (db) db[RevisionChange.name] = RevisionChange;
      }

      if (db) db[Revision.name] = Revision;

      if (options.userModel) {
        Revision.belongsTo(sequelize.model(options.userModel), options.belongsToUserOptions);
      }
      return Revision;
    }
  };
};

/**
 * Throw exceptions when the user identifier from CLS is not set or if the
 * revisionAttribute was not loaded on the model.
 */
exports.enableFailHard = function () {
  failHard = true;
},

module.exports = exports;
<commit_msg>Revert "Update lib/index.js"<commit_after>'use strict';

var Sequelize = require('sequelize');
var diff = require('deep-diff').diff;
var jsdiff = require('diff');
var _ = require('lodash');
var helpers = require('./helpers');
var cls = require('continuation-local-storage');

var failHard = false;

exports.init = function (sequelize, optionsArg) {
  // In case that options is being parsed as a readonly attribute.
  var options = _.cloneDeep(optionsArg);
  var defaultAttributes = {
    documentId: 'documentId',
    revisionId: 'revisionId'
  };

  if (optionsArg.defaultAttributes) {
    if (optionsArg.defaultAttributes.documentId) {
      defaultAttributes.documentId = optionsArg.defaultAttributes.documentId;
    }
    if (optionsArg.defaultAttributes.revisionId) {
      defaultAttributes.revisionId = optionsArg.defaultAttributes.revisionId;
    }
  }

  // if no options are passed the function
  if (!options) {
    options = {};
  }
  // enable debug logging
  var debug = false;
  if (options.debug) {
    debug = options.debug;
  }

  // TODO: implement logging option
  var log = options.log || console.log;

  // show the current sequelize and options objects
  if (debug) {
    // log('sequelize object:');
    // log(sequelize);
    log('options object:');
    log(options);
  }

  // attribute name for revision number in the models
  if (!options.revisionAttribute) {
    options.revisionAttribute = "revision";
  }

  // fields we want to exclude from audit trails
  if (!options.exclude) {
    options.exclude = ["id", "createdAt", "updatedAt", "deletedAt", // if the model is paranoid
      "created_at", "updated_at", "deleted_at", options.revisionAttribute];
  }

  // model name for revision table
  if (!options.revisionModel) {
    options.revisionModel = "Revision";
  }

  // model name for revision changes tables
  if (!options.revisionChangeModel) {
    options.revisionChangeModel = "RevisionChange";
  }

  if (!options.enableRevisionChangeModel) {
    options.enableRevisionChangeModel = false;
  }
  // support UUID for postgresql
  if (options.UUID === undefined) {
    options.UUID = false;
  }

  // underscored created and updated attributes
  if (!options.underscored) {
    options.underscored = false;
  }

  if (!options.underscoredAttributes) {
    options.underscoredAttributes = false;
    options.defaultAttributes = defaultAttributes;
  } else {
    options.defaultAttributes = helpers.toUnderscored(defaultAttributes);
  }

  // To track the user that made the changes
  if (!options.userModel) {
    options.userModel = false;
  }

  // full revisions or compressed revisions (track only the difference in models)
  // default: full revisions
  if (!options.enableCompression) {
    options.enableCompression = false;
  }

  // add the column to the database if it doesn't exist
  if (!options.enableMigration) {
    options.enableMigration = false;
  }

  // enable strict diff
  // when true: 10 !== '10'
  // when false: 10 == '10'
  // default: true
  if (!options.enableStrictDiff) {
    options.enableStrictDiff = true;
  }

  var ns;
  if (options.continuationNamespace) {
    ns = cls.getNamespace(options.continuationNamespace);
    if (!ns) {
      ns = cls.createNamespace(options.continuationNamespace);
    }

    if (!options.continuationKey) {
      options.continuationKey = 'userId';
    }

    if (!options.extraDataContinuationKey) {
      options.extraDataContinuationKey = 'extraData';
    }
  }

  if (debug) {
    log('parsed options:');
    log(options);
  }

  // order in which sequelize processes the hooks
  // (1)
  // beforeBulkCreate(instances, options, fn)
  // beforeBulkDestroy(instances, options, fn)
  // beforeBulkUpdate(instances, options, fn)
  // (2)
  // beforeValidate(instance, options, fn)
  // (-)
  // validate
  // (3)
  // afterValidate(instance, options, fn)
  // - or -
  // validationFailed(instance, options, error, fn)
  // (4)
  // beforeCreate(instance, options, fn)
  // beforeDestroy(instance, options, fn)
  // beforeUpdate(instance, options, fn)
  // (-)
  // create
  // destroy
  // update
  // (5)
  // afterCreate(instance, options, fn)
  // afterDestroy(instance, options, fn)
  // afterUpdate(instance, options, fn)
  // (6)
  // afterBulkCreate(instances, options, fn)
  // afterBulkDestroy(instances, options, fn)
  // afterBulkUpdate(instances, options, fn)

  // Extend model prototype with "hasPaperTrail" function
  // Call model.hasPaperTrail() to enable revisions for model
  _.extend(sequelize.Model, {
    hasPaperTrail: function hasPaperTrail() {
      if (debug) {
        log('Enabling paper trail on', this.name);
      }

      this.attributes[options.revisionAttribute] = {
        type: Sequelize.INTEGER,
        defaultValue: 0
      };
      this.revisionable = true;
      this.refreshAttributes();

      if (options.enableMigration) {
        var tableName = this.getTableName();
        sequelize.getQueryInterface().describeTable(tableName).then(function (attributes) {
          if (!attributes[options.revisionAttribute]) {
            if (debug) {
              log('adding revision attribute to the database');
            }
            sequelize.getQueryInterface().addColumn(tableName, options.revisionAttribute, {
              type: Sequelize.INTEGER,
              defaultValue: 0
            }).then(function () {
              return null;
            }).catch(function (err) {
              log('something went really wrong..');
              log(err);
              return null;
            });
          }
          return null;
        });
      }

      this.addHook("beforeCreate", createBeforeHook('create'));
      this.addHook("beforeUpdate", createBeforeHook('update'));
      this.addHook("beforeDestroy", createBeforeHook('destroy'));
      this.addHook("afterCreate", createAfterHook('create'));
      this.addHook("afterUpdate", createAfterHook('update'));
      this.addHook("afterDestroy", createAfterHook('destroy'));

      // create association
      return this.hasMany(sequelize.models[options.revisionModel], {
        foreignKey: options.defaultAttributes.documentId,
        constraints: false,
        scope: {
          model: this.name
        }
      });
    }
  });

  function createBeforeHook(operation) {
    var beforeHook = function beforeHook(instance, opt) {
      if (opt.noPaperTrail) {
        if (debug) {
          log('noPaperTrail opt: is true, not logging');
        }
        return;
      }

      if (debug) {
        log('beforeHook called');
        log('instance:');
        log(instance);
        log('opt:');
        log(opt);
      }

      var destroyOperation = operation === 'destroy';

      if (!destroyOperation && options.enableCompression) {
        var previousVersion = {};
        var currentVersion = {};

        _.forEach(opt.defaultFields, function (a) {
          const previousDataValuesOfA = instance._previousDataValues[a] === undefined ? null : instance._previousDataValues[a];
          previousVersion[a] = instance._previousDataValues[a];
          previousVersion[a] = instance._previousDataValues[a];
          currentVersion[a] = instance.dataValues[a];
          currentVersion[a] = instance.dataValues[a];
        });
      } else {
        var previousVersion = instance._previousDataValues;
        var currentVersion = instance.dataValues;
      }
      // Supported nested models.
      previousVersion = _.omitBy(previousVersion, function (i) {
        return i != null && typeof i === 'object' && !(i instanceof Date);
      });
      previousVersion = _.omit(previousVersion, options.exclude);
      currentVersion = _.omitBy(currentVersion, function (i) {
        return i != null && typeof i === 'object' && !(i instanceof Date);
      });
      currentVersion = _.omit(currentVersion, options.exclude);

      // Disallow change of revision
      instance.set(options.revisionAttribute, instance._previousDataValues[options.revisionAttribute]);

      // Get diffs
      var delta = helpers.calcDelta(previousVersion, currentVersion, options.exclude, options.enableStrictDiff);
      var currentRevisionId = instance.get(options.revisionAttribute);
      if (failHard && !currentRevisionId && opt.type === 'UPDATE') {
        throw new Error('Revision Id was undefined');
      }
      if (debug) {
        log('delta:');
        log(delta);
        log('revisionId', currentRevisionId);
      }

      // Check if all required fields have been provided to the opts / CLS
      if (options.extraDataFields) {
        // get all required field keys as an array
        var requiredFields = _.keys(_.pickBy(options.extraDataFields, function (required) {
          return required;
        }));
        if (requiredFields && requiredFields.length) {
          var extraData = ns && ns.get(options.extraDataContinuationKey) || opt['extraData'];
          var requiredFieldsProvided = _.filter(requiredFields, function(field) {
            return extraData[field] !== undefined;
          });
          if (requiredFieldsProvided.length !== requiredFields.length) {
            log('Required fields: ', options.extraDataFields, requiredFields);
            log('Required fields provided: ', extraData, requiredFieldsProvided);
            throw new Error('Not all required fields are provided to paper trail!');
          }
        }
      }

      if (destroyOperation || delta && delta.length > 0) {
        var revisionId = (currentRevisionId || 0) + 1;
        instance.set(options.revisionAttribute, revisionId);

        if (!instance.context) {
          instance.context = {};
        }
        instance.context.delta = delta;
      }
      if (debug) {
        log('end of beforeHook');
      }
    };
    return beforeHook;
  }

  function createAfterHook(operation) {
    var afterHook = function afterHook(instance, opt) {
      if (opt.noPaperTrail) {
        if (debug) {
          log('noPaperTrail opt: is true, not logging');
        }
        return;
      }

      if (debug) {
        log('afterHook called');
        log('instance:', instance);
        log('opt:', opt);
        if (ns) {
          log(`CLS ${options.continuationKey}:`, ns.get(options.continuationKey));
          log(`CLS ${options.extraDataFields}:`, ns.get(options.extraDataContinuationKey));
        }
      }

      var destroyOperation = operation === 'destroy';

      if (instance.context && (instance.context.delta && instance.context.delta.length > 0 || destroyOperation)) {
        var Revision = sequelize.model(options.revisionModel);
        if (options.enableRevisionChangeModel) {
          var RevisionChange = sequelize.model(options.revisionChangeModel);
        }
        var delta = instance.context.delta;

        if (!destroyOperation && options.enableCompression) {
          var previousVersion = {};
          var currentVersion = {};

          _.forEach(opt.defaultFields, function (a) {
            previousVersion[a] = instance._previousDataValues[a];
            currentVersion[a] = instance.dataValues[a];
          });
        } else {
          var previousVersion = instance._previousDataValues;
          var currentVersion = instance.dataValues;
        }

        // Supported nested models.
        previousVersion = _.omitBy(previousVersion, function (i) {
          return i != null && typeof i === 'object' && !(i instanceof Date);
        });
        previousVersion = _.omit(previousVersion, options.exclude);
        currentVersion = _.omitBy(currentVersion, function (i) {
          return i != null && typeof i === 'object' && !(i instanceof Date);
        });
        currentVersion = _.omit(currentVersion, options.exclude);

        if (failHard && !ns.get(options.continuationKey)) {
          throw new Error('The CLS continuationKey ' + options.continuationKey + ' was not defined.');
        }

        var document = currentVersion;

        if (options.mysql) {
          document = JSON.stringify(document);
        }

        // Build revision
        var query = {
          model: this.name,
          document: document,
          userId: ns && ns.get(options.continuationKey) || opt['userId'],
          operation: operation
        };
        query[options.defaultAttributes.documentId] = instance.id;

        // Add all extra data fields to the query object
        if (options.extraDataFields) {
          var extraData = ns && ns.get(options.extraDataContinuationKey) || opt['extraData'];
          if (extraData) {
            _.forEach(options.extraDataFields, function (required, field) {
              var value = extraData[field];
              if (debug) {
                log(`Adding extraData field to Revision - ${field} => ${value}`);
              }
              if (!query.hasOwnProperty(field)) {
                query[field] = value;
              } else if (debug) {
                log(`Revision object already has a value at ${field} => ${query[field]}`);
                log('Not overwriting the original value');
              }
            });
          }
        }

        var revision = Revision.build(query);

        revision[options.revisionAttribute] = instance.get(options.revisionAttribute);

        // Save revision
        return revision.save({transaction: opt.transaction}).then(function (revision) {
          // Loop diffs and create a revision-diff for each
          if (options.enableRevisionChangeModel) {
            _.forEach(delta, function (difference) {
              var o = helpers.diffToString(difference.item ? difference.item.lhs : difference.lhs);
              var n = helpers.diffToString(difference.item ? difference.item.rhs : difference.rhs);

              var document = difference;
              var diff = o || n ? jsdiff.diffChars(o, n) : []

              if (options.mysql) {
                document = JSON.stringify(document);
                diff = JSON.stringify(diff)
              }

              var d = RevisionChange.build({
                path: difference.path[0],
                document: document,
                revisionId: revision.id,
                diff: diff
              });

              d.save({transaction: opt.transaction}).then(function (d) {
                // Add diff to revision
                revision['add' + helpers.capitalizeFirstLetter(options.revisionChangeModel)](d);

                return null;
              }).catch(function (err) {
                log('RevisionChange save error');
                log(err);
                throw err;
              });
            });
          }
          return null;
        }).catch(function (err) {
          log('Revision save error');
          log(err);
          throw err;
        });
      }

      if (debug) {
        log('end of afterHook');
      }
    };
    return afterHook;
  };

  return {
    // Return defineModels()
    defineModels: function defineModels(db) {
      var attributes = {
        model: {
          type: Sequelize.TEXT,
          allowNull: false
        },
        document: {
          type: Sequelize.JSON,
          allowNull: false
        },
        operation: Sequelize.STRING(7)
      };

      if (options.mysql) {
        attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
      }

      attributes[options.defaultAttributes.documentId] = {
        type: Sequelize.INTEGER,
        allowNull: false
      };

      attributes[options.revisionAttribute] = {
        type: Sequelize.INTEGER,
        allowNull: false
      };

      if (debug) {
        log('attributes');
        log(attributes);
      }
      if (options.UUID) {
        attributes.id = {
          primaryKey: true,
          type: Sequelize.UUID,
          defaultValue: Sequelize.UUIDV4
        };
        attributes[options.defaultAttributes.documentId].type = Sequelize.UUID;
      }
      // Revision model
      var Revision = sequelize.define(options.revisionModel, attributes, {
        underscored: options.underscored
      });
      Revision.associate = function associate(models) {
        Revision.belongsTo(sequelize.model(options.userModel), options.belongsToUserOptions);
      };

      attributes = {
        path: {
          type: Sequelize.TEXT,
          allowNull: false
        },
        document: {
          type: Sequelize.JSON,
          allowNull: false
        },
        diff: {
          type: Sequelize.JSON,
          allowNull: false
        }
      };
      if (options.mysql) {
        attributes.document.type = Sequelize.TEXT('MEDIUMTEXT');
        attributes.diff.type = Sequelize.TEXT('MEDIUMTEXT');
      }
      if (options.UUID) {
        attributes.id = {
          primaryKey: true,
          type: Sequelize.UUID,
          defaultValue: Sequelize.UUIDV4
        };
      }

      if (options.enableRevisionChangeModel) {
        // RevisionChange model
        var RevisionChange = sequelize.define(options.revisionChangeModel, attributes, {
          underscored: options.underscored
        });

        // Set associations
        Revision.hasMany(RevisionChange, {
          foreignKey: options.defaultAttributes.revisionId,
          constraints: false
        });

        RevisionChange.belongsTo(Revision);
        if (db) db[RevisionChange.name] = RevisionChange;
      }

      if (db) db[Revision.name] = Revision;

      if (options.userModel) {
        Revision.belongsTo(sequelize.model(options.userModel), options.belongsToUserOptions);
      }
      return Revision;
    }
  };
};

/**
 * Throw exceptions when the user identifier from CLS is not set or if the
 * revisionAttribute was not loaded on the model.
 */
exports.enableFailHard = function () {
  failHard = true;
},

module.exports = exports;
<|endoftext|>


<commit_before>// Copyright 2002-2013, University of Colorado

/**
 * A rectangular node that inherits Path, and allows for optimized drawing,
 * and improved rectangle handling.
 *
 * TODO: add DOM support
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );

  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );

  scenery.Rectangle = function Rectangle( x, y, width, height, arcWidth, arcHeight, options ) {
    if ( typeof x === 'object' ) {
      // allow new Rectangle( { rectX: x, rectY: y, rectWidth: width, rectHeight: height, ... } )
      // the mutators will call invalidateRectangle() and properly set the shape
      options = x;
    } else if ( arguments.length < 6 ) {
      // new Rectangle( x, y, width, height, [options] )
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = 0;
      this._rectArcHeight = 0;

      // ensure we have a parameter object
      options = arcWidth || {};

      // fallback for non-canvas or non-svg rendering, and for proper bounds computation
      options.shape = this.createRectangleShape();
    } else {
      // normal case with args (including arcWidth / arcHeight)
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = arcWidth;
      this._rectArcHeight = arcHeight;

      // ensure we have a parameter object
      options = options || {};

      // fallback for non-canvas or non-svg rendering, and for proper bounds computation
      options.shape = this.createRectangleShape();
    }

    Path.call( this, options );
  };
  var Rectangle = scenery.Rectangle;

  inherit( Rectangle, Path, {
    isRounded: function() {
      return this._rectArcWidth !== 0 && this._rectArcHeight !== 0;
    },

    createRectangleShape: function() {
      if ( this.isRounded() ) {
        return Shape.roundRectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight, this._rectArcWidth, this._rectArcHeight );
      } else {
        return Shape.rectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
      }
    },

    invalidateRectangle: function() {
      // setShape should invalidate the path and ensure a redraw
      this.setShape( this.createRectangleShape() );
    },

    // override paintCanvas with a faster version, since fillRect and drawRect don't affect the current default path
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;

      // use the standard version if it's a rounded rectangle, since there is no Canvas-optimized version for that
      if ( this.isRounded() ) {
        return Path.prototype.paintCanvas.call( this, wrapper );
      }

      // TODO: how to handle fill/stroke delay optimizations here?
      if ( this._fill ) {
        this.beforeCanvasFill( wrapper ); // defined in Fillable
        context.fillRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasFill( wrapper ); // defined in Fillable
      }
      if ( this._stroke ) {
        this.beforeCanvasStroke( wrapper ); // defined in Strokable
        context.strokeRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasStroke( wrapper ); // defined in Strokable
      }
    },

    // create a rect instead of a path, hopefully it is faster in implementations
    createSVGFragment: function( svg, defs, group ) {
      return document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
    },

    // optimized for the rect element instead of path
    updateSVGFragment: function( rect ) {
      // see http://www.w3.org/TR/SVG/shapes.html#RectElement
      rect.setAttribute( 'x', this._rectX );
      rect.setAttribute( 'y', this._rectY );
      rect.setAttribute( 'width', this._rectWidth );
      rect.setAttribute( 'height', this._rectHeight );
      rect.setAttribute( 'rx', this._rectArcWidth );
      rect.setAttribute( 'ry', this._rectArcHeight );

      rect.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
    },

    getBasicConstructor: function( propLines ) {
      return 'new scenery.Rectangle( ' + this._rectX + ', ' + this._rectY + ', ' +
                                         this._rectWidth + ', ' + this._rectHeight + ', ' +
                                         this._rectArcWidth + ', ' + this._rectArcHeight + ', {' + propLines + '} )';
    }

  } );

  function addRectProp( capitalizedShort ) {
    var getName = 'getRect' + capitalizedShort;
    var setName = 'setRect' + capitalizedShort;
    var privateName = '_rect' + capitalizedShort;

    Rectangle.prototype[getName] = function() {
      return this[privateName];
    };

    Rectangle.prototype[setName] = function( value ) {
      this[privateName] = value;
      this.invalidateRectangle();
      return this;
    };

    Object.defineProperty( Rectangle.prototype, 'rect' + capitalizedShort, {
      set: Rectangle.prototype[setName],
      get: Rectangle.prototype[getName]
    } );
  }

  addRectProp( 'X' );
  addRectProp( 'Y' );
  addRectProp( 'Width' );
  addRectProp( 'Height' );
  addRectProp( 'ArcWidth' );
  addRectProp( 'ArcHeight' );

  // not adding mutators for now
  Rectangle.prototype._mutatorKeys = [ 'rectX', 'rectY', 'rectWidth', 'rectHeight', 'rectArcWidth', 'rectArcHeight' ].concat( Path.prototype._mutatorKeys );

  return Rectangle;
} );

<commit_msg>Adding Rectangle.setRect( x, y, width, height [, arcWidth, arcHeight] ), closes #59<commit_after>// Copyright 2002-2013, University of Colorado

/**
 * A rectangular node that inherits Path, and allows for optimized drawing,
 * and improved rectangle handling.
 *
 * TODO: add DOM support
 *
 * @author Jonathan Olson <olsonsjc@gmail.com>
 */

define( function( require ) {
  'use strict';

  var inherit = require( 'PHET_CORE/inherit' );
  var scenery = require( 'SCENERY/scenery' );

  var Path = require( 'SCENERY/nodes/Path' );
  var Shape = require( 'KITE/Shape' );

  scenery.Rectangle = function Rectangle( x, y, width, height, arcWidth, arcHeight, options ) {
    if ( typeof x === 'object' ) {
      // allow new Rectangle( { rectX: x, rectY: y, rectWidth: width, rectHeight: height, ... } )
      // the mutators will call invalidateRectangle() and properly set the shape
      options = x;
    } else if ( arguments.length < 6 ) {
      // new Rectangle( x, y, width, height, [options] )
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = 0;
      this._rectArcHeight = 0;

      // ensure we have a parameter object
      options = arcWidth || {};

      // fallback for non-canvas or non-svg rendering, and for proper bounds computation
      options.shape = this.createRectangleShape();
    } else {
      // normal case with args (including arcWidth / arcHeight)
      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = arcWidth;
      this._rectArcHeight = arcHeight;

      // ensure we have a parameter object
      options = options || {};

      // fallback for non-canvas or non-svg rendering, and for proper bounds computation
      options.shape = this.createRectangleShape();
    }

    Path.call( this, options );
  };
  var Rectangle = scenery.Rectangle;

  inherit( Rectangle, Path, {
    setRect: function( x, y, width, height, arcWidth, arcHeight ) {
      sceneryAssert && sceneryAssert( x !== undefined && y !== undefined && width !== undefined && height !== undefined, 'x/y/width/height need to be defined' );

      this._rectX = x;
      this._rectY = y;
      this._rectWidth = width;
      this._rectHeight = height;
      this._rectArcWidth = arcWidth || 0;
      this._rectArcHeight = arcHeight || 0;
      this.invalidateRectangle();
    },

    isRounded: function() {
      return this._rectArcWidth !== 0 && this._rectArcHeight !== 0;
    },

    createRectangleShape: function() {
      if ( this.isRounded() ) {
        return Shape.roundRectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight, this._rectArcWidth, this._rectArcHeight );
      } else {
        return Shape.rectangle( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
      }
    },

    invalidateRectangle: function() {
      // setShape should invalidate the path and ensure a redraw
      this.setShape( this.createRectangleShape() );
    },

    // override paintCanvas with a faster version, since fillRect and drawRect don't affect the current default path
    paintCanvas: function( wrapper ) {
      var context = wrapper.context;

      // use the standard version if it's a rounded rectangle, since there is no Canvas-optimized version for that
      if ( this.isRounded() ) {
        return Path.prototype.paintCanvas.call( this, wrapper );
      }

      // TODO: how to handle fill/stroke delay optimizations here?
      if ( this._fill ) {
        this.beforeCanvasFill( wrapper ); // defined in Fillable
        context.fillRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasFill( wrapper ); // defined in Fillable
      }
      if ( this._stroke ) {
        this.beforeCanvasStroke( wrapper ); // defined in Strokable
        context.strokeRect( this._rectX, this._rectY, this._rectWidth, this._rectHeight );
        this.afterCanvasStroke( wrapper ); // defined in Strokable
      }
    },

    // create a rect instead of a path, hopefully it is faster in implementations
    createSVGFragment: function( svg, defs, group ) {
      return document.createElementNS( 'http://www.w3.org/2000/svg', 'rect' );
    },

    // optimized for the rect element instead of path
    updateSVGFragment: function( rect ) {
      // see http://www.w3.org/TR/SVG/shapes.html#RectElement
      rect.setAttribute( 'x', this._rectX );
      rect.setAttribute( 'y', this._rectY );
      rect.setAttribute( 'width', this._rectWidth );
      rect.setAttribute( 'height', this._rectHeight );
      rect.setAttribute( 'rx', this._rectArcWidth );
      rect.setAttribute( 'ry', this._rectArcHeight );

      rect.setAttribute( 'style', this.getSVGFillStyle() + this.getSVGStrokeStyle() );
    },

    getBasicConstructor: function( propLines ) {
      return 'new scenery.Rectangle( ' + this._rectX + ', ' + this._rectY + ', ' +
                                         this._rectWidth + ', ' + this._rectHeight + ', ' +
                                         this._rectArcWidth + ', ' + this._rectArcHeight + ', {' + propLines + '} )';
    }

  } );

  function addRectProp( capitalizedShort ) {
    var getName = 'getRect' + capitalizedShort;
    var setName = 'setRect' + capitalizedShort;
    var privateName = '_rect' + capitalizedShort;

    Rectangle.prototype[getName] = function() {
      return this[privateName];
    };

    Rectangle.prototype[setName] = function( value ) {
      this[privateName] = value;
      this.invalidateRectangle();
      return this;
    };

    Object.defineProperty( Rectangle.prototype, 'rect' + capitalizedShort, {
      set: Rectangle.prototype[setName],
      get: Rectangle.prototype[getName]
    } );
  }

  addRectProp( 'X' );
  addRectProp( 'Y' );
  addRectProp( 'Width' );
  addRectProp( 'Height' );
  addRectProp( 'ArcWidth' );
  addRectProp( 'ArcHeight' );

  // not adding mutators for now
  Rectangle.prototype._mutatorKeys = [ 'rectX', 'rectY', 'rectWidth', 'rectHeight', 'rectArcWidth', 'rectArcHeight' ].concat( Path.prototype._mutatorKeys );

  return Rectangle;
} );

<|endoftext|>