Skip to content

Instantly share code, notes, and snippets.

@james-jlo-long
Last active January 10, 2017 17:37
Show Gist options
  • Save james-jlo-long/2397294cb34dff205629be2103442bf8 to your computer and use it in GitHub Desktop.
Save james-jlo-long/2397294cb34dff205629be2103442bf8 to your computer and use it in GitHub Desktop.
A manager for URL query strings
/*
TODO: Allow Query#parseString to correctly parse square brackets.
TODO: Handle nested objects correctly.
*/
var Query = (function () {
"use strict";
var hasOwn = Object.prototype.hasOwnProperty;
// Checks to see if the given object is an object. Will return true for
// arrays as well.
var isObject = function (object) {
return object !== null && typeof object === "object";
};
// Converts any element into a string, an empty string for empty values.
var interpretString = function (string) {
return (string === null || string === undefined)
? ""
: String(string);
};
/**
* class Query
*
* Parses query strings and allows them to be manipulated. It does not set
* any query string (so if the current URL is used, any changes will still
* need to be set) but it allows the data to be managed.
*
* [[Query#has]], [[Query#set]], [[Query#get]], [[Query#add]] and
* [[Query.unset]] will allow [[Query#data]] to be manipulated.
* [[Query#parseString]] can be used to load a query string and parse it.
* [[Query#toString]] will convert [[Query#data]] into a query string.
**/
var Query = function () {
return this.init.apply(this, arguments);
};
Query.prototype = {
/**
* new Query([data])
* - data (String|Object): Optional initial data.
*
* `data` can be provided as either an object or a string. In either
* case it will be used to populate [[Query#data]].
*
* var q1 = new Query();
* q1.data; // -> {}
* var q2 = new Query({foo: 1, bar: 2});
* q2.data; // -> {foo: 1, bar: 2}
* var q3 = new Query("foo=1&bar=2");
* q3.data; // -> {foo: 1, bar: 2}
*
* In the examples gives, `q2` is a short-cut for calling [[Query#set]]
* and `q3` is a short-cut for calling [[Query#parseString]]. See those
* function descriptions for full details.
**/
init: function (data) {
/**
* Query#data -> Object
*
* The data for the query string. Try not to manipulate this
* directly. Instead, use the helper methods [[Query#set]],
* [[Query#get]], [[Query#add]] and [[Query.unset]]. The data can
* be checked using either [[Query#has]] or [[Query#keys]] and
* [[Query#each]] will iterate over the data.
*
* In a future version of this object, [[Query#data]] may become
* hidden.
**/
this.data = {};
this.setArrayType(Query.ARRAY_KEYS_REPEAT);
if (typeof data === "string") {
this.parseString(data);
} else if (isObject(data)) {
this.set(data);
}
},
/** chainable
* Query#setArrayType(type) -> Query
* - type (Number): Array type.
*
* Sets the array handling type, changing the way that arrays are
* handled when converting the query into a string. There are currently
* 3 possibilities:
*
* **[[Query.ARRAY_KEYS_REPEAT]] (default)**
*
* This setting causes the array keys to simply repeat.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_REPEAT)
* .toString();
* // -> "foo=1&foo=2"
*
* **[[Query.ARRAY_KEYS_BRACKET]]**
*
* This setting causes the array keys to include empty square brackets
* after them.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_BRACKET)
* .toString();
* // -> "foo[]=1&foo[]=2"
*
* **[[Query.ARRAY_KEYS_INDEX]]**
*
* This setting causes the array keys to include square brackets
* containing the index after them.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_BRACKET)
* .toString();
* // -> "foo[0]=1&foo[1]=2"
*
* If a value of `type` is given and not recognised,
* [[Query.ARRAY_KEYS_REPEAT]] will be assumed.
**/
setArrayType: function (type) {
/**
* Query#arrayType -> Number
*
* A flag detailing how to handle array keys when converting the
* query into a string. See [[Query#setArrayType]] for full
* details.
**/
this.arrayType = type;
return this;
},
/**
* Query#has(key) -> Boolean
* - key (String): Key to check.
*
* Checks to see if the given key exists in [[Query#data]].
*
* var q = new Query();
* q.set("foo", "alpha");
* q.has("foo"); // -> true
* q.has("bar"); // -> false
*
* Be aware that data is decoded from the URL component so a raw key
* should be checked instead of the original query value.
*
* q.parseString("foo%20bar=bravo");
* q.has("foo bar"); // -> true
* q.has("foo%20bar"); // -> false
*
**/
has: function (key) {
return hasOwn.call(this.data, key);
},
/** chainable
* Query#set(data) -> Query
* Query#set(key[, value]) -> Query
* - data (Object): Key/Value pairs for setting.
* - key (String): Key to set.
* - value (?): Optional value for the `key`.
*
* Sets information in [[Query#data]]. The instance is returned to
* allow for chaining.
*
* var q = new Query();
* q.set("foo", "alpha");
* q.has("foo"); // -> true
*
* q.set("bar", "bravo").set("baz", "charlie");
* q.has("bar"); // -> true
* q.has("baz"); // -> true
*
* Data can be passed as an object to speed up adding information.
*
* q.set({
* blip: "delta",
* blop: ["echo", "foxtrot"]
* });
* q.has("blip"); // -> true
* q.has("blop"); // -> true
*
* If the same data is set again, the value is overridden.
*
* q.get("foo"); // -> "alpha"
* q.set("foo", "golf");
* q.get("foo"); // -> "golf"
* q.set({
* foo: "hotel"
* });
* q.get("foo"); // -> "hotel"
*
**/
set: function (key, value) {
if (isObject(key)) {
Object.keys(key).forEach(function (part) {
this.set(part, key[part]);
}, this);
} else {
this.data[key] = value;
}
return this;
},
/**
* Query#get(keys) -> Object
* Query#get(key) -> ?
* - keys (Array): Array of keys to access.
* - key (String): Key to access.
*
* Gets the value from [[Query#data]]. If the data cannot be found,
* `undefined` is returned.
*
* var q = new Query();
* q.set("foo", "alpha");
* q.get("foo"); // -> "alpha"
* q.get("bar"); // -> undefined
*
* If [[Query#get]] is passed an array, the entries are returned as an
* object.
*
* q.get(["foo", "bar"]); // -> {foo: "alpha", bar: undefined}
*
* This means that a shallow clone of [[Query#data]] can be returned
* by combining [[Query#get]] and [[Query#keys]].
*
* q.get(q.keys()); // -> {foo: "alpha"}
*
* Be aware that it is possible for a query string to have a key but
* not a value. In that situation, `undefined` would also be returned.
* Use [[Query#has]] to ensure that the key exists but is empty.
*
* q.parseString("baz=&blip=something");
* q.get("blip"); // -> "something"
* q.get("baz"); // -> undefined
* q.has("baz"); // -> true
* q.has("bar"); // -> false
*
**/
get: function (key) {
var result = this.data[key];
if (Array.isArray(key)) {
result = {};
key.forEach(function (k) {
result[k] = this.get(k);
}, this);
}
return result;
},
/** chainable
* Query#add(data) -> Query
* Query#add(key, value) -> Query
* - data (Object): Key/Value pairs for setting.
* - key (String): Key to add.
* - value (?): Value for the `key`.
*
* Adds a value to [[Query#data]]. Unlike [[Query#set]] which will
* replace an existing value, [[Query#add]] will add the value and
* preserve existing data. Since it will also create data if the data
* does not exist, it can be more useful than [[Query#set]]. The data
* created by [[Query#add]] will always be an array even if there is
* only one value. This is normalised when converted into a string
* using [[Query#toString]].
*
* var q = new Query();
*
* q.set("foo", "alpha");
* q.get("foo"); // -> "alpha"
* q.set("foo", "bravo");
* q.get("foo"); // -> "bravo"
*
* q.add("bar", "alpha");
* q.get("bar"); // -> ["alpha"]
* q.add("bar", "bravo");
* q.get("bar"); // -> ["alpha", "bravo"]
*
* The instance is returned to allow for chaining.
**/
add: function (key, value) {
if (isObject(key)) {
Object.keys(key).forEach(function (part) {
this.add(part, key[part]);
}, this);
} else {
if (!this.has(key)) {
this.set(key, []);
} else if (!this.isArray(key)) {
this.set(key, [this.get(key)]);
}
this.data[key].push(value);
}
return this;
},
/**
* Query#unset(key[, index]) -> Boolean
* - key (String): Key to unset.
* - index (Number): Optional index for `key`.
*
* Unsets data from [[Query#data]]. Unsetting data that does not exist
* will not cause any errors.
*
* var q = new Query();
* q.parseString("foo=alpha&bar=bravo&bar=charlie");
* q.get("foo"); // -> "alpha"
* q.get("bar"); // -> ["bravo", "charlie"]
* q.unset("foo"); // -> true
* q.has("foo"); // -> false
* q.unset("foo"); // -> false
* q.has("foo"); // -> false
*
* If `index` is provided, is a number and `key` refers to an array
* then that entry in the array is removed instead of the entire value.
* Attempting to remove an entry in an array that does not exist will
* not cause any errors.
*
* q.get("bar"); // -> ["bravo", "charlie"]
* q.unset("bar", 1); // -> true
* q.get("bar"); // -> ["bravo"]
* q.unset("bar", 10); // -> false
* q.get("bar"); // -> ["bravo"]
*
* A boolean is returned that is `true` if data was successfully
* removed and `false` otherwise.
**/
unset: function (key, index) {
var existed = this.has(key);
if (this.isArray(key) && (/^\d+$/).test(index)) {
existed = hasOwn.call(this.data[key], index);
this.data[key].splice(index, 1);
} else {
delete this.data[key];
}
return existed;
},
/**
* Query#isArray(key) -> Boolean
* - key (String): Key to check.
*
* Checks to see if the `key` refers to an array in [[Query#data]].
*
* var q = new Query();
* q.parseString("foo=alpha&bar=bravo&bar=charlie");
* q.isArray("foo"); // -> false
* q.isArray("bar"); // -> true
* q.set("baz", "delta");
* q.add("blip", "echo");
* q.isArray("baz"); // -> false
* q.isArray("blip"); // -> true
*
**/
isArray: function (key) {
return this.has(key) && Array.isArray(this.get(key));
},
/**
* Query#keys() -> Array
*
* Returns all the keys from [[Query#data]].
*
* var q = new Query();
* q.parseString("foo=alpha&bar=bravo&bar=charlie");
* q.keys(); // -> ["foo", "bar"]
*
**/
keys: function () {
return Object.keys(this.data);
},
/**
* Query#each(handler[, context])
* - handler (Function): Function to execute.
* - context (?): Optional context for `handler`.
*
* Executes a function on all entries in [[Query#data]]. The `handler`
* function is passed the `key` and the `value` for each entry. Be
* aware that the order of execution is not guarenteed.
*
* var q = new Query();
* q.parseString("foo=alpha&bar=bravo&bar=charlie");
* q.each(function (key, value) {
* console.log("Query#data[%s] = %o", key, value);
* });
* // Logs: "Query#data[foo] = "alpha"
* // Logs: "Query#data[bar] = ["bravo", "charlie"]
*
**/
each: function (handler, context) {
this.keys().forEach(function (key) {
handler.call(context, key, this.data[key]);
}, this);
},
/**
* Query#parseString(string[, isOverride = false])
* - string (String): Query string to parse.
* - isOverride (Boolean): Whether a match should be added to (`false`)
* or replaced (`true`).
*
* Parses the given query string and adds all information to
* [[Query#data]]. Care is taken so that single entries in the `string`
* are not converted into arrays in [[Query#data]]. The keys and values
* are URL decoded. Ampersands are normalised from HTML entities thus
* `&` and `&` are considered the same.
*
* var q = new Query();
* q.parseString("foo=a&bar=b&bar=c&baz%20blip=d");
* q.keys(); // -> ["foo", "bar", "baz blip"]
* q.get("foo"); // -> "a"
* q.get("bar"); // -> ["b", "c"]
* q.get("baz blip"); // -> "d"
*
* When parsing a string, if a similar value is found, the value is
* converted into an array (if it is not already) and the second value
* is added to that array. You can see that happening with the `"bar"`
* key in the example above. To change that behaviour so that a second
* key replaces the value of the first, set `isOverride` to `true`.
*
* var q1 = new Query();
* q1.parseString("bar=1&bar=2");
* q1.get("bar"); // -> ["1", "2"]
* var q2 = new Query();
* q2.parseString("bar=1&bar=2", true);
* q1.get("bar"); // -> "2"
*
**/
parseString: function (string, isOverride) {
var str = interpretString(string).trim();
if (str) {
str.split(/&(?:amp;)?/).forEach(function (parameter) {
var param = parameter.trim();
var parts = parameter.split("=");
var key = window.decodeURIComponent(parts[0]);
var value = parts.length > 1
? window.decodeURIComponent(
parts[1].replace(/\+/g, " ")
)
: undefined;
this[
(this.has(key) && !isOverride)
? "add"
: "set"
](key, value);
}, this);
}
},
/**
* Query#stringifyKey(key[, index]) -> String
* - key (String): Key to be stringified.
* - index (Number): Optional index for the key.
*
* Converts the given `key` into a URL-friendly key.
*
* var q = new Query();
* q.stringifyKey("foo"); // -> "foo"
* q.stringifyKey("bar baz"); // -> "bar%20baz"
*
* Keys are not checked against [[Query#data]] so an associated value
* does not need to exist for the key to be converted.
*
* If `index` is included, the key is assumed to be part of an array
* and handled according to the [[Query#arrayType]] that has been set
* (see [[Query#setArrayType]] for full details).
*
* var q = new Query();
* q.setArrayType(Query.ARRAY_KEYS_INDEX);
* q.stringifyKey("foo", 0); // -> "foo[0]"
* q.stringifyKey("bar baz", 1); // -> "bar%20baz[1]"
*
* This function is mainly used in [[Query.stringify]].
**/
stringifyKey: function (key, index) {
var string = window.encodeURIComponent(key);
if (index !== undefined) {
if (this.arrayType === Query.ARRAY_KEYS_INDEX) {
string += "[" + window.encodeURIComponent(index) + "]";
} else if (this.arrayType === Query.ARRAY_KEYS_BRACKET) {
string += "[]";
}
}
return string;
},
/**
* Query#stringifyValue(value) -> String
* - value (String): Value to convert.
*
* Converts the value into a URL-friendly string and prefixes it with
* `"="` so it can be included in a query string.
*
* var q = new Query();
* q.stringifyValue("foo"); // -> "=foo"
* q.stringifyValue("bar baz"); // -> "=bar+baz"
*
* Since there is no key provided, the value does not need to be
* associated with any data.
*
* This function is mainly used in [[Query#stringify]].
**/
stringifyValue: function (value) {
var string = "";
if (value !== undefined) {
value = window
.encodeURIComponent(
// Normalize newlines as \r\n because the HTML spec
// says newlines should be encoded as CRLFs.
interpretString(value).replace(/(\r)?\n/g, "\r\n")
)
// Likewise, according to the spec, spaces should be "+"
// rather than "%20".
.replace(/%20/g, "+");
string += "=" + value;
}
return string;
},
/**
* Query#stringify(key[, value]) -> String
* - key (String): Key to convert.
* - value (?): Optional value to convert.
*
* Converts the given `key`/`value` pair into a URL-ready query string
* component. According to the HTML spec, spaces in the values should
* be converted into `+` rather than `%20` but the same is not true of
* keys.
*
* var q = new Query();
* q.stringify("foo", "alpha"); // -> "foo=alpha"
* q.stringify("bar", ["bravo", "charlie"]);
* // -> "bar=bravo&bar=charlie
* q.stringify("baz blip", "delta echo");
* // -> "bar%20blip=delta+echo"
* q.stringify("blop"); // -> "blop"
*
* Whenever `value` is an array, the `key` is modified according to the
* current [[Query#arrayType]] - see [[Query#setArrayType]] for full
* details.
*
* This function may not be especially useful on its own but it is a
* key component of [[Query#toString]].
**/
stringify: function (key, value) {
return Array.isArray(value)
? value
.map(function (val, i) {
return this.stringifyKey(key, i)
+ this.stringifyValue(val);
}, this)
.join("&")
: (this.stringifyKey(key) + this.stringifyValue(value));
},
/**
* Query#toString() -> String
*
* Converts [[Query#data]] to a URL-encoded query string, joined with
* ampersands. Due to the way that query strings are handled, there
* will be no difference between a string value and an array value with
* a single entry.
*
* Be warned that the order of the query string parameters cannot be
* guarenteed. If the order is important, use [[Query#keys()]] to get
* an array of [[Query#data]] keys, `.sort()` to handle the sorting,
* [[Query#get]] to get the values of [[Query#data]] and
* [[Query#stringify]] to convert the results. You will have to
* manually concatenate the results.
*
* var q = new Query();
* q.set({
* foo: "a",
* bar: ["b", "c"],
* baz: ["d"],
* "blip blop": "d e",
* }).set("blap");
* q.toString();
* // -> "foo=a&bar=b&bar=c&baz=d&blip%20blop=d+e&blap"
*
* When converting an array value into a string, the key is converted
* according to the current [[Query#arrayType]] - see
* [[Query#setArrayType]] for full details and examples.
*
* Because the method name is `toString`, JavaScript will automatically
* execute it if the instance is treated as a string.
*
* "?" + q; // -> "?foo=a&bar=b&bar=c&baz=d&blip%20blop=d+e&blap"
*
**/
toString: function () {
var string = [];
this.each(function (key, value) {
string.push(this.stringify(key, value));
}, this);
return string.join("&");
}
};
/**
* Query.parseUrl() -> Query
*
* Creates an instance of [[Query]] with the current URL query string
* parsed.
*
* // Assume URL is http://example.com?foo=alpha&bar=bravo&bar=charlie
* var q = Query.parseUrl();
* q.get("foo"); // -> "alpha"
* q.get("bar"); // -> ["bravo", "charlie"]
*
**/
Query.parseUrl = function () {
return new Query(window.location.search.slice(1));
};
Object.defineProperties(Query, {
/**
* Query.ARRAY_KEYS_REPEAT = 0
*
* A setting used for [[Query#setArrayType]]. Causes the keys to
* repeat.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_REPEAT)
* .toString();
* // -> "foo=1&foo=2"
*
**/
ARRAY_KEYS_REPEAT: {
configurable: false,
enumerable: true,
value: 0,
writable: false
},
/**
* Query.ARRAY_KEYS_BRACKET = 1
*
* A setting used for [[Query#setArrayType]]. Causes the keys to
* include empty square brackets.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_BRACKET)
* .toString();
* // -> "foo[]=1&foo[]=2"
*
**/
ARRAY_KEYS_BRACKET: {
configurable: false,
enumerable: true,
value: 1,
writable: false
},
/**
* Query.ARRAY_KEYS_INDEX = 1
*
* A setting used for [[Query#setArrayType]]. Causes the keys to
* include square brackets containing the index.
*
* new Query("foo=1&foo=2")
* .setArrayType(Query.ARRAY_KEYS_INDEX)
* .toString();
* // -> "foo[0]=1&foo[1]=2"
*
**/
ARRAY_KEYS_INDEX: {
configurable: false,
enumerable: true,
value: 2,
writable: false
}
});
return Query;
}());
@james-jlo-long
Copy link
Author

