Skip to content

Commit 17a2446

Browse files
authored
Merge pull request #52 from garyb/misc
Misc bits and pieces
2 parents 0807876 + 806aa21 commit 17a2446

File tree

13 files changed

+229
-28
lines changed

13 files changed

+229
-28
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
Bi-directional codecs for [argonaut](https://github.com/purescript-contrib/purescript-argonaut-core).
77

8-
This library is build on [`purescript-codec`](https://github.com/garyb/purescript-codec) and offers a different approach to dealing with JSON encoding/decoding than [`purescript-argonaut-codecs`](https://github.com/purescript-contrib/purescript-argonaut-codecs). Instead of using type classes, codecs are constructed as values explicitly. As long as the basic codec values provided by this library are used, the codecs are guaranteed to roundtrip successfully.
8+
This library is built on [`purescript-codec`](https://github.com/garyb/purescript-codec) and offers a different approach to dealing with JSON encoding/decoding than [`purescript-argonaut-codecs`](https://github.com/purescript-contrib/purescript-argonaut-codecs). Instead of using type classes, codecs are constructed as values explicitly. As long as the basic codec values provided by this library are used, the codecs are guaranteed to roundtrip successfully.
99

1010
The errors reported from this library are a little better than those provided by `purescript-argonaut-codecs` too - they contain the full JSON structure to the point of failure, and the error can be inspected as a value before being printed as a string.
1111

@@ -115,7 +115,11 @@ codec =
115115
Objects with optional properties can be defined using the [`CAR.optional`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut.Record#v:optional):
116116

117117
```purescript
118-
type Person =
118+
import Data.Codec.Argonaut as CA
119+
import Data.Codec.Argonaut.Record as CAR
120+
import Data.Maybe (Maybe)
121+
122+
type Person =
119123
{ name ∷ String
120124
, age ∷ Int
121125
, active ∷ Boolean
@@ -258,7 +262,7 @@ import Data.String.NonEmpty (NonEmptyString)
258262
import Data.String.NonEmpty as NES
259263
260264
codec ∷ CA.JsonCodec NonEmptyString
261-
codec = CA.prismaticCodec NES.fromString NES.toString CA.string
265+
codec = CA.prismaticCodec "NonEmptyString" NES.fromString NES.toString CA.string
262266
```
263267

264268
See the documentation for [another example](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut#v:prismaticCodec) of how [`CA.prismaticCodec`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut#v:prismaticCodec) might be used. The main downside to [`CA.prismaticCodec`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut#v:prismaticCodec) is the error reporting for the `Nothing` case might not be good as it otherwise could be, since [`UnexpectedValue`](https://pursuit.purescript.org/packages/purescript-codec-argonaut/docs/Data.Codec.Argonaut#t:JsonDecodeError) is the only information we have at that point.

src/Data/Codec/Argonaut.purs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ module Data.Codec.Argonaut
2424
, recordProp
2525
, recordPropOptional
2626
, fix
27+
, named
28+
, coercible
2729
, prismaticCodec
2830
, module Exports
2931
) where
@@ -34,6 +36,7 @@ import Control.Monad.Reader (ReaderT(..), runReaderT)
3436
import Control.Monad.Writer (Writer, mapWriter, writer)
3537
import Data.Argonaut.Core as J
3638
import Data.Array as A
39+
import Data.Bifunctor (bimap, lmap)
3740
import Data.Bifunctor as BF
3841
import Data.Codec (BasicCodec, Codec, GCodec(..), basicCodec, bihoistGCodec, decode, encode)
3942
import Data.Codec (decode, encode, (<~<), (>~>), (~)) as Exports
@@ -51,7 +54,9 @@ import Data.Traversable (traverse)
5154
import Data.Tuple (Tuple(..))
5255
import Foreign.Object as FO
5356
import Partial.Unsafe (unsafePartial)
57+
import Prim.Coerce (class Coercible)
5458
import Prim.Row as Row
59+
import Safe.Coerce (coerce)
5560
import Type.Proxy (Proxy)
5661
import Unsafe.Coerce (unsafeCoerce)
5762

@@ -369,6 +374,24 @@ fix f =
369374
(\x → decode (f (fix f)) x)
370375
(\x → encode (f (fix f)) x)
371376

377+
-- | A codec for introducing names into error messages - useful when definiting a codec for a type
378+
-- | synonym for a record, for instance.
379+
named a. String JsonCodec a -> JsonCodec a
380+
named name codec =
381+
basicCodec
382+
(lmap (Named name) <<< decode codec)
383+
(encode codec)
384+
385+
-- | A codec for types that can be safely coerced.
386+
-- |
387+
-- | Accepts the name of the target type as an argument to improve error messaging when the inner
388+
-- | codec fails.
389+
coercible a b. Coercible a b String JsonCodec a JsonCodec b
390+
coercible name codec =
391+
basicCodec
392+
(bimap (Named name) coerce <<< decode codec)
393+
(coerce (encode codec))
394+
372395
-- | Adapts an existing codec with a pair of functions to allow a value to be
373396
-- | further refined. If the inner decoder fails an `UnexpectedValue` error will
374397
-- | be raised for JSON input.

src/Data/Codec/Argonaut/Common.purs

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,48 @@
11
module Data.Codec.Argonaut.Common
2-
( module Data.Codec.Argonaut.Common
2+
( nonEmptyString
3+
, nonEmptyArray
4+
, maybe
5+
, tuple
6+
, either
7+
, list
8+
, nonEmptyList
9+
, map
10+
, set
11+
, nonEmptySet
12+
, foreignObject
313
, module Data.Codec.Argonaut
414
) where
515

6-
import Prelude hiding (map)
16+
import Prelude hiding (map, void)
717

8-
import Data.Array as A
9-
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, recordPropOptional, string, (<~<), (~))
18+
import Data.Array as Array
19+
import Data.Array.NonEmpty as NEA
20+
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, codePoint, coercible, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, named, null, number, object, printJsonDecodeError, prismaticCodec, prop, record, recordProp, recordPropOptional, string, void, (<~<), (>~>), (~))
1021
import Data.Codec.Argonaut.Sum (taggedSum)
1122
import Data.Either (Either(..))
1223
import Data.Functor as F
13-
import Data.List as L
14-
import Data.Map as M
24+
import Data.List as List
25+
import Data.List.NonEmpty as NEL
26+
import Data.Map as Map
1527
import Data.Maybe (Maybe(..))
1628
import Data.Profunctor (dimap)
29+
import Data.Set as Set
30+
import Data.Set.NonEmpty as NESet
31+
import Data.String.NonEmpty as NEString
1732
import Data.Tuple (Tuple(..), fst, snd)
18-
import Foreign.Object as FO
33+
import Foreign.Object as Object
34+
35+
-- | A codec for `NonEmptyString` values.
36+
-- |
37+
-- | Encodes as the standard type in JSON, but will fail to decode if the string is empty.
38+
nonEmptyString JsonCodec NEString.NonEmptyString
39+
nonEmptyString = prismaticCodec "NonEmptyString" NEString.fromString NEString.toString string
40+
41+
-- | A codec for `NonEmptyArray` values.
42+
-- |
43+
-- | Encodes as the standard type in JSON, but will fail to decode if the array is empty.
44+
nonEmptyArray a. JsonCodec a JsonCodec (NEA.NonEmptyArray a)
45+
nonEmptyArray codec = prismaticCodec "NonEmptyArray" NEA.fromArray NEA.toArray (array codec)
1946

2047
-- | A codec for `Maybe` values.
2148
-- |
@@ -68,17 +95,35 @@ either codecA codecB = taggedSum "Either" printTag parseTag dec enc
6895
-- | A codec for `List` values.
6996
-- |
7097
-- | Encodes as an array in JSON.
71-
list a. JsonCodec a JsonCodec (L.List a)
72-
list = dimap A.fromFoldable L.fromFoldable <<< array
98+
list a. JsonCodec a JsonCodec (List.List a)
99+
list codec = dimap Array.fromFoldable List.fromFoldable (named "List" (array codec))
100+
101+
-- | A codec for `NonEmptyList` values.
102+
-- |
103+
-- | Encodes as an array in JSON.
104+
nonEmptyList a. JsonCodec a JsonCodec (NEL.NonEmptyList a)
105+
nonEmptyList codec = prismaticCodec "NonEmptyList" NEL.fromFoldable Array.fromFoldable (array codec)
73106

74107
-- | A codec for `Map` values.
75108
-- |
76109
-- | Encodes as an array of two-element key/value arrays in JSON.
77-
map a b. Ord a JsonCodec a JsonCodec b JsonCodec (M.Map a b)
78-
map codecA = dimap M.toUnfoldable M.fromFoldable <<< array <<< tuple codecA
110+
map a b. Ord a JsonCodec a JsonCodec b JsonCodec (Map.Map a b)
111+
map codecA codecB = dimap Map.toUnfoldable (Map.fromFoldable) (named "Map" (array (tuple codecA codecB)))
112+
113+
-- | A codec for `Set` values.
114+
-- |
115+
-- | Encodes as an array in JSON.
116+
set a. Ord a JsonCodec a JsonCodec (Set.Set a)
117+
set codec = dimap Array.fromFoldable Set.fromFoldable (named "Set" (array codec))
118+
119+
-- | A codec for `NonEmptySet` values.
120+
-- |
121+
-- | Encodes as an array in JSON.
122+
nonEmptySet a. Ord a JsonCodec a JsonCodec (NESet.NonEmptySet a)
123+
nonEmptySet codec = prismaticCodec "NonEmptySet" NESet.fromFoldable NESet.toUnfoldable (array codec)
79124

80125
-- | A codec for `StrMap` values.
81126
-- |
82127
-- | Encodes as an array of two-element key/value arrays in JSON.
83-
foreignObject a. JsonCodec a JsonCodec (FO.Object a)
84-
foreignObject = dimap FO.toUnfoldable FO.fromFoldable <<< array <<< tuple string
128+
foreignObject a. JsonCodec a JsonCodec (Object.Object a)
129+
foreignObject = dimap Object.toUnfoldable Object.fromFoldable <<< array <<< tuple string

src/Data/Codec/Argonaut/Compat.purs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
-- | Codecs that are compatible with `purescript-argonaut-codecs`.
22
module Data.Codec.Argonaut.Compat
33
( module Data.Codec.Argonaut.Compat
4-
, module Data.Codec.Argonaut
5-
, module Common
4+
, module Data.Codec.Argonaut.Common
65
) where
76

8-
import Prelude
7+
import Prelude hiding (void)
98

109
import Data.Argonaut.Core as J
1110
import Data.Bifunctor as BF
1211
import Data.Codec (basicCodec, mapCodec)
13-
import Data.Codec.Argonaut (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, decode, encode, fix, index, indexedArray, int, jarray, jobject, json, null, number, object, printJsonDecodeError, prop, record, recordProp, recordPropOptional, string, (<~<), (~))
14-
import Data.Codec.Argonaut.Common (either, list, map, tuple) as Common
12+
import Data.Codec.Argonaut.Common (JIndexedCodec, JPropCodec, JsonCodec, JsonDecodeError(..), array, boolean, char, codePoint, coercible, decode, either, encode, fix, index, indexedArray, int, jarray, jobject, json, list, named, nonEmptyArray, nonEmptyList, nonEmptySet, nonEmptyString, null, number, object, printJsonDecodeError, prismaticCodec, prop, record, recordProp, recordPropOptional, set, string, tuple, void, (<~<), (>~>), (~))
1513
import Data.Either (Either)
1614
import Data.Functor as F
1715
import Data.Maybe (Maybe(..))

test/Test/Example/Newtype.purs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Test.Example.Newtype where
2+
3+
import Data.Codec.Argonaut.Common as CA
4+
import Data.Codec.Argonaut.Record as CAR
5+
import Data.Newtype (class Newtype)
6+
import Data.Profunctor (wrapIso)
7+
8+
type PersonRec = { "Name" String, age Int, "is active"Boolean }
9+
10+
newtype Person = Person PersonRec
11+
12+
derive instance newtypePersonNewtype Person _
13+
14+
codec CA.JsonCodec Person
15+
codec =
16+
wrapIso Person
17+
(CAR.object "Person"
18+
{ "Name": CA.string
19+
, age: CA.int
20+
, "is active": CA.boolean
21+
})

test/Test/Example/Object1.purs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Test.Example.Object1 where
2+
3+
import Data.Codec.Argonaut as CA
4+
import Data.Codec.Argonaut.Record as CAR
5+
6+
type Person = { "Name" String, age Int, "is active"Boolean }
7+
8+
codec CA.JsonCodec Person
9+
codec =
10+
CA.object "Person"
11+
(CAR.record
12+
{ "Name": CA.string
13+
, age: CA.int
14+
, "is active": CA.boolean
15+
})

test/Test/Example/Object2.purs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module Test.Example.Object2 where
2+
3+
import Data.Codec.Argonaut as CA
4+
import Data.Codec.Argonaut.Record as CAR
5+
6+
type Person = { name String, age Int, active Boolean }
7+
8+
codec CA.JsonCodec Person
9+
codec =
10+
CA.object "Person"
11+
(CAR.record
12+
{ name: CA.string
13+
, age: CA.int
14+
, active: CA.boolean
15+
})

test/Test/Example/Object3.purs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module Test.Example.Object3 where
2+
3+
import Data.Codec.Argonaut as CA
4+
import Data.Codec.Argonaut.Record as CAR
5+
import Data.Maybe (Maybe)
6+
7+
type Person =
8+
{ name String
9+
, age Int
10+
, active Boolean
11+
, email Maybe String
12+
}
13+
14+
codec CA.JsonCodec Person
15+
codec =
16+
CA.object "Person"
17+
( CAR.record
18+
{ name: CA.string
19+
, age: CA.int
20+
, active: CA.boolean
21+
, email: CAR.optional CA.string
22+
}
23+
)

test/Test/Example/Prismatic1.purs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module Test.Example.Prismatic1 where
2+
3+
import Data.Codec.Argonaut as CA
4+
import Data.String.NonEmpty (NonEmptyString)
5+
import Data.String.NonEmpty as NES
6+
7+
codec CA.JsonCodec NonEmptyString
8+
codec = CA.prismaticCodec "NonEmptyString" NES.fromString NES.toString CA.string

test/Test/Example/Sum1.purs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Test.Example.Sum1 where
2+
3+
import Prelude
4+
5+
import Data.Codec.Argonaut as CA
6+
import Data.Codec.Argonaut.Variant as CAV
7+
import Data.Either (Either(..))
8+
import Data.Variant as V
9+
10+
type SomeValue = V.Variant
11+
( str String
12+
, int Int
13+
, neither Unit
14+
)
15+
16+
codec CA.JsonCodec SomeValue
17+
codec = CAV.variantMatch
18+
{ str: Right CA.string
19+
, int: Right CA.int
20+
, neither: Left unit
21+
}

0 commit comments

Comments
 (0)