packages feed

hjsonpointer 1.0.0.2 → 1.1.0.0

raw patch · 7 files changed

+217/−160 lines, 7 filesdep +hashabledep +semigroupsdep ~aesonPVP ok

version bump matches the API change (PVP)

Dependencies added: hashable, semigroups

Dependency ranges changed: aeson

API changes (from Hackage documentation)

- 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.FromJSON.FromJSON Data.Aeson.Pointer.Pointer
- Data.Aeson.Pointer: instance Data.Aeson.Types.ToJSON.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
+ JSONPointer: ArrayElemNotFound :: ResolutionError
+ JSONPointer: ArrayIndexInvalid :: ResolutionError
+ JSONPointer: ExpectedObjectOrArray :: ResolutionError
+ JSONPointer: Index :: Int -> Index
+ JSONPointer: InvalidFirstChar :: FormatError
+ JSONPointer: Key :: Text -> Key
+ JSONPointer: ObjectLookupFailed :: ResolutionError
+ JSONPointer: Pointer :: [Token] -> Pointer
+ JSONPointer: Token :: Text -> Token
+ JSONPointer: UnescapedTilde :: FormatError
+ JSONPointer: [_unIndex] :: Index -> Int
+ JSONPointer: [_unKey] :: Key -> Text
+ JSONPointer: [_unPointer] :: Pointer -> [Token]
+ JSONPointer: [_unToken] :: Token -> Text
+ JSONPointer: data FormatError
+ JSONPointer: data ResolutionError
+ JSONPointer: escape :: Pointer -> Text
+ JSONPointer: instance Data.Aeson.Types.FromJSON.FromJSON JSONPointer.Pointer
+ JSONPointer: instance Data.Aeson.Types.ToJSON.ToJSON JSONPointer.Pointer
+ JSONPointer: instance Data.Hashable.Class.Hashable JSONPointer.Index
+ JSONPointer: instance Data.Hashable.Class.Hashable JSONPointer.Key
+ JSONPointer: instance Data.Hashable.Class.Hashable JSONPointer.Pointer
+ JSONPointer: instance Data.Hashable.Class.Hashable JSONPointer.Token
+ JSONPointer: instance Data.Semigroup.Semigroup JSONPointer.Pointer
+ JSONPointer: instance GHC.Base.Monoid JSONPointer.Pointer
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.FormatError
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.Index
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.Key
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.Pointer
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.ResolutionError
+ JSONPointer: instance GHC.Classes.Eq JSONPointer.Token
+ JSONPointer: instance GHC.Generics.Generic JSONPointer.Index
+ JSONPointer: instance GHC.Generics.Generic JSONPointer.Key
+ JSONPointer: instance GHC.Generics.Generic JSONPointer.Pointer
+ JSONPointer: instance GHC.Generics.Generic JSONPointer.Token
+ JSONPointer: instance GHC.Show.Show JSONPointer.FormatError
+ JSONPointer: instance GHC.Show.Show JSONPointer.Index
+ JSONPointer: instance GHC.Show.Show JSONPointer.Key
+ JSONPointer: instance GHC.Show.Show JSONPointer.Pointer
+ JSONPointer: instance GHC.Show.Show JSONPointer.ResolutionError
+ JSONPointer: instance GHC.Show.Show JSONPointer.Token
+ JSONPointer: instance Test.QuickCheck.Arbitrary.Arbitrary JSONPointer.Pointer
+ JSONPointer: instance Test.QuickCheck.Arbitrary.Arbitrary JSONPointer.Token
+ JSONPointer: newtype Index
+ JSONPointer: newtype Key
+ JSONPointer: newtype Pointer
+ JSONPointer: newtype Token
+ JSONPointer: resolve :: Pointer -> Value -> Either ResolutionError Value
+ JSONPointer: resolveToken :: Token -> Value -> Either ResolutionError Value
+ JSONPointer: unescape :: Text -> Either FormatError Pointer
+ JSONPointer: unescapeToken :: Text -> Maybe Token

