diff --git a/jscomp/others/js_json.ml b/jscomp/others/js_json.ml index 4a348a43d6..27b0ca2151 100644 --- a/jscomp/others/js_json.ml +++ b/jscomp/others/js_json.ml @@ -129,3 +129,88 @@ 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 + } +|} + +(** 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 + } 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/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/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 */