<commit_before>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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.

var databank = require("databank"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    // XXX: I hope this works

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            var name = "activityobject:replies:"+obj.id;
            Stream.create({name: name}, this);
        },
        function(err, stream) {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.afterDel =
ActivityObject.prototype.afterEfface = function(callback) {
    var obj = this;

    Step(
        function() {
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.removeObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.repliesCount(this.parallel());
            obj.favoritersCount(this.parallel());
            obj.getReplies(0, 4, this.parallel());
        },
        function(err, repliesCount, favoritersCount, replies) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: favoritersCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                obj.replies = {
                    totalItems: repliesCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/replies")
                };
                if (repliesCount > 0) {
                    obj.replies.items = replies;
                }
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {

    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Stream.get(name, callback);
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this,
        full = [];

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        function(err, results) {
            var i, group = this.group();
            if (err) throw err;
            full = results;
            for (i = 0; i < full.length; i++) {
                full[i].expandFeeds(group());
            }
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, full);
            }
        }
    );
};

ActivityObject.prototype.repliesCount = function(callback) {
    var name = "activityobject:replies:"+this.id;
    Stream.count(name, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.keepers = function() {
    return ["id", "objectType", "author", "published", "updated", "uuid", "inReplyTo"];
};

// Default hooks for efface()

ActivityObject.prototype.beforeEfface = function(callback) {
    callback(null);
};

ActivityObject.prototype.efface = function(callback) {

    var keepers = this.keepers(),
        prop,
        obj = this;

    for (prop in obj) {
        if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
            delete obj[prop];
        }
    }

    var now = Stamper.stamp();

    obj.deleted = obj.updated = now;

    Step(
        function() {
            obj.beforeEfface(this);
        },
        function(err) {
            if (err) throw err;
            obj.save(this);
        },
        function(err) {
            obj.afterEfface(this);
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<commit_msg>object is whole before effacement<commit_after>// activityobject.js
//
// utility superclass for activity stuff
//
// Copyright 2012, StatusNet Inc.
//
// 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.

var databank = require("databank"),
    _ = require("underscore"),
    Step = require("step"),
    NoSuchThingError = databank.NoSuchThingError,
    AlreadyExistsError = databank.AlreadyExistsError,
    DatabankObject = databank.DatabankObject,
    uuid = require("node-uuid"),
    URLMaker = require("../urlmaker").URLMaker,
    IDMaker = require("../idmaker").IDMaker,
    Stamper = require("../stamper").Stamper,
    Stream = require("./stream").Stream;

var UnknownTypeError = function(type) {
    Error.captureStackTrace(this, UnknownTypeError);
    this.name = "UnknownTypeError";
    this.type = type;
    this.message = "Unknown type: " + type;
};

UnknownTypeError.prototype = new Error();
UnknownTypeError.prototype.constructor = UnknownTypeError;

var ActivityObject = function(properties) {
    ActivityObject.init(this, properties);
};

ActivityObject.init = DatabankObject.init;

ActivityObject.prototype = new DatabankObject({});

ActivityObject.beforeCreate = function(props, callback) {

    // XXX: I hope this works

    props.objectType = this.type;

    var now = Stamper.stamp();

    props.published = props.updated = now;

    if (!_(props).has("id")) {
        props.uuid = IDMaker.makeID();
        props.id   = ActivityObject.makeURI(this.type, props.uuid);
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

ActivityObject.prototype.afterUpdate =
ActivityObject.prototype.afterSave =
ActivityObject.prototype.afterGet = function(callback) {
    this.expand(callback);
};

ActivityObject.prototype.afterCreate = function(callback) {
    var obj = this;

    Step(
        function() {
            var name = "activityobject:replies:"+obj.id;
            Stream.create({name: name}, this);
        },
        function(err, stream) {
            obj.expand(this);
        },
        function(err) {
            if (err) throw err;
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.deliverObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.afterDel =
ActivityObject.prototype.afterEfface = function(callback) {
    var obj = this;

    Step(
        function() {
            if (!_(obj).has("inReplyTo") || !_(obj.inReplyTo).isObject()) {
                callback(null);
            } else {
                ActivityObject.ensureObject(obj.inReplyTo, this);
            }
        },
        function(err, irt) {
            if (err) throw err;
            irt.getRepliesStream(this);
        },
        function(err, replies) {
            var compressed;
            if (err) throw err;
            compressed = {id: obj.id,
                          objectType: obj.objectType};
            replies.removeObject(compressed, this);
        },
        callback
    );
};

ActivityObject.prototype.expand = function(callback) {
    ActivityObject.expandProperty(this, "author", callback);
};

ActivityObject.prototype.beforeSave = function(callback) {

    var obj = this,
        now = Stamper.stamp();

    this.updated = now;

    if (_(this).has("likes")) {
        delete this.likes;
    }

    // Save the author by reference; don't save the whole thing

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(obj, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(obj, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, obj);
            }
        }
    );
};

ActivityObject.prototype.beforeUpdate = function(props, callback) {

    var immutable = ["id", "objectType", "uuid", "published"],
        i, prop;

    for (i = 0; i < immutable.length; i++) {
        prop = immutable[i];
        if (props.hasOwnProperty(prop)) {
            delete props[prop];
        }
    }

    if (_(props).has("likes")) {
        delete props.likes;
    }

    var now = Stamper.stamp();

    props.updated = now;

    Step(
        function() {
            // Save the author by reference; don't save the whole thing
            ActivityObject.compressProperty(props, "author", this);
        },
        function(err) {
            if (err) throw err;
            ActivityObject.compressProperty(props, "inReplyTo", this);
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, props);
            }
        }
    );
};

// For now, we make HTTP URIs. Maybe someday we'll
// do something else. I like HTTP URIs, though.

ActivityObject.makeURI = function(type, uuid) {
    return URLMaker.makeURL("api/" + type + "/" + uuid);
};

ActivityObject.toClass = function(type) {
    var module, className;

    if (!type ||
        ActivityObject.objectTypes.indexOf(type.toLowerCase()) == -1) {
        return null;
    }

    module = require("./" + type);
    className = type.substring(0,1).toUpperCase() + type.substring(1, type.length).toLowerCase();
    return module[className];
};

ActivityObject.toObject = function(props, defaultType) {
    var Cls, type;

    // Try rational fallbacks
    type = props.objectType || defaultType || ActivityObject.NOTE;

    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        return new Cls(props);
    } else {
        // XXX: is this really the best we can do?
        // XXX: extension mechanism
        // XXX: "Other" object type
        return props;
    }
};

ActivityObject.getObject = function(type, id, callback) {
    var Cls;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.get(id, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.createObject = function(obj, callback) {
    var Cls, type = obj.objectType;
    if (ActivityObject.objectTypes.indexOf(type) != -1) {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, callback);
    } else {
        callback(new UnknownTypeError(type), null);
    }
};

ActivityObject.ensureObject = function(obj, callback) {
    var Cls, type = obj.objectType, id = obj.id;
    if (ActivityObject.objectTypes.indexOf(type) === -1) {
        callback(new UnknownTypeError(type), null);
    } else {
        Cls = ActivityObject.toClass(type);
        Cls.create(obj, function(err, result) {
            if (err) {
                if (err instanceof AlreadyExistsError) {
                    Cls.get(id, callback);
                } else {
                    callback(err, null);
                }
            } else {
                callback(null, result);
            }
        });
    }
};

ActivityObject.compressProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            var Cls;
            if (err) {
                callback(err);
            } else {
                Cls = ActivityObject.toClass(sub.objectType);
                if (!Cls) {
                    callback(new UnknownTypeError(sub.objectType));
                } else {
                    obj[name] = new Cls({id: sub.id,
                                         objectType: sub.objectType});
                    callback(null);
                }
            }
        }
    );
};

ActivityObject.expandProperty = function(obj, name, callback) {
    // Easy enough!
    if (!_(obj).has(name)) {
        callback(null);
        return;
    }
    Step(
        function() {
            ActivityObject.ensureObject(obj[name], this);
        },
        function(err, sub) {
            if (err) {
                callback(err);
            } else {
                obj[name] = sub;
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.favoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.deliver(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.unfavoritedBy = function(id, callback) {
    var obj = this,
        name = "favoriters:"+obj.id;
    Step(
        function() {
            Stream.get(name, this);
        },
        function(err, stream) {
            if (err && err instanceof NoSuchThingError) {
                Stream.create({name: name}, this);
            } else if (err) {
                throw err;
            } else {
                this(null, stream);
            }
        },
        function(err, stream) {
            if (err) throw err;
            stream.remove(id, this);
        },
        function(err) {
            if (err) {
                callback(err);
            } else {
                callback(null);
            }
        }
    );
};

ActivityObject.getObjectStream = function(className, streamName, start, end, callback) {

    var ids,
        Cls = ActivityObject.toClass(className);

    Step(
        function() {
            Stream.get(streamName, this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getIDs(start, end, this);
        },
        function(err, results) {
            if (err) throw err;
            ids = results;
            if (ids.length === 0) {
                callback(null, []);
            } else {
                Cls.readAll(ids, this);
            }
        },
        function(err, map) {
            var i, objects = [];
            if (err) {
                if (err instanceof NoSuchThingError) {
                    callback(null, []);
                } else {
                    callback(err, null);
                }
            } else {
                objects = new Array(ids.length);
                // Try to get it in the right order
                for (i = 0; i < ids.length; i++) {
                    objects[i] = map[ids[i]];
                }
                callback(null, objects);
            }
        }
    );
};

ActivityObject.prototype.getFavoriters = function(start, end, callback) {
    ActivityObject.getObjectStream("person", "favoriters:"+this.id, start, end, callback);
};

ActivityObject.prototype.favoritersCount = function(callback) {
    Stream.count("favoriters:"+this.id, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.expandFeeds = function(callback) {
    var obj = this;
    Step(
        function() {
            obj.repliesCount(this.parallel());
            obj.favoritersCount(this.parallel());
            obj.getReplies(0, 4, this.parallel());
        },
        function(err, repliesCount, favoritersCount, replies) {
            if (err) {
                callback(err);
            } else {
                obj.likes = {
                    totalItems: favoritersCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/likes")
                };
                obj.replies = {
                    totalItems: repliesCount,
                    url: URLMaker.makeURL("api/"+obj.objectType+"/"+obj.uuid+"/replies")
                };
                if (repliesCount > 0) {
                    obj.replies.items = replies;
                }
                callback(null);
            }
        }
    );
};

ActivityObject.prototype.getRepliesStream = function(callback) {

    var obj = this,
        name = "activityobject:replies:"+obj.id;

    Stream.get(name, callback);
};

ActivityObject.prototype.getReplies = function(start, end, callback) {

    var obj = this,
        full = [];

    Step(
        function() {
            obj.getRepliesStream(this);
        },
        function(err, stream) {
            if (err) throw err;
            stream.getObjects(start, end, this);
        },
        function(err, compressed) {
            var i, group = this.group();
            if (err) throw err;
            for (i = 0; i < compressed.length; i++) {
                ActivityObject.ensureObject(compressed[i], group());
            }
        },
        function(err, results) {
            var i, group = this.group();
            if (err) throw err;
            full = results;
            for (i = 0; i < full.length; i++) {
                full[i].expandFeeds(group());
            }
        },
        function(err) {
            if (err) {
                callback(err, null);
            } else {
                callback(null, full);
            }
        }
    );
};

ActivityObject.prototype.repliesCount = function(callback) {
    var name = "activityobject:replies:"+this.id;
    Stream.count(name, function(err, count) {
        if (err && err instanceof NoSuchThingError) {
            callback(null, 0);
        } else if (err) {
            callback(err, null);
        } else {
            callback(null, count);
        }
    });
};

ActivityObject.prototype.keepers = function() {
    return ["id", "objectType", "author", "published", "updated", "uuid", "inReplyTo"];
};

// Default hooks for efface()

ActivityObject.prototype.beforeEfface = function(callback) {
    callback(null);
};

ActivityObject.prototype.efface = function(callback) {

    var keepers = this.keepers(),
        prop,
        obj = this;

    Step(
        function() {
            obj.beforeEfface(this);
        },
        function(err) {
            if (err) throw err;

            for (prop in obj) {
                if (obj.hasOwnProperty(prop) && keepers.indexOf(prop) === -1) {
                    delete obj[prop];
                }
            }

            var now = Stamper.stamp();

            obj.deleted = obj.updated = now;

            obj.save(this);
        },
        function(err) {
            obj.afterEfface(this);
        },
        callback
    );
};

ActivityObject.objectTypes = ["article",
                              "audio",
                              "badge",
                              "bookmark",
                              "collection",
                              "comment",
                              "event",
                              "file",
                              "group",
                              "image",
                              "note",
                              "person",
                              "place",
                              "product",
                              "question",
                              "review",
                              "service",
                              "video"];

var objectType, i;

// Constants-like members for activity object types

for (i = 0; i < ActivityObject.objectTypes.length; i++) {
    objectType = ActivityObject.objectTypes[i];
    ActivityObject[objectType.toUpperCase().replace("-", "_")] = objectType;
}

exports.ActivityObject = ActivityObject;
exports.UnknownTypeError = UnknownTypeError;
<|endoftext|>


<commit_before>/**
 * Copyright [2013] [runrightfast.co]
 *
 * 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.
 */

module.exports = function(objectSchemaDatabase,log){
	'use strict';

	var extend = require('extend');
	var uuid = require('runrightfast-commons').uuid;
	var hapi = require('hapi');
	var types = hapi.types;
	var when = require('when');
	var lodash = require('lodash');
	var querystring = require('querystring');

	var apiVersion = '/v1';
	var resource = 'objectschemas';

	var path = function(resourcePath){
		var routePath =  apiVersion + '/resources/' + resource;
		if(resourcePath){
			routePath += resourcePath;
		}
		if(log.isDebugEnabled()){
			log.debug('path(): ' + routePath);
		}
		return routePath;
	};

	var ResponseMetaData = function(path){
		this.path = path;
		this.id = uuid();
		this.receivedOn = new Date();
	};

	ResponseMetaData.prototype.stopTimer = function(){
		this.processingTime = Date.now() - this.receivedOn .getTime();
	};

	var getObjectSchemas = function getObjectSchemas(_path,request){
		var defaultGetObjectSchemasParams = {
			limit : 10,
			offset : 0,
			dataOnly : false,
			links : true,
			actions : true,
			version : false,
			raw : false
		};

		var getParams = function(){
			var params = {
				limit : request.query.limit,
				offset : request.query.offset,
				dataFields : request.query.dataFields,
				timeout: request.query.timeout,
				version: request.query.version,
				dataOnly : request.query.dataOnly,
				actions : request.query.actions,
				links : request.query.links,
				sort : request.query.sort,
				raw : request.query.raw
			};

			params = extend(defaultGetObjectSchemasParams,params);

			if(log.isDebugEnabled()){
				log.debug('getObjectSchemas(): request.query: ' + JSON.stringify(request.query,undefined,2));
				log.debug('getObjectSchemas(): getParams(): ' + JSON.stringify(params,undefined,2));
			}

			return params;
		};

		var toSearchOptions = function(params){
			var findAllOptions = {
				pageSize : params.limit,
				from : params.offset,
				returnFields : (params.dataFields ? params.dataFields.split(',') : undefined),
				timeout : params.timeout,
				version : params.version
			};

			if(params.sort){
				var tokens;
				findAllOptions.multiFieldSort = params.sort.split(',').map(function(sortField){
					tokens = sortField.split('|');
					return {
						field : tokens[0],
						descending : (tokens.length > 1 ? tokens[1] === 'desc' : false)
					};
				});
			}

			if(log.isDebugEnabled()){
				log.debug('getObjectSchemas(): toSearchOptions(): ' + JSON.stringify(findAllOptions,undefined,2));
			}

			return findAllOptions;
		};

		var searchResults = {
			meta : new ResponseMetaData(_path)
		};
		var params = getParams();
		var findAllOptions = toSearchOptions(params);

		when(objectSchemaDatabase.database.findAll(findAllOptions),
			function(results){
				searchResults.meta.searchResult = {
					limit: params.limit,
					offset: params.offset,
					total : results.hits.total,
					count: results.hits.hits.length
				};
				if(params.raw){
					searchResults.data = results;
				}else{
					if(params.version){
						if(params.dataFields){
							searchResults.data = {
								hits: lodash.map(results.hits.hits,function(hit){
									return {
										version : hit._version,
										data : hit.fields
									};
								})
							};
						}else{
							searchResults.data = {
								hits: lodash.map(results.hits.hits,function(hit){
									return {
										version : hit._version,
										data : hit._source
									};
								})
							};
						}
					}else{
						if(params.dataFields){
							searchResults.data = {
								hits: lodash.pluck(results.hits.hits,'fields')
							};
						}else{
							searchResults.data = {
								hits: lodash.pluck(results.hits.hits,'_source')
							};
						}
					}
				}

				var addActions = function(){
					if(params.actions){
						searchResults.actions = [
							{
								name : 'create',
								title : 'Create Object Schema',
								method : 'POST',
								auth : ['hawk'],
								requestPayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchema'
								},
								responsePayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchemaId'
								},
								href : path()
							},
							{
								name : 'count',
								title : 'Total Number of Object Schemas',
								method : 'GET',
								auth : ['hawk'],
								requestPayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchema'
								},
								responsePayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchemaCount'
								},
								href : path('/count')
							}
						];
					}
				};

				var addLinks = function(){
					if(params.links){
						var linkQueryString = '?' + querystring.stringify(request.query);
						searchResults.links = [
							{
								rel: 'self',
								href : path() + linkQueryString,
								title : 'Get Object Schemas',
								auth : ['hawk']
							}
						];

						if(searchResults.meta.searchResult.total > 0){
							linkQueryString = lodash.clone(request.query);
							linkQueryString.offset = 0;
							linkQueryString = '?' + querystring.stringify(linkQueryString);
							searchResults.links.push({
								rel: 'firstPage',
								href : path() + linkQueryString,
								title : 'Get Object Schemas - First Page',
								auth : ['hawk']
							});

							if(searchResults.meta.searchResult.total > params.limit){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = searchResults.meta.searchResult.total - params.limit;
								if(linkQueryString.offset < (params.offset + params.limit)){
									linkQueryString.offset = (params.offset + params.limit);
								}
								linkQueryString = '?' + querystring.stringify(linkQueryString);
								searchResults.links.push({
									rel: 'lastPage',
									href : path() + linkQueryString,
									title : 'Get Object Schemas - Last Page',
									auth : ['hawk']
								});
							}

							if(searchResults.meta.searchResult.count < searchResults.meta.searchResult.total){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = (params.offset + params.limit);
								if(linkQueryString.offset < searchResults.meta.searchResult.total){
									linkQueryString = '?' + querystring.stringify(linkQueryString);
									searchResults.links.push({
										rel: 'nextPage',
										href : path() + linkQueryString,
										title : 'Get Object Schemas - Next Page',
										auth : ['hawk']
									});
								}
							}

							if(searchResults.meta.searchResult.offset > 0){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = params.offset - params.limit;
								if(linkQueryString.offset > 0){
									linkQueryString = '?' + querystring.stringify(linkQueryString);
									searchResults.links.push({
										rel: 'prevPage',
										href : path() + linkQueryString,
										title : 'Get Object Schemas - Previous Page',
										auth : ['hawk']
									});
								}
							}
						}
					}
				};

				addActions();
				addLinks();
				request.reply(searchResults);
				searchResults.meta.stopTimer();
			},
			function(err){
				searchResults.error = {
					code: 500,
					developerMessage : err.toString(),
					userMessage : 'Failed to retrieve object schemas'
				};
				request.reply(searchResults).code(searchResults.error.code);
				searchResults.meta.stopTimer();
			}
		);

	};

	return [
		{
			method: 'GET',
			path : path(),
			config: {
				description : 'Used to page through ObjectSchemas. By default, the first 10 are returned sorted by updatedOn in descending order.',
				tags : ['objectschemas','paging'],
				validate: {
					query : {
						limit : types.Number(),
						offset : types.Number(),
						sort : types.String(),
						dataFields : types.String(),
						dataOnly : types.Boolean(),
						actions : types.Boolean(),
						links : types.Boolean(),
						timeout : types.Number(),
						version : types.Boolean(),
						raw : types.Boolean()
					}
				},
				handler : getObjectSchemas.bind(null,path())
			}
		}
	];
};<commit_msg>refactored<commit_after>/**
 * Copyright [2013] [runrightfast.co]
 *
 * 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.
 */

module.exports = function(objectSchemaDatabase,log){
	'use strict';

	var extend = require('extend');
	var uuid = require('runrightfast-commons').uuid;
	var hapi = require('hapi');
	var types = hapi.types;
	var when = require('when');
	var lodash = require('lodash');
	var querystring = require('querystring');

	var apiVersion = '/v1';
	var resource = 'objectschemas';

	var path = function(resourcePath){
		var routePath =  apiVersion + '/resources/' + resource;
		if(resourcePath){
			routePath += resourcePath;
		}
		if(log.isDebugEnabled()){
			log.debug('path(): ' + routePath);
		}
		return routePath;
	};

	var ResponseMetaData = function(path){
		this.path = path;
		this.id = uuid();
		this.receivedOn = new Date();
	};

	ResponseMetaData.prototype.stopTimer = function(){
		this.processingTime = Date.now() - this.receivedOn .getTime();
	};

	var getObjectSchemas = function getObjectSchemas(_path,request){
		var defaultGetObjectSchemasParams = {
			limit : 10,
			offset : 0,
			dataOnly : false,
			links : true,
			actions : true,
			version : false,
			raw : false
		};

		var getParams = function(){
			var params = {
				limit : request.query.limit,
				offset : request.query.offset,
				dataFields : request.query.dataFields,
				timeout: request.query.timeout,
				version: request.query.version,
				dataOnly : request.query.dataOnly,
				actions : request.query.actions,
				links : request.query.links,
				sort : request.query.sort,
				raw : request.query.raw
			};

			params = extend(defaultGetObjectSchemasParams,params);

			if(log.isDebugEnabled()){
				log.debug('getObjectSchemas(): request.query: ' + JSON.stringify(request.query,undefined,2));
				log.debug('getObjectSchemas(): getParams(): ' + JSON.stringify(params,undefined,2));
			}

			return params;
		};

		var toSearchOptions = function(params){
			var findAllOptions = {
				pageSize : params.limit,
				from : params.offset,
				returnFields : (params.dataFields ? params.dataFields.split(',') : undefined),
				timeout : params.timeout,
				version : params.version
			};

			if(params.sort){
				var tokens;
				findAllOptions.multiFieldSort = params.sort.split(',').map(function(sortField){
					tokens = sortField.split('|');
					return {
						field : tokens[0],
						descending : (tokens.length > 1 ? tokens[1] === 'desc' : false)
					};
				});
			}

			if(log.isDebugEnabled()){
				log.debug('getObjectSchemas(): toSearchOptions(): ' + JSON.stringify(findAllOptions,undefined,2));
			}

			return findAllOptions;
		};

		var searchResults = {
			meta : new ResponseMetaData(_path)
		};
		var params = getParams();
		var findAllOptions = toSearchOptions(params);

		when(objectSchemaDatabase.database.findAll(findAllOptions),
			function(results){
				searchResults.meta.searchResult = {
					limit: params.limit,
					offset: params.offset,
					total : results.hits.total,
					count: results.hits.hits.length
				};
				if(params.raw){
					searchResults.data = results;
				}else{
					if(params.version){
						if(params.dataFields){
							searchResults.data = {
								hits: lodash.map(results.hits.hits,function(hit){
									return {
										version : hit._version,
										data : hit.fields
									};
								})
							};
						}else{
							searchResults.data = {
								hits: lodash.map(results.hits.hits,function(hit){
									return {
										version : hit._version,
										data : hit._source
									};
								})
							};
						}
					}else{
						if(params.dataFields){
							searchResults.data = {
								hits: lodash.pluck(results.hits.hits,'fields')
							};
						}else{
							searchResults.data = {
								hits: lodash.pluck(results.hits.hits,'_source')
							};
						}
					}
				}

				var addActions = function(){
					if(params.actions){
						searchResults.actions = [
							{
								name : 'create',
								title : 'Create Object Schema',
								method : 'POST',
								auth : ['hawk'],
								requestPayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchema'
								},
								responsePayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchemaId'
								},
								href : path()
							},
							{
								name : 'count',
								title : 'Total Number of Object Schemas',
								method : 'GET',
								auth : ['hawk'],
								requestPayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchema'
								},
								responsePayloadSchema : {
									namespace : 'ns://runrightfast.co/ObjectSchema',
									version : '1.0.0',
									type : 'ObjectSchemaCount'
								},
								href : path('/count')
							}
						];
					}
				};

				var addLinks = function(){
					if(params.links){
						var linkQueryString = '?' + querystring.stringify(request.query);
						searchResults.links = [
							{
								rel: 'self',
								href : path() + linkQueryString,
								title : 'Get Object Schemas',
								auth : ['hawk']
							}
						];

						if(searchResults.meta.searchResult.total > 0){
							linkQueryString = lodash.clone(request.query);
							linkQueryString.offset = 0;
							linkQueryString = '?' + querystring.stringify(linkQueryString);
							searchResults.links.push({
								rel: 'firstPage',
								href : path() + linkQueryString,
								title : 'Get Object Schemas - First Page',
								auth : ['hawk']
							});

							if(searchResults.meta.searchResult.total > params.limit){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = searchResults.meta.searchResult.total - params.limit;
								if(linkQueryString.offset < (params.offset + params.limit)){
									linkQueryString.offset = (params.offset + params.limit);
								}
								linkQueryString = '?' + querystring.stringify(linkQueryString);
								searchResults.links.push({
									rel: 'lastPage',
									href : path() + linkQueryString,
									title : 'Get Object Schemas - Last Page',
									auth : ['hawk']
								});
							}

							if(searchResults.meta.searchResult.count < searchResults.meta.searchResult.total){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = (params.offset + params.limit);
								if(linkQueryString.offset < searchResults.meta.searchResult.total){
									linkQueryString = '?' + querystring.stringify(linkQueryString);
									searchResults.links.push({
										rel: 'nextPage',
										href : path() + linkQueryString,
										title : 'Get Object Schemas - Next Page',
										auth : ['hawk']
									});
								}
							}

							if(searchResults.meta.searchResult.offset > 0){
								linkQueryString = lodash.clone(request.query);
								linkQueryString.offset = params.offset - params.limit;
								if(linkQueryString.offset > 0){
									linkQueryString = '?' + querystring.stringify(linkQueryString);
									searchResults.links.push({
										rel: 'prevPage',
										href : path() + linkQueryString,
										title : 'Get Object Schemas - Previous Page',
										auth : ['hawk']
									});
								}
							}
						}
					}
				};

				addActions();
				addLinks();
				request.reply(searchResults);
				searchResults.meta.stopTimer();
			},
			function(err){
				searchResults.error = {
					code: 500,
					developerMessage : err.toString(),
					userMessages : {
            server_error : 'Failed to retrieve object schemas because of unexpected server error'
          }
				};
				request.reply(searchResults).code(searchResults.error.code);
				searchResults.meta.stopTimer();
			}
		);

	};

	return [
		{
			method: 'GET',
			path : path(),
			config: {
				description : 'Used to page through ObjectSchemas. By default, the first 10 are returned sorted by updatedOn in descending order.',
				tags : ['objectschemas','paging'],
				validate: {
					query : {
						limit : types.Number(),
						offset : types.Number(),
						sort : types.String(),
						dataFields : types.String(),
						dataOnly : types.Boolean(),
						actions : types.Boolean(),
						links : types.Boolean(),
						timeout : types.Number(),
						version : types.Boolean(),
						raw : types.Boolean()
					}
				},
				handler : getObjectSchemas.bind(null,path())
			}
		}
	];
};<|endoftext|>


<commit_before>var util = require('util'),
    ometajs = require('../../ometajs'),
    globals = ometajs.globals,
    grammars = ometajs.grammars;

//
// ### function StringBuffer(*contents)
// #### contents - any number of string arguments
// @constructor
//
function StringBuffer() {
  this.buffer = [];
  for (var i = 0; i < arguments.length; i++) {
    this.put(arguments[idx]);
  }
};
exports.StringBuffer = StringBuffer;

//
// ### function put(item)
// #### @item {String} item to put in
// Put item into buffer
//
StringBuffer.prototype.put = function put(item) {
  this.buffer.push(item);

  return this;
};

//
// ### function contents()
// Return joined contents of the buffer
//
StringBuffer.prototype.contents = function contents()  {
  return this.buffer.join('');
};

//
// ### function objectThatDelegatesTo(obj, props)
// #### @obj {Object} parent object
// #### @props {Object} object to merge result with
// Returns object with merged properties of `obj` and `props`
//
function objectThatDelegatesTo(obj, props) {
  // Regular case
  if (!(obj.prototype instanceof grammars.AbstractGrammar)) {
    var clone = Object.create(obj || {});

    Object.getOwnPropertyNames(props || {}).forEach(function(key) {
      clone[key] = props[key];
    });

    return clone;
  }

  // When old ometajs grammar is trying to inherit from the new one:
  //
  // 1. Create wrapper
  function Wrapper(source) {
    obj.call(this, source);
  }
  util.inherits(Wrapper, obj);
  Wrapper.match = grammars.AbstractGrammar.match;
  Wrapper.matchAll = grammars.AbstractGrammar.matchAll;

  var adapter = Object.create(globals.OMeta);
  adapter._genericApply = function genericApply(recv, rule, args) {
    var grmr = this._grmr;

    grmr._source = this.input.lst;
    grmr._offset = this.input.idx;

    if (grmr._rule(rule, false, args, recv === null ? undefined : recv)) {
      var res = grmr._getIntermediate();

      this.input.lst = grmr._source;
      this.input.idx = grmr._offset;

      return res;
    } else {
      throw globals.fail();
    }
  };

  adapter._apply = function wrapedApply(rule) {
    return this._genericApply(null, rule);
  };
  adapter._applyWithArgs = function wrapedApplyWithArgs(rule) {
    return this._genericApply(
      null,
      rule,
      Array.prototype.slice.call(arguments, 1)
    );
  };
  adapter._superApplyWithArgs = function wrappedSuperWithArgs(recv, rule) {
    return this._genericApply(
      recv,
      rule,
      Array.prototype.slice.call(arguments, 2)
    );
  };

  Object.getOwnPropertyNames(props || {}).forEach(function(key) {
    var method = props[key];
    Wrapper.prototype[key] = function methodWrapper() {
      // Create new OMInputStream
      adapter.input = globals.makeListOMInputStream(
        this._source,
        this._offset
      );
      adapter._grmr = this;

      // Apply rule and set result
      try {
        var result = method.call(adapter);
        this._setIntermediate(result, true);
        return true;
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
        return false;
      }
    };
  });

  return Wrapper;
};
exports.objectThatDelegatesTo = objectThatDelegatesTo;

//
// ### function isImmutable(o)
// #### @o {any} object to perform check against
// Returns true if object is immutable
//
function isImmutable(o) {
   return o === null || o === undefined ||
          typeof o === 'boolean' || typeof o === 'number' ||
          typeof o === 'string';
};
exports.isImmutable = isImmutable;

//
// ### function digitValue(str)
// #### @str {String} a string.
// Returns digit value of first character in string
//
function digitValue(str) {
  return str.charCodeAt(0) - '0'.charCodeAt(0);
};
exports.digitValue = digitValue;

//
// ### function isSequenceable(o)
// #### @o {any} object to perform check against
// Returns true if object is sequenceable
//
function isSequenceable(o) {
  return typeof o == 'string' || Array.isArray(o);
}
exports.isSequenceable = isSequenceable;

//
// ### function padNumber(num, len)
// #### @num {Number} input number
// #### @len {String} length of result
// Adds padding zeros to the left of string
//
function padNumber(num, len) {
  return new Array(len - r.length).join('0') + num.toString(16);
};

var escapeHash = {};
for (var c = 0; c < 128; c++) {
  escapeHash[c] = String.fromCharCode(c);
};

escapeHash['\''.charCodeAt(0)]  = '\\\'';
escapeHash['"'.charCodeAt(0)]  = '\\"';
escapeHash['\\'.charCodeAt(0)] = '\\\\';
escapeHash['\b'.charCodeAt(0)] = '\\b';
escapeHash['\f'.charCodeAt(0)] = '\\f';
escapeHash['\n'.charCodeAt(0)] = '\\n';
escapeHash['\r'.charCodeAt(0)] = '\\r';
escapeHash['\t'.charCodeAt(0)] = '\\t';
escapeHash['\v'.charCodeAt(0)] = '\\v';

//
// ### function escapeChar(c)
// #### @c {String}
// Escapes character with \
//
function escapeChar(c) {
  var code = c.charCodeAt(0);

  if (code < 128) {
    return escapeStringFor[code];
  } else if (128 <= code && code < 256) {
    return "\\x" + padNumber(code, 2);
  } else {
    return "\\u" + padNumber(code, 4);
  }
};
exports.escapeChar = escapeChar;

//
// ### function unescape(s)
// #### @s {String} input
// Unescape character escaped with escapeChar
//
function unescape(s) {
  if (s.charAt(0) == '\\') {
    switch (s.charAt(1)) {
      case "'":  return "'";
      case '"':  return '"';
      case '\\': return '\\';
      case 'b':  return '\b';
      case 'f':  return '\f';
      case 'n':  return '\n';
      case 'r':  return '\r';
      case 't':  return '\t';
      case 'v':  return '\v';
      case 'x':  return String.fromCharCode(parseInt(s.substring(2, 4), 16))
      case 'u':  return String.fromCharCode(parseInt(s.substring(2, 6), 16));
      default:   return s.charAt(1);
    }
  }

  return s;
};
exports.unescape = unescape;

//
// ### function getTag(o)
// #### @o {Object} input
// unique tags for objects (useful for making "hash tables")
//
function getTag(o) {
  if (o === null || o === undefined) {
    return x
  }

  switch (typeof o) {
    case "boolean":
      return o == true ? "Btrue" : "Bfalse";
    case "string":
      return "S" + o;
    case "number":
      return "N" + o;
    default:
      if (o.hasOwnProperty("_id_")) {
        return o._id_;
      } else {
        return o._id_ = "R" + getTag.id++;
      }
  }
};
getTag.id = 0;
exports.getTag = getTag;

// Lift inspect to context
exports.inspect = require('util').inspect;

//
// ### function lift(target, sources)
// #### @target {Object} object to lift properties to
// #### @sourcs {Array} source objects
// Lift all properties from source objects to target
//
exports.lift = function lift(target, sources) {
  sources.forEach(function(obj) {
    Object.keys(obj).forEach(function(key) {
      target[key] = obj[key];
    });
  });
};

//
// ### function clone (obj)
// #### @obj {Object} source
// Returns object with same property-value pairs as in source
//
exports.clone = function clone(obj) {
  var o = {};

  Object.keys(obj).forEach(function(key) {
    o[key] = obj[key];
  });

  return o;
};
<commit_msg>utils: restore input on fail<commit_after>var util = require('util'),
    ometajs = require('../../ometajs'),
    globals = ometajs.globals,
    grammars = ometajs.grammars;

//
// ### function StringBuffer(*contents)
// #### contents - any number of string arguments
// @constructor
//
function StringBuffer() {
  this.buffer = [];
  for (var i = 0; i < arguments.length; i++) {
    this.put(arguments[idx]);
  }
};
exports.StringBuffer = StringBuffer;

//
// ### function put(item)
// #### @item {String} item to put in
// Put item into buffer
//
StringBuffer.prototype.put = function put(item) {
  this.buffer.push(item);

  return this;
};

//
// ### function contents()
// Return joined contents of the buffer
//
StringBuffer.prototype.contents = function contents()  {
  return this.buffer.join('');
};

//
// ### function objectThatDelegatesTo(obj, props)
// #### @obj {Object} parent object
// #### @props {Object} object to merge result with
// Returns object with merged properties of `obj` and `props`
//
function objectThatDelegatesTo(obj, props) {
  // Regular case
  if (!(obj.prototype instanceof grammars.AbstractGrammar)) {
    var clone = Object.create(obj || {});

    Object.getOwnPropertyNames(props || {}).forEach(function(key) {
      clone[key] = props[key];
    });

    return clone;
  }

  // When old ometajs grammar is trying to inherit from the new one:
  //
  // 1. Create wrapper
  function Wrapper(source) {
    obj.call(this, source);
  }
  util.inherits(Wrapper, obj);
  Wrapper.match = grammars.AbstractGrammar.match;
  Wrapper.matchAll = grammars.AbstractGrammar.matchAll;

  var adapter = Object.create(globals.OMeta);
  adapter._genericApply = function genericApply(recv, rule, args) {
    var grmr = this._grmr,
        input = this.input;

    grmr._source = this.input.lst;
    grmr._offset = this.input.idx;

    if (grmr._rule(rule, false, args, recv === null ? undefined : recv)) {
      var res = grmr._getIntermediate();

      this.input = globals.makeListOMInputStream(
        grmr._source,
        grmr._offset
      );
      return res;
    } else {
      this.input = input;
      throw globals.fail();
    }
  };

  adapter._apply = function wrapedApply(rule) {
    return this._genericApply(null, rule);
  };
  adapter._applyWithArgs = function wrapedApplyWithArgs(rule) {
    return this._genericApply(
      null,
      rule,
      Array.prototype.slice.call(arguments, 1)
    );
  };
  adapter._superApplyWithArgs = function wrappedSuperWithArgs(recv, rule) {
    return this._genericApply(
      recv,
      rule,
      Array.prototype.slice.call(arguments, 2)
    );
  };

  Object.getOwnPropertyNames(props || {}).forEach(function(key) {
    var method = props[key];
    Wrapper.prototype[key] = function methodWrapper() {
      // Create new OMInputStream
      adapter.input = globals.makeListOMInputStream(
        this._source,
        this._offset
      );
      adapter._grmr = this;

      // Apply rule and set result
      try {
        var result = method.call(adapter);
        this._setIntermediate(result, true);
        return true;
      } catch (e) {
        if (!(e instanceof SyntaxError)) throw e;
        return false;
      }
    };
  });

  return Wrapper;
};
exports.objectThatDelegatesTo = objectThatDelegatesTo;

//
// ### function isImmutable(o)
// #### @o {any} object to perform check against
// Returns true if object is immutable
//
function isImmutable(o) {
   return o === null || o === undefined ||
          typeof o === 'boolean' || typeof o === 'number' ||
          typeof o === 'string';
};
exports.isImmutable = isImmutable;

//
// ### function digitValue(str)
// #### @str {String} a string.
// Returns digit value of first character in string
//
function digitValue(str) {
  return str.charCodeAt(0) - '0'.charCodeAt(0);
};
exports.digitValue = digitValue;

//
// ### function isSequenceable(o)
// #### @o {any} object to perform check against
// Returns true if object is sequenceable
//
function isSequenceable(o) {
  return typeof o == 'string' || Array.isArray(o);
}
exports.isSequenceable = isSequenceable;

//
// ### function padNumber(num, len)
// #### @num {Number} input number
// #### @len {String} length of result
// Adds padding zeros to the left of string
//
function padNumber(num, len) {
  return new Array(len - r.length).join('0') + num.toString(16);
};

var escapeHash = {};
for (var c = 0; c < 128; c++) {
  escapeHash[c] = String.fromCharCode(c);
};

escapeHash['\''.charCodeAt(0)]  = '\\\'';
escapeHash['"'.charCodeAt(0)]  = '\\"';
escapeHash['\\'.charCodeAt(0)] = '\\\\';
escapeHash['\b'.charCodeAt(0)] = '\\b';
escapeHash['\f'.charCodeAt(0)] = '\\f';
escapeHash['\n'.charCodeAt(0)] = '\\n';
escapeHash['\r'.charCodeAt(0)] = '\\r';
escapeHash['\t'.charCodeAt(0)] = '\\t';
escapeHash['\v'.charCodeAt(0)] = '\\v';

//
// ### function escapeChar(c)
// #### @c {String}
// Escapes character with \
//
function escapeChar(c) {
  var code = c.charCodeAt(0);

  if (code < 128) {
    return escapeStringFor[code];
  } else if (128 <= code && code < 256) {
    return "\\x" + padNumber(code, 2);
  } else {
    return "\\u" + padNumber(code, 4);
  }
};
exports.escapeChar = escapeChar;

//
// ### function unescape(s)
// #### @s {String} input
// Unescape character escaped with escapeChar
//
function unescape(s) {
  if (s.charAt(0) == '\\') {
    switch (s.charAt(1)) {
      case "'":  return "'";
      case '"':  return '"';
      case '\\': return '\\';
      case 'b':  return '\b';
      case 'f':  return '\f';
      case 'n':  return '\n';
      case 'r':  return '\r';
      case 't':  return '\t';
      case 'v':  return '\v';
      case 'x':  return String.fromCharCode(parseInt(s.substring(2, 4), 16))
      case 'u':  return String.fromCharCode(parseInt(s.substring(2, 6), 16));
      default:   return s.charAt(1);
    }
  }

  return s;
};
exports.unescape = unescape;

//
// ### function getTag(o)
// #### @o {Object} input
// unique tags for objects (useful for making "hash tables")
//
function getTag(o) {
  if (o === null || o === undefined) {
    return x
  }

  switch (typeof o) {
    case "boolean":
      return o == true ? "Btrue" : "Bfalse";
    case "string":
      return "S" + o;
    case "number":
      return "N" + o;
    default:
      if (o.hasOwnProperty("_id_")) {
        return o._id_;
      } else {
        return o._id_ = "R" + getTag.id++;
      }
  }
};
getTag.id = 0;
exports.getTag = getTag;

// Lift inspect to context
exports.inspect = require('util').inspect;

//
// ### function lift(target, sources)
// #### @target {Object} object to lift properties to
// #### @sourcs {Array} source objects
// Lift all properties from source objects to target
//
exports.lift = function lift(target, sources) {
  sources.forEach(function(obj) {
    Object.keys(obj).forEach(function(key) {
      target[key] = obj[key];
    });
  });
};

//
// ### function clone (obj)
// #### @obj {Object} source
// Returns object with same property-value pairs as in source
//
exports.clone = function clone(obj) {
  var o = {};

  Object.keys(obj).forEach(function(key) {
    o[key] = obj[key];
  });

  return o;
};
<|endoftext|>


<commit_before>var Promise = require('bluebird');
var _ = require('underscore');

var parser = require('./parser');
var errors = require('../errors');

// The following functions are adapted from PEG.js because it currently doesn't
// expose this functionality (and I'm not 100% sure exposing it is a good idea).
// This is the exact code taken as a base:
//
//     https://github.com/pegjs/pegjs/blob/eaca5f0acf97b66ef141fed84aa95d4e72e33757/lib/compiler/passes/generate-javascript.js#L1075-L1119
//

function buildExpectedDescription(expected) {
    var descriptions = _.pluck(expected, 'description');

    return descriptions.length > 1
        ? descriptions.slice(0, -1).join(', ')
              + ' or '
              + descriptions[expected.length - 1]
        : descriptions[0];
}

function buildFoundDescription(found) {
    function stringEscape(s) {
        function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }

        // ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
        // string literal except for the closing quote character, backslash,
        // carriage return, line separator, paragraph separator, and line feed.
        // Any character may appear in the form of an escape sequence.
        //
        // For portability, we also escape all control and non-ASCII characters.
        // Note that "\0" and "\v" escape sequences are not used because JSHint
        // does not like the first and IE the second.
        return s
            .replace(/\\/g,   '\\\\')   // backslash
            .replace(/"/g,    '\\"')    // closing double quote
            .replace(/\x08/g, '\\b')    // backspace
            .replace(/\t/g,   '\\t')    // horizontal tab
            .replace(/\n/g,   '\\n')    // line feed
            .replace(/\f/g,   '\\f')    // form feed
            .replace(/\r/g,   '\\r')    // carriage return
            .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
            .replace(/[\x10-\x1F\x80-\xFF]/g,    function(ch) { return '\\x'  + hex(ch); })
            .replace(/[\u0100-\u0FFF]/g,         function(ch) { return '\\u0' + hex(ch); })
            .replace(/[\u1000-\uFFFF]/g,         function(ch) { return '\\u'  + hex(ch); });
    }

    return found ? '"' + stringEscape(found) + '"' : 'end of input';
}

function buildMessage(expectedDescription, foundDescription) {
    return 'Expected ' + expectedDescription + ' but ' + foundDescription + ' found.';
}

function processSyntaxError(error, filename) {
    var location = _.extend({ filename: filename }, error.location);
    var expected, expectedDescription, found, foundDescription, message;

    if (error.expected) {
        // Massage the expectations so that they don't refer to trivialities
        // such as whitespace or comments. Arguably, this should be possible to
        // do in PEG.js itself, but it currently isn't. Also, it's a gross hack,
        // but we'll do everything for our users :-)
        expected = _.reject(error.expected, function(expectation) {
            return expectation.description === 'whitespace'
                || expectation.description === 'end of line'
                || expectation.description === 'comment';
        });
        expectedDescription = buildExpectedDescription(expected);
        found = error.found;
        foundDescription = buildFoundDescription(found);
        message = buildMessage(expectedDescription, foundDescription);

        throw errors.syntaxError('JUTTLE-SYNTAX-ERROR-WITH-EXPECTED', {
            expected: expected,
            expectedDescription: expectedDescription,
            found: found,
            foundDescription: foundDescription,
            location: location
        });
    } else {
        throw errors.syntaxError('JUTTLE-SYNTAX-ERROR-WITHOUT-EXPECTED', {
            message: error.message,
            location: location
        });
    }
}

function doParse(source, options) {
    try {
        return parser.parse(source, options);
    } catch (e) {
        if (e instanceof parser.SyntaxError) {
            processSyntaxError(e, options.filename);
        } else {
            throw e;
        }
    }
}

/*
 parseFilter(source)
 ===================

 Takes `source` with a filter expression source code and parses
 it into an AST synchronously.
 */
function parseFilter(source) {
    return parseSync(source, { startRule: "startFilter" });
}

function checkImportNode(node) {
    if (node.modulename.type !== 'StringLiteral') {
        throw errors.compileError('RT-IMPORT-INTERPOLATION', {
            location: node.modulename.location
        });
    }
}

function processOptions(options, defaultResolver) {
    if (options === undefined) {
        options = {};
    }

    return _.defaults(_.clone(options), {
        filename: 'main',
        startRule: 'start',
        autocompleteCallback: null,
        modules: {},
        moduleResolver: defaultResolver
    });
}

function extractImports(ast) {
    return _.filter(ast.elements, function(element) {
        return element.type === 'ImportStatement';
    });
}

function buildModuleDefs(asts) {
    return _.map(_.omit(asts, 'main'), function(ast, name) {
        return { type: "ModuleDef", name: name, elements: ast.elements };
    });
}

/*
 parseValue(source)
 ==================

 Takes `source` with a Juttle value expression (a literal
 without any external references such as variables) and parses
 it into an AST synchronously.
 */
function parseValue(source) {
    return parseSync(source, { startRule: "startValue" });
}

/*
 parse(source)
 =============

 Takes `source` and returns a promise that is fulfilled with the
 resulting AST. This method supports Juttle programs that use
 modules if `moduleResolver` is specified when `JuttleParser` is
 instantiated.
 */
function parse(juttle, options) {
    var asts = {};

    function defaultResolver(path, name) {
        if (_.has(options.modules, path)) {
            return Promise.resolve({
                name: path,
                source: options.modules[path]
            });
        } else {
            return Promise.reject(new Error('Could not find module: ' + path));
        }
    }

    function resolveImport(import_) {
        return options.moduleResolver(import_.modulename.value, import_.localname)
            .catch(function(err) {
                throw errors.compileError('RT-MODULE-NOT-FOUND', {
                    module: import_.modulename.value,
                    location: import_.location
                });
            });
    }

    function parseModule(module) {
        // Don't break on cyclic imports and continue. There is a more proper
        // check in the semantic pass.
        if (_.has(asts, module.name)) {
            return Promise.resolve();
        }

        return Promise.resolve(module.source)
            .then(function(juttle) {
                return doParse(juttle, _.extend(options, { filename: module.name }));
            })
            .then(function(ast) {
                asts[module.name] = ast;

                return ast;
            })
            .then(extractImports)
            .each(checkImportNode)
            .map(resolveImport)
            .each(parseModule);
    }

    options = processOptions(options, defaultResolver);

    return parseModule({ source: juttle, name: 'main' })
        .then(function() {
            asts.main.modules = buildModuleDefs(asts);

            return asts.main;
        });
}

function parseSync(juttle, options) {
    var asts = {};

    function defaultResolver(path, name) {
        if (_.has(options.modules, path)) {
            return {
                name: path,
                source: options.modules[path]
            };
        } else {
            throw new Error("Could not find module: " + path);
        }
    }

    function resolveImport(import_) {
        try {
            return options.moduleResolver(import_.modulename.value, import_.localname);
        } catch (e) {
            throw errors.compileError('RT-MODULE-NOT-FOUND', {
                module: import_.modulename.value,
                location: import_.location
            });
        }
    }

    function parseModule(module) {
        // Don't break on cyclic imports and continue. There is a more proper
        // check in the semantic pass.
        if (_.has(asts, module.name)) {
            return;
        }

        var ast = doParse(module.source, _.extend(options, { filename: module.name }));
        asts[module.name] = ast;

        var imports = extractImports(ast);
        _.chain(imports)
            .each(checkImportNode)
            .map(resolveImport)
            .each(parseModule);
    }

    options = processOptions(options, defaultResolver);

    parseModule({ source: juttle, name: 'main' });
    asts.main.modules = buildModuleDefs(asts);

    return asts.main;
}

module.exports = {
    parseSync: parseSync,
    parseFilter: parseFilter,
    parseValue: parseValue,
    parse: parse,
};
<commit_msg>parser: Reorder functions by their call graph<commit_after>var Promise = require('bluebird');
var _ = require('underscore');

var parser = require('./parser');
var errors = require('../errors');

/*
 parseFilter(source)
 ===================

 Takes `source` with a filter expression source code and parses
 it into an AST synchronously.
 */
function parseFilter(source) {
    return parseSync(source, { startRule: "startFilter" });
}

/*
 parseValue(source)
 ==================

 Takes `source` with a Juttle value expression (a literal
 without any external references such as variables) and parses
 it into an AST synchronously.
 */
function parseValue(source) {
    return parseSync(source, { startRule: "startValue" });
}

/*
 parse(source)
 =============

 Takes `source` and returns a promise that is fulfilled with the
 resulting AST. This method supports Juttle programs that use
 modules if `moduleResolver` is specified when `JuttleParser` is
 instantiated.
 */
function parse(juttle, options) {
    var asts = {};

    function defaultResolver(path, name) {
        if (_.has(options.modules, path)) {
            return Promise.resolve({
                name: path,
                source: options.modules[path]
            });
        } else {
            return Promise.reject(new Error('Could not find module: ' + path));
        }
    }

    function resolveImport(import_) {
        return options.moduleResolver(import_.modulename.value, import_.localname)
            .catch(function(err) {
                throw errors.compileError('RT-MODULE-NOT-FOUND', {
                    module: import_.modulename.value,
                    location: import_.location
                });
            });
    }

    function parseModule(module) {
        // Don't break on cyclic imports and continue. There is a more proper
        // check in the semantic pass.
        if (_.has(asts, module.name)) {
            return Promise.resolve();
        }

        return Promise.resolve(module.source)
            .then(function(juttle) {
                return doParse(juttle, _.extend(options, { filename: module.name }));
            })
            .then(function(ast) {
                asts[module.name] = ast;

                return ast;
            })
            .then(extractImports)
            .each(checkImportNode)
            .map(resolveImport)
            .each(parseModule);
    }

    options = processOptions(options, defaultResolver);

    return parseModule({ source: juttle, name: 'main' })
        .then(function() {
            asts.main.modules = buildModuleDefs(asts);

            return asts.main;
        });
}

function parseSync(juttle, options) {
    var asts = {};

    function defaultResolver(path, name) {
        if (_.has(options.modules, path)) {
            return {
                name: path,
                source: options.modules[path]
            };
        } else {
            throw new Error("Could not find module: " + path);
        }
    }

    function resolveImport(import_) {
        try {
            return options.moduleResolver(import_.modulename.value, import_.localname);
        } catch (e) {
            throw errors.compileError('RT-MODULE-NOT-FOUND', {
                module: import_.modulename.value,
                location: import_.location
            });
        }
    }

    function parseModule(module) {
        // Don't break on cyclic imports and continue. There is a more proper
        // check in the semantic pass.
        if (_.has(asts, module.name)) {
            return;
        }

        var ast = doParse(module.source, _.extend(options, { filename: module.name }));
        asts[module.name] = ast;

        var imports = extractImports(ast);
        _.chain(imports)
            .each(checkImportNode)
            .map(resolveImport)
            .each(parseModule);
    }

    options = processOptions(options, defaultResolver);

    parseModule({ source: juttle, name: 'main' });
    asts.main.modules = buildModuleDefs(asts);

    return asts.main;
}

function processOptions(options, defaultResolver) {
    if (options === undefined) {
        options = {};
    }

    return _.defaults(_.clone(options), {
        filename: 'main',
        startRule: 'start',
        autocompleteCallback: null,
        modules: {},
        moduleResolver: defaultResolver
    });
}

function extractImports(ast) {
    return _.filter(ast.elements, function(element) {
        return element.type === 'ImportStatement';
    });
}

function checkImportNode(node) {
    if (node.modulename.type !== 'StringLiteral') {
        throw errors.compileError('RT-IMPORT-INTERPOLATION', {
            location: node.modulename.location
        });
    }
}

function buildModuleDefs(asts) {
    return _.map(_.omit(asts, 'main'), function(ast, name) {
        return { type: "ModuleDef", name: name, elements: ast.elements };
    });
}

function doParse(source, options) {
    try {
        return parser.parse(source, options);
    } catch (e) {
        if (e instanceof parser.SyntaxError) {
            processSyntaxError(e, options.filename);
        } else {
            throw e;
        }
    }
}

function processSyntaxError(error, filename) {
    var location = _.extend({ filename: filename }, error.location);
    var expected, expectedDescription, found, foundDescription, message;

    if (error.expected) {
        // Massage the expectations so that they don't refer to trivialities
        // such as whitespace or comments. Arguably, this should be possible to
        // do in PEG.js itself, but it currently isn't. Also, it's a gross hack,
        // but we'll do everything for our users :-)
        expected = _.reject(error.expected, function(expectation) {
            return expectation.description === 'whitespace'
                || expectation.description === 'end of line'
                || expectation.description === 'comment';
        });
        expectedDescription = buildExpectedDescription(expected);
        found = error.found;
        foundDescription = buildFoundDescription(found);
        message = buildMessage(expectedDescription, foundDescription);

        throw errors.syntaxError('JUTTLE-SYNTAX-ERROR-WITH-EXPECTED', {
            expected: expected,
            expectedDescription: expectedDescription,
            found: found,
            foundDescription: foundDescription,
            location: location
        });
    } else {
        throw errors.syntaxError('JUTTLE-SYNTAX-ERROR-WITHOUT-EXPECTED', {
            message: error.message,
            location: location
        });
    }
}

// The following functions are adapted from PEG.js because it currently doesn't
// expose this functionality (and I'm not 100% sure exposing it is a good idea).
// This is the exact code taken as a base:
//
//     https://github.com/pegjs/pegjs/blob/eaca5f0acf97b66ef141fed84aa95d4e72e33757/lib/compiler/passes/generate-javascript.js#L1075-L1119
//

function buildExpectedDescription(expected) {
    var descriptions = _.pluck(expected, 'description');

    return descriptions.length > 1
        ? descriptions.slice(0, -1).join(', ')
              + ' or '
              + descriptions[expected.length - 1]
        : descriptions[0];
}

function buildFoundDescription(found) {
    function stringEscape(s) {
        function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); }

        // ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
        // string literal except for the closing quote character, backslash,
        // carriage return, line separator, paragraph separator, and line feed.
        // Any character may appear in the form of an escape sequence.
        //
        // For portability, we also escape all control and non-ASCII characters.
        // Note that "\0" and "\v" escape sequences are not used because JSHint
        // does not like the first and IE the second.
        return s
            .replace(/\\/g,   '\\\\')   // backslash
            .replace(/"/g,    '\\"')    // closing double quote
            .replace(/\x08/g, '\\b')    // backspace
            .replace(/\t/g,   '\\t')    // horizontal tab
            .replace(/\n/g,   '\\n')    // line feed
            .replace(/\f/g,   '\\f')    // form feed
            .replace(/\r/g,   '\\r')    // carriage return
            .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); })
            .replace(/[\x10-\x1F\x80-\xFF]/g,    function(ch) { return '\\x'  + hex(ch); })
            .replace(/[\u0100-\u0FFF]/g,         function(ch) { return '\\u0' + hex(ch); })
            .replace(/[\u1000-\uFFFF]/g,         function(ch) { return '\\u'  + hex(ch); });
    }

    return found ? '"' + stringEscape(found) + '"' : 'end of input';
}

