From 649b01b4c385e292dd08feebd85eab4c958d3554 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Wed, 23 Aug 2023 11:30:46 -0400 Subject: [PATCH 01/15] change to rescript syntax, upgrade bs-platform --- package.json | 10 +- src/VJson.re | 137 ------------------------ src/VJson.res | 131 ++++++++++++++++++++++ src/{VJson.rei => VJson.resi} | 83 +++++++------- src/VJsonBuilder.re | 34 ------ src/VJsonBuilder.res | 33 ++++++ src/VJsonParse.re | 96 ----------------- src/VJsonParse.res | 86 +++++++++++++++ src/{VJsonParse.rei => VJsonParse.resi} | 13 ++- src/VJsonTypes.re | 10 -- src/VJsonTypes.rei | 10 -- src/VJsonTypes.res | 10 ++ src/VJsonTypes.resi | 10 ++ yarn.lock | 8 +- 14 files changed, 326 insertions(+), 345 deletions(-) delete mode 100644 src/VJson.re create mode 100644 src/VJson.res rename src/{VJson.rei => VJson.resi} (57%) delete mode 100644 src/VJsonBuilder.re create mode 100644 src/VJsonBuilder.res delete mode 100644 src/VJsonParse.re create mode 100644 src/VJsonParse.res rename src/{VJsonParse.rei => VJsonParse.resi} (86%) delete mode 100644 src/VJsonTypes.re delete mode 100644 src/VJsonTypes.rei create mode 100644 src/VJsonTypes.res create mode 100644 src/VJsonTypes.resi diff --git a/package.json b/package.json index 77224b8..9362e30 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "0.8.1", + "version": "0.9.0", "declaration": true, "main": "dist/VJson.bs", "types": "src/index.d.ts", @@ -32,7 +32,7 @@ "license": "MIT", "devDependencies": { "@glennsl/bs-json": "^5.0.2", - "bs-platform": "^8.4.2", + "bs-platform": "^9.0.2", "jest": "^26.4.1", "lint-staged": "^10.2.11", "parcel": "^1.12.4", @@ -43,10 +43,10 @@ }, "peerDependencies": { "@glennsl/bs-json": "^5.0.2", - "rescript-js-collections": "^1.8.2", - "bs-platform": "^7 || ^8", + "bs-platform": "^7 || ^8 || ^9", "relude": "^0.59.0", - "relude-parse": "^0.8.0" + "relude-parse": "^0.8.0", + "rescript-js-collections": "^1.8.2" }, "lint-staged": { "*.{re,rei}": [ diff --git a/src/VJson.re b/src/VJson.re deleted file mode 100644 index 6c2dacb..0000000 --- a/src/VJson.re +++ /dev/null @@ -1,137 +0,0 @@ -// Some useful functions that operate on VJson -module Types = VJsonTypes; -include Types; -module Builder = VJsonBuilder; - -// Traverse a JSON structure with a function -let rec map = (vjson, f) => { - switch (vjson) { - | Variable(v) => Variable(f(v)) - | Bool(b) => Bool(b) - | 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)) - }; -}; - -let mapL = (f, vjson) => map(vjson, f); - -// Traverse a JSON structure with a function -let rec reduce = (vjson, result, f) => { - let newResult = f(result, vjson); - switch (vjson) { - | Bool(_) - | Null - | String(_) - | 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)) - }; -}; - -let reduceL = (start, f, vjson) => reduce(vjson, start, f); - -// Translate to JSON, given a conversion function. Use sig-data-last for better -// compatibility with `@glennsl/bs-json`. -let rec toJson = variableToJson => - Json.Encode.( - fun - | 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 - ); - -// 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 rec serialize = (vj, variableToString) => - switch (vj) { - | Null => "null" - | Bool(true) => "true" - | Bool(false) => "false" - | Number(n) => n->Js.Float.toString - | String(s) => Js.Json.(s->string->stringify) - | Variable(v) => v->variableToString - | Array(arr) => - "[" - ++ arr - ->Belt.Array.map(vj' => vj'->serialize(variableToString)) - ->Js.Array2.joinWith(", ") - ++ "]" - | Object(o) => - "{" - ++ o - ->JsMap.toArray - ->Belt.Array.map(((k, v)) => - Js.Json.(k->string->stringify) - ++ ": " - ++ v->serialize(variableToString) - ) - ->Js.Array2.joinWith(", ") - ++ "}" - }; - -let serializeWrapCurlyBraces = (variableToString, vj) => - vj->serialize((v: 'v) => "{{" ++ v->variableToString ++ "}}"); - -let findVariables = root => - root->reduce( - [||], - (vars, vj) => { - switch (vj) { - | Variable(v) => vars->Js.Array2.push(v)->ignore - | _ => () - }; - vars; - }, - ); - -let defaultVariableRegex = [%re {|/[a-zA-Z_][a-zA-Z0-9_]*/|}]; - -let parseWith = (parseVariable, input) => { - ReludeParse.Parser.( - ws - *> VJsonParse.parseVJsonWithVariable(parseVariable) - // Ensure that we have consumed the whole string - <* eof - |> runParser(input) - ); -}; - -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(string, string) = - (reg, myString) => - switch (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 = input => - switch (parseDefault(input)) { - | Ok(vjson) => vjson - | Error(ReludeParse.Parser.ParseError.ParseError(message)) => - failwith(message) - }; - -let parseWithRegex = regex => parseWith(parseFromRegex(regex)); diff --git a/src/VJson.res b/src/VJson.res new file mode 100644 index 0000000..5e54693 --- /dev/null +++ b/src/VJson.res @@ -0,0 +1,131 @@ +// Some useful functions that operate on VJson +module Types = VJsonTypes +include Types +module Builder = VJsonBuilder + +// Traverse a JSON structure with a function +let rec map = (vjson, f) => + switch vjson { + | Variable(v) => Variable(f(v)) + | Bool(b) => Bool(b) + | 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)) + } + +let mapL = (f, vjson) => map(vjson, f) + +// Traverse a JSON structure with a function +let rec reduce = (vjson, result, f) => { + let newResult = f(result, vjson) + switch vjson { + | Bool(_) + | Null + | String(_) + | 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)) + } +} + +let reduceL = (start, f, vjson) => reduce(vjson, start, f) + +// Translate to JSON, given a conversion function. Use sig-data-last for better +// compatibility with `@glennsl/bs-json`. +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 + } +} + +// 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 rec serialize = (vj, variableToString) => + switch vj { + | Null => "null" + | Bool(true) => "true" + | Bool(false) => "false" + | Number(n) => n->Js.Float.toString + | String(s) => + open Js.Json + s->string->stringify + | Variable(v) => v->variableToString + | Array(arr) => + "[" ++ + (arr->Belt.Array.map(vj' => vj'->serialize(variableToString))->Js.Array2.joinWith(", ") ++ + "]") + | Object(o) => + "{" ++ + (o + ->JsMap.toArray + ->Belt.Array.map(((k, v)) => + { + open Js.Json + k->string->stringify + } ++ + (": " ++ + v->serialize(variableToString)) + ) + ->Js.Array2.joinWith(", ") ++ + "}") + } + +let serializeWrapCurlyBraces = (variableToString, vj) => + vj->serialize((v: 'v) => "{{" ++ (v->variableToString ++ "}}")) + +let findVariables = root => + root->reduce([], (vars, vj) => { + switch vj { + | Variable(v) => vars->Js.Array2.push(v)->ignore + | _ => () + } + vars + }) + +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 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 = input => + switch parseDefault(input) { + | Ok(vjson) => vjson + | Error(ReludeParse.Parser.ParseError.ParseError(message)) => failwith(message) + } + +let parseWithRegex = regex => parseWith(parseFromRegex(regex)) diff --git a/src/VJson.rei b/src/VJson.resi similarity index 57% rename from src/VJson.rei rename to src/VJson.resi index 9cc9821..3794c9c 100644 --- a/src/VJson.rei +++ b/src/VJson.resi @@ -1,29 +1,29 @@ // Some useful functions that operate on VJson -type vjson('v) = VJsonTypes.vjson('v); +type vjson<'v> = VJsonTypes.vjson<'v> // Abbreviation -type t('v) = vjson('v); +type t<'v> = vjson<'v> // Alter every variable in a VJson tree -let map: (vjson('v1), 'v1 => 'v2) => vjson('v2); +let map: (vjson<'v1>, 'v1 => 'v2) => vjson<'v2> // `map` in significant-data-last order -let mapL: ('v1 => 'v2, vjson('v1)) => vjson('v2); +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; +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; +let reduceL: ('a, ('a, vjson<'v>) => 'a, vjson<'v>) => 'a // Translate to JSON, given a conversion function for a variable. Use // sig-data-last for better compatibility with `@glennsl/bs-json`. -let toJson: ('v => Js.Json.t, vjson('v)) => Js.Json.t; +let toJson: ('v => Js.Json.t, vjson<'v>) => Js.Json.t // 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; +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 @@ -31,84 +31,83 @@ let serialize: (vjson('v), 'v => string) => string; // 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; +let serializeWrapCurlyBraces: ('v => string, vjson<'v>) => string // Traverse the tree, returning a set of all of the variables. -let findVariables: vjson('v) => array('v); +let findVariables: vjson<'v> => array<'v> // The regex used to make the default variable parser. -let defaultVariableRegex: Js.Re.t; +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(string, string); +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(string, string); +let parseAnyStringVariable: string => result // Parse a VJson tree with a custom variable parser. -let parseWith: - (string => result('v, string), string) => - result(vjson('v), ReludeParse.Parser.ParseError.t); +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); +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(vjson(string), ReludeParse.Parser.ParseError.t); +let parseWithRegex: (Js.Re.t, string) => result, ReludeParse.Parser.ParseError.t> // Parse a VJson tree with the default variable parser. -let parseDefault: - string => result(vjson(string), ReludeParse.Parser.ParseError.t); +let parseDefault: string => result, ReludeParse.Parser.ParseError.t> // Exception-throwing version of `parseDefault` -let parseDefaultExn: string => vjson(string); +let parseDefaultExn: string => vjson module Types: { - type vjson('v) = + type rec vjson<'v> = | Null | Bool(bool) | Number(float) | String(string) | Variable('v) - | Array(array(vjson('v))) - | Object(JsMap.t(string, vjson('v))); + | Array(array>) + | Object(JsMap.t>) - type t('v) = vjson('v); -}; + 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); + 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); + 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); + 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); + 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); + 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)) => vjson('v); + let vjsonArray: array> => vjson<'v> // Create a VJson object from a key/value array - let object_: array((string, vjson('v))) => vjson('v); + 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); + 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); -}; + let jsMap: ('k => string, 'a => vjson<'v>, JsMap.t<'k, 'a>) => vjson<'v> +} diff --git a/src/VJsonBuilder.re b/src/VJsonBuilder.re deleted file mode 100644 index f52ce0d..0000000 --- a/src/VJsonBuilder.re +++ /dev/null @@ -1,34 +0,0 @@ -include VJsonTypes; - -let null = Null; -let bool = b => Bool(b); -let number = n => Number(n); -let float = f => Number(f); -let int = i => Number(i->float_of_int); -let string = s => String(s); -let variable = v => Variable(v); -let id = vj => vj; - -let array = (toVJson, arr) => Array(arr->Belt.Array.map(toVJson)); - -let vjsonArray = arr => Array(arr); - -let object_ = keyVals => Object(keyVals->JsMap.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 rec json = j => { - switch (j->Js.Json.classify) { - | JSONFalse => Bool(false) - | JSONTrue => Bool(true) - | JSONNull => Null - | 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)) - }; -}; diff --git a/src/VJsonBuilder.res b/src/VJsonBuilder.res new file mode 100644 index 0000000..35a6fc3 --- /dev/null +++ b/src/VJsonBuilder.res @@ -0,0 +1,33 @@ +include VJsonTypes + +let null = Null +let bool = b => Bool(b) +let number = n => Number(n) +let float = f => Number(f) +let int = i => Number(i->float_of_int) +let string = s => String(s) +let variable = v => Variable(v) +let id = vj => vj + +let array = (toVJson, arr) => Array(arr->Belt.Array.map(toVJson)) + +let vjsonArray = arr => Array(arr) + +let object_ = keyVals => Object(keyVals->JsMap.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 rec json = j => + switch j->Js.Json.classify { + | JSONFalse => Bool(false) + | JSONTrue => Bool(true) + | JSONNull => Null + | 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)) + } diff --git a/src/VJsonParse.re b/src/VJsonParse.re deleted file mode 100644 index 9a9c774..0000000 --- a/src/VJsonParse.re +++ /dev/null @@ -1,96 +0,0 @@ -open VJsonTypes; - -let parseVJsonWithVariable = parseVariableString => { - // (variableFromString: string => result('v, string)) => { - - open ReludeParse.Parser; - - // Run a parser and transparently consume all trailing whitespace. - let lexeme: 'a. t('a) => t('a) = p => p <* ws; - - // Parse the "null" keyword" - let parseNull: t(vjson('v)) = 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(float) = - regex(floatRegex) - "Not a valid number" - |> map(Js.Float.fromString) - |> lexeme; - - // Parse a boolean. - let parseBool: t(bool) = - str("true") - |> map(_ => true) - <|> (str("false") |> map(_ => false)) - |> 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(string) = - betweenDoubleQuotes( - many(str("\\\"") |> map(_ => "\"") <|> anyCharNotIn(["\""])) - |> map(l => l->Belt.List.toArray->Js.Array2.joinWith("")), - ) - |> 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) - |> 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(t(array(vjson('v)))) = - lazy( - betweenSquares( - parseVjsonLazy->Lazy.force |> sepByOptEnd(str(",") |> lexeme), - ) - |> map(Belt.List.toArray) - |> lexeme - ) - - and parseObject: Lazy.t(t(Js.Dict.t(vjson('v)))) = - lazy({ - let parseKeyValuePair: t((string, vjson('v))) = - tuple2( - 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(t(vjson('v))) = - lazy( - parseNull - <|> (parseString |> map(s => String(s))) - <|> (parseNumber |> map(n => Number(n))) - <|> (parseBool |> map(b => Bool(b))) - <|> (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; -}; diff --git a/src/VJsonParse.res b/src/VJsonParse.res new file mode 100644 index 0000000..7b29a8d --- /dev/null +++ b/src/VJsonParse.res @@ -0,0 +1,86 @@ +open VJsonTypes + +let parseVJsonWithVariable = parseVariableString => { + open // (variableFromString: string => result('v, string)) => { + + ReludeParse.Parser + + let lexeme: 'a. t<'a> => t< + 'a, + > = // Run a parser and transparently consume all trailing whitespace. + p => \"<*"(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 + + // Parse a boolean. + let parseBool: t = + \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> 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 = + betweenDoubleQuotes( + many(\"<|>"(str("\\\"") |> map(_ => "\""), anyCharNotIn(list{"\""}))) |> map(l => + l->Belt.List.toArray->Js.Array2.joinWith("") + ), + ) |> 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)), + ) |> 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( + \"<*"(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 ( + \"<|>"( + \"<|>"( + \"<|>"( + \"<|>"(parseNull, parseString |> map(s => String(s))), + parseNumber |> map(n => Number(n)), + ), + parseBool |> map(b => Bool(b)), + ), + 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 +} diff --git a/src/VJsonParse.rei b/src/VJsonParse.resi similarity index 86% rename from src/VJsonParse.rei rename to src/VJsonParse.resi index 1ae6e27..1aaa911 100644 --- a/src/VJsonParse.rei +++ b/src/VJsonParse.resi @@ -1,9 +1,9 @@ -open VJsonTypes; +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.( @@ -11,17 +11,16 @@ open VJsonTypes; // |> between(str("{{"), str("}}")) // ), // ); -// -let parseVJsonWithVariable: - (string => result('v, string)) => ReludeParse.Parser.t(vjson('v)); + +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.re b/src/VJsonTypes.re deleted file mode 100644 index 07ff4e8..0000000 --- a/src/VJsonTypes.re +++ /dev/null @@ -1,10 +0,0 @@ -type vjson('v) = - | Null - | Bool(bool) - | Number(float) - | String(string) - | Variable('v) - | Array(array(vjson('v))) - | Object(JsMap.t(string, vjson('v))); - -type t('v) = vjson('v); diff --git a/src/VJsonTypes.rei b/src/VJsonTypes.rei deleted file mode 100644 index 07ff4e8..0000000 --- a/src/VJsonTypes.rei +++ /dev/null @@ -1,10 +0,0 @@ -type vjson('v) = - | Null - | Bool(bool) - | Number(float) - | String(string) - | Variable('v) - | Array(array(vjson('v))) - | Object(JsMap.t(string, vjson('v))); - -type t('v) = vjson('v); diff --git a/src/VJsonTypes.res b/src/VJsonTypes.res new file mode 100644 index 0000000..8e8458c --- /dev/null +++ b/src/VJsonTypes.res @@ -0,0 +1,10 @@ +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/VJsonTypes.resi b/src/VJsonTypes.resi new file mode 100644 index 0000000..8e8458c --- /dev/null +++ b/src/VJsonTypes.resi @@ -0,0 +1,10 @@ +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/yarn.lock b/yarn.lock index 849a160..cb9f789 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1790,10 +1790,10 @@ bs-bastet@adnelson/bastet#no-bisect: dependencies: bs-stdlib-shims "^0.1.0" -bs-platform@^8.4.2: - version "8.4.2" - resolved "https://registry.yarnpkg.com/bs-platform/-/bs-platform-8.4.2.tgz#778dabd1dfb3bc95e0086c58dabae74e4ebdee8a" - integrity sha512-9q7S4/LLV/a68CweN382NJdCCr/lOSsJR3oQYnmPK98ChfO/AdiA3lYQkQTp6T+U0I5Z5RypUAUprNstwDtMDQ== +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" From a12ce8c171b96308fc604de4a089e85ed80c3a00 Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Mon, 19 Feb 2024 13:35:14 -0600 Subject: [PATCH 02/15] trying to get a minimum failure case for this stack overflow --- __tests__/VJsonParserTests.re | 231 --------------------------------- __tests__/VJsonParserTests.res | 208 +++++++++++++++++++++++++++++ __tests__/VJsonTests.re | 160 ----------------------- __tests__/VJsonTests.res | 128 ++++++++++++++++++ 4 files changed, 336 insertions(+), 391 deletions(-) delete mode 100644 __tests__/VJsonParserTests.re create mode 100644 __tests__/VJsonParserTests.res delete mode 100644 __tests__/VJsonTests.re create mode 100644 __tests__/VJsonTests.res diff --git a/__tests__/VJsonParserTests.re b/__tests__/VJsonParserTests.re deleted file mode 100644 index 9ec82a3..0000000 --- a/__tests__/VJsonParserTests.re +++ /dev/null @@ -1,231 +0,0 @@ -open Jest; -open Expect; -open VJsonTypes; - -let expectParse = (input, handler) => { - switch (input |> VJson.(parseWith(defaultParseVariable)), 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) - | (Ok(vj), Error(_)) => - failwith( - "Expected an Error result, but successfully parsed: " - ++ Json.stringify(Obj.magic(vj)), - ) - }; -}; - -// Checks that parsing is successful and the output matches the expected. -let expectOkParse = (input, output) => - input->expectParse(Ok(vj => expect(vj)->toEqual(output))); - -// Given checks to run on the error message, checks that parse fails in -// the given manner. -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(_ => ()); - -module ParserTests = { - 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", (ParseError(m)) => - expect(m)->toEqual(stringContaining("]")) - ) - ); - test("bad array (no comma)", () => - expectSomeParseFail("[ 1 true ]") - ); - }); - - describe("known failure cases", () => { - test("this is throwing an exception for some reason", () => { - let rawText = "{\n \"fields\": {\n \"Name\": {{name}},\n \"Link\": {{linkUrl},\n \"Monthly Rent\": {{monthlyRent}},\n \"Square Feet\": {{squareFeet}},\n \"My Notes\": {{notes}}\n }\n}"; - - expectSomeParseFail(rawText); - }) - }); - - describe("bools", () => { - test("true", () => - expectOkParse("true", Bool(true)) - ); - test("false", () => - expectOkParse("false", Bool(false)) - ); - test("with trailing whitespace", () => - expectOkParse("false ", Bool(false)) - ); - test("with preceding whitespace", () => - expectOkParse(" true ", Bool(true)) - ); - }); - - describe("numbers", () => { - test("integer", () => - expectOkParse("123", Number(123.0)) - ); - test("float", () => - expectOkParse("123.456", Number(123.456)) - ); - test("negative float", () => - expectOkParse("-123.456", Number(-123.456)) - ); - test("float without leading digits", () => - expectOkParse(".456", Number(0.456)) - ); - test("float in scientific notation", () => - expectOkParse("+1.2e5", Number(120000.0)) - ); - Skip.test("float without trailing digits", () => - expectOkParse("123.", Number(123.0)) - ); - }); - - describe("strings", () => { - test("simple", () => - expectOkParse("\"hello!\"", String("hello!")) - ); - test("with internal quotes", () => - expectOkParse("\"hello \\\"world\\\"!\"", String("hello \"world\"!")) - ); - test("unicode", () => - expectOkParse( - "\"世界こんにちは!\"", - String("世界こんにちは!"), - ) - ); - }); - - describe("variables", () => { - test("simple", () => - expectOkParse("{{myVar}}", Variable("myVar")) - ); - 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", () => { - test("empty", () => - expectOkParse("[]", Array([||])) - ); - test("empty with whitespace", () => - expectOkParse("[ ]", Array([||])) - ); - test("numbers (single)", () => - expectOkParse("[1]", Array([|Number(1.0)|])) - ); - test("numbers (more)", () => - expectOkParse( - "[1, 2.345, 6]", - Array([|Number(1.0), Number(2.345), Number(6.0)|]), - ) - ); - test("nulls", () => - expectOkParse("[null, null]", Array([|Null, Null|])) - ); - test("bools", () => - expectOkParse( - "[true, true, false]", - Array([|Bool(true), Bool(true), Bool(false)|]), - ) - ); - test("variables", () => - expectOkParse( - "[{{x}}, {{coolVar}}, {{yo_123}}]", - Array( - [|"x", "coolVar", "yo_123"|] - ->Belt.Array.map(s => VJsonTypes.Variable(s)), - ), - ) - ); - test("trailing whitespace", () => - expectOkParse("[null, null] ", Array([|Null, Null|])) - ); - }); - - describe("object", () => { - test("empty", () => - expectOkParse("{}", Object(JsMap.empty())) - ); - test("empty with whitespace", () => - expectOkParse("{ }", Object(JsMap.empty())) - ); - test("single key", () => - expectOkParse( - "{\"x\": 1}", - Object([|("x", Number(1.0))|]->JsMap.fromArray), - ) - ); - test("multiple keys", () => - expectOkParse( - "{\"x\": 1, \"YYY\": \"hey there!\"}", - Object( - [|("x", Number(1.0)), ("YYY", String("hey there!"))|] - ->JsMap.fromArray, - ), - ) - ); - test("extra whitespace", () => - expectOkParse( - " { \"x\" : 1 } ", - Object([|("x", Number(1.0))|]->JsMap.fromArray), - ) - ); - test("nested", () => - expectOkParse( - "{\"x\": 1, \"YYY\": { \"z\": {{myZVariable}}, \"blib\": [123, \"hey there!\"]}}", - Object( - [| - ("x", Number(1.0)), - ( - "YYY", - Object( - [| - ("z", Variable("myZVariable")), - ("blib", Array([|Number(123.0), String("hey there!")|])), - |] - ->JsMap.fromArray, - ), - ), - |] - ->JsMap.fromArray, - ), - ) - ); - }); -}; diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res new file mode 100644 index 0000000..f93d8e9 --- /dev/null +++ b/__tests__/VJsonParserTests.res @@ -0,0 +1,208 @@ +open Jest +open Expect +open VJsonTypes + +let expectParse = (input, handler) => + switch (input |> VJson.parseWith(VJson.defaultParseVariable), handler) { + | (Ok(vj), Ok(checks)) => vj->checks + | (Error(err), Error(checks)) => { Js.log2("error", err); err->checks} + | (Error(ReludeParse.Parser.ParseError.ParseError(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))) + } + +// Checks that parsing is successful and the output matches the expected. +let expectOkParse = (input, output) => input->expectParse(Ok(vj => expect(vj)->toEqual(output))) + +// Given checks to run on the error message, checks that parse fails in +// the given manner. +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(_ => ()) + +module ParserTests = { + 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", (ParseError(m)) => expect(m)->toEqual(stringContaining("]"))) + ) + test("bad array (no comma)", () => expectSomeParseFail("[ 1 true ]")) + }) + + describe("known failure cases", () => { + test("unmatched curly braces on a variable", () => { + let rawText = `{ + "fields": { + "Name": {{name}}, + "Link": {{linkUrl}, + "Monthly Rent": {{monthlyRent}}, + "Square Feet": {{squareFeet}}, + "My Notes": {{notes}} + } + }` + + expectSomeParseFail(rawText) + }) + + test("infinite loop guy", () => { + let rawText = `{"query": " + ... on ComplexProductView { + options { + id + title + required + values { + id + title + } + } + priceRange { + maximum { + final { + amount { + value + currency + } + } + regular { + amount { + value + currency + } + } + } + minimum { + final { + amount { + value + currency + } + } + regular { + amount { + value + currency + } + } + } + } + } + } + } + } +} +", +"variables": +{ + "searchTerm": "bag", + "sortBy" : {{sortBy}}, +} +}` + + expectSomeParseFail(rawText) + }) + }) + + describe("bools", () => { + test("true", () => expectOkParse("true", Bool(true))) + test("false", () => expectOkParse("false", Bool(false))) + test("with trailing whitespace", () => expectOkParse("false ", Bool(false))) + test("with preceding whitespace", () => expectOkParse(" true ", Bool(true))) + }) + + describe("numbers", () => { + test("integer", () => expectOkParse("123", Number(123.0))) + test("float", () => expectOkParse("123.456", Number(123.456))) + test("negative float", () => expectOkParse("-123.456", Number(-123.456))) + test("float without leading digits", () => expectOkParse(".456", Number(0.456))) + test("float in scientific notation", () => expectOkParse("+1.2e5", Number(120000.0))) + Skip.test("float without trailing digits", () => expectOkParse("123.", Number(123.0))) + }) + + describe("strings", () => { + test("simple", () => expectOkParse("\"hello!\"", String("hello!"))) + test("with internal quotes", () => + expectOkParse("\"hello \\\"world\\\"!\"", String("hello \"world\"!")) + ) + test("unicode", () => + expectOkParse("\"世界こんにちは!\"", String("世界こんにちは!")) + ) + }) + + describe("variables", () => { + test("simple", () => expectOkParse("{{myVar}}", Variable("myVar"))) + 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", () => { + test("empty", () => expectOkParse("[]", Array([]))) + test("empty with whitespace", () => expectOkParse("[ ]", Array([]))) + test("numbers (single)", () => expectOkParse("[1]", Array([Number(1.0)]))) + test("numbers (more)", () => + expectOkParse("[1, 2.345, 6]", Array([Number(1.0), Number(2.345), Number(6.0)])) + ) + test("nulls", () => expectOkParse("[null, null]", Array([Null, Null]))) + test("bools", () => + expectOkParse("[true, true, false]", Array([Bool(true), Bool(true), Bool(false)])) + ) + test("variables", () => + expectOkParse( + "[{{x}}, {{coolVar}}, {{yo_123}}]", + Array(["x", "coolVar", "yo_123"]->Belt.Array.map(s => VJsonTypes.Variable(s))), + ) + ) + test("trailing whitespace", () => expectOkParse("[null, null] ", Array([Null, Null]))) + }) + + describe("object", () => { + test("empty", () => expectOkParse("{}", Object(JsMap.empty()))) + test("empty with whitespace", () => expectOkParse("{ }", Object(JsMap.empty()))) + test("single key", () => + expectOkParse("{\"x\": 1}", Object([("x", Number(1.0))]->JsMap.fromArray)) + ) + test("multiple keys", () => + expectOkParse( + "{\"x\": 1, \"YYY\": \"hey there!\"}", + Object([("x", Number(1.0)), ("YYY", String("hey there!"))]->JsMap.fromArray), + ) + ) + test("extra whitespace", () => + expectOkParse(" { \"x\" : 1 } ", Object([("x", Number(1.0))]->JsMap.fromArray)) + ) + test("nested", () => + expectOkParse( + "{\"x\": 1, \"YYY\": { \"z\": {{myZVariable}}, \"blib\": [123, \"hey there!\"]}}", + Object( + [ + ("x", Number(1.0)), + ( + "YYY", + Object( + [ + ("z", Variable("myZVariable")), + ("blib", Array([Number(123.0), String("hey there!")])), + ]->JsMap.fromArray, + ), + ), + ]->JsMap.fromArray, + ), + ) + ) + }) +} diff --git a/__tests__/VJsonTests.re b/__tests__/VJsonTests.re deleted file mode 100644 index dde4bdf..0000000 --- a/__tests__/VJsonTests.re +++ /dev/null @@ -1,160 +0,0 @@ -open Jest; -open Expect; -open VJson; - -let exampleJson: Js.Json.t = [%raw - {|{x: 1, y: 2, z: [1, 2, "hello"], q: { x: [false, true], y: null }}|} -]; - -let example = - VJson.( - parseWith( - defaultParseVariable, - "{ - \"id\": {{id}}, - \"color\": {{color}}, - \"size\": {{size}}, - }", - ) - ) - ->Belt.Result.getExn; - -module FindVariablesTests = { - describe("findVariables", () => { - test("it finds variables", () => { - expect(example->findVariables)->toEqual([|"id", "color", "size"|]) - }) - }); -}; - -module MapTests = { - describe("map a function over VJson", () => { - test("complex with variables", () => { - open VJson.Builder; - let vj = - object_([| - ("x", variable(1234)), - ("z", vjsonArray([|float(1.0), number(2.0), variable(4321)|])), - |]); - expect(vj->VJson.map(string_of_int)) - ->toEqual( - object_([| - ("x", variable("1234")), - ( - "z", - vjsonArray([|float(1.0), number(2.0), variable("4321")|]), - ), - |]), - ); - }) - }); -}; - -module SerializeTests = { - let expectSerialize = (vj: vjson(string), str) => { - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s); - expect(serialized)->toEqual(str); - }; - test("simple values", () => { - expectSerialize(Bool(true), "true"); - expectSerialize(String("hello"), "\"hello\""); - expectSerialize(Variable("varvara"), "{{varvara}}"); - }); - - test("complex json", () => - expect(exampleJson->VJson.Builder.json->VJson.serialize(s => s)) - ->toMatchSnapshot() - ); - - test("complex with variables", () => { - let vj = - VJson.Builder.( - object_([| - ("x", float(1.0)), - ("y", int(2)), - ( - "z", - vjsonArray([|float(1.0), number(2.0), variable("vivvy")|]), - ), - ( - "q", - object_([| - ("x", Array([|Bool(false), "oogabooga"->variable|])), - ("y", variable("yo")), - |]), - ), - |]) - ); - expect(vj->VJson.serialize(s => s))->toMatchSnapshot(); - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s); - expect(serialized |> VJson.parseDefaultExn)->toEqual(vj); - }); - - test("serializing VJson without variables produces valid json", () => { - let vj = - VJson.Builder.( - object_([| - ("x", float(1.0)), - ("y", int(2)), - ("z", vjsonArray([|float(1.0), number(2.0)|])), - ("q", object_([|("x", Array([|Bool(false)|]))|])), - |]) - ); - let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s); - expect(serialized |> Js.Json.parseExn |> Obj.magic) - ->toEqual({ - "x": 1.0, - "y": 2, - "z": [|1.0, 2.0|], - "q": { - "x": [|false|], - }, - }); - }); -}; - -module FromJsonTests = { - describe("fromJson", () => { - test("nested object", () => - expect(exampleJson->VJson.Builder.json) - ->toEqual( - Object( - VJson.Builder.( - [| - ("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), - |]), - ), - |] - ) - ->JsMap.fromArray, - ), - ) - ) - }); -}; - -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 - }; - - expect(example |> toJson(variableToJson)) - ->toEqual([%raw {|{id: 123, size: null, color: "pink"}|}]); - }); -}; diff --git a/__tests__/VJsonTests.res b/__tests__/VJsonTests.res new file mode 100644 index 0000000..1310aba --- /dev/null +++ b/__tests__/VJsonTests.res @@ -0,0 +1,128 @@ +open Jest +open Expect +open VJson + +let exampleJson: Js.Json.t = %raw(`{x: 1, y: 2, z: [1, 2, "hello"], q: { x: [false, true], y: null }}`) + +let example = { + open VJson + parseWith( + defaultParseVariable, + "{ + \"id\": {{id}}, + \"color\": {{color}}, + \"size\": {{size}}, + }", + ) +}->Belt.Result.getExn + +module FindVariablesTests = { + describe("findVariables", () => + test("it finds variables", () => + expect(example->findVariables)->toEqual(["id", "color", "size"]) + ) + ) +} + +module MapTests = { + describe("map a function over VJson", () => + test("complex with variables", () => { + open VJson.Builder + let vj = object_([ + ("x", variable(1234)), + ("z", vjsonArray([float(1.0), number(2.0), variable(4321)])), + ]) + expect(vj->VJson.map(string_of_int))->toEqual( + object_([ + ("x", variable("1234")), + ("z", vjsonArray([float(1.0), number(2.0), variable("4321")])), + ]), + ) + }) + ) +} + +module SerializeTests = { + let expectSerialize = (vj: vjson, str) => { + let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) + expect(serialized)->toEqual(str) + } + test("simple values", () => { + expectSerialize(Bool(true), "true") + expectSerialize(String("hello"), "\"hello\"") + expectSerialize(Variable("varvara"), "{{varvara}}") + }) + + test("complex json", () => + expect(exampleJson->VJson.Builder.json->VJson.serialize(s => s))->toMatchSnapshot() + ) + + test("complex with variables", () => { + let vj = { + open VJson.Builder + object_([ + ("x", float(1.0)), + ("y", int(2)), + ("z", vjsonArray([float(1.0), number(2.0), variable("vivvy")])), + ("q", object_([("x", Array([Bool(false), "oogabooga"->variable])), ("y", variable("yo"))])), + ]) + } + expect(vj->VJson.serialize(s => s))->toMatchSnapshot() + let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) + expect(serialized |> VJson.parseDefaultExn)->toEqual(vj) + }) + + test("serializing VJson without variables produces valid json", () => { + let vj = { + open VJson.Builder + object_([ + ("x", float(1.0)), + ("y", int(2)), + ("z", vjsonArray([float(1.0), number(2.0)])), + ("q", object_([("x", Array([Bool(false)]))])), + ]) + } + let serialized: string = vj |> VJson.serializeWrapCurlyBraces(s => s) + expect(serialized |> Js.Json.parseExn |> Obj.magic)->toEqual({ + "x": 1.0, + "y": 2, + "z": [1.0, 2.0], + "q": { + "x": [false], + }, + }) + }) +} + +module FromJsonTests = { + describe("fromJson", () => + test("nested object", () => + expect(exampleJson->VJson.Builder.json)->toEqual( + Object( + { + open VJson.Builder + [ + ("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)])), + ] + }->JsMap.fromArray, + ), + ) + ) + ) +} + +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 + } + + expect(example |> toJson(variableToJson))->toEqual(%raw(`{id: 123, size: null, color: "pink"}`)) + }) +} From f4cf76bdd39bfe37930b368630fabc28a93a63a4 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Tue, 20 Feb 2024 08:35:21 +0200 Subject: [PATCH 03/15] Update query to run into maxmum call stack exceeded --- __tests__/VJsonParserTests.res | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index f93d8e9..cae283b 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -49,6 +49,10 @@ module ParserTests = { test("infinite loop guy", () => { let rawText = `{"query": " +query GetProductsBySearch($searchTerm: String!, $sortBy: String!) { + productSearch() { + items { + productView { ... on ComplexProductView { options { id From d4b790cad7ff91aba001525323176075b9b4bca7 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Tue, 20 Feb 2024 09:29:04 +0200 Subject: [PATCH 04/15] Replace test case with same length random string --- __tests__/VJsonParserTests.res | 63 ++++------------------------------ 1 file changed, 7 insertions(+), 56 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index cae283b..ec5ca52 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -48,63 +48,14 @@ module ParserTests = { }) test("infinite loop guy", () => { - let rawText = `{"query": " -query GetProductsBySearch($searchTerm: String!, $sortBy: String!) { - productSearch() { - items { - productView { - ... on ComplexProductView { - options { - id - title - required - values { - id - title - } - } - priceRange { - maximum { - final { - amount { - value - currency - } - } - regular { - amount { - value - currency - } - } - } - minimum { - final { - amount { - value - currency - } - } - regular { - amount { - value - currency - } - } - } - } + let rawText = ` + { + "query": "pzqtdxvnhzvmzzigsojovukwfssmzadolomslufahgjbuininzcexwrkvnncmktcqpdcsrcnerdjrwgcncswsoovrrplikznwfqbvmrkeequwohejmrdmwmfcdkkfwhmqqiqgpfpelmhutdpshonwbtkxmwfoccaqogmzulumtcywyplotpbsldrampwwtmjwsmfhnnzumyhpyforahmivaalkhrenxxvnhuwpovjnkdjbrxvhobpmffinjtuaegkqejfxfejiatxkpxvxboftretjleyxysfwlkiyjfdnvhjdtsopwuvznzpzrzvntiixdqifzsiktwhvgimwpgsgxrbczqhnycsalycgcngilyjwlxkhmaieffcpfptwzqffrlwpksmjbndftkjkjcnwqgbfwoucguujltbcfkerbwrsoeofdmmawmzlegojrujqbkftagqarwbvrlmsgwyxhpcxrdyynjrbltvrtiruhbsmroovmgqfgvogrjshjbhzgucsmavrnyuxqwaqpjhlrqargmuoixwnorvepyvtybqbjrjzyafgzwxedpyezprhtbfzrcmpysfirgemwihpzlmciehdlolhrszfqnserejqqwsazshizvyuuagitooleocilvlfmoriwzpudhqdcngayfyyptuggzloyamzxtrekqeegawxjddprqkrwepeynqifacltgbxsmpqinpaegwkuvbawuuimculerazmttvvqptbyjmnsxrpiwtkqitsoljjqgfsghckooyzdqxeagckeqmmnkpmxqsgvqvbuhlwzgumrslhyvtegfaosnbfgmqdpbkgipiequsjgyopmbiwhvuryzknwayxedhiimqqnddlzgaxbklsvwjigjqltzitphlzguzgljzvylilltqystjnbyafopkanphhuntwbffnslweajomjvgzpkcjznwpnyliymimrfvcdcbcjgwmjdpmlfjiqhizk", + "variables": { + "searchTerm": "bag", + "sortBy" : {{sortBy}}, } - } - } - } -} -", -"variables": -{ - "searchTerm": "bag", - "sortBy" : {{sortBy}}, -} -}` + }` expectSomeParseFail(rawText) }) From 3e5bd8f2e5b8005b81e00b7f31c1e18a8e0545d7 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Tue, 20 Feb 2024 20:37:16 +0200 Subject: [PATCH 05/15] Updated string parsing to use regex --- __tests__/VJsonParserTests.res | 26 ++++++++++++-------------- src/VJsonParse.res | 5 ++--- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index ec5ca52..7f8a8ac 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -43,20 +43,6 @@ module ParserTests = { "My Notes": {{notes}} } }` - - expectSomeParseFail(rawText) - }) - - test("infinite loop guy", () => { - let rawText = ` - { - "query": "pzqtdxvnhzvmzzigsojovukwfssmzadolomslufahgjbuininzcexwrkvnncmktcqpdcsrcnerdjrwgcncswsoovrrplikznwfqbvmrkeequwohejmrdmwmfcdkkfwhmqqiqgpfpelmhutdpshonwbtkxmwfoccaqogmzulumtcywyplotpbsldrampwwtmjwsmfhnnzumyhpyforahmivaalkhrenxxvnhuwpovjnkdjbrxvhobpmffinjtuaegkqejfxfejiatxkpxvxboftretjleyxysfwlkiyjfdnvhjdtsopwuvznzpzrzvntiixdqifzsiktwhvgimwpgsgxrbczqhnycsalycgcngilyjwlxkhmaieffcpfptwzqffrlwpksmjbndftkjkjcnwqgbfwoucguujltbcfkerbwrsoeofdmmawmzlegojrujqbkftagqarwbvrlmsgwyxhpcxrdyynjrbltvrtiruhbsmroovmgqfgvogrjshjbhzgucsmavrnyuxqwaqpjhlrqargmuoixwnorvepyvtybqbjrjzyafgzwxedpyezprhtbfzrcmpysfirgemwihpzlmciehdlolhrszfqnserejqqwsazshizvyuuagitooleocilvlfmoriwzpudhqdcngayfyyptuggzloyamzxtrekqeegawxjddprqkrwepeynqifacltgbxsmpqinpaegwkuvbawuuimculerazmttvvqptbyjmnsxrpiwtkqitsoljjqgfsghckooyzdqxeagckeqmmnkpmxqsgvqvbuhlwzgumrslhyvtegfaosnbfgmqdpbkgipiequsjgyopmbiwhvuryzknwayxedhiimqqnddlzgaxbklsvwjigjqltzitphlzguzgljzvylilltqystjnbyafopkanphhuntwbffnslweajomjvgzpkcjznwpnyliymimrfvcdcbcjgwmjdpmlfjiqhizk", - "variables": { - "searchTerm": "bag", - "sortBy" : {{sortBy}}, - } - }` - expectSomeParseFail(rawText) }) }) @@ -159,5 +145,17 @@ module ParserTests = { ), ) ) + test("long string value", () => { + let longValue = "pzqtdxvnhzvmzzigsojovukwfssmzadolomslufahgjbuininzcexwrkvnncmktcqpdcsrcnerdjrwgcncswsoovrrplikznwfqbvmrkeequwohejmrdmwmfcdkkfwhmqqiqgpfpelmhutdpshonwbtkxmwfoccaqogmzulumtcywyplotpbsldrampwwtmjwsmfhnnzumyhpyforahmivaalkhrenxxvnhuwpovjnkdjbrxvhobpmffinjtuaegkqejfxfejiatxkpxvxboftretjleyxysfwlkiyjfdnvhjdtsopwuvznzpzrzvntiixdqifzsiktwhvgimwpgsgxrbczqhnycsalycgcngilyjwlxkhmaieffcpfptwzqffrlwpksmjbndftkjkjcnwqgbfwoucguujltbcfkerbwrsoeofdmmawmzlegojrujqbkftagqarwbvrlmsgwyxhpcxrdyynjrbltvrtiruhbsmroovmgqfgvogrjshjbhzgucsmavrnyuxqwaqpjhlrqargmuoixwnorvepyvtybqbjrjzyafgzwxedpyezprhtbfzrcmpysfirgemwihpzlmciehdlolhrszfqnserejqqwsazshizvyuuagitooleocilvlfmoriwzpudhqdcngayfyyptuggzloyamzxtrekqeegawxjddprqkrwepeynqifacltgbxsmpqinpaegwkuvbawuuimculerazmttvvqptbyjmnsxrpiwtkqitsoljjqgfsghckooyzdqxeagckeqmmnkpmxqsgvqvbuhlwzgumrslhyvtegfaosnbfgmqdpbkgipiequsjgyopmbiwhvuryzknwayxedhiimqqnddlzgaxbklsvwjigjqltzitphlzguzgljzvylilltqystjnbyafopkanphhuntwbffnslweajomjvgzpkcjznwpnyliymimrfvcdcbcjgwmjdpmlfjiqhizk" + let rawText = ` + { + "query": "${longValue}", + "variables": { + "searchTerm": "bag", + "sortBy" : {{sortBy}}, + } + }` + expectOkParse(rawText, Object([("variables", Object([("searchTerm", String("bag")), ("sortBy", Variable("sortBy"))]->JsMap.fromArray)),("query", String(longValue))]->JsMap.fromArray)) + }) }) } diff --git a/src/VJsonParse.res b/src/VJsonParse.res index 7b29a8d..bd36ca0 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -25,14 +25,13 @@ let parseVJsonWithVariable = parseVariableString => { let parseBool: t = \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme + let nonQuoteCharacterRegex = %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. let parseString: t = betweenDoubleQuotes( - many(\"<|>"(str("\\\"") |> map(_ => "\""), anyCharNotIn(list{"\""}))) |> map(l => - l->Belt.List.toArray->Js.Array2.joinWith("") - ), + regex(nonQuoteCharacterRegex) ) |> lexeme // Parse a variable wrapped in a pair of doubled curly braces `{{ }}`. The From aec47ae914648bd574f241660ac09cb51e22b049 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:29:22 +0200 Subject: [PATCH 06/15] Copy over format rescript script --- package.json | 3 +++ scripts/format-rescript.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 scripts/format-rescript.js diff --git a/package.json b/package.json index 9362e30..601bd05 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,9 @@ "*.{re,rei}": [ "bsrefmt --in-place" ], + "*.res": [ + "node ./scripts/format-rescript.js" + ], "*.{json,yaml}": [ "prettier --write" ] diff --git a/scripts/format-rescript.js b/scripts/format-rescript.js new file mode 100644 index 0000000..20d13f7 --- /dev/null +++ b/scripts/format-rescript.js @@ -0,0 +1,38 @@ +/* eslint-disable no-console */ +const { basename } = require("path"); +const { readFileSync, writeFileSync, lstatSync } = require("fs"); +const { execSync } = require("child_process"); +const fileNames = process.argv.slice(2); + +if (!fileNames.length) { + throw new Error(`Usage: ${basename(__filename)} [...]`); +} + +let hasError = false; + +fileNames.forEach(fileName => { + if (!fileName.endsWith(".res")) { + throw new Error(`Not a rescript file: ${fileName}`); + } + + if (lstatSync(fileName).isSymbolicLink()) { + console.log(`Ignoring symlink ${fileName}`); + return; + } + + const contents = readFileSync(fileName).toString(); + + try { + const formattedCode = execSync(`node_modules/.bin/bsc -format ${fileName}`); + if (formattedCode !== contents) { + writeFileSync(fileName, formattedCode); + console.log("Formatted", fileName); + } + } catch { + hasError = true; + } +}); + +if (hasError) { + process.exit(1); +} From 916839c8c9343cda42e85964ea1739c7738109d0 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:30:40 +0200 Subject: [PATCH 07/15] Finalize new way of parsing strings --- src/VJsonParse.res | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/VJsonParse.res b/src/VJsonParse.res index bd36ca0..f8d68d1 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -25,14 +25,19 @@ let parseVJsonWithVariable = parseVariableString => { let parseBool: t = \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme - let nonQuoteCharacterRegex = %re(`/[^"]*/`) + let escapedQuoteRegex = %re(`/\\\\"/gm`) + let inQuotes = %re(`/"(?:[^"\\\\]|\\\\.)*"/gm`) // 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 = - betweenDoubleQuotes( - regex(nonQuoteCharacterRegex) - ) |> lexeme + regex(inQuotes) + |> map(match => { + match + ->Js.String2.substring(~from=1, ~to_=match->Js.String2.length - 1) // Remove quotes from start and end + ->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`. From 84fdd9daa26ae592ed99c4c37b8dc17b5f827e12 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:37:11 +0200 Subject: [PATCH 08/15] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 601bd05..8ea611b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "0.9.0", + "version": "1.0.0", "declaration": true, "main": "dist/VJson.bs", "types": "src/index.d.ts", From f3d3eed039baaabd6f1c15bee7ff4a9af7e27caa Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:47:14 +0200 Subject: [PATCH 09/15] Fix typo --- src/VJsonParse.res | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/VJsonParse.res b/src/VJsonParse.res index f8d68d1..a0bf993 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -26,12 +26,12 @@ let parseVJsonWithVariable = parseVariableString => { \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme let escapedQuoteRegex = %re(`/\\\\"/gm`) - let inQuotes = %re(`/"(?:[^"\\\\]|\\\\.)*"/gm`) + let inQuotesRegex = %re(`/"(?:[^"\\\\]|\\\\.)*"/gm`) // 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(inQuotes) + regex(inQuotesRegex) |> map(match => { match ->Js.String2.substring(~from=1, ~to_=match->Js.String2.length - 1) // Remove quotes from start and end From 20648b67b08366963ced605474707ee4a7fe1941 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:54:11 +0200 Subject: [PATCH 10/15] include res files in npm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8ea611b..8944fa8 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ ], "files": [ "dist/VJson.bs.js", - "src/*.re", + "src/*.res", "bsconfig.json" ], "author": "Allen Nelson ", From 4e8ef757306a8e98be79976670a6a6291b000a41 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Wed, 21 Feb 2024 19:54:26 +0200 Subject: [PATCH 11/15] 1.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8944fa8..55d5b80 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "1.0.0", + "version": "1.0.1", "declaration": true, "main": "dist/VJson.bs", "types": "src/index.d.ts", From 1ecbe6b145e415daf2ecd23f60eab18f863461ef Mon Sep 17 00:00:00 2001 From: Allen Nelson Date: Wed, 21 Feb 2024 19:24:08 -0600 Subject: [PATCH 12/15] remove console log --- __tests__/VJsonParserTests.res | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index 7f8a8ac..11e0463 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -5,7 +5,7 @@ open VJsonTypes let expectParse = (input, handler) => switch (input |> VJson.parseWith(VJson.defaultParseVariable), handler) { | (Ok(vj), Ok(checks)) => vj->checks - | (Error(err), Error(checks)) => { Js.log2("error", err); err->checks} + | (Error(err), Error(checks)) => err->checks | (Error(ReludeParse.Parser.ParseError.ParseError(message)), Ok(_)) => failwith("Expected an Ok result, but got error: " ++ message) | (Ok(vj), Error(_)) => @@ -155,7 +155,20 @@ module ParserTests = { "sortBy" : {{sortBy}}, } }` - expectOkParse(rawText, Object([("variables", Object([("searchTerm", String("bag")), ("sortBy", Variable("sortBy"))]->JsMap.fromArray)),("query", String(longValue))]->JsMap.fromArray)) + expectOkParse( + rawText, + Object( + [ + ( + "variables", + Object( + [("searchTerm", String("bag")), ("sortBy", Variable("sortBy"))]->JsMap.fromArray, + ), + ), + ("query", String(longValue)), + ]->JsMap.fromArray, + ), + ) }) }) } From df4ceda9e060494e525749eae5ea3b847c3a1c66 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Thu, 22 Feb 2024 10:07:15 +0200 Subject: [PATCH 13/15] Use regex for removing quotes --- src/VJsonParse.res | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/VJsonParse.res b/src/VJsonParse.res index a0bf993..6aa9779 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -26,18 +26,19 @@ let parseVJsonWithVariable = parseVariableString => { \"<|>"(str("true") |> map(_ => true), str("false") |> map(_ => false)) |> lexeme let escapedQuoteRegex = %re(`/\\\\"/gm`) + let nonEsacapedQuoteRegex = %re(`/(? = - regex(inQuotesRegex) - |> map(match => { - match - ->Js.String2.substring(~from=1, ~to_=match->Js.String2.length - 1) // Remove quotes from start and end - ->Js.String2.replaceByRe(escapedQuoteRegex, `"`) - }) - |> lexeme + let parseString: t = regex(inQuotesRegex) + |> tapLog + |> map(match => { + match + ->Js.String2.replaceByRe(nonEsacapedQuoteRegex, ``) // Remove quotes from start and end + ->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`. From c2a69792765fba7288b64e64263d88d6861ac7d8 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Thu, 22 Feb 2024 10:16:42 +0200 Subject: [PATCH 14/15] Fix failure on certain conditions and add test case --- __tests__/VJsonParserTests.res | 3 +++ src/VJsonParse.res | 18 +++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/__tests__/VJsonParserTests.res b/__tests__/VJsonParserTests.res index 11e0463..e327abe 100644 --- a/__tests__/VJsonParserTests.res +++ b/__tests__/VJsonParserTests.res @@ -170,5 +170,8 @@ module ParserTests = { ), ) }) + test("extra line", () => + expectOkParse("{\n\"id\": 5}", Object([("id", Number(5.0))]->JsMap.fromArray)) + ) }) } diff --git a/src/VJsonParse.res b/src/VJsonParse.res index 6aa9779..25bacc7 100644 --- a/src/VJsonParse.res +++ b/src/VJsonParse.res @@ -27,18 +27,18 @@ let parseVJsonWithVariable = parseVariableString => { let escapedQuoteRegex = %re(`/\\\\"/gm`) let nonEsacapedQuoteRegex = %re(`/(? = regex(inQuotesRegex) - |> tapLog - |> map(match => { - match - ->Js.String2.replaceByRe(nonEsacapedQuoteRegex, ``) // Remove quotes from start and end - ->Js.String2.replaceByRe(escapedQuoteRegex, `"`) - }) - |> lexeme + let parseString: t = + regex(inQuotesRegex) + |> map(match => { + match + ->Js.String2.replaceByRe(nonEsacapedQuoteRegex, ``) + ->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`. From df363a37ac570461e45c1c4d883a7a9ee0d93a02 Mon Sep 17 00:00:00 2001 From: Youssef Henna Date: Thu, 22 Feb 2024 10:17:10 +0200 Subject: [PATCH 15/15] Version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 55d5b80..e62eac9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@draftbit/variable-json", "private": false, - "version": "1.0.1", + "version": "1.0.2", "declaration": true, "main": "dist/VJson.bs", "types": "src/index.d.ts",