From 043ba0be3c15a6a01b19d4fce5b8e5548fba1cd4 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Thu, 17 Oct 2024 13:39:51 +0300 Subject: [PATCH 01/15] got everything building, unfortunately tests are failing --- bsconfig.json | 1 + package.json | 20 +++++++++++--------- src/VJsonParse.res | 4 +--- yarn.lock | 41 +++++++++++++++++++---------------------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/bsconfig.json b/bsconfig.json index 34e1466..db3e512 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,6 +1,7 @@ { "name": "@draftbit/variable-json", "version": "0.8.1", + "uncurried": false, "sources": [ { "dir": "src", diff --git a/package.json b/package.json index b07b936..417c781 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,12 @@ "scripts": { "copy-git-hooks": "if [ -e ./hooks ]; then cp -v ./hooks/* ./.git/hooks; fi", "prepare": "mkdir -p ./.git/hooks && yarn copy-git-hooks && yarn re:build", - "re:clean": "bsb -clean-world", - "re:build": "bsb -make-world", + "re:clean": "rescript clean", + "re:build": "rescript build", "re:cleanbuild": "yarn re:clean && yarn re:build", "re:formatall": "find src __tests__ -name '*.re' | xargs yarn bsrefmt --in-place", "re:formatchanged": "git diff --name-only --diff-filter=d HEAD -- '*.re' | xargs -I{} realpath --relative-to=. $(git rev-parse --show-toplevel)/'{}' | while read f; do yarn bsrefmt --in-place \"$f\"; done", - "re:watch": "bsb -make-world -w", + "re:watch": "rescript build -w", "test": "jest __tests__/*.bs.js", "npm:publish": "npm publish --access public", "npm:publish-patch": "npm version patch && yarn npm:publish && git push --tags", @@ -30,18 +30,20 @@ "author": "Allen Nelson ", "license": "MIT", "devDependencies": { - "@glennsl/bs-json": "^5.0.2", - "bs-platform": "^9.0.2", + "@glennsl/bs-json": "https://github.com/adnelson/bs-json.git#rescript-10", + "rescript": "^11", "jest": "^26.4.1", "lint-staged": "^10.2.11", "prettier": "^2.0.5", - "reason-jest": "^2.2.0", - "relude": "^0.59.0", - "relude-parse": "^0.8.0" + "reason-jest": "^3.0.0", + "relude": "https://github.com/adnelson/relude.git#rescript-11.0", + "relude-parse": "https://github.com/adnelson/relude-parse.git#rescript-11.1", + "bs-bastet": "adnelson/bastet#rescript-11.0" }, "peerDependencies": { "@glennsl/bs-json": "^5.0.2", "bs-platform": "^7 || ^8 || ^9", + "rescript": "^10 || ^11", "relude": "^0.59.0", "relude-parse": "^0.8.0", "rescript-js-collections": "^1.8.2" @@ -58,7 +60,7 @@ ] }, "resolutions": { - "bs-bastet": "adnelson/bastet#no-bisect" + "bs-bastet": "adnelson/bastet#rescript-11.0" }, "dependencies": { "rescript-js-collections": "^1.8.2" diff --git a/src/VJsonParse.res b/src/VJsonParse.res index affcc63..465f49a 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -1,9 +1,7 @@ open VJsonTypes let parseVJsonWithVariable = parseVariableString => { - open // (variableFromString: string => result('v, string)) => { - - ReludeParse.Parser + open ReludeParse.Parser let lexeme: 'a. t<'a> => t< 'a, diff --git a/yarn.lock b/yarn.lock index 51fdf3f..7052b9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -271,10 +271,9 @@ exec-sh "^0.3.2" minimist "^1.2.0" -"@glennsl/bs-json@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@glennsl/bs-json/-/bs-json-5.0.2.tgz#cfb85d94d370ec6dc17849e0ddb1a51eee08cfcc" - integrity sha512-vVlHJNrhmwvhyea14YiV4L5pDLjqw1edE3GzvMxlbPPQZVhzgO3sTWrUxCpQd2gV+CkMfk4FHBYunx9nWtBoDg== +"@glennsl/bs-json@https://github.com/adnelson/bs-json.git#rescript-10": + version "5.0.4" + resolved "https://github.com/adnelson/bs-json.git#d481c48ffbb6c7c4c4e8004d9f0e0d2efa563e11" "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -857,17 +856,12 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -bs-bastet@adnelson/bastet#no-bisect: +bs-bastet@adnelson/bastet#rescript-11.0: version "1.2.5" - resolved "https://codeload.github.com/adnelson/bastet/tar.gz/b40c7d8eeb85f22c507e340f850ff7e3ec161c4c" + resolved "https://codeload.github.com/adnelson/bastet/tar.gz/420f27333fa07bb211a36e6ab95580ce9112eb39" dependencies: bs-stdlib-shims "^0.1.0" -bs-platform@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-9.0.2.tgz#a6eac70eb8924a322556dacaccbfbc9b2a0d3a37" - integrity sha512-Ye9JqJ4Oa7mcjjoOVRYI8Uc2Cf8N7jQLWDcdUplY7996d/YErSR7WitmV7XnSwr4EvdrbwjEsg1NxNjUQv3ChA== - bs-stdlib-shims@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/bs-stdlib-shims/-/bs-stdlib-shims-0.1.0.tgz#f64166392652a07c37b9be462beebb6c0c66a74e" @@ -2958,10 +2952,10 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -reason-jest@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/reason-jest/-/reason-jest-2.2.0.tgz#6c8f1ce01fa730b9a1430a5c6b00f520ee154e8f" - integrity sha512-rsueII7rtAk+woYrC10hqBQdn+FJvDxy8aSWxXwEk3KCHaXFFW0hINrO3har5CRuBNG9l+Yof99Q5agbv6EZ6g== +reason-jest@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/reason-jest/-/reason-jest-3.0.0.tgz#1c5e44f10ff6ba87b3d2d79506717ffd7afe61a2" + integrity sha512-MfaISH3+6VzGWIeRPooH1D7hYZvgeJXTMEz9iaBvt8aElpXUoBxTaJJBXenJoWNydyZo1ckgVBuq9XxYRtGqYA== regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" @@ -2971,15 +2965,13 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" -relude-parse@^0.8.0: +"relude-parse@https://github.com/adnelson/relude-parse.git#rescript-11.1": version "0.8.0" - resolved "https://registry.yarnpkg.com/relude-parse/-/relude-parse-0.8.0.tgz#6aa7e2b1d5bc1b28d4ee2eb0a304c63f73fad25c" - integrity sha512-KsPpD4Wi5Nswe9FpeT5I2/DvD+pClgB6kmVXYqd94WrS6wxsxHUyl0ueSGWTTjbsrubNSph2YtxD1zElIGdBbA== + resolved "https://github.com/adnelson/relude-parse.git#cd0543e350a65ce3d8e3b44a74b17eb2c0114fa9" -relude@^0.59.0: - version "0.59.0" - resolved "https://registry.yarnpkg.com/relude/-/relude-0.59.0.tgz#b396ecd5ec4d8cd974782d4bf4b1af1d3fafb378" - integrity sha512-Rc+Lr2SjsGJAejuF7yHqzk3Gsr0Ha4JpdRTn+17DYxbQjLVNHPcO24JSGBHaADdyHJMuNbCHqr/1O+LWb2eIaQ== +"relude@https://github.com/adnelson/relude.git#rescript-11.0": + version "0.66.1" + resolved "https://github.com/adnelson/relude.git#24522d276e91bc57e36a7690c4d607b5ebad1dae" remove-trailing-separator@^1.0.1: version "1.1.0" @@ -3053,6 +3045,11 @@ rescript-js-collections@^1.8.2: resolved "https://registry.yarnpkg.com/rescript-js-collections/-/rescript-js-collections-1.8.2.tgz#b75a331f3b3046a3c800b6d812c20a3adc0d2fbe" integrity sha512-ngyYsu6yfgvONCmjstx+ImH+VtX2HcxY4KrQEoR488nVoMtJRfIzp8vGcExsh8yuILLE/AHABxeJmppwodcEBg== +rescript@^11: + version "11.1.4" + resolved "https://registry.yarnpkg.com/rescript/-/rescript-11.1.4.tgz#9a42ebc4fc5363707e39cef5b3188160b63bee42" + integrity sha512-0bGU0bocihjSC6MsE3TMjHjY0EUpchyrREquLS8VsZ3ohSMD+VHUEwimEfB3kpBI1vYkw3UFZ3WD8R28guz/Vw== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" From d458c657d7ce11657c9bfeb561c70163521a83ce Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Thu, 17 Oct 2024 13:54:15 +0300 Subject: [PATCH 02/15] tests now passing --- __tests__/VJsonParserTests.res | 4 ++-- src/Operators.res | 17 ++++++++++++++ src/Regexes.res | 9 ++++++++ src/VJsonParse.res | 42 +++++++++++++--------------------- 4 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 src/Operators.res create mode 100644 src/Regexes.res diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index e327abe..ef530a4 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -79,13 +79,13 @@ module ParserTests = { test("with numbers", () => expectOkParse("{{x123}}", Variable("x123"))) test("with underscore", () => expectOkParse("{{_123}}", Variable("_123"))) test("with custom format", () => - expect("{{123}}" |> VJson.parseWithRegex(%re(`/\\d+/`)) |> Belt.Result.getExn)->toEqual( + expect("{{123}}" |> VJson.parseWithRegex(%re(`/\d+/`)) |> Belt.Result.getExn)->toEqual( Variable("123"), ) ) test("with different type", () => expect( - ("{{123}}" |> VJson.parseWithRegex(%re(`/\\d+/`)) |> Belt.Result.getExn) + ("{{123}}" |> VJson.parseWithRegex(%re(`/\d+/`)) |> Belt.Result.getExn) ->VJson.map(int_of_string), )->toEqual(Variable(123)) ) diff --git a/src/Operators.res b/src/Operators.res new file mode 100644 index 0000000..850f952 --- /dev/null +++ b/src/Operators.res @@ -0,0 +1,17 @@ +// Make alphanumeric equivalents of the operators in Bastet which are really hard to read in rescript syntax +open ReludeParse.Parser + +// Given two parsers, try the first one, if it fails try the second one +let or_ = \"<|>" + +let maybe = \"" + +// Given two parsers, run both, but only return the left value +let left = \"<*" + +// Given two parsers, run both, but only return the right value +let right = \"*>" + +// Given a parser that returns a value, and a parser that takes a value as an argument, +// run the first parser and pass its result to the second +let flatMap = \">>=" diff --git a/src/Regexes.res b/src/Regexes.res new file mode 100644 index 0000000..1521b85 --- /dev/null +++ b/src/Regexes.res @@ -0,0 +1,9 @@ +// These mess up the emacs syntax highlighter so I put them in their own module for clarity + +// Note: regex taken from https://rgxdb.com/r/1RSPF8MG, but the '$' +// at the end has been omitted because it causes parse errors +let floatRegex: Js.Re.t = %re(`/^([-+]?\d*\.?\d+)(?:[eE]([-+]?\d+))?/`) + +let escapedQuoteRegex = %re(`/\\\"/gm`) +let nonEscapedQuoteRegex = %re(`/((?:^|[^\\])(?:\\{2})*)"/gm`) +let inQuotesRegex = %re(`/"(?:[^"\\]|\\.)*"/`) diff --git a/src/VJsonParse.res b/src/VJsonParse.res index 465f49a..6176cdc 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -2,30 +2,25 @@ open VJsonTypes let parseVJsonWithVariable = parseVariableString => { open ReludeParse.Parser + open Operators + open Regexes let lexeme: 'a. t<'a> => t< 'a, > = // Run a parser and transparently consume all trailing whitespace. - p => \"<*"(p, ws) + p => left(p, ws) // Parse the "null" keyword" let parseNull: t> = str("null") |> map(_ => Null) |> lexeme - // Note: regex taken from https://rgxdb.com/r/1RSPF8MG, but the '$' - // at the end has been omitted because it causes parse errors - let floatRegex: Js.Re.t = %re(`/^([-+]?\\d*\\.?\\d+)(?:[eE]([-+]?\\d+))?/`) - // Parse a number (float or int) let parseNumber: t = - \""(regex(floatRegex), "Not a valid number") |> map(Js.Float.fromString) |> lexeme + maybe(regex(floatRegex), "Not a valid number") |> map(Js.Float.fromString) |> lexeme // Parse a boolean. let parseBool: t = - \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme + or_(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme - let escapedQuoteRegex = %re(`/\\\\"/gm`) - let nonEscapedQuoteRegex = %re(`/((?:^|[^\\\\])(?:\\\\{2})*)"/gm`) - let inQuotesRegex = %re(`/"(?:[^"\\\\]|\\\\.)*"/`) // Parse a string. Allows for escaped quotes. // NOTE: not to be confused with `parse`, which takes a raw string and parses // a whole AST -- this parses string literal syntax. @@ -34,16 +29,16 @@ let parseVJsonWithVariable = parseVariableString => { |> map(match => { match ->Js.String2.replaceByRe(nonEscapedQuoteRegex, `$1`) // First group of the regex is characters before the quote that should be kept - ->Js.String2.replaceByRe(escapedQuoteRegex, `"`) + ->Js.String2.replaceByRe(escapedQuoteRegex, "\"") }) |> lexeme // Parse a variable wrapped in a pair of doubled curly braces `{{ }}`. The // string of text between the curly braces is parsed by `parseVariable`. - let parseVariable_ = \">>="( - \"*>"( - \"*>"(str("{{"), ws), - manyUntil(str("}}"), \"<|>"(str("\\}}") |> map(_ => "}}"), anyChar)), + let parseVariable_ = flatMap( + right( + right(str("{{"), ws), + manyUntil(str("}}"), or_(str("\\}}") |> map(_ => "}}"), anyChar)), ) |> map(l => l->Belt.List.toArray->Js.Array2.joinWith("")->Js.String.trim), rawVariableString => switch parseVariableString(rawVariableString) { @@ -62,7 +57,7 @@ let parseVJsonWithVariable = parseVariableString => { and parseObject: Lazy.t>>> = lazy { let parseKeyValuePair: t<(string, vjson<'v>)> = tuple2( - \"<*"(parseString, str(":")) |> lexeme, + left(parseString, str(":")) |> lexeme, parseVjsonLazy->Lazy.force, ) betweenCurlies(parseKeyValuePair |> sepByOptEnd(str(",") |> lexeme)) @@ -71,16 +66,11 @@ let parseVJsonWithVariable = parseVariableString => { } and parseVjsonLazy: Lazy.t>> = lazy ( - \"<|>"( - \"<|>"( - \"<|>"( - \"<|>"(parseNull, parseString |> map(s => String(s))), - parseNumber |> map(n => Number(n)), - ), - parseBool |> map(b => Bool(b)), - ), - parseVariable_ |> map(v => Variable(v)), - ) + parseNull + ->or_(parseNumber |> map(n => Number(n))) + ->or_(parseString |> map(s => String(s))) + ->or_(parseBool |> map(b => Bool(b))) + ->or_(parseVariable_ |> map(v => Variable(v))) |> orElseLazy(~fallback=() => parseArray->Lazy.force |> map(arr => Array(arr))) |> orElseLazy(~fallback=() => parseObject->Lazy.force |> map(d => Object(d->JsMap.fromDict))) ) From 9830240acb9aeb7bcbd3bd7f65da46f7e7ae852d Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Thu, 17 Oct 2024 13:55:24 +0300 Subject: [PATCH 03/15] v2.0.0 --- bsconfig.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bsconfig.json b/bsconfig.json index db3e512..fe9df58 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,6 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "0.8.1", + "version": "2.0.0", "uncurried": false, "sources": [ { diff --git a/package.json b/package.json index 417c781..fe806eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "1.1.1", + "version": "2.0.0", "declaration": true, "main": "src/VJson.bs", "types": "src/index.d.ts", From 231054b00c6a433014b51f72eb324b0a4c6d46a6 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sat, 19 Oct 2024 19:39:05 +0300 Subject: [PATCH 04/15] fix type errors, tests still failing tho --- __tests__/VJsonParserTests.res | 42 +++++------- __tests__/VJsonTests.res | 78 +++++++++++----------- bsconfig.json | 9 +-- package.json | 10 +-- src/Operators.res | 17 ----- src/Regexes.res | 13 ++-- src/StringParser.res | 96 +++++++++++++++++++++++++++ src/VJson.res | 104 +++++++++++------------------ src/VJson.resi | 115 --------------------------------- src/VJsonBuilder.res | 11 ++-- src/VJsonParse.res | 103 ++++++++++------------------- src/VJsonParse.resi | 26 -------- src/VJsonTypes.res | 10 +-- src/VJsonTypes.resi | 10 --- src/VJsonUtil.res | 8 +++ 15 files changed, 244 insertions(+), 408 deletions(-) delete mode 100644 src/Operators.res create mode 100644 src/StringParser.res delete mode 100644 src/VJson.resi delete mode 100644 src/VJsonParse.resi delete mode 100644 src/VJsonTypes.resi create mode 100644 src/VJsonUtil.res diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index ef530a4..751f740 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -3,13 +3,14 @@ open Expect open VJsonTypes let expectParse = (input, handler) => - switch (input |> VJson.parseWith(VJson.defaultParseVariable), handler) { + switch (input->VJson.parse, handler) { | (Ok(vj), Ok(checks)) => vj->checks | (Error(err), Error(checks)) => err->checks - | (Error(ReludeParse.Parser.ParseError.ParseError(message)), Ok(_)) => - failwith("Expected an Ok result, but got error: " ++ message) + | (Error(message), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ message) | (Ok(vj), Error(_)) => - failwith("Expected an Error result, but successfully parsed: " ++ Json.stringify(Obj.magic(vj))) + failwith( + "Expected an Error result, but successfully parsed: " ++ Js.Json.stringify(Obj.magic(vj)), + ) } // Checks that parsing is successful and the output matches the expected. @@ -27,7 +28,7 @@ module ParserTests = { test("single value", () => expectOkParse("null", Null)) test("array", () => expectOkParse("[null, null]", Array([Null, Null]))) test("bad array (missing closing brackets)", () => - expectParseFail("[ null, null", (ParseError(m)) => expect(m)->toEqual(stringContaining("]"))) + expectParseFail("[ null, null", m => expect(m)->toEqual(stringContaining("]"))) ) test("bad array (no comma)", () => expectSomeParseFail("[ 1 true ]")) }) @@ -78,17 +79,6 @@ module ParserTests = { test("single letter", () => expectOkParse("{{x}}", Variable("x"))) test("with numbers", () => expectOkParse("{{x123}}", Variable("x123"))) test("with underscore", () => expectOkParse("{{_123}}", Variable("_123"))) - test("with custom format", () => - expect("{{123}}" |> VJson.parseWithRegex(%re(`/\d+/`)) |> Belt.Result.getExn)->toEqual( - Variable("123"), - ) - ) - test("with different type", () => - expect( - ("{{123}}" |> VJson.parseWithRegex(%re(`/\d+/`)) |> Belt.Result.getExn) - ->VJson.map(int_of_string), - )->toEqual(Variable(123)) - ) }) describe("array", () => { @@ -112,19 +102,19 @@ module ParserTests = { }) describe("object", () => { - test("empty", () => expectOkParse("{}", Object(JsMap.empty()))) - test("empty with whitespace", () => expectOkParse("{ }", Object(JsMap.empty()))) + test("empty", () => expectOkParse("{}", Object(Js.Dict.empty()))) + test("empty with whitespace", () => expectOkParse("{ }", Object(Js.Dict.empty()))) test("single key", () => - expectOkParse("{\"x\": 1}", Object([("x", Number(1.0))]->JsMap.fromArray)) + expectOkParse("{\"x\": 1}", Object([("x", Number(1.0))]->Js.Dict.fromArray)) ) test("multiple keys", () => expectOkParse( "{\"x\": 1, \"YYY\": \"hey there!\"}", - Object([("x", Number(1.0)), ("YYY", String("hey there!"))]->JsMap.fromArray), + Object([("x", Number(1.0)), ("YYY", String("hey there!"))]->Js.Dict.fromArray), ) ) test("extra whitespace", () => - expectOkParse(" { \"x\" : 1 } ", Object([("x", Number(1.0))]->JsMap.fromArray)) + expectOkParse(" { \"x\" : 1 } ", Object([("x", Number(1.0))]->Js.Dict.fromArray)) ) test("nested", () => expectOkParse( @@ -138,10 +128,10 @@ module ParserTests = { [ ("z", Variable("myZVariable")), ("blib", Array([Number(123.0), String("hey there!")])), - ]->JsMap.fromArray, + ]->Js.Dict.fromArray, ), ), - ]->JsMap.fromArray, + ]->Js.Dict.fromArray, ), ) ) @@ -162,16 +152,16 @@ module ParserTests = { ( "variables", Object( - [("searchTerm", String("bag")), ("sortBy", Variable("sortBy"))]->JsMap.fromArray, + [("searchTerm", String("bag")), ("sortBy", Variable("sortBy"))]->Js.Dict.fromArray, ), ), ("query", String(longValue)), - ]->JsMap.fromArray, + ]->Js.Dict.fromArray, ), ) }) test("extra line", () => - expectOkParse("{\n\"id\": 5}", Object([("id", Number(5.0))]->JsMap.fromArray)) + expectOkParse("{\n\"id\": 5}", Object([("id", Number(5.0))]->Js.Dict.fromArray)) ) }) } diff --git a/__tests__/VJsonTests.res b/__tests__/VJsonTests.res index 38b4f24..cc6e010 100644 --- a/__tests__/VJsonTests.res +++ b/__tests__/VJsonTests.res @@ -6,21 +6,16 @@ let exampleJson: Js.Json.t = %raw(`{x: 1, y: 2, z: [1, 2, "hello"], q: { x: [fal let example = { open VJson - parseWith( - defaultParseVariable, - "{ + parse("{ \"id\": {{id}}, \"color\": {{color}}, \"size\": {{size}}, - }", - ) + }") }->Belt.Result.getExn let moreComplexExample = { open VJson - parseWith( - defaultParseVariable, - "{ + parse("{ \"id\": {{id}}, \"color\": {{color}}, \"size\": {{size}}, @@ -29,8 +24,7 @@ let moreComplexExample = { \"q\": 3, \"z\": {{z}} } - }", - ) + }") }->Belt.Result.getExn module FindVariablesTests = { @@ -46,13 +40,13 @@ module MapTests = { test("complex with variables", () => { open VJson.Builder let vj = object_([ - ("x", variable(1234)), - ("z", vjsonArray([float(1.0), number(2.0), variable(4321)])), + ("x", variable("1234")), + ("z", vjsonArray([float(1.0), number(2.0), variable("4321")])), ]) - expect(vj->VJson.map(string_of_int))->toEqual( + expect(vj->VJson.map(s => s ++ "!!"))->toEqual( object_([ - ("x", variable("1234")), - ("z", vjsonArray([float(1.0), number(2.0), variable("4321")])), + ("x", variable("1234!!")), + ("z", vjsonArray([float(1.0), number(2.0), variable("4321!!")])), ]), ) }) @@ -60,8 +54,8 @@ module MapTests = { } module SerializeTests = { - let expectSerialize = (vj: vjson, str) => { - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) + let expectSerialize = (vj: vjson, str) => { + let serialized: string = VJson.serializeWrapCurlyBraces(s => s, vj) expect(serialized)->toEqual(str) } test("simple values", () => { @@ -85,8 +79,8 @@ module SerializeTests = { ]) } expect(vj->VJson.serialize(s => s))->toMatchSnapshot() - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) - expect(serialized |> VJson.parseDefaultExn)->toEqual(vj) + let serialized: string = VJson.serializeWrapCurlyBraces(s => s, vj) + expect(serialized->VJson.parse->Belt.Result.getExn)->toEqual(vj) }) test("serializing VJson without variables produces valid json", () => { @@ -99,8 +93,8 @@ module SerializeTests = { ("q", object_([("x", Array([Bool(false)]))])), ]) } - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) - expect(serialized |> Js.Json.parseExn |> Obj.magic)->toEqual({ + let serialized: string = VJson.serializeWrapCurlyBraces(s => s, vj) + expect(Obj.magic(Js.Json.parseExn(serialized)))->toEqual({ "x": 1.0, "y": 2, "z": [1.0, 2.0], @@ -121,10 +115,10 @@ module FromJsonTests = { [ ("x", number(1.0)), ("y", number(2.0)), - ("z", [number(1.0), number(2.0), string("hello")] |> array(id)), - ("q", object_([("x", [false, true] |> array(bool)), ("y", null)])), + ("z", array(id, [number(1.0), number(2.0), string("hello")])), + ("q", object_([("x", array(bool, [false, true])), ("y", null)])), ] - }->JsMap.fromArray, + }->Js.Dict.fromArray, ), ) ) @@ -135,24 +129,24 @@ module ToJsonTests = { describe("toJson", () => { let variableToJson: string => Js.Json.t = v => switch v { - | "id" => Json.Encode.int(123) - | "color" => Json.Encode.string("pink") - | _ => Json.Encode.null + | "id" => Js.Json.number(123.0) + | "color" => Js.Json.string("pink") + | _ => Js.Json.null } - expect(example |> toJson(variableToJson))->toEqual(%raw(`{id: 123, size: null, color: "pink"}`)) + expect(toJson(variableToJson)(example))->toEqual(%raw(`{id: 123, size: null, color: "pink"}`)) let variableToJson: string => Js.Json.t = v => switch v { - | "id" => Json.Encode.int(123) - | "color" => Json.Encode.string("pink") - | "x" => Json.Encode.int(-5) - | "y" => Json.Encode.bool(false) - | "z" => Json.Encode.string("yo") - | _ => Json.Encode.null + | "id" => Js.Json.number(123.0) + | "color" => Js.Json.string("pink") + | "x" => Js.Json.number(-5.0) + | "y" => Js.Json.boolean(false) + | "z" => Js.Json.string("yo") + | _ => Js.Json.null } - expect(moreComplexExample |> toJson(variableToJson))->toEqual( + expect(toJson(variableToJson)(moreComplexExample))->toEqual( %raw(`{id: 123, size: null, color: "pink", array: [-5, false], obj: {q: 3, z: "yo"}}`), ) }) @@ -160,22 +154,22 @@ module ToJsonTests = { describe("toJsonOptional", () => { let variableToJson: string => option = v => switch v { - | "id" => Json.Encode.int(123)->Some - | "color" => Json.Encode.string("pink")->Some + | "id" => Js.Json.number(123.0)->Some + | "color" => Js.Json.string("pink")->Some | _ => None } - expect(example |> toJsonOptional(variableToJson))->toEqual(%raw(`{id: 123, color: "pink"}`)) + expect(toJsonOptional(variableToJson, example))->toEqual(%raw(`{id: 123, color: "pink"}`)) let variableToJson = v => switch v { - | "id" => Json.Encode.int(123)->Some - | "color" => Json.Encode.string("blue")->Some - | "y" => Json.Encode.bool(false)->Some + | "id" => Js.Json.number(123.0)->Some + | "color" => Js.Json.string("blue")->Some + | "y" => Js.Json.boolean(false)->Some | _ => None } - expect(moreComplexExample |> toJsonOptional(variableToJson))->toEqual( + expect(toJsonOptional(variableToJson, moreComplexExample))->toEqual( %raw(`{id: 123, array: [null, false], color: "blue", obj: { q: 3}}`), ) }) diff --git a/bsconfig.json b/bsconfig.json index fe9df58..8a0a068 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,7 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "2.0.0", - "uncurried": false, + "version": "3.0.0", "sources": [ { "dir": "src", @@ -18,11 +17,7 @@ "in-source": true }, "suffix": ".bs.js", - "bs-dependencies": [ - "@glennsl/bs-json", - "rescript-js-collections", - "relude-parse" - ], + "bs-dependencies": [], "bs-dev-dependencies": ["reason-jest"], "warnings": { "number": "-44-23", diff --git a/package.json b/package.json index fe806eb..5aa0180 100644 --- a/package.json +++ b/package.json @@ -30,22 +30,14 @@ "author": "Allen Nelson ", "license": "MIT", "devDependencies": { - "@glennsl/bs-json": "https://github.com/adnelson/bs-json.git#rescript-10", "rescript": "^11", "jest": "^26.4.1", "lint-staged": "^10.2.11", "prettier": "^2.0.5", - "reason-jest": "^3.0.0", - "relude": "https://github.com/adnelson/relude.git#rescript-11.0", - "relude-parse": "https://github.com/adnelson/relude-parse.git#rescript-11.1", - "bs-bastet": "adnelson/bastet#rescript-11.0" + "reason-jest": "^3.0.0" }, "peerDependencies": { - "@glennsl/bs-json": "^5.0.2", - "bs-platform": "^7 || ^8 || ^9", "rescript": "^10 || ^11", - "relude": "^0.59.0", - "relude-parse": "^0.8.0", "rescript-js-collections": "^1.8.2" }, "lint-staged": { diff --git a/src/Operators.res b/src/Operators.res deleted file mode 100644 index 850f952..0000000 --- a/src/Operators.res +++ /dev/null @@ -1,17 +0,0 @@ -// Make alphanumeric equivalents of the operators in Bastet which are really hard to read in rescript syntax -open ReludeParse.Parser - -// Given two parsers, try the first one, if it fails try the second one -let or_ = \"<|>" - -let maybe = \"" - -// Given two parsers, run both, but only return the left value -let left = \"<*" - -// Given two parsers, run both, but only return the right value -let right = \"*>" - -// Given a parser that returns a value, and a parser that takes a value as an argument, -// run the first parser and pass its result to the second -let flatMap = \">>=" diff --git a/src/Regexes.res b/src/Regexes.res index 1521b85..74544c1 100644 --- a/src/Regexes.res +++ b/src/Regexes.res @@ -1,9 +1,4 @@ -// These mess up the emacs syntax highlighter so I put them in their own module for clarity - -// Note: regex taken from https://rgxdb.com/r/1RSPF8MG, but the '$' -// at the end has been omitted because it causes parse errors -let floatRegex: Js.Re.t = %re(`/^([-+]?\d*\.?\d+)(?:[eE]([-+]?\d+))?/`) - -let escapedQuoteRegex = %re(`/\\\"/gm`) -let nonEscapedQuoteRegex = %re(`/((?:^|[^\\])(?:\\{2})*)"/gm`) -let inQuotesRegex = %re(`/"(?:[^"\\]|\\.)*"/`) +let number = %re(`/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/`) +let string = %re(`/^"((?:[^"\\\\]|\\\\.)*)"/`) +let escapedQuote = %re(`/\\\\"/g`) +let variable = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) diff --git a/src/StringParser.res b/src/StringParser.res new file mode 100644 index 0000000..95f4e0f --- /dev/null +++ b/src/StringParser.res @@ -0,0 +1,96 @@ +type parser<'res> = string => result<(string, 'res), string> + +let map: (parser<'a>, 'a => 'b) => parser<'b> = (parser, f) => str => + str->parser->Belt.Result.map(((str, res)) => (str, f(res))) + +let opt: parser<'a> => parser> = parser => str => + switch str->parser { + | Ok((str2, res)) => Ok((str2, Some(res))) + | _ => Ok(str, None) + } + +let or_: (parser<'a>, parser<'a>) => parser<'a> = (parser1, parser2) => str => + switch str->parser1 { + | Ok(res) => Ok(res) + | Error(_) => str->parser2 + } + +let and_: (parser<'a>, parser<'b>) => parser<('a, 'b)> = (parser1, parser2) => str1 => + switch str1->parser1 { + | Ok((str2, res1)) => str2->parser2->Belt.Result.map(((str3, res2)) => (str3, (res1, res2))) + | Error(e) => Error(e) + } + +let left: (parser<'a>, parser<_>) => parser<'a> = (parser1, parser2) => + parser1->and_(parser2)->map(fst) +let right: (parser<_>, parser<'a>) => parser<'a> = (parser1, parser2) => + parser1->and_(parser2)->map(snd) +let between: 'a 'b 'c. (parser<'a>, parser<'b>, parser<'c>) => parser<'b> = (l, parser, r) => + l->and_(parser)->and_(r)->map((((_, res), _)) => res) + +let many: 'a. parser<'a> => parser> = parser => startStr => { + let results = [] + let str = ref(startStr) + let keepGoing = ref(true) + while keepGoing.contents { + switch str.contents->parser { + | Ok((next, res)) => { + results->Js.Array2.push(res)->ignore + str := next + } + | Error(_) => keepGoing := false + } + } + Ok((str.contents, results)) +} + +let many1: 'a. parser<'a> => parser> = parser => + parser->and_(many(parser))->map(((first, rest)) => [first]->Js.Array2.concat(rest)) + +let manySep: 'a. (parser<'a>, parser<_>) => parser> = (parser, sepParser) => startStr => { + let results = [] + let str = ref(startStr) + let keepGoing = ref(true) + while keepGoing.contents { + switch str.contents->parser { + | Ok((next, res)) => { + results->Js.Array2.push(res)->ignore + switch next->sepParser { + | Ok((next2, _)) => str := next2 + | _ => { + str := next + keepGoing := false + } + } + } + | Error(_) => keepGoing := false + } + } + Ok((str.contents, results)) +} + +let sliceN = (str, from) => str->Js.String2.sliceToEnd(~from) + +let literal: string => parser = expected => str => + str->Js.String2.startsWith(expected) + ? Ok((str->sliceN(expected->Js.String.length), expected)) + : Error(`expected "${expected}"`) + +let regex: (Js.Re.t, ~index: int=?, string) => parser = ( + regex, + // Which index of the returned regex capture array to return + ~index=0, + // Used to generate error message if the parser fails + description, +) => str => + switch str->Js.String2.match_(regex) { + | Some(arr) => + Ok(str->sliceN(arr[0]->Belt.Option.getExn->Js.String.length), arr[index]->Belt.Option.getExn) + | _ => Error(`expected ${description}`) + } + +let whitespace: parser = regex(%re(`/^\s*/`), "whitespace") + +let lexeme: parser<'a> => parser<'a> = parser => left(parser, whitespace) + +let eof: parser = str => str === "" ? Ok("", ()) : Error("expected end of file") diff --git a/src/VJson.res b/src/VJson.res index 0cdcabc..2fa43c8 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -2,8 +2,9 @@ module Types = VJsonTypes include Types module Builder = VJsonBuilder +open VJsonUtil -// Traverse a JSON structure with a function +// Map over a JSON structure with a function operating on variables let rec map = (vjson, f) => switch vjson { | Variable(v) => Variable(f(v)) @@ -11,10 +12,11 @@ let rec map = (vjson, f) => | Null => Null | String(s) => String(s) | Number(n) => Number(n) - | Array(arr) => arr |> Builder.array(vj => vj->map(f)) - | Object(obj) => obj |> Builder.jsMap(k => k, vj => vj->map(f)) + | Array(arr) => Builder.array(vj => vj->map(f), arr) + | Object(obj) => Builder.dict(vj => vj->map(f), obj) } +// `map` in significant-data-last order let mapL = (f, vjson) => map(vjson, f) // Traverse a JSON structure with a function @@ -27,56 +29,49 @@ let rec reduce = (vjson, result, f) => { | Number(_) | Variable(_) => newResult | Array(arr) => arr->Belt.Array.reduce(newResult, (r, j) => j->reduce(r, f)) - | Object(obj) => obj->JsMap.valuesArray->Belt.Array.reduce(newResult, (r, j) => j->reduce(r, f)) + | Object(obj) => obj->Js.Dict.values->Belt.Array.reduce(newResult, (r, j) => j->reduce(r, f)) } } +// `reduce` in significant-data-last order let reduceL = (start, f, vjson) => reduce(vjson, start, f) // Translate to JSON, given a conversion function which must return a JSON value for each variable. let rec toJson = variableToJson => { - open Json.Encode - x => switch x { - | Null => null - | Bool(b) => b |> bool - | Number(n) => n |> float - | String(s) => s |> string - | Array(arr) => arr |> array(toJson(variableToJson)) - | Object(d) => d |> JsMap.toJson(~k=s => s, ~v=toJson(variableToJson)) - | Variable(var) => var |> variableToJson + | Null => Js.Json.null + | Bool(b) => Js.Json.boolean(b) + | Number(n) => Js.Json.number(n) + | String(s) => Js.Json.string(s) + | Array(arr) => arr->Js.Array2.map(toJson(variableToJson))->Js.Json.array + | Object(d) => d->mapDict(toJson(variableToJson))->Js.Json.object_ + | Variable(var) => variableToJson(var) } } // Translate to JSON, given a conversion function which will optionally return a JSON value for each variable. -let rec toJsonOptional: ( - 'v => option, - vjson<'v>, -) => option = variableToJson => { - open Json.Encode - - x => - switch x { - | Null => null->Some - | Bool(b) => b->bool->Some - | Number(n) => n->float->Some - | String(s) => s->string->Some - | Array(arr) => - arr - ->Js.Array2.map(vj => - vj->toJsonOptional(variableToJson, _)->Belt.Option.getWithDefault(Js.Json.null) - ) - ->Js.Json.array - ->Some - | Object(d) => - d->JsMap.keepMap(toJsonOptional(variableToJson))->JsMap.toDict->Js.Json.object_->Some - | Variable(var) => var->variableToJson - } +let rec toJsonOptional: (string => option, vjson) => option = ( + variableToJson, + x, +) => { + switch x { + | Null => Js.Json.null->Some + | Bool(b) => b->Js.Json.boolean->Some + | Number(n) => n->Js.Json.number->Some + | String(s) => s->Js.Json.string->Some + | Array(arr) => + arr + ->Js.Array2.map(vj => + vj->(toJsonOptional(variableToJson, _))->Belt.Option.getWithDefault(Js.Json.null) + ) + ->Js.Json.array + ->Some + | Object(d) => d->keepMapDict(vj => toJsonOptional(variableToJson, vj))->Js.Json.object_->Some + | Variable(var) => var->variableToJson + } } -let quote: string => string = s => s->Js.Json.string->Js.Json.stringify - // Convert to a string of valid vjson syntax, using whatever method to serialize a variable. // It's up to the user to guarantee that the variable name serializer produces valid output, // so be careful. @@ -93,8 +88,8 @@ let rec serialize = (vj, variableToString) => | Object(o) => "{" ++ (o - ->JsMap.toArray - ->Belt.Array.map(((k, v)) => `${k->quote}: ${v->serialize(variableToString)}`) + ->Js.Dict.entries + ->Js.Array2.map(((k, v)) => `${k->quote}: ${v->serialize(variableToString)}`) ->Js.Array2.joinWith(", ") ++ "}") } @@ -113,30 +108,7 @@ let findVariables = root => let defaultVariableRegex = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) -let parseWith = (parseVariable, input) => { - open ReludeParse.Parser - // Ensure that we have consumed the whole string - \"<*"(\"*>"(ws, VJsonParse.parseVJsonWithVariable(parseVariable)), eof) |> runParser(input) +let parse = s => { + let p = VJsonParse.parseTerm->StringParser.left(StringParser.eof) + s->p->Belt.Result.map(snd) } - -let parseWithExn = (parseVariable, input) => - switch parseWith(parseVariable, input) { - | Ok(vjson) => vjson - | Error(ReludeParse.Parser.ParseError.ParseError(message)) => failwith(message) - } - -// Turn a regex into a string parser using relude-parse -let parseFromRegex: (Js.Re.t, string) => result = (reg, myString) => - switch { - open ReludeParse.Parser - \"<*"(regex(reg), eof) |> runParser(myString) - } { - | Ok(variable) => Ok(variable) - | Error(ParseError(m)) => Error(m) - } - -let defaultParseVariable = parseFromRegex(defaultVariableRegex) -let parseAnyStringVariable = s => Ok(s) -let parseDefault = parseWith(defaultParseVariable) -let parseDefaultExn = parseWithExn(defaultParseVariable) -let parseWithRegex = regex => parseWith(parseFromRegex(regex)) diff --git a/src/VJson.resi b/src/VJson.resi deleted file mode 100644 index a46d41d..0000000 --- a/src/VJson.resi +++ /dev/null @@ -1,115 +0,0 @@ -// Some useful functions that operate on VJson -type vjson<'v> = VJsonTypes.vjson<'v> - -// Abbreviation -type t<'v> = vjson<'v> - -// Alter every variable in a VJson tree -let map: (vjson<'v1>, 'v1 => 'v2) => vjson<'v2> - -// `map` in significant-data-last order -let mapL: ('v1 => 'v2, vjson<'v1>) => vjson<'v2> - -// Traverse a JSON structure with a function -let reduce: (vjson<'v>, 'a, ('a, vjson<'v>) => 'a) => 'a - -// `reduce` in significant-data-last order -let reduceL: ('a, ('a, vjson<'v>) => 'a, vjson<'v>) => 'a - -// Translate to JSON, given a conversion function which must return a JSON value for each variable. -let toJson: ('v => Js.Json.t, vjson<'v>) => Js.Json.t - -// Translate to JSON, given a conversion function which will optionally return a JSON value for each variable. -let toJsonOptional: ('v => option, vjson<'v>) => option - -// Convert to a string of valid vjson syntax, using whatever method to serialize a variable. -// It's up to the user to guarantee that the variable name serializer produces valid output, -// so be careful. -let serialize: (vjson<'v>, 'v => string) => string - -// Serializes into a pure, JSON-like syntax, wrapping variables in -// pairs of curly braces. The serialization of the variable itself -// is user defined. In many cases the identify function `s => s` works fine. -// If you want to create a string without wrapping variables at all, you -// can use `serialize.` As the inverse of parsing, take care that -// this function never includes `}}` (TODO: support escape sequences) -let serializeWrapCurlyBraces: ('v => string, vjson<'v>) => string - -// Traverse the tree, returning a set of all of the variables. -let findVariables: vjson<'v> => array<'v> - -// The regex used to make the default variable parser. -let defaultVariableRegex: Js.Re.t - -// This is a parser for a basic alphanumeric identifier variable, such as -// `My_variable123`, using `defaultVariableRegex` under the hood. -// This is used in the definition of `parseDefault`. -let defaultParseVariable: string => result - -// Parses an arbitrary string as a variable, so anything between `{{` and `}}` -// will be captured by this function, even an empty string. -let parseAnyStringVariable: string => result - -// Parse a VJson tree with a custom variable parser. -let parseWith: ( - string => result<'v, string>, - string, -) => result, ReludeParse.Parser.ParseError.t> - -// Exception-throwing version of `parseWith` -let parseWithExn: (string => result<'v, string>, string) => vjson<'v> - -// Parse a VJson tree using the given regex to parse variables. -let parseWithRegex: (Js.Re.t, string) => result, ReludeParse.Parser.ParseError.t> - -// Parse a VJson tree with the default variable parser. -let parseDefault: string => result, ReludeParse.Parser.ParseError.t> - -// Exception-throwing version of `parseDefault` -let parseDefaultExn: string => vjson - -module Types: { - type rec vjson<'v> = - | Null - | Bool(bool) - | Number(float) - | String(string) - | Variable('v) - | Array(array>) - | Object(JsMap.t>) - - type t<'v> = vjson<'v> -} - -module Builder: { - let null: vjson<'v> - let bool: bool => vjson<'v> - let number: float => vjson<'v> - let float: float => vjson<'v> - let int: int => vjson<'v> - let string: string => vjson<'v> - - // Create a VJson Variable from any variable type. - let variable: 'v => vjson<'v> - - // Returns its input. Useful if some of your data is already in VJson format. - let id: vjson<'v> => vjson<'v> - - // VJson is a superset of JSON, so any JSON can be converted to VJson. - let json: Js.Json.t => vjson<'v> - - // You can use this to build an array with a conversion function - let array: ('a => vjson<'v>, array<'a>) => vjson<'v> - - // If you already have an array of vjsonObjects, use this to combine them into one - let vjsonArray: array> => vjson<'v> - - // Create a VJson object from a key/value array - let object_: array<(string, vjson<'v>)> => vjson<'v> - - // Use this to convert a dictionary of objects into a VJson object. - let dict: ('a => vjson<'v>, Js.Dict.t<'a>) => vjson<'v> - - // Use this to convert a Map of objects into a VJson object. - let jsMap: ('k => string, 'a => vjson<'v>, JsMap.t<'k, 'a>) => vjson<'v> -} diff --git a/src/VJsonBuilder.res b/src/VJsonBuilder.res index 35a6fc3..f75498b 100644 --- a/src/VJsonBuilder.res +++ b/src/VJsonBuilder.res @@ -1,4 +1,5 @@ include VJsonTypes +open VJsonUtil let null = Null let bool = b => Bool(b) @@ -13,13 +14,9 @@ let array = (toVJson, arr) => Array(arr->Belt.Array.map(toVJson)) let vjsonArray = arr => Array(arr) -let object_ = keyVals => Object(keyVals->JsMap.fromArray) +let object_ = keyVals => Object(keyVals->Js.Dict.fromArray) -let dict = (toVJson, dict) => Object(dict->JsMap.fromDict->JsMap.map(toVJson)) - -let jsMap = (keyToString, vToVJson, m) => Object( - m->JsMap.mapEntries((k, v) => (k->keyToString, v->vToVJson)), -) +let dict = (toVJson, dict) => Object(dict->mapDict(toVJson)) let rec json = j => switch j->Js.Json.classify { @@ -29,5 +26,5 @@ let rec json = j => | JSONString(s) => String(s) | JSONNumber(n) => Number(n) | JSONArray(arr) => Array(arr->Belt.Array.map(json)) - | JSONObject(obj) => Object(obj->JsMap.fromDict->JsMap.map(json)) + | JSONObject(obj) => Object(obj->mapDict(json)) } diff --git a/src/VJsonParse.res b/src/VJsonParse.res index 6176cdc..ea4523d 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -1,79 +1,44 @@ open VJsonTypes +open StringParser -let parseVJsonWithVariable = parseVariableString => { - open ReludeParse.Parser - open Operators - open Regexes +let lit = s => literal(s)->lexeme - let lexeme: 'a. t<'a> => t< - 'a, - > = // Run a parser and transparently consume all trailing whitespace. - p => left(p, ws) +let parseNull: parser = lit("null")->map(_ => Null) - // Parse the "null" keyword" - let parseNull: t> = str("null") |> map(_ => Null) |> lexeme +let parseBool: parser = lit("true")->or_(lit("false"))->map(s => Bool(s === "true")) - // Parse a number (float or int) - let parseNumber: t = - maybe(regex(floatRegex), "Not a valid number") |> map(Js.Float.fromString) |> lexeme +let parseNumber: parser = + regex(Regexes.number, "a number")->map(s => Number(s->Js.Float.fromString))->lexeme - // Parse a boolean. - let parseBool: t = - or_(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme +let parseString: parser = + regex(Regexes.string, ~index=1, "a string literal") + ->map(str => str->Js.String2.replaceByRe(Regexes.escapedQuote, "\"")) + ->lexeme - // Parse a string. Allows for escaped quotes. - // NOTE: not to be confused with `parse`, which takes a raw string and parses - // a whole AST -- this parses string literal syntax. - let parseString: t = - regex(inQuotesRegex) - |> map(match => { - match - ->Js.String2.replaceByRe(nonEscapedQuoteRegex, `$1`) // First group of the regex is characters before the quote that should be kept - ->Js.String2.replaceByRe(escapedQuoteRegex, "\"") - }) - |> lexeme +let parseVariable: parser = + between(lit("{{"), regex(Regexes.variable, "a variable")->lexeme, lit("}}"))->map(v => Variable( + v, + )) - // Parse a variable wrapped in a pair of doubled curly braces `{{ }}`. The - // string of text between the curly braces is parsed by `parseVariable`. - let parseVariable_ = flatMap( - right( - right(str("{{"), ws), - manyUntil(str("}}"), or_(str("\\}}") |> map(_ => "}}"), anyChar)), - ) |> map(l => l->Belt.List.toArray->Js.Array2.joinWith("")->Js.String.trim), - rawVariableString => - switch parseVariableString(rawVariableString) { - | Ok(variable) => pure(variable) - | Error(message) => fail(message) - }, - ) - - // Parse an array of VJson. - // Define these as lazy because of mutual recursion. - let rec parseArray: Lazy.t>>> = lazy ( - betweenSquares(parseVjsonLazy->Lazy.force |> sepByOptEnd(str(",") |> lexeme)) - |> map(Belt.List.toArray) - |> lexeme - ) - - and parseObject: Lazy.t>>> = lazy { - let parseKeyValuePair: t<(string, vjson<'v>)> = tuple2( - left(parseString, str(":")) |> lexeme, - parseVjsonLazy->Lazy.force, - ) - betweenCurlies(parseKeyValuePair |> sepByOptEnd(str(",") |> lexeme)) - |> map(pairs => pairs->Belt.List.toArray->Js.Dict.fromArray) - |> lexeme - } - - and parseVjsonLazy: Lazy.t>> = lazy ( +let rec parseTerm: parser = s => { + let p = parseNull - ->or_(parseNumber |> map(n => Number(n))) - ->or_(parseString |> map(s => String(s))) - ->or_(parseBool |> map(b => Bool(b))) - ->or_(parseVariable_ |> map(v => Variable(v))) - |> orElseLazy(~fallback=() => parseArray->Lazy.force |> map(arr => Array(arr))) - |> orElseLazy(~fallback=() => parseObject->Lazy.force |> map(d => Object(d->JsMap.fromDict))) - ) - - parseVjsonLazy->Lazy.force + ->or_(parseBool) + ->or_(parseNumber) + ->or_(parseString->map(s => String(s))) + ->or_(parseVariable) + ->or_(parseArray) + ->or_(parseObject) + p(s) +} +and parseArray = s => { + let p = between(lit("["), manySep(parseTerm, lit(",")), lit("]"))->map(vjs => Array(vjs)) + p(s) } +and parseKV = s => (parseString->left(lit(":"))->and_(parseTerm))(s) +and parseObject = s => + ( + between(lit("{"), manySep(parseKV, lit(",")), lit("}"))->map(kvs => Object( + kvs->Js.Dict.fromArray, + )) + )(s) diff --git a/src/VJsonParse.resi b/src/VJsonParse.resi deleted file mode 100644 index 1aaa911..0000000 --- a/src/VJsonParse.resi +++ /dev/null @@ -1,26 +0,0 @@ -open VJsonTypes - -// Given a parser for a variable, produces a parser of VJson. -// For example, if you want your variable syntax to be `{{myVariable}}`, then -// you can do something like - -// let parseVJson: ReludeParse.Parser.t(vjson(string)) = -// parseVJsonWithVariable( -// ReludeParse.Parser.( -// regex([%re {|/[a-zA-Z_][a-zA-Z0-9_]*/|}]) -// |> between(str("{{"), str("}}")) -// ), -// ); - -let parseVJsonWithVariable: (string => result<'v, string>) => ReludeParse.Parser.t> - -// Parses a variable in between double curly braces. The parser for the variable -// itself is up to the user. - -// let parseVJson: ReludeParse.Parser.t(vjson(string)) = -// parseVJsonWithDoubleCurlyBracesVariable( -// ReludeParse.Parser.regex([%re {|/[a-zA-Z_][a-zA-Z0-9_]*/|}]) -// ); - -//let parseVJsonWithDoubleCurlyBracesVariable: -// ReludeParse.Parser.t('v) => ReludeParse.Parser.t(vjson('v)); diff --git a/src/VJsonTypes.res b/src/VJsonTypes.res index 8e8458c..90c1866 100644 --- a/src/VJsonTypes.res +++ b/src/VJsonTypes.res @@ -1,10 +1,10 @@ -type rec vjson<'v> = +type rec vjson = | Null | Bool(bool) | Number(float) | String(string) - | Variable('v) - | Array(array>) - | Object(JsMap.t>) + | Variable(string) + | Array(array) + | Object(Js.Dict.t) -type t<'v> = vjson<'v> +type t = vjson diff --git a/src/VJsonTypes.resi b/src/VJsonTypes.resi deleted file mode 100644 index 8e8458c..0000000 --- a/src/VJsonTypes.resi +++ /dev/null @@ -1,10 +0,0 @@ -type rec vjson<'v> = - | Null - | Bool(bool) - | Number(float) - | String(string) - | Variable('v) - | Array(array>) - | Object(JsMap.t>) - -type t<'v> = vjson<'v> diff --git a/src/VJsonUtil.res b/src/VJsonUtil.res new file mode 100644 index 0000000..aa5fce0 --- /dev/null +++ b/src/VJsonUtil.res @@ -0,0 +1,8 @@ +let mapDict = (dict, f) => + dict->Js.Dict.entries->Js.Array2.map(((k, v)) => (k, f(v)))->Js.Dict.fromArray +let keepMapDict = (dict, f) => + dict + ->Js.Dict.entries + ->Belt.Array.keepMap(((k, v)) => f(v)->Belt.Option.map(v2 => (k, v2))) + ->Js.Dict.fromArray +let quote: string => string = s => s->Js.Json.string->Js.Json.stringify From f96afbf143a506b85e6cbad9d1d8827d28c45eb5 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sat, 19 Oct 2024 23:54:21 +0300 Subject: [PATCH 05/15] trying to improve error messages, it's not easy --- __tests__/VJsonParserTests.res | 6 ++--- src/StringParser.res | 43 +++++++++++++++++++++------------- src/VJson.res | 2 +- src/VJsonUtil.res | 6 +++++ 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index 751f740..374d9f2 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -6,7 +6,7 @@ let expectParse = (input, handler) => switch (input->VJson.parse, handler) { | (Ok(vj), Ok(checks)) => vj->checks | (Error(err), Error(checks)) => err->checks - | (Error(message), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ message) + | (Error(e), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ e.expected) | (Ok(vj), Error(_)) => failwith( "Expected an Error result, but successfully parsed: " ++ Js.Json.stringify(Obj.magic(vj)), @@ -24,11 +24,11 @@ let expectParseFail = (input, errMessageChecks) => input->expectParse(Error(errM let expectSomeParseFail = input => input->expectParseFail(_ => ()) module ParserTests = { - describe("null", () => { + Only.describe("null", () => { test("single value", () => expectOkParse("null", Null)) test("array", () => expectOkParse("[null, null]", Array([Null, Null]))) test("bad array (missing closing brackets)", () => - expectParseFail("[ null, null", m => expect(m)->toEqual(stringContaining("]"))) + expectParseFail("[ null, null", m => expect(m.expected)->toEqual(stringContaining("]"))) ) test("bad array (no comma)", () => expectSomeParseFail("[ 1 true ]")) }) diff --git a/src/StringParser.res b/src/StringParser.res index 95f4e0f..810f73e 100644 --- a/src/StringParser.res +++ b/src/StringParser.res @@ -1,23 +1,30 @@ -type parser<'res> = string => result<(string, 'res), string> +type success<'res> = {str: string, res: 'res} +type failure = {expected: string, remaining: string} +type parser<'res> = string => result, failure> let map: (parser<'a>, 'a => 'b) => parser<'b> = (parser, f) => str => - str->parser->Belt.Result.map(((str, res)) => (str, f(res))) + str->parser->Belt.Result.map(({str, res}) => {str, res: f(res)}) let opt: parser<'a> => parser> = parser => str => switch str->parser { - | Ok((str2, res)) => Ok((str2, Some(res))) - | _ => Ok(str, None) + | Ok({str: str2, res}) => Ok({str: str2, res: Some(res)}) + | _ => Ok({str, res: None}) } let or_: (parser<'a>, parser<'a>) => parser<'a> = (parser1, parser2) => str => switch str->parser1 { | Ok(res) => Ok(res) - | Error(_) => str->parser2 + | Error(_e1) => + switch str->parser2 { + | Ok(res) => Ok(res) + | Error(e2) => Error(e2) + } } let and_: (parser<'a>, parser<'b>) => parser<('a, 'b)> = (parser1, parser2) => str1 => switch str1->parser1 { - | Ok((str2, res1)) => str2->parser2->Belt.Result.map(((str3, res2)) => (str3, (res1, res2))) + | Ok({str: str2, res: res1}) => + str2->parser2->Belt.Result.map(({str: str3, res: res2}) => {str: str3, res: (res1, res2)}) | Error(e) => Error(e) } @@ -34,14 +41,14 @@ let many: 'a. parser<'a> => parser> = parser => startStr => { let keepGoing = ref(true) while keepGoing.contents { switch str.contents->parser { - | Ok((next, res)) => { + | Ok({str: next, res}) => { results->Js.Array2.push(res)->ignore str := next } | Error(_) => keepGoing := false } } - Ok((str.contents, results)) + Ok({str: str.contents, res: results}) } let many1: 'a. parser<'a> => parser> = parser => @@ -53,10 +60,10 @@ let manySep: 'a. (parser<'a>, parser<_>) => parser> = (parser, sepPars let keepGoing = ref(true) while keepGoing.contents { switch str.contents->parser { - | Ok((next, res)) => { + | Ok({str: next, res}) => { results->Js.Array2.push(res)->ignore switch next->sepParser { - | Ok((next2, _)) => str := next2 + | Ok({str: next2}) => str := next2 | _ => { str := next keepGoing := false @@ -66,15 +73,15 @@ let manySep: 'a. (parser<'a>, parser<_>) => parser> = (parser, sepPars | Error(_) => keepGoing := false } } - Ok((str.contents, results)) + Ok({str: str.contents, res: results}) } let sliceN = (str, from) => str->Js.String2.sliceToEnd(~from) let literal: string => parser = expected => str => str->Js.String2.startsWith(expected) - ? Ok((str->sliceN(expected->Js.String.length), expected)) - : Error(`expected "${expected}"`) + ? Ok({str: str->sliceN(expected->Js.String.length), res: expected}) + : Error({expected, remaining: str}->VJsonUtil.tapLog("errrr")) let regex: (Js.Re.t, ~index: int=?, string) => parser = ( regex, @@ -85,12 +92,16 @@ let regex: (Js.Re.t, ~index: int=?, string) => parser = ( ) => str => switch str->Js.String2.match_(regex) { | Some(arr) => - Ok(str->sliceN(arr[0]->Belt.Option.getExn->Js.String.length), arr[index]->Belt.Option.getExn) - | _ => Error(`expected ${description}`) + Ok({ + str: str->sliceN(arr[0]->Belt.Option.getExn->Js.String.length), + res: arr[index]->Belt.Option.getExn, + }) + | _ => Error({expected: description, remaining: str}) } let whitespace: parser = regex(%re(`/^\s*/`), "whitespace") let lexeme: parser<'a> => parser<'a> = parser => left(parser, whitespace) -let eof: parser = str => str === "" ? Ok("", ()) : Error("expected end of file") +let eof: parser = str => + str === "" ? Ok({str: "", res: ()}) : Error({expected: "end of file", remaining: str}) diff --git a/src/VJson.res b/src/VJson.res index 2fa43c8..cc79dea 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -110,5 +110,5 @@ let defaultVariableRegex = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) let parse = s => { let p = VJsonParse.parseTerm->StringParser.left(StringParser.eof) - s->p->Belt.Result.map(snd) + s->p->Belt.Result.map(r => r.res) } diff --git a/src/VJsonUtil.res b/src/VJsonUtil.res index aa5fce0..eb8befe 100644 --- a/src/VJsonUtil.res +++ b/src/VJsonUtil.res @@ -6,3 +6,9 @@ let keepMapDict = (dict, f) => ->Belt.Array.keepMap(((k, v)) => f(v)->Belt.Option.map(v2 => (k, v2))) ->Js.Dict.fromArray let quote: string => string = s => s->Js.Json.string->Js.Json.stringify + +@gentype +let tapLog = (value, description) => { + Js.log2(description, value) + value +} From 691504e017564a23a34e12ef362bbf4e803a562c Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 00:02:24 +0300 Subject: [PATCH 06/15] escaping rules for regexes changed in v11 --- src/Regexes.res | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Regexes.res b/src/Regexes.res index 74544c1..0b7634a 100644 --- a/src/Regexes.res +++ b/src/Regexes.res @@ -1,4 +1,4 @@ -let number = %re(`/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/`) -let string = %re(`/^"((?:[^"\\\\]|\\\\.)*)"/`) -let escapedQuote = %re(`/\\\\"/g`) +let number = %re(`/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/`) +let string = %re(`/^"((?:[^"\\]|\\.)*)"/`) +let escapedQuote = %re(`/\\"/g`) let variable = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) From d86f7b376f6b50881d8dc9c66eec5c7da8cadf02 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 00:02:51 +0300 Subject: [PATCH 07/15] useful util --- src/VJsonUtil.res | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/VJsonUtil.res b/src/VJsonUtil.res index aa5fce0..eb8befe 100644 --- a/src/VJsonUtil.res +++ b/src/VJsonUtil.res @@ -6,3 +6,9 @@ let keepMapDict = (dict, f) => ->Belt.Array.keepMap(((k, v)) => f(v)->Belt.Option.map(v2 => (k, v2))) ->Js.Dict.fromArray let quote: string => string = s => s->Js.Json.string->Js.Json.stringify + +@gentype +let tapLog = (value, description) => { + Js.log2(description, value) + value +} From 41d6b0da11cfda1dd7a4983c46556cb85bbc45ff Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 00:19:55 +0300 Subject: [PATCH 08/15] test suite all passes wow --- __tests__/VJsonParserTests.res | 2 +- src/Regexes.res | 2 +- src/StringParser.res | 7 ++++--- src/VJson.res | 5 ++++- src/VJsonParse.res | 2 +- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index 374d9f2..fa0595e 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -24,7 +24,7 @@ let expectParseFail = (input, errMessageChecks) => input->expectParse(Error(errM let expectSomeParseFail = input => input->expectParseFail(_ => ()) module ParserTests = { - Only.describe("null", () => { + describe("null", () => { test("single value", () => expectOkParse("null", Null)) test("array", () => expectOkParse("[null, null]", Array([Null, Null]))) test("bad array (missing closing brackets)", () => diff --git a/src/Regexes.res b/src/Regexes.res index 0b7634a..ae02c96 100644 --- a/src/Regexes.res +++ b/src/Regexes.res @@ -1,4 +1,4 @@ -let number = %re(`/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/`) +let number = %re(`/^([-+]?\d*\.?\d+)(?:[eE]([-+]?\d+))?/`) let string = %re(`/^"((?:[^"\\]|\\.)*)"/`) let escapedQuote = %re(`/\\"/g`) let variable = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) diff --git a/src/StringParser.res b/src/StringParser.res index 810f73e..9e83386 100644 --- a/src/StringParser.res +++ b/src/StringParser.res @@ -14,10 +14,11 @@ let opt: parser<'a> => parser> = parser => str => let or_: (parser<'a>, parser<'a>) => parser<'a> = (parser1, parser2) => str => switch str->parser1 { | Ok(res) => Ok(res) - | Error(_e1) => + | Error(e1) => switch str->parser2 { | Ok(res) => Ok(res) - | Error(e2) => Error(e2) + // Prefer the error message of whichever one consumed more input + | Error(e2) => Error(e1.remaining->Js.String.length < e2.remaining->Js.String.length ? e1 : e2) } } @@ -81,7 +82,7 @@ let sliceN = (str, from) => str->Js.String2.sliceToEnd(~from) let literal: string => parser = expected => str => str->Js.String2.startsWith(expected) ? Ok({str: str->sliceN(expected->Js.String.length), res: expected}) - : Error({expected, remaining: str}->VJsonUtil.tapLog("errrr")) + : Error({expected, remaining: str}) let regex: (Js.Re.t, ~index: int=?, string) => parser = ( regex, diff --git a/src/VJson.res b/src/VJson.res index cc79dea..319f9d2 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -109,6 +109,9 @@ let findVariables = root => let defaultVariableRegex = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) let parse = s => { - let p = VJsonParse.parseTerm->StringParser.left(StringParser.eof) + let p = + StringParser.whitespace + ->StringParser.right(VJsonParse.parseTerm) + ->StringParser.left(StringParser.eof) s->p->Belt.Result.map(r => r.res) } diff --git a/src/VJsonParse.res b/src/VJsonParse.res index ea4523d..e9260f8 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -27,8 +27,8 @@ let rec parseTerm: parser = s => { ->or_(parseNumber) ->or_(parseString->map(s => String(s))) ->or_(parseVariable) - ->or_(parseArray) ->or_(parseObject) + ->or_(parseArray) p(s) } and parseArray = s => { From a50a088aa486f10960b372f0298c924db34969a0 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 00:31:33 +0300 Subject: [PATCH 09/15] at least somewhat improved error messages --- __tests__/VJsonParserTests.res | 4 ++-- src/VJson.res | 17 ++++++++++++++++- src/VJsonUtil.res | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index fa0595e..0f282ef 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -6,7 +6,7 @@ let expectParse = (input, handler) => switch (input->VJson.parse, handler) { | (Ok(vj), Ok(checks)) => vj->checks | (Error(err), Error(checks)) => err->checks - | (Error(e), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ e.expected) + | (Error(e), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ e) | (Ok(vj), Error(_)) => failwith( "Expected an Error result, but successfully parsed: " ++ Js.Json.stringify(Obj.magic(vj)), @@ -28,7 +28,7 @@ module ParserTests = { test("single value", () => expectOkParse("null", Null)) test("array", () => expectOkParse("[null, null]", Array([Null, Null]))) test("bad array (missing closing brackets)", () => - expectParseFail("[ null, null", m => expect(m.expected)->toEqual(stringContaining("]"))) + expectParseFail("[ null, null", m => expect(m)->toEqual(stringContaining("]"))) ) test("bad array (no comma)", () => expectSomeParseFail("[ 1 true ]")) }) diff --git a/src/VJson.res b/src/VJson.res index 319f9d2..f36a274 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -113,5 +113,20 @@ let parse = s => { StringParser.whitespace ->StringParser.right(VJsonParse.parseTerm) ->StringParser.left(StringParser.eof) - s->p->Belt.Result.map(r => r.res) + switch s->p { + | Ok(r) => Ok(r.res) + | Error(e) => { + let posn = s->Js.String.length - e.remaining->Js.String.length + let location = switch getLineAndColumn(s, posn) { + | Some(lineAndCol) => renderLineAndColumn(lineAndCol) + | None => `position ${posn->Js.Int.toString}` + } + Error( + `At ${location}: Expected ${e.expected}, got ${e.remaining->Js.String2.slice( + ~from=0, + ~to_=100, + )}`, + ) + } + } } diff --git a/src/VJsonUtil.res b/src/VJsonUtil.res index eb8befe..05a1185 100644 --- a/src/VJsonUtil.res +++ b/src/VJsonUtil.res @@ -12,3 +12,20 @@ let tapLog = (value, description) => { Js.log2(description, value) value } + +let renderLineAndColumn = ((line, column)) => + `line ${line->Js.Int.toString}, column ${column->Js.Int.toString}` + +// Given an index in a string, return the line and column number at that index. +let getLineAndColumn: (string, int) => option<(int, int)> = (str, index) => + if index >= str->Js.String.length || index < 0 { + None + } else { + let substr = str->Js.String2.slice(~from=0, ~to_=index) + let lines = substr->Js.String2.split("\n") + let nLines = lines->Js.Array2.length + Some(( + nLines, + lines->Belt.Array.get(nLines - 1)->Belt.Option.mapWithDefault(0, Js.String.length), + )) + } From 309a1b9a3c53b00c85ed44c98904d8b88da9e372 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 10:02:32 +0300 Subject: [PATCH 10/15] v2.1.0 --- bsconfig.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bsconfig.json b/bsconfig.json index 8a0a068..92ee613 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,6 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "3.0.0", + "version": "2.1.0", "sources": [ { "dir": "src", diff --git a/package.json b/package.json index 5aa0180..45b57bb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "2.0.0", + "version": "2.1.0", "declaration": true, "main": "src/VJson.bs", "types": "src/index.d.ts", From 1b714eb418ed5a64c6e87bfedc558ac91f4404a2 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 21:55:38 +0300 Subject: [PATCH 11/15] reintroduce generic type, but always parse as a string --- __tests__/VJsonTests.res | 8 ++++---- src/VJson.res | 11 +++++------ src/VJsonParse.res | 39 +++++++++++++++++++-------------------- src/VJsonTypes.res | 10 +++++----- 4 files changed, 33 insertions(+), 35 deletions(-) diff --git a/__tests__/VJsonTests.res b/__tests__/VJsonTests.res index cc6e010..b80db5a 100644 --- a/__tests__/VJsonTests.res +++ b/__tests__/VJsonTests.res @@ -43,10 +43,10 @@ module MapTests = { ("x", variable("1234")), ("z", vjsonArray([float(1.0), number(2.0), variable("4321")])), ]) - expect(vj->VJson.map(s => s ++ "!!"))->toEqual( + expect(vj->VJson.map(int_of_string))->toEqual( object_([ - ("x", variable("1234!!")), - ("z", vjsonArray([float(1.0), number(2.0), variable("4321!!")])), + ("x", variable(1234)), + ("z", vjsonArray([float(1.0), number(2.0), variable(4321)])), ]), ) }) @@ -54,7 +54,7 @@ module MapTests = { } module SerializeTests = { - let expectSerialize = (vj: vjson, str) => { + let expectSerialize = (vj: vjson, str) => { let serialized: string = VJson.serializeWrapCurlyBraces(s => s, vj) expect(serialized)->toEqual(str) } diff --git a/src/VJson.res b/src/VJson.res index f36a274..aac3b89 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -1,6 +1,5 @@ // Some useful functions that operate on VJson -module Types = VJsonTypes -include Types +include VJsonTypes module Builder = VJsonBuilder open VJsonUtil @@ -51,7 +50,7 @@ let rec toJson = variableToJson => { } // Translate to JSON, given a conversion function which will optionally return a JSON value for each variable. -let rec toJsonOptional: (string => option, vjson) => option = ( +let rec toJsonOptional: 'v. ('v => option, vjson<'v>) => option = ( variableToJson, x, ) => { @@ -106,12 +105,12 @@ let findVariables = root => vars }) -let defaultVariableRegex = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) +let defaultVariableRegex = Regexes.variable -let parse = s => { +let parse = (~variableRegex=defaultVariableRegex, s) => { let p = StringParser.whitespace - ->StringParser.right(VJsonParse.parseTerm) + ->StringParser.right(VJsonParse.parseTerm(variableRegex)) ->StringParser.left(StringParser.eof) switch s->p { | Ok(r) => Ok(r.res) diff --git a/src/VJsonParse.res b/src/VJsonParse.res index e9260f8..cc174fd 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -1,13 +1,17 @@ open VJsonTypes open StringParser +// NOTE: these parsers only return a `vjson` with an internal type of `string`. +// In order to use another variable type, use VJson.map and convert the variables +// from strings. + let lit = s => literal(s)->lexeme -let parseNull: parser = lit("null")->map(_ => Null) +let parseNull: parser> = lit("null")->map(_ => Null) -let parseBool: parser = lit("true")->or_(lit("false"))->map(s => Bool(s === "true")) +let parseBool: parser> = lit("true")->or_(lit("false"))->map(s => Bool(s === "true")) -let parseNumber: parser = +let parseNumber: parser> = regex(Regexes.number, "a number")->map(s => Number(s->Js.Float.fromString))->lexeme let parseString: parser = @@ -15,30 +19,25 @@ let parseString: parser = ->map(str => str->Js.String2.replaceByRe(Regexes.escapedQuote, "\"")) ->lexeme -let parseVariable: parser = - between(lit("{{"), regex(Regexes.variable, "a variable")->lexeme, lit("}}"))->map(v => Variable( - v, - )) +let parseVariable: Js.Re.t => parser> = variableRegex => + between(lit("{{"), regex(variableRegex, "a variable")->lexeme, lit("}}"))->map(v => Variable(v)) + +let rec parseTerm: Js.Re.t => parser> = variableRegex => s => { + let parseArray = + between(lit("["), manySep(parseTerm(variableRegex), lit(",")), lit("]"))->map(vjs => Array(vjs)) + let parseKV = parseString->left(lit(":"))->and_(parseTerm(variableRegex)) + let parseObject = + between(lit("{"), manySep(parseKV, lit(",")), lit("}"))->map(kvs => Object( + kvs->Js.Dict.fromArray, + )) -let rec parseTerm: parser = s => { let p = parseNull ->or_(parseBool) ->or_(parseNumber) ->or_(parseString->map(s => String(s))) - ->or_(parseVariable) + ->or_(parseVariable(variableRegex)) ->or_(parseObject) ->or_(parseArray) p(s) } -and parseArray = s => { - let p = between(lit("["), manySep(parseTerm, lit(",")), lit("]"))->map(vjs => Array(vjs)) - p(s) -} -and parseKV = s => (parseString->left(lit(":"))->and_(parseTerm))(s) -and parseObject = s => - ( - between(lit("{"), manySep(parseKV, lit(",")), lit("}"))->map(kvs => Object( - kvs->Js.Dict.fromArray, - )) - )(s) diff --git a/src/VJsonTypes.res b/src/VJsonTypes.res index 90c1866..3ebb5db 100644 --- a/src/VJsonTypes.res +++ b/src/VJsonTypes.res @@ -1,10 +1,10 @@ -type rec vjson = +type rec vjson<'v> = | Null | Bool(bool) | Number(float) | String(string) - | Variable(string) - | Array(array) - | Object(Js.Dict.t) + | Variable('v) + | Array(array>) + | Object(Js.Dict.t>) -type t = vjson +type t<'v> = vjson<'v> From 401c43c25e39303458a2718c8bd91b7a04e37f36 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 21:56:49 +0300 Subject: [PATCH 12/15] v2.2.0 --- package.json | 2 +- bsconfig.json => rescript.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename bsconfig.json => rescript.json (95%) diff --git a/package.json b/package.json index 45b57bb..9f8abd1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "2.1.0", + "version": "2.2.0", "declaration": true, "main": "src/VJson.bs", "types": "src/index.d.ts", diff --git a/bsconfig.json b/rescript.json similarity index 95% rename from bsconfig.json rename to rescript.json index 92ee613..12b60d9 100644 --- a/bsconfig.json +++ b/rescript.json @@ -1,6 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "2.1.0", + "version": "2.2.0", "sources": [ { "dir": "src", From 77d9ea1825537dc64671e028321ae2d929a7adee Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Sun, 20 Oct 2024 21:59:41 +0300 Subject: [PATCH 13/15] v2.2.1 --- rescript.json => bsconfig.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename rescript.json => bsconfig.json (95%) diff --git a/rescript.json b/bsconfig.json similarity index 95% rename from rescript.json rename to bsconfig.json index 12b60d9..ac5e1b5 100644 --- a/rescript.json +++ b/bsconfig.json @@ -1,6 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "2.2.0", + "version": "2.2.1", "sources": [ { "dir": "src", diff --git a/package.json b/package.json index 9f8abd1..bb2bf08 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "2.2.0", + "version": "2.2.1", "declaration": true, "main": "src/VJson.bs", "types": "src/index.d.ts", From 4762b0e7369ff3274afd7cacd8e220553d07b371 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Wed, 23 Oct 2024 13:46:21 +0300 Subject: [PATCH 14/15] improved error messages --- __tests__/VJsonParserTests.res | 3 +- src/Regexes.res | 1 + src/StringParser.res | 62 ++++++++++++++++++++-------------- src/VJson.res | 12 +++---- src/VJsonParse.res | 15 +++++--- src/VJsonUtil.res | 2 ++ 6 files changed, 56 insertions(+), 39 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index 0f282ef..9c89a60 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -21,7 +21,7 @@ let expectOkParse = (input, output) => input->expectParse(Ok(vj => expect(vj)->t let expectParseFail = (input, errMessageChecks) => input->expectParse(Error(errMessageChecks)) // Tests failure in a generic way, just that it's an Error of some kind. -let expectSomeParseFail = input => input->expectParseFail(_ => ()) +let expectSomeParseFail = input => input->expectParseFail(e => Js.log2("Some error", e)) module ParserTests = { describe("null", () => { @@ -45,6 +45,7 @@ module ParserTests = { } }` expectSomeParseFail(rawText) + // expectParseFail(rawText, m => expect(m)->toEqual(stringContaining("}}"))) }) }) diff --git a/src/Regexes.res b/src/Regexes.res index ae02c96..00508ad 100644 --- a/src/Regexes.res +++ b/src/Regexes.res @@ -2,3 +2,4 @@ let number = %re(`/^([-+]?\d*\.?\d+)(?:[eE]([-+]?\d+))?/`) let string = %re(`/^"((?:[^"\\]|\\.)*)"/`) let escapedQuote = %re(`/\\"/g`) let variable = %re(`/[a-zA-Z_][a-zA-Z0-9_]*/`) +let whitespace = %re(`/^\s*/`) diff --git a/src/StringParser.res b/src/StringParser.res index 9e83386..d1bf25d 100644 --- a/src/StringParser.res +++ b/src/StringParser.res @@ -1,3 +1,5 @@ +open VJsonUtil + type success<'res> = {str: string, res: 'res} type failure = {expected: string, remaining: string} type parser<'res> = string => result, failure> @@ -33,48 +35,56 @@ let left: (parser<'a>, parser<_>) => parser<'a> = (parser1, parser2) => parser1->and_(parser2)->map(fst) let right: (parser<_>, parser<'a>) => parser<'a> = (parser1, parser2) => parser1->and_(parser2)->map(snd) -let between: 'a 'b 'c. (parser<'a>, parser<'b>, parser<'c>) => parser<'b> = (l, parser, r) => - l->and_(parser)->and_(r)->map((((_, res), _)) => res) -let many: 'a. parser<'a> => parser> = parser => startStr => { +// Parse as many items of the first parser as possible. After each parse, attempt to +// parse a separator. If successful, keep parsing. Otherwise, parse with the end parser. +let manySepEnd: 'a. (parser<'a>, ~sep: parser<_>, ~end_: parser<_>) => parser> = ( + parser, + ~sep, + ~end_, +) => startStr => { let results = [] let str = ref(startStr) let keepGoing = ref(true) - while keepGoing.contents { - switch str.contents->parser { - | Ok({str: next, res}) => { - results->Js.Array2.push(res)->ignore - str := next - } - | Error(_) => keepGoing := false + let error = ref(None) + let endLoop = (lastError, ~combine) => { + keepGoing := false + switch str.contents->end_ { + // The results we parsed are only valid if the endParser also matches. + | Ok({str: next}) => str := next + // Otherwise, the last failure is the "real" error. + | Error(e) => + error := + Some( + combine + ? {...lastError, expected: lastError.expected->concatIfNotIncluded(` OR ${e.expected}`)} + : lastError, + ) } } - Ok({str: str.contents, res: results}) -} - -let many1: 'a. parser<'a> => parser> = parser => - parser->and_(many(parser))->map(((first, rest)) => [first]->Js.Array2.concat(rest)) -let manySep: 'a. (parser<'a>, parser<_>) => parser> = (parser, sepParser) => startStr => { - let results = [] - let str = ref(startStr) - let keepGoing = ref(true) while keepGoing.contents { switch str.contents->parser { | Ok({str: next, res}) => { results->Js.Array2.push(res)->ignore - switch next->sepParser { + switch next->sep { | Ok({str: next2}) => str := next2 - | _ => { + | Error(e) => { str := next - keepGoing := false + // After a successful item parse, either a separator or an end parse is + // valid, so if both fail, combine their errors + endLoop(e, ~combine=true) } } } - | Error(_) => keepGoing := false + // If the item parse fails, only an end parse is valid, don't combine errors + | Error(e) => endLoop(e, ~combine=false) } } - Ok({str: str.contents, res: results}) + switch error.contents { + | None => Ok({str: str.contents, res: results}) + | Some(e) => Error(e) + } } let sliceN = (str, from) => str->Js.String2.sliceToEnd(~from) @@ -82,7 +92,7 @@ let sliceN = (str, from) => str->Js.String2.sliceToEnd(~from) let literal: string => parser = expected => str => str->Js.String2.startsWith(expected) ? Ok({str: str->sliceN(expected->Js.String.length), res: expected}) - : Error({expected, remaining: str}) + : Error({expected: expected->quote, remaining: str}) let regex: (Js.Re.t, ~index: int=?, string) => parser = ( regex, @@ -100,7 +110,7 @@ let regex: (Js.Re.t, ~index: int=?, string) => parser = ( | _ => Error({expected: description, remaining: str}) } -let whitespace: parser = regex(%re(`/^\s*/`), "whitespace") +let whitespace: parser = regex(Regexes.whitespace, "whitespace") let lexeme: parser<'a> => parser<'a> = parser => left(parser, whitespace) diff --git a/src/VJson.res b/src/VJson.res index aac3b89..236482c 100644 --- a/src/VJson.res +++ b/src/VJson.res @@ -115,17 +115,15 @@ let parse = (~variableRegex=defaultVariableRegex, s) => { switch s->p { | Ok(r) => Ok(r.res) | Error(e) => { - let posn = s->Js.String.length - e.remaining->Js.String.length + let posn = s->Js.String.length - e.remaining->Js.String.length - 1 let location = switch getLineAndColumn(s, posn) { | Some(lineAndCol) => renderLineAndColumn(lineAndCol) | None => `position ${posn->Js.Int.toString}` } - Error( - `At ${location}: Expected ${e.expected}, got ${e.remaining->Js.String2.slice( - ~from=0, - ~to_=100, - )}`, - ) + let remaining = + e.remaining->Js.String2.replaceByRe(%re(`/\s+/`), " ")->Js.String2.slice(~from=0, ~to_=20) + let remaining = remaining === "" ? "end of file" : remaining->quote + Error(`At ${location}: Expected ${e.expected} but got ${remaining}`) } } } diff --git a/src/VJsonParse.res b/src/VJsonParse.res index cc174fd..48cc5e2 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -20,16 +20,21 @@ let parseString: parser = ->lexeme let parseVariable: Js.Re.t => parser> = variableRegex => - between(lit("{{"), regex(variableRegex, "a variable")->lexeme, lit("}}"))->map(v => Variable(v)) + lit("{{") + ->right(regex(variableRegex, "a variable")->lexeme) + ->left(lit("}}")) + ->map(v => Variable(v)) let rec parseTerm: Js.Re.t => parser> = variableRegex => s => { let parseArray = - between(lit("["), manySep(parseTerm(variableRegex), lit(",")), lit("]"))->map(vjs => Array(vjs)) + lit("[") + ->right(manySepEnd(parseTerm(variableRegex), ~sep=lit(","), ~end_=lit("]"))) + ->map(vjs => Array(vjs)) let parseKV = parseString->left(lit(":"))->and_(parseTerm(variableRegex)) let parseObject = - between(lit("{"), manySep(parseKV, lit(",")), lit("}"))->map(kvs => Object( - kvs->Js.Dict.fromArray, - )) + lit("{") + ->right(manySepEnd(parseKV, ~sep=lit(","), ~end_=lit("}"))) + ->map(kvs => Object(kvs->Js.Dict.fromArray)) let p = parseNull diff --git a/src/VJsonUtil.res b/src/VJsonUtil.res index 05a1185..f9d1804 100644 --- a/src/VJsonUtil.res +++ b/src/VJsonUtil.res @@ -7,6 +7,8 @@ let keepMapDict = (dict, f) => ->Js.Dict.fromArray let quote: string => string = s => s->Js.Json.string->Js.Json.stringify +let concatIfNotIncluded = (str, str2) => str->Js.String2.includes(str2) ? str : str ++ str2 + @gentype let tapLog = (value, description) => { Js.log2(description, value) From 451b2a07a292ae3417b2dfd29edd1e69513cb471 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Wed, 23 Oct 2024 13:47:02 +0300 Subject: [PATCH 15/15] v2.2.2 --- bsconfig.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bsconfig.json b/bsconfig.json index ac5e1b5..f800560 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -1,6 +1,6 @@ { "name": "@draftbit/variable-json", - "version": "2.2.1", + "version": "2.2.2", "sources": [ { "dir": "src", diff --git a/package.json b/package.json index bb2bf08..c477a7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "2.2.1", + "version": "2.2.2", "declaration": true, "main": "src/VJson.bs", "types": "src/index.d.ts",