diff --git a/docs/modules/Codec.ts.md b/docs/modules/Codec.ts.md index 841c0f3f..7728a51c 100644 --- a/docs/modules/Codec.ts.md +++ b/docs/modules/Codec.ts.md @@ -31,6 +31,7 @@ Added in v2.2.3 - [fromSum](#fromsum) - [fromTuple](#fromtuple) - [intersect](#intersect) + - [json](#json) - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) @@ -194,6 +195,16 @@ export declare const intersect: ( Added in v2.2.3 +## json + +**Signature** + +```ts +export declare const json: Codec +``` + +Added in v2.2.17 + ## lazy **Signature** diff --git a/docs/modules/Decoder.ts.md b/docs/modules/Decoder.ts.md index e2b947d7..d5c0e3fb 100644 --- a/docs/modules/Decoder.ts.md +++ b/docs/modules/Decoder.ts.md @@ -41,6 +41,7 @@ Added in v2.2.7 - [fromSum](#fromsum) - [fromTuple](#fromtuple) - [intersect](#intersect) + - [json](#json) - [lazy](#lazy) - [mapLeftWithInput](#mapleftwithinput) - [nullable](#nullable) @@ -275,6 +276,16 @@ export declare const intersect: ( Added in v2.2.7 +## json + +**Signature** + +```ts +export declare const json: Decoder +``` + +Added in v2.2.17 + ## lazy **Signature** diff --git a/docs/modules/Encoder.ts.md b/docs/modules/Encoder.ts.md index 153decb6..c2eb4175 100644 --- a/docs/modules/Encoder.ts.md +++ b/docs/modules/Encoder.ts.md @@ -28,6 +28,7 @@ Added in v2.2.3 - [combinators](#combinators) - [array](#array) - [intersect](#intersect) + - [json](#json) - [lazy](#lazy) - [nullable](#nullable) - [partial](#partial) @@ -108,6 +109,16 @@ export declare const intersect: (right: Encoder) => (left: Enc Added in v2.2.3 +## json + +**Signature** + +```ts +export declare const json: Encoder +``` + +Added in v2.2.17 + ## lazy **Signature** diff --git a/package-lock.json b/package-lock.json index 5234b1b7..71473318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14286,9 +14286,9 @@ "dev": true }, "fp-ts": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.5.0.tgz", - "integrity": "sha512-xkC9ZKl/i2cU+8FAsdyLcTvPRXphp42FcK5WmZpB47VXb4gggC3DHlVDKNLdbC+U8zz6yp1b0bj0mZg0axmZYQ==", + "version": "2.10.5", + "resolved": "https://registry.npmjs.org/fp-ts/-/fp-ts-2.10.5.tgz", + "integrity": "sha512-X2KfTIV0cxIk3d7/2Pvp/pxL/xr2MV1WooyEzKtTWYSc1+52VF4YzjBTXqeOlSiZsPCxIBpDGfT9Dyo7WEY0DQ==", "dev": true }, "fragment-cache": { @@ -17911,6 +17911,11 @@ "ret": "~0.1.10" } }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", diff --git a/package.json b/package.json index a6a8108b..2dc6f07d 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,11 @@ "url": "https://github.com/gcanti/io-ts/issues" }, "homepage": "https://github.com/gcanti/io-ts", + "dependencies": { + "safe-stable-stringify": "^2.3.1" + }, "peerDependencies": { - "fp-ts": "^2.5.0" + "fp-ts": "^2.10.0" }, "devDependencies": { "@types/benchmark": "1.0.31", @@ -53,7 +56,7 @@ "dtslint": "github:gcanti/dtslint", "eslint": "^7.18.0", "fast-check": "^1.24.2", - "fp-ts": "^2.5.0", + "fp-ts": "^2.10.5", "import-path-rewrite": "github:gcanti/import-path-rewrite", "jest": "25.2.7", "mocha": "7.1.1", diff --git a/src/Codec.ts b/src/Codec.ts index 9465e02e..962410d2 100644 --- a/src/Codec.ts +++ b/src/Codec.ts @@ -8,6 +8,7 @@ * * @since 2.2.3 */ +import { Json } from 'fp-ts/lib/Json' import { identity, Refinement } from 'fp-ts/lib/function' import { Invariant3 } from 'fp-ts/lib/Invariant' import { pipe } from 'fp-ts/lib/pipeable' @@ -142,6 +143,12 @@ export function nullable(or: Codec): Codec = make(D.json, E.json) + /** * @category combinators * @since 2.2.15 diff --git a/src/Decoder.ts b/src/Decoder.ts index 702cc9bb..2e94ba67 100644 --- a/src/Decoder.ts +++ b/src/Decoder.ts @@ -12,7 +12,8 @@ import { Alt2, Alt2C } from 'fp-ts/lib/Alt' import { Bifunctor2 } from 'fp-ts/lib/Bifunctor' import { Category2 } from 'fp-ts/lib/Category' import * as E from 'fp-ts/lib/Either' -import { identity, Refinement } from 'fp-ts/lib/function' +import { flow, identity, Refinement } from 'fp-ts/lib/function' +import * as J from 'fp-ts/lib/Json' import { Functor2 } from 'fp-ts/lib/Functor' import { MonadThrow2C } from 'fp-ts/lib/MonadThrow' import { pipe } from 'fp-ts/lib/pipeable' @@ -21,6 +22,7 @@ import * as FS from './FreeSemigroup' import * as G from './Guard' import * as K from './Kleisli' import * as S from './Schemable' +import Json = J.Json // ------------------------------------------------------------------------------------- // Kleisli config @@ -226,6 +228,23 @@ export const nullable: (or: Decoder) => Decoder /*#__PURE__*/ K.nullable(M)((u, e) => FS.concat(FS.of(DE.member(0, error(u, 'null'))), FS.of(DE.member(1, e)))) +/** + * @category combinators + * @since 2.2.17 + */ +export const json: Decoder = { + decode: (i) => + pipe( + string.decode(i), + E.chain( + flow( + J.parse, + E.altW(() => failure(i, 'JSON')) + ) + ) + ) +} + /** * @category combinators * @since 2.2.15 diff --git a/src/Encoder.ts b/src/Encoder.ts index 8cf7a085..476bb0f2 100644 --- a/src/Encoder.ts +++ b/src/Encoder.ts @@ -10,8 +10,10 @@ */ import { Contravariant2 } from 'fp-ts/lib/Contravariant' import { Category2 } from 'fp-ts/lib/Category' +import { Json } from 'fp-ts/lib/Json' import { memoize, intersect_ } from './Schemable' import { identity } from 'fp-ts/lib/function' +import safeStringify from 'safe-stable-stringify' // ------------------------------------------------------------------------------------- // model @@ -39,6 +41,14 @@ export function nullable(or: Encoder): Encoder { } } +/** + * @category combinators + * @since 2.2.17 + */ +export const json: Encoder = { + encode: safeStringify +} + /** * @category combinators * @since 2.2.15 diff --git a/test/Codec.ts b/test/Codec.ts index 8b228ee3..376d34d0 100644 --- a/test/Codec.ts +++ b/test/Codec.ts @@ -205,6 +205,33 @@ describe('Codec', () => { }) }) + describe('json', () => { + describe('decode', () => { + it('should decode valid JSON', () => { + assert.deepStrictEqual(_.json.decode('null'), D.success(null)) + assert.deepStrictEqual(_.json.decode('"a"'), D.success('a')) + assert.deepStrictEqual(_.json.decode('{"a":1}'), D.success({ a: 1 })) + }) + + it('should reject invalid JSON', () => { + assert.deepStrictEqual(_.json.decode(undefined), D.failure(undefined, 'string')) + assert.deepStrictEqual(_.json.decode('{"a":}'), D.failure('{"a":}', 'JSON')) + }) + }) + + describe('encode', () => { + it('should encode a value', () => { + const circular: any = { ref: null } + circular.ref = circular + + assert.deepStrictEqual(_.json.encode(null), 'null') + assert.deepStrictEqual(_.json.encode('a'), '"a"') + assert.deepStrictEqual(_.json.encode({ a: 1 }), '{"a":1}') + assert.deepStrictEqual(_.json.encode(circular), '{"ref":"[Circular]"}') + }) + }) + }) + describe('struct', () => { describe('decode', () => { it('should decode a valid input', () => { diff --git a/test/Decoder.ts b/test/Decoder.ts index a7e82449..a1a768ca 100644 --- a/test/Decoder.ts +++ b/test/Decoder.ts @@ -153,6 +153,19 @@ describe('Decoder', () => { }) }) + describe('json', () => { + it('should decode valid JSON', () => { + assert.deepStrictEqual(_.json.decode('null'), _.success(null)) + assert.deepStrictEqual(_.json.decode('"a"'), _.success('a')) + assert.deepStrictEqual(_.json.decode('{"a":1}'), _.success({ a: 1 })) + }) + + it('should reject invalid JSON', () => { + assert.deepStrictEqual(_.json.decode(undefined), _.failure(undefined, 'string')) + assert.deepStrictEqual(_.json.decode('{"a":}'), _.failure('{"a":}', 'JSON')) + }) + }) + describe('struct', () => { it('should decode a valid input', async () => { const decoder = _.struct({ diff --git a/test/Encoder.ts b/test/Encoder.ts index d4155a63..26491534 100644 --- a/test/Encoder.ts +++ b/test/Encoder.ts @@ -20,6 +20,16 @@ describe('Encoder', () => { assert.deepStrictEqual(encoder.encode(null), null) }) + it('json', () => { + const circular: any = { ref: null } + circular.ref = circular + + assert.deepStrictEqual(E.json.encode(null), 'null') + assert.deepStrictEqual(E.json.encode('a'), '"a"') + assert.deepStrictEqual(E.json.encode({ a: 1 }), '{"a":1}') + assert.deepStrictEqual(E.json.encode(circular), '{"ref":"[Circular]"}') + }) + it('struct', () => { const encoder = E.struct({ a: H.encoderNumberToString, b: H.encoderBooleanToNumber }) assert.deepStrictEqual(encoder.encode({ a: 1, b: true }), { a: '1', b: 1 }) diff --git a/tslint.json b/tslint.json index 3c8e16e7..52f211d3 100644 --- a/tslint.json +++ b/tslint.json @@ -6,6 +6,6 @@ "variable-name": false, "ter-indent": false, "strict-type-predicates": false, - "deprecation": true + "deprecation": false } }