From 7a84aba9fbc63ba8447f1b3512e9ffa871015cb5 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Thu, 6 Sep 2018 12:27:42 -0600 Subject: [PATCH 1/3] Add `serializeAnyToJson` and `unserializeAnyFromJsonUnsafe` See docs for more info. This also retains debug information if bsc was with with `-bs-g`. ``` [anyToJson value] turns any [value] into a JSON object This will throw an error if there are any functions anywhere in the value. [unsafeAnyFromJson json] converts a serialized JSON object back into the bucklescript runtime value. Warning: marshaling is currently not type-safe. The type of marshaled data is not transmitted along the value of the data, making it impossible to check that the data read back possesses the type expected by the context. The return type of this function is given as 'a, but this is misleading: the returned OCaml value does not possess type 'a for all 'a; it has one, unique type which cannot be determined at compile-time. The programmer should explicitly give the expected type of the returned value, using the following syntax: (Js.Json.unserializeAnyFromJsonUnsafe json : type) Anything can happen at run-time if the object does not correspond to the assumed type. ``` Test Plan: ``` make libs make -C jscomp/tests all ./node_modules/.bin/mocha jscomp/test/js_json_test.js ``` --- jscomp/others/js_json.ml | 81 ++++++++++++++++++++++++++++ jscomp/others/js_json.mli | 21 ++++++++ jscomp/test/ext_filename_test.js | 3 +- jscomp/test/js_json_test.js | 57 ++++++++++++++++++++ jscomp/test/js_json_test.ml | 12 +++++ jscomp/test/ocaml_typedtree_test.js | 6 +-- lib/js/js_json.js | 82 +++++++++++++++++++++++++++++ 7 files changed, 257 insertions(+), 5 deletions(-) diff --git a/jscomp/others/js_json.ml b/jscomp/others/js_json.ml index 4a348a43d6..092773e0f0 100644 --- a/jscomp/others/js_json.ml +++ b/jscomp/others/js_json.ml @@ -129,3 +129,84 @@ external stringify: t -> string = "stringify" [@@bs.val] [@@bs.scope "JSON"] external stringifyWithSpace: t -> (_ [@bs.as {json|null|json}]) -> int -> string = "stringify" [@@bs.val] [@@bs.scope "JSON"] + +type preJson +let _toJson: preJson -> t = fun%raw data -> {| + if (!data) { + return data + } else if (typeof data === 'function') { + throw new Error("Cannot serialize a function") + } else if (Array.isArray(data)) { + if (data.tag != null) { + return { + $$tag: data.tag, + $$contents: data.map(_toJson), + $$bsVariant: data[Symbol.for('BsVariant')], + } + } else if (data[Symbol.for('BsVariant')] != null) { + return { + $$bsVariant: data[Symbol.for('BsVariant')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsLocalModule')] != null) { + return { + $$bsLocalModule: data[Symbol.for('BsLocalModule')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsPolyVar')] != null) { + return { + $$bsPolyVar: data[Symbol.for('BsPolyVar')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsRecord')] != null) { + return { + $$bsRecord: data[Symbol.for('BsRecord')], + $$contents: data.map(_toJson) + } + } else { + return data.map(_toJson) + } + } else if (typeof data == 'object') { + var result = {} + Object.keys(data).forEach(key => result[key] = _toJson(data[key])) + return result + } else { + return data + } +|} + +external toT : 'a -> preJson = "%identity" +let serializeAnyToJson data = _toJson (toT data) +let unserializeAnyFromJsonUnsafe: t -> 'a = fun%raw data -> {| + if (!data) { + return data + } else if (typeof data == 'object') { + if (Array.isArray(data)) { + return data.map(unserializeAnyFromJsonUnsafe) + } else if (data.$$contents) { + var result = data.$$contents.map(unserializeAnyFromJsonUnsafe) + if (data.$$tag != null) { + result.tag = data.$$tag + } + if (data.$$bsRecord) { + result[Symbol.for('BsRecord')] = data.$$bsRecord + } + if (data.$$bsPolyVar) { + result[Symbol.for('BsPolyVar')] = data.$$bsPolyVar + } + if (data.$$bsVariant) { + result[Symbol.for('BsVariant')] = data.$$bsVariant + } + if (data.$$bsLocalModule) { + result[Symbol.for('BsLocalModule')] = data.$$bsLocalModule + } + return result + } else { + var result = {} + Object.keys(data).forEach(key => result[key] = unserializeAnyFromJsonUnsafe(data[key])) + return result + } + } else { + return data + } +|} \ No newline at end of file diff --git a/jscomp/others/js_json.mli b/jscomp/others/js_json.mli index da7f09b517..e44c39a3a2 100644 --- a/jscomp/others/js_json.mli +++ b/jscomp/others/js_json.mli @@ -236,4 +236,25 @@ Js.log \@\@ Js.Json.stringify [| "foo"; "bar" |] *) +val serializeAnyToJson : 'a -> t +(** [anyToJson value] turns any [value] into a JSON object +This will throw an error if there are any functions anywhere in the value. +*) + + +val unserializeAnyFromJsonUnsafe : t -> 'a +(** [unsafeAnyFromJson json] converts a serialized JSON object back into the bucklescript runtime value. + +Warning: marshaling is currently not type-safe. The type of marshaled data is not transmitted along +the value of the data, making it impossible to check that the data read back possesses the type expected +by the context. The return type of this function is given as 'a, but this is misleading: the returned +OCaml value does not possess type 'a for all 'a; it has one, unique type which cannot be determined at +compile-time. + +The programmer should explicitly give the expected type of the returned value, using the following syntax: + + (Js.Json.unserializeAnyFromJsonUnsafe json : type) + +Anything can happen at run-time if the object does not correspond to the assumed type. +*) \ No newline at end of file diff --git a/jscomp/test/ext_filename_test.js b/jscomp/test/ext_filename_test.js index 244cd1b11d..9de39c697a 100644 --- a/jscomp/test/ext_filename_test.js +++ b/jscomp/test/ext_filename_test.js @@ -16,7 +16,6 @@ var Test_literals = require("./test_literals.js"); var Ext_string_test = require("./ext_string_test.js"); var CamlinternalLazy = require("../../lib/js/camlinternalLazy.js"); var Ext_pervasives_test = require("./ext_pervasives_test.js"); -var Caml_missing_polyfill = require("../../lib/js/caml_missing_polyfill.js"); var Caml_builtin_exceptions = require("../../lib/js/caml_builtin_exceptions.js"); var node_sep = "/"; @@ -204,7 +203,7 @@ function node_relative_path(node_modules_shorten, file1, dep_file) { function find_root_filename(_cwd, filename) { while(true) { var cwd = _cwd; - if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { + if (Caml_sys.caml_sys_file_exists(Filename.concat(cwd, filename))) { return cwd; } else { var cwd$prime = Curry._1(Filename.dirname, cwd); diff --git a/jscomp/test/js_json_test.js b/jscomp/test/js_json_test.js index bb92cea9ee..0c5bb4e12f 100644 --- a/jscomp/test/js_json_test.js +++ b/jscomp/test/js_json_test.js @@ -6,6 +6,7 @@ var Block = require("../../lib/js/block.js"); var Js_json = require("../../lib/js/js_json.js"); var Caml_array = require("../../lib/js/caml_array.js"); var Js_primitive = require("../../lib/js/js_primitive.js"); +var Belt_MapString = require("../../lib/js/belt_MapString.js"); var Caml_builtin_exceptions = require("../../lib/js/caml_builtin_exceptions.js"); var suites = /* record */[/* contents : [] */0]; @@ -585,6 +586,61 @@ eq("File \"js_json_test.ml\", line 387, characters 5-12", Js_json.decodeNull({ } eq("File \"js_json_test.ml\", line 389, characters 5-12", Js_json.decodeNull(1.23), undefined); +function check(loc, value) { + return eq(loc, Js_json.unserializeAnyFromJsonUnsafe(Js_json.serializeAnyToJson(value)), value); +} + +check("File \"js_json_test.ml\", line 398, characters 8-15", /* record */[ + /* a */2, + /* b */undefined, + /* c : Ok */Block.__(0, ["folks"]), + /* d : [] */0 + ]); + +check("File \"js_json_test.ml\", line 399, characters 8-15", /* :: */[ + /* tuple */[ + 2, + 3, + 4, + 5 + ], + /* [] */0 + ]); + +check("File \"js_json_test.ml\", line 400, characters 8-15", /* `Poly */[ + 892709484, + 3 + ]); + +check("File \"js_json_test.ml\", line 401, characters 8-15", Belt_MapString.set(Belt_MapString.set(Belt_MapString.empty, "hello", /* :: */[ + 2, + /* :: */[ + 3, + /* :: */[ + 4, + /* [] */0 + ] + ] + ]), "folks", /* :: */[ + 100, + /* [] */0 + ])); + +check("File \"js_json_test.ml\", line 402, characters 8-15", /* array */[ + /* `A */[ + 65, + 3 + ], + /* `B */[ + 66, + 2.0 + ], + /* `C */[ + 67, + "hi" + ] + ]); + Mt.from_pair_suites("js_json_test.ml", suites[0]); exports.suites = suites; @@ -594,4 +650,5 @@ exports.false_ = false_; exports.true_ = true_; exports.option_get = option_get; exports.eq_at_i = eq_at_i; +exports.check = check; /* v Not a pure module */ diff --git a/jscomp/test/js_json_test.ml b/jscomp/test/js_json_test.ml index 45fccef7e6..460677fe5b 100644 --- a/jscomp/test/js_json_test.ml +++ b/jscomp/test/js_json_test.ml @@ -389,4 +389,16 @@ let () = eq __LOC__ (Js.Json.decodeNull (Js.Json.number 1.23)) None +(* serializeAny tests *) +type aRecord = {a: int; b: float option; c: (string, string) Belt.Result.t; d: aRecord list} + +let check loc value = eq loc (Js.Json.unserializeAnyFromJsonUnsafe (Js.Json.serializeAnyToJson value)) value + +let () = + check __LOC__ {a=2; b=None; c=Ok "folks"; d=[]}; + check __LOC__ [2,3,4,5]; + check __LOC__ (`Poly 3); + check __LOC__ (Belt.Map.String.empty |. Belt.Map.String.set "hello" [2;3;4] |. Belt.Map.String.set "folks" [100]); + check __LOC__ [|`A 3; `B 2.0; `C "hi"|] + let () = Mt.from_pair_suites __FILE__ !suites diff --git a/jscomp/test/ocaml_typedtree_test.js b/jscomp/test/ocaml_typedtree_test.js index 68a81663e0..5ce2ab6bfa 100644 --- a/jscomp/test/ocaml_typedtree_test.js +++ b/jscomp/test/ocaml_typedtree_test.js @@ -224,9 +224,9 @@ function find_in_path_uncap(path, name) { var dir = param[0]; var fullname = Filename.concat(dir, name); var ufullname = Filename.concat(dir, uname); - if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { + if (Caml_sys.caml_sys_file_exists(ufullname)) { return ufullname; - } else if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { + } else if (Caml_sys.caml_sys_file_exists(fullname)) { return fullname; } else { _param = param[1]; @@ -76346,7 +76346,7 @@ function type_implementation_more(sourcefile, outputprefix, modulename, initial_ } else { var sourceintf = chop_extension_if_any(sourcefile) + interface_suffix[0]; var mli_status = assume_no_mli[0]; - if (mli_status === /* Mli_na */0 && Caml_missing_polyfill.not_implemented("caml_sys_file_exists") || mli_status === /* Mli_exists */1) { + if (mli_status === /* Mli_na */0 && Caml_sys.caml_sys_file_exists(sourceintf) || mli_status === /* Mli_exists */1) { var intf_file; try { intf_file = find_in_path_uncap(load_path[0], modulename + ".cmi"); diff --git a/lib/js/js_json.js b/lib/js/js_json.js index e499063f5c..d0097d1f8e 100644 --- a/lib/js/js_json.js +++ b/lib/js/js_json.js @@ -88,6 +88,86 @@ function decodeNull(json) { } +var _toJson = function (data){ + if (!data) { + return data + } else if (typeof data === 'function') { + throw new Error("Cannot serialize a function") + } else if (Array.isArray(data)) { + if (data.tag != null) { + return { + $$tag: data.tag, + $$contents: data.map(_toJson), + $$bsVariant: data[Symbol.for('BsVariant')], + } + } else if (data[Symbol.for('BsVariant')] != null) { + return { + $$bsVariant: data[Symbol.for('BsVariant')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsLocalModule')] != null) { + return { + $$bsLocalModule: data[Symbol.for('BsLocalModule')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsPolyVar')] != null) { + return { + $$bsPolyVar: data[Symbol.for('BsPolyVar')], + $$contents: data.map(_toJson) + } + } else if (data[Symbol.for('BsRecord')] != null) { + return { + $$bsRecord: data[Symbol.for('BsRecord')], + $$contents: data.map(_toJson) + } + } else { + return data.map(_toJson) + } + } else if (typeof data == 'object') { + var result = {} + Object.keys(data).forEach(key => result[key] = _toJson(data[key])) + return result + } else { + return data + } +}; + +var serializeAnyToJson = _toJson; + +var unserializeAnyFromJsonUnsafe = function (data){ + if (!data) { + return data + } else if (typeof data == 'object') { + if (Array.isArray(data)) { + return data.map(unserializeAnyFromJsonUnsafe) + } else if (data.$$contents) { + var result = data.$$contents.map(unserializeAnyFromJsonUnsafe) + if (data.$$tag != null) { + result.tag = data.$$tag + } + if (data.$$bsRecord) { + result[Symbol.for('BsRecord')] = data.$$bsRecord + } + if (data.$$bsPolyVar) { + result[Symbol.for('BsPolyVar')] = data.$$bsPolyVar + } + if (data.$$bsVariant) { + result[Symbol.for('BsVariant')] = data.$$bsVariant + } + if (data.$$bsLocalModule) { + result[Symbol.for('BsLocalModule')] = data.$$bsLocalModule + } + return result + } else { + var result = {} + Object.keys(data).forEach(key => result[key] = unserializeAnyFromJsonUnsafe(data[key])) + return result + } + } else { + return data + } +}; + exports.classify = classify; exports.test = test; exports.decodeString = decodeString; @@ -96,4 +176,6 @@ exports.decodeObject = decodeObject; exports.decodeArray = decodeArray; exports.decodeBoolean = decodeBoolean; exports.decodeNull = decodeNull; +exports.serializeAnyToJson = serializeAnyToJson; +exports.unserializeAnyFromJsonUnsafe = unserializeAnyFromJsonUnsafe; /* No side effect */ From 9519934bbf92b468fb743bf37b1515aef43f080f Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Sat, 22 Sep 2018 20:01:09 -0600 Subject: [PATCH 2/3] add a note --- jscomp/others/js_json.ml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jscomp/others/js_json.ml b/jscomp/others/js_json.ml index 092773e0f0..27b0ca2151 100644 --- a/jscomp/others/js_json.ml +++ b/jscomp/others/js_json.ml @@ -175,8 +175,12 @@ let _toJson: preJson -> t = fun%raw data -> {| } |} +(** This dance is required to appease the type checker -- doing + * `toJson: 'a -> t = fun%raw` gives the error "contains type variables + * that cannot be generalized". *) external toT : 'a -> preJson = "%identity" let serializeAnyToJson data = _toJson (toT data) + let unserializeAnyFromJsonUnsafe: t -> 'a = fun%raw data -> {| if (!data) { return data From e737aa841a5dcc1479226ed80fef41d28a57a422 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Sat, 22 Sep 2018 20:07:37 -0600 Subject: [PATCH 3/3] rebuild after rebase --- jscomp/test/ext_filename_test.js | 3 ++- jscomp/test/ocaml_typedtree_test.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/jscomp/test/ext_filename_test.js b/jscomp/test/ext_filename_test.js index 9de39c697a..244cd1b11d 100644 --- a/jscomp/test/ext_filename_test.js +++ b/jscomp/test/ext_filename_test.js @@ -16,6 +16,7 @@ var Test_literals = require("./test_literals.js"); var Ext_string_test = require("./ext_string_test.js"); var CamlinternalLazy = require("../../lib/js/camlinternalLazy.js"); var Ext_pervasives_test = require("./ext_pervasives_test.js"); +var Caml_missing_polyfill = require("../../lib/js/caml_missing_polyfill.js"); var Caml_builtin_exceptions = require("../../lib/js/caml_builtin_exceptions.js"); var node_sep = "/"; @@ -203,7 +204,7 @@ function node_relative_path(node_modules_shorten, file1, dep_file) { function find_root_filename(_cwd, filename) { while(true) { var cwd = _cwd; - if (Caml_sys.caml_sys_file_exists(Filename.concat(cwd, filename))) { + if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { return cwd; } else { var cwd$prime = Curry._1(Filename.dirname, cwd); diff --git a/jscomp/test/ocaml_typedtree_test.js b/jscomp/test/ocaml_typedtree_test.js index 5ce2ab6bfa..68a81663e0 100644 --- a/jscomp/test/ocaml_typedtree_test.js +++ b/jscomp/test/ocaml_typedtree_test.js @@ -224,9 +224,9 @@ function find_in_path_uncap(path, name) { var dir = param[0]; var fullname = Filename.concat(dir, name); var ufullname = Filename.concat(dir, uname); - if (Caml_sys.caml_sys_file_exists(ufullname)) { + if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { return ufullname; - } else if (Caml_sys.caml_sys_file_exists(fullname)) { + } else if (Caml_missing_polyfill.not_implemented("caml_sys_file_exists")) { return fullname; } else { _param = param[1]; @@ -76346,7 +76346,7 @@ function type_implementation_more(sourcefile, outputprefix, modulename, initial_ } else { var sourceintf = chop_extension_if_any(sourcefile) + interface_suffix[0]; var mli_status = assume_no_mli[0]; - if (mli_status === /* Mli_na */0 && Caml_sys.caml_sys_file_exists(sourceintf) || mli_status === /* Mli_exists */1) { + if (mli_status === /* Mli_na */0 && Caml_missing_polyfill.not_implemented("caml_sys_file_exists") || mli_status === /* Mli_exists */1) { var intf_file; try { intf_file = find_in_path_uncap(load_path[0], modulename + ".cmi");