Skip to content

Commit c9c7609

Browse files
Merge pull request #12 from LiamGoodacre/feature/config
Support configuring which keys are used for tag & values
2 parents 8db3423 + d5b6078 commit c9c7609

File tree

5 files changed

+76
-133
lines changed

5 files changed

+76
-133
lines changed

bower.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"purescript-argonaut-core": "^4.0.1",
20-
"purescript-argonaut-codecs": "^4.0.1",
20+
"purescript-argonaut-codecs": "^5.0.0",
2121
"purescript-generics-rep": "^6.0.0",
2222
"purescript-record": "^1.0.0"
2323
},
2424
"devDependencies": {
2525
"purescript-assert": "^4.0.0",
2626
"purescript-console": "^4.1.0"
2727
}
28-
}
28+
}

src/Data/Argonaut/Decode/Generic/Rep.purs

Lines changed: 21 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
module Data.Argonaut.Decode.Generic.Rep (
22
class DecodeRep,
33
class DecodeRepArgs,
4-
class DecodeRepRowList,
54
class DecodeLiteral,
6-
decodeRep,
5+
decodeRepWith,
76
decodeRepArgs,
8-
decodeRepRowList,
97
genericDecodeJson,
8+
genericDecodeJsonWith,
109
decodeLiteralSum,
1110
decodeLiteralSumWithTransform,
1211
decodeLiteral
1312
) where
1413

1514
import Prelude
15+
import Data.Argonaut.Types.Generic.Rep (Encoding, defaultEncoding)
1616

1717
import Control.Alt ((<|>))
1818
import Data.Argonaut.Core (Json, toArray, toObject, toString)
@@ -21,40 +21,35 @@ import Data.Array (uncons)
2121
import Data.Bifunctor (lmap)
2222
import Data.Either (Either(..))
2323
import Data.Generic.Rep as Rep
24-
import Data.Maybe (Maybe(..), maybe)
24+
import Data.Maybe (Maybe, maybe)
2525
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
2626
import Foreign.Object as FO
2727
import Partial.Unsafe (unsafeCrashWith)
28-
import Prim.Row as Row
29-
import Prim.RowList (class RowToList, Cons, Nil, kind RowList)
3028
import Prim.TypeError (class Fail, Text)
31-
import Record.Builder (Builder)
32-
import Record.Builder as Builder
33-
import Type.Data.RowList (RLProxy(..))
3429

3530
class DecodeRep r where
36-
decodeRep :: Json -> Either String r
31+
decodeRepWith :: Encoding -> Json -> Either String r
3732

3833
instance decodeRepNoConstructors :: DecodeRep Rep.NoConstructors where
39-
decodeRep _ = Left "Cannot decode empty data type"
34+
decodeRepWith e _ = Left "Cannot decode empty data type"
4035

4136
instance decodeRepSum :: (DecodeRep a, DecodeRep b) => DecodeRep (Rep.Sum a b) where
42-
decodeRep j = Rep.Inl <$> decodeRep j <|> Rep.Inr <$> decodeRep j
37+
decodeRepWith e j = Rep.Inl <$> decodeRepWith e j <|> Rep.Inr <$> decodeRepWith e j
4338