Extremely rough notes about a possible couple of enhancements:

(function () {

    var keyMakers = {};

    Query.setKeymaker = function (name, handler) {

        if (keyMakers[name]) {
            throw new Error("Unable to replace existing keymaker " + name);
        }

        keyMakers[name] = handler;
        
    };

    Query.setKeymaker(Query.ARRAY_KEY_REPEAT, function (key) {
        return window.encodeURIComponent(key);
    });

    Query.prototype.stringifyKey = function (key, index) {

        var handler = (
                index === undefined
                || !hasOwn.call(keyMakers, this.arrayType])
            )
            ? keyMakers[Query.ARRAY_KEY_REPEAT]
            : keyMakers[this.arrayType];

        return handler(key, index);
        
    };

    function addConstants(object, constants) {

        Object.keys(constants).forEach(function (key) {

            Object.defineProperty(object, key, {
                configurable: false,
                enumerable: true,
                value: constants[key],
                writable: false
            });
            
        });
        
    }

    addConstants(Query, {
        ARRAY_TYPE_REPEAT: 0,
        ARRAY_TYPE_BRACKET: 1,
        ARRAY_TYPE_INDEX: 2,
        ARRAY_TYPE_COMMA: 3,
        JOINER_AMPERSAND: "&",
        JOINER_SEMICOLON: ";"
    });

    Query.defaultArrayType = Query.ARRAY_TYPE_REPEAT;
    Query.defaultJoiner = Query.JOINER_AMPERSAND;


    // {name: [1, 2, 3]} -> name=1,2,3

    Query.prototype = {
        stringifyKeyPart: function (key) {
            return window.encodeURIComponent(key);
        },
        stringifyValuePart: function (value) {

            return window
                .encodeURIComponent(
                    value.replace(/(\r)?\n/g, "\r\n")
                )
                .replace(/%20/g, "+");
            
        },

        stringify: function (key, value) {

            var parts = [];
            var keyString = this.stringifyKeyPart(key);

            if (Array.isArray(value)) {

                value.forEach(function (val, i) {

                    var valString = this.stringifyValuePart(val);

                    switch (this.arrayType) {

                    case Query.ARRAY_TYPE_COMMA:

                        if (!parts[0]) {
                            parts[0] = "";
                        }

                        parts[0] += i === 0
                            ? keyString + "=" + valString
                            : "," + valString;

                        break;

                    case Query.ARRAY_TYPE_INDEX:

                        parts.push(
                            keyString
                            + "["
                            + this.stringifyKeyPart(i)
                            + "]="
                            + valString
                        );
                        break;

                    case Query.ARRAY_TYPE_BRACKET:

                        parts.push(
                            keyString
                            + "[]="
                            + valString
                        );
                        break;

                    case Query.ARRAY_TYPE_REPEAT:
                    default:

                        parts.push(keyString + "=" + valString);
                    
                    }
                    
                }, this);

            } else {
                parts.push(keyString + "=" + this.stringifyValuePart(value));
            }

            return parts.join(this.joiner);
            
        }
        
    }


}());

