Skip to content

Commit 3dec91e

Browse files
committed
Add ToJSONKey and FromJSONKey instances
1 parent 14a85ae commit 3dec91e

3 files changed

Lines changed: 52 additions & 9 deletions

File tree

‎json-pointer.cabal‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ test-suite spec
6161
build-depends:
6262
, aeson
6363
, base
64+
, containers
6465
, hspec
6566
, json-pointer
6667
, QuickCheck

‎src/Data/JsonPointer/Aeson.hs‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
module Data.JsonPointer.Aeson where
44

5-
import Data.Aeson (FromJSON (..), ToJSON (..))
5+
import Data.Aeson (FromJSON (..), FromJSONKey (..), FromJSONKeyFunction (..), ToJSON (..), ToJSONKey (..))
66
import Data.Aeson qualified as Aeson
77
import Data.Aeson.Key qualified as KM
88
import Data.Aeson.KeyMap qualified as KM
9-
import Data.Aeson.Types (withText)
9+
import Data.Aeson.Types (toJSONKeyText, withText)
1010
import Data.Maybe
1111
import Data.Semigroup
12-
import Data.Text (unpack)
12+
import Data.Text (pack, unpack)
1313
import Data.Vector qualified as Vector
1414

1515
import Data.JsonPointer.Model
@@ -36,11 +36,18 @@ pointToNullable pointer json = fromMaybe Aeson.Null $ pointTo pointer json
3636
--
3737
-- See `parseJsonPointer` for the details.
3838
instance FromJSON JsonPointer where
39-
parseJSON = withText "JsonPointer" $ \t ->
40-
case parseJsonPointer t of
41-
Left err -> fail $ unpack err
42-
Right x -> pure x
39+
parseJSON = withText "JsonPointer" $ either (fail . unpack) pure . parseJsonPointer
40+
41+
-- | Parse both the plain and the relative URI form.
42+
--
43+
-- See `parseJsonPointer` for the details.
44+
instance FromJSONKey JsonPointer where
45+
fromJSONKey = FromJSONKeyTextParser $ either (fail . unpack) pure . parseJsonPointer
4346

4447
-- | Render the plain form, e.g., @\/foo\/bar@
4548
instance ToJSON JsonPointer where
4649
toJSON p = Aeson.toJSON $ show p
50+
51+
-- | Render the plain form, e.g., @\/foo\/bar@
52+
instance ToJSONKey JsonPointer where
53+
toJSONKey = toJSONKeyText $ pack . show

‎test/Data/JsonPointer/AesonSpec.hs‎

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Data.JsonPointer.AesonSpec (spec) where
33
import Data.Aeson (Result (..), Value (..), fromJSON, object, toJSON, (.=))
44
import Data.JsonPointer
55
import Data.JsonPointer.Gen
6+
import Data.Map.Strict qualified as Map
67
import Test.Hspec
78
import Test.Hspec.QuickCheck
89

@@ -11,7 +12,7 @@ document =
1112
object
1213
[ "foo" .= object ["bar" .= (1 :: Int), "0" .= ("keyed" :: String), "-1" .= ("negative" :: String)]
1314
, "list" .= [object ["x" .= True], Null]
14-
, "empty" .= object ["" .= ("blank" :: String)]
15+
, "" .= ("blank" :: String)
1516
]
1617

1718
spec :: Spec
@@ -27,7 +28,7 @@ spec = do
2728
pointTo (atKey "list" <> atIndex 0 <> atKey "x") document `shouldBe` Just (Bool True)
2829

2930
it "looks up an empty key" $
30-
pointTo (atKey "empty" <> atKey "") document `shouldBe` Just (String "blank")
31+
pointTo (atKey "") document `shouldBe` Just (String "blank")
3132

3233
it "returns a null stored in the document" $
3334
pointTo (atKey "list" <> atIndex 1) document `shouldBe` Just Null
@@ -80,6 +81,19 @@ spec = do
8081
it "escapes the reference tokens" $
8182
toJSON (atKey "a/b") `shouldBe` String "/a~1b"
8283

84+
describe "ToJSONKey" $ do
85+
it "renders the plain form as an object key" $
86+
toJSON (Map.singleton (atKey "foo" <> atIndex 0) (1 :: Int))
87+
`shouldBe` object ["/foo/0" .= (1 :: Int)]
88+
89+
it "renders the empty pointer as the empty key" $
90+
toJSON (Map.singleton (mempty @JsonPointer) (1 :: Int))
91+
`shouldBe` object ["" .= (1 :: Int)]
92+
93+
it "escapes the reference tokens" $
94+
toJSON (Map.singleton (atKey "a/b") (1 :: Int))
95+
`shouldBe` object ["/a~1b" .= (1 :: Int)]
96+
8397
describe "FromJSON" $ do
8498
it "parses the plain form" $
8599
fromJSON (String "/foo/0") `shouldBe` Success (atKey "foo" <> atIndex 0)
@@ -99,6 +113,27 @@ spec = do
99113
prop "round-trips a pointer through ToJSON" $ \(Pointer pointer) ->
100114
fromJSON (toJSON pointer) `shouldBe` Success pointer
101115

116+
describe "FromJSONKey" $ do
117+
it "parses the plain form from an object key" $
118+
fromJSON (object ["/foo/0" .= (1 :: Int)])
119+
`shouldBe` Success (Map.singleton (atKey "foo" <> atIndex 0) (1 :: Int))
120+
121+
it "parses the relative URI form from an object key" $
122+
fromJSON (object ["#/foo/0" .= (1 :: Int)])
123+
`shouldBe` Success (Map.singleton (atKey "foo" <> atIndex 0) (1 :: Int))
124+
125+
it "parses the empty key as the empty pointer" $
126+
fromJSON (object ["" .= (1 :: Int)])
127+
`shouldBe` Success (Map.singleton (mempty @JsonPointer) (1 :: Int))
128+
129+
it "fails on an illegal escape sequence" $
130+
fromJSON @(Map.Map JsonPointer Int) (object ["/a~2b" .= (1 :: Int)])
131+
`shouldSatisfy` isError
132+
133+
prop "round-trips a pointer through ToJSONKey" $ \(Pointer pointer) ->
134+
fromJSON (toJSON (Map.singleton pointer (1 :: Int)))
135+
`shouldBe` Success (Map.singleton pointer (1 :: Int))
136+
102137
isError :: Result a -> Bool
103138
isError = \case
104139
Error _ -> True

0 commit comments

Comments
 (0)