packages feed

hjsonpointer 0.2.0.4 → 0.3.0.0

raw patch · 8 files changed

+282/−173 lines, 8 filesdep +QuickCheckdep +test-framework-quickcheck2dep ~aesondep ~basedep ~http-typesnew-component:exe:examplePVP ok

version bump matches the API change (PVP)

Dependencies added: QuickCheck, test-framework-quickcheck2

Dependency ranges changed: aeson, base, http-types

API changes (from Hackage documentation)

- Data.JsonPointer: ArrayElemNotFound :: ResolutionError
- Data.JsonPointer: ArrayIndexInvalid :: ResolutionError
- Data.JsonPointer: ExpectedObjectOrArray :: ResolutionError
- Data.JsonPointer: InvalidFirstChar :: PointerFormatError
- Data.JsonPointer: JsonPointer :: [Text] -> JsonPointer
- Data.JsonPointer: ObjectLookupFailed :: ResolutionError
- Data.JsonPointer: UnescapedTilde :: PointerFormatError
- Data.JsonPointer: [_unJsonPointer] :: JsonPointer -> [Text]
- Data.JsonPointer: data PointerFormatError
- Data.JsonPointer: data ResolutionError
- Data.JsonPointer: instance GHC.Classes.Eq Data.JsonPointer.JsonPointer
- Data.JsonPointer: instance GHC.Classes.Eq Data.JsonPointer.PointerFormatError
- Data.JsonPointer: instance GHC.Classes.Eq Data.JsonPointer.ResolutionError
- Data.JsonPointer: instance GHC.Show.Show Data.JsonPointer.JsonPointer
- Data.JsonPointer: instance GHC.Show.Show Data.JsonPointer.PointerFormatError
- Data.JsonPointer: instance GHC.Show.Show Data.JsonPointer.ResolutionError
- Data.JsonPointer: jsonPointer :: Text -> Either PointerFormatError JsonPointer
- Data.JsonPointer: newtype JsonPointer
- Data.JsonPointer: resolvePointer :: JsonPointer -> Value -> Either ResolutionError Value
- Data.JsonPointer: resolveRefTok :: JsonPointer -> Value -> Either ResolutionError (JsonPointer, Value)
+ Data.Aeson.Pointer: ArrayElemNotFound :: ResolutionError
+ Data.Aeson.Pointer: ArrayIndexInvalid :: ResolutionError
+ Data.Aeson.Pointer: ExpectedObjectOrArray :: ResolutionError
+ Data.Aeson.Pointer: InvalidFirstChar :: FormatError
+ Data.Aeson.Pointer: ObjectLookupFailed :: ResolutionError
+ Data.Aeson.Pointer: Pointer :: [Token] -> Pointer
+ Data.Aeson.Pointer: Token :: Text -> Token
+ Data.Aeson.Pointer: UnescapedTilde :: FormatError
+ Data.Aeson.Pointer: [_unPointer] :: Pointer -> [Token]
+ Data.Aeson.Pointer: [_unToken] :: Token -> Text
+ Data.Aeson.Pointer: data FormatError
+ Data.Aeson.Pointer: data ResolutionError
+ Data.Aeson.Pointer: escape :: Pointer -> Text
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.FromJSON Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance Data.Aeson.Types.Class.ToJSON Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Base.Monoid Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.FormatError
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.ResolutionError
+ Data.Aeson.Pointer: instance GHC.Classes.Eq Data.Aeson.Pointer.Token
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.FormatError
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.ResolutionError
+ Data.Aeson.Pointer: instance GHC.Show.Show Data.Aeson.Pointer.Token
+ Data.Aeson.Pointer: instance Test.QuickCheck.Arbitrary.Arbitrary Data.Aeson.Pointer.Pointer
+ Data.Aeson.Pointer: instance Test.QuickCheck.Arbitrary.Arbitrary Data.Aeson.Pointer.Token
+ Data.Aeson.Pointer: newtype Pointer
+ Data.Aeson.Pointer: newtype Token
+ Data.Aeson.Pointer: resolve :: Pointer -> Value -> Either ResolutionError Value
+ Data.Aeson.Pointer: resolveToken :: Token -> Value -> Either ResolutionError Value
+ Data.Aeson.Pointer: unescape :: Text -> Either FormatError Pointer
+ Data.Aeson.Pointer: unescapeToken :: Text -> Maybe Token

Files