"foo=1&foo=2"
"foo[]=1&foo[]=2"
"foo[1]=2&foo[0]=1"
"foo[a][0]=1&foo[a][1]=2"

/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g
/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|((?:[^\\]|\\.)*?))\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g

//var data = {};
// var pointer = data;
// var methods = {
//     set: function (value) {
//         pointer;
//     },
//     push: function (value) {
//         pointer.push(value);
//     }
// };
// var method = "set";

var data = {};
var steps = [];
"foo[a][0][]".replace(
    /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|((?:[^\\]|\\.)*?))\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,
    function (match, number, string) {

        steps.push(
            typeof number === "string"
                ? number
                : typeof string === "string"
                    ? string
                    : match
        );

    }
);

function getNextKey(object) {

    var keys = Object
        .keys(object)
        .filter(function (key) {
            return +key === parseInt(key);
        })
        .sort(function (a, b) {
            return a - b;
        });

    return keys.length
        ? +keys[keys.length - 1] + 1
        : 0;

}

// http://stackoverflow.com/questions/18936915/dynamically-set-property-of-nested-object
// Also, look at lodash _.set() or _.update()
function addToData(keys, value) {

    var pointer = data;
    var length = keys.length;

// "foo[a][0][]" -> ["foo", "a", "0", ""]
    keys.forEach(function (key) {

        if (key === "") {

            key = getNextKey(pointer);
            keys[i][1] = key;

        }

        if (i !== length - 1) {

            if (!pointer[key]) {
                pointer[key] = {};
            }

            pointer = pointer[key];

        }

    });

    pointer[keys[length - 1][1]] = value;

}

