diff --git a/.eslintrc.js b/.eslintrc.js index 84514c1..9a55c4d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,7 +40,7 @@ const naming = [ { selector: 'objectLiteralProperty', format: null }, - { selector: 'variable', format: ['camelCase', 'UPPER_CASE'], leadingUnderscore: 'allow' }, + { selector: 'variable', format: ['camelCase', 'PascalCase', 'UPPER_CASE'], leadingUnderscore: 'allow' }, ]; module.exports = { diff --git a/rust/src/serde.rs b/rust/src/serde.rs index 82641cd..5b61744 100644 --- a/rust/src/serde.rs +++ b/rust/src/serde.rs @@ -1,29 +1,137 @@ #[cfg(test)] mod tests { + use serde::{Deserialize, Serialize}; + #[test] - fn it_result_to_json() -> serde_json::Result<()> { + fn it_converts_result_to_externally_tagged_json() -> serde_json::Result<()> { let result1: Result = Ok(1); let result2: Result = Err("Some error message"); + let result3: Result, Option<&str>> = Ok(Some(1)); + let result4: Result, Option<&str>> = Ok(None); + let result5: Result, Option<&str>> = Err(None); let json1 = serde_json::to_string(&result1)?; let json2 = serde_json::to_string(&result2)?; + let json3 = serde_json::to_string(&result3)?; + let json4 = serde_json::to_string(&result4)?; + let json5 = serde_json::to_string(&result5)?; assert_eq!(json1.as_str(), "{\"Ok\":1}"); assert_eq!(json2.as_str(), "{\"Err\":\"Some error message\"}"); + assert_eq!(json3.as_str(), "{\"Ok\":1}"); + assert_eq!(json4.as_str(), "{\"Ok\":null}"); + assert_eq!(json5.as_str(), "{\"Err\":null}"); Ok(()) } #[test] - fn it_result_from_json() -> serde_json::Result<()> { + fn it_converts_externally_tagged_json_to_result() -> serde_json::Result<()> { let str1 = "{\"Ok\":1}"; let str2 = "{\"Err\":\"Some error message\"}"; + let str3 = "{\"Ok\":1}"; + let str4 = "{\"Ok\":null}"; + let str5 = "{\"Err\":null}"; let result1: Result = serde_json::from_str(str1)?; let result2: Result = serde_json::from_str(str2)?; + let result3: Result, Option<&str>> = serde_json::from_str(str3)?; + let result4: Result, Option<&str>> = serde_json::from_str(str4)?; + let result5: Result, Option<&str>> = serde_json::from_str(str5)?; assert_eq!(result1, Ok(1)); assert_eq!(result2, Err("Some error message")); + assert_eq!(result3, Ok(Some(1))); + assert_eq!(result4, Ok(None)); + assert_eq!(result5, Err(None)); + + Ok(()) + } + + #[derive(Serialize, Deserialize)] + #[serde(remote = "Result", tag = "type", content = "value")] + enum ResultDef { + Ok(T), + Err(E), + } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct ResultTest { + #[serde(with = "ResultDef")] + result: Result, + } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct ResultTestOption { + #[serde(with = "ResultDef")] + result: Result, Option>, + } + + #[test] + fn it_converts_result_to_adjacent_tagged_json() -> serde_json::Result<()> { + let result1 = ResultTest { result: Ok(1) }; + let result2 = ResultTest { + result: Err("Some error message".to_string()), + }; + let result3 = ResultTestOption { + result: Ok(Some(1)), + }; + let result4 = ResultTestOption { result: Ok(None) }; + let result5 = ResultTestOption { result: Err(None) }; + + let json1 = serde_json::to_string(&result1)?; + let json2 = serde_json::to_string(&result2)?; + let json3 = serde_json::to_string(&result3)?; + let json4 = serde_json::to_string(&result4)?; + let json5 = serde_json::to_string(&result5)?; + + assert_eq!(json1.as_str(), "{\"result\":{\"type\":\"Ok\",\"value\":1}}"); + assert_eq!( + json2.as_str(), + "{\"result\":{\"type\":\"Err\",\"value\":\"Some error message\"}}" + ); + assert_eq!(json3.as_str(), "{\"result\":{\"type\":\"Ok\",\"value\":1}}"); + assert_eq!( + json4.as_str(), + "{\"result\":{\"type\":\"Ok\",\"value\":null}}" + ); + assert_eq!( + json5.as_str(), + "{\"result\":{\"type\":\"Err\",\"value\":null}}" + ); + + Ok(()) + } + + #[test] + fn it_converts_adjacent_tagged_json_to_result() -> serde_json::Result<()> { + let str1 = "{\"result\":{\"type\":\"Ok\",\"value\":1}}"; + let str2 = "{\"result\":{\"type\":\"Err\",\"value\":\"Some error message\"}}"; + let str3 = "{\"result\":{\"type\":\"Ok\",\"value\":1}}"; + let str4 = "{\"result\":{\"type\":\"Ok\",\"value\":null}}"; + let str5 = "{\"result\":{\"type\":\"Err\",\"value\":null}}"; + + let result1: ResultTest = serde_json::from_str(str1)?; + let result2: ResultTest = serde_json::from_str(str2)?; + let result3: ResultTestOption = serde_json::from_str(str3)?; + let result4: ResultTestOption = serde_json::from_str(str4)?; + let result5: ResultTestOption = serde_json::from_str(str5)?; + + assert_eq!(result1, ResultTest { result: Ok(1) }); + assert_eq!( + result2, + ResultTest { + result: Err("Some error message".to_string()), + } + ); + assert_eq!( + result3, + ResultTestOption { + result: Ok(Some(1)), + } + ); + assert_eq!(result4, ResultTestOption { result: Ok(None) }); + assert_eq!(result5, ResultTestOption { result: Err(None) }); Ok(()) } diff --git a/src/__tests__/json.test.ts b/src/__tests__/json.test.ts new file mode 100644 index 0000000..dea8415 --- /dev/null +++ b/src/__tests__/json.test.ts @@ -0,0 +1,76 @@ +import { Err, Ok } from '../factory'; +import { ResultJSON } from '../json'; + +describe(`Test fn \`${ResultJSON.stringify.name}\``, () => { + it('should convert a Result to a JSON string', () => { + expect(ResultJSON.stringify(Ok(1))).toBe('{"type":"ok","value":1}'); + expect(ResultJSON.stringify(Ok('hello world'))).toBe('{"type":"ok","value":"hello world"}'); + expect(ResultJSON.stringify(Ok(null))).toBe('{"type":"ok","value":null}'); + expect(ResultJSON.stringify(Ok(undefined))).toBe('{"type":"ok"}'); + expect(ResultJSON.stringify(Ok({}))).toBe('{"type":"ok","value":{}}'); + expect(ResultJSON.stringify(Ok([]))).toBe('{"type":"ok","value":[]}'); + + expect(ResultJSON.stringify(Err(1))).toBe('{"type":"err","value":1}'); + expect(ResultJSON.stringify(Err('hello world'))).toBe('{"type":"err","value":"hello world"}'); + expect(ResultJSON.stringify(Err(null))).toBe('{"type":"err","value":null}'); + expect(ResultJSON.stringify(Err(undefined))).toBe('{"type":"err"}'); + expect(ResultJSON.stringify(Err({}))).toBe('{"type":"err","value":{}}'); + expect(ResultJSON.stringify(Err([]))).toBe('{"type":"err","value":[]}'); + + expect(ResultJSON.stringify(Ok(Ok(1)))).toBe('{"type":"ok","value":{"type":"ok","value":1}}'); + expect(ResultJSON.stringify(Ok(Err(1)))).toBe('{"type":"ok","value":{"type":"err","value":1}}'); + expect(ResultJSON.stringify(Err(Ok(1)))).toBe('{"type":"err","value":{"type":"ok","value":1}}'); + expect(ResultJSON.stringify(Err(Err(1)))).toBe('{"type":"err","value":{"type":"err","value":1}}'); + }); +}); + +describe(`Test fn \`${ResultJSON.parse.name}\``, () => { + it('should parse the valid json string', () => { + expect(ResultJSON.parse('{"type":"ok","value":1}').unwrap().unwrap()).toBe(1); + expect(ResultJSON.parse('{"type":"ok","value":"hello world"}').unwrap().unwrap()).toBe('hello world'); + expect(ResultJSON.parse('{"type":"ok","value":null}').unwrap().unwrap()).toBeNull(); + expect(ResultJSON.parse('{"type":"ok"}').unwrap().unwrap()).toBeUndefined(); + expect(ResultJSON.parse('{"type":"ok","value":{}}').unwrap().unwrap()).toStrictEqual({}); + expect(ResultJSON.parse('{"type":"ok","value":[]}').unwrap().unwrap()).toStrictEqual([]); + + expect(ResultJSON.parse('{"type":"err","value":1}').unwrap().unwrapErr()).toBe(1); + expect(ResultJSON.parse('{"type":"err","value":"hello world"}').unwrap().unwrapErr()).toBe('hello world'); + expect(ResultJSON.parse('{"type":"err","value":null}').unwrap().unwrapErr()).toBeNull(); + expect(ResultJSON.parse('{"type":"err"}').unwrap().unwrapErr()).toBeUndefined(); + expect(ResultJSON.parse('{"type":"err","value":{}}').unwrap().unwrapErr()).toStrictEqual({}); + expect(ResultJSON.parse('{"type":"err","value":[]}').unwrap().unwrapErr()).toStrictEqual([]); + + expect(ResultJSON.parse('{"type":"ok","value":{"type":"ok","value":1}}').unwrap().unwrap()).toStrictEqual({ + type: 'ok', + value: 1, + }); + expect(ResultJSON.parse('{"type":"ok","value":{"type":"err","value":1}}').unwrap().unwrap()).toStrictEqual({ + type: 'err', + value: 1, + }); + expect(ResultJSON.parse('{"type":"err","value":{"type":"ok","value":1}}').unwrap().unwrapErr()).toStrictEqual({ + type: 'ok', + value: 1, + }); + expect(ResultJSON.parse('{"type":"err","value":{"type":"err","value":1}}').unwrap().unwrapErr()).toStrictEqual({ + type: 'err', + value: 1, + }); + }); + + it('should return `Err` if failed to parse the valid json string', () => { + expect(ResultJSON.parse('{}').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + expect(ResultJSON.parse('1').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + expect(ResultJSON.parse('"1"').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + expect(ResultJSON.parse('true').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + expect(ResultJSON.parse('false').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + expect(ResultJSON.parse('[]').unwrapErr()).toStrictEqual(new Error('Cannot parse to Result')); + }); + + it('should return `Err` if parsing invalid json string', () => { + expect(ResultJSON.parse('{').unwrapErr()).toStrictEqual(new SyntaxError('Unexpected end of JSON input')); + expect(ResultJSON.parse('tru').unwrapErr()).toStrictEqual(new SyntaxError('Unexpected end of JSON input')); + expect(ResultJSON.parse('"1').unwrapErr()).toStrictEqual(new SyntaxError('Unexpected end of JSON input')); + expect(ResultJSON.parse('[').unwrapErr()).toStrictEqual(new SyntaxError('Unexpected end of JSON input')); + }); +}); diff --git a/src/index.ts b/src/index.ts index 5b73caf..6cea6ab 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './factory'; +export * from './json'; export * from './resultify'; export * from './types'; diff --git a/src/json.ts b/src/json.ts new file mode 100644 index 0000000..eaf3802 --- /dev/null +++ b/src/json.ts @@ -0,0 +1,67 @@ +import { Err, Ok } from './factory'; +import { RustlikeResult } from './result'; +import type { Result } from './types'; + +// `value` may not exist if `T` or `E` is `undefined` +type ResultJson = { type: 'ok'; value: T } | { type: 'err'; value: E }; + +/** + * Converts a `Result` to a JSON object. + */ +function toJSON(result: unknown): unknown { + if (result instanceof RustlikeResult) { + return result.isOk() + ? { type: 'ok', value: toJSON(result.unwrapUnchecked()) } + : { type: 'err', value: toJSON(result.unwrapErrUnchecked()) }; + } + return result; +} + +/** + * A simple implementation that convert `Result` to and from JSON string. + * + * The format of the JSON string follows the adjacently tagged enum representation in Rust library Serde. + * https://serde.rs/enum-representations.html#adjacently-tagged + */ +export const ResultJSON = { + /** + * Converts a `Result` to a JSON string. + * + * The format of the JSON string follows the adjacently tagged enum representation in Rust library Serde. + * https://serde.rs/enum-representations.html#adjacently-tagged + * + * The nested `Result` will be stringified. + * + * @param result A `Result`. + */ + stringify(result: Result): string { + return JSON.stringify(toJSON(result)); + }, + + /** + * Converts a JSON string into a `Result`. + * + * This function won't convert any `Result` JSON string inside of `Result`. + * The result of `{"type":"ok","value":{"type":"ok","value":1}}` is `Ok({ type: 'ok', value: 1 })`. + * + * The format of the JSON string follows the adjacently tagged enum representation in Rust library Serde. + * https://serde.rs/enum-representations.html#adjacently-tagged + * + * @param text A valid JSON string. + */ + parse(text: string): Result, Error> { + let json: ResultJson; + try { + json = JSON.parse(text) as ResultJson; + } catch (err) { + return Err(err as Error); + } + + if (typeof json !== 'object' || json === null) return Err(new Error('Cannot parse to Result')); + + if ('type' in json) { + return json.type === 'ok' ? Ok(Ok(json.value)) : Ok(Err(json.value)); + } + return Err(new Error('Cannot parse to Result')); + }, +};