<commit_before>/*
	curl-to-Go
	by Matt Holt

	https://github.com/mholt/curl-to-go

	A simple utility to convert curl commands into Go code.
*/

function curlToGo(curl) {
	var err = 'if err != nil {\n\t// handle err\n}\n';
	var deferClose = 'defer resp.Body.Close()\n';
	var promo = "// Generated by curl-to-Go: https://mholt.github.io/curl-to-go";

	// List of curl flags that are boolean typed; this helps with parsing
	// a command like `curl -abc value` to know whether 'value' belongs to '-c'
	// or is just a positional argument instead.
	var boolOptions = ['#', 'progress-bar', '-', 'next', '0', 'http1.0', 'http1.1', 'http2',
		'no-npn', 'no-alpn', '1', 'tlsv1', '2', 'sslv2', '3', 'sslv3', '4', 'ipv4', '6', 'ipv6',
		'a', 'append', 'anyauth', 'B', 'use-ascii', 'basic', 'compressed', 'create-dirs',
		'crlf', 'digest', 'disable-eprt', 'disable-epsv', 'environment', 'cert-status',
		'false-start', 'f', 'fail', 'ftp-create-dirs', 'ftp-pasv', 'ftp-skip-pasv-ip',
		'ftp-pret', 'ftp-ssl-ccc', 'ftp-ssl-control', 'g', 'globoff', 'G', 'get',
		'ignore-content-length', 'i', 'include', 'I', 'head', 'j', 'junk-session-cookies',
		'J', 'remote-header-name', 'k', 'insecure', 'l', 'list-only', 'L', 'location',
		'location-trusted', 'metalink', 'n', 'netrc', 'N', 'no-buffer', 'netrc-file',
		'netrc-optional', 'negotiate', 'no-keepalive', 'no-sessionid', 'ntlm', 'O',
		'remote-name', 'oauth2-bearer', 'p', 'proxy-tunnel', 'path-as-is', 'post301', 'post302',
		'post303', 'proxy-anyauth', 'proxy-basic', 'proxy-digest', 'proxy-negotiate',
		'proxy-ntlm', 'q', 'raw', 'remote-name-all', 's', 'silent', 'sasl-ir', 'S', 'show-error',
		'ssl', 'ssl-reqd', 'ssl-allow-beast', 'ssl-no-revoke', 'socks5-gssapi-nec', 'tcp-nodelay',
		'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tr-encoding', 'trace-time', 'v', 'verbose', 'xattr',
		'h', 'help', 'M', 'manual', 'V', 'version'];

	if (!curl.trim())
		return;
	var cmd = parseCommand(curl, { boolFlags: boolOptions });

	if (cmd._[0] != "curl")
		throw "Not a curl command";

	var req = extractRelevantPieces(cmd);

	if (req.headers.length == 0 && !req.data.ascii && !req.data.files && !req.basicauth) {
		return promo+"\n"+renderSimple(req.method, req.url);
	} else {
		return promo+"\n\n"+renderComplex(req);
	}

	// renderSimple renders a simple HTTP request using net/http convenience methods
	function renderSimple(method, url) {
		if (method == "GET")
			return 'resp, err := http.Get("'+url+'")\n'+err+deferClose;
		else if (method == "POST")
			return 'resp, err := http.Post("'+url+'", "", nil)\n'+err+deferClose;
		else if (method == "HEAD")
			return 'resp, err := http.Head("'+url+'")\n'+err+deferClose;
		else
			return 'req, err := http.NewRequest("'+method+'", "'+url+'", nil)\n'+err+'resp, err := http.DefaultClient.Do(req)\n'+err+deferClose;
	}

	// renderComplex renders Go code that requires making a http.Request.
	function renderComplex(req) {
		var go = "";

		// First, figure out the headers
		var headers = {};
		for (var i = 0; i < req.headers.length; i++) {
			var split = req.headers[i].indexOf(":");
			if (split == -1) continue;
			var name = req.headers[i].substr(0, split).trim();
			var value = req.headers[i].substr(split+1).trim();
			headers[toTitleCase(name)] = value;
		}

		// load body data
		// KNOWN ISSUE: -d and --data are treated like --data-binary in
		// that we don't strip out carriage returns and newlines.
		var defaultPayloadVar = "body";
		if (!req.data.ascii && !req.data.files) {
			// no data; this is easy
			go += 'req, err := http.NewRequest("'+req.method+'", "'+goEsc(req.url)+'", nil)\n'+err;
		} else {
			var ioReaders = [];

			// if there's text data...
			if (req.data.ascii) {
				var stringBody = function() {
					go += defaultPayloadVar+' := strings.NewReader(`'+req.data.ascii+'`)\n'
					ioReaders.push(defaultPayloadVar);
				}

				if (headers["Content-Type"] && headers["Content-Type"].indexOf("json") > -1) {
					// create a struct for the JSON
					var result = jsonToGo(req.data.ascii, "Payload");
					if (result.error)
						stringBody(); // not valid JSON, so just treat as a regular string
					else if (result.go) {
						// valid JSON, so create a struct to hold it
						go += result.go+'\n\ndata := Payload{\n\t// fill struct\n}\n';
						go += 'payloadBytes, err := json.Marshal(data)\n'+err;
						go += defaultPayloadVar+' := bytes.NewReader(payloadBytes)\n\n';
					}
				} else {
					// not a json Content-Type, so treat as string
					stringBody();
				}
			}

			// if file data...
			if (req.data.files && req.data.files.length > 0) {
				var varName = "f";
				for (var i = 0; i < req.data.files.length; i++) {
					var thisVarName = (req.data.files.length > 1 ? varName+(i+1) : varName);
					go += thisVarName+', err := os.Open("'+goEsc(req.data.files[i])+'")\n'+err;
					go += 'defer '+thisVarName+'.Close()\n';
					ioReaders.push(thisVarName);
				}
			}

			// render go code to put all the data in the body, concatenating if necessary
			var payloadVar = defaultPayloadVar;
			if (ioReaders.length > 0)
				payloadVar = ioReaders[0];
			if (ioReaders.length > 1) {
				payloadVar = "payload";
				// KNOWN ISSUE: The way we separate file and ascii data values
				// loses the order between them... our code above just puts the
				// ascii values first, followed by the files.
				go += 'payload := io.MultiReader('+ioReaders.join(", ")+')\n';
			}
			go += 'req, err := http.NewRequest("'+req.method+'", "'+goEsc(req.url)+'", '+payloadVar+')\n'+err;
		}

		// set basic auth
		if (req.basicauth) {
			go += 'req.SetBasicAuth("'+goEsc(req.basicauth.user)+'", "'+goEsc(req.basicauth.pass)+'")\n';
		}

		// set headers
		for (var name in headers) {
			go += 'req.Header.Set("'+goEsc(name)+'", "'+goEsc(headers[name])+'")\n';
		}

		// execute request
		go += "\nresp, err := http.DefaultClient.Do(req)\n";
		go += err+deferClose;

		return go;
	}

	// extractRelevantPieces returns an object with relevant pieces
	// extracted from cmd, the parsed command. This accounts for
	// multiple flags that do the same thing and return structured
	// data that makes it easy to spit out Go code.
	function extractRelevantPieces(cmd) {
		var relevant = {
			url: "",
			method: "",
			headers: [],
			data: {}
		};

		// curl supports multiple URLs but we'll just use the first
		if (cmd._.length > 1)
			relevant.url = cmd._[1]; // position 1 because index 0 is the curl command itself

		// gather the headers together
		if (cmd.H)
			relevant.headers = relevant.headers.concat(cmd.H);
		if (cmd.header)
			relevant.headers = relevant.headers.concat(cmd.header);

		// set method to HEAD?
		if (cmd.I || cmd.head)
			relevant.method = "HEAD";

		// between -X and --request, prefer the long form I guess
		if (cmd.request && cmd.request.length > 0)
			relevant.method = cmd.request[cmd.request.length-1].toUpperCase();
		else if (cmd.X && cmd.X.length > 0)
			relevant.method = cmd.X[cmd.X.length-1].toUpperCase(); // if multiple, use last (according to curl docs)

		// join multiple request body data, if any
		var dataAscii = [];
		var dataFiles = [];
		var loadData = function(d) {
			if (!relevant.method)
				relevant.method = "POST";
			for (var i = 0; i < d.length; i++)
			{
				if (d[i].length > 0 && d[i][0] == "@")
					dataFiles.push(d[i].substr(1));
				else
					dataAscii.push(d[i]);
			}
		};
		if (cmd.d)
			loadData(cmd.d);
		if (cmd.data)
			loadData(cmd.data);
		if (dataAscii.length > 0)
			relevant.data.ascii = dataAscii.join("&");
		if (dataFiles.length > 0)
			relevant.data.files = dataFiles;

		// between -u and --user, choose the long form...
		var basicAuthString = "";
		if (cmd.user && cmd.user.length > 0)
			basicAuthString = cmd.user[cmd.user.length-1];
		else if (cmd.u && cmd.u.length > 0)
			basicAuthString = cmd.u[cmd.u.length-1];
		var basicAuthSplit = basicAuthString.indexOf(":");
		if (basicAuthSplit > -1) {
			relevant.basicauth = {
				user: basicAuthString.substr(0, basicAuthSplit),
				pass: basicAuthString.substr(basicAuthSplit+1)
			};
		} else {
			relevant.basicAuth = { user: basicAuthString, pass: "<PASSWORD>" };
		}

		// default to GET if nothing else specified
		if (!relevant.method)
			relevant.method = "GET";

		return relevant;
	}

	function toTitleCase(str) {
		return str.replace(/\w*/g, function(txt) {
			return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
		});
	}

	// goEsc escapes characters in s so that it is safe to use s in a "quoted string" in a Go program
	function goEsc(s) {
		return s.replace('"', '\\"').replace('\\', '\\\\');
	}
}

