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 19, 2023
1 parent 950747d commit 0478058
Show file tree
Hide file tree
Showing 5 changed files with 255 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
76 changes: 76 additions & 0 deletions src/__tests__/json.test.ts
Original file line number Diff line number Diff line change
@@ -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'));
});
});
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';
67 changes: 67 additions & 0 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -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<T, E> = { 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<unknown, unknown>): 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<T, E>(text: string): Result<Result<T, E>, Error> {
let json: ResultJson<T, E>;
try {
json = JSON.parse(text) as ResultJson<T, E>;
} 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'));
},
};

0 comments on commit 0478058

Please sign in to comment.