+ Example.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Control.Monad      (unless)+import           Data.Aeson+import qualified Data.Aeson.Pointer as P++main :: IO ()+main = do+  -- JSON Pointers must either be empty or start with a /.+  pntr1 <- case P.unescape "/foo/0" of+             Left _     -> error "Failed to construct JSON Pointer."+             Right pntr -> return pntr++  -- We can also write JSON Pointers in Haskell.+  let pntr2 = P.Pointer [P.Token "/"]+  -- When we do this we don't have to escape / or ~ characters+  -- (as ~1 and ~0 respectively) like we do in an escaped JSON+  -- Pointer string.+  unless (P.unescape "/~1" == Right pntr2) (error "ohno!")++  print (P.resolve pntr1 document)+  print (P.resolve pntr2 document)++  where+    document :: Value+    document = object [ "foo" .= [String "bar", String "baz"]+                      , "/"   .= String "quux"+                      ]
README.md view
@@ -2,28 +2,45 @@  [JSON Pointer](http://tools.ietf.org/html/rfc6901) library for Haskell. +[Hackage](https://hackage.haskell.org/package/hjsonpointer) / [GitHub](https://github.com/seagreen/hjsonpointer) / [Travis CI](https://travis-ci.org/seagreen/hjsonpointer)+ # Example -```+```haskell {-# LANGUAGE OverloadedStrings #-}  module Main where +import           Control.Monad      (unless) import           Data.Aeson-import qualified Data.HashMap.Strict as H-import           Data.JsonPointer+import qualified Data.Aeson.Pointer as P  main :: IO ()-main =-  case jsonPointer "/foo" of-    Left e     -> print e-    Right pntr ->-      case resolvePointer pntr (Object $ H.singleton "foo" $ String "bar") of-        Left e2 -> print e2-        Right v -> print v+main = do+  -- JSON Pointers must either be empty or start with a /.+  pntr1 <- case P.unescape "/foo/0" of+             Left _     -> error "Failed to construct JSON Pointer."+             Right pntr -> pure pntr++  -- We can also write JSON Pointers in Haskell.+  let pntr2 = P.Pointer [P.Token "/"]+  -- When we do this we don't have to escape / or ~ characters+  -- (as ~1 and ~0 respectively) like we do in an escaped JSON+  -- Pointer string.+  unless (P.unescape "/~1" == Right pntr2) (error "ohno!")++  print (P.resolve pntr1 document)+  print (P.resolve pntr2 document)++  where+    document :: Value+    document = object [ "foo" .= [String "bar", String "baz"]+                      , "/"   .= String "quux"+                      ] ```  Output: ```-String "bar"+Right (String "bar")+Right (String "quux") ```
+ changelog.md view
@@ -0,0 +1,13 @@+# 0.3+++ Rewrite.++# 0.2+++ Fix mistake in `resolveRefTok`.++ Split errors over two types.++ Switch the order of pointer and value arguments for functions that take both. The pointer now comes first.++# 0.1+++ Initial implementation.
− changelog.txt
@@ -1,9 +0,0 @@-# 0.2--Fix mistake in resolveRefTok.-Split errors over two types.-Switch the order of pointer and value arguments for functions that take both. The pointer now comes first.--# 0.1--Initial implementation.
hjsonpointer.cabal view
@@ -1,5 +1,5 @@ name:                   hjsonpointer-version:                0.2.0.4+version:                0.3.0.0 synopsis:               JSON Pointer library homepage:               https://github.com/seagreen/hjsonpointer license:                MIT@@ -9,17 +9,20 @@ category:               Data build-type:             Simple cabal-version:          >=1.10-extra-source-files:     changelog.txt+tested-with:            GHC == 7.8.4, GHC == 7.10.3+extra-source-files:     changelog.md                         README.md  library   hs-source-dirs:       src-  exposed-modules:      Data.JsonPointer+  exposed-modules:      Data.Aeson.Pointer   default-language:     Haskell2010   default-extensions:   OverloadedStrings+  other-extensions:     GeneralizedNewtypeDeriving   ghc-options:          -Wall-  build-depends:        aeson                >= 0.7  && < 0.10+  build-depends:        aeson                >= 0.7  && < 0.12                       , base                 >= 4.6  && < 4.9+                      , QuickCheck           >= 2.8  && < 2.9                       , unordered-containers >= 0.2  && < 0.3                       , text                 >= 1.2  && < 1.3                       , vector               >= 0.10 && < 0.12@@ -37,10 +40,19 @@                       , unordered-containers                       , text                       , vector-                      , http-types           >= 0.8 && < 0.9-                      , HUnit                >= 1.2 && < 1.4-                      , test-framework       >= 0.8 && < 0.9-                      , test-framework-hunit >= 0.3 && < 0.4+                      , http-types                 >= 0.8 && < 0.10+                      , HUnit                      >= 1.2 && < 1.4+                      , test-framework             >= 0.8 && < 0.9+                      , test-framework-hunit       >= 0.3 && < 0.4+                      , test-framework-quickcheck2 >= 0.3 && < 0.4++executable example+  main-is:              Example.hs+  default-language:     Haskell2010+  ghc-options:          -Wall+  build-depends:        aeson+                      , base+                      , hjsonpointer  source-repository head   type:               git
+ src/Data/Aeson/Pointer.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module Data.Aeson.Pointer where++import           Control.Monad       (when)+import           Data.Aeson+import qualified Data.HashMap.Strict as H+import           Data.Text           (Text)+import qualified Data.Text           as T+import qualified Data.Vector         as V+import           Test.QuickCheck+import           Text.Read           (readMaybe)++-- For GHCs before 7.10:+import           Control.Applicative+import           Data.Monoid+import           Data.Traversable++newtype Pointer+  = Pointer { _unPointer :: [Token] }+  deriving (Eq, Show, Monoid, Arbitrary)++instance FromJSON Pointer where+  parseJSON = withText "JSON Pointer" $ \t ->+    case unescape t of+      Left e  -> fail (show e)+      Right p -> pure p++instance ToJSON Pointer where+  toJSON = String . escape++-- | We don't try to distinguish between integer tokens and string+-- tokens since all tokens start as strings, and all tokens can+-- be used to resolve JSON objects.+newtype Token = Token { _unToken :: Text } deriving (Eq, Show)++instance Arbitrary Token where+  arbitrary = Token . T.pack <$> arbitrary++escape :: Pointer -> Text+escape (Pointer []) = ""+escape (Pointer ts) =+    T.cons '/'+  . T.intercalate "/"+  . fmap (T.replace "/" "~1" . T.replace "~" "~0" . _unToken)+  $ ts++-- * Unescaping++data FormatError+  = InvalidFirstChar+  -- ^ JSON Pointers must either be empty or start with a @/@.+  | UnescapedTilde+  deriving (Eq, Show)+++-- | JSON Pointers must either be empty or start with a @/@. This means+-- that if you're turning a URI Fragment into a JSON Pointer you must+-- drop the initial @#@.+--+-- Note that the unescaping happening here is not the same as URI+-- decoding. If you are turning a URI fragment into a JSON Pointer you+-- must URI decode the 'Text' before using it as an argument to this+-- function. There's an example of how to do this in the tests using+-- "Network.HTTP.Types.URI.urlDecode" from http-types.+unescape :: Text -> Either FormatError Pointer+unescape txt =+  case T.splitOn "/" txt of+    []    -> Right (Pointer [])+    "":xs -> Pointer <$> traverse f xs+    _     -> Left InvalidFirstChar+  where+    f :: Text -> Either FormatError Token+    f t = case unescapeToken t of+            Nothing  -> Left UnescapedTilde+            Just tok -> Right tok++-- | For internal use by 'unescape'.+unescapeToken :: Text -> Maybe Token+unescapeToken t+  | not (isValid t) = Nothing+  | otherwise       = Just . Token . replace $ t+  where+    -- All tildes must be followed by 0s or 1s.+    isValid :: Text -> Bool+    isValid x = all (\y -> T.isPrefixOf "0" y || T.isPrefixOf "1" y) afterTildes+      where+        afterTildes :: [Text]+        afterTildes = drop 1 $ T.splitOn "~" x++    replace :: Text -> Text+    replace = T.replace "~0" "~" . T.replace "~1" "/"++-- * Resolution++data ResolutionError+  = ObjectLookupFailed+  | ArrayIndexInvalid+  | ArrayElemNotFound+  | ExpectedObjectOrArray+  deriving (Eq, Show)++resolve :: Pointer -> Value -> Either ResolutionError Value+resolve (Pointer []) v     = Right v+resolve (Pointer (t:ts)) v = resolveToken t v >>= resolve (Pointer ts)++-- | For internal use (or specialized applications that don't want to+-- resolve an entire pointer at once).+resolveToken :: Token -> Value -> Either ResolutionError Value+resolveToken tok (Array vs) =+  case readMaybe . T.unpack . _unToken $ tok of+    Nothing -> Left ArrayIndexInvalid+    Just n  -> do+      when (n < 0) (Left ArrayIndexInvalid)+      case vs V.!? n of+        Nothing  -> Left ArrayElemNotFound+        Just res -> Right res+resolveToken tok (Object h) =+  case H.lookup (_unToken tok) h of+    Nothing  -> Left ObjectLookupFailed+    Just res -> Right res+resolveToken _ _ = Left ExpectedObjectOrArray
− src/Data/JsonPointer.hs
@@ -1,79 +0,0 @@-module Data.JsonPointer where--import           Control.Applicative-import           Control.Monad-import           Data.Aeson-import qualified Data.HashMap.Strict as H-import           Data.Text           (Text)-import qualified Data.Text           as T-import qualified Data.Vector         as V-import           Text.Read           (readMaybe)--newtype JsonPointer = JsonPointer { _unJsonPointer :: [Text] } deriving (Eq, Show)--data PointerFormatError-  -- | The Text to build a JSON Pointer must either be empty-  -- or start with a "/".-  = InvalidFirstChar-  | UnescapedTilde-  deriving (Eq, Show)--data ResolutionError-  = ObjectLookupFailed-  | ArrayIndexInvalid-  | ArrayElemNotFound-  | ExpectedObjectOrArray-  deriving (Eq, Show)---- | The Text to build a JSON Pointer must either be empty or start--- with a "/". If you're turning a URI Fragment into a JSON Pointer--- you must drop the initial "#".-jsonPointer :: Text -> Either PointerFormatError JsonPointer-jsonPointer t =-  JsonPointer <$> (unescape =<< process (T.splitOn "/" t))-  where-    process ::[Text] -> Either PointerFormatError [Text]-    process []     = Right []-    process (x:xs)-      -- This checks that the JsonPointer started with a "/":-      | (not . T.null $ x) = Left InvalidFirstChar-      | otherwise          = Right xs--    unescape :: [Text] -> Either PointerFormatError [Text]-    unescape xs = do-      void $ mapM checkValid xs-      Right $ T.replace "~0" "~" . T.replace "~1" "/" <$> xs--    checkValid :: Text -> Either PointerFormatError ()-    checkValid x = do-      let afterTildes = drop 1 $ T.splitOn "~" x-      if all (\y -> T.isPrefixOf "0" y || T.isPrefixOf "1" y) afterTildes-        then Right ()-        else Left UnescapedTilde--resolvePointer :: JsonPointer -> Value -> Either ResolutionError Value-resolvePointer p v =-  case _unJsonPointer p of-    [] -> Right v-    _  -> resolveRefTok p v >>= uncurry resolvePointer---- | For internal use and specialized applications that don't want to--- resolve the entire pointer at once.-resolveRefTok :: JsonPointer -> Value -> Either ResolutionError (JsonPointer, Value)-resolveRefTok p v = do-  case _unJsonPointer p of-    []       -> Right (p, v)-    (tok:ps) ->-      case v of-        Object h ->-          case H.lookup tok h of-            Nothing -> Left ObjectLookupFailed-            Just vv -> Right (JsonPointer ps, vv)-        Array vs -> do-          case readMaybe (T.unpack tok) of-            Nothing -> Left ArrayIndexInvalid-            Just n  -> do-              when (n < 0)            $ Left ArrayIndexInvalid-              when (n >= V.length vs) $ Left ArrayElemNotFound-              Right (JsonPointer ps, vs V.! n)-        _ -> Left ExpectedObjectOrArray
tests/Unit.hs view
@@ -1,87 +1,90 @@+ module Main where -import           Control.Applicative import           Control.Arrow-import           Control.Monad import           Data.Aeson-import qualified Data.HashMap.Strict            as H-import           Data.JsonPointer-import           Data.Monoid-import           Data.Text                      (Text)-import qualified Data.Text                      as T+import qualified Data.Aeson.Pointer                   as P+import           Data.Text                            (Text)+import qualified Data.Text                            as T import           Data.Text.Encoding-import qualified Data.Vector                    as V-import           Network.HTTP.Types.URI-import           Test.Framework                 (defaultMain, testGroup)-import           Test.Framework.Providers.HUnit (testCase)-import           Test.HUnit                     hiding (Test)+import qualified Data.Vector                          as V+import           Network.HTTP.Types.URI               (urlDecode)+import           Test.Framework                       (defaultMain, testGroup)+import           Test.Framework.Providers.HUnit       (testCase)+import           Test.Framework.Providers.QuickCheck2 (testProperty)+import           Test.HUnit                           hiding (Test) +-- For GHCs before 7.10:+import           Control.Applicative+import           Data.Foldable+import           Data.Monoid  main :: IO () main = defaultMain   [ testGroup "unit"-    [ testCase "Can be represented in a JSON string value" jsonString+    [ testProperty "Storing a Pointer as JSON doesn't change its value" roundtrip+    , testCase "Can be represented in a JSON string value" jsonString     , testCase "Can be represented in a URI fragment identifier" uriFragment     ]   ] -specExample :: Value-specExample =-  Object $ H.fromList-    [ ("foo" , Array $ V.fromList ["bar", "baz"])-    , (""    , Number 0)-    , ("a/b" , Number 1)-    , ("c%d" , Number 2)-    , ("e^f" , Number 3)-    , ("g|h" , Number 4)-    , ("i\\j", Number 5)-    , ("k\"l", Number 6)-    , (" "   , Number 7)-    , ("m~n" , Number 8)-    ]--resolvesTo :: (Text, Value) -> Assertion-resolvesTo (t, expected) =-  case jsonPointer t of-    Left e  -> assertFailure (show e <> " error for pointer: " <> show t)-    Right p ->-      assertEqual ("Resolved value for pointer: " <> show t)-        (Right expected)-        $ resolvePointer p specExample+roundtrip :: P.Pointer -> Bool+roundtrip a = Just a == decode (encode a)  jsonString :: Assertion-jsonString =-  void $ mapM resolvesTo-    [ (""      , specExample)-    , ("/foo"  , Array $ V.fromList ["bar", "baz"])-    , ("/foo/0", String "bar")-    , ("/"     , Number 0)-    , ("/a~1b" , Number 1)-    , ("/c%d"  , Number 2)-    , ("/e^f"  , Number 3)-    , ("/g|h"  , Number 4)-    , ("/i\\j" , Number 5)-    , ("/k\"l" , Number 6)-    , ("/ "    , Number 7)-    , ("/m~0n" , Number 8)-    ]+jsonString = traverse_ resolvesTo+  [ (""      , specExample)+  , ("/foo"  , Array $ V.fromList ["bar", "baz"])+  , ("/foo/0", String "bar")+  , ("/"     , Number 0)+  , ("/a~1b" , Number 1)+  , ("/c%d"  , Number 2)+  , ("/e^f"  , Number 3)+  , ("/g|h"  , Number 4)+  , ("/i\\j" , Number 5)+  , ("/k\"l" , Number 6)+  , ("/ "    , Number 7)+  , ("/m~0n" , Number 8)+  ]  uriFragment :: Assertion-uriFragment =-  void $ mapM resolvesTo $ first decodeFragment <$>-    [ ("#"      , specExample)-    , ("#/foo"  , Array $ V.fromList ["bar", "baz"])-    , ("#/foo/0", String "bar")-    , ("#/"     , Number 0)-    , ("#/a~1b" , Number 1)-    , ("#/c%25d", Number 2)-    , ("#/e%5Ef", Number 3)-    , ("#/g%7Ch", Number 4)-    , ("#/i%5Cj", Number 5)-    , ("#/k%22l", Number 6)-    , ("#/%20"  , Number 7)-    , ("#/m~0n" , Number 8)-    ]+uriFragment = traverse_ resolvesTo . fmap (first decodeFragment) $+  [ ("#"      , specExample)+  , ("#/foo"  , Array $ V.fromList ["bar", "baz"])+  , ("#/foo/0", String "bar")+  , ("#/"     , Number 0)+  , ("#/a~1b" , Number 1)+  , ("#/c%25d", Number 2)+  , ("#/e%5Ef", Number 3)+  , ("#/g%7Ch", Number 4)+  , ("#/i%5Cj", Number 5)+  , ("#/k%22l", Number 6)+  , ("#/%20"  , Number 7)+  , ("#/m~0n" , Number 8)+  ]   where     decodeFragment :: Text -> Text     decodeFragment = T.drop 1 . decodeUtf8 . urlDecode True . encodeUtf8++resolvesTo :: (Text, Value) -> Assertion+resolvesTo (t, expected) =+  case P.unescape t of+    Left e  -> assertFailure (show e <> " error for pointer: " <> show t)+    Right p -> assertEqual+                 ("Resolved value for pointer: " <> show t)+                 (Right expected)+                 (P.resolve p specExample)++specExample :: Value+specExample = object+  [ "foo"  .= (["bar", "baz"] :: [Text])+  , ""     .= (0 :: Int)+  , "a/b"  .= (1 :: Int)+  , "c%d"  .= (2 :: Int)+  , "e^f"  .= (3 :: Int)+  , "g|h"  .= (4 :: Int)+  , "i\\j" .= (5 :: Int)+  , "k\"l" .= (6 :: Int)+  , " "    .= (7 :: Int)+  , "m~n"  .= (8 :: Int)+  ]