Skip to content

Commit

Permalink
feat: add a simple implementation for stringifing and parsing JSON
Browse files Browse the repository at this point in the history
  • Loading branch information
yifanwww committed Sep 20, 2023
1 parent bf829de commit b090169
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
112 changes: 110 additions & 2 deletions rust/src/serde.rs
Original file line number Diff line number Diff line change
@@ -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<i32, &str> = Ok(1);
let result2: Result<i32, &str> = Err("Some error message");
let result3: Result<Option<i32>, Option<&str>> = Ok(Some(1));
let result4: Result<Option<i32>, Option<&str>> = Ok(None);
let result5: Result<Option<i32>, 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<i32, &str> = serde_json::from_str(str1)?;
let result2: Result<i32, &str> = serde_json::from_str(str2)?;
let result3: Result<Option<i32>, Option<&str>> = serde_json::from_str(str3)?;
let result4: Result<Option<i32>, Option<&str>> = serde_json::from_str(str4)?;
let result5: Result<Option<i32>, 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<T, E> {
Ok(T),
Err(E),
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct ResultTest {
#[serde(with = "ResultDef")]
result: Result<i32, String>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct ResultTestOption {
#[serde(with = "ResultDef")]
result: Result<Option<i32>, Option<String>>,
}

#[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(())
}
Expand Down
75 changes: 75 additions & 0 deletions src/__tests__/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Err, Ok } from '../factory';
import { ResultJSON } from '../json';

describe(`Test fn \`${ResultJSON.serialize.name}\``, () => {
it('should convert a Result to a JSON string', () => {
expect(ResultJSON.serialize(Ok(1))).toStrictEqual({ type: 'ok', value: 1 });
expect(ResultJSON.serialize(Ok('hello world'))).toStrictEqual({ type: 'ok', value: 'hello world' });
expect(ResultJSON.serialize(Ok(null))).toStrictEqual({ type: 'ok', value: null });
expect(ResultJSON.serialize(Ok(undefined))).toStrictEqual({ type: 'ok', value: undefined });
expect(ResultJSON.serialize(Ok({}))).toStrictEqual({ type: 'ok', value: {} });
expect(ResultJSON.serialize(Ok([]))).toStrictEqual({ type: 'ok', value: [] });

expect(ResultJSON.serialize(Err(1))).toStrictEqual({ type: 'err', value: 1 });
expect(ResultJSON.serialize(Err('hello world'))).toStrictEqual({ type: 'err', value: 'hello world' });
expect(ResultJSON.serialize(Err(null))).toStrictEqual({ type: 'err', value: null });
expect(ResultJSON.serialize(Err(undefined))).toStrictEqual({ type: 'err', value: undefined });
expect(ResultJSON.serialize(Err({}))).toStrictEqual({ type: 'err', value: {} });
expect(ResultJSON.serialize(Err([]))).toStrictEqual({ type: 'err', value: [] });

expect(ResultJSON.serialize(Ok(Ok(1)))).toStrictEqual({ type: 'ok', value: { type: 'ok', value: 1 } });
expect(ResultJSON.serialize(Ok(Err(1)))).toStrictEqual({ type: 'ok', value: { type: 'err', value: 1 } });
expect(ResultJSON.serialize(Err(Ok(1)))).toStrictEqual({ type: 'err', value: { type: 'ok', value: 1 } });
expect(ResultJSON.serialize(Err(Err(1)))).toStrictEqual({ type: 'err', value: { type: 'err', value: 1 } });
});
});