Files

Example.hs view
@@ -1,27 +1,26 @@-{-# LANGUAGE OverloadedStrings #-}  module Example where -import           Control.Monad      (unless)+import           Control.Monad (unless) import           Data.Aeson-import qualified Data.Aeson.Pointer as P+import qualified JSONPointer   as JP  main :: IO () main = do     -- JSON Pointers must either be empty or start with a /.-    pntr1 <- case P.unescape "/foo/0" of+    pntr1 <- case JP.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 "/"]+    let pntr2 = JP.Pointer [JP.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!")+    unless (JP.unescape "/~1" == Right pntr2) (error "ohno!") -    print (P.resolve pntr1 document)-    print (P.resolve pntr2 document)+    print (JP.resolve pntr1 document)+    print (JP.resolve pntr2 document)    where     document :: Value
README.md view
@@ -7,30 +7,28 @@ # Example  ```haskell-{-# LANGUAGE OverloadedStrings #-}- module Example where -import           Control.Monad      (unless)+import           Control.Monad (unless) import           Data.Aeson-import qualified Data.Aeson.Pointer as P+import qualified JSONPointer   as JP  main :: IO () main = do     -- JSON Pointers must either be empty or start with a /.-    pntr1 <- case P.unescape "/foo/0" of+    pntr1 <- case JP.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 "/"]+    let pntr2 = JP.Pointer [JP.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!")+    unless (JP.unescape "/~1" == Right pntr2) (error "ohno!") -    print (P.resolve pntr1 document)-    print (P.resolve pntr2 document)+    print (JP.resolve pntr1 document)+    print (JP.resolve pntr2 document)    where     document :: Value
changelog.md view
@@ -1,3 +1,10 @@+# 1.1.0.0+++ Rename the module to `JSONPointer`.++ Bump aeson.++ Derive `Semigroup` for `Pointer`.++ Reorganize code.+ # 1.0.0.2  + Bump hspec.
hjsonpointer.cabal view
@@ -1,5 +1,5 @@ name:               hjsonpointer-version:            1.0.0.2+version:            1.1.0.0 synopsis:           JSON Pointer library homepage:           https://github.com/seagreen/hjsonpointer license:            MIT@@ -17,19 +17,18 @@   hs-source-dirs:     src   default-language: Haskell2010-  default-extensions:-    OverloadedStrings-  other-extensions:-    GeneralizedNewtypeDeriving   ghc-options:     -Wall   exposed-modules:-    Data.Aeson.Pointer+    JSONPointer   build-depends:-      aeson                >= 0.7  && < 1.1-    , base                 >= 4.6  && < 4.10+      base                 >= 4.6  && < 4.10+    , aeson                >= 0.7  && < 1.2+    , hashable             >= 1.2  && < 1.3     , QuickCheck           >= 2.8  && < 2.10     , unordered-containers >= 0.2  && < 0.3+    , semigroups           >= 0.18 && < 1.0+      -- ^ for GHCs < 8     , text                 >= 1.2  && < 1.3     , vector               >= 0.10 && < 0.12 @@ -49,12 +48,12 @@       aeson     , base     , hjsonpointer+    , hspec                >= 2.2 && < 2.4+    , http-types           >= 0.8 && < 0.10     , QuickCheck     , unordered-containers     , text     , vector-    , hspec                >= 2.2 && < 2.4-    , http-types           >= 0.8 && < 0.10  source-repository head   type: git
− src/Data/Aeson/Pointer.hs
@@ -1,124 +0,0 @@-{-# 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/JSONPointer.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE DeriveGeneric              #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}++module JSONPointer where++import           Control.Monad       (when)+import           Data.Aeson+import qualified Data.Hashable       as HA+import qualified Data.HashMap.Strict as HM+import           Data.Semigroup      (Semigroup)+import           Data.Text           (Text)+import qualified Data.Text           as T+import qualified Data.Vector         as V+import           GHC.Generics        (Generic)+import           Test.QuickCheck+import           Text.Read           (readMaybe)++-- For GHCs before 7.10:+import           Control.Applicative+import           Data.Monoid+import           Data.Traversable++--------------------------------------------------+-- * 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)++--------------------------------------------------+-- * Main types and escaping+--------------------------------------------------++newtype Pointer+    = Pointer { _unPointer :: [Token] }+    deriving (Eq, Show, Semigroup, Monoid, Generic, Arbitrary)++instance HA.Hashable Pointer++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.+--+-- Since these are unescaped you can write @"/"@ and @"~"@ normally.+-- (e.g. if you're referencing a key such as @"abc/123"@, go ahead+-- and write that exactly.+newtype Token+    = Token { _unToken :: Text }+    deriving (Eq, Show, Generic)++instance HA.Hashable Token++instance Arbitrary Token where+    arbitrary = Token . T.pack <$> arbitrary++-- | This escapes @"/"@ (because it's the token separator character).+--+-- It also escapes @"~"@ (because it's the escape character).+escape :: Pointer -> Text+escape (Pointer []) = ""+escape (Pointer ts) =+      T.cons '/'+    . T.intercalate "/"+    . fmap (T.replace "/" "~1" . T.replace "~" "~0" . _unToken)+    $ ts++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++--------------------------------------------------+-- * Wrapper Types+--+-- These aren't used by the rest of the library+-- (as explained in the docs for 'Token').+--+-- However, they might be useful if you need to distinguish JSON Pointer+-- tokens from plain 'Text' or 'Int' without losing information by+-- converting to 'Token'.+--------------------------------------------------++-- | A glorified @type@ alias. If you need to do JSON Pointer operations+-- you're looking for 'Token' instead.+--+-- NOTE: Unlike 'Token' this is escaped.+newtype Key+    = Key { _unKey :: Text }+    deriving (Eq, Show, Generic)++instance HA.Hashable Key++-- | A glorified @type@ alias. If you need to do JSON Pointer operations+-- you're looking for 'Token' instead.+newtype Index+    = Index { _unIndex :: Int }+    deriving (Eq, Show, Generic)++instance HA.Hashable Index++--------------------------------------------------+-- * Internals+--------------------------------------------------++-- | 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" "/"++-- | For internal use (by 'resolve').+--+-- Might also be useful for 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 HM.lookup (_unToken tok) h of+        Nothing  -> Left ObjectLookupFailed+        Just res -> Right res+resolveToken _ _ = Left ExpectedObjectOrArray
test/Unit.hs view
@@ -3,15 +3,15 @@  import           Control.Arrow import           Data.Aeson-import qualified Data.Aeson.Pointer      as P-import           Data.Text               (Text)-import qualified Data.Text               as T+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  (urlDecode)+import qualified Data.Vector            as V+import qualified JSONPointer            as JP+import           Network.HTTP.Types.URI (urlDecode)  import           Test.Hspec-import           Test.QuickCheck         (property)+import           Test.QuickCheck        (property)  -- For GHCs before 7.10: import           Control.Applicative@@ -29,7 +29,7 @@         it "can be represented in a JSON string value" jsonString         it "can be represented in a URI fragment identifier" uriFragment -roundtrip :: P.Pointer -> Bool+roundtrip :: JP.Pointer -> Bool roundtrip a = Just a == decode (encode a)  jsonString :: Expectation@@ -69,9 +69,9 @@  resolvesTo :: (Text, Value) -> Expectation resolvesTo (t, expected) =-    case P.unescape t of+    case JP.unescape t of         Left e  -> expectationFailure (show e <> " error for pointer: " <> show t)-        Right p -> P.resolve p specExample `shouldBe` Right expected+        Right p -> JP.resolve p specExample `shouldBe` Right expected  specExample :: Value specExample = object