4439
instance decodeRepConstructor :: (IsSymbol name, DecodeRepArgs a) => DecodeRep (Rep.Constructor name a) where
45-
decodeRep j = do
40+
decodeRepWith e j = do
4641
let name = reflectSymbol (SProxy :: SProxy name)
4742
let decodingErr msg = "When decoding a " <> name <> ": " <> msg
4843
jObj <- mFail (decodingErr "expected an object") (toObject j)
49-
jTag <- mFail (decodingErr "'tag' property is missing") (FO.lookup "tag" jObj)
50-
tag <- mFail (decodingErr "'tag' property is not a string") (toString jTag)
44+
jTag <- mFail (decodingErr $ "'" <> e.tagKey <> "' property is missing") (FO.lookup e.tagKey jObj)
45+
tag <- mFail (decodingErr $ "'" <> e.tagKey <> "' property is not a string") (toString jTag)
5146
when (tag /= name) $
52-
Left $ decodingErr "'tag' property has an incorrect value"
53-
jValues <- mFail (decodingErr "'values' property is missing") (FO.lookup "values" jObj)
54-
values <- mFail (decodingErr "'values' property is not an array") (toArray jValues)
47+
Left $ decodingErr $ "'" <> e.tagKey <> "' property has an incorrect value"
48+
jValues <- mFail (decodingErr $ "'" <> e.valuesKey <> "' property is missing") (FO.lookup e.valuesKey jObj)
49+
values <- mFail (decodingErr $ "'" <> e.valuesKey <> "' property is not an array") (toArray jValues)
5550
{init, rest} <- lmap decodingErr $ decodeRepArgs values
5651
when (rest /= []) $
57-
Left $ decodingErr "'values' property had too many values"
52+
Left $ decodingErr $ "'" <> e.valuesKey <> "' property had too many values"
5853
pure $ Rep.Constructor init
5954

6055
class DecodeRepArgs r where
@@ -69,72 +64,18 @@ instance decodeRepArgsProduct :: (DecodeRepArgs a, DecodeRepArgs b) => DecodeRep
6964
{init: b, rest: js''} <- decodeRepArgs js'
7065
pure {init: Rep.Product a b, rest: js''}
7166

72-
instance decodeRepRecordArgument ::
73-
( RowToList row rl
74-
, DecodeRepRowList rl () row
75-
) => DecodeRepArgs (Rep.Argument (Record row)) where
76-
decodeRepArgs js = do
77-
{head, tail} <- mFail "to few values were present" (uncons js)
78-
obj <- mFail "no json object" (toObject head)
79-
steps <- decodeRepRowList rlp obj
80-
let arg = Rep.Argument $ Builder.build steps {}
81-
pure {init: arg, rest: tail}
82-
where
83-
rlp :: RLProxy rl
84-
rlp = RLProxy
85-
86-
else instance decodeRepArgsArgument :: (DecodeJson a) => DecodeRepArgs (Rep.Argument a) where
67+
instance decodeRepArgsArgument :: (DecodeJson a) => DecodeRepArgs (Rep.Argument a) where
8768
decodeRepArgs js = do
8869
{head, tail} <- mFail "too few values were present" (uncons js)
8970
{init: _, rest: tail} <<< Rep.Argument <$> decodeJson head
9071

