diff --git a/.tool-versions b/.tool-versions index 11affa2..4e03911 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1,2 @@ -bun 1.0.35 +bun 1.0.11 purescript 0.15.15 diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 8179e33..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..493c8cd --- /dev/null +++ b/package-lock.json @@ -0,0 +1,202 @@ +{ + "name": "purs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "purs", + "dependencies": { + "pg": "^8.11.3" + }, + "devDependencies": { + "bun-types": "1.0.11", + "purs-tidy": "^0.10.0", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "postgres-range": "^1.1.4" + } + }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/bun-types": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.11.tgz", + "integrity": "sha512-XaDwjnBlkdTOtBEAcXhDnPSKFMlwFK/526z0iyairYIDhZJMzZM1QU4D7XRiEI2SpKQWexn0S/LN9Mwx5xSJNg==", + "dev": true + }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "peer": true + }, + "node_modules/purs-tidy": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/purs-tidy/-/purs-tidy-0.10.1.tgz", + "integrity": "sha512-i1QvMaDEaZXv/GWZNFWs5CISiBOkwPhG4D1S4Rw6zUCGaE+NQNWTjvwY21rifynGa2N2TiBJRC61LkORbmGxrA==", + "dev": true, + "bin": { + "purs-tidy": "bin/index.js" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json index c21954b..cae51fa 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,11 @@ }, "devDependencies": { "bun-types": "1.0.11", - "postgres-range": "^1.1.4", "purs-tidy": "^0.10.0", - "spago": "next" - }, - "peerDependencies": { "typescript": "^5.0.0" }, - "dependencies": { "pg-types": "^4.0.2" } + "dependencies": { + "postgres-range": "^1.1.4", + "pg": "^8.11.3" + } } diff --git a/spago.lock b/spago.lock index 1b9e047..d50ce4f 100644 --- a/spago.lock +++ b/spago.lock @@ -12,13 +12,16 @@ workspace: - foreign - lists - maybe + - mmorph - newtype - node-buffer - precise-datetime - prelude + - simple-json - transformers - unsafe-coerce test_dependencies: + - foreign-object - quickcheck - spec - spec-quickcheck @@ -45,6 +48,7 @@ workspace: - fixed-points - foldable-traversable - foreign + - foreign-object - fork - formatters - free @@ -80,6 +84,7 @@ workspace: - record - refs - safe-coerce + - simple-json - spec - spec-quickcheck - st @@ -88,9 +93,11 @@ workspace: - transformers - tuples - type-equality + - typelevel-prelude - unfoldable - unicode - unsafe-coerce + - variant extra_packages: {} packages: aff: @@ -333,6 +340,23 @@ packages: - prelude - strings - transformers + foreign-object: + type: registry + version: 4.1.0 + integrity: sha256-q24okj6mT+yGHYQ+ei/pYPj5ih6sTbu7eDv/WU56JVo= + dependencies: + - arrays + - foldable-traversable + - functions + - gen + - lists + - maybe + - prelude + - st + - tailrec + - tuples + - typelevel-prelude + - unfoldable fork: type: registry version: 6.0.0 @@ -735,6 +759,20 @@ packages: integrity: sha256-a1ibQkiUcbODbLE/WAq7Ttbbh9ex+x33VCQ7GngKudU= dependencies: - unsafe-coerce + simple-json: + type: registry + version: 9.0.0 + integrity: sha256-K3RJaThqsszTd+TEklzZmAdDqvIHWgXIfKqlsoykU1c= + dependencies: + - arrays + - exceptions + - foreign + - foreign-object + - nullable + - prelude + - record + - typelevel-prelude + - variant spec: type: registry version: 7.6.0 @@ -852,6 +890,13 @@ packages: version: 4.0.1 integrity: sha256-Hs9D6Y71zFi/b+qu5NSbuadUQXe5iv5iWx0226vOHUw= dependencies: [] + typelevel-prelude: + type: registry + version: 7.0.0 + integrity: sha256-uFF2ph+vHcQpfPuPf2a3ukJDFmLhApmkpTMviHIWgJM= + dependencies: + - prelude + - type-equality unfoldable: type: registry version: 6.0.0 @@ -875,3 +920,16 @@ packages: version: 6.0.0 integrity: sha256-IqIYW4Vkevn8sI+6aUwRGvd87tVL36BBeOr0cGAE7t0= dependencies: [] + variant: + type: registry + version: 8.0.0 + integrity: sha256-SR//zQDg2dnbB8ZHslcxieUkCeNlbMToapvmh9onTtw= + dependencies: + - enums + - lists + - maybe + - partial + - prelude + - record + - tuples + - unsafe-coerce diff --git a/spago.yaml b/spago.yaml index c7cd20c..6ecf5b9 100644 --- a/spago.yaml +++ b/spago.yaml @@ -13,15 +13,18 @@ package: - foreign - lists - maybe + - mmorph - newtype - node-buffer - precise-datetime - prelude + - simple-json - transformers - unsafe-coerce test: main: Test.Main dependencies: + - foreign-object - quickcheck - spec - spec-quickcheck diff --git a/src/Data.Postgres.js b/src/Data.Postgres.js index a833d11..58bb6f0 100644 --- a/src/Data.Postgres.js +++ b/src/Data.Postgres.js @@ -1,4 +1,4 @@ -import { getTypeParser, setTypeParser } from 'pg-types' +import * as Pg from 'pg' import * as Range from 'postgres-range' export const null_ = null @@ -20,18 +20,18 @@ export const modifyPgTypes = () => { // @ts-ignore const asString = a => a - const asStringArray = getTypeParser(oid['text[]']) + const asStringArray = Pg.types.getTypeParser(oid['text[]']) const asStringRange = Range.parse - setTypeParser(oid['json'], asString) - setTypeParser(oid['jsonb'], asString) - setTypeParser(oid['json[]'], asStringArray) - setTypeParser(oid['jsonb[]'], asStringArray) + Pg.types.setTypeParser(oid['json'], asString) + Pg.types.setTypeParser(oid['jsonb'], asString) + Pg.types.setTypeParser(oid['json[]'], asStringArray) + Pg.types.setTypeParser(oid['jsonb[]'], asStringArray) - setTypeParser(oid['timestamp'], asString) - setTypeParser(oid['timestamptz'], asString) - setTypeParser(oid['timestamp[]'], asStringArray) - setTypeParser(oid['timestamptz[]'], asStringArray) - setTypeParser(oid['tsrange'], asStringRange) - setTypeParser(oid['tstzrange'], asStringRange) + Pg.types.setTypeParser(oid['timestamp'], asString) + Pg.types.setTypeParser(oid['timestamptz'], asString) + Pg.types.setTypeParser(oid['timestamp[]'], asStringArray) + Pg.types.setTypeParser(oid['timestamptz[]'], asStringArray) + Pg.types.setTypeParser(oid['tsrange'], asStringRange) + Pg.types.setTypeParser(oid['tstzrange'], asStringRange) } diff --git a/src/Data.Postgres.purs b/src/Data.Postgres.purs index 75e9fbc..1ddf643 100644 --- a/src/Data.Postgres.purs +++ b/src/Data.Postgres.purs @@ -4,13 +4,14 @@ import Prelude import Control.Alt ((<|>)) import Control.Monad.Error.Class (liftEither, liftMaybe) -import Control.Monad.Except (Except, ExceptT, except, runExcept, runExceptT, withExceptT) +import Control.Monad.Except (ExceptT, runExceptT) +import Control.Monad.Morph (hoist) import Data.Bifunctor (lmap) import Data.DateTime (DateTime) import Data.Generic.Rep (class Generic) import Data.List.NonEmpty (NonEmptyList) import Data.Maybe (Maybe(..)) -import Data.Newtype (unwrap, wrap) +import Data.Newtype (class Newtype, unwrap, wrap) import Data.Postgres.Raw (Raw) import Data.Postgres.Raw (unsafeFromForeign, unsafeToForeign) as Raw import Data.RFC3339String as DateTime.ISO @@ -18,19 +19,21 @@ import Data.Show.Generic (genericShow) import Data.Traversable (traverse) import Effect (Effect) import Effect.Exception (error) -import Foreign (ForeignError) +import Foreign (ForeignError(..)) import Foreign as F import Node.Buffer (Buffer) +import Simple.JSON (class ReadForeign, class WriteForeign, readJSON', writeJSON) -newtype JSON = JSON String +newtype JSON a = JSON a + +derive instance Newtype (JSON a) _ +derive newtype instance WriteForeign a => WriteForeign (JSON a) +derive newtype instance ReadForeign a => ReadForeign (JSON a) foreign import null_ :: Raw --- | Important! This effect MUST be evaluated to guarantee --- | that (de)serialization will work for timestamp and JSON types. --- | --- | This mutates the `pg-types`, overriding the default deserialization --- | behavior for JSON and timestamp types. +-- | This mutates `import('pg').types`, setting deserialization +-- | for some types to unmarshal as strings rather than JS values. foreign import modifyPgTypes :: Effect Unit -- | The SQL value NULL @@ -43,35 +46,12 @@ instance Show Null where show = genericShow -- | The serialization & deserialization monad. -type RepT a = ExceptT RepError Effect a - --- | Errors encounterable while serializing & deserializing. -data RepError - = RepErrorTypeMismatch { expected :: String, found :: String } - | RepErrorInvalid String - | RepErrorForeign ForeignError - | RepErrorOther String - | RepErrorMultiple (NonEmptyList RepError) - -derive instance Generic RepError _ -derive instance Eq RepError -instance Show RepError where - show a = genericShow a - -instance Semigroup RepError where - append (RepErrorMultiple as) (RepErrorMultiple bs) = RepErrorMultiple (as <> bs) - append (RepErrorMultiple as) b = RepErrorMultiple (as <> pure b) - append a (RepErrorMultiple bs) = RepErrorMultiple (pure a <> bs) - append a b = RepErrorMultiple (pure a <> pure b) +type RepT a = ExceptT (NonEmptyList ForeignError) Effect a -- | Flatten to an Effect, rendering any `RepError`s to `String` using `Show`. smash :: forall a. RepT a -> Effect a smash = liftEither <=< map (lmap (error <<< show)) <<< runExceptT --- | Lift an `Except` returned by functions in the `Foreign` module to `RepT` -liftForeign :: forall a. Except (NonEmptyList ForeignError) a -> RepT a -liftForeign = except <<< runExcept <<< withExceptT (RepErrorMultiple <<< map RepErrorForeign) - -- | Serialize data of type `a` to a `Raw` SQL value. class Serialize a where serialize :: a -> RepT Raw @@ -95,6 +75,9 @@ instance Serialize Raw where instance Serialize Null where serialize _ = unsafeSerializeCoerce null_ +instance WriteForeign a => Serialize (JSON a) where + serialize = serialize <<< writeJSON <<< unwrap + -- | `bytea` instance Serialize Buffer where serialize = unsafeSerializeCoerce @@ -127,36 +110,39 @@ instance Serialize a => Serialize (Array a) where instance Deserialize Raw where deserialize = pure +instance Deserialize Null where + deserialize = map (const Null) <<< F.readNullOrUndefined <<< Raw.unsafeToForeign + +instance ReadForeign a => Deserialize (JSON a) where + deserialize = map wrap <<< (hoist (pure <<< unwrap) <<< readJSON') <=< deserialize @String + -- | `bytea` instance Deserialize Buffer where - deserialize = liftForeign <<< (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign - -instance Deserialize Null where - deserialize = map (const Null) <<< liftForeign <<< F.readNullOrUndefined <<< Raw.unsafeToForeign + deserialize = (F.unsafeReadTagged "Buffer") <<< Raw.unsafeToForeign instance Deserialize Int where - deserialize = liftForeign <<< F.readInt <<< Raw.unsafeToForeign + deserialize = F.readInt <<< Raw.unsafeToForeign instance Deserialize Boolean where - deserialize = liftForeign <<< F.readBoolean <<< Raw.unsafeToForeign + deserialize = F.readBoolean <<< Raw.unsafeToForeign instance Deserialize String where - deserialize = liftForeign <<< F.readString <<< Raw.unsafeToForeign + deserialize = F.readString <<< Raw.unsafeToForeign instance Deserialize Number where - deserialize = liftForeign <<< F.readNumber <<< Raw.unsafeToForeign + deserialize = F.readNumber <<< Raw.unsafeToForeign instance Deserialize Char where - deserialize = liftForeign <<< F.readChar <<< Raw.unsafeToForeign + deserialize = F.readChar <<< Raw.unsafeToForeign instance Deserialize DateTime where deserialize raw = do s :: String <- deserialize raw - let invalid = RepErrorInvalid $ "Not a valid ISO8601 string: `" <> s <> "`" + let invalid = pure $ ForeignError $ "Not a valid ISO8601 string: `" <> s <> "`" liftMaybe invalid $ DateTime.ISO.toDateTime $ wrap s instance Deserialize a => Deserialize (Array a) where - deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< liftForeign <<< F.readArray <<< Raw.unsafeToForeign + deserialize = traverse (deserialize <<< Raw.unsafeFromForeign) <=< F.readArray <<< Raw.unsafeToForeign instance Deserialize a => Deserialize (Maybe a) where deserialize raw = diff --git a/test/Test.Data.Postgres.purs b/test/Test.Data.Postgres.purs index 2a1858f..5a0a36f 100644 --- a/test/Test.Data.Postgres.purs +++ b/test/Test.Data.Postgres.purs @@ -8,8 +8,8 @@ import Data.DateTime (DateTime(..)) import Data.DateTime.Instant as Instant import Data.Int as Int import Data.Maybe (Maybe, fromJust, fromMaybe, maybe) -import Data.Newtype (wrap) -import Data.Postgres (class Rep, Null(..), deserialize, null_, serialize, smash) +import Data.Newtype (unwrap, wrap) +import Data.Postgres (class Rep, JSON(..), Null(..), deserialize, null_, serialize, smash) import Data.Postgres.Range as Range import Data.Postgres.Raw (Raw) import Data.Postgres.Raw as Raw @@ -19,7 +19,9 @@ import Effect.Class (liftEffect) import Effect.Console (log) import Effect.Unsafe (unsafePerformEffect) import Foreign (unsafeToForeign) +import Foreign.Object as Object import Partial.Unsafe (unsafePartial) +import Simple.JSON (writeImpl, writeJSON) import Test.QuickCheck (class Arbitrary, (==?)) import Test.Spec (Spec, describe, it) import Test.Spec.Assertions (shouldEqual) @@ -54,6 +56,21 @@ spec = check @(Array String) "Array String" identity asRaw check @DateTime "DateTime" dateTimeFromArbitrary (asRaw <<< DateTime.ISO.fromDateTime) + describe "JSON" do + describe "Record" do + it "deserialize" $ + quickCheck \(a /\ b /\ c :: Int /\ String /\ Array {"foo" :: String}) -> unsafePerformEffect do + let + obj = {a, b, c} + json = writeJSON obj + act :: JSON _ <- smash $ deserialize $ asRaw json + pure $ obj ==? unwrap act + it "serialize" $ + quickCheck \(a /\ b /\ c :: Int /\ String /\ Array {"foo" :: String}) -> unsafePerformEffect do + let obj = {a, b, c} + act <- smash $ serialize $ JSON obj + pure $ asRaw (writeJSON obj) ==? act + describe "Null" do it "serialize" $ liftEffect $ shouldEqual null_ =<< (smash $ serialize Null) it "deserialize" $ liftEffect $ shouldEqual Null =<< (smash $ deserialize null_)