addToData(steps, true);
console.debug(JSON.stringify(data));

@james-jlo-long
Copy link
Author

More notes:

var Query = (function () {

    var Query = function () {
        return this.init.apply(this, arguments);
    };

    Query.prototype = {

        init: function (data, settings) {

            var config = Object.assign({
                arrayType: Query.defaultArrayType,
                joiner: Query.defaultJoiner
            }, settings);

            this.data = {};

            return this;
            
        },

        get: function (path) {

            var result = {};

            if (typeof path === "string") {
                result = _.get(this.data, path);
            } else if (Array.isArray(path)) {

                path.forEach(function (key) {
                    _.set(result, key, this.get(key));
                }, this);
                
            } else if (path === undefined) {
                result = this.get(this.keys());
            }

            return result;

        },

        has: function (path) {

            return Array.isArray(path)
                ? path.map(this.has, this)
                : _.has(this.data, path);
            
        },

        set: function (key, value) {

            if (_.isPlainObject(key)) {

                Object.keys(key).forEach(function (k) {
                    this.set(k, key[k]);
                }, this);
                
            } else {
                _.set(this.data, key, value);
            }

            return this;
            
        },

        unset: function (path) {

            return Array.isArray(path)
                ? path.map(this.unset, this)
                : _.unset(this.data, path);

        },

        add: function (key, value) {

            var result;

            if (_.isPlainObject(key)) {

                Object.keys(key).forEach(function (k) {
                    this.add(k, key[k]);
                }, this);

            } else {

                if (this.has(key)) {

                    result = this.get(key);

                    if (Array.isArray(result)) {
                        result.push(value);
                    } else if (_.isPlainObject(result)) {

                        result[
                            Object.keys(result).filter(function (key) {
                                return _.isInteger(+key);
                            }).length
                        ] = value;
                        
                    } else {
                        result = [result, value];
                    }

                    this.set(key, result);

                } else {
                    this.set(key, value);
                }
                
            }

            return this;
            
        },

        keys: function () {
            return _.keys(this.data);
        },

        values: function () {
            return _.values(this.data)
        },

        each: function (handler, context) {

            this.keys().forEach(function (key) {
                handler.call(context, key, this.get(key));
            }, this);
            
        },

        parseString: function (string, arrayType) {

            var parser = Query.parsers[
                arrayType === undefined
                    ? this.arrayType
                    : arrayType
            ];

            if (typeof parser === "function") {
                parser.call(this, interpretString(string).replace(/^\??/, ""));
            }
            
        }
        
    };

    

}());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment