Skip to content

Commit

Permalink
Merge pull request #2 from yifanwww/json
Browse files Browse the repository at this point in the history
feat: support JSON serialization and deserialization
  • Loading branch information
yifanwww committed Sep 23, 2023
2 parents bf829de + ef38d67 commit 9bbecb6
Show file tree
Hide file tree
Showing 14 changed files with 1,020 additions and 11 deletions.
6 changes: 5 additions & 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 Expand Up @@ -284,6 +284,10 @@ module.exports = {
// https://typescript-eslint.io/rules/no-use-before-define
'@typescript-eslint/no-use-before-define': 'error',

// https://typescript-eslint.io/rules/no-useless-constructor
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',

// https://typescript-eslint.io/rules/restrict-template-expressions
'@typescript-eslint/restrict-template-expressions': [
'error',
Expand Down
105 changes: 103 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ This package implement a Rust-like `Result`, nearly all methods are similar to t
```ts
const ok = Ok(1);
const err = Err('Some error message');
```

```ts
import fs, { Dirent, Stats } from 'node:fs/promises';

const result1: Result<Stats, Error> = await fs
.stat(path)
.then((value) => Ok(value))
.catch((err) => Err(err));

// TODO: complex examples
const result2: Result<Dirent[], Error> = await fs
.readdir(path, { withFileTypes: true })
.then((value) => Ok(value))
.catch((err) => Err(err));
```

[result]: https://doc.rust-lang.org/std/result/enum.Result.html
Expand Down Expand Up @@ -129,7 +141,7 @@ There is a [proposal] (stage 2) that introduces `Record` and `Tuple` which are c

[proposal]: https://github.com/tc39/proposal-record-tuple

## More Helper Functions
## Helpers for Resultifying
### resultify

Takes a function and returns a version that returns results asynchronously.
Expand Down Expand Up @@ -170,6 +182,95 @@ const result = await resultify.promise(promise);
Due to the limit of TypeScript,it's impossible to resultify overloaded functions perfectly that the returned functions are still overloaded.
This function allows you to resultify the promise that the overloaded functions return.

## JSON Serialization & Deserialization

You can always write your (de)serialization implementation for your use cases. But before you write it, you can check following helper functions to see if they can help you.

### Built-in Simple Implementation

This package provides a simple implementation for JSON (de)serialization.

```ts
// serialization
ResultJSON.serialize(Ok(1)) // { type: 'ok', value: 1 }
ResultJSON.serialize(Err('Some error message')) // { type: 'err', value: 'Some error message' }
ResultJSON.serialize(Ok(Ok(2))) // { type: 'ok', value: { type: 'ok', value: 2 } }

// deserialization
ResultJSON.deserialize({ type: 'ok', value: 1 }) // Ok(1)
ResultJSON.deserialize({ type: 'err', value: 'Some error message' }) // Err('Some error message')
ResultJSON.deserialize({ type: 'ok', value: { type: 'ok', value: 2 } }) // Ok({ type: 'ok', value: 2 }) *the nested `Result` won't be deserialized*
```

This simple implementation only covers a few use cases. It may not be suitable if:
- the `Result` has a nested `Result`
- the `Result` is in a complex structure
- the `Result` contains a complex object, such as a class instance, requiring custom (de)serialization

### Community (De)Serialization Solutions

There're some great JSON (de)serialization libraries for complex objects. This package also provides some helper functions to help you use some of them.

#### serializr

Please install `serializr` first, then you can use two helper functions `resultPropSchema` and `createResultModelSchema` as shown in the following example:

```ts
import { createResultModelSchema, resultPropSchema } from 'rustlike-result/serializr';

class User {
username: string;
password: string;
}

const userSchema = createModelSchema(User, {
username: primitive(),
password: primitive(),
})

// example 1

class Job {
result: Result<User[], string>;
}

const schema = createModelSchema(Job, {
result: resultPropSchema({ ok: list(object(userSchema)) }),
});

const job: Job;
serialize(schema, job)
// {
// result: {
// type: 'ok',
// value: [{ username: '<name>', password: '<password>' }, { ... }, ...],
// },
// }

// example 2

const schema = createResultModelSchema({ ok: list(object(userSchema)) });

const result: Result<User[], string>;
serialize(schema, result)
// {
// type: 'ok',
// value: [{ username: '<name>', password: '<password>' }, { ... }, ...],
// }
```

#### class-transformer

TODO.

### JSON Representation Format

The format of the JSON object follows the [adjacently tagged enum representation] in Rust library Serde.
The reason it doesn't follow the [externally tagged enum representation] (the default in Serde) is that, the externally tagged representation of `Ok(undefined)` and `Err(undefined)` are both `{}`, therefore we can't tell whether `{}` should be deserialized to `Ok(undefined)` or `Err(undefined)`.

[adjacently tagged enum representation]: https://serde.rs/enum-representations.html#adjacently-tagged
[externally tagged enum representation]: https://serde.rs/enum-representations.html#externally-tagged

## Write Your Own Implementation of `Result`?

Although you do have the ability to do so, it's not recommended that you write your own implementation.
Expand Down
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
"main": "lib-commonjs/index.js",
"module": "lib/index.js",
"types": "lib/index.d.ts",
"exports": {
".": {
"types": "./lib/index.d.ts",
"import": "./lib/index.js",
"default": "./lib-commonjs/index.js"
},
"./serializr": {
"types": "./lib/json/serializr.d.ts",
"import": "./lib/json/serializr.js",
"default": "./lib-commonjs/json/serializr.js"
}
},
"homepage": "https://github.com/yifanwww/rustlike-result#readme",
"license": "MIT",
"author": "yifanwww <yifanw1101@gmail.com> (https://github.com/yifanwww)",
Expand Down Expand Up @@ -50,8 +62,17 @@
"prettier": "3.0.3",
"rimraf": "^5.0.1",
"semver": "^7.5.4",
"serializr": "^3.0.2",
"typescript": "5.2.2"
},
"peerDependencies": {
"serializr": "^3.0.0"
},
"peerDependenciesMeta": {
"serializr": {
"optional": true
}
},
"keywords": [
"rust",
"rustlike",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
1 change: 0 additions & 1 deletion scripts/unit-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ function unitTest(watch) {
'--config',
require.resolve('./jest/jest.config.js'),
watch ? '--watch' : '--coverage',
'--passWithNoTests',
...argv,
);

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './factory';
export * from './json/simple';
export * from './json/types';
export * from './resultify';
export * from './types';
Loading

0 comments on commit 9bbecb6

Please sign in to comment.