function parseCommand(input, options) {
	if (typeof options === 'undefined') {
		options = {};
	}

	var result = {_: []}, // what we return
	    cursor = 0,       // iterator position
	    token = "";       // current token (word or quoted string) being built

	// trim leading $ or # that may have been left in
	input = input.trim();
	if (input.length > 2 && (input[0] == '$' || input[0] == '#') && whitespace(input[1]))
		input = input.substr(1).trim();

	for (cursor = 0; cursor < input.length; cursor++) {
		if (whitespace(input[cursor]))
			continue;
		if (input[cursor] == "-") {
			flagSet();
		} else {
			unflagged();
		}
	}

	return result;

	// flagSet handles flags and it assumes the current cursor
	// points to a first dash.
	function flagSet() {
		if (cursor < input.length-1 && input[cursor+1] == "-") {
			return longFlag();
		}
		cursor++; // skip leading dash
		while (cursor < input.length && !whitespace(input[cursor]))
		{
			var flagName = input[cursor];
			if (typeof result[flagName] == 'undefined') {
				result[flagName] = [];
			}
			cursor++; // skip the flag name
			if (boolFlag(flagName))
				result[flagName] = true;
			else if (Array.isArray(result[flagName]))
				result[flagName].push(nextString());
		}
	}

	// longFlag consumes a "--long-flag" sequence and
	// stores it in result.
	function longFlag() {
		cursor += 2; // skip leading dashes
		var flagName = nextString("=");
		if (boolFlag(flagName))
			result[flagName] = true;
		else {
			if (typeof result[flagName] == 'undefined') {
				result[flagName] = [];
			}
			if (Array.isArray(result[flagName])) {
				result[flagName].push(nextString());
			}
		}
	}

	// unflagged consumes the next string as an unflagged value,
	// storing it in the result.
	function unflagged() {
		result._.push(nextString());
	}

	// boolFlag returns whether a flag is known to be boolean type
	function boolFlag(flag) {
		if (Array.isArray(options.boolFlags)) {
			for (var i = 0; i < options.boolFlags.length; i++) {
				if (options.boolFlags[i] == flag)
					return true;
			}
		}
		return false;
	}

	// nextString skips any leading whitespace and consumes the next
	// space-delimited string value and returns it. If endChar is set,
	// it will be used to determine the end of the string. Normally just
	// unescaped whitespace is the end of the string, but endChar can
	// be used to specify another end-of-string.
	function nextString(endChar) {
		for (; cursor < input.length && whitespace(input[cursor]); cursor++); // skip whitespace

		var str = "";

		var quoted = false,
			quoteCh = "",
			escaped = false;

		if (input[cursor] == '"' || input[cursor] == "'") {
			quoted = true;
			quoteCh = input[cursor];
			cursor++;
		}

		for (; cursor < input.length; cursor++) {
			if (quoted) {
				if (input[cursor] == quoteCh && !escaped) {
					cursor++; // skip closing quote
					return str;
				}
			}
			if (!quoted) {
				if (!escaped) {
					if (whitespace(input[cursor])) {
						return str;
					}
					if (endChar && input[cursor] == endChar) {
						cursor++; // skip the endChar
						return str;
					}
				}
			}
			str += input[cursor];
			escaped = false;
			if (input[cursor] == "\\") {
				escaped = true;
			}
		}

		return str;
	}

	// whitespace returns true if ch is a whitespace character.
	function whitespace(ch) {
		return ch == " " || ch == "\t" || ch == "\n" || ch == "\r";
	}
}
<commit_msg>Fix certain escape sequences<commit_after>/*
	curl-to-Go
	by Matt Holt

	https://github.com/mholt/curl-to-go

	A simple utility to convert curl commands into Go code.
*/

function curlToGo(curl) {
	var err = 'if err != nil {\n\t// handle err\n}\n';
	var deferClose = 'defer resp.Body.Close()\n';
	var promo = "// Generated by curl-to-Go: https://mholt.github.io/curl-to-go";

	// List of curl flags that are boolean typed; this helps with parsing
	// a command like `curl -abc value` to know whether 'value' belongs to '-c'
	// or is just a positional argument instead.
	var boolOptions = ['#', 'progress-bar', '-', 'next', '0', 'http1.0', 'http1.1', 'http2',
		'no-npn', 'no-alpn', '1', 'tlsv1', '2', 'sslv2', '3', 'sslv3', '4', 'ipv4', '6', 'ipv6',
		'a', 'append', 'anyauth', 'B', 'use-ascii', 'basic', 'compressed', 'create-dirs',
		'crlf', 'digest', 'disable-eprt', 'disable-epsv', 'environment', 'cert-status',
		'false-start', 'f', 'fail', 'ftp-create-dirs', 'ftp-pasv', 'ftp-skip-pasv-ip',
		'ftp-pret', 'ftp-ssl-ccc', 'ftp-ssl-control', 'g', 'globoff', 'G', 'get',
		'ignore-content-length', 'i', 'include', 'I', 'head', 'j', 'junk-session-cookies',
		'J', 'remote-header-name', 'k', 'insecure', 'l', 'list-only', 'L', 'location',
		'location-trusted', 'metalink', 'n', 'netrc', 'N', 'no-buffer', 'netrc-file',
		'netrc-optional', 'negotiate', 'no-keepalive', 'no-sessionid', 'ntlm', 'O',
		'remote-name', 'oauth2-bearer', 'p', 'proxy-tunnel', 'path-as-is', 'post301', 'post302',
		'post303', 'proxy-anyauth', 'proxy-basic', 'proxy-digest', 'proxy-negotiate',
		'proxy-ntlm', 'q', 'raw', 'remote-name-all', 's', 'silent', 'sasl-ir', 'S', 'show-error',
		'ssl', 'ssl-reqd', 'ssl-allow-beast', 'ssl-no-revoke', 'socks5-gssapi-nec', 'tcp-nodelay',
		'tlsv1.0', 'tlsv1.1', 'tlsv1.2', 'tr-encoding', 'trace-time', 'v', 'verbose', 'xattr',
		'h', 'help', 'M', 'manual', 'V', 'version'];

	if (!curl.trim())
		return;
	var cmd = parseCommand(curl, { boolFlags: boolOptions });

	if (cmd._[0] != "curl")
		throw "Not a curl command";

	var req = extractRelevantPieces(cmd);

	if (req.headers.length == 0 && !req.data.ascii && !req.data.files && !req.basicauth) {
		return promo+"\n"+renderSimple(req.method, req.url);
	} else {
		return promo+"\n\n"+renderComplex(req);
	}

	// renderSimple renders a simple HTTP request using net/http convenience methods
	function renderSimple(method, url) {
		if (method == "GET")
			return 'resp, err := http.Get("'+url+'")\n'+err+deferClose;
		else if (method == "POST")
			return 'resp, err := http.Post("'+url+'", "", nil)\n'+err+deferClose;
		else if (method == "HEAD")
			return 'resp, err := http.Head("'+url+'")\n'+err+deferClose;
		else
			return 'req, err := http.NewRequest("'+method+'", "'+url+'", nil)\n'+err+'resp, err := http.DefaultClient.Do(req)\n'+err+deferClose;
	}

	// renderComplex renders Go code that requires making a http.Request.
	function renderComplex(req) {
		var go = "";

		// First, figure out the headers
		var headers = {};
		for (var i = 0; i < req.headers.length; i++) {
			var split = req.headers[i].indexOf(":");
			if (split == -1) continue;
			var name = req.headers[i].substr(0, split).trim();
			var value = req.headers[i].substr(split+1).trim();
			headers[toTitleCase(name)] = value;
		}

		// load body data
		// KNOWN ISSUE: -d and --data are treated like --data-binary in
		// that we don't strip out carriage returns and newlines.
		var defaultPayloadVar = "body";
		if (!req.data.ascii && !req.data.files) {
			// no data; this is easy
			go += 'req, err := http.NewRequest("'+req.method+'", "'+goEsc(req.url)+'", nil)\n'+err;
		} else {
			var ioReaders = [];

			// if there's text data...
			if (req.data.ascii) {
				var stringBody = function() {
					go += defaultPayloadVar+' := strings.NewReader(`'+req.data.ascii+'`)\n'
					ioReaders.push(defaultPayloadVar);
				}

				if (headers["Content-Type"] && headers["Content-Type"].indexOf("json") > -1) {
					// create a struct for the JSON
					var result = jsonToGo(req.data.ascii, "Payload");
					if (result.error)
						stringBody(); // not valid JSON, so just treat as a regular string
					else if (result.go) {
						// valid JSON, so create a struct to hold it
						go += result.go+'\n\ndata := Payload{\n\t// fill struct\n}\n';
						go += 'payloadBytes, err := json.Marshal(data)\n'+err;
						go += defaultPayloadVar+' := bytes.NewReader(payloadBytes)\n\n';
					}
				} else {
					// not a json Content-Type, so treat as string
					stringBody();
				}
			}

			// if file data...
			if (req.data.files && req.data.files.length > 0) {
				var varName = "f";
				for (var i = 0; i < req.data.files.length; i++) {
					var thisVarName = (req.data.files.length > 1 ? varName+(i+1) : varName);
					go += thisVarName+', err := os.Open("'+goEsc(req.data.files[i])+'")\n'+err;
					go += 'defer '+thisVarName+'.Close()\n';
					ioReaders.push(thisVarName);
				}
			}

			// render go code to put all the data in the body, concatenating if necessary
			var payloadVar = defaultPayloadVar;
			if (ioReaders.length > 0)
				payloadVar = ioReaders[0];
			if (ioReaders.length > 1) {
				payloadVar = "payload";
				// KNOWN ISSUE: The way we separate file and ascii data values
				// loses the order between them... our code above just puts the
				// ascii values first, followed by the files.
				go += 'payload := io.MultiReader('+ioReaders.join(", ")+')\n';
			}
			go += 'req, err := http.NewRequest("'+req.method+'", "'+goEsc(req.url)+'", '+payloadVar+')\n'+err;
		}

		// set basic auth
		if (req.basicauth) {
			go += 'req.SetBasicAuth("'+goEsc(req.basicauth.user)+'", "'+goEsc(req.basicauth.pass)+'")\n';
		}

		// set headers
		for (var name in headers) {
			go += 'req.Header.Set("'+goEsc(name)+'", "'+goEsc(headers[name])+'")\n';
		}

		// execute request
		go += "\nresp, err := http.DefaultClient.Do(req)\n";
		go += err+deferClose;

		return go;
	}

	// extractRelevantPieces returns an object with relevant pieces
	// extracted from cmd, the parsed command. This accounts for
	// multiple flags that do the same thing and return structured
	// data that makes it easy to spit out Go code.
	function extractRelevantPieces(cmd) {
		var relevant = {
			url: "",
			method: "",
			headers: [],
			data: {}
		};

		// curl supports multiple URLs but we'll just use the first
		if (cmd._.length > 1)
			relevant.url = cmd._[1]; // position 1 because index 0 is the curl command itself

		// gather the headers together
		if (cmd.H)
			relevant.headers = relevant.headers.concat(cmd.H);
		if (cmd.header)
			relevant.headers = relevant.headers.concat(cmd.header);

		// set method to HEAD?
		if (cmd.I || cmd.head)
			relevant.method = "HEAD";

		// between -X and --request, prefer the long form I guess
		if (cmd.request && cmd.request.length > 0)
			relevant.method = cmd.request[cmd.request.length-1].toUpperCase();
		else if (cmd.X && cmd.X.length > 0)
			relevant.method = cmd.X[cmd.X.length-1].toUpperCase(); // if multiple, use last (according to curl docs)

		// join multiple request body data, if any
		var dataAscii = [];
		var dataFiles = [];
		var loadData = function(d) {
			if (!relevant.method)
				relevant.method = "POST";
			for (var i = 0; i < d.length; i++)
			{
				if (d[i].length > 0 && d[i][0] == "@")
					dataFiles.push(d[i].substr(1));
				else
					dataAscii.push(d[i]);
			}
		};
		if (cmd.d)
			loadData(cmd.d);
		if (cmd.data)
			loadData(cmd.data);
		if (dataAscii.length > 0)
			relevant.data.ascii = dataAscii.join("&");
		if (dataFiles.length > 0)
			relevant.data.files = dataFiles;

		// between -u and --user, choose the long form...
		var basicAuthString = "";
		if (cmd.user && cmd.user.length > 0)
			basicAuthString = cmd.user[cmd.user.length-1];
		else if (cmd.u && cmd.u.length > 0)
			basicAuthString = cmd.u[cmd.u.length-1];
		var basicAuthSplit = basicAuthString.indexOf(":");
		if (basicAuthSplit > -1) {
			relevant.basicauth = {
				user: basicAuthString.substr(0, basicAuthSplit),
				pass: basicAuthString.substr(basicAuthSplit+1)
			};
		} else {
			relevant.basicAuth = { user: basicAuthString, pass: "<PASSWORD>" };
		}

		// default to GET if nothing else specified
		if (!relevant.method)
			relevant.method = "GET";

		return relevant;
	}

	function toTitleCase(str) {
		return str.replace(/\w*/g, function(txt) {
			return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
		});
	}

	// goEsc escapes characters in s so that it is safe to use s in a "quoted string" in a Go program
	function goEsc(s) {
		return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
	}
}

