diff options
author | mrkutly <mark.sauer.utley@gmail.com> | 2022-06-04 19:24:57 -0400 |
---|---|---|
committer | Louis Pilfold <louis@lpil.uk> | 2022-06-11 18:39:10 +0100 |
commit | d984c047c771ec6c89ce23b2f29128a2a931b9db (patch) | |
tree | 8ee81363eabde356b39ce6c690ce5fc20f066bb4 /src/gleam_json_ffi.mjs | |
parent | b17b4f401b9ec281c4e0aadcaf29d1c467f8ce48 (diff) | |
download | gleam_json-d984c047c771ec6c89ce23b2f29128a2a931b9db.tar.gz gleam_json-d984c047c771ec6c89ce23b2f29128a2a931b9db.zip |
add runtime-specific error handling
Diffstat (limited to 'src/gleam_json_ffi.mjs')
-rw-r--r-- | src/gleam_json_ffi.mjs | 138 |
1 files changed, 128 insertions, 10 deletions
diff --git a/src/gleam_json_ffi.mjs b/src/gleam_json_ffi.mjs index 9d3daee..0116ab3 100644 --- a/src/gleam_json_ffi.mjs +++ b/src/gleam_json_ffi.mjs @@ -1,6 +1,12 @@ import { Ok, Error } from './gleam.mjs' import { UnexpectedByte, UnexpectedEndOfInput } from './gleam/json.mjs' +const Runtime = { + chromium: 'chromium', + spidermonkey: 'spidermonkey', + jscore: 'jscore', +} + export function json_to_string(json) { return JSON.stringify(json) } @@ -26,29 +32,141 @@ export function decode(string) { const result = JSON.parse(string) return new Ok(result) } catch (err) { - return new Error(getJsonDecodeError(err)) + return new Error(getJsonDecodeError(err, string)) } } -function getJsonDecodeError(stdErr) { - if (isUnexpectedByte(stdErr)) return new toUnexpectedByteError(stdErr) +function getJsonDecodeError(stdErr, json) { if (isUnexpectedEndOfInput(stdErr)) return new UnexpectedEndOfInput() + const unexpectedByteRuntime = getUnexpectedByteRuntime(stdErr) + if (unexpectedByteRuntime) return toUnexpectedByteError(unexpectedByteRuntime, stdErr, json) return new UnexpectedByte('', 0) } +/** + * Matches unexpected end of input messages in: + * - Chromium (edge, chrome, node) + * - Spidermonkey (firefox) + * - JavascriptCore (safari) + * + * Note that Spidermonkey and JavascriptCore will both incorrectly report some + * UnexpectedByte errors as UnexpectedEndOfInput errors. For example: + * + * @example + * // in JavascriptCore + * JSON.parse('{"a"]: "b"}) + * // => JSON Parse error: Expected ':' before value + * + * JSON.parse('{"a"') + * // => JSON Parse error: Expected ':' before value + * + * // in Chromium (correct) + * JSON.parse('{"a"]: "b"}) + * // => Unexpected token ] in JSON at position 4 + * + * JSON.parse('{"a"') + * // => Unexpected end of JSON input + * + * // in Chromium (correct) + * + */ +const unexpectedEndOfInputRegex = /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i + function isUnexpectedEndOfInput(err) { - return err.message.includes('Unexpected end') || err.message.includes('Unexpected EOF') + return unexpectedEndOfInputRegex.test(err.message) } -const unexpectedByteRegex = /Unexpected token (.) in JSON at position (\d+)/ -function isUnexpectedByte(err) { - return unexpectedByteRegex.test(err) +/** + * Matches unexpected byte messages in: + * - Chromium (edge, chrome, node) + * + * Matches the character and its position. + */ +const chromiumUnexpectedByteRegex = /unexpected token (.) in JSON at position (\d+)/ + +/** + * Matches unexpected byte messages in: + * - JavascriptCore (safari) + * + * JavascriptCore only reports what the character is and not its position. + */ +const jsCoreUnexpectedByteRegex = /unexpected identifier "(.)"/i + +/** + * Matches unexpected byte messages in: + * - Spidermonkey (firefox) + * + * Matches the position in a 2d grid only and not the character. + */ +const spidermonkeyUnexpectedByteRegex = /((unexpected character|expected double-quoted property name) at line (\d+) column (\d+))/i + +function getUnexpectedByteRuntime(err) { + if (chromiumUnexpectedByteRegex.test(err.message)) return Runtime.chromium + if (jsCoreUnexpectedByteRegex.test(err.message)) return Runtime.jscore + if (spidermonkeyUnexpectedByteRegex.test(err.message)) return Runtime.spidermonkey + return null +} + +function toUnexpectedByteError(runtime, err, json) { + switch (runtime) { + case Runtime.chromium: + return toChromiumUnexpectedByteError(err) + case Runtime.spidermonkey: + return toSpidermonkeyUnexpectedByteError(err, json) + case Runtime.jscore: + return toJsCoreUnexpectedByteError(err) + } } -function toUnexpectedByteError(err) { - const match = unexpectedByteRegex.exec(err.message) - const byte = "0x" + match[1].charCodeAt(0).toString(16).toUpperCase() +function toChromiumUnexpectedByteError(err) { + const match = chromiumUnexpectedByteRegex.exec(err.message) + const byte = toHex(match[1]) const position = Number(match[2]) return new UnexpectedByte(byte, position) } + +function toSpidermonkeyUnexpectedByteError(err, json) { + const match = spidermonkeyUnexpectedByteRegex.exec(err.message) + const line = Number(match[1]) + const column = Number(match[2]) + const position = getPositionFromMultiline(line, column, json) + const byte = toHex(err.message[position]) + return new UnexpectedByte(byte, position) +} + +function toJsCoreUnexpectedByteError(err) { + const match = jsCoreUnexpectedByteRegex.exec(err.message) + const byte = toHex(match[1]) + return new UnexpectedByte(byte, 0) +} + +function toHex(char) { + return "0x" + char.charCodeAt(0).toString(16).toUpperCase() +} + +/** + * Gets the position of a character in a flattened (i.e. single line) string + * from a line and column number. Note that the position is 0-indexed and + * the line and column numbers are 1-indexed. + * + * @param {number} line + * @param {number} column + * @param {string} string + */ +function getPositionFromMultiline(line, column, string) { + if (line === 1) return column - 1 + + let currentLn = 1 + let position = 0 + string.split('').find((char, idx) => { + if (char === '\n') currentLn += 1 + if (currentLn === line) { + position = idx + column + return true + } + return false + }) + + return position +}
\ No newline at end of file |