From 9d9c6b232a4dcc38201fb7fc083f5264a38735d3 Mon Sep 17 00:00:00 2001 From: bloodyowl Date: Wed, 27 Mar 2024 12:43:48 +0100 Subject: [PATCH 1/3] fromJSON/toJSON --- src/AsyncData.ts | 18 +++++++++++++++++- src/Boxed.ts | 1 + src/OptionResult.ts | 26 +++++++++++++++++++++++++- src/types.ts | 11 +++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/AsyncData.ts b/src/AsyncData.ts index 0791feb..91e8d6b 100644 --- a/src/AsyncData.ts +++ b/src/AsyncData.ts @@ -1,6 +1,6 @@ import { keys, values } from "./Dict"; import { Option, Result } from "./OptionResult"; -import { LooseRecord } from "./types"; +import { JsonAsyncData, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; class __AsyncData { @@ -92,6 +92,14 @@ class __AsyncData { // @ts-ignore value != null && value.__boxed_type__ === "AsyncData"; + static fromJSON = (value: JsonAsyncData) => { + return value.tag === "NotAsked" + ? AsyncData.NotAsked() + : value.tag === "Loading" + ? AsyncData.Loading() + : AsyncData.Done(value.value); + }; + map(this: AsyncData, func: (value: A) => B): AsyncData { if (this === NOT_ASKED || this === LOADING) { return this as unknown as AsyncData; @@ -266,6 +274,14 @@ class __AsyncData { isNotAsked(this: AsyncData): this is NotAsked { return this === NOT_ASKED; } + + toJSON(this: AsyncData): JsonAsyncData { + return this.match>({ + NotAsked: () => ({ tag: "NotAsked" }), + Loading: () => ({ tag: "Loading" }), + Done: (value) => ({ tag: "Done", value }), + }); + } } // @ts-expect-error diff --git a/src/Boxed.ts b/src/Boxed.ts index 4e19b13..0139ed4 100644 --- a/src/Boxed.ts +++ b/src/Boxed.ts @@ -6,3 +6,4 @@ export { Future } from "./Future"; export { Lazy } from "./Lazy"; export { Option, Result } from "./OptionResult"; export * as Serializer from "./Serializer"; +export { JsonAsyncData, JsonOption, JsonResult } from "./types"; diff --git a/src/OptionResult.ts b/src/OptionResult.ts index 569f606..97505f8 100644 --- a/src/OptionResult.ts +++ b/src/OptionResult.ts @@ -1,5 +1,5 @@ import { keys, values } from "./Dict"; -import { LooseRecord } from "./types"; +import { JsonOption, JsonResult, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; class __Option { @@ -99,6 +99,10 @@ class __Option { : a.tag === b.tag; }; + static fromJSON = (value: JsonOption) => { + return value.tag === "None" ? Option.None() : Option.Some(value.value); + }; + /** * Returns the Option containing the value from the callback * @@ -222,6 +226,13 @@ class __Option { isNone(this: Option): this is None { return this === NONE; } + + toJSON(this: Option): JsonOption { + return this.match>({ + None: () => ({ tag: "None" }), + Some: (value) => ({ tag: "Some", value }), + }); + } } // @ts-expect-error @@ -387,6 +398,12 @@ class __Result { return false; }; + static fromJSON = (value: JsonResult) => { + return value.tag === "Ok" + ? Result.Ok(value.value) + : Result.Error(value.error); + }; + /** * Returns the Result containing the value from the callback * @@ -522,6 +539,13 @@ class __Result { isError(this: Result): this is Error { return this.tag === "Error"; } + + toJSON(this: Result): JsonResult { + return this.match>({ + Ok: (value) => ({ tag: "Ok", value }), + Error: (error) => ({ tag: "Error", error }), + }); + } } // @ts-expect-error diff --git a/src/types.ts b/src/types.ts index 163c8ab..a85d055 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1 +1,12 @@ export type LooseRecord = Record; + +export type JsonResult = + | { tag: "Ok"; value: A } + | { tag: "Error"; error: E }; + +export type JsonAsyncData = + | { tag: "NotAsked" } + | { tag: "Loading" } + | { tag: "Done"; value: A }; + +export type JsonOption = { tag: "None" } | { tag: "Some"; value: A }; From f54ce074521aaf7afed32caa3841c5899a91ae26 Mon Sep 17 00:00:00 2001 From: bloodyowl Date: Wed, 27 Mar 2024 14:25:27 +0100 Subject: [PATCH 2/3] Add __boxed_type__ --- src/AsyncData.ts | 6 +++--- src/OptionResult.ts | 8 ++++---- src/Serializer.ts | 45 ++++-------------------------------------- src/types.ts | 14 +++++++------ test/AsyncData.test.ts | 3 +++ test/Option.test.ts | 2 ++ test/Result.test.ts | 2 ++ 7 files changed, 26 insertions(+), 54 deletions(-) diff --git a/src/AsyncData.ts b/src/AsyncData.ts index 91e8d6b..81485f9 100644 --- a/src/AsyncData.ts +++ b/src/AsyncData.ts @@ -277,9 +277,9 @@ class __AsyncData { toJSON(this: AsyncData): JsonAsyncData { return this.match>({ - NotAsked: () => ({ tag: "NotAsked" }), - Loading: () => ({ tag: "Loading" }), - Done: (value) => ({ tag: "Done", value }), + NotAsked: () => ({ __boxed_type__: "AsyncData", tag: "NotAsked" }), + Loading: () => ({ __boxed_type__: "AsyncData", tag: "Loading" }), + Done: (value) => ({ __boxed_type__: "AsyncData", tag: "Done", value }), }); } } diff --git a/src/OptionResult.ts b/src/OptionResult.ts index 97505f8..ea16a0e 100644 --- a/src/OptionResult.ts +++ b/src/OptionResult.ts @@ -229,8 +229,8 @@ class __Option { toJSON(this: Option): JsonOption { return this.match>({ - None: () => ({ tag: "None" }), - Some: (value) => ({ tag: "Some", value }), + None: () => ({ __boxed_type__: "Option", tag: "None" }), + Some: (value) => ({ __boxed_type__: "Option", tag: "Some", value }), }); } } @@ -542,8 +542,8 @@ class __Result { toJSON(this: Result): JsonResult { return this.match>({ - Ok: (value) => ({ tag: "Ok", value }), - Error: (error) => ({ tag: "Error", error }), + Ok: (value) => ({ __boxed_type__: "Result", tag: "Ok", value }), + Error: (error) => ({ __boxed_type__: "Result", tag: "Error", error }), }); } } diff --git a/src/Serializer.ts b/src/Serializer.ts index 742e63d..e9115a2 100644 --- a/src/Serializer.ts +++ b/src/Serializer.ts @@ -2,38 +2,7 @@ import { AsyncData } from "./AsyncData"; import { Option, Result } from "./OptionResult"; export const encode = (value: any, indent?: number | undefined) => { - return JSON.stringify( - value, - function (key, value) { - if (value == null) { - return; - } - if (value.__boxed_type__ === "Option") { - return { - __boxed_type__: "Option", - tag: value.tag, - value: value.value, - }; - } - if (value.__boxed_type__ === "Result") { - return { - __boxed_type__: "Result", - tag: value.tag, - value: value.value, - error: value.error, - }; - } - if (value.__boxed_type__ === "AsyncData") { - return { - __boxed_type__: "AsyncData", - tag: value.tag, - value: value.value, - }; - } - return value; - }, - indent, - ); + return JSON.stringify(value, null, indent); }; export const decode = (value: string) => { @@ -42,19 +11,13 @@ export const decode = (value: string) => { return value; } if (value.__boxed_type__ === "Option") { - return value.tag === "Some" ? Option.Some(value.value) : Option.None(); + return Option.fromJSON(value); } if (value.__boxed_type__ === "Result") { - return value.tag === "Ok" - ? Result.Ok(value.value) - : Result.Error(value.error); + return Result.fromJSON(value); } if (value.__boxed_type__ === "AsyncData") { - return value.tag === "NotAsked" - ? AsyncData.NotAsked() - : value.tag === "Loading" - ? AsyncData.Loading() - : AsyncData.Done(value.value); + return AsyncData.fromJSON(value); } return value; }); diff --git a/src/types.ts b/src/types.ts index a85d055..f8a93de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,14 @@ export type LooseRecord = Record; export type JsonResult = - | { tag: "Ok"; value: A } - | { tag: "Error"; error: E }; + | { __boxed_type__: "Result"; tag: "Ok"; value: A } + | { __boxed_type__: "Result"; tag: "Error"; error: E }; export type JsonAsyncData = - | { tag: "NotAsked" } - | { tag: "Loading" } - | { tag: "Done"; value: A }; + | { __boxed_type__: "AsyncData"; tag: "NotAsked" } + | { __boxed_type__: "AsyncData"; tag: "Loading" } + | { __boxed_type__: "AsyncData"; tag: "Done"; value: A }; -export type JsonOption = { tag: "None" } | { tag: "Some"; value: A }; +export type JsonOption = + | { __boxed_type__: "Option"; tag: "None" } + | { __boxed_type__: "Option"; tag: "Some"; value: A }; diff --git a/test/AsyncData.test.ts b/test/AsyncData.test.ts index 91f2e96..365c929 100644 --- a/test/AsyncData.test.ts +++ b/test/AsyncData.test.ts @@ -131,12 +131,15 @@ test("AsyncData.equals", () => { test("AsyncData serialize", () => { expect(JSON.parse(JSON.stringify(AsyncData.NotAsked()))).toEqual({ + __boxed_type__: "AsyncData", tag: "NotAsked", }); expect(JSON.parse(JSON.stringify(AsyncData.Loading()))).toEqual({ + __boxed_type__: "AsyncData", tag: "Loading", }); expect(JSON.parse(JSON.stringify(AsyncData.Done(1)))).toEqual({ + __boxed_type__: "AsyncData", tag: "Done", value: 1, }); diff --git a/test/Option.test.ts b/test/Option.test.ts index 20539f5..d8b5667 100644 --- a/test/Option.test.ts +++ b/test/Option.test.ts @@ -107,9 +107,11 @@ test("Option.equals", () => { test("Option serialize", () => { expect(JSON.parse(JSON.stringify(Option.None()))).toEqual({ + __boxed_type__: "Option", tag: "None", }); expect(JSON.parse(JSON.stringify(Option.Some(1)))).toEqual({ + __boxed_type__: "Option", tag: "Some", value: 1, }); diff --git a/test/Result.test.ts b/test/Result.test.ts index 6f575f0..7f8f625 100644 --- a/test/Result.test.ts +++ b/test/Result.test.ts @@ -138,10 +138,12 @@ test("Result.equals", () => { test("Result serialize", () => { expect(JSON.parse(JSON.stringify(Result.Error(1)))).toEqual({ + __boxed_type__: "Result", tag: "Error", error: 1, }); expect(JSON.parse(JSON.stringify(Result.Ok(1)))).toEqual({ + __boxed_type__: "Result", tag: "Ok", value: 1, }); From d3a5cbd01fad350f14e2e71adba7114f7e8375df Mon Sep 17 00:00:00 2001 From: bloodyowl Date: Sat, 20 Apr 2024 09:53:21 +0200 Subject: [PATCH 3/3] Don't expose __boxed_type__ outside of serializer Closes #71 --- src/AsyncData.ts | 7 ++++--- src/OptionResult.ts | 9 +++++---- src/Serializer.ts | 12 +++++++++++- src/symbols.ts | 1 + src/types.ts | 14 ++++++-------- test/AsyncData.test.ts | 3 --- test/Option.test.ts | 2 -- test/Result.test.ts | 2 -- 8 files changed, 27 insertions(+), 23 deletions(-) create mode 100644 src/symbols.ts diff --git a/src/AsyncData.ts b/src/AsyncData.ts index 81485f9..40a9039 100644 --- a/src/AsyncData.ts +++ b/src/AsyncData.ts @@ -1,5 +1,6 @@ import { keys, values } from "./Dict"; import { Option, Result } from "./OptionResult"; +import { BOXED_TYPE } from "./symbols"; import { JsonAsyncData, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; @@ -277,9 +278,9 @@ class __AsyncData { toJSON(this: AsyncData): JsonAsyncData { return this.match>({ - NotAsked: () => ({ __boxed_type__: "AsyncData", tag: "NotAsked" }), - Loading: () => ({ __boxed_type__: "AsyncData", tag: "Loading" }), - Done: (value) => ({ __boxed_type__: "AsyncData", tag: "Done", value }), + NotAsked: () => ({ [BOXED_TYPE]: "AsyncData", tag: "NotAsked" }), + Loading: () => ({ [BOXED_TYPE]: "AsyncData", tag: "Loading" }), + Done: (value) => ({ [BOXED_TYPE]: "AsyncData", tag: "Done", value }), }); } } diff --git a/src/OptionResult.ts b/src/OptionResult.ts index ea16a0e..6d9e2ad 100644 --- a/src/OptionResult.ts +++ b/src/OptionResult.ts @@ -1,4 +1,5 @@ import { keys, values } from "./Dict"; +import { BOXED_TYPE } from "./symbols"; import { JsonOption, JsonResult, LooseRecord } from "./types"; import { zip } from "./ZipUnzip"; @@ -229,8 +230,8 @@ class __Option { toJSON(this: Option): JsonOption { return this.match>({ - None: () => ({ __boxed_type__: "Option", tag: "None" }), - Some: (value) => ({ __boxed_type__: "Option", tag: "Some", value }), + None: () => ({ [BOXED_TYPE]: "Option", tag: "None" }), + Some: (value) => ({ [BOXED_TYPE]: "Option", tag: "Some", value }), }); } } @@ -542,8 +543,8 @@ class __Result { toJSON(this: Result): JsonResult { return this.match>({ - Ok: (value) => ({ __boxed_type__: "Result", tag: "Ok", value }), - Error: (error) => ({ __boxed_type__: "Result", tag: "Error", error }), + Ok: (value) => ({ [BOXED_TYPE]: "Result", tag: "Ok", value }), + Error: (error) => ({ [BOXED_TYPE]: "Result", tag: "Error", error }), }); } } diff --git a/src/Serializer.ts b/src/Serializer.ts index e9115a2..698f24d 100644 --- a/src/Serializer.ts +++ b/src/Serializer.ts @@ -1,8 +1,18 @@ import { AsyncData } from "./AsyncData"; import { Option, Result } from "./OptionResult"; +import { BOXED_TYPE } from "./symbols"; export const encode = (value: any, indent?: number | undefined) => { - return JSON.stringify(value, null, indent); + return JSON.stringify( + value, + (key, value) => { + if (typeof value[BOXED_TYPE] === "string") { + return { ...value, __boxed_type__: value[BOXED_TYPE] }; + } + return value; + }, + indent, + ); }; export const decode = (value: string) => { diff --git a/src/symbols.ts b/src/symbols.ts new file mode 100644 index 0000000..fc9e6cf --- /dev/null +++ b/src/symbols.ts @@ -0,0 +1 @@ +export const BOXED_TYPE = Symbol.for("__boxed_type__"); diff --git a/src/types.ts b/src/types.ts index f8a93de..a85d055 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,12 @@ export type LooseRecord = Record; export type JsonResult = - | { __boxed_type__: "Result"; tag: "Ok"; value: A } - | { __boxed_type__: "Result"; tag: "Error"; error: E }; + | { tag: "Ok"; value: A } + | { tag: "Error"; error: E }; export type JsonAsyncData = - | { __boxed_type__: "AsyncData"; tag: "NotAsked" } - | { __boxed_type__: "AsyncData"; tag: "Loading" } - | { __boxed_type__: "AsyncData"; tag: "Done"; value: A }; + | { tag: "NotAsked" } + | { tag: "Loading" } + | { tag: "Done"; value: A }; -export type JsonOption = - | { __boxed_type__: "Option"; tag: "None" } - | { __boxed_type__: "Option"; tag: "Some"; value: A }; +export type JsonOption = { tag: "None" } | { tag: "Some"; value: A }; diff --git a/test/AsyncData.test.ts b/test/AsyncData.test.ts index 365c929..91f2e96 100644 --- a/test/AsyncData.test.ts +++ b/test/AsyncData.test.ts @@ -131,15 +131,12 @@ test("AsyncData.equals", () => { test("AsyncData serialize", () => { expect(JSON.parse(JSON.stringify(AsyncData.NotAsked()))).toEqual({ - __boxed_type__: "AsyncData", tag: "NotAsked", }); expect(JSON.parse(JSON.stringify(AsyncData.Loading()))).toEqual({ - __boxed_type__: "AsyncData", tag: "Loading", }); expect(JSON.parse(JSON.stringify(AsyncData.Done(1)))).toEqual({ - __boxed_type__: "AsyncData", tag: "Done", value: 1, }); diff --git a/test/Option.test.ts b/test/Option.test.ts index d8b5667..20539f5 100644 --- a/test/Option.test.ts +++ b/test/Option.test.ts @@ -107,11 +107,9 @@ test("Option.equals", () => { test("Option serialize", () => { expect(JSON.parse(JSON.stringify(Option.None()))).toEqual({ - __boxed_type__: "Option", tag: "None", }); expect(JSON.parse(JSON.stringify(Option.Some(1)))).toEqual({ - __boxed_type__: "Option", tag: "Some", value: 1, }); diff --git a/test/Result.test.ts b/test/Result.test.ts index 7f8f625..6f575f0 100644 --- a/test/Result.test.ts +++ b/test/Result.test.ts @@ -138,12 +138,10 @@ test("Result.equals", () => { test("Result serialize", () => { expect(JSON.parse(JSON.stringify(Result.Error(1)))).toEqual({ - __boxed_type__: "Result", tag: "Error", error: 1, }); expect(JSON.parse(JSON.stringify(Result.Ok(1)))).toEqual({ - __boxed_type__: "Result", tag: "Ok", value: 1, });