function parseCommand(input, options) {
	if (typeof options === 'undefined') {
		options = {};
	}

	var result = {_: []}, // what we return
	    cursor = 0,       // iterator position
	    token = "";       // current token (word or quoted string) being built

	// trim leading $ or # that may have been left in
	input = input.trim();
	if (input.length > 2 && (input[0] == '$' || input[0] == '#') && whitespace(input[1]))
		input = input.substr(1).trim();

	for (cursor = 0; cursor < input.length; cursor++) {
		if (whitespace(input[cursor]))
			continue;
		if (input[cursor] == "-") {
			flagSet();
		} else {
			unflagged();
		}
	}

	return result;

	// flagSet handles flags and it assumes the current cursor
	// points to a first dash.
	function flagSet() {
		if (cursor < input.length-1 && input[cursor+1] == "-") {
			return longFlag();
		}
		cursor++; // skip leading dash
		while (cursor < input.length && !whitespace(input[cursor]))
		{
			var flagName = input[cursor];
			if (typeof result[flagName] == 'undefined') {
				result[flagName] = [];
			}
			cursor++; // skip the flag name
			if (boolFlag(flagName))
				result[flagName] = true;
			else if (Array.isArray(result[flagName]))
				result[flagName].push(nextString());
		}
	}

	// longFlag consumes a "--long-flag" sequence and
	// stores it in result.
	function longFlag() {
		cursor += 2; // skip leading dashes
		var flagName = nextString("=");
		if (boolFlag(flagName))
			result[flagName] = true;
		else {
			if (typeof result[flagName] == 'undefined') {
				result[flagName] = [];
			}
			if (Array.isArray(result[flagName])) {
				result[flagName].push(nextString());
			}
		}
	}

	// unflagged consumes the next string as an unflagged value,
	// storing it in the result.
	function unflagged() {
		result._.push(nextString());
	}

	// boolFlag returns whether a flag is known to be boolean type
	function boolFlag(flag) {
		if (Array.isArray(options.boolFlags)) {
			for (var i = 0; i < options.boolFlags.length; i++) {
				if (options.boolFlags[i] == flag)
					return true;
			}
		}
		return false;
	}

	// nextString skips any leading whitespace and consumes the next
	// space-delimited string value and returns it. If endChar is set,
	// it will be used to determine the end of the string. Normally just
	// unescaped whitespace is the end of the string, but endChar can
	// be used to specify another end-of-string.
	function nextString(endChar) {
		for (; cursor < input.length && whitespace(input[cursor]); cursor++); // skip whitespace

		var str = "";

		var quoted = false,
			quoteCh = "",
			escaped = false;

		if (input[cursor] == '"' || input[cursor] == "'") {
			quoted = true;
			quoteCh = input[cursor];
			cursor++;
		}

		for (; cursor < input.length; cursor++) {
			if (quoted) {
				if (input[cursor] == quoteCh && !escaped) {
					cursor++; // skip closing quote
					return str;
				}
			}
			if (!quoted) {
				if (!escaped) {
					if (whitespace(input[cursor])) {
						return str;
					}
					if (endChar && input[cursor] == endChar) {
						cursor++; // skip the endChar
						return str;
					}
				}
			}
			if (!escaped && input[cursor] == "\\") {
				escaped = true;
				continue
			}
			str += input[cursor];
			escaped = false;
		}

		return str;
	}

	// whitespace returns true if ch is a whitespace character.
	function whitespace(ch) {
		return ch == " " || ch == "\t" || ch == "\n" || ch == "\r";
	}
}
<|endoftext|>


<commit_before>/**
 * Model js - A simple javascript library for creating the Model part of a MVC application.
 * https://github.com/dgeorges/modeljs.git
 * @project modeljs
 * @version 1.0.0
 * @author Daniel Georges
 *
 *
 * Features
 * - Simple easy to use and intuitive  library
 * - Can save/load Model to/from JSON with/without model meta data
 * - Can register on change events with any single property or group of properties
 * - Model change events bubble up.
 * - Can tie validation methods to models and properties
 * - Can suppress events notification.
 * - Can batch changes into a transaction.
 * - Transaction can be easily optimized.
 * - Incorrect uses of api are logged as errors to the console
 *
 *
 * TODO:
 *  - hook up/create/clean documentation and document methods properly.
 *  - add basic validators that can be reused.
 *  - set up gitHub site
 *  - update browser support
 *
 * BUGS:
 * - The callback executed when listening to modelChange event on one of your childrens needs to refined
 *     to make more sense. Right now its the same as a property change.
 */

