aboutsummaryrefslogtreecommitdiff
path: root/src/gleam_json_ffi.mjs
diff options
context:
space:
mode:
authormrkutly <mark.sauer.utley@gmail.com>2022-06-04 19:24:57 -0400
committerLouis Pilfold <louis@lpil.uk>2022-06-11 18:39:10 +0100
commitd984c047c771ec6c89ce23b2f29128a2a931b9db (patch)
tree8ee81363eabde356b39ce6c690ce5fc20f066bb4 /src/gleam_json_ffi.mjs
parentb17b4f401b9ec281c4e0aadcaf29d1c467f8ce48 (diff)
downloadgleam_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.mjs138
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