├── appsscript.json ├── README.md ├── Util_.gs └── Assert.gs /appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Europe/London", 3 | "dependencies": { 4 | "libraries": [ 5 | { 6 | "userSymbol": "Dialog", 7 | "libraryId": "1zEZzCZxV-i91zf1OCOn6-wT4kTY1lco2dkAZuyZJSpHq3CkJM6OSNH__", 8 | "version": "9" 9 | } 10 | ] 11 | }, 12 | "exceptionLogging": "STACKDRIVER" 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Assert 2 | Google Apps Script library for debug, assertions, unit testing and error handling. 3 | 4 | Library ID: 1R-rxUHuQU0ez95FlZz9fxqifbYFszHnukqzv_3Uk5zM3eG4eRBrh_E1M 5 | 6 | [GSheet containing automatic regression tests.](https://docs.google.com/spreadsheets/d/1KCLIohr987jIlkcEUrtoyEAGUYeOWPgFLQgMmZ5k4k4/edit#gid=324486024) 7 | 8 | [The Script project used by the library](https://script.google.com/d/1R-rxUHuQU0ez95FlZz9fxqifbYFszHnukqzv_3Uk5zM3eG4eRBrh_E1M/edit?usp=drive_web). 9 | -------------------------------------------------------------------------------- /Util_.gs: -------------------------------------------------------------------------------- 1 | // This is taken from https://github.com/Jxck/assert/blob/master/assert.js 2 | 3 | var Util_ = { 4 | inherits: function(ctor, superCtor) { 5 | ctor.super_ = superCtor; 6 | ctor.prototype = create(superCtor.prototype, { 7 | constructor: { 8 | value: ctor, 9 | enumerable: false, 10 | writable: true, 11 | configurable: true 12 | } 13 | }); 14 | }, 15 | isArray: function(ar) { 16 | return Array.isArray(ar); 17 | }, 18 | isBoolean: function(arg) { 19 | return typeof arg === 'boolean'; 20 | }, 21 | isNull: function(arg) { 22 | return arg === null; 23 | }, 24 | isNullOrUndefined: function(arg) { 25 | return arg == null; 26 | }, 27 | isNumber: function(arg) { 28 | return typeof arg === 'number'; 29 | }, 30 | isString: function(arg) { 31 | return typeof arg === 'string'; 32 | }, 33 | isSymbol: function(arg) { 34 | return typeof arg === 'symbol'; 35 | }, 36 | isUndefined: function(arg) { 37 | return arg === void 0; 38 | }, 39 | isRegExp: function(re) { 40 | return Util_.isObject(re) && Util_.objectToString(re) === '[object RegExp]'; 41 | }, 42 | isObject: function(arg) { 43 | return typeof arg === 'object' && arg !== null; 44 | }, 45 | isDate: function(d) { 46 | return Util_.isObject(d) && Util_.objectToString(d) === '[object Date]'; 47 | }, 48 | isError: function(e) { 49 | return Util_.isObject(e) && 50 | (Util_.objectToString(e) === '[object Error]' || e instanceof Error); 51 | }, 52 | isFunction: function(arg) { 53 | return typeof arg === 'function'; 54 | }, 55 | isPrimitive: function(arg) { 56 | return arg === null || 57 | typeof arg === 'boolean' || 58 | typeof arg === 'number' || 59 | typeof arg === 'string' || 60 | typeof arg === 'symbol' || // ES6 symbol 61 | typeof arg === 'undefined'; 62 | }, 63 | objectToString: function(o) { 64 | return Object.prototype.toString.call(o); 65 | }, 66 | objectsAreEqual: function (obj) { 67 | //Loop through properties in object 1 68 | var obj1 = obj.obj1 69 | var obj2 = obj.obj2 70 | for (var p in obj1) { 71 | //Check property exists on both objects 72 | if (obj1.hasOwnProperty(p) !== obj2.hasOwnProperty(p)) return false; 73 | 74 | switch (typeof (obj1[p])) { 75 | //Deep compare objects 76 | case 'object': 77 | if (!Util_.objectsAreEqual({obj1: obj1[p], obj2: obj2[p]})) return false; 78 | break; 79 | //Compare function code 80 | case 'function': 81 | if (typeof (obj2[p]) == 'undefined' || (p != 'compare' && obj1[p].toString() != obj2[p].toString())) return false; 82 | break; 83 | //Compare values 84 | default: 85 | if (obj1[p] != obj2[p]) return false; 86 | } 87 | } 88 | 89 | //Check object 2 for any extra properties 90 | for (var p in obj2) { 91 | if (typeof (obj1[p]) == 'undefined') return false; 92 | } 93 | return true; 94 | } 95 | 96 | }; 97 | -------------------------------------------------------------------------------- /Assert.gs: -------------------------------------------------------------------------------- 1 | //234567890123456789012345678901234567890123456789012345678901234567890123456789 2 | 3 | // JShint: 22 March 2015 13:00 GMT 4 | // Unit Tests: 22 March 2015 13:00 GMT 5 | 6 | /* 7 | * Copyright (C) 2014-2018 Andrew Roberts 8 | * 9 | * This program is free software: you can redistribute it and/or modify it under 10 | * the terms of the GNU General Public License as published by the Free Software 11 | * Foundation, either version 3 of the License, or (at your option) any later 12 | * version. 13 | * 14 | * This program is distributed in the hope that it will be useful, but WITHOUT 15 | * ANY WARRANTY without even the implied warranty of MERCHANTABILITY or FITNESS 16 | * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License along with 19 | * this program. If not, see http://www.gnu.org/licenses/. 20 | */ 21 | 22 | // Assert.gs 23 | // ========= 24 | // 25 | // Debug, assertions and error handling. 26 | // 27 | // These functions assume that all top-level functions (event handlers) 28 | // catch errors thrown by any lower level functions to centralise the processing 29 | // of errors before the are passed - or not - to the user, by calling 30 | // Assert.handleError(). 31 | 32 | // TODO - Reword this to allow for not passing in the function name, just a string. 33 | // TODO - Make calling function last param 34 | 35 | var HandleError = Object.freeze({ 36 | 37 | // Throw the full error 38 | THROW: 'throw', 39 | 40 | // Just throw the user portion of the error plus internalErrorMessage 41 | THROW_SHORT: 'throwShort', 42 | 43 | // Try to display it to the user if that is possible within the context the 44 | // script is running, for example in a spreadsheet 45 | 46 | // Display the full error message using Dialog 47 | DISPLAY_FULL: 'displayFull', 48 | 49 | // Display only the user portion of the messgage using Dialog 50 | DISPLAY_SHORT: 'displayShort', 51 | 52 | // Simply return (the caller will already have the error message) 53 | RETURN: 'return', 54 | 55 | }) 56 | 57 | var DEFAULT_INTERNAL_ERROR_MESSAGE = 'Internal error - contact IT department.' 58 | 59 | // Public Functions 60 | // ---------------- 61 | 62 | /** 63 | * Handle an error. This is only called from the top level of 64 | * the script, everything else should throw an error which 65 | * will be caught at the top-level and handled by this function. 66 | * 67 | * @param {object} 68 | * {Error} error Error object 69 | * {string} userMessage User message 70 | * {object} log Logging object 71 | * {HandleError} handleError How to handle the error 72 | * {boolean} sendErrorEmail Whether to send an error notification email 73 | * {string} emailAddress Where to send the email 74 | * {string} internalErrorMessage The error to display to the user, if the actual internal error is hidden from them 75 | * {string} scriptName 76 | * {string} scriptVersion 77 | */ 78 | 79 | function handleError(config) { 80 | 81 | var functionName = 'handleError()' 82 | 83 | //TODO - Error checking 84 | 85 | if (typeof config === 'undefined') { 86 | throw new TypeError('No config') 87 | } 88 | 89 | //TODO - Use defaults - see BBLog 90 | 91 | var error = config.error 92 | var userMessage = config.userMessage || '' 93 | var internalErrorMessage = config.internalErrorMessage || DEFAULT_INTERNAL_ERROR_MESSAGE 94 | var log = config.log 95 | var fullErrorMessage = '' 96 | 97 | fullErrorMessage = 'user message:' + userMessage + ' - ' + 98 | 'name: ' + error.name + ' - ' + 99 | 'error message: ' + error.message + '\n\n' + 100 | 'fileName: ' + error.fileName + ' - ' + 101 | 'lineNumber: ' + error.lineNumber + ' - ' + 102 | 'stack: ' + error.stack 103 | 104 | console.log(fullErrorMessage) 105 | if (log) log.severe(fullErrorMessage) 106 | 107 | if (config.sendErrorEmail) { 108 | MailApp.sendEmail( 109 | config.emailAddress, 110 | 'Error thrown in ' + config.scriptName + ', ' + 111 | config.scriptVersion, 112 | 'Error: ' + fullErrorMessage) 113 | } 114 | 115 | switch (config.handleError) { 116 | 117 | case HandleError.DISPLAY_FULL: 118 | 119 | // TODO - Shouldn't we be doing some of the error checking like below?? 120 | Dialog.init(config.log) 121 | Dialog.show(userMessage, error.message) 122 | 123 | break 124 | 125 | case HandleError.DISPLAY_SHORT: 126 | 127 | Dialog.init(config.log) 128 | Dialog.show(userMessage, internalErrorMessage) 129 | break 130 | 131 | case HandleError.THROW: 132 | 133 | var message = getUserMessage() 134 | 135 | // Tag the user message on and re-throw 136 | error.message += message.hyphen + message.userMessage 137 | throw error 138 | 139 | case HandleError.THROW_SHORT: 140 | 141 | var message = getUserMessage() 142 | 143 | // Tag the user message on and re-throw 144 | error.message = internalErrorMessage + message.hyphen + message.userMessage 145 | throw error 146 | 147 | case HandleError.RETURN: 148 | return 149 | 150 | default: 151 | throw new Error(functionName + ' - bad HandleError') 152 | 153 | } // switch (config.handleError) 154 | 155 | // Private Functions 156 | // ----------------- 157 | 158 | function getUserMessage() { 159 | 160 | var hyphen = ' - ' 161 | 162 | if (Util_.isUndefined(userMessage)) { 163 | 164 | userMessage = '' 165 | hyphen = '' 166 | 167 | } else { 168 | 169 | if (!Util_.isString(userMessage)) { 170 | throw new Error(functionName + ' - second arg not a string') 171 | } 172 | } 173 | 174 | return { 175 | hyphen: hyphen, 176 | userMessage: userMessage 177 | } 178 | } 179 | } // handleError() 180 | 181 | /** 182 | * Assert the passed condition is true. 183 | * 184 | * @param {boolean} assertion value to test 185 | * @param {string} callingfunction calling function name 186 | * @param {string} message error message if it fails 187 | */ 188 | 189 | function assert(assertion, callingfunction, message) { 190 | 191 | var functionName = 'assert()' 192 | var errorMessage 193 | 194 | if (!Util_.isBoolean(assertion)) { 195 | 196 | throw_(functionName, 'first arg should be a boolean') 197 | } 198 | 199 | if (!Util_.isString(callingfunction)) { 200 | 201 | throw_(functionName, 'second arg should be the calling function.') 202 | } 203 | 204 | if (!Util_.isUndefined(message) && !Util_.isString(message)) { 205 | 206 | throw_(functionName, 'third arg should be a string.') 207 | } 208 | 209 | if (!assertion) { 210 | 211 | throw_(callingfunction, message) 212 | } 213 | 214 | } // assert() 215 | 216 | /** 217 | * Assert that the passed value is a number. 218 | * 219 | * @param {number} testNumber value to test 220 | * @param {string} callingfunction calling function name 221 | * @param {string} message error message if it fails 222 | */ 223 | 224 | function assertNumber(testNumber, callingfunction, message) { 225 | 226 | assert(Util_.isNumber(testNumber), callingfunction, message) 227 | 228 | } // assertNumber() 229 | 230 | /** 231 | * Assert that the passed value is a string. 232 | * 233 | * @param {string} testString value to test 234 | * @param {string} callingfunction calling function name 235 | * @param {string} message error message if it fails 236 | */ 237 | 238 | function assertString(testString, callingfunction, message) { 239 | 240 | assert(Util_.isString(testString), callingfunction, message) 241 | 242 | } // assertString() 243 | 244 | /** 245 | * Assert that the passed value is a not null. 246 | * 247 | * @param {string} test value to test 248 | * @param {string} callingfunction calling function name 249 | * @param {string} message error message if it fails 250 | */ 251 | 252 | function assertNotNull(test, callingfunction, message) { 253 | 254 | assert(!Util_.isNull(test), callingfunction, message) 255 | 256 | } // assertNotNull() 257 | 258 | /** 259 | * Assert that the passed value is defined. 260 | * 261 | * @param {string} test value to test 262 | * @param {string} callingfunction calling function name 263 | * @param {string} message error message if it fails 264 | */ 265 | 266 | function assertDefined(test, callingfunction, message) { 267 | 268 | assert(!Util_.isUndefined(test), callingfunction, message) 269 | 270 | } // assertDefined() 271 | 272 | /** 273 | * Assert that the passed value is an object. 274 | * 275 | * @param {string} test value to test 276 | * @param {string} callingfunction calling function name 277 | * @param {string} message error message if it fails 278 | */ 279 | 280 | function assertObject(test, callingfunction, message) { 281 | 282 | assert(Util_.isObject(test), callingfunction, message) 283 | 284 | } // assertObject() 285 | 286 | /** 287 | * Assert that the passed value is a date object. 288 | * 289 | * @param {string} test value to test 290 | * @param {string} callingfunction calling function name 291 | * @param {string} message error message if it fails 292 | */ 293 | 294 | function assertDate(test, callingfunction, message) { 295 | 296 | assert(Util_.isDate(test), callingfunction, message) 297 | 298 | } // assertDate() 299 | 300 | /** 301 | * Assert that the passed value is a boolean. 302 | * 303 | * @param {string} test value to test 304 | * @param {string} callingfunction calling function name 305 | * @param {string} message error message if it fails 306 | */ 307 | 308 | function assertBoolean(test, callingfunction, message) { 309 | 310 | assert(Util_.isBoolean(test), callingfunction, message) 311 | 312 | } // assertBoolean() 313 | 314 | /** 315 | * Assert that two objects are equal 316 | * 317 | * @param {string} test value to test 318 | * @param {string} callingfunction calling function name 319 | * @param {string} message error message if it fails 320 | */ 321 | 322 | function objectsAreEqual(obj, callingfunction, message) { 323 | 324 | assert(Util_.objectsAreEqual(obj), callingfunction, message) 325 | 326 | } // objectsAreEqual() 327 | 328 | // Error functions called at low-level 329 | // ----------------------------------- 330 | 331 | /** 332 | * Throw an error, to be caught and processed by handleError(). 333 | * 334 | * @param {string} callingfunction calling function name 335 | * @param {string} message error message 336 | */ 337 | 338 | function throw_(callingfunction, message) { 339 | 340 | var functionName = 'throw_()' 341 | 342 | var errorMessage 343 | 344 | if (!Util_.isString(callingfunction)) { 345 | 346 | errorMessage = getErrorMessage(functionName + 347 | ' - first arg should be the calling function.') 348 | 349 | throw new TypeError(errorMessage) 350 | } 351 | 352 | if (!Util_.isUndefined(message) && !Util_.isString(message)) { 353 | 354 | errorMessage = getErrorMessage(functionName + 355 | ' - second arg should be a string.') 356 | 357 | throw new TypeError(errorMessage) 358 | } 359 | 360 | errorMessage = getErrorMessage(callingfunction + ' - ' + message) 361 | 362 | throw new Error(errorMessage) 363 | 364 | // Private Functions 365 | // ----------------- 366 | 367 | /** 368 | * If we're throwing the error - usually during developement - display 369 | * the internal error message, otherwise just display 'internal error'. 370 | */ 371 | 372 | function getErrorMessage(message) { 373 | 374 | // TODO - Add another option to display the internal error message or not 375 | 376 | /* 377 | return (handleError === null) ? 'Internal error.' : message 378 | */ 379 | 380 | return message 381 | 382 | } // throw_().getErrorMessage() 383 | 384 | } // throw_() 385 | --------------------------------------------------------------------------------