(function (window, undefined) {
    "use strict";

    // copied from underscorejs
    function isObject (obj) {
        return obj === new Object(obj) && Object.prototype.toString.call(obj) != '[object Function]';
    }

    /**
     * Centralized place where all Model Events pass through.
     * @type {Object}
     */
    var eventProxy = function () {
        var eventQueue = [],
            state = {   ACTIVE: "active", TRANSACTION: "transaction"},
            currentState = state.ACTIVE;

        var executedCallbacks = [];
        var callbackHashs = [];
        function _fireEvent(property, oldValue) {

            // This weird executeCallback function is a bit more complicated than it needs to be but is
            // used to get around the JSLint warning of creating a function within the while loop below
            var executeCallbacksFunction = function (oldValue, property) {
                return function (callback){
                    if (Model.eventOptimization.enableSingleCallbackCall || Model.eventOptimization.enableCallbackHashOpimization){
                        var callbackExecuted = false;
                        if (Model.eventOptimization.enableSingleCallbackCall){
                            if(executedCallbacks.indexOf(callback) === -1) { // Only call callback once
                                executedCallbacks.push(callback);
                                callback.call(null, oldValue, property._value, property.getName());
                                callbackExecuted = true;
                            }
                        }
                        if(Model.eventOptimization.enableCallbackHashOpimization){
                             if(!callback.hash || callbackHashs.indexOf(callback.hash) === -1) { // Only call hash identified callback once
                                if (callback.hash) {
                                    callbackHashs.push(callback.hash);
                                }
                                if (!callbackExecuted){
                                    callback.call(null, oldValue, property._value, property.getName());
                                    callbackExecuted = true;
                                }
                            }
                        }
                    } else {
                        callback.call(null, oldValue, property._value, property.getName());
                    }
                };
            };

            var allPropertyListeners = property._propertyListeners.concat(property._modelListeners);
            allPropertyListeners.forEach(
                executeCallbacksFunction(oldValue, property)
            );

            var propertyParent = property._parent;
            while (propertyParent){

                propertyParent._modelListeners.forEach( // when we bubble the event we only notify modelListeners
                    executeCallbacksFunction(oldValue, propertyParent)
                );
                propertyParent = propertyParent._parent;
            }
        }

        function fireEvent (property, oldValue) {
            if (currentState === state.ACTIVE){
                _fireEvent(property, oldValue);
            } else { //place event on queue to be called at a later time.
                eventQueue.push({
                    property: property,
                    oldValue: oldValue
                });
            }
        }

        function changeState(newState){
            if (state[newState] !== currentState){
                currentState = newState;
                if (newState === state.ACTIVE){
                    flushEventQueue();
                }
            }
        }

        function flushEventQueue() {

            executedCallbacks = []; //reset state
            callbackHashs = [];
            if(Model.eventOptimization.suppressPreviousPropertyChangeEvents){
                var optimizedQueue = [];
                var seenProperties = [];
                for(var i = eventQueue.length - 1; i >= 0; i-=1){// iterate backwards since last events are most recent.
                    var eventProperty = eventQueue[i].property;
                    if (seenProperties.indexOf(eventProperty) === -1){
                        // Not seen yet add it.
                        seenProperties.push(eventProperty);
                        optimizedQueue.push(eventQueue[i]);
                    } else {
                        //eventQueue[i] = null; // null out event since it's propertyChange is on the Queue Already
                    }
                }
                eventQueue = optimizedQueue;
            }

            eventQueue.forEach( function (event){
                _fireEvent(event.property, event.oldValue);
            });
            eventQueue = [];
        }

        return {
            fireEvent: fireEvent,
            startTransaction: changeState.bind(null, state.TRANSACTION),
            endTransaction: changeState.bind(null, state.ACTIVE),
            inTransaction: function () { return currentState === state.TRANSACTION;}
        };
    }();

    /**
     * A given property in the model.
     * @class
     * @constructor
     * @classdesc
     * @param {[type]} value   property Value
     * @param {[type]} options May contain the following:
     *                         validator - a function to validate the new value is valid before it is assigned.
     */
    function Property (name, value, parent, options) {

        var myName = "/" + name;
        if (parent){
            myName = parent.getName() + myName;
        }

        Object.defineProperty(this, "_name", {
            value: myName,
            enumerable: false
        });

        Object.defineProperty(this, "_parent", {
            value: parent,
            enumerable: false
        });

        Object.defineProperty(this, "_myValue", {
            value: value,
            enumerable: false,
            writable: true
        });

        Object.defineProperty(this, "_options", {
            value: options || {},
            enumerable: false
        });

        Object.defineProperty(this, "_propertyListeners", {
            value: [],
            enumerable: false
        });
         Object.defineProperty(this, "_modelListeners", {
            value: [],
            enumerable: false
        });

        Object.defineProperty(this, "_value", {
            get: function () { //ideally would like to override the getter in the Model calss but this will have to do.
                if (this instanceof Model) {
                    return this.toJSON();
                } else {
                    return this._myValue;
                }
            },
            set: function (newValue) { this.setValue(newValue);}
        });

        this.setValue(value);
    }

    Property.prototype.getName = function () {
        return this._name;
    };

    /**
     * [getter_setter description]
     * @param  {[type]} newValue The Value you want to assign to the Property.
     * @param  {[type]} options  May contain the following:
     *                           suppressNotifications - boolean indicating if listeners should be notified of change.
     *
     * @return {[type]}          The resulting value of the Property
     */
    Property.prototype.setValue = function (value, suppressNotifications) {
        var newValue,
            suppressOnChangeEvent = suppressNotifications;

        if (isObject(value) && value._value) {
            newValue = value._value;
            suppressOnChangeEvent = value.suppressNotifications;
        } else {
            newValue = value;
        }

        // Note: this disallows setting a property to undefined. Only when it's first created can it be undefined.
        if (newValue !== undefined && newValue !== this._myValue) {
            var validationFunction = this._options && this._options.validator;

            if (!validationFunction || validationFunction(newValue)){
                var oldValue = this._myValue;

                if (newValue instanceof Property || newValue instanceof Model ) {
                    // this is misleading syntax because other property attributes are not copied like _listener and _parent
                    // so prevent it and provide alternate.
                    window.console.error("Incorrect Syntax: use ._value = [property|model]._value instead");
                } else if (isObject(newValue)) {
                    if (!(this instanceof Model)){
                        window.console.error("Not Supported: Can't set the Model value to a property. Delete the model and use createProperty");
                    } else {
                        //This model need to be set to the newValue
                        this.merge(newValue, false);
                    }
                } else { // newValue is a property
                    if (this instanceof Model){
                        window.console.error("Not Supported: Can't set a Property value to a model. Delete the property and use createProperty");
                    } else {
                        this._myValue = newValue;
                    }
                }

                if (!suppressOnChangeEvent){
                    eventProxy.fireEvent(this, oldValue);
                }
            }
        }
        return this._myValue;
    };

    /**
     * [addChangeCallback description]
     * @param {Function} callback [description]
     * @param {[type]}   options May contain the following:
     *                         listenToChildren - register the listener with any sub property change.
     */
    Property.prototype.onChange = function (callback, options) {
        if (options && options.listenToChildren){
            this._modelListeners.push(callback);
        } else {
            this._propertyListeners.push(callback);
        }
        return this;
    };

    Property.prototype.hasValidator = function () {
        return !!this._options.validator;
    };

    Property.prototype.validateValue = function (value) {
        if (this._options.validator){
            return this._options.validator(value);
        }
        return true;
    };
    //don't want a set validator function.

    /**
     * The model Object that wraps the JSON.
     *
     * @param {[type]} options May contain the following:
     *                         validator - a function to validate the new value is valid before it is assigned.
     *                         name - name of modei
     * @param {[type]} json [description]
     */
    function Model (json, options, parent) {
        var jsonModel = json || {} ,
            modelOptions = options|| {},
            modelName = (modelOptions.name || "root"),
            modelParent = parent || null;

        //A Model is in itself a Property so let inherit property
        // call with empty options for now and this is the value
        Property.call(this, modelName, jsonModel, modelParent, modelOptions);

        Object.keys(jsonModel).forEach(function (name){

            if (name.match(Model.PROPERTY_OPTIONS_SERIALIZED_NAME_REGEX)){
                return;
            }

            var value = jsonModel[name];
            var options = json[name + Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX];

            this.createProperty(name, value, options);
        }, this);
    }
    Model.prototype = Object.create(Property.prototype);

    Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX = "__modeljs__options";
    Model.PROPERTY_OPTIONS_SERIALIZED_NAME_REGEX = /__modeljs__options$/;

    /**
     * [createProperty description]
     * @param  {[type]} name    [description]
     * @param  {[type]} value   [description]
     * @param  {[type]} options [description]
     * @return {[type]}         [description]
     */
    Model.prototype.createProperty = function createProperty(name, value, options) {
        if (value instanceof Model || value instanceof Property){
            window.console.error("Unsupported Opperation: Try passing the Model/Properties value instead");
        } else if (isObject(value)){
            var modelOptions = options || {};
            modelOptions.name = name;
            this[name] = new Model(value, modelOptions, this);
        } else {
            this[name] = new Property (name, value, this, options);
        }
        return this;
    };

    Model.prototype.clone = function (){
        var myName = this.getName();
        var options = {
            name : myName.substring(myName.lastIndexOf("/") + 1),
            validator: this._options.validator
        };
        return new Model(this.toJSON(true), options);
    };

    function mergeLoop (model, json, doModification, keepOldProperties) {

        for (var name in json) {
            var value = json[name];
            if (model[name]){
                if (isObject(value)){ // right hand side is an object
                    if (model[name] instanceof Model) {// merging objects
                        var successful = mergeLoop(model[name], value, doModification, keepOldProperties );
                        if (!successful){
                            return false;
                        }
                    } else {
                        // Trying to assign a model to a property. This will fail.
                        return false;
                    }

                } else { // right hand side is a property
                    if (!(model[name] instanceof Model)){ // Its not a Model therefore it's a Property
                        if (doModification){
                            model[name]._value = value;
                        }
                    } else {
                        // Trying to assign a property to a Model. This will fail.
                        return false;
                    }
                }
            } else { //create new property
                if (doModification){
                    model.createProperty(name, value);
                }
            }
        }

        // delete properties that are not found in json
        if (!keepOldProperties && doModification){
            for (var modelProp in model) {
                if (!json[modelProp]){
                    delete model[modelProp];
                }
            }
        }
        return true;
    }

    Model.prototype.merge = function (json, keepOldProperties) {
        //will merge the properties in json with this. result will be the same as the Object extend.
        //if a property exists in the model but not in the json it will only be kept if keepOldProperties is true.
        if (mergeLoop(this, json, false, keepOldProperties)){// check if merge will be successful
            Model.startTransaction();
            mergeLoop(this, json, true, keepOldProperties);
            Model.endTransaction();
            return true;
        } else {
            window.console.error("Operation Not Supported: Model assignment not valid. Model not modified");
            return false;
        }
    };

    /**
     * [toJSON description]
     * @param  {[type]} includeMetaData indicates if model meta data should be included in the returned JSON
     * @return {[type]}                 The json representation of the Model.
     */
    Model.prototype.toJSON = function (includeMetaData) {
        var json = {};
        Object.keys(this).forEach( function (name){
            var property = this[name];
            if (property instanceof Model) {
                json[name] = property.toJSON(includeMetaData);
            } else {
                var value = this[name]._value;
                json[name] = value;
                if (includeMetaData && this[name]._options){
                    json[name + Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX] = this[name]._options;
                }
            }
        }, this);
        return json;
    };

    Model.startTransaction = function () {
        eventProxy.startTransaction();
    };

    Model.endTransaction = function (options) {
        var originalEventOptimization;

        if (options){ // if option override global setting keeping them so they can be restored later
            originalEventOptimization = JSON.parse(JSON.stringify(Model.eventOptimization));
            Model.eventOptimization.suppressPreviousPropertyChangeEvents = !!options.suppressPreviousPropertyChangeEvents;
            Model.eventOptimization.enableSingleCallbackCall = !!options.enableSingleCallbackCall;
            Model.eventOptimization.enableCallbackHashOpimization = !!options.enableCallbackHashOpimization;
        }

        eventProxy.endTransaction();

        if (options){ //restore global settings
            Model.eventOptimization.suppressPreviousPropertyChangeEvents = originalEventOptimization.suppressPreviousPropertyChangeEvents;
            Model.eventOptimization.enableSingleCallbackCall = originalEventOptimization.enableSingleCallbackCall;
            Model.eventOptimization.enableCallbackHashOpimization = originalEventOptimization.enableCallbackHashOpimization;
        }

    };

    Model.inTransaction = function() {
        return eventProxy.inTransaction();
    };

    Model.eventOptimization = {
        suppressPreviousPropertyChangeEvents: false, //Only fires last Property Change of a property during a transaction.
        enableSingleCallbackCall: false, //will make sure a callback only gets called only once during a transaction. Even if registared with several properties.
        enableCallbackHashOpimization: false //will make sure a callbacks itentified by .hash only gets called only once during a transaction. Even if registared with several properties.
    };
    Object.seal(Model.eventOptimization);

    var oldModel = window.Model;
    Model.noConflict = function () {
        window.Model = oldModel;
        return this;
    };

    /** @global */
    window.Model = Model;
}(window));<commit_msg>minor touches to spelling and comments<commit_after>/**
 * Model js - A simple javascript library for creating the Model part of a MVC application.
 * https://github.com/dgeorges/modeljs.git
 * @project modeljs
 * @version 1.0.0
 * @author Daniel Georges
 *
 *
 * Features
 * - Simple easy to use and intuitive  library
 * - Can save/load Model to/from JSON with/without model meta data
 * - Can register on change events with any single property or group of properties
 * - Model change events bubble up.
 * - Can tie validation methods to models and properties
 * - Can suppress events notification.
 * - Can batch changes into a transaction.
 * - Transaction can be easily optimized.
 * - Incorrect uses of api are logged as errors to the console
 *
 *
 * TODO:
 *  - hook up/create/clean documentation and document methods properly.
 *  - add basic validators that can be reused.
 *  - set up gitHub site
 *  - update browser support
 *
 * BUGS:
 * - The callback executed when listening to modelChange event on one of your children needs to refined
 *     to make more sense. Right now its the same as a property change.
 */