91-
92-
-- | a `DecodeRepRowList` represents a relation between a `RowList` and a record you
93-
-- | can build from it by deserializing it's fields from a JSON `Object`
94-
-- |
95-
-- | this one is strictly internal to help out `decodeRepRecordArgument` handling records
96-
-- |
97-
-- | a `RowList` on the type level is very similar to a *cons-list* on the value level
98-
-- | so the two instances handle all possible `RowList`s
99-
-- |
100-
-- | the idea is to use `Builder` to convert a `RowList` into a record at the type-level
101-
-- | and have `decodeRepRowList` as witness on the value level that will try to decode
102-
-- | JSON in to the resulting record value
103-
-- |
104-
-- | `from` and `to` are two helper types - using these `decodeRepRowListCons` can
105-
-- | recursively create `Builder`-steps and make sure that every *symbol* in `rl`
106-
-- | can only occur once (the fields in the records must be unique)
107-
-- | (see `Row.Lacks`)
108-
class DecodeRepRowList (rl :: RowList) (from :: #Type) (to :: #Type) | rl -> from to where
109-
decodeRepRowList :: forall g . g rl -> FO.Object Json -> Either String (Builder (Record from) (Record to))
110-
111-
instance decodeRepRowListNil :: DecodeRepRowList Nil () () where
112-
decodeRepRowList _ _ = pure identity
113-
114-
instance decodeRepRowListCons ::
115-
( DecodeJson ty
116-
, IsSymbol name
117-
, DecodeRepRowList tail from from'
118-
, Row.Lacks name from'
119-
, Row.Cons name ty from' to
120-
) => DecodeRepRowList (Cons name ty tail) from to where
121-
decodeRepRowList _ obj = do
122-
value :: ty <- (error $ FO.lookup name obj) >>= decodeJson
123-
rest <- decodeRepRowList tailp obj
124-
let
125-
first :: Builder (Record from') (Record to)
126-
first = Builder.insert namep value
127-
pure $ first <<< rest
128-
where
129-
namep = SProxy :: SProxy name
130-
tailp = RLProxy :: RLProxy tail
131-
name = reflectSymbol namep
132-
error Nothing = Left ("error while decoding field " <> name)
133-
error (Just a) = Right a
134-
13572
-- | Decode `Json` representation of a value which has a `Generic` type.
13673
genericDecodeJson :: forall a r. Rep.Generic a r => DecodeRep r => Json -> Either String a
137-
genericDecodeJson = map Rep.to <<< decodeRep
74+
genericDecodeJson = genericDecodeJsonWith defaultEncoding
75+
76+
-- | Decode `Json` representation of a value which has a `Generic` type.
77+
genericDecodeJsonWith :: forall a r. Rep.Generic a r => DecodeRep r => Encoding -> Json -> Either String a
78+
genericDecodeJsonWith e = map Rep.to <<< decodeRepWith e
13879

13980
mFail :: forall a. String -> Maybe a -> Either String a
14081
mFail msg = maybe (Left msg) Right
@@ -164,7 +105,7 @@ instance decodeLiteralConstructor :: (IsSymbol name) => DecodeLiteral (Rep.Const
164105
pure $ Rep.Constructor (Rep.NoArguments)
165106

166107

167-
type FailMessage =
108+
type FailMessage =
168109
Text "`decodeLiteralSum` can only be used with sum types, where all of the constructors are nullary. This is because a string literal cannot be encoded into a product type."
169110

170111
instance decodeLiteralConstructorCannotTakeProduct

src/Data/Argonaut/Encode/Generic/Rep.purs

Lines changed: 16 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,43 @@ module Data.Argonaut.Encode.Generic.Rep (
22
class EncodeRep,
33
class EncodeRepArgs,
44
class EncodeRepFields,
5-
class EncodeRepRowList,
65
class EncodeLiteral,
7-
encodeRep,
6+
encodeRepWith,
87
encodeRepArgs,
98
encodeRepFields,
10-
encodeRepRowList,
119
genericEncodeJson,
10+
genericEncodeJsonWith,
1211
encodeLiteralSum,
1312
encodeLiteralSumWithTransform,
1413
encodeLiteral
1514
) where
1615

1716
import Prelude
17+
import Data.Argonaut.Types.Generic.Rep (Encoding, defaultEncoding)
1818

1919
import Data.Argonaut.Core (Json, fromArray, fromObject, fromString)
2020
import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson)
2121
import Data.Generic.Rep as Rep
2222
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
2323
import Foreign.Object as FO
2424
import Partial.Unsafe (unsafeCrashWith)
25-
import Prim.Row as Row
26-
import Prim.RowList (class RowToList, Cons, Nil, kind RowList)
2725
import Prim.TypeError (class Fail, Text)
28-
import Record (get)
29-
import Type.Data.RowList (RLProxy(..))
3026

3127
class EncodeRep r where
32-
encodeRep :: r -> Json
28+
encodeRepWith :: Encoding -> r -> Json
3329

3430
instance encodeRepNoConstructors :: EncodeRep Rep.NoConstructors where
35-
encodeRep r = encodeRep r
31+
encodeRepWith e = encodeRepWith e
3632

3733
instance encodeRepSum :: (EncodeRep a, EncodeRep b) => EncodeRep (Rep.Sum a b) where
38-
encodeRep (Rep.Inl a) = encodeRep a
39-
encodeRep (Rep.Inr b) = encodeRep b
34+
encodeRepWith e (Rep.Inl a) = encodeRepWith e a
35+
encodeRepWith e (Rep.Inr b) = encodeRepWith e b
4036

4137
instance encodeRepConstructor :: (IsSymbol name, EncodeRepArgs a) => EncodeRep (Rep.Constructor name a) where
42-
encodeRep (Rep.Constructor a) =
38+
encodeRepWith e (Rep.Constructor a) =
4339
fromObject
44-
$ FO.insert "tag" (fromString (reflectSymbol (SProxy :: SProxy name)))
45-
$ FO.insert "values" (fromArray (encodeRepArgs a))
40+
$ FO.insert e.tagKey (fromString (reflectSymbol (SProxy :: SProxy name)))
41+
$ FO.insert e.valuesKey (fromArray (encodeRepArgs a))
4642
$ FO.empty
4743

4844
class EncodeRepArgs r where
@@ -54,11 +50,7 @@ instance encodeRepArgsNoArguments :: EncodeRepArgs Rep.NoArguments where
5450
instance encodeRepArgsProduct :: (EncodeRepArgs a, EncodeRepArgs b) => EncodeRepArgs (Rep.Product a b) where
5551
encodeRepArgs (Rep.Product a b) = encodeRepArgs a <> encodeRepArgs b
5652

57-
instance encodeRepRecordArgument :: (RowToList row rl, EncodeRepRowList rl row) => EncodeRepArgs (Rep.Argument (Record row)) where
58-
encodeRepArgs (Rep.Argument rec) = [ fromObject (encodeRepRowList rlp rec FO.empty) ]
59-
where rlp = RLProxy :: RLProxy rl
60-
61-
else instance encodeRepArgsArgument :: (EncodeJson a) => EncodeRepArgs (Rep.Argument a) where
53+
instance encodeRepArgsArgument :: (EncodeJson a) => EncodeRepArgs (Rep.Argument a) where
6254
encodeRepArgs (Rep.Argument a) = [encodeJson a]
6355

6456
class EncodeRepFields r where
@@ -69,38 +61,13 @@ instance encodeRepFieldsProduct :: (EncodeRepFields a, EncodeRepFields b) => Enc
6961
FO.union (encodeRepFields a) (encodeRepFields b)
7062

7163

72-
-- | a `EncodeRepRowList` represents a relation between a `RowList` and a record you
73-
-- | can serialize into a Json `Object`
74-
-- |
75-
-- | this one is strictly internal to help out `encodeRepRecordArgument` handling records
76-
-- |
77-
-- | a `RowList` on the type level is very similar to a *cons-list* on the value level
78-
-- | so the two instances handle all possible `RowList`s
79-
-- |
80-
-- | the idea is use the `Cons` cases to to compose functions that adds the field
81-
-- | and values from the given record into a Json-`Object`
82-
-- | the field in question is indicated by the head of the `RowList`
83-
-- |
84-
-- | the `Nil` case just returns `identity` to bootstrap the composition-chain
85-
class EncodeRepRowList (rl :: RowList) (row :: #Type) | rl -> row where
86-
encodeRepRowList :: forall g . g rl -> Record row -> (FO.Object Json -> FO.Object Json)
87-
88-
instance encodeRepRowListNil :: EncodeRepRowList Nil row where
89-
encodeRepRowList _ _ = identity
90-
91-
instance encodeRepRowListCons :: (EncodeJson ty, IsSymbol name, EncodeRepRowList tail row, Row.Cons name ty ignore row) => EncodeRepRowList (Cons name ty tail) row where
92-
encodeRepRowList _ rec = \obj -> FO.insert (reflectSymbol namep) (encodeJson value) (cont obj)
93-
where
94-
namep = SProxy :: SProxy name
95-
value = get namep rec
96-
tailp = RLProxy :: RLProxy tail
97-
cont = encodeRepRowList tailp rec
98-
99-
100-
10164
-- | Encode any `Generic` data structure into `Json`.
10265
genericEncodeJson :: forall a r. Rep.Generic a r => EncodeRep r => a -> Json
103-
genericEncodeJson = encodeRep <<< Rep.from
66+
genericEncodeJson = genericEncodeJsonWith defaultEncoding
67+
68+
-- | Encode any `Generic` data structure into `Json`.
69+
genericEncodeJsonWith :: forall a r. Rep.Generic a r => EncodeRep r => Encoding -> a -> Json
70+
genericEncodeJsonWith e = encodeRepWith e <<< Rep.from
10471

10572
-- | A function for encoding `Generic` sum types using string literal representations
10673
encodeLiteralSum :: forall a r. Rep.Generic a r => EncodeLiteral r => a -> Json
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Data.Argonaut.Types.Generic.Rep (
2+
Encoding(..),
3+
defaultEncoding
4+
) where
5+
6+
type Encoding =
7+
{ tagKey :: String
8+
, valuesKey :: String
9+
}
10+
11+
defaultEncoding :: Encoding
12+
defaultEncoding =
13+
{ tagKey: "tag"
14+
, valuesKey: "values"
15+
}
16+

test/Main.purs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import Effect (Effect)
88
import Effect.Console (log)
99
import Data.Argonaut.Core (stringify)
1010
import Data.Argonaut.Decode.Class (class DecodeJson, decodeJson)
11-
import Data.Argonaut.Decode.Generic.Rep (class DecodeLiteral, decodeLiteralSumWithTransform, genericDecodeJson)
11+
import Data.Argonaut.Decode.Generic.Rep (class DecodeLiteral, decodeLiteralSumWithTransform, genericDecodeJson, genericDecodeJsonWith)
1212
import Data.Argonaut.Encode.Class (class EncodeJson, encodeJson)
13-
import Data.Argonaut.Encode.Generic.Rep (class EncodeLiteral, encodeLiteralSumWithTransform, genericEncodeJson)
13+
import Data.Argonaut.Encode.Generic.Rep (class EncodeLiteral, encodeLiteralSumWithTransform, genericEncodeJson, genericEncodeJsonWith)
14+
import Data.Argonaut.Types.Generic.Rep (Encoding, defaultEncoding)
1415
import Data.Argonaut.Parser (jsonParser)
1516
import Data.Either (Either(..), fromRight)
1617
import Data.Generic.Rep (class Generic)
@@ -47,13 +48,31 @@ instance encodeJsonLiteralStringExample :: EncodeJson LiteralStringExample where
4748
instance decodeJsonLiteralStringExample :: DecodeJson LiteralStringExample where
4849
decodeJson a = decodeLiteralSumWithTransform identity a
4950

51+
diffEncodingOptions :: Encoding
52+
diffEncodingOptions = defaultEncoding
53+
{ tagKey = "type"
54+
, valuesKey = "value"
55+
}
56+
57+
data DiffEncoding = A | B Int
58+
derive instance eqDiffEncoding :: Eq DiffEncoding
59+
derive instance genericDiffEncoding :: Generic DiffEncoding _
60+
instance showDiffENcoding :: Show DiffEncoding where
61+
show a = genericShow a
62+
instance encodeJsonDiffEncoding :: EncodeJson DiffEncoding where
63+
encodeJson a = genericEncodeJsonWith diffEncodingOptions a
64+
instance decodeJsonDiffEncoding :: DecodeJson DiffEncoding where
65+
decodeJson a = genericDecodeJsonWith diffEncodingOptions a
66+
5067
main :: Effect Unit
5168
main = do
5269
example $ Either $ Left "foo"
5370
example $ Either $ Right $ Either $ Left "foo"
5471
example $ Record {foo: 42, bar: "bar"}
5572
example $ Product 1 2 $ Either $ Left "foo"
5673
example $ Frikandel
74+
example $ A
75+
example $ B 42
5776
testLiteralSumWithTransform identity Frikandel "\"Frikandel\""
5877
testLiteralSumWithTransform toUpper Frikandel "\"FRIKANDEL\""
5978
testLiteralSumWithTransform toLower Frikandel "\"frikandel\""

0 commit comments

Comments
 (0)