function buildMessage(expectedDescription, foundDescription) {
    return 'Expected ' + expectedDescription + ' but ' + foundDescription + ' found.';
}

module.exports = {
    parseSync: parseSync,
    parseFilter: parseFilter,
    parseValue: parseValue,
    parse: parse,
};
<|endoftext|>


<commit_before>(function(){

// http://jsfiddle.net/k5Ywe/31/
function merge(o1, o2) {
	for (var p in o2) {
		o1[p] = (
			o1[p] != null
			&& o2[p] != null
			&& !Array.isArray(o1[p])
			&& !Array.isArray(o2[p])
			&& typeof o1[p] == 'object'
			&& typeof o2[p] == 'object'
		) ? merge(o1[p], o2[p]) : o2[p];
	}
	return o1;
}

// from https://github.com/csuwldcat/mootools-core/blob/2b9703c3bbd467020a39e56e383e08033ba310d9/Source/Types/Object.js#L23
function get(object, path){
	if (typeof path == 'string') path = path.split('.');
	for (var i = 0, l = path.length; i < l; i++){
		if (hasOwnProperty.call(object, path[i])) object = object[path[i]];
		else return object[path[i]];
	}
	return object;
}

function set(object, path, value){
	if (typeof path == 'string') path = path.split('.');
	var key = path.pop(),
		len = path.length,
		i = 0,
		current;
	while (len--){
		current = path[i++];
		object = current in object ? object[current] : (object[current] = {});
	}
	object[key] = value;
	path.push(key); // harmonize path
	return object;
}

exports.State = {

	register: function(server){
		server.$state = {};
		server.$locks = {};
		server.$timeouts = {};
	},

	send: function(server, event, data){
		server.$socket.sockets.emit(event, data);
		return this;
	},

	broadcast: function(conn, event, data){
		conn.broadcast.emit(event, data);
		return this;
	},

	reply: function(conn, event, data){
		conn.emit(event, data);
		return this;
	},

	error: function(conn, name, data){
		conn.emit('error', name, data);
		return this;
	},

	onClientConnect: function(server, conn){
		conn.on('update', this.onUpdate.bind(this, server, conn));
		conn.on('put', this.onPut.bind(this, server, conn));
		conn.on('delete', this.onDelete.bind(this, server, conn));
		conn.on('acquire lock', this.onAcquireLock.bind(this, server, conn));
		conn.on('release lock', this.onReleaseLock.bind(this, server, conn));
		this.reply(conn, 'initial state', server.$state);
	},

	onUpdate: function(server, conn, data){
		if (data == null || data.value == null){
			return this.error(conn, 'update error', data);
		}
		if (data.key == null && data.path == null){
			return this.error(conn, 'update error', data);
		}
		if (server.$locks[data.key] && server.$locks[data.key] !== conn.id){
			return this.error(conn, 'lock error', data.key);
		}

		if (data.path != null) console.log('data.path', data.path);

		if (data.path != null) set(server.$state, data.path, data.value);
		else server.$state[data.key] = data.value;

		if (data.path != null) console.log('data.path', data.path);

		this.send(server, 'state update', data);
	},

	onPut: function(server, conn, data){
		merge(server.$state, data);
		this.send(server, 'put', data);
	},

	onDelete: function(server, conn, key){
		if (server.$state[key] == null){
			return this.error(conn, 'delete error', key);
		}
		delete server.$state[key];
		this.releaseLock(server, conn, key);
		this.send(server, 'delete', key);
	},

	onAcquireLock: function(server, conn, key){
		var locks = server.$locks,
			timeouts = server.$timeouts;

		if (locks[key] && locks[key] != conn.id){
			return this.error(conn, 'acquire lock error', key);
		}
		if (timeouts[key]) clearTimeout(timeouts[key]);
		this.acquireLock(server, conn, key);
		timeouts[key] = setTimeout(this.releaseLock.bind(this, server, conn, key), 20000);
	},

	acquireLock: function(server, conn, key){
		server.$locks[key] = conn.id;
		this.reply(conn, 'lock acquired', key);
		this.broadcast(conn, 'lock key', key);
		server.emit('keyLocked', server, key, conn.id);
		return this;
	},

	onReleaseLock: function(server, conn, key){
		if (server.$locks[key] != conn.id){
			return this.error(conn, 'release lock error', key);
		}
		this.releaseLock(server, conn, key);
	},

	releaseLock: function(server, conn, key){
		var locks = server.$locks,
			timeouts = server.$timeouts;

		if (locks[key] == conn.id){
			if (timeouts[key]) clearTimeout(timeouts[key]);
			delete locks[key];
			delete timeouts[key];
			this.reply(conn, 'lock released', key);
			this.send(server, 'release key', key);
			server.emit('keyReleased', server, key, conn.id);
		}
		return this;
	},

	onClientDisconnect: function(server, conn){
		this.clearLocks(server, conn);
	},

	clearLocks: function(server, conn){
		for (var key in server.$locks){
			this.releaseLock(server, conn, key);
		}
		return this;
	}

};

})();
<commit_msg>cleanup<commit_after>(function(){

// http://jsfiddle.net/k5Ywe/31/
function merge(o1, o2) {
	for (var p in o2) {
		o1[p] = (
			o1[p] != null
			&& o2[p] != null
			&& !Array.isArray(o1[p])
			&& !Array.isArray(o2[p])
			&& typeof o1[p] == 'object'
			&& typeof o2[p] == 'object'
		) ? merge(o1[p], o2[p]) : o2[p];
	}
	return o1;
}

// from https://github.com/csuwldcat/mootools-core/blob/2b9703c3bbd467020a39e56e383e08033ba310d9/Source/Types/Object.js#L23
function get(object, path){
	if (typeof path == 'string') path = path.split('.');
	for (var i = 0, l = path.length; i < l; i++){
		if (hasOwnProperty.call(object, path[i])) object = object[path[i]];
		else return object[path[i]];
	}
	return object;
}

function set(object, path, value){
	if (typeof path == 'string') path = path.split('.');
	var key = path.pop(),
		len = path.length,
		i = 0,
		current;
	while (len--){
		current = path[i++];
		object = current in object ? object[current] : (object[current] = {});
	}
	object[key] = value;
	path.push(key); // harmonize path
	return object;
}

exports.State = {

	register: function(server){
		server.$state = {};
		server.$locks = {};
		server.$timeouts = {};
	},

	send: function(server, event, data){
		server.$socket.sockets.emit(event, data);
		return this;
	},

	broadcast: function(conn, event, data){
		conn.broadcast.emit(event, data);
		return this;
	},

	reply: function(conn, event, data){
		conn.emit(event, data);
		return this;
	},

	error: function(conn, name, data){
		conn.emit('error', name, data);
		return this;
	},

	onClientConnect: function(server, conn){
		conn.on('update', this.onUpdate.bind(this, server, conn));
		conn.on('put', this.onPut.bind(this, server, conn));
		conn.on('delete', this.onDelete.bind(this, server, conn));
		conn.on('acquire lock', this.onAcquireLock.bind(this, server, conn));
		conn.on('release lock', this.onReleaseLock.bind(this, server, conn));
		this.reply(conn, 'initial state', server.$state);
	},

	onUpdate: function(server, conn, data){
		if (data == null || data.value == null){
			return this.error(conn, 'update error', data);
		}
		if (data.key == null && data.path == null){
			return this.error(conn, 'update error', data);
		}
		if (server.$locks[data.key] && server.$locks[data.key] !== conn.id){
			return this.error(conn, 'lock error', data.key);
		}
		if (data.path != null) set(server.$state, data.path, data.value);
		else server.$state[data.key] = data.value;

		this.send(server, 'state update', data);
	},

	onPut: function(server, conn, data){
		merge(server.$state, data);
		this.send(server, 'put', data);
	},

	onDelete: function(server, conn, key){
		if (server.$state[key] == null){
			return this.error(conn, 'delete error', key);
		}
		delete server.$state[key];
		this.releaseLock(server, conn, key);
		this.send(server, 'delete', key);
	},

	onAcquireLock: function(server, conn, key){
		var locks = server.$locks,
			timeouts = server.$timeouts;

		if (locks[key] && locks[key] != conn.id){
			return this.error(conn, 'acquire lock error', key);
		}
		if (timeouts[key]) clearTimeout(timeouts[key]);
		this.acquireLock(server, conn, key);
		timeouts[key] = setTimeout(this.releaseLock.bind(this, server, conn, key), 20000);
	},

	acquireLock: function(server, conn, key){
		server.$locks[key] = conn.id;
		this.reply(conn, 'lock acquired', key);
		this.broadcast(conn, 'lock key', key);
		server.emit('keyLocked', server, key, conn.id);
		return this;
	},

	onReleaseLock: function(server, conn, key){
		if (server.$locks[key] != conn.id){
			return this.error(conn, 'release lock error', key);
		}
		this.releaseLock(server, conn, key);
	},

	releaseLock: function(server, conn, key){
		var locks = server.$locks,
			timeouts = server.$timeouts;

		if (locks[key] == conn.id){
			if (timeouts[key]) clearTimeout(timeouts[key]);
			delete locks[key];
			delete timeouts[key];
			this.reply(conn, 'lock released', key);
			this.send(server, 'release key', key);
			server.emit('keyReleased', server, key, conn.id);
		}
		return this;
	},

	onClientDisconnect: function(server, conn){
		this.clearLocks(server, conn);
	},

	clearLocks: function(server, conn){
		for (var key in server.$locks){
			this.releaseLock(server, conn, key);
		}
		return this;
	}

};

})();
<|endoftext|>