(function (window, undefined) {
    "use strict";

    // copied from underscorejs
    function isObject (obj) {
        return obj === new Object(obj) && Object.prototype.toString.call(obj) != '[object Function]';
    }

    /**
     * Centralized place where all Model Events pass through.
     * @type {Object}
     */
    var eventProxy = function () {
        var eventQueue = [],
            state = {   ACTIVE: "active", TRANSACTION: "transaction"},
            currentState = state.ACTIVE;

        var executedCallbacks = [];
        var callbackHashs = [];
        function _fireEvent(property, oldValue) {

            // This weird executeCallback function is a bit more complicated than it needs to be but is
            // used to get around the JSLint warning of creating a function within the while loop below
            var executeCallbacksFunction = function (oldValue, property) {
                return function (callback){
                    if (Model.eventOptimization.enableSingleCallbackCall || Model.eventOptimization.enableCallbackHashOpimization){
                        var callbackExecuted = false;
                        if (Model.eventOptimization.enableSingleCallbackCall){
                            if(executedCallbacks.indexOf(callback) === -1) { // Only call callback once
                                executedCallbacks.push(callback);
                                callback.call(null, oldValue, property._value, property.getName());
                                callbackExecuted = true;
                            }
                        }
                        if(Model.eventOptimization.enableCallbackHashOpimization){
                             if(!callback.hash || callbackHashs.indexOf(callback.hash) === -1) { // Only call hash identified callback once
                                if (callback.hash) {
                                    callbackHashs.push(callback.hash);
                                }
                                if (!callbackExecuted){
                                    callback.call(null, oldValue, property._value, property.getName());
                                    callbackExecuted = true;
                                }
                            }
                        }
                    } else {
                        callback.call(null, oldValue, property._value, property.getName());
                    }
                };
            };

            var allPropertyListeners = property._propertyListeners.concat(property._modelListeners);
            allPropertyListeners.forEach(
                executeCallbacksFunction(oldValue, property)
            );

            var propertyParent = property._parent;
            while (propertyParent){

                propertyParent._modelListeners.forEach( // when we bubble the event we only notify modelListeners
                    executeCallbacksFunction(oldValue, propertyParent)
                );
                propertyParent = propertyParent._parent;
            }
        }

        function fireEvent (property, oldValue) {
            if (currentState === state.ACTIVE){
                _fireEvent(property, oldValue);
            } else { //place event on queue to be called at a later time.
                eventQueue.push({
                    property: property,
                    oldValue: oldValue
                });
            }
        }

        function changeState(newState){
            if (state[newState] !== currentState){
                currentState = newState;
                if (newState === state.ACTIVE){
                    flushEventQueue();
                }
            }
        }

        function flushEventQueue() {

            executedCallbacks = []; //reset state
            callbackHashs = [];
            if(Model.eventOptimization.suppressPreviousPropertyChangeEvents){
                var optimizedQueue = [];
                var seenProperties = [];
                for(var i = eventQueue.length - 1; i >= 0; i-=1){// iterate backwards since last events are most recent.
                    var eventProperty = eventQueue[i].property;
                    if (seenProperties.indexOf(eventProperty) === -1){
                        // Not seen yet add it.
                        seenProperties.push(eventProperty);
                        optimizedQueue.push(eventQueue[i]);
                    } else {
                        //eventQueue[i] = null; // null out event since it's propertyChange is on the Queue Already
                    }
                }
                eventQueue = optimizedQueue;
            }

            eventQueue.forEach( function (event){
                _fireEvent(event.property, event.oldValue);
            });
            eventQueue = [];
        }

        return {
            fireEvent: fireEvent,
            startTransaction: changeState.bind(null, state.TRANSACTION),
            endTransaction: changeState.bind(null, state.ACTIVE),
            inTransaction: function () { return currentState === state.TRANSACTION;}
        };
    }();

    /**
     * A given property in the model.
     * @class
     * @constructor
     * @classdesc
     * @param {[type]} value   property Value
     * @param {[type]} options May contain the following:
     *                         validator - a function to validate the new value is valid before it is assigned.
     */
    function Property (name, value, parent, options) {

        var myName = "/" + name;
        if (parent){
            myName = parent.getName() + myName;
        }

        Object.defineProperty(this, "_name", {
            value: myName,
            enumerable: false
        });

        Object.defineProperty(this, "_parent", {
            value: parent,
            enumerable: false
        });

        Object.defineProperty(this, "_myValue", {
            value: value,
            enumerable: false,
            writable: true
        });

        Object.defineProperty(this, "_options", {
            value: options || {},
            enumerable: false
        });

        Object.defineProperty(this, "_propertyListeners", {
            value: [],
            enumerable: false
        });
         Object.defineProperty(this, "_modelListeners", {
            value: [],
            enumerable: false
        });

        Object.defineProperty(this, "_value", {
            get: function () { //ideally would like to override the getter in the Model class but this will have to do.
                if (this instanceof Model) {
                    return this.toJSON();
                } else {
                    return this._myValue;
                }
            },
            set: function (newValue) { this.setValue(newValue);}
        });

        this.setValue(value);
    }

    Property.prototype.getName = function () {
        return this._name;
    };

    /**
     * [getter_setter description]
     * @param  {[type]} newValue The Value you want to assign to the Property.
     * @param  {[type]} options  May contain the following:
     *                           suppressNotifications - boolean indicating if listeners should be notified of change.
     *
     * @return {[type]}          The resulting value of the Property
     */
    Property.prototype.setValue = function (value, suppressNotifications) {
        var newValue,
            suppressOnChangeEvent = suppressNotifications;

        if (isObject(value) && value._value) {
            newValue = value._value;
            suppressOnChangeEvent = value.suppressNotifications;
        } else {
            newValue = value;
        }

        // Note: this disallows setting a property to undefined. Only when it's first created can it be undefined.
        if (newValue !== undefined && newValue !== this._myValue) {
            var validationFunction = this._options && this._options.validator;

            if (!validationFunction || validationFunction(newValue)){
                var oldValue = this._myValue;

                if (newValue instanceof Property || newValue instanceof Model ) {
                    // this is misleading syntax because other property attributes are not copied like _listener and _parent
                    // so prevent it and provide alternate.
                    window.console.error("Incorrect Syntax: use ._value = [property|model]._value instead");
                } else if (isObject(newValue)) {
                    if (!(this instanceof Model)){
                        window.console.error("Not Supported: Can't set the Model value to a property. Delete the model and use createProperty");
                    } else {
                        //This model need to be set to the newValue
                        this.merge(newValue, false);
                    }
                } else { // newValue is a property
                    if (this instanceof Model){
                        window.console.error("Not Supported: Can't set a Property value to a model. Delete the property and use createProperty");
                    } else {
                        this._myValue = newValue;
                    }
                }

                if (!suppressOnChangeEvent){
                    eventProxy.fireEvent(this, oldValue);
                }
            }
        }
        return this._myValue;
    };

    /**
     * [addChangeCallback description]
     * @param {Function} callback [description]
     * @param {[type]}   options May contain the following:
     *                         listenToChildren - register the listener with any sub property change.
     */
    Property.prototype.onChange = function (callback, options) {
        if (options && options.listenToChildren){
            this._modelListeners.push(callback);
        } else {
            this._propertyListeners.push(callback);
        }
        return this;
    };

    Property.prototype.hasValidator = function () {
        return !!this._options.validator;
    };

    Property.prototype.validateValue = function (value) {
        if (this._options.validator){
            return this._options.validator(value);
        }
        return true;
    };
    //don't want a set validator function.

    /**
     * The model Object that wraps the JSON.
     *
     * @param {[type]} options May contain the following:
     *                         validator - a function to validate the new value is valid before it is assigned.
     *                         name - name of modei
     * @param {[type]} json [description]
     */
    function Model (json, options, parent) {
        var jsonModel = json || {} ,
            modelOptions = options|| {},
            modelName = (modelOptions.name || "root"),
            modelParent = parent || null;

        //A Model is in itself a Property so let inherit property
        // call with empty options for now and this is the value
        Property.call(this, modelName, jsonModel, modelParent, modelOptions);

        Object.keys(jsonModel).forEach(function (name){

            if (name.match(Model.PROPERTY_OPTIONS_SERIALIZED_NAME_REGEX)){ // skip special meta data properties
                return;
            }

            var value = jsonModel[name];
            var options = json[name + Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX];

            this.createProperty(name, value, options);
        }, this);
    }
    Model.prototype = Object.create(Property.prototype);

    Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX = "__modeljs__options";
    Model.PROPERTY_OPTIONS_SERIALIZED_NAME_REGEX = /__modeljs__options$/;

    /**
     * [createProperty description]
     * @param  {[type]} name    [description]
     * @param  {[type]} value   [description]
     * @param  {[type]} options [description]
     * @return {[type]}         [description]
     */
    Model.prototype.createProperty = function createProperty(name, value, options) {
        if (value instanceof Model || value instanceof Property){
            window.console.error("Unsupported Operation: Try passing the Model/Properties value instead");
        } else if (isObject(value)){
            var modelOptions = options || {};
            modelOptions.name = name;
            this[name] = new Model(value, modelOptions, this);
        } else {
            this[name] = new Property (name, value, this, options);
        }
        return this;
    };

    Model.prototype.clone = function (){
        var myName = this.getName();
        var options = {
            name : myName.substring(myName.lastIndexOf("/") + 1),
            validator: this._options.validator
        };
        return new Model(this.toJSON(true), options);
    };

    function mergeLoop (model, json, doModification, keepOldProperties) {

        for (var name in json) {
            var value = json[name];
            if (model[name]){
                if (isObject(value)){ // right hand side is an object
                    if (model[name] instanceof Model) {// merging objects
                        var successful = mergeLoop(model[name], value, doModification, keepOldProperties );
                        if (!successful){
                            return false;
                        }
                    } else {
                        // Trying to assign a model to a property. This will fail.
                        return false;
                    }

                } else { // right hand side is a property
                    if (!(model[name] instanceof Model)){ // Its not a Model therefore it's a Property
                        if (doModification){
                            model[name]._value = value;
                        }
                    } else {
                        // Trying to assign a property to a Model. This will fail.
                        return false;
                    }
                }
            } else { //create new property
                if (doModification){
                    model.createProperty(name, value);
                }
            }
        }

        // delete properties that are not found in json
        if (!keepOldProperties && doModification){
            for (var modelProp in model) {
                if (!json[modelProp]){
                    delete model[modelProp];
                }
            }
        }
        return true;
    }

    Model.prototype.merge = function (json, keepOldProperties) {
        //will merge the properties in json with this. result will be the same as the Object extend.
        //if a property exists in the model but not in the json it will only be kept if keepOldProperties is true.
        if (mergeLoop(this, json, false, keepOldProperties)){// check if merge will be successful
            Model.startTransaction();
            mergeLoop(this, json, true, keepOldProperties);
            Model.endTransaction();
            return true;
        } else {
            window.console.error("Operation Not Supported: Model assignment not valid. Model not modified");
            return false;
        }
    };

    /**
     * [toJSON description]
     * @param  {[type]} includeMetaData indicates if model meta data should be included in the returned JSON
     * @return {[type]}                 The json representation of the Model.
     */
    Model.prototype.toJSON = function (includeMetaData) {
        var json = {};
        Object.keys(this).forEach( function (name){
            var property = this[name];
            if (property instanceof Model) {
                json[name] = property.toJSON(includeMetaData);
            } else {
                var value = this[name]._value;
                json[name] = value;
                if (includeMetaData && this[name]._options){
                    json[name + Model.PROPERTY_OPTIONS_SERIALIZED_NAME_SUFFIX] = this[name]._options;
                }
            }
        }, this);
        return json;
    };

    Model.startTransaction = function () {
        eventProxy.startTransaction();
    };

    Model.endTransaction = function (options) {
        var originalEventOptimization;

        if (options){ // if option override global setting keeping them so they can be restored later
            originalEventOptimization = JSON.parse(JSON.stringify(Model.eventOptimization));
            Model.eventOptimization.suppressPreviousPropertyChangeEvents = !!options.suppressPreviousPropertyChangeEvents;
            Model.eventOptimization.enableSingleCallbackCall = !!options.enableSingleCallbackCall;
            Model.eventOptimization.enableCallbackHashOpimization = !!options.enableCallbackHashOpimization;
        }

        eventProxy.endTransaction();

        if (options){ //restore global settings
            Model.eventOptimization.suppressPreviousPropertyChangeEvents = originalEventOptimization.suppressPreviousPropertyChangeEvents;
            Model.eventOptimization.enableSingleCallbackCall = originalEventOptimization.enableSingleCallbackCall;
            Model.eventOptimization.enableCallbackHashOpimization = originalEventOptimization.enableCallbackHashOpimization;
        }

    };

    Model.inTransaction = function() {
        return eventProxy.inTransaction();
    };

    Model.eventOptimization = {
        suppressPreviousPropertyChangeEvents: false, //Only fires last Property Change of a property during a transaction.
        enableSingleCallbackCall: false, //will make sure a callback only gets called only once during a transaction. Even if registared with several properties.
        enableCallbackHashOpimization: false //will make sure a callbacks itentified by .hash only gets called only once during a transaction. Even if registared with several properties.
    };
    Object.seal(Model.eventOptimization);

    var oldModel = window.Model;
    Model.noConflict = function () {
        window.Model = oldModel;
        return this;
    };

    /** @global */
    window.Model = Model;
}(window));<|endoftext|>


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