describe(`Test fn \`${ResultJSON.deserialize.name}\``, () => {
it('should parse the valid json string', () => {
expect(ResultJSON.deserialize({ type: 'ok', value: 1 })).toStrictEqual(Ok(Ok(1)));
expect(ResultJSON.deserialize({ type: 'ok', value: 'hello world' })).toStrictEqual(Ok(Ok('hello world')));
expect(ResultJSON.deserialize({ type: 'ok', value: null })).toStrictEqual(Ok(Ok(null)));
expect(ResultJSON.deserialize({ type: 'ok' })).toStrictEqual(Ok(Ok(undefined)));
expect(ResultJSON.deserialize({ type: 'ok', value: undefined })).toStrictEqual(Ok(Ok(undefined)));
expect(ResultJSON.deserialize({ type: 'ok', value: {} })).toStrictEqual(Ok(Ok({})));
expect(ResultJSON.deserialize({ type: 'ok', value: [] })).toStrictEqual(Ok(Ok([])));

expect(ResultJSON.deserialize({ type: 'err', value: 1 })).toStrictEqual(Ok(Err(1)));
expect(ResultJSON.deserialize({ type: 'err', value: 'hello world' })).toStrictEqual(Ok(Err('hello world')));
expect(ResultJSON.deserialize({ type: 'err', value: null })).toStrictEqual(Ok(Err(null)));
expect(ResultJSON.deserialize({ type: 'err' })).toStrictEqual(Ok(Err(undefined)));
expect(ResultJSON.deserialize({ type: 'err', value: undefined })).toStrictEqual(Ok(Err(undefined)));
expect(ResultJSON.deserialize({ type: 'err', value: {} })).toStrictEqual(Ok(Err({})));
expect(ResultJSON.deserialize({ type: 'err', value: [] })).toStrictEqual(Ok(Err([])));

expect(ResultJSON.deserialize({ type: 'ok', value: { type: 'ok', value: 1 } })).toStrictEqual(
Ok(Ok({ type: 'ok', value: 1 })),
);
expect(ResultJSON.deserialize({ type: 'ok', value: { type: 'err', value: 1 } })).toStrictEqual(
Ok(Ok({ type: 'err', value: 1 })),
);
expect(ResultJSON.deserialize({ type: 'err', value: { type: 'ok', value: 1 } })).toStrictEqual(
Ok(Err({ type: 'ok', value: 1 })),
);
expect(ResultJSON.deserialize({ type: 'err', value: { type: 'err', value: 1 } })).toStrictEqual(
Ok(Err({ type: 'err', value: 1 })),
);
});

it('should return `Err` if failed to parse the valid json string', () => {
expect(ResultJSON.deserialize({ value: 1 } as never)).toStrictEqual(Err(new Error('Cannot parse to Result')));
expect(ResultJSON.deserialize(1 as never)).toStrictEqual(
Err(new TypeError("Cannot use 'in' operator to search for 'type' in 1")),
);
expect(ResultJSON.deserialize('1' as never)).toStrictEqual(
Err(new TypeError("Cannot use 'in' operator to search for 'type' in 1")),
);
expect(ResultJSON.deserialize(true as never)).toStrictEqual(
Err(new TypeError("Cannot use 'in' operator to search for 'type' in true")),
);
expect(ResultJSON.deserialize(false as never)).toStrictEqual(
Err(new TypeError("Cannot use 'in' operator to search for 'type' in false")),
);
expect(ResultJSON.deserialize([] as never)).toStrictEqual(Err(new Error('Cannot parse to Result')));
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './factory';
export * from './json';
export * from './resultify';
export * from './types';
64 changes: 64 additions & 0 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Err, Ok } from './factory';
import { RustlikeResult } from './result';
import type { Result } from './types';

/**
* The type that represents the JSON object structure of `Result`.
*
* `value` may not exist if `T` or `E` is `undefined`.
*/
export type ResultJson<T, E> =
| (undefined extends T ? { type: 'ok'; value?: T } : { type: 'ok'; value: T })
| (undefined extends T ? { type: 'err'; value?: E } : { 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 object.
*
* The format of the JSON object 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 object.
*
* The format of the JSON object follows the adjacently tagged enum representation in Rust library Serde.
* https://serde.rs/enum-representations.html#adjacently-tagged
*
* The nested `Result` will be serialized.
*/
serialize<T, E>(result: Result<unknown, unknown>): ResultJson<T, E> {
return toJSON(result) as ResultJson<T, E>;
},

/**
* Converts a JSON object into a `Result`.
*
* This function won't convert any `Result` JSON object inside of `Result`.
* The result of `{"type":"ok","value":{"type":"ok","value":1}}` is `Ok({ type: 'ok', value: 1 })`.
*
* The format of the JSON object follows the adjacently tagged enum representation in Rust library Serde.
* https://serde.rs/enum-representations.html#adjacently-tagged
*/
deserialize<T, E>(json: ResultJson<unknown, unknown>): Result<Result<T, E>, Error> {
try {
if ('type' in json) {
return json.type === 'ok' ? Ok(Ok(json.value as T)) : Ok(Err(json.value as E));
}
} catch (err) {
return Err(err as Error);
}
return Err(new Error('Cannot parse to Result'));
},
};

0 comments on commit b090169

Please sign in to comment.