/**
Command-line interface (CLI) for HackMyResume.
@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot)
@module index.js
*/

var SPAWNW = require('./core/spawn-watch')
  , HMR  = require( './hackmyapi')
  , PKG = require('../package.json')
  , FS = require('fs')
  , EXTEND = require('./utils/extend')
  , chalk = require('chalk')
  , PATH = require('path')
  , HACKMYSTATUS = require('./core/status-codes')
  , safeLoadJSON = require('./utils/safe-json-loader')
  , _opts = { }
  , title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
  , StringUtils = require('./utils/string.js')
  , _ = require('underscore')
  , Command = require('commander').Command;

try {
  main();
}
catch( ex ) {
  require('./core/error-handler').err( ex, true );
}

/**
Kick off the HackMyResume application.
*/
function main() {

  var args = initialize();

  // Create the top-level (application) command...
  var program = new Command('hackmyresume')
    .version(PKG.version)
    .description(chalk.yellow.bold('*** HackMyResume ***'))
    .option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
    .option('-s --silent', 'Run in silent mode')
    .option('--no-color', 'Disable colors')
    .option('--color', 'Enable colors');
    //.usage('COMMAND <sources> [TO <targets>]');

  // Create the NEW command
  program
    .command('new')
    .arguments('<sources...>')
    .option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
    .alias('create')
    .description('Create resume(s) in FRESH or JSON RESUME format.')
    .action(function( sources ) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the VALIDATE command
  program
    .command('validate')
    .arguments('<sources...>')
    .description('Validate a resume in FRESH or JSON RESUME format.')
    .action(function(sources) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the CONVERT command
  program
    .command('convert')
    //.arguments('<sources...>')
    .description('Convert a resume to/from FRESH or JSON RESUME format.')
    .action(function() {
      var x = splitSrcDest.call( this );
      execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
    });

  // Create the ANALYZE command
  program
    .command('analyze')
    .arguments('<sources...>')
    .description('Analyze one or more resumes.')
    .action(function( sources ) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the BUILD command
  program
    .command('build')
    .alias('generate')
    //.arguments('<sources> TO [targets]')
    //.usage('...')
    .option('-t --theme <theme>', 'Theme name or path')
    .option('-n --no-prettify', 'Disable HTML prettification', true)
    .option('-c --css <option>', 'CSS linking / embedding', 'embed')
    .option('-p --pdf <engine>', 'PDF generation engine')
    .description('Generate resume to multiple formats')
    .action(function( sources, targets, options ) {
      var x = splitSrcDest.call( this );
      execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
    });

  // program.on('--help', function(){
  //   console.log('  Examples:');
  //   console.log('');
  //   console.log('    $ custom-help --help');
  //   console.log('    $ custom-help -h');
  //   console.log('');
  // });

  program.parse( args );

  if (!program.args.length) { throw { fluenterror: 4 }; }

}

/**
Massage command-line args and setup Commander.js.
*/
function initialize() {

  logMsg( title );

  // Support case-insensitive sub-commands (build, generate, validate, etc.)..
  var oVerb, verb = '', args = process.argv.slice(), cleanArgs = args.slice(2);
  if( cleanArgs.length ) {
    var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
    if( verbIdx !== -1 ) {
      oVerb = cleanArgs[ verbIdx ];
      verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
    }
  }

  // Handle invalid verbs here (a bit easier here than in commander.js)...
  if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
    throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
            attempted: oVerb };
  }

  // Override the .missingArgument behavior
  Command.prototype.missingArgument = function(name) {
    throw { fluenterror: HACKMYSTATUS.resumeNotFound };
  };

  // Override the .helpInformation behavior
  Command.prototype.helpInformation = function() {
    var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
    return chalk.green.bold(manPage);
  };

  return args;
}

/**
Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
  loadOptions.call( this, opts );
  HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}

/**
Initialize HackMyResume options.
*/
function loadOptions( opts ) {

  opts.opts = this.parent.opts;

  // Load the specified options file (if any) and apply options
  if( opts.opts && String.is( opts.opts )) {
    var json = safeLoadJSON( PATH.relative( process.cwd(), opts.opts ) );
    json && ( opts = EXTEND( true, opts, json ) );
    if( !json ) {
      throw safeLoadJSON.error;
    }
  }

  // Merge in command-line options
  opts = EXTEND( true, opts, this.opts() );
  opts.silent = this.parent.silent;
  _opts = opts;
}

/**
Split multiple command-line filenames by the 'TO' keyword
*/
function splitSrcDest() {

  var params = this.parent.args.filter(function(j) { return String.is(j); });
  if( params.length === 0 )
    throw { fluenterror: HACKMYSTATUS.resumeNotFound };

  // Find the TO keyword, if any
  var splitAt = _.findIndex( params, function(p) {
    return p.toLowerCase() === 'to';
  });

  // TO can't be the last keyword
  if( splitAt === params.length - 1 && splitAt !== -1 ) {
    logMsg(chalk.yellow('Please ') +
      chalk.yellow.bold('specify an output file') +
      chalk.yellow(' for this operation or ') +
      chalk.yellow.bold('omit the TO keyword') +
      chalk.yellow('.') );
    return;
  }

  return {
    src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
    dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
  };
}

/**
Simple logging placeholder.
*/
function logMsg( msg ) {
  msg = msg || '';
  _opts.silent || console.log( msg );
}
<commit_msg>Support --no-tips flag.<commit_after>#! /usr/bin/env node

/**
Command-line interface (CLI) for HackMyResume.
@license MIT. Copyright (c) 2015 hacksalot (https://github.com/hacksalot)
@module index.js
*/

var SPAWNW = require('./core/spawn-watch')
  , HMR  = require( './hackmyapi')
  , PKG = require('../package.json')
  , FS = require('fs')
  , EXTEND = require('./utils/extend')
  , chalk = require('chalk')
  , PATH = require('path')
  , HACKMYSTATUS = require('./core/status-codes')
  , safeLoadJSON = require('./utils/safe-json-loader')
  , _opts = { }
  , title = chalk.white.bold('\n*** HackMyResume v' + PKG.version + ' ***')
  , StringUtils = require('./utils/string.js')
  , _ = require('underscore')
  , Command = require('commander').Command;

try {
  main();
}
catch( ex ) {
  require('./core/error-handler').err( ex, true );
}

/**
Kick off the HackMyResume application.
*/
function main() {

  var args = initialize();

  // Create the top-level (application) command...
  var program = new Command('hackmyresume')
    .version(PKG.version)
    .description(chalk.yellow.bold('*** HackMyResume ***'))
    .option('-o --opts <optionsFile>', 'Path to a .hackmyrc options file')
    .option('-s --silent', 'Run in silent mode')
    .option('--no-color', 'Disable colors')
    .option('--color', 'Enable colors');
    //.usage('COMMAND <sources> [TO <targets>]');

  // Create the NEW command
  program
    .command('new')
    .arguments('<sources...>')
    .option('-f --format <fmt>', 'FRESH or JRS format', 'FRESH')
    .alias('create')
    .description('Create resume(s) in FRESH or JSON RESUME format.')
    .action(function( sources ) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the VALIDATE command
  program
    .command('validate')
    .arguments('<sources...>')
    .description('Validate a resume in FRESH or JSON RESUME format.')
    .action(function(sources) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the CONVERT command
  program
    .command('convert')
    //.arguments('<sources...>')
    .description('Convert a resume to/from FRESH or JSON RESUME format.')
    .action(function() {
      var x = splitSrcDest.call( this );
      execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
    });

  // Create the ANALYZE command
  program
    .command('analyze')
    .arguments('<sources...>')
    .description('Analyze one or more resumes.')
    .action(function( sources ) {
      execVerb.call( this, sources, [], this.opts(), logMsg);
    });

  // Create the BUILD command
  program
    .command('build')
    .alias('generate')
    //.arguments('<sources> TO [targets]')
    //.usage('...')
    .option('-t --theme <theme>', 'Theme name or path')
    .option('-n --no-prettify', 'Disable HTML prettification', true)
    .option('-c --css <option>', 'CSS linking / embedding', 'embed')
    .option('-p --pdf <engine>', 'PDF generation engine')
    .option('--no-tips', 'Disable theme tips and warnings.', false)
    .description('Generate resume to multiple formats')
    .action(function( sources, targets, options ) {
      var x = splitSrcDest.call( this );
      execVerb.call( this, x.src, x.dst, this.opts(), logMsg);
    });

  // program.on('--help', function(){
  //   console.log('  Examples:');
  //   console.log('');
  //   console.log('    $ custom-help --help');
  //   console.log('    $ custom-help -h');
  //   console.log('');
  // });

  program.parse( args );

  if (!program.args.length) { throw { fluenterror: 4 }; }

}

/**
Massage command-line args and setup Commander.js.
*/
function initialize() {

  logMsg( title );

  // Support case-insensitive sub-commands (build, generate, validate, etc.)..
  var oVerb, verb = '', args = process.argv.slice(), cleanArgs = args.slice(2);
  if( cleanArgs.length ) {
    var verbIdx = _.findIndex( cleanArgs, function(v){ return v[0] !== '-'; });
    if( verbIdx !== -1 ) {
      oVerb = cleanArgs[ verbIdx ];
      verb = args[ verbIdx + 2 ] = oVerb.trim().toLowerCase();
    }
  }

  // Handle invalid verbs here (a bit easier here than in commander.js)...
  if( verb && !HMR.verbs[ verb ] && !HMR.alias[ verb ] ) {
    throw { fluenterror: HACKMYSTATUS.invalidCommand, shouldExit: true,
            attempted: oVerb };
  }

  // Override the .missingArgument behavior
  Command.prototype.missingArgument = function(name) {
    throw { fluenterror: HACKMYSTATUS.resumeNotFound };
  };

  // Override the .helpInformation behavior
  Command.prototype.helpInformation = function() {
    var manPage = FS.readFileSync( PATH.join(__dirname, 'use.txt'), 'utf8' );
    return chalk.green.bold(manPage);
  };

  return args;
}

/**
Invoke a HackMyResume verb.
*/
function execVerb( src, dst, opts, log ) {
  loadOptions.call( this, opts );
  HMR.verbs[ this.name() ].call( null, src, dst, _opts, log );
}

/**
Initialize HackMyResume options.
*/
function loadOptions( opts ) {

  opts.opts = this.parent.opts;

  // Load the specified options file (if any) and apply options
  if( opts.opts && String.is( opts.opts )) {
    var json = safeLoadJSON( PATH.relative( process.cwd(), opts.opts ) );
    json && ( opts = EXTEND( true, opts, json ) );
    if( !json ) {
      throw safeLoadJSON.error;
    }
  }

  // Merge in command-line options
  opts = EXTEND( true, opts, this.opts() );
  opts.silent = this.parent.silent;
  _opts = opts;
}

/**
Split multiple command-line filenames by the 'TO' keyword
*/
function splitSrcDest() {

  var params = this.parent.args.filter(function(j) { return String.is(j); });
  if( params.length === 0 )
    throw { fluenterror: HACKMYSTATUS.resumeNotFound };

  // Find the TO keyword, if any
  var splitAt = _.findIndex( params, function(p) {
    return p.toLowerCase() === 'to';
  });

  // TO can't be the last keyword
  if( splitAt === params.length - 1 && splitAt !== -1 ) {
    logMsg(chalk.yellow('Please ') +
      chalk.yellow.bold('specify an output file') +
      chalk.yellow(' for this operation or ') +
      chalk.yellow.bold('omit the TO keyword') +
      chalk.yellow('.') );
    return;
  }

  return {
    src: params.slice(0, splitAt === -1 ? undefined : splitAt ),
    dst: splitAt === -1 ? [] : params.slice( splitAt + 1 )
  };
}

/**
Simple logging placeholder.
*/
function logMsg( msg ) {
  msg = msg || '';
  _opts.silent || console.log( msg );
}
<|endoftext|>


<commit_before>var RSVP = require('rsvp');

var Query = require('./query.js');
var Collection = require('./collection.js');
var _ = require('./utils.js');

var Instance = function() {
  var inst = {
    klass: 'Instance',
    model: null,
    id: null,
    localId: _.uuid(),
    data: {
      local: {},
      remote: {},
      temporary: {}
    },
    // collections: {},
    computedProperties: {},

    // The server URL of this model instance
    url: function() {
      return this.id && this.model.url() + '/' + this.id;
    },

    // Return the current value at the given key
    get: function(key) {
      // Catch remaining arguments as parameters for computed properties
      var args = Array.prototype.slice.call(arguments).splice(1);
      //XXX Should return a deep copy of defaults and remote data
      // var value = this.collections[key] || this.data.local[key] || this.data.remote[key] || this.model.defaults[key];
      var value = this.data.local[key];
      if(value === undefined) {
        value = this.data.remote[key];
      }
      // Defaults may have changed in code since object was created
      //XXX Do the same for default collections and queries
      if(value === undefined) {
        value = this.model.defaults[key];
      }
      // Use temporary value as last resort
      if(value === undefined) {
        value = this.data.temporary[key];
      }
      // Methods called through get() are treated as computed properties
      if(typeof(this[key]) == 'function') {
        if(!value) {
          var self = this;
          var comp = findDependencies(function() {
            // Pass potential arguments to computed property
            return self[key].apply(self, args);
          });
          value = comp.value;
          // Remember dependencies
          this.computedProperties[key] = comp.keys;
        }
      } else {
        // Leave a trace in case we were called from a computed property
        propertiesUsed.push(key);
      }
      return value;
    },

    // Register a local change
    set: function(valuesOrKey, value, options) {
      options = options || {};
      // Allow for setting single values or whole hashes
      var values;
      if(value === undefined) {
        values = valuesOrKey;
      } else {
        values = {};
        values[valuesOrKey] = value;
      }
      for(var key in values) {
        // Save given value locally
        if(options.local) {
          this.data.temporary[key] = values[key];
        } else {
          this.data.local[key] = values[key];
        }
        //XXX Catch attempt to set a computed property or collection
        // Emit specific change event
        this.emit('change:' + key);
        // Emit change events for computed properties as well
        for(var k in this.computedProperties) {
          if(_.contains(this.computedProperties[k], key)) {
            this.emit('change:' + k);
          }
        }
      }
      // Emit generic change event
      this.emit('change');
      return this;
    },

    // The current state of the object, including local modifications
    properties: function() {
      return _.merge(this.data.remote, this.data.local);
    },

    // Are local modifications present that need to be saved?
    isDirty: function() {
      return !!Object.keys(this.data.local).length;
    },

    // Discard local modifications and revert to server state
    // Will also discard temporary data
    revert: function() {
      // var locals = _.merge(this.data.temporary, this.data.local);
      var locals = this.data.local;
      this.data.local = {};
      // this.data.temporary = {};
      for(var key in locals) {
        this.emit('change:' + key);
      }
      this.emit('change');
      this.emit('revert');
      return this;
    },

    // Persist object
    // Also save local modifications to the server if possible
    save: function(values) {
      var self = this;
      // Detect event object to allow calling from actions
      if(values && values.target) {
        values = undefined;
      }
      // Allow setting values immediately before saving
      if(values) {
        self.set(values);
      }
      if(self.isDirty() || !self.id) {
        // Persist local changes to server
        var url = self.id ? self.url() : self.model.url();
        if(self.id) {
          self.model.dataInterface.update(self.id, self.data.local, function(err, updatedValues) {
            self.data.remote = _.merge(self.data.remote, updatedValues);
            finish();
          });
        } else {
          // Create fresh instance on the server
          //XXX offline case
          self.model.dataInterface.create(self, function(err, data) {
            self.data.remote = data;
            // self.id = data._id;
            self.connect();
            finish();
          });
        }
        var finish = function() {
          self.data.local = {};
          // cb && typeof(cb) == 'function' && cb();
          self.emit('save');
        };
      } else {
        // Emit save event anyway to be more predictable
        self.emit('save');
      }
      return self;
    },

    // Update the base state of the object from the server,
    // but leave local modifications intact
    fetch: function(cb) {
      var self = this;
      self.model.dataInterface.one(self.id, function(err, data) {
        if(err) {
          cb(null);
        } else {
          //XXX Rebuild collections
          //XXX Trigger change events
          self.data.remote = data;
          // localStore.save(self.localId, self.data);
          cb(self);
          self.emit('fetch');
        }
      }, true);
      return self;
    },

    // Delete object from all local models and collections it's referenced from
    // Also delete from the database, if model was persisted
    //XXX All references from remote models and collections will auto-dissolve on next read
    delete: function(cb) {
      var self = this;
      self.disconnect();
      self.deleted = true;
      self.model.dataInterface.delete(self.id, function() {
        // Check if cb really is a callback -> Allows for directly invoking delete as an action handler
        cb && typeof(cb) == 'function' && cb();
        self.emit('delete');
        // Discard event handlers
        self.discardEventHandlers();
      });
    },

    // Returns a reference object, usable for storage in the db
    reference: function() {
      return {_ref: {id: this.id || this.localId, collection: this.model.name}};
    },

    // Serialize object into a representation suitable for
    // storage on the server by referencing other model instances by ID
    serialize: function() {
      return references(this.properties());
    },

    // Load all referenced model instances
    resolve: function(cb) {
      var self = this;
      var resolved = _.map(self.data.remote, function(item) {
        if(item && item._ref) {
          var model = models[item._ref.collection];
          return model.load(item._ref.id);
        } else {
          return RSVP.resolve(item);
        }
      });
      RSVP.hash(resolved).then(function(instances) {
        _.each(instances, function(inst, key) {
          self.data.remote[key] = inst;
        });
        cb();
      });
    },

    // Subscribe to push updates from the server
    connect: function() {
      var self = this;
      if(self.connected) throw("Trying to connect twice");
      if(!self.id) throw("Trying to connect unsaved model");
      // Merge push data into remote data bucket and emit change events
      self.model.app.pubSub.subscribe('update', self.model.name, self.id, function(data) {
        console.log("Received push update");
        console.log(data.data);
        self.data.remote = _.merge(self.data.remote, data.data);
        for(var key in data.data) {
          self.emit('change:' + key);
        }
        self.emit('change');
        self.emit('fetch');
      });
      // Delete object unless the delete push originated from ourselves
      self.model.app.pubSub.subscribe('delete', self.model.name, self.id, function() {
        console.log("Received push delete");
        if(!self.deleted) self.delete();
      });
      self.connected = true;
      return self;
    },

    // Unsubscribe from updates
    disconnect: function() {
      this.model.app.pubSub.unsubscribe(this.model.name, this.id);
      this.connected = false;
      return this;
    }
  };

  _.eventHandling(inst);

  return inst;
};

var references = function(values) {
  var out = {};
  for(var key in values) {
    var value = values[key];
    if(value && value.klass == 'Instance') {
      out[key] = value.reference();
    } else if(value && value.klass == 'Collection') {
      out[key] = value.serialize();
    } else {
      out[key] = value;
    }
  }
  return out;
};

var models = {}; //XXX This is per package and should be per app

// Define a new data model under the given name
var Model = function(dbCollection, reference, constructor) {
  var ref = separateMethods(reference);
  var model = {
    klass: 'Model',
    name: dbCollection,
    defaults: ref.defaults,

    // The server URL of this model's collection
    url: function() {
      return '/api/' + this.name;
    },

    // Return a Query over all items of this model
    all: function() {
      return Query(this);
    },

    // Return a Query over all items of this model with a filter already set
    filter: function(query, options) {
      return Query(this, query, options);
    },

    // Create a fresh model instance
    create: function(values) {
      var inst = Instance();
      inst.model = this;
      // Mix in methods from reference object
      for(var key in ref.methods) {
        inst[key] = ref.methods[key];
      }
      // Copy default collections to instance
      for(var key in ref.collections) {
        var collection = ref.collections[key].clone();
        inst.data.remote[key] = collection;
      }
      // Copy default queries to instance
      for(var key in ref.queries) {
        var query = ref.queries[key].clone();
        inst.data.remote[key] = query;
      }
      // Copy default values from model and arguments
      _.each(_.merge(ref.defaults, values), function(value, key) {
        inst.data.remote[key] = value;
      });
      // Call constructor function
      constructor && constructor.call(inst, values);
      return inst;
    },

    // Load the object with the given id
    // Local storage will be used immediately if available
    // Server data gets fetched afterwards
    load: function(id, cb) {
      var self = this;
      return _.promise(function(ok, fail) {
        self.dataInterface.one(id, function(err, inst) {
          if(inst) {
            cb && cb(inst);
            ok(inst);
          } else {
            cb && cb();
            fail('Could not load ' + self.name + '#' + id);
          }
        });
      });
    }
  };
  models[model.name] = model;
  return model;
};

// Log all getters called while generating
// a computed property in a global array
propertiesUsed = [];
var findDependencies = function(cb) {
  propertiesUsed = [];
  var ret = cb();
  return {
    value: ret,
    keys: propertiesUsed
  };
};

// Separate the methods from the other values
var separateMethods = function(reference) {
  var ret = {
    defaults: {},
    methods: {},
    collections: {},
    queries: {}
  };
  for(var key in reference) {
    var val = reference[key];
    if(typeof(val) == 'function') {
      ret.methods[key] = val;
    } else if(val && val.klass == 'Collection') {
      ret.collections[key] = val;
    } else if(val && val.klass == 'Query') {
      ret.queries[key] = val;
    } else if(val && Array.isArray(val)) {
      ret.collections[key] = Collection(val);
    } else {
      ret.defaults[key] = val;
    }
  }
  return ret;
};

module.exports = Model;
<commit_msg>Clone default properties when creating model instances<commit_after>var RSVP = require('rsvp');

var Query = require('./query.js');
var Collection = require('./collection.js');
var _ = require('./utils.js');

var Instance = function() {
  var inst = {
    klass: 'Instance',
    model: null,
    id: null,
    localId: _.uuid(),
    data: {
      local: {},
      remote: {},
      temporary: {}
    },
    // collections: {},
    computedProperties: {},

    // The server URL of this model instance
    url: function() {
      return this.id && this.model.url() + '/' + this.id;
    },

    // Return the current value at the given key
    get: function(key) {
      // Catch remaining arguments as parameters for computed properties
      var args = Array.prototype.slice.call(arguments).splice(1);
      //XXX Should return a deep copy of defaults and remote data
      // var value = this.collections[key] || this.data.local[key] || this.data.remote[key] || this.model.defaults[key];
      var value = this.data.local[key];
      if(value === undefined) {
        value = this.data.remote[key];
      }
      // Defaults may have changed in code since object was created
      //XXX Do the same for default collections and queries
      if(value === undefined) {
        value = this.model.defaults[key];
      }
      // Use temporary value as last resort
      if(value === undefined) {
        value = this.data.temporary[key];
      }
      // Methods called through get() are treated as computed properties
      if(typeof(this[key]) == 'function') {
        if(!value) {
          var self = this;
          var comp = findDependencies(function() {
            // Pass potential arguments to computed property
            return self[key].apply(self, args);
          });
          value = comp.value;
          // Remember dependencies
          this.computedProperties[key] = comp.keys;
        }
      } else {
        // Leave a trace in case we were called from a computed property
        propertiesUsed.push(key);
      }
      return value;
    },

    // Register a local change
    set: function(valuesOrKey, value, options) {
      options = options || {};
      // Allow for setting single values or whole hashes
      var values;
      if(value === undefined) {
        values = valuesOrKey;
      } else {
        values = {};
        values[valuesOrKey] = value;
      }
      for(var key in values) {
        // Save given value locally
        if(options.local) {
          this.data.temporary[key] = values[key];
        } else {
          this.data.local[key] = values[key];
        }
        //XXX Catch attempt to set a computed property or collection
        // Emit specific change event
        this.emit('change:' + key);
        // Emit change events for computed properties as well
        for(var k in this.computedProperties) {
          if(_.contains(this.computedProperties[k], key)) {
            this.emit('change:' + k);
          }
        }
      }
      // Emit generic change event
      this.emit('change');
      return this;
    },

    // The current state of the object, including local modifications
    properties: function() {
      return _.merge(this.data.remote, this.data.local);
    },

    // Are local modifications present that need to be saved?
    isDirty: function() {
      return !!Object.keys(this.data.local).length;
    },

    // Discard local modifications and revert to server state
    // Will also discard temporary data
    revert: function() {
      // var locals = _.merge(this.data.temporary, this.data.local);
      var locals = this.data.local;
      this.data.local = {};
      // this.data.temporary = {};
      for(var key in locals) {
        this.emit('change:' + key);
      }
      this.emit('change');
      this.emit('revert');
      return this;
    },

    // Persist object
    // Also save local modifications to the server if possible
    save: function(values) {
      var self = this;
      // Detect event object to allow calling from actions
      if(values && values.target) {
        values = undefined;
      }
      // Allow setting values immediately before saving
      if(values) {
        self.set(values);
      }
      if(self.isDirty() || !self.id) {
        // Persist local changes to server
        var url = self.id ? self.url() : self.model.url();
        if(self.id) {
          self.model.dataInterface.update(self.id, self.data.local, function(err, updatedValues) {
            self.data.remote = _.merge(self.data.remote, updatedValues);
            finish();
          });
        } else {
          // Create fresh instance on the server
          //XXX offline case
          self.model.dataInterface.create(self, function(err, data) {
            self.data.remote = data;
            // self.id = data._id;
            self.connect();
            finish();
          });
        }
        var finish = function() {
          self.data.local = {};
          // cb && typeof(cb) == 'function' && cb();
          self.emit('save');
        };
      } else {
        // Emit save event anyway to be more predictable
        self.emit('save');
      }
      return self;
    },

    // Update the base state of the object from the server,
    // but leave local modifications intact
    fetch: function(cb) {
      var self = this;
      self.model.dataInterface.one(self.id, function(err, data) {
        if(err) {
          cb(null);
        } else {
          //XXX Rebuild collections
          //XXX Trigger change events
          self.data.remote = data;
          // localStore.save(self.localId, self.data);
          cb(self);
          self.emit('fetch');
        }
      }, true);
      return self;
    },

    // Delete object from all local models and collections it's referenced from
    // Also delete from the database, if model was persisted
    //XXX All references from remote models and collections will auto-dissolve on next read
    delete: function(cb) {
      var self = this;
      self.disconnect();
      self.deleted = true;
      self.model.dataInterface.delete(self.id, function() {
        // Check if cb really is a callback -> Allows for directly invoking delete as an action handler
        cb && typeof(cb) == 'function' && cb();
        self.emit('delete');
        // Discard event handlers
        self.discardEventHandlers();
      });
    },

    // Returns a reference object, usable for storage in the db
    reference: function() {
      return {_ref: {id: this.id || this.localId, collection: this.model.name}};
    },

    // Serialize object into a representation suitable for
    // storage on the server by referencing other model instances by ID
    serialize: function() {
      return references(this.properties());
    },

    // Load all referenced model instances
    resolve: function(cb) {
      var self = this;
      var resolved = _.map(self.data.remote, function(item) {
        if(item && item._ref) {
          var model = models[item._ref.collection];
          return model.load(item._ref.id);
        } else {
          return RSVP.resolve(item);
        }
      });
      RSVP.hash(resolved).then(function(instances) {
        _.each(instances, function(inst, key) {
          self.data.remote[key] = inst;
        });
        cb();
      });
    },

    // Subscribe to push updates from the server
    connect: function() {
      var self = this;
      if(self.connected) throw("Trying to connect twice");
      if(!self.id) throw("Trying to connect unsaved model");
      // Merge push data into remote data bucket and emit change events
      self.model.app.pubSub.subscribe('update', self.model.name, self.id, function(data) {
        console.log("Received push update");
        console.log(data.data);
        self.data.remote = _.merge(self.data.remote, data.data);
        for(var key in data.data) {
          self.emit('change:' + key);
        }
        self.emit('change');
        self.emit('fetch');
      });
      // Delete object unless the delete push originated from ourselves
      self.model.app.pubSub.subscribe('delete', self.model.name, self.id, function() {
        console.log("Received push delete");
        if(!self.deleted) self.delete();
      });
      self.connected = true;
      return self;
    },

    // Unsubscribe from updates
    disconnect: function() {
      this.model.app.pubSub.unsubscribe(this.model.name, this.id);
      this.connected = false;
      return this;
    }
  };

  _.eventHandling(inst);

  return inst;
};

var references = function(values) {
  var out = {};
  for(var key in values) {
    var value = values[key];
    if(value && value.klass == 'Instance') {
      out[key] = value.reference();
    } else if(value && value.klass == 'Collection') {
      out[key] = value.serialize();
    } else {
      out[key] = value;
    }
  }
  return out;
};

var models = {}; //XXX This is per package and should be per app

// Define a new data model under the given name
var Model = function(dbCollection, reference, constructor) {
  var ref = separateMethods(reference);
  var model = {
    klass: 'Model',
    name: dbCollection,
    defaults: ref.defaults,

    // The server URL of this model's collection
    url: function() {
      return '/api/' + this.name;
    },

    // Return a Query over all items of this model
    all: function() {
      return Query(this);
    },

    // Return a Query over all items of this model with a filter already set
    filter: function(query, options) {
      return Query(this, query, options);
    },

    // Create a fresh model instance
    create: function(values) {
      var inst = Instance();
      inst.model = this;
      // Mix in methods from reference object
      for(var key in ref.methods) {
        inst[key] = ref.methods[key];
      }
      // Copy default collections to instance
      for(var key in ref.collections) {
        var collection = ref.collections[key].clone();
        inst.data.remote[key] = collection;
      }
      // Copy default queries to instance
      for(var key in ref.queries) {
        var query = ref.queries[key].clone();
        inst.data.remote[key] = query;
      }
      // Copy default values from model
      _.each(ref.defaults, function(value, key) {
        inst.data.remote[key] = _.clone(value);
      });
      // Copy constructor arguments
      _.each(values, function(value, key) {
        inst.data.remote[key] = value;
      });
      // Call constructor function
      constructor && constructor.call(inst, values);
      return inst;
    },

    // Load the object with the given id
    // Local storage will be used immediately if available
    // Server data gets fetched afterwards
    load: function(id, cb) {
      var self = this;
      return _.promise(function(ok, fail) {
        self.dataInterface.one(id, function(err, inst) {
          if(inst) {
            cb && cb(inst);
            ok(inst);
          } else {
            cb && cb();
            fail('Could not load ' + self.name + '#' + id);
          }
        });
      });
    }
  };
  models[model.name] = model;
  return model;
};

// Log all getters called while generating
// a computed property in a global array
propertiesUsed = [];
var findDependencies = function(cb) {
  propertiesUsed = [];
  var ret = cb();
  return {
    value: ret,
    keys: propertiesUsed
  };
};

// Separate the methods from the other values
var separateMethods = function(reference) {
  var ret = {
    defaults: {},
    methods: {},
    collections: {},
    queries: {}
  };
  for(var key in reference) {
    var val = reference[key];
    if(typeof(val) == 'function') {
      ret.methods[key] = val;
    } else if(val && val.klass == 'Collection') {
      ret.collections[key] = val;
    } else if(val && val.klass == 'Query') {
      ret.queries[key] = val;
    } else if(val && Array.isArray(val)) {
      ret.collections[key] = Collection(val);
    } else {
      ret.defaults[key] = val;
    }
  }
  return ret;
};

module.exports = Model;
<|endoftext|>


<commit_before>/* globals Matreshka */

import Router from './router';

function initRouter(obj, route, type) {
    Router[type || 'hash'].subscribe(obj, route);
    return obj;
}

/* istanbul ignore if */
if (typeof Matreshka === 'function') {
    Matreshka.Router = Router;
    Matreshka.initRouter = initRouter;
}

module.exports = initRouter;
<commit_msg>fix: Force push fix<commit_after>/* globals Matreshka */

import Router from './router';

function initRouter(obj, route, type) {
    Router[type || 'hash'].subscribe(obj, route);
    return obj;
}

/* istanbul ignore if */
if (typeof Matreshka === 'function') {
	foobar(); // eslint-disable-line
    Matreshka.Router = Router;
    Matreshka.initRouter = initRouter;
}

module.exports = initRouter;